47 Commits
Author SHA1 Message Date
Igor Opaniuk 1dcbdd0e6d Merge pull request #289 from igoropaniuk/refactor/edl-interface-identity
qdl: drop the EDL product id allowlist and match the EDL interface signature when enumerating
2026-07-23 11:20:46 +02:00
Igor Opaniuk 7c5a28d7dd Merge pull request #291 from igoropaniuk/fix/windows-test-warnings
Address oscompat warnings on Windows
2026-07-21 15:02:08 +02:00
Igor Opaniuk 760d15b4d0 oscompat: harden path_is_absolute against empty strings
The Windows branch reads path[1] guarded only by short-circuit
evaluation, which is correct at runtime but trips -Warray-bounds when
the function is inlined with a string literal shorter than two bytes,
as the oscompat unit tests do with "". It also passes a plain char
to isalpha(), which is undefined behavior for negative values.

Return early for the empty string, so the path[1] access is provably
in bounds, and feed isalpha() an unsigned char.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-18 09:18:37 +02:00
Igor Opaniuk a3fa75b761 vip: open signed tables in binary mode on Windows
The signed VIP table (DigestsToSign.bin.mbn) and the chained digest
tables are opened in vip_transfer_init() with a plain O_RDONLY. On
Windows/MSYS2 a descriptor opened without O_BINARY defaults to text
mode, which performs CRLF translation and treats a 0x1A (Ctrl-Z) byte
as end-of-file.

These tables are arbitrary binary data (e.g. the 8192-byte signed
table). As soon as read() hits a 0x1A byte or collapses a CRLF pair it
returns fewer bytes than the file size, so vip_transfer_send_raw() fails
its n == sb.st_size length check and aborts with "failed to read
binary". The VIP configure request then fails. On Linux there is no
text/binary distinction (O_BINARY is defined as 0) so the same code
works fine.

Add O_BINARY to both open() calls in vip_transfer_init(), matching what
write_digests_to_table() already does, so the tables are read verbatim
on Windows as well.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-17 17:29:15 -05:00
Igor Opaniuk 4c300a243c qdl: drop the EDL product id allowlist
EDL, crash-mode and ramdump devices enumerate with a growing set of
product ids, and every newly discovered variant needed another
allowlist entry - devices with ids not yet on the list were silently
invisible until someone updated it. Both backends now identify the
EDL function by the vendor-specific interface signature instead: the
libusb backend via the config descriptors, the QUD backend via the
Windows compatible ids derived from them.

Trust the vendor id alone in qdl_is_edl_device() and drop the
now-redundant pid gate in the QUD enumeration. The pid parameter
stays in the signature so call sites are unchanged; the interface
gates are what reject non-EDL Qualcomm devices from here on.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-17 16:21:49 +02:00
Igor Opaniuk 52ae194b09 qud: match the EDL interface signature when enumerating
The QUD backend accepts any Qualcomm COM port whose pid is on the
shared EDL allowlist, while the libusb backend identifies the EDL
function by its interface signature - vendor-specific class and
subclass with a known Sahara protocol. Windows exposes exactly that
triple without opening the device: PnP derives compatible ids of the
form "USB\Class_xx&SubClass_xx&Prot_xx" for every USB devnode,
including each interface of a composite device.

Require the same signature set the libusb backend matches (0x10, 0x11
and 0x13 on modern devices, 0xff on older targets), bringing the two
backends' notion of an EDL device in line and filtering Qualcomm COM
ports that expose no vendor-specific function at all.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-17 16:21:49 +02:00
Bjorn Andersson 16ca90ba9a nbdkit: add qdl plugin exposing a LUN as a block device
Inspecting or editing a device's flash previously meant dumping partitions
to files and reflashing them; there was no way to treat the live storage as
a random-access block device. Exposing a LUN over nbdkit lets ordinary host
tools - parted, mount, dd, imaging tools - operate on it directly.

Add an nbdkit plugin that uploads the firehose programmer and serves a
physical partition (LUN) as a read/write block device:

$ modprobe nbd
$ nbdkit -fv ./nbdkit-qdl-plugin.so programmer=prog_firehose_ddr.elf lun=0

meson builds it only when the nbdkit dependency is present (pass
-Dnbdkit=enabled to require it), producing nbdkit-qdl-plugin.so from the
shared qdl sources; the qdl binary is unaffected.

