diff --git a/auto.c b/auto.c new file mode 100644 index 0000000..837927d --- /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/firehose.c b/firehose.c index 9aebbec..5a200ff 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; @@ -699,31 +759,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 131c620..2e1684f 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" @@ -14,13 +16,21 @@ 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(); + + if (type == QDL_DEVICE_AUTO) + return auto_init(); + return NULL; } 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) @@ -45,14 +55,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/meson.build b/meson.build index fff5ff2..c774841 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 @@ -55,6 +62,7 @@ shared_lib = static_library('qdl_common', common_sources, dependencies: common_d # --- qdl executable --- qdl_sources = files( + 'auto.c', 'qud.c', 'firehose.c', 'io.c', 'qdl.c', 'patch.c', 'program.c', 'read.c', 'sha2.c', 'sim.c', 'ufs.c', 'usb.c', @@ -71,7 +79,7 @@ qdl_exe = executable('qdl', # --- qdl-ramdump executable --- ramdump_sources = files( - 'ramdump.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 48e4784..5f7ae4a 100644 --- a/qdl.c +++ b/qdl.c @@ -102,6 +102,36 @@ static int detect_type(const char *verb) return type; } +/* + * 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")) { + *out = QDL_DEVICE_AUTO; + return 0; + } + + if (!strcmp(name, "usb")) { + *out = QDL_DEVICE_USB; + return 0; + } + + if (!strcmp(name, "qud")) { + *out = QDL_DEVICE_QUD; + return 0; + } + + return -1; +} + enum qdl_storage_type decode_storage(const char *storage) { @@ -461,6 +491,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"); @@ -476,33 +507,47 @@ 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; } +/* 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_AUTO; int ret = 0; int opt; @@ -511,6 +556,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} }; @@ -529,6 +575,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; @@ -548,9 +598,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(); @@ -710,7 +762,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'}, @@ -727,6 +779,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} }; @@ -779,6 +832,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 a692557..240d850 100644 --- a/qdl.h +++ b/qdl.h @@ -47,6 +47,15 @@ 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 { @@ -75,6 +84,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 { @@ -92,12 +113,30 @@ 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); 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; @@ -107,6 +146,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 new file mode 100644 index 0000000..c8ff5ca --- /dev/null +++ b/qud.c @@ -0,0 +1,566 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. + * + * 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; +} + +/* + * 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 + * *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; +} + +int qud_probe_present(void) +{ + return 0; +} + +struct qud_device_desc *qud_list(unsigned int *devices_found) +{ + *devices_found = 0; + return NULL; +} + +#endif diff --git a/ramdump.c b/ramdump.c index 2f6b00e..718f951 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 6602cb9..399909c 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]; - visible++; + if (libusb_get_device_descriptor(dev, &desc) < 0) + continue; + if (desc.idVendor != 0x05c6 || !usb_is_edl_pid(desc.idProduct)) + continue; - 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; - } + 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; } + } + + if (visible_out) + *visible_out = visible; - if (found) { - const char *action = matched_pid == 0x900e ? "Collecting crash dump from" : "Flashing"; + 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); + 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) @@ -352,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) {