From 20f7a21e4ebab172e709b167df0b688adfc5cdad Mon Sep 17 00:00:00 2001 From: Igor Opaniuk Date: Sun, 10 May 2026 17:50:29 +0200 Subject: [PATCH 1/5] qdl: add --backend selector and QUD backend skeleton Introduce QDL_DEVICE_QUD as a third device-transport flavour alongside the libusb-based USB backend and the SIM backend used by --dry-run. After this patch --backend=qud parses successfully but errors out at init time on every host. The actual transport is added in a follow-up. The intent is to support kernel-mode drivers that expose the EDL device as a character file (e.g. the official QDLoader 9008 driver on Windows, where the device shows up as \\.\COMx), so libusb is not required on hosts where claiming the USB interface from user space is inconvenient. Signed-off-by: Igor Opaniuk --- io.c | 3 +++ meson.build | 3 ++- qdl.c | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++-- qdl.h | 2 ++ qud.c | 23 +++++++++++++++++++++++ 5 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 qud.c diff --git a/io.c b/io.c index 131c6209..7a3f4553 100644 --- a/io.c +++ b/io.c @@ -14,6 +14,9 @@ struct qdl_device *qdl_init(enum QDL_DEVICE_TYPE type) if (type == QDL_DEVICE_SIM) return sim_init(); + if (type == QDL_DEVICE_QUD) + return qud_init(); + return NULL; } diff --git a/meson.build b/meson.build index 8864ee7d..d0e4f81e 100644 --- a/meson.build +++ b/meson.build @@ -55,6 +55,7 @@ shared_lib = static_library('qdl_common', common_sources, dependencies: common_d # --- qdl executable --- qdl_sources = files( + 'qud.c', 'firehose.c', 'io.c', 'qdl.c', 'patch.c', 'program.c', 'read.c', 'sha2.c', 'sim.c', 'ufs.c', 'usb.c', @@ -72,7 +73,7 @@ qdl_exe = executable('qdl', # --- qdl-ramdump executable --- ramdump_sources = files( - 'ramdump.c', 'io.c', 'sha2.c', 'sim.c', 'usb.c' + 'ramdump.c', 'qud.c', 'io.c', 'sha2.c', 'sim.c', 'usb.c' ) qdl_ramdump_exe = executable('qdl-ramdump', diff --git a/qdl.c b/qdl.c index 384dd82a..a888d97e 100644 --- a/qdl.c +++ b/qdl.c @@ -105,6 +105,31 @@ static int detect_type(const char *verb) return type; } +/* + * Parse a --backend= value into an enum. "auto" picks the best available real + * backend; today that is QDL_DEVICE_USB (libusb / WinUSB on Windows). The + * "qud" backend covers official kernel drivers that expose the device as a + * character device (e.g. /dev/ttyUSB* on Linux, \\.\COMx on Windows) and is + * recognised here for forward compatibility but not yet wired to qdl_init(). + * + * QDL_DEVICE_SIM is intentionally not selectable via --backend; --dry-run / + * --create-digests pick it implicitly. + */ +static int decode_backend(const char *name, enum QDL_DEVICE_TYPE *out) +{ + if (!name || !strcmp(name, "auto") || !strcmp(name, "usb")) { + *out = QDL_DEVICE_USB; + return 0; + } + + if (!strcmp(name, "qud")) { + *out = QDL_DEVICE_QUD; + return 0; + } + + return -1; +} + #define CPIO_MAGIC "070701" struct cpio_newc_header { char c_magic[6]; /* "070701" */ @@ -453,6 +478,7 @@ static void print_usage(FILE *out) fprintf(out, " -T, --slot=T\t\t\tSet slot number T for multiple storage devices\n"); fprintf(out, " -D, --vip-table-path=T\t\tUse digest tables in the T folder for VIP\n"); fprintf(out, " -R, --skip-reset\t\tDo not send the reset command after flashing completes\n"); + fprintf(out, " --backend=B\t\tSelect device backend B: (default: auto)\n"); fprintf(out, " -h, --help\t\t\tPrint this usage info\n"); fprintf(out, " \t\txml file containing or directives\n"); fprintf(out, " \t\txml file containing directives\n"); @@ -493,12 +519,18 @@ static int qdl_list(FILE *out) return 0; } +/* Long-only option ids, distinct from any short option character. */ +enum { + OPT_BACKEND = 0x100, +}; + static int qdl_ramdump(int argc, char **argv) { struct qdl_device *qdl; char *ramdump_path = "."; char *filter = NULL; char *serial = NULL; + enum QDL_DEVICE_TYPE qdl_dev_type = QDL_DEVICE_USB; int ret = 0; int opt; @@ -507,6 +539,7 @@ static int qdl_ramdump(int argc, char **argv) {"version", no_argument, 0, 'v'}, {"output", required_argument, 0, 'o'}, {"serial", required_argument, 0, 'S'}, + {"backend", required_argument, 0, OPT_BACKEND}, {"help", no_argument, 0, 'h'}, {0, 0, 0, 0} }; @@ -525,6 +558,10 @@ static int qdl_ramdump(int argc, char **argv) case 'S': serial = optarg; break; + case OPT_BACKEND: + if (decode_backend(optarg, &qdl_dev_type) < 0) + errx(1, "unknown backend \"%s\" (expected auto|usb|qud)", optarg); + break; case 'h': print_usage(stdout); return 0; @@ -544,9 +581,11 @@ static int qdl_ramdump(int argc, char **argv) ux_init(); - qdl = qdl_init(QDL_DEVICE_USB); - if (!qdl) + qdl = qdl_init(qdl_dev_type); + if (!qdl) { + ux_err("backend not available\n"); return 1; + } if (qdl_debug) print_version(); @@ -789,6 +828,7 @@ static int qdl_flash(int argc, char **argv) {"create-digests", required_argument, 0, 't'}, {"slot", required_argument, 0, 'T'}, {"skip-reset", no_argument, 0, 'R'}, + {"backend", required_argument, 0, OPT_BACKEND}, {"help", no_argument, 0, 'h'}, {0, 0, 0, 0} }; @@ -841,6 +881,15 @@ static int qdl_flash(int argc, char **argv) case 'R': skip_reset = true; break; + case OPT_BACKEND: + /* + * --dry-run / --create-digests already pinned the backend to + * QDL_DEVICE_SIM; honour that and ignore --backend in that case. + */ + if (qdl_dev_type != QDL_DEVICE_SIM && + decode_backend(optarg, &qdl_dev_type) < 0) + errx(1, "unknown backend \"%s\" (expected auto|usb|qud)", optarg); + break; case 'h': print_usage(stdout); return 0; diff --git a/qdl.h b/qdl.h index 863bff2e..8f58ee06 100644 --- a/qdl.h +++ b/qdl.h @@ -47,6 +47,7 @@ enum QDL_DEVICE_TYPE { QDL_DEVICE_USB, QDL_DEVICE_SIM, + QDL_DEVICE_QUD, }; enum qdl_storage_type { @@ -98,6 +99,7 @@ int qdl_vip_transfer_enable(struct qdl_device *qdl, const char *vip_table_path); struct qdl_device *usb_init(void); struct qdl_device *sim_init(void); +struct qdl_device *qud_init(void); struct qdl_device_desc { int vid; diff --git a/qud.c b/qud.c new file mode 100644 index 00000000..8ac49553 --- /dev/null +++ b/qud.c @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * Copyright (c) 2026, Qualcomm Innovation Center, Inc. All rights reserved. + * + * QUD ("Qualcomm USB Driver") backend skeleton. + * + * This is a transport that talks to a Qualcomm EDL device through a + * kernel-mode driver presenting it as a character file (e.g. \\.\COMx + * on Windows via the official QDLoader 9008 driver), rather than via + * libusb. No platform implementation lives here yet -- qud_init() returns + * NULL on every host, signalling that --backend=qud is recognised but not + * usable until a follow-up patch wires up an actual transport. + */ + +#include + +#include "qdl.h" + +struct qdl_device *qud_init(void) +{ + ux_err("qud backend is not supported on this platform\n"); + return NULL; +} From 8642e9296f7e5d1f99aec6a0c34a68d4b6c2569e Mon Sep 17 00:00:00 2001 From: Igor Opaniuk Date: Sun, 10 May 2026 17:52:39 +0200 Subject: [PATCH 2/5] qud: add Windows backend for the QDLoader 9008 driver Wire up an actual transport for the QUD backend on Windows. The official QDLoader 9008 driver exposes a Qualcomm EDL device as a serial port (\\.\COMx). The Sahara/Firehose framing on top is identical to the libusb backend; this patch only deals with how user space reaches the underlying bulk pipes. Non-Windows hosts keep the stub from the previous patch. Signed-off-by: Igor Opaniuk --- meson.build | 9 +- qdl.c | 26 ++- qdl.h | 14 ++ qud.c | 541 +++++++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 571 insertions(+), 19 deletions(-) diff --git a/meson.build b/meson.build index d0e4f81e..6bd166e8 100644 --- a/meson.build +++ b/meson.build @@ -22,11 +22,18 @@ inc = include_directories('.') # Windows-specific link flags ws2_dep = [] +setupapi_dep = [] if host_machine.system() == 'windows' ws2_dep = meson.get_compiler('c').find_library('ws2_32', required : false) + # SetupAPI / cfgmgr32 are needed by the QUD backend to enumerate Qualcomm + # COM ports exposed by the official QDLoader 9008 driver. + setupapi_dep = [ + meson.get_compiler('c').find_library('setupapi', required : false), + meson.get_compiler('c').find_library('cfgmgr32', required : false), + ] endif -common_dep = [libusb_dep, libxml_dep, libzip_dep, ws2_dep] +common_dep = [libusb_dep, libxml_dep, libzip_dep, ws2_dep, setupapi_dep] # Compile-only view of common_dep (includes + cflags, no link args). # Used on executables so their own sources see the headers, while link diff --git a/qdl.c b/qdl.c index a888d97e..3ef2a91b 100644 --- a/qdl.c +++ b/qdl.c @@ -498,23 +498,31 @@ static void print_usage(FILE *out) static int qdl_list(FILE *out) { - struct qdl_device_desc *devices; - unsigned int count; + struct qdl_device_desc *usb_devices; + struct qud_device_desc *qud_devices; + unsigned int usb_count = 0; + unsigned int qud_count = 0; unsigned int i; - devices = usb_list(&count); - if (!devices) - return 1; + usb_devices = usb_list(&usb_count); + qud_devices = qud_list(&qud_count); - if (count == 0) { + if (usb_count == 0 && qud_count == 0) { fprintf(out, "No devices found\n"); } else { - for (i = 0; i < count; i++) + for (i = 0; i < usb_count; i++) fprintf(out, "%04x:%04x\t%s\n", - devices[i].vid, devices[i].pid, devices[i].serial); + usb_devices[i].vid, usb_devices[i].pid, + usb_devices[i].serial); + for (i = 0; i < qud_count; i++) + fprintf(out, "05c6:%04x\t%s\t%s\n", + qud_devices[i].pid, + qud_devices[i].serial, + qud_devices[i].path); } - free(devices); + free(usb_devices); + free(qud_devices); return 0; } diff --git a/qdl.h b/qdl.h index 8f58ee06..ed5c4614 100644 --- a/qdl.h +++ b/qdl.h @@ -109,6 +109,20 @@ struct qdl_device_desc { struct qdl_device_desc *usb_list(unsigned int *devices_found); +/* + * QUD-side counterpart to qdl_device_desc. Serial here is the iSerial as + * Windows stored it in the device-instance ID (no fixed length guarantee + * across OEMs), and path is the kernel-driver-exposed handle the QUD + * backend will open (e.g. "\\\\.\\COM5"). + */ +struct qud_device_desc { + unsigned int pid; + char serial[64]; + char path[64]; +}; + +struct qud_device_desc *qud_list(unsigned int *devices_found); + int firehose_run(struct qdl_device *qdl, struct list_head *ops); int firehose_provision(struct qdl_device *qdl, bool skip_reset); int firehose_read_buf(struct qdl_device *qdl, struct firehose_op *read_op, void *out_buf, size_t out_size); diff --git a/qud.c b/qud.c index 8ac49553..fdb53fae 100644 --- a/qud.c +++ b/qud.c @@ -1,23 +1,546 @@ // SPDX-License-Identifier: BSD-3-Clause /* - * Copyright (c) 2026, Qualcomm Innovation Center, Inc. All rights reserved. + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. * - * QUD ("Qualcomm USB Driver") backend skeleton. - * - * This is a transport that talks to a Qualcomm EDL device through a - * kernel-mode driver presenting it as a character file (e.g. \\.\COMx - * on Windows via the official QDLoader 9008 driver), rather than via - * libusb. No platform implementation lives here yet -- qud_init() returns - * NULL on every host, signalling that --backend=qud is recognised but not - * usable until a follow-up patch wires up an actual transport. + * QUD ("Qualcomm USB Driver") backend: talks to a Qualcomm EDL device + * through a kernel-mode driver that exposes the device as a character + * file, rather than via libusb. Today only the Windows path is implemented: + * the official QDLoader 9008 driver presents the device as a serial port + * (\\.\COMx). The Sahara/Firehose framing is identical to the libusb + * backend; this file only deals with how user space reaches the underlying + * bulk pipes. */ +#include +#include +#include #include +#include + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +/* + * CancelIoEx() requires Vista+; MinGW headers gate it on _WIN32_WINNT >= 0x0600. + * Windows 7 is the de-facto floor for this project so this is safe. + */ +#ifndef _WIN32_WINNT +#define _WIN32_WINNT 0x0600 +#endif +#include +/* + * must precede any header that declares the GUIDs we reference, + * so that GUID_DEVCLASS_PORTS gets a real definition in this TU instead of an + * extern reference (MinGW does not ship a uuid.lib that would resolve it). + */ +#include +#include +#include +#include +#include +#endif #include "qdl.h" +#include "oscompat.h" + +#ifdef _WIN32 + +#define QCOM_HWID_PREFIX "USB\\VID_05C6&PID_" + +/* + * The bus-reported device description is a PnP property (DEVPKEY), not + * an SPDRP_* registry property; older MinGW headers don't define the + * key, so we declare it locally. GUID + PID per Microsoft docs. + */ +static const DEVPROPKEY qud_devpkey_bus_reported_device_desc = { + { 0x540b947e, 0x8b40, 0x45bc, + { 0xa8, 0xa2, 0x6a, 0x0b, 0x89, 0x4c, 0xbd, 0xa2 } }, + 4 +}; + +struct qdl_device_qud { + struct qdl_device base; + void *handle; /* HANDLE; void* avoids leaking to consumers */ + char path[PATH_MAX]; +}; + +/* + * Pull the iSerial out of the SetupAPI device-instance ID. The instance ID for + * a USB device looks like "USB\\VID_05C6&PID_9008\\"; the trailing + * component after the last backslash is the iSerial as Windows stored it. + */ +static void win_extract_serial(const char *instance_id, char *out, size_t out_size) +{ + const char *last; + + out[0] = '\0'; + last = strrchr(instance_id, '\\'); + if (!last) + return; + last++; + snprintf(out, out_size, "%s", last); +} + +/* + * Read PortName ("COM5", "COM12", ...) from the device's "Device Parameters" + * registry subkey. + */ +static bool win_read_port_name(HDEVINFO devinfo, SP_DEVINFO_DATA *info, + char *out, size_t out_size) +{ + DWORD type = 0; + DWORD size = 0; + HKEY key; + LSTATUS s; + + key = SetupDiOpenDevRegKey(devinfo, info, DICS_FLAG_GLOBAL, 0, + DIREG_DEV, KEY_QUERY_VALUE); + if (key == INVALID_HANDLE_VALUE) + return false; + + size = (DWORD)out_size; + s = RegQueryValueExA(key, "PortName", NULL, &type, + (LPBYTE)out, &size); + RegCloseKey(key); + + if (s != ERROR_SUCCESS || type != REG_SZ) + return false; + + if (size > 0 && out[size - 1] != '\0' && size < out_size) + out[size] = '\0'; + return true; +} + +/* + * The EDL firmware leaves iSerial == 0 and stuffs the real serial into + * iProduct as "..._SN:_...". Windows surfaces iProduct verbatim + * through SPDRP_BUSREPORTEDDEVICEDESC, so we can recover the same serial + * the libusb backend extracts in usb_list() without opening the device. + * Returns true and writes the serial on success; false if the property + * is missing, empty, or does not contain the _SN: token. + */ +static bool win_read_iproduct_sn(HDEVINFO devinfo, SP_DEVINFO_DATA *info, + char *out, size_t out_size) +{ + WCHAR wdesc[256]; + char desc[512]; + DEVPROPTYPE prop_type = 0; + DWORD required = 0; + const char *p; + size_t len; + int n; + + if (!SetupDiGetDevicePropertyW(devinfo, info, + &qud_devpkey_bus_reported_device_desc, + &prop_type, (PBYTE)wdesc, sizeof(wdesc), + &required, 0)) + return false; + if (prop_type != DEVPROP_TYPE_STRING) + return false; + + wdesc[ARRAY_SIZE(wdesc) - 1] = L'\0'; + + n = WideCharToMultiByte(CP_UTF8, 0, wdesc, -1, desc, sizeof(desc), + NULL, NULL); + if (n <= 0) + return false; + + p = strstr(desc, "_SN:"); + if (!p) + return false; + p += 4; + len = strcspn(p, " _"); + if (len == 0 || len + 1 > out_size) + return false; + + memcpy(out, p, len); + out[len] = '\0'; + return true; +} + +/* + * Parse the 4-digit hex PID immediately following the QCOM_HWID_PREFIX + * in a HARDWAREID string. Returns 0 if no valid PID is found. + */ +static unsigned int win_parse_pid(const char *hwid) +{ + const char *p = hwid + sizeof(QCOM_HWID_PREFIX) - 1; + unsigned int pid = 0; + int i; + + for (i = 0; i < 4; i++) { + char c = p[i]; + unsigned int digit; + + if (c >= '0' && c <= '9') + digit = (unsigned int)(c - '0'); + else if (c >= 'a' && c <= 'f') + digit = (unsigned int)(c - 'a') + 10; + else if (c >= 'A' && c <= 'F') + digit = (unsigned int)(c - 'A') + 10; + else + return 0; + pid = (pid << 4) | digit; + } + return pid; +} + +static int win_enumerate_qcom(struct qud_device_desc *out, size_t out_max) +{ + HDEVINFO devinfo; + SP_DEVINFO_DATA info = { .cbSize = sizeof(info) }; + char hwid[256]; + char instance_id[256]; + char port_name[32]; + size_t count = 0; + DWORD i; + + devinfo = SetupDiGetClassDevsA(&GUID_DEVCLASS_PORTS, NULL, NULL, + DIGCF_PRESENT); + if (devinfo == INVALID_HANDLE_VALUE) + return -1; + + for (i = 0; SetupDiEnumDeviceInfo(devinfo, i, &info); i++) { + DWORD type = 0; + DWORD size = 0; + + if (!SetupDiGetDeviceRegistryPropertyA(devinfo, &info, + SPDRP_HARDWAREID, &type, + (BYTE *)hwid, sizeof(hwid), + &size)) + continue; + + /* HARDWAREID is a REG_MULTI_SZ; first entry is what we want. */ + if (strncmp(hwid, QCOM_HWID_PREFIX, + sizeof(QCOM_HWID_PREFIX) - 1) != 0) + continue; + + if (!SetupDiGetDeviceInstanceIdA(devinfo, &info, instance_id, + sizeof(instance_id), NULL)) + continue; + + if (!win_read_port_name(devinfo, &info, port_name, + sizeof(port_name))) + continue; + + if (count < out_max) { + out[count].pid = win_parse_pid(hwid); + if (!win_read_iproduct_sn(devinfo, &info, + out[count].serial, + sizeof(out[count].serial))) + win_extract_serial(instance_id, out[count].serial, + sizeof(out[count].serial)); + snprintf(out[count].path, sizeof(out[count].path), + "\\\\.\\%s", port_name); + } + count++; + } + + SetupDiDestroyDeviceInfoList(devinfo); + return (int)count; +} + +static int win_configure_handle(HANDLE h) +{ + COMMTIMEOUTS to = { 0 }; + DCB dcb = { .DCBlength = sizeof(dcb) }; + + /* + * The QDLoader 9008 driver presents the device as a serial port, but + * line-discipline parameters (baud, parity, stop bits) are irrelevant + * over USB-CDC. Some driver builds nevertheless reject I/O until DCB + * has been initialised, so we set sane defaults and never touch them + * again. + */ + if (!GetCommState(h, &dcb)) + return -1; + dcb.BaudRate = 115200; + dcb.ByteSize = 8; + dcb.Parity = NOPARITY; + dcb.StopBits = ONESTOPBIT; + dcb.fBinary = TRUE; + dcb.fNull = FALSE; + dcb.fAbortOnError = FALSE; + /* + * Force-disable hardware/software flow control: some QDLoader driver + * builds gate bulk-IN delivery on these handshake lines, which would + * stall reads even though USB-CDC has no real RS-232 signals. + */ + dcb.fOutxCtsFlow = FALSE; + dcb.fOutxDsrFlow = FALSE; + dcb.fDsrSensitivity = FALSE; + dcb.fOutX = FALSE; + dcb.fInX = FALSE; + dcb.fDtrControl = DTR_CONTROL_ENABLE; + dcb.fRtsControl = RTS_CONTROL_ENABLE; + if (!SetCommState(h, &dcb)) + return -1; + + /* + * Mirror libusb bulk-read semantics: ReadFile returns as soon as any + * data is available, or after ReadTotalTimeoutConstant ms with zero + * bytes if the link is idle (MSDN COMMTIMEOUTS, "MAXDWORD interval + + * MAXDWORD multiplier" case). qud_read() rewrites the constant + * per call; this just seeds a sane default for writes and any read + * issued before that. + */ + to.ReadIntervalTimeout = MAXDWORD; + to.ReadTotalTimeoutMultiplier = MAXDWORD; + to.ReadTotalTimeoutConstant = 1000; + to.WriteTotalTimeoutMultiplier = 0; + to.WriteTotalTimeoutConstant = 0; + if (!SetCommTimeouts(h, &to)) + return -1; + + SetupComm(h, 64 * 1024, 64 * 1024); + /* + * Only purge the TX side. The QDLoader driver claims the USB + * interface during PnP enumeration, well before CreateFile() runs, + * so the device's Sahara HELLO is typically already sitting in the + * driver's RX FIFO by the time we get here. Purging RX would + * discard it, and the device only sends HELLO once. + */ + PurgeComm(h, PURGE_TXCLEAR | PURGE_TXABORT); + return 0; +} + +static int qud_open(struct qdl_device *qdl, const char *serial) +{ + struct qdl_device_qud *qd = container_of(qdl, + struct qdl_device_qud, + base); + struct qud_device_desc matches[16]; + const char *path = NULL; + HANDLE h; + int found; + int i; + + /* Allow an explicit \\.\COMx (or COMx) path via --serial. */ + if (serial && (serial[0] == '\\' || + (strlen(serial) >= 3 && + (serial[0] == 'C' || serial[0] == 'c') && + (serial[1] == 'O' || serial[1] == 'o') && + (serial[2] == 'M' || serial[2] == 'm')))) { + path = serial; + } else { + found = win_enumerate_qcom(matches, ARRAY_SIZE(matches)); + if (found < 0) { + ux_err("qud: SetupDiGetClassDevs failed (error %lu)\n", + (unsigned long)GetLastError()); + return -1; + } + if (found == 0) { + ux_err("qud: no Qualcomm COM ports found\n"); + return -1; + } + + for (i = 0; i < found && i < (int)ARRAY_SIZE(matches); i++) { + if (!serial || serial[0] == '\0' || + _stricmp(matches[i].serial, serial) == 0) { + path = matches[i].path; + break; + } + } + + if (!path) { + ux_err("qud: no Qualcomm device matching serial \"%s\"\n", + serial); + return -1; + } + } + + h = CreateFileA(path, GENERIC_READ | GENERIC_WRITE, 0, NULL, + OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL); + if (h == INVALID_HANDLE_VALUE) { + ux_err("qud: CreateFile(%s) failed (error %lu)\n", + path, (unsigned long)GetLastError()); + return -1; + } + + if (win_configure_handle(h) < 0) { + ux_err("qud: failed to configure %s (error %lu)\n", + path, (unsigned long)GetLastError()); + CloseHandle(h); + return -1; + } + + qd->handle = h; + snprintf(qd->path, sizeof(qd->path), "%s", path); + + ux_debug("qud: opened %s\n", path); + return 0; +} + +static int win_overlapped_io(HANDLE h, void *buf, size_t len, + unsigned int timeout_ms, bool is_write) +{ + OVERLAPPED ovl = { 0 }; + HANDLE ev; + DWORD wait; + DWORD done = 0; + BOOL ok; + + ev = CreateEvent(NULL, TRUE, FALSE, NULL); + if (!ev) + return -EIO; + ovl.hEvent = ev; + + if (is_write) + ok = WriteFile(h, buf, (DWORD)len, NULL, &ovl); + else + ok = ReadFile(h, buf, (DWORD)len, NULL, &ovl); + + if (!ok && GetLastError() != ERROR_IO_PENDING) { + CloseHandle(ev); + return -EIO; + } + + wait = WaitForSingleObject(ev, timeout_ms); + if (wait == WAIT_TIMEOUT) { + CancelIoEx(h, &ovl); + /* Drain the cancellation so the OVERLAPPED isn't reused live. */ + GetOverlappedResult(h, &ovl, &done, TRUE); + CloseHandle(ev); + return -ETIMEDOUT; + } + if (wait != WAIT_OBJECT_0) { + CloseHandle(ev); + return -EIO; + } + + if (!GetOverlappedResult(h, &ovl, &done, FALSE)) { + CloseHandle(ev); + return -EIO; + } + + CloseHandle(ev); + return (int)done; +} + +static int qud_read(struct qdl_device *qdl, void *buf, size_t len, + unsigned int timeout) +{ + struct qdl_device_qud *qd = container_of(qdl, + struct qdl_device_qud, + base); + HANDLE h = (HANDLE)qd->handle; + COMMTIMEOUTS to = { + .ReadIntervalTimeout = MAXDWORD, + .ReadTotalTimeoutMultiplier = MAXDWORD, + .ReadTotalTimeoutConstant = timeout ? timeout : 1, + .WriteTotalTimeoutMultiplier = 0, + .WriteTotalTimeoutConstant = 0, + }; + int ret; + + /* + * The COM driver itself enforces the per-call timeout via + * COMMTIMEOUTS; the WaitForSingleObject() inside win_overlapped_io() + * is a backstop with a small grace window in case the driver + * completes a hair after the constant. + */ + if (!SetCommTimeouts(h, &to)) + return -EIO; + + ret = win_overlapped_io(h, buf, len, timeout + 250, false); + if (ret == 0) + return -ETIMEDOUT; + return ret; +} + +static int qud_write(struct qdl_device *qdl, const void *buf, size_t len, + unsigned int timeout) +{ + struct qdl_device_qud *qd = container_of(qdl, + struct qdl_device_qud, + base); + + return win_overlapped_io((HANDLE)qd->handle, (void *)buf, len, timeout, + true); +} + +static void qud_close(struct qdl_device *qdl) +{ + struct qdl_device_qud *qd = container_of(qdl, + struct qdl_device_qud, + base); + HANDLE h = (HANDLE)qd->handle; + + if (h && h != INVALID_HANDLE_VALUE) { + CloseHandle(h); + qd->handle = NULL; + } +} + +static void qud_set_out_chunk_size(struct qdl_device *qdl __unused, + long size __unused) +{ + /* The kernel-mode driver handles bulk-transfer chunking. */ +} + +struct qdl_device *qud_init(void) +{ + struct qdl_device_qud *qd = calloc(1, sizeof(*qd)); + + if (!qd) + return NULL; + + qd->handle = NULL; + qd->base.dev_type = QDL_DEVICE_QUD; + qd->base.open = qud_open; + qd->base.read = qud_read; + qd->base.write = qud_write; + qd->base.close = qud_close; + qd->base.set_out_chunk_size = qud_set_out_chunk_size; + qd->base.max_payload_size = 1048576; + + return &qd->base; +} + +/* + * Enumerate Qualcomm COM ports the QUD backend can drive, returning a + * heap-allocated array of qud_device_desc and writing the count to + * *devices_found. Caller frees with free(). Returns NULL on error or + * when no devices are present (count is still written). + */ +struct qud_device_desc *qud_list(unsigned int *devices_found) +{ + struct qud_device_desc *result; + int found; + + *devices_found = 0; + + result = calloc(16, sizeof(*result)); + if (!result) + return NULL; + + found = win_enumerate_qcom(result, 16); + if (found <= 0) { + free(result); + return NULL; + } + + if (found > 16) { + ux_err("qud: %d devices present, only the first 16 will be listed\n", + found); + found = 16; + } + + *devices_found = (unsigned int)found; + return result; +} + +#else /* unsupported platform */ struct qdl_device *qud_init(void) { ux_err("qud backend is not supported on this platform\n"); return NULL; } + +struct qud_device_desc *qud_list(unsigned int *devices_found) +{ + *devices_found = 0; + return NULL; +} + +#endif From e2c5ce39254417bdbd3a74eb70820e11867a72f2 Mon Sep 17 00:00:00 2001 From: Igor Opaniuk Date: Sun, 10 May 2026 22:49:47 +0200 Subject: [PATCH 3/5] firehose: parse multi-document and rawmode-trailed reads firehose_read() previously assumed each qdl_read() returns a single Firehose XML document. That holds on libusb (each USB bulk-in transfer surfaces as its own read return), but not on stream-oriented transports. The Windows COM port driver behind the QUD backend just streams bytes into its RX buffer, and a single ReadFile() can deliver several back-to-back Firehose responses concatenated; on top of that the binary payload of a rawmode response (e.g. the sector data that follows a ACK) can be spliced onto the same buffer right after the closing tag. Walk the read buffer using "" as the envelope end, then dispatch each XML chunk through the existing firehose_response_parse() + response_parser path. and elements are still honoured in arrival order, and rawmode is observed on whichever chunk carries it. When that chunk is followed by trailing bytes, those bytes are the first sectors of the binary payload: hand them to qdl_push_back() so the next qdl_read() picks them up before the transport is touched again. firehose_issue_read() now accumulates each sector-chunk across qdl_read() calls instead of requiring a single full-sized return, since on a stream transport the rawmode tail and the remaining sector bytes generally arrive in several reads. libusb keeps delivering an entire bulk transfer in one call, so the inner loop runs once there. The change is a no-op on libusb where each read already contains exactly one document and the rawmode payload arrives in its own bulk transfer; it unblocks all current and future stream-oriented transports without touching the libusb path's behaviour. Signed-off-by: Igor Opaniuk --- firehose.c | 124 ++++++++++++++++++++++++++++++++++++++++++----------- io.c | 69 ++++++++++++++++++++++++++++- qdl.h | 13 ++++++ 3 files changed, 181 insertions(+), 25 deletions(-) diff --git a/firehose.c b/firehose.c index 87de4cca..2e6dc7d6 100644 --- a/firehose.c +++ b/firehose.c @@ -179,15 +179,75 @@ static int firehose_read(struct qdl_device *qdl, int timeout_ms, ux_debug("FIREHOSE READ: %s\n", buf); - node = firehose_response_parse(buf, n, &error); - if (!node) - return error; + /* + * On stream-oriented transports (Windows COM port via the + * QDLoader driver, virtio-console, ...) a single read can + * deliver multiple back-to-back Firehose responses + * concatenated, since the driver doesn't preserve USB bulk- + * transfer boundaries. Walk the buffer using the "" envelope to bound each message; the closing tag + * is what really delimits the document so that any rawmode + * binary payload that arrives spliced onto the same read + * doesn't end up fed into libxml2 as if it were XML. + * + * libusb preserves transfer boundaries, so on that path each + * read still contains exactly one document and the loop runs + * once. + */ + char *cursor = buf; + char *bufend = buf + n; - ret = response_parser(node, data, &rawmode); - xmlFreeDoc(node->doc); + while (cursor < bufend) { + char *start = strstr(cursor, "= 0) - resp = ret; + if (!start) + break; + + /* + * Bound the XML on the closing tag. If it's + * missing the message was either truncated or doesn't + * fit the schema we know how to parse; hand the rest + * of the buffer to libxml2 and let it error out + * gracefully. + */ + xml_end = strstr(start, ""); + if (xml_end) { + xml_end += sizeof("") - 1; + chunk = (size_t)(xml_end - start); + } else { + chunk = (size_t)(bufend - start); + } + + node = firehose_response_parse(start, chunk, &error); + if (!node) + return error; + + ret = response_parser(node, data, &rawmode); + xmlFreeDoc(node->doc); + + if (ret >= 0) + resp = ret; + + cursor = start + chunk; + + /* + * The response we just parsed told the host to switch + * to raw mode (e.g. the ACK that precedes the binary + * sectors of a ). On a stream transport the + * first chunk of that binary payload can have arrived + * tacked onto this same read. Push it back so the + * next qdl_read() picks it up before the transport + * is touched again. + */ + if (rawmode) { + if (cursor < bufend) + qdl_push_back(qdl, cursor, + (size_t)(bufend - cursor)); + break; + } + } if (rawmode) break; @@ -697,31 +757,47 @@ static int firehose_issue_read(struct qdl_device *qdl, struct firehose_op *read_ left = read_op->num_sectors; while (left > 0) { - chunk_size = MIN(qdl->max_payload_size / sector_size, left); + size_t want; + size_t got; - n = qdl_read(qdl, buf, chunk_size * sector_size, 30000); - if (n < 0) { - ux_err("failed to read sector data\n"); - ret = -1; - goto out; - } + chunk_size = MIN(qdl->max_payload_size / sector_size, left); + want = chunk_size * sector_size; - if ((size_t)n != chunk_size * sector_size) { - ux_err("failed to read full sector\n"); - ret = -1; - goto out; + /* + * Accumulate the chunk across qdl_read() calls. libusb usually + * delivers an entire bulk transfer in one shot, but stream + * transports (QUD's Windows COM port, virtio-console, ...) can + * fragment it - including the rawmode tail that firehose_read() + * pushed back from the same buffer as the ACK response. + */ + got = 0; + while (got < want) { + n = qdl_read(qdl, (char *)buf + got, want - got, 30000); + if (n < 0) { + ux_err("failed to read sector data\n"); + ret = -1; + goto out; + } + if (n == 0) { + ux_err("unexpected EOF while reading sector data\n"); + ret = -1; + goto out; + } + got += (size_t)n; } if (out_buf) { - if ((size_t)n > out_len - out_offset) - n = out_len - out_offset; + size_t copy = want; + + if (copy > out_len - out_offset) + copy = out_len - out_offset; - memcpy(out_buf + out_offset, buf, n); - out_offset += n; + memcpy((char *)out_buf + out_offset, buf, copy); + out_offset += copy; } else { - n = write(fd, buf, n); + n = write(fd, buf, want); - if (n < 0 || (size_t)n != chunk_size * sector_size) { + if (n < 0 || (size_t)n != want) { ux_err("failed to write sector data\n"); ret = -1; goto out; diff --git a/io.c b/io.c index 7a3f4553..21d0142a 100644 --- a/io.c +++ b/io.c @@ -2,7 +2,9 @@ /* * Copyright (c) 2025, Qualcomm Innovation Center, Inc. All rights reserved. */ +#include #include +#include #include "qdl.h" @@ -22,8 +24,10 @@ struct qdl_device *qdl_init(enum QDL_DEVICE_TYPE type) void qdl_deinit(struct qdl_device *qdl) { - if (qdl) + if (qdl) { + free(qdl->pending_buf); free(qdl); + } } void qdl_set_out_chunk_size(struct qdl_device *qdl, long size) @@ -48,14 +52,77 @@ void qdl_close(struct qdl_device *qdl) * @len: maximum length of data to be read * @timeout: timeout for the read, in milliseconds * + * Drains the pushback buffer (qdl_push_back()) before touching the underlying + * transport, so a previous read that crossed a Firehose message boundary can + * deliver the trailing bytes here. + * * Returns: number of bytes read, might be zero for a ZLP * negative errno on failure (notably -ETIMEDOUT) */ int qdl_read(struct qdl_device *qdl, void *buf, size_t len, unsigned int timeout) { + size_t available; + size_t copy; + + if (qdl->pending_buf) { + available = qdl->pending_len - qdl->pending_off; + copy = available < len ? available : len; + memcpy(buf, qdl->pending_buf + qdl->pending_off, copy); + qdl->pending_off += copy; + if (qdl->pending_off >= qdl->pending_len) { + free(qdl->pending_buf); + qdl->pending_buf = NULL; + qdl->pending_len = 0; + qdl->pending_off = 0; + } + return (int)copy; + } + return qdl->read(qdl, buf, len, timeout); } +/** + * qdl_push_back() - Stash unread bytes for a future qdl_read() + * @qdl: device handle + * @buf: bytes to remember + * @len: number of bytes + * + * Concatenates @buf onto whatever is already pending (after any already-served + * prefix). Used by firehose_read() when a single transport read returned more + * than one Firehose message - or an XML envelope followed immediately by its + * rawmode binary payload - and we need the trailing bytes to surface on the + * next qdl_read() before any new transport I/O happens. + * + * Returns: 0 on success, negative errno on allocation failure. + */ +int qdl_push_back(struct qdl_device *qdl, const void *buf, size_t len) +{ + char *grown; + size_t total; + + if (!len) + return 0; + + if (qdl->pending_off > 0) { + size_t leftover = qdl->pending_len - qdl->pending_off; + + memmove(qdl->pending_buf, qdl->pending_buf + qdl->pending_off, + leftover); + qdl->pending_len = leftover; + qdl->pending_off = 0; + } + + total = qdl->pending_len + len; + grown = realloc(qdl->pending_buf, total); + if (!grown) + return -ENOMEM; + + memcpy(grown + qdl->pending_len, buf, len); + qdl->pending_buf = grown; + qdl->pending_len = total; + return 0; +} + /** * qdl_write() - Write a message from the device * @qdl: device handle diff --git a/qdl.h b/qdl.h index ed5c4614..b7c528c5 100644 --- a/qdl.h +++ b/qdl.h @@ -76,6 +76,18 @@ struct qdl_device { const char *chained_table); struct vip_transfer_data vip_data; + + /* + * Pushback buffer for stream-oriented transports (Windows COM via the + * QDLoader driver, virtio-console, ...). When a single read crosses a + * Firehose message boundary - typically because the binary payload of + * a rawmode response trails the XML envelope in the same read - the + * leftover bytes are stashed here and qdl_read() returns them before + * pulling more data from the transport. + */ + char *pending_buf; + size_t pending_len; + size_t pending_off; }; struct sahara_image { @@ -93,6 +105,7 @@ void qdl_deinit(struct qdl_device *qdl); int qdl_open(struct qdl_device *qdl, const char *serial); void qdl_close(struct qdl_device *qdl); int qdl_read(struct qdl_device *qdl, void *buf, size_t len, unsigned int timeout); +int qdl_push_back(struct qdl_device *qdl, const void *buf, size_t len); int qdl_write(struct qdl_device *qdl, const void *buf, size_t len, unsigned int timeout); void qdl_set_out_chunk_size(struct qdl_device *qdl, long size); int qdl_vip_transfer_enable(struct qdl_device *qdl, const char *vip_table_path); From d558eb9684b4bacde8993f5c3c96dc5f8c0cf1e8 Mon Sep 17 00:00:00 2001 From: Igor Opaniuk Date: Sun, 10 May 2026 23:00:07 +0200 Subject: [PATCH 4/5] qdl: add auto backend that resolves USB/QUD inside the wait loop Add a meta-backend QDL_DEVICE_AUTO that defers the transport choice to qdl_open(): instead of picking libusb or QUD upfront, its open() runs a unified wait loop and binds whichever transport first reaches an EDL device. The "auto" --backend token (and the default) maps to it. Each 250 ms tick the loop does: 1. usb_open_attempt() - one libusb scan-and-open. On success the libusb device is bound as the inner transport. 2. On Windows, if libusb saw the device but could not open it (-EBUSY: the QDLoader 9008 driver has claimed the interface) or SetupAPI sees a Qualcomm COM port libusb missed, calls qud_open(). On success the QUD device is bound as the inner. 3. Otherwise sleeps and retries. No timeout: the loop waits indefinitely, same cadence as the prior libusb-only loop. qdl_read/write/close on the outer auto wrapper forward to the inner. Pre-open out_chunk_size is stashed on the wrapper and applied at bind time; vip_data and slot live on the outer where firehose accesses them, so no forwarding is needed for those. usb_open_attempt() (factored out of usb_open()) returns 0 / -ENODEV / -EBUSY / -EIO. The --backend usb path loops over it with visibility-transition messaging. The auto loop owns its own messaging and never emits the -EBUSY diagnostic because that condition triggers the QUD attempt in the same tick. qdl-ramdump now uses QDL_DEVICE_AUTO so Windows users with the official driver can collect crash dumps without re-binding the USB driver to WinUSB. QDL_DEVICE_AUTO is explicitly a meta-backend, not a peer of the USB and QUD backends: its job is to defer the transport choice to the moment a probe reaches a device. The libusb and QUD backends remain standalone and selectable via --backend usb / --backend qud. Signed-off-by: Igor Opaniuk --- auto.c | 165 ++++++++++++++++++++++++++++++++++++++++++++++++++++ io.c | 3 + meson.build | 4 +- qdl.c | 21 ++++--- qdl.h | 24 ++++++++ qud.c | 20 +++++++ ramdump.c | 2 +- usb.c | 164 +++++++++++++++++++++++++++++---------------------- 8 files changed, 322 insertions(+), 81 deletions(-) create mode 100644 auto.c diff --git a/auto.c b/auto.c new file mode 100644 index 00000000..837927d3 --- /dev/null +++ b/auto.c @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. + * + * QDL_DEVICE_AUTO: meta-backend that defers transport selection to the + * wait loop. Each 250 ms tick it runs both the libusb open attempt and + * (on Windows) a QUD SetupAPI probe; whichever first reaches an EDL + * device wins, and its concrete qdl_device is bound as the inner. All + * subsequent qdl_read/write/close calls on the outer forward to the + * inner. + */ + +#include +#include +#include +#include +#include + +#include "qdl.h" + +struct qdl_device_auto { + struct qdl_device base; + struct qdl_device *inner; + long pending_chunk_size; + bool chunk_size_set; +}; + +static struct qdl_device_auto *to_auto(struct qdl_device *qdl) +{ + return container_of(qdl, struct qdl_device_auto, base); +} + +static void auto_bind_inner(struct qdl_device_auto *wrap, struct qdl_device *inner) +{ + wrap->inner = inner; + wrap->base.max_payload_size = inner->max_payload_size; + if (wrap->chunk_size_set) + inner->set_out_chunk_size(inner, wrap->pending_chunk_size); +} + +static int auto_open(struct qdl_device *qdl, const char *serial) +{ + struct qdl_device_auto *wrap = to_auto(qdl); + struct qdl_device *usb_dev; +#ifdef _WIN32 + struct qdl_device *qud_dev; + int qud_count; +#endif + int visible_prev = -1; + int visible; + int ret; + + usb_dev = usb_init(); + if (!usb_dev) + return -1; + +#ifdef _WIN32 + qud_dev = qud_init(); + if (!qud_dev) { + qdl_deinit(usb_dev); + return -1; + } +#endif + + for (;;) { + ret = try_usb_open(usb_dev, serial, &visible); + if (ret == 0) { +#ifdef _WIN32 + qdl_deinit(qud_dev); +#endif + auto_bind_inner(wrap, usb_dev); + return 0; + } + if (ret == -EIO) + goto fail; + +#ifdef _WIN32 + qud_count = qud_probe_present(); + if (qud_count > 0 || ret == -EBUSY) { + if (qud_dev->open(qud_dev, serial) == 0) { + qdl_deinit(usb_dev); + auto_bind_inner(wrap, qud_dev); + return 0; + } + } + visible += qud_count; +#endif + + if (visible != visible_prev) { + if (visible == 0) + ux_info("Waiting for EDL device\n"); + else if (serial) + ux_info("%d EDL device(s) visible, none match serial \"%s\"\n", + visible, serial); + else + ux_info("%d EDL device(s) visible, none could be opened\n", + visible); + visible_prev = visible; + } + + usleep(250000); + } + +fail: + qdl_deinit(usb_dev); +#ifdef _WIN32 + qdl_deinit(qud_dev); +#endif + return -1; +} + +static int auto_read(struct qdl_device *qdl, void *buf, size_t len, unsigned int timeout) +{ + struct qdl_device *inner = to_auto(qdl)->inner; + + return inner->read(inner, buf, len, timeout); +} + +static int auto_write(struct qdl_device *qdl, const void *buf, size_t len, unsigned int timeout) +{ + struct qdl_device *inner = to_auto(qdl)->inner; + + return inner->write(inner, buf, len, timeout); +} + +static void auto_close(struct qdl_device *qdl) +{ + struct qdl_device_auto *wrap = to_auto(qdl); + + if (!wrap->inner) + return; + wrap->inner->close(wrap->inner); + qdl_deinit(wrap->inner); + wrap->inner = NULL; +} + +static void auto_set_out_chunk_size(struct qdl_device *qdl, long size) +{ + struct qdl_device_auto *wrap = to_auto(qdl); + + if (wrap->inner) { + wrap->inner->set_out_chunk_size(wrap->inner, size); + return; + } + wrap->pending_chunk_size = size; + wrap->chunk_size_set = true; +} + +struct qdl_device *auto_init(void) +{ + struct qdl_device_auto *wrap = calloc(1, sizeof(*wrap)); + + if (!wrap) + return NULL; + + wrap->base.dev_type = QDL_DEVICE_AUTO; + wrap->base.open = auto_open; + wrap->base.read = auto_read; + wrap->base.write = auto_write; + wrap->base.close = auto_close; + wrap->base.set_out_chunk_size = auto_set_out_chunk_size; + wrap->base.max_payload_size = 1048576; + + return &wrap->base; +} diff --git a/io.c b/io.c index 21d0142a..2e1684f1 100644 --- a/io.c +++ b/io.c @@ -19,6 +19,9 @@ struct qdl_device *qdl_init(enum QDL_DEVICE_TYPE type) if (type == QDL_DEVICE_QUD) return qud_init(); + if (type == QDL_DEVICE_AUTO) + return auto_init(); + return NULL; } diff --git a/meson.build b/meson.build index 6bd166e8..908ec8d6 100644 --- a/meson.build +++ b/meson.build @@ -62,7 +62,7 @@ shared_lib = static_library('qdl_common', common_sources, dependencies: common_d # --- qdl executable --- qdl_sources = files( - 'qud.c', + 'auto.c', 'qud.c', 'firehose.c', 'io.c', 'qdl.c', 'patch.c', 'program.c', 'read.c', 'sha2.c', 'sim.c', 'ufs.c', 'usb.c', @@ -80,7 +80,7 @@ qdl_exe = executable('qdl', # --- qdl-ramdump executable --- ramdump_sources = files( - 'ramdump.c', 'qud.c', 'io.c', 'sha2.c', 'sim.c', 'usb.c' + 'ramdump.c', 'auto.c', 'qud.c', 'io.c', 'sha2.c', 'sim.c', 'usb.c' ) qdl_ramdump_exe = executable('qdl-ramdump', diff --git a/qdl.c b/qdl.c index 3ef2a91b..a9b64217 100644 --- a/qdl.c +++ b/qdl.c @@ -106,18 +106,23 @@ static int detect_type(const char *verb) } /* - * Parse a --backend= value into an enum. "auto" picks the best available real - * backend; today that is QDL_DEVICE_USB (libusb / WinUSB on Windows). The - * "qud" backend covers official kernel drivers that expose the device as a - * character device (e.g. /dev/ttyUSB* on Linux, \\.\COMx on Windows) and is - * recognised here for forward compatibility but not yet wired to qdl_init(). + * Parse a --backend= value into an enum. "auto" maps to the meta-backend + * QDL_DEVICE_AUTO, which inside its open path runs a unified wait loop + * over libusb and (on Windows) the QUD SetupAPI enumeration, binding + * whichever first reaches an EDL device. Explicit "usb"/"qud" pin to a + * single concrete transport and skip the meta layer entirely. * * QDL_DEVICE_SIM is intentionally not selectable via --backend; --dry-run / * --create-digests pick it implicitly. */ static int decode_backend(const char *name, enum QDL_DEVICE_TYPE *out) { - if (!name || !strcmp(name, "auto") || !strcmp(name, "usb")) { + if (!name || !strcmp(name, "auto")) { + *out = QDL_DEVICE_AUTO; + return 0; + } + + if (!strcmp(name, "usb")) { *out = QDL_DEVICE_USB; return 0; } @@ -538,7 +543,7 @@ static int qdl_ramdump(int argc, char **argv) char *ramdump_path = "."; char *filter = NULL; char *serial = NULL; - enum QDL_DEVICE_TYPE qdl_dev_type = QDL_DEVICE_USB; + enum QDL_DEVICE_TYPE qdl_dev_type = QDL_DEVICE_AUTO; int ret = 0; int opt; @@ -819,7 +824,7 @@ static int qdl_flash(int argc, char **argv) long out_chunk_size = 0; unsigned int slot = UINT_MAX; struct qdl_device *qdl = NULL; - enum QDL_DEVICE_TYPE qdl_dev_type = QDL_DEVICE_USB; + enum QDL_DEVICE_TYPE qdl_dev_type = QDL_DEVICE_AUTO; static struct option options[] = { {"debug", no_argument, 0, 'd'}, diff --git a/qdl.h b/qdl.h index b7c528c5..bb8f18cb 100644 --- a/qdl.h +++ b/qdl.h @@ -48,6 +48,14 @@ enum QDL_DEVICE_TYPE { QDL_DEVICE_USB, QDL_DEVICE_SIM, QDL_DEVICE_QUD, + /* + * Meta-backend: defers transport selection to the wait loop inside + * auto_open(), which polls libusb and (on Windows) the QUD SetupAPI + * enumeration each tick and binds whichever first reaches an EDL + * device. Resolves the UX hazard of an upfront probe timeout where + * the user plugs in the cable just after the grace window expires. + */ + QDL_DEVICE_AUTO, }; enum qdl_storage_type { @@ -113,6 +121,22 @@ int qdl_vip_transfer_enable(struct qdl_device *qdl, const char *vip_table_path); struct qdl_device *usb_init(void); struct qdl_device *sim_init(void); struct qdl_device *qud_init(void); +struct qdl_device *auto_init(void); + +/* + * try_usb_open() - single libusb scan-and-open pass; shared between + * the --backend usb wait loop in usb.c and the unified auto_open() loop. + * Returns 0 on success (and emits the "Flashing/Collecting device" UX + * line), -ENODEV when no EDL device is visible, -EBUSY when one is + * visible but cannot be opened (typically: the Qualcomm QDLoader 9008 + * driver has claimed it), -EIO on a libusb failure. @visible_out, if + * non-NULL, receives the count of EDL devices seen on this pass. + * + * qud_probe_present() returns the number of Qualcomm COM ports the QUD + * backend enumerated via SetupAPI; 0 on non-Windows hosts. + */ +int try_usb_open(struct qdl_device *qdl, const char *serial, int *visible_out); +int qud_probe_present(void); struct qdl_device_desc { int vid; diff --git a/qud.c b/qud.c index fdb53fae..c8ff5cac 100644 --- a/qud.c +++ b/qud.c @@ -496,6 +496,21 @@ struct qdl_device *qud_init(void) return &qd->base; } +/* + * qud_probe_present() - count Qualcomm COM ports exposed by the QDLoader + * 9008 driver. Used by the auto-backend selector on Windows to decide + * whether the QUD backend can take over when libusb cannot open the + * device. + */ +int qud_probe_present(void) +{ + struct qud_device_desc matches[16]; + int found; + + found = win_enumerate_qcom(matches, ARRAY_SIZE(matches)); + return found > 0 ? found : 0; +} + /* * Enumerate Qualcomm COM ports the QUD backend can drive, returning a * heap-allocated array of qud_device_desc and writing the count to @@ -537,6 +552,11 @@ struct qdl_device *qud_init(void) return NULL; } +int qud_probe_present(void) +{ + return 0; +} + struct qud_device_desc *qud_list(unsigned int *devices_found) { *devices_found = 0; diff --git a/ramdump.c b/ramdump.c index 2f6b00e2..718f951f 100644 --- a/ramdump.c +++ b/ramdump.c @@ -31,7 +31,7 @@ int main(int argc, char **argv) { struct qdl_device *qdl; - qdl = qdl_init(QDL_DEVICE_USB); + qdl = qdl_init(QDL_DEVICE_AUTO); if (!qdl) return 1; diff --git a/usb.c b/usb.c index 6602cb95..52e2f108 100644 --- a/usb.c +++ b/usb.c @@ -195,7 +195,33 @@ static bool usb_is_edl_pid(uint16_t pid) return pid == 0x9008 || pid == 0x900e || pid == 0x901d; } -static int usb_open(struct qdl_device *qdl, const char *serial) +/* + * try_usb_open() - one libusb scan-and-open pass. + * + * Used as the single iteration unit by both usb_open() (the --backend usb + * wait loop) and auto_open() (the unified Windows libusb+QUD wait loop). + * + * On success, populates @qdl and emits the "Flashing/Collecting device" + * UX line; the caller need not log anything. + * + * Returns: + * 0 - device opened, @qdl is ready + * -ENODEV - no EDL device currently visible + * -EBUSY - EDL device(s) visible but none could be opened + * (typically: another driver, usually the Qualcomm + * QDLoader 9008 driver behind the QUD backend, has + * claimed the USB interface) + * -EIO - libusb itself failed (init/get_device_list) + * + * @visible_out, when non-NULL, receives the number of EDL devices that + * libusb saw on this pass, so callers that loop can print transition + * diagnostics without re-enumerating. + * + * Does its own libusb_init()/libusb_exit() so the next call sees a + * fresh enumeration (libusb's udev-less backends otherwise cache the + * list, which would mask newly-attached devices in containerised setups). + */ +int try_usb_open(struct qdl_device *qdl, const char *serial, int *visible_out) { struct qdl_device_usb *qdl_usb = container_of(qdl, struct qdl_device_usb, base); struct libusb_device_descriptor desc; @@ -203,100 +229,98 @@ static int usb_open(struct qdl_device *qdl, const char *serial) struct libusb_device *dev; char matched_serial[64]; uint16_t matched_pid = 0; - int visible_prev = -1; bool found = false; - int visible; + int visible = 0; ssize_t n; int ret; int i; - for (;;) { - ret = libusb_init(NULL); - if (ret < 0) - ux_err("failed to initialize libusb: %s\n", libusb_strerror(ret)); + if (visible_out) + *visible_out = 0; - n = libusb_get_device_list(NULL, &devs); - if (n < 0) { - ux_err("failed to list USB devices: %s\n", libusb_strerror(n)); - libusb_exit(NULL); - return -1; - } + ret = libusb_init(NULL); + if (ret < 0) { + ux_err("failed to initialize libusb: %s\n", libusb_strerror(ret)); + return -EIO; + } - visible = 0; - for (i = 0; devs[i]; i++) { - dev = devs[i]; + n = libusb_get_device_list(NULL, &devs); + if (n < 0) { + ux_err("failed to list USB devices: %s\n", libusb_strerror(n)); + libusb_exit(NULL); + return -EIO; + } - if (libusb_get_device_descriptor(dev, &desc) < 0) - continue; - if (desc.idVendor != 0x05c6 || !usb_is_edl_pid(desc.idProduct)) - continue; + for (i = 0; devs[i]; i++) { + dev = devs[i]; + + if (libusb_get_device_descriptor(dev, &desc) < 0) + continue; + if (desc.idVendor != 0x05c6 || !usb_is_edl_pid(desc.idProduct)) + continue; - visible++; + visible++; - ret = usb_try_open(dev, qdl_usb, serial); - if (ret == 1) { - found = true; - matched_pid = desc.idProduct; - if (!usb_read_serial(qdl_usb->usb_handle, &desc, - matched_serial, sizeof(matched_serial))) - matched_serial[0] = '\0'; - break; - } + ret = usb_try_open(dev, qdl_usb, serial); + if (ret == 1) { + found = true; + matched_pid = desc.idProduct; + if (!usb_read_serial(qdl_usb->usb_handle, &desc, + matched_serial, sizeof(matched_serial))) + matched_serial[0] = '\0'; + break; } + } - if (found) { - const char *action = matched_pid == 0x900e ? "Collecting crash dump from" : "Flashing"; + if (visible_out) + *visible_out = visible; - libusb_free_device_list(devs, 1); - if (matched_serial[0]) - ux_info("%s device (PID 0x%04x, serial: %s)\n", - action, matched_pid, matched_serial); - else - ux_info("%s device (PID 0x%04x)\n", action, matched_pid); + if (found) { + const char *action = matched_pid == 0x900e ? "Collecting crash dump from" : "Flashing"; + + libusb_free_device_list(devs, 1); + if (matched_serial[0]) + ux_info("%s device (PID 0x%04x, serial: %s)\n", + action, matched_pid, matched_serial); + else + ux_info("%s device (PID 0x%04x)\n", action, matched_pid); + return 0; + } + + libusb_free_device_list(devs, 1); + libusb_exit(NULL); + + return visible == 0 ? -ENODEV : -EBUSY; +} + +static int usb_open(struct qdl_device *qdl, const char *serial) +{ + int visible_prev = -1; + int visible; + int ret; + + for (;;) { + ret = try_usb_open(qdl, serial, &visible); + if (ret == 0) return 0; - } + if (ret == -EIO) + return -1; if (visible != visible_prev) { if (visible == 0) { ux_info("Waiting for EDL device\n"); + } else if (serial) { + ux_info("%d EDL device(s) visible, none match serial \"%s\"\n", + visible, serial); } else { - if (serial) - ux_info("%d EDL device(s) visible, none match serial \"%s\":\n", - visible, serial); - else - ux_info("%d EDL device(s) visible, none could be opened:\n", - visible); - for (i = 0; devs[i]; i++) { - dev = devs[i]; - if (libusb_get_device_descriptor(dev, &desc) < 0) - continue; - if (desc.idVendor != 0x05c6 || !usb_is_edl_pid(desc.idProduct)) - continue; - ux_info(" [bus %u, addr %u] PID 0x%04x\n", - libusb_get_bus_number(dev), - libusb_get_device_address(dev), - desc.idProduct); - } + ux_info("%d EDL device(s) visible, none could be opened\n", + visible); } visible_prev = visible; } - libusb_free_device_list(devs, 1); - - /* - * Tear down libusb before retrying so the next iteration - * builds a fresh device list from scratch. Without this, - * libusb's udev-based backend caches the enumeration and - * never notices newly attached devices - a problem in - * containerised environments where udev events are not - * available. - */ - libusb_exit(NULL); - usleep(250000); } - - return -1; } struct qdl_device_desc *usb_list(unsigned int *devices_found) From 6a950534bb9b1b7938d3dee81d17951a2a8e3d11 Mon Sep 17 00:00:00 2001 From: Igor Opaniuk Date: Thu, 14 May 2026 13:19:45 +0200 Subject: [PATCH 5/5] usb: drop per-device open warning during enumeration The libusb_open() call in usb_list() fails routinely on Windows because the QDLoader 9008 driver claims matched devices. Printing a warning per failing match drowns the actual rows that qdl list emits. The open-attempt path in usb_open_attempt() still warns, so the diagnostic survives where it is actionable. Signed-off-by: Igor Opaniuk --- usb.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/usb.c b/usb.c index 52e2f108..399909c4 100644 --- a/usb.c +++ b/usb.c @@ -376,10 +376,8 @@ struct qdl_device_desc *usb_list(unsigned int *devices_found) continue; ret = libusb_open(dev, &handle); - if (ret < 0) { - warnx("unable to open USB device"); + if (ret < 0) continue; - } ret = libusb_get_string_descriptor_ascii(handle, desc.iProduct, buf, sizeof(buf) - 1); if (ret < 0) {