An EDL device can only be programmed once per session - uploading the
programmer is a one-shot transition and a reset reboots it out of firehose
- but nbdkit calls .open/.close several times per client (NBD_OPT_INFO to
probe, then NBD_OPT_GO to transfer). Programming per open and resetting per
close therefore made the first close reboot the board and every later open
time out. To solve this the plugin sets the device up once, on the first
open, keeps it configured across connections, and resets it only when the
plugin unloads.

Signed-off-by: Bjorn Andersson <bjorn.andersson@linaro.org>
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-17 09:36:28 +02:00
Igor Opaniuk fa4872777d qdl: move decode_sahara_config into its own library unit
decode_sahara_config() was defined in qdl.c - the command-line front-end -
but it is also called from contents.c, which is part of the shared qdl
library. The qdl binary links both, so it built and ran fine; but any
program that links the library without qdl.c ends up with an undefined
reference to decode_sahara_config().

That is exactly what happens to the nbdkit plugin, which is built from
the shared sources plus its own entry point and no qdl.c: loading the
resulting shared object aborts with "undefined symbol:
decode_sahara_config".

Move the function into a dedicated src/sahara_config.c that is part of
the shared source set, so the library is self-contained. It is kept in
its own translation unit rather than folded into contents.c so that the
contents unit tests, which #include contents.c and stub this function,
keep working.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-17 09:36:28 +02:00
Bjorn Andersson a8f21545f0 firehose: add block-level read/write/size interface
The firehose layer only drives file-oriented flashing: <program>/<read>
operations described by XML and executed as an op list. Nothing let an
in-process consumer configure the programmer on its own, learn how large a
LUN is, or read and write arbitrary sector ranges to and from memory
buffers.

The upcoming nbdkit plugin needs exactly that - random-access block I/O
against a live device - so add the missing primitives:

  - firehose_open()    configure the programmer without running any ops
  - firehose_getsize() report a LUN's sector size and count, parsed from
                       the <getstorageinfo> JSON response
  - firehose_pread()   read a sector range into a buffer, reusing the
                       chunked read path
  - firehose_pwrite()  write a buffer to a sector range, streaming it in
                       max_payload_size chunks
  - firehose_reset()   made non-static, out-of-tree callers can reuse it

Signed-off-by: Bjorn Andersson <bjorn.andersson@linaro.org>
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-17 09:34:14 +02:00
Yvonne Kaire f9f953bb74 usb: accept bInterfaceProtocol 0x13 for crash-mode devices
Sahara is identified by bInterfaceProtocol 0x10, 0x11, or 0x13 on
modern devices, and 0xff on older targets. Add 0x13 to the accepted
protocols and use hex notation consistently.

Signed-off-by: Yvonne Kaire <ykaire@qti.qualcomm.com>
2026-07-16 21:15:08 -05:00
Igor Opaniuk 4a96950c03 qdl: reject mixing input XML files with command verbs
The usage synopsis documents input XML files and the command verbs
(read/write/erase/sha256/flash/reset) as separate invocation forms,
but the argument loop accepted any mixture of them. Combining a bare
program XML with the reset verb, for example, appended the reset op
during parsing - before qdl_determine_bootable() adds the
setbootablestoragedrive op after the loop - so the device could be
told to reboot before its bootable storage drive was set. The same
mixture on the provisioning path silently performed no reset at all.

Rather than order the two against each other, reject the combination:
track whether a file-type and a verb-type argument have both been
seen and error out when they have. This keeps each documented form
self-consistent and makes the reset verb usable only on its own or
alongside other verbs, where ordering is well defined.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-16 17:29:18 -05:00
Igor Opaniuk cb5331ac72 firehose: harden response, geometry and retry handling
Several long-standing robustness gaps in the Firehose layer:

- firehose_write() keyed its timeout retry on errno, but the qdl_write()
  backends report the timeout through the return value and never touch
  errno, so the retry could misfire and a stale errno leaked into the
  return code. Key both the retry and the error return on the return
  value, and free the serialized document on the VIP send-table error
  path.

- firehose_configure_response_parser() leaked the response value on the
  ACK path when MaxPayloadSizeToTargetInBytesSupported was absent; free
  it.

- firehose_program() and firehose_issue_read() divided by a sector size
  that could be zero; guard against it and fail cleanly. Route the
  VIP send-table failure through the existing cleanup path instead of
  returning with the document and buffer still allocated.

- firehose_detect_and_configure() ignored its skip_storage_init argument
  and always passed false; honor the caller's request.

- Normalize a few error returns to -1 so callers see a consistent
  failure status.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-16 17:28:34 -05:00
Igor Opaniuk b2e63e2cdd firehose: reserve a byte for the NUL terminator on a full read
firehose_read() read up to sizeof(buf) bytes and then wrote a NUL at
buf[n], overflowing the stack buffer by one byte whenever a read filled
it - routine on stream transports that concatenate responses. Read at
most sizeof(buf) - 1 bytes.

Fixes: da68b4add8 ("qdl: Support for program and patch")
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-16 17:28:34 -05:00
Igor Opaniuk c3f4389f50 firehose: report a failed final programming response
firehose_program() logged a failed or NAK'd final programming response
but then returned 0, so firehose_execute_ops() carried on and qdl
exited successfully with the partition left unwritten. Return an error
on a non-ACK final response.

Fixes: a9ec681705 ("program: Refactor the global "programs" list")
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-16 17:28:34 -05:00
Igor Opaniuk 8fc29dec5b sahara: fix ramdump crash and harden device-driven parsing
qdl ramdump passes a NULL images array, but the READ_DATA handlers and
the done-pending quirk indexed it unconditionally and crashed when the
device requested an image. Treat NULL images as "none". Also make the
offset+len range check overflow-safe, use a signed type for write()'s
result, propagate I/O errors from the ramdump helper, bound the region
loop by the bytes actually read, and reject region filenames that
contain a path separator.

Fixes: ff1a3bb1d3 ("ramdump: Make ramdump a qdl subcommand")
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-16 17:28:34 -05:00
Igor Opaniuk 637b48f3d0 gpt: use 64-bit sector arithmetic and harden entry parsing
GPT LBAs are 64-bit but were narrowed to unsigned int, so a partition
beyond 2 TB wrapped and a name-addressed write/erase hit the wrong
sectors. Carry start_sector/num_sectors as uint64_t and range-check
before storing num_sectors. Also reject a bad part_entry_size, fix an
off-by-one in the UTF-16 surrogate handling that read past the name
buffer, and initialise the sim-mode outputs.

Fixes: 60db50966d ("qdl: Extend read/write support to accept GPT partition names")
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-16 17:28:34 -05:00
Igor Opaniuk 2b5b3ce0f9 qdl: validate image ids and programmer-archive entry names
The image-id checks used "id == 0", but id comes from strtoul(), so an
id above LONG_MAX parsed negative, passed the check and indexed the
sahara_image array out of bounds.

Reject id <= 0 and also require the cpio entry name to be non-empty
and NUL-terminated before strtok_r()/strcmp(), and treat an empty
image_path as invalid rather than calling strlen(NULL).

Fixes: 4bdcaaec1a ("qdl: Support for "programmer archives"")
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-16 17:28:34 -05:00
Igor Opaniuk 016facd205 program: fix sparse program list corruption and guard geometry
A program tagged sparse="true" with no sparse header but a matching
size was appended to the op list twice - once in program_load_sparse()
and again by the caller - self-linking the node (infinite loop /
use-after-free) and dropping its filename so it is never flashed.

Return a distinct code so the caller appends it once. Also guard the
sparse path against a zero SECTOR_SIZE_IN_BYTES (SIGFPE) and empty
start_sector/label attributes (NULL deref).

Fixes: 6a67b6bd64 ("sparse: Parse sparse files at load time")
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-16 17:28:34 -05:00
Igor Opaniuk 9ad4ef1509 flashmap: report failure when flashmap.json cannot be loaded
flashmap_load() left "ret" holding the 0 from a successful open, so a
failed load or JSON parse returned success and qdl continued with an
empty op list instead of aborting.

Set ret to -1 on both paths, and NULL-check the configure-op allocation.

Fixes: 81b68cbdb2 ("flashmap: Introduce flashmap support")
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-16 17:28:34 -05:00
Igor Opaniuk a95250d3d4 zipper: report failures and avoid double-buffering fill chunks
zipper_write() reused "ret" as a loop scratch variable, so several
error paths jumped to the discard label without resetting it to -1:
create-zip then exited 0 with no output file.

Set ret before those gotos, also hand sparse fill buffers straight to
libzip instead of copying them a second time, which could double peak
memory on large fills.

Fixes: cc6d9c3596 ("zipper: Introduce support for creating a flashmap zip file")
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-16 17:28:34 -05:00
Igor Opaniuk 9311e31e1b qud: apply the shared EDL identity policy when enumerating
win_enumerate_qcom() accepted any present COM port whose hardware id
starts with USB\VID_05C6&PID_, i.e. any Qualcomm serial port the
QDLoader driver (or another vendor serial driver) has bound. That
over-matches: the diag or NMEA port of a phone in normal composite
mode also enumerates this way, and would be counted by
qud_probe_present(), listed by 'qdl list' and even opened and driven
by qud_open() as if it were an EDL device.

Gate the enumeration with qdl_is_edl_device(), the same identity
policy the libusb backend applies. The backends can no longer drift
apart on what counts as an EDL device: extending the policy (for
example with a ramdump-specific pid) is a one-line change in qdl.h
that both transports pick up.

Devices whose hardware id carries no parsable pid were previously
listed with pid 0 and are now skipped.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-16 16:51:54 -05:00
Igor Opaniuk 8cabf4ed64 usb: consolidate device matching in usb_match_device()
usb_open_device() interleaved matching and acting: the interface
signature check, the device open, the serial filter and the claim all
lived in one loop, with the device-level serial check nested inside
the per-interface iteration. A serial mismatch was therefore
discovered only after opening the device, and rediscovered - open,
read, close - for every matching interface.

Introduce usb_match_device(), which answers the complete device-level
question on an opened candidate: does it carry the requested serial,
and does it expose an EDL interface? usb_open_device() becomes a
plain sequence: open, match, detach and claim, fill in the endpoint
pair. The match helpers now form one family: usb_match_device() is
usb_match_serial() (renamed from usb_match_usb_serial() to drop the
stutter) plus usb_match_edl_interface() over the active config.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-16 16:51:54 -05:00
Igor Opaniuk b3adc41453 usb: share candidate enumeration between open and list paths
usb_open_once() and usb_list() each carried their own copy of the
same enumeration loop: walk the bus, fetch descriptors, apply the EDL
identity gate. On top of that, usb_open_once() applied the gate
before handing a device to usb_open_device(), which then fetched the
same descriptor and applied the same gate a second time; the
duplicated check let the two layers disagree about what counts as an
EDL candidate.

Extract the loop into usb_enumerate_edl_devices(), which hands every
candidate to a callback and reports how many it saw, and pass the
already-fetched descriptor down to usb_open_device() so candidate
selection happens exactly once, in exactly one place. Only what
happens to a matched candidate now differs between the paths: the
open path claims the first candidate that opens, while the list verb
reads the descriptors of all of them without claiming anything
(listing must never detach a kernel driver or claim an interface, as
that could disturb a concurrently running flash). usb_list() no
longer sizes its result array from the raw device count but grows it
per matched candidate.

The libusb session stays with the callers, since their lifetimes
differ: usb_open_once() must keep the session alive for the handle it
returns, while usb_list() tears it down before returning.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-16 16:51:54 -05:00
Igor Opaniuk 897eafa99c usb: reuse usb_read_serial() in usb_list()
usb_list() open-codes the iProduct "_SN:" parsing that
usb_read_serial() already implements for the open path. Reuse the
helper so the serial-number format knowledge exists once.

Two edge cases change deliberately: a device whose iProduct string
cannot be read is now listed with a "(none)" serial instead of
being skipped, and a serial longer than the 16-byte descriptor field
is truncated (as the open path already does) instead of hiding the
device. Suppressing a present device was the more surprising
behavior for a listing verb.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-16 16:51:54 -05:00
Igor Opaniuk 29629b784d usb: extract EDL device and interface matching helpers
usb_open_device() mixes three concerns in one hundred-line function:
deciding whether a USB device is an EDL candidate, locating the
vendor-specific EDL interface with its bulk endpoint pair, and
actually opening and claiming it. usb_open_once() and usb_list()
additionally duplicate the vid/pid gate inline, so the answer to
"what does qdl consider an EDL device" is currently scattered over
three call sites that can (and in the past did) drift apart.

Centralize the policy:

 - qdl_is_edl_pid()/qdl_is_edl_device() in qdl.h hold the shared,
   transport-agnostic identity check (Qualcomm vid, known EDL pids).
   Living in the common header, the same policy is available to the
   QUD backend, which parses pids from Windows hardware ids.
 - usb_is_edl_device() wraps it for libusb descriptor types and is
   now the single gate used by the open, scan and list paths.
 - usb_match_edl_interface() checks one interface descriptor for the
   ff/ff class/subclass signature and a known protocol, and extracts
   the bulk endpoint pair.

usb_open_device() shrinks to the actual open/claim sequence. Future
changes to device acceptance (for example crash-mode devices exposing
a different interface protocol or pid) become a one-line change in
exactly one place, applied consistently everywhere.

No functional change.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-16 16:51:54 -05:00