Commit Graph
100 Commits
Author SHA1 Message Date
Igor Opaniuk ced92634a8 Merge pull request #293 from igoropaniuk/doc/changelog-v2.8
Prepare for the v2.8 release
2026-08-02 10:04:08 +02:00
Igor Opaniuk 0a443ad948 doc: add v2.8 changelog
Populate the changelog for the upcoming v2.8 release, per step 1 of
the release process. Beyond the mirrored list of merged pull requests,
the entry introduces two curated sections: Highlights, summarizing the
release from a user's perspective, and Breaking changes, calling out
the dropped qdl-ks/qdl-ramdump standalone binaries and the rejection
of mixed XML-and-verb invocations that scripts and packaging may rely
on.

Tested-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com> (RB3Gen2 secure, Linux)
Tested-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com> (Uno Q, Linux)
Tested-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com> (Uno Q, Win)
Tested-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com> (RB3Gen2, MacOS)
Tested-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com> (Radxa Q8B, SC8280XP, Linux)
Tested-by: liuth <lth@t-chip.com.cn> (IQ-9075)
Tested-by: Christopher Obbard <chris.obbard@oss.qualcomm.com> (RB3Gen2, Debian Unstable amd64)
Tested-by: Shawn Guo <shengchao.guo@oss.qualcomm.com> (IQ10 RRD, Windows)
Tested-by: Pankaj Patil <pankaj.patil@oss.qualcomm.com> (Glymur CRD, Linux - Ubuntu 24.04.4 LTS)
Tested-by: Ananthu C V <ananthu.cv@oss.qualcomm.com> (Hamoa CRD, Ubuntu 24.04.4 LTS)
Tested-by: Shoudi Li <shoudil@qti.qualcomm.com> (Hamoa/Purwa IoT EVK, Ubuntu 24.04.4 LTS)
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-08-02 09:53:52 +02:00
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 f72f1902e3 firehose: do not probe sector size when storage init is skipped
firehose_try_configure() discovers the storage sector size by reading
sector 1 of partition 0 with each candidate size. The provisioning
path also runs this probe, and on a device with unprovisioned UFS the
reads fail inside the programmer with "OPEN handle NULL and no error,
weird": there is no LU 0 to open. The programmer does not recover
from that failed open - the subsequent provisioning commit fails with
"ufs_open(0, WLUN_DEV) failed for configure" and the device is left
unprovisioned, while the same programmer, device and XML provision
fine through QFIL, which sends no reads before the ufs tags.

Gate the probe on skip_storage_init: the probe reads force the
programmer to initialize the very storage the caller declared
uninitialized, and provisioning - the only path passing
skip_storage_init - expresses UFS geometry in kilobytes and never
needs a sector size. Reported in the github issue [1].

[1] https://github.com/linux-msm/qdl/issues/294

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-23 10:20:55 +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 fd0e86d542 tests: warn and skip overly-long paths in rmtree
rmtree() concatenates the directory path and entry name into a
PATH_MAX buffer without checking for truncation, which -Wformat-
truncation flags on the Windows build where PATH_MAX is 260. A
truncated path would also make the cleanup delete the wrong file in
principle.

Check the snprintf() result and skip entries that do not fit; using
the return value also satisfies the warning. A skipped entry means
the parent rmdir() fails with ENOTEMPTY and the workdir lingers, so
report the skip on stderr instead of hiding it - the message names
the entry, making a leftover workdir traceable to its cause.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-21 14:57:42 +02:00
Igor Opaniuk e0ad540d5a Merge pull request #292 from igoropaniuk/chore/markdownlint-all
Lint all markdown files and extend the release documentation
2026-07-21 14:23:22 +02:00
Igor Opaniuk 89bfed3976 README: replace non-ASCII punctuation
The WSL2 section carries a typographic apostrophe and em dashes;
project documentation is plain ASCII (see AGENTS.md). Replace them
with their ASCII equivalents.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-21 14:03:24 +02:00
Igor Opaniuk dd6ab15ec9 doc: document stable branch maintenance in RELEASING.md
Describe the kernel-style stable branches (qdl-X.Y.y) agreed for
shipping urgent fixes independently of master: cut the branch from
the release tag immediately after tagging, backport fixes that landed
on master with cherry-pick -x through stable-targeted pull requests
(CI runs on those unchanged), and cut point releases (vX.Y.Z) from
the branch with the same flow as a normal release.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-21 14:03:24 +02:00
Igor Opaniuk eca982a599 scripts: lint every tracked markdown file
The markdown lint wrapper names README.md and docs/ explicitly, so
documents like AGENTS.md, CONTRIBUTING.md and RELEASING.md have never
been linted and accumulated findings silently.

Derive the file list from git ls-files instead, excluding
CHANGELOG.md - generated by the release process, where long
release-note URLs and per-release repeated section headers are part
of the format - and CLAUDE.md, a symlink to the already-linted
AGENTS.md. New documents are picked up automatically.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-21 14:03:24 +02:00
Igor Opaniuk 6c47f1cb7c doc: fix markdown lint findings in AGENTS.md and RELEASING.md
Preparation for linting every markdown file in the tree: surround the
sign-off example in AGENTS.md with the blank lines the linter wants,
and flatten the RELEASING.md table of contents - its single top-level
entry only repeated the document title, and the nested indentation
tripped the linter's list-indent rule under kramdown.

While at it, replace the literal Signed-off-by example with the
'git commit -s' command that produces it. checkpatch validates
anything shaped like a signature tag on any patch line - added,
removed and context lines alike - so any patch touching these lines
trips 'Unrecognized email address' on the $(git config ...)
placeholders. Documenting the command instead keeps trailer-shaped
text out of the file entirely, with no checkpatch exceptions needed.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-21 14:03:19 +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
Igor Opaniuk f312075fee Merge pull request #283 from igoropaniuk/feat/nbdkit
nbdkit: expose a device LUN as a block device
2026-07-17 09:48:14 +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
Igor Opaniuk 81a061df4a github: allow 75-character commit body lines
Kernel-style commit bodies occasionally wrap a sentence or an
example command a few characters past 72, which tripped
commitlint's body-max-line-length even though the messages are
otherwise fine. Raise the body limit from 72 to 75 to match the
wrapping already used in the tree.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-17 09:34:14 +02: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 4ef8df7db2 tests: extend HIL test suite
Extend the hardware-in-the-loop suite with self-contained steps that
fit the existing round-trip model:

- list devices: enumerate the attached EDL device with the list verb
  and require a vid:pid line in the output, covering device discovery
  over real USB.

- chipinfo via Sahara: read the chip identity (serial number, HW ID
  and OEM PK hash) with the chipinfo verb. The step runs before the
  programmer upload because chip identity is only available while the
  device still speaks Sahara; the verb switches the device back to
  image-transfer mode on exit, so the subsequent flash still gets its
  Sahara HELLO. SKIPs when a programmer is already running.

- create-zip and flash: package a build from a contents.xml with the
  create-zip verb, then flash the produced zip, covering the zip
  writer and the zip flashing path end to end. Gated on
  QDL_HIL_CONTENTS.

- flash sparse build: flash a bundle that uses Android sparse images,
  covering the sparse decoding path against real storage. Gated on
  QDL_HIL_SPARSE.

- NAK on bad partition: a read of a nonexistent physical partition
  must fail, verifying that a device NAK is reported as an error
  instead of silently succeeding.

- sha256 by partition name: digest a partition addressed by its GPT
  name rather than by partition and sector numbers, covering GPT name
  resolution against the device.

- write chunked: rewrite the partition image with an overridden
  --out-chunk-size=16384, covering the chunked transfer path.

- verify write integrity: compare the device-computed partition
  digest against a local sha256 of the previously read-back and
  rewritten image, proving the read/write round trip is bit-exact.

- reset board: switch the final step to the new reset verb, covering
  the explicit device-reset path instead of relying on the implicit
  reset after a dummy 1-sector read.

The two gated steps SKIP when their environment variable is unset,
and every step SKIPs without hardware, so a plain meson test stays
green.

Example invocation against an attached EDL device:

  D=<path_to_build>
  QDL_HIL_BUILD=$D QDL_HIL_CONTENTS=$D/contents.xml \
    QDL_HIL_STORAGE=ufs meson test -C build --suite=hil

   1/18 hil - qdl:HIL list devices             OK          0.05s
   2/18 hil - qdl:HIL chipinfo via Sahara      OK          0.4s
   3/18 hil - qdl:HIL flash whole build        OK         29.03s
   4/18 hil - qdl:HIL create-zip and flash     OK         79.62s
   5/18 hil - qdl:HIL flash sparse build       SKIP        0.02s
  ...
  18/18 hil - qdl:HIL reset board              OK          3.68s

  Ok:                17
  Fail:              0
  Skipped:           1

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-16 17:15: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
Igor Opaniuk b8e5a2f525 usb: rename open-path functions to clarify layering
try_usb_open() and usb_try_open() differ only in word order, which
makes the libusb open path needlessly hard to follow: one is a full
enumeration pass over the bus, the other opens a single candidate
device.

Rename them so the names encode the layering instead of hiding it:

  usb_open()        - backend .open op, wait loop (unchanged)
  usb_open_once()   - one enumeration pass (was try_usb_open())
  usb_open_device() - open a single candidate (was usb_try_open())

Document the resulting call chain, including the auto backend that
sits on top of usb_open_once() on Windows, at the top of usb.c.

No functional change.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-16 16:51:54 -05:00
Igor Opaniuk e106964206 usb: release libusb resources on all usb_list() exit paths
usb_list() initializes a libusb session but never calls
libusb_exit() on any path, and the empty-bus early return also leaks
the device list itself. This is harmless for the one-shot 'qdl list'
verb, but it is inconsistent with usb_open_once(), which sets up and
tears down its session on every pass, and it trips leak checkers.

Free the device list and close the session on all exit paths.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-16 16:51:54 -05:00
Igor Opaniuk 59aa99b04e Merge pull request #280 from igoropaniuk/feat/chipinfo_sahara
sahara: add chipinfo command to read chip identity
2026-07-15 14:53:21 +02:00
Igor Opaniuk 870e6db020 Merge pull request #281 from edsj-sony/master
qdl: Add reset verb for device reset
2026-07-14 10:03:00 +02:00
Igor Opaniuk 47f420c011 sahara: add chipinfo command to read chip identity
QDL only ever drives Sahara in its image-transfer and memory-debug
modes. Sahara also defines a command mode that lets the primary
bootloader be queried for the device's identity before any programmer
is uploaded. Expose that as a new "chipinfo" subcommand.

It enters command mode by requesting SAHARA_MODE_COMMAND in the HELLO
response and echoing the device's advertised protocol version, then
drives EXECUTE transactions to read the chip serial number, the HW ID
(broken out into MSM_ID / OEM_ID / MODEL_ID) and the OEM PK hash.
Version 3 targets no longer answer MSM_HW_ID_READ, so the same fields
are recovered through READ_CHIP_ID_V3 on those devices.

Command mode is left by switching back to image-transfer mode, which
returns the device to its power-on HELLO state and keeps it usable for
a subsequent query or flash. A Sahara reset is deliberately avoided
here: on some targets it leaves the device enumerated on PID 0x9008 but
no longer answering Sahara until it is power-cycled back into EDL.

This is handy for identifying an attached target and, in particular,
for troubleshooting secure-boot provisioning: the fused OEM PK hash and
OEM ID can be read straight from the PBL without loading a programmer.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-13 19:25:46 +02:00
Igor Opaniuk 13a0c76626 usb: print "Talking to" instead of "Flashing" on open
The success line for a matched EDL device is emitted at open time,
before any data is transferred, and it is shared by every operation
that opens a device - including read-only ones like the new chipinfo
query. Labelling that moment "Flashing" is inaccurate: nothing is being
flashed yet, and for a pure query nothing will be.

Use the operation-neutral verb "Talking to", which is accurate at
connect time regardless of what follows. The crash-dump wording for the
0x900e product ID is left untouched, as it remains specific and correct.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-09 17:53:21 +02:00
Igor Opaniuk a00d81bc63 Merge pull request #274 from yangh/master
usb: recognize 0x90db as an EDL ramdump product ID
2026-07-08 11:16:43 +02:00
Igor Opaniuk 6aaa17edaf doc: add CLAUDE.md symlink to AGENTS.md
Claude Code looks for its project instructions in CLAUDE.md and
ignores AGENTS.md.

Add a CLAUDE.md symlink pointing to AGENTS.md so Claude Code reads
the same instructions without duplicating the content.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-03 10:41:56 +02:00
Igor Opaniuk 18f5f07112 doc: add AGENTS.md guide for automation agents
Automation agents contributing to QDL need a single, machine-oriented
reference for how to build, test, and format their changes. Scattering
this information across the README and CONTRIBUTING.md makes it easy for
agents to miss the project conventions.

Add AGENTS.md consolidating the build prerequisites, the pre-commit
build and check targets, the Linux kernel coding style requirement, the
subsystem-tagged commit convention with a mandatory rationale body, the
ASCII-only rule for code, docs, and commit messages, and the mandatory
S-o-b trailer. It points back to CONTRIBUTING.md so the two stay
in sync.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-07-03 09:02:58 +02:00
Igor Opaniuk 3effa725f4 Merge pull request #278 from igoropaniuk/docs/releasing_and_changelog
doc: add RELEASING.md describing the release process
2026-07-02 18:33:52 +02:00
Igor Opaniuk c065108d75 doc: add release and packaging badges to the README
The README advertised only the license and CI build status, leaving
the project's availability invisible: qdl ships a tagged GitHub
release and is packaged across many distributions (Debian, Arch,
Fedora, Ubuntu, Nix, ...).

Add three badges near the title:
 - latest GitHub release, so the current version is visible at a glance;
 - Debian package version, linking to the Debian package tracker;
 - Repology packaging status, summarizing distro coverage in one badge.

Also fix the existing build badge link, which pointed at the badge image
instead of the workflow's runs page.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-06-30 16:45:49 +02:00
Igor Opaniuk 1b9e6894b9 github: package self-contained binaries on Linux and macOS
The release artifacts were not self-contained. qdl links libxml2 and
libzip (and, on Linux, their transitive closure), but packaging copied
only a subset of those libraries, so qdl failed to start on hosts
without them installed. Inspecting the uploaded artifacts:

  Artifact         Missing                       Status
  ---------------  ----------------------------  ------
  ubuntu-22-x64    libxml2.so.2, libzip.so.4     broken
  ubuntu-22-arm64  libxml2.so.2, libzip.so.4     broken
  ubuntu-24-x64    libxml2.so.2, libzip.so.4     broken
  ubuntu-24-arm64  libxml2.so.2, libzip.so.4     broken
  macos-arm64      libzip.5.dylib (Homebrew)     broken
  macos-intel      libzip.5.dylib (Homebrew)     broken
  windows-x64      -                             ok
  windows-arm64    -                             ok

On Linux only libusb was copied; libxml2 and libzip (plus their closure:
libz, liblzma, libzstd, libbz2, libcrypto, libudev) were left to the
host. On macOS libzip was never handled, so the binary kept an absolute
Homebrew path to libzip.5.dylib. Windows already bundled the full set.

Bundle the complete dependency closure on Linux (every ldd entry except
the C runtime and dynamic loader) and set an $ORIGIN RUNPATH on the
binary and each bundled library, since RUNPATH is not inherited by
transitively-loaded libraries.

On macOS, use dylibbundler to recursively copy the non-system dylibs and
rewrite the load commands to @executable_path, replacing the
hand-maintained install_name_tool calls that missed libzip.

Both jobs now fail if any non-system library remains unresolved after
packaging.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-06-27 20:49:13 -05:00
Igor Opaniuk af1fcc243f doc: add RELEASING.md describing the release process
Cutting a release has so far been an informal, tribal-knowledge effort:
the exact steps, the tag conventions, which hardware/driver combinations
must be validated, and how the GitHub release is assembled from CI
artifacts.

Add RELEASING.md to formalize and standardize that process so any
maintainer can cut a release the same way. It documents, end to end:

  - how to populate CHANGELOG.md from the merged PRs and start testing;
  - opening the changelog PR and tagging release candidates;
  - the validation matrix (HIL suite across Windows+QUD, Windows+WinUSB,
    Linux and macOS) plus manual VIP validation, and rc-tag iteration;
  - applying the final immutable release tag (with a note on the
    annotated vs lightweight tag history);
  - creating the GitHub release from the existing Buildtest artifacts,
    with release notes extracted straight from CHANGELOG.md.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-06-27 18:57:12 +02:00
Igor Opaniuk 51dff698cb doc: add CHANGELOG.md mirroring the GitHub releases
Provide an in-tree changelog so the release history is available without
visiting GitHub. Each entry corresponds to a published GitHub release
(v2.4 onwards), listing the merged pull requests and new contributors,
with PR references rendered as #NNN links.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-06-27 17:23:47 +02:00
Igor Opaniuk 187c4496a3 CONTRIBUTION: split contribution guide out of README
The README has grown to cover build instructions, a wide range of
usage workflows (EDL, WSL2, VIP, multi-programmer targets, crash
dumps, kickstart, ...) and the contribution policy in a single file,
which makes it long and harder to navigate.

Move the contribution documentation - PR workflow, Linux kernel coding
style, the checkpatch (check-cached/check-range/check) and markdown-lint
targets - into a dedicated CONTRIBUTING.md. GitHub surfaces that file
automatically on the new-PR and issue pages, so contributors find the
guidance without scrolling through the README. The README keeps a short
Contributing section linking to it.

No content was changed in the move; only its location.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-06-27 15:59:24 +02:00
Igor Opaniuk fd0f3436c7 Merge pull request #265 from igoropaniuk/feat/skipblock_chunked_digest
firehose: chunk skipblock digest into bounded sub-regions
2026-06-27 15:57:41 +02:00
Igor Opaniuk d8cef1ace5 README: use consistent dash bullets for unordered lists
mdl's MD004 rule defaults to "consistent" style, taking the first
unordered list's marker as the expected one for the whole file. The
first list used "*" while every other list used "-", so the dash lists
were flagged. Switch the three "*" bullets to "-" to match the dominant
style and satisfy the linter.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-06-27 10:07:26 +02:00
Igor Opaniuk 29cf915799 Merge pull request #269 from igoropaniuk/ramdump/create-output-dir 2026-06-27 09:05:19 +02:00
Igor Opaniuk a1d5e23232 tests: fix test_contents_selectors link failure at -O0
test_contents_selectors.c pulls in contents.c via #include, and
contents.c calls firehose_alloc_op(). The test target links neither
firehose.c nor libqdl_common, so the only reason it linked at all is
that at -O2 the compiler drops the (unreachable in this test) call.
At -O0 the call survives and the link fails:

  src/contents.c:899: undefined reference to `firehose_alloc_op'

Provide a firehose_alloc_op() stub, like the other contents tests
already do, so the test links regardless of optimization level.

Closes: https://github.com/linux-msm/qdl/issues/260

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-06-22 12:34:40 +02:00
Igor Opaniuk 533d176269 tests: add a hardware-in-the-loop (HIL) test suite
Before tagging a release we want to exercise qdl against real
hardware - flashing a full build, reading partitions, skipblock,
sha256, erase and write - but there is no automated way to do that
today. Add a "hil" meson suite that drives the built qdl binary
against an attached EDL device, so a release can be validated with a
single command. It is a stop-gap that simplifies testing until we have
proper CI HIL coverage via LAVA.

Each step is its own test in the "hil" suite. The steps are stateful,
so they run serially and in order (is_parallel:false plus a descending
priority): flash the build, read the EFI partition / GPT / address
forms, re-flash with --skipblock=sha256, run the sha256 command, erase
and write the EFI partition back, and finally reboot. Every step runs
with --skip-reset except the reboot. The erase/write pair round-trips
the partition's own readback, so nothing is left wiped, and once one
step fails the rest of the suite stops.

The suite is opt-in: the build and storage type are passed via the
environment, and when unset every step exits 77 (SKIP), so a plain
"meson test" / CI run stays green without hardware.

Usage:

  $ QDL_HIL_BUILD=/path/to/build QDL_HIL_STORAGE=ufs \
        meson test -C build --suite hil -v

   1/10 hil - qdl:HIL flash whole build       OK   21.63s
   2/10 hil - qdl:HIL read EFI partition      OK   10.50s
   3/10 hil - qdl:HIL read GPT table          OK    2.47s
   4/10 hil - qdl:HIL read address P/S+L      OK    2.46s
   5/10 hil - qdl:HIL read address P/name     OK   11.17s
   6/10 hil - qdl:HIL flash skipblock sha256  OK   22.12s
   7/10 hil - qdl:HIL sha256 command          OK    2.47s
   8/10 hil - qdl:HIL erase EFI partition     OK    4.97s
   9/10 hil - qdl:HIL write EFI partition     OK   10.18s
  10/10 hil - qdl:HIL reboot board            OK    3.64s

  Ok: 10   Fail: 0

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-06-21 17:43:25 +02:00
Igor Opaniuk 36cf435495 firehose: tolerate zero-length packets while reading sector data
firehose_issue_read() treated a 0-byte qdl_read() return as a fatal
"unexpected EOF" and aborted the transfer. On a USB bulk-in pipe a
0-byte completion is not EOF, it is a zero-length packet (ZLP) - a
legitimate USB-level event that the transport layer can surface to
user space.

In practice this trips on Windows with the libusb (WinUSB) backend
when reading a large partition: after the device ACKs a <read> with
rawmode="true" and transitions from XML responses to the raw binary
phase, it emits a ZLP before the first sector chunk. WinUSB returns
that as libusb_bulk_transfer() success with actual=0, usb_read()
forwards 0, and the inner accumulation loop in firehose_issue_read()
treated it as a terminated stream and bailed out. The same workload
succeeds on Linux because the usbfs path does not surface the
transitional ZLP on the rawmode boundary, so the first qdl_read()
already returns the full payload.

Small reads (the per-sector GPT loads at the start of every session
go through the same loop) did not hit this: their request size is a
single sector, the device sends the bytes immediately, and there is
no preceding boundary ZLP to consume. The failure only showed up on
the first big payload, e.g.:

  $ qdl.exe --debug --storage=emmc prog_firehose_ddr.elf \
        read 0/rootfs rootfs.image
  ...
  FIREHOSE READ: <response value="ACK" rawmode="true" />
  unexpected EOF while reading sector data

Treat n == 0 as "no payload yet, keep reading" instead of EOF and
continue the inner loop. A genuine stall is still caught: qdl_read()
returns -ETIMEDOUT once the 30s timeout passed to libusb expires,
and that path already errors out via the n < 0 arm just above.

Fixes: 0512c0b983 ("firehose: parse multi-document and rawmode-trailed reads")
Closes: https://github.com/linux-msm/qdl/issues/132
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-06-21 12:55:24 +02:00
Igor Opaniuk d3ff3e028b tests: cover oscompat path helpers
Add a cmocka unit test for the header-only helpers in oscompat.h:

 - path_is_absolute(), checking POSIX and (under _WIN32) drive-letter
   and UNC paths;
 - qdl_mkdir_p(), checking recursive creation, idempotency on an
   existing directory, trailing slashes, the "." and empty-path cases,
   the ENAMETOOLONG guard, and failure when a path component is not a
   directory.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-06-21 11:47:44 +02:00
Igor Opaniuk 860d8fa979 ramdump: create the output directory if it does not exist
"qdl ramdump -o <dir>" writes each downloaded region to <dir>/<file>,
but it never created <dir> itself. When the directory did not exist
every open() failed with ENOENT, and the user only saw a confusing
per-file error with no hint that the missing output directory was the
actual problem:

  $ qdl ramdump -o ./ramdump
  Waiting for EDL device
  Collecting crash dump from device (PID 0x900e, serial: 8AE002FE)
  qdl: failed to open "OCIMEM.BIN": No such file or directory

Create the output directory, and any missing parents like "mkdir -p",
before the download starts via a new portable qdl_mkdir_p() helper, so
the ramdump path is guaranteed to exist. Failing to create it is now
reported explicitly instead of surfacing as an opaque open() error.

Closes: https://github.com/linux-msm/qdl/issues/224

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-06-21 11:47:35 +02:00
Igor Opaniuk 7ee70eece7 firehose: chunk skipblock digest into bounded sub-regions
The --skipblock=sha256 fast-path issued a single <getsha256digest> over
the entire <program> region before deciding whether to write it. On a
multi-GB partition such as rootfs the device has to read and hash the
whole region synchronously, which easily exceeds
firehose_getsha256digest()'s 30 s read timeout. Worse, while the device
is busy hashing it does not service the bulk-OUT endpoint, so qdl's next
write stalls and fails - turning the intended optimization into a hard
flashing failure.

Walk the region in 512 MiB chunks instead. Each chunk is hashed locally,
compared against the device's digest for the matching flash sub-region,
and reflashed on its own (via an independent <program>) only when it
differs; matching chunks are left untouched. No single <getsha256digest>
now covers more than 512 MiB, keeping every digest well within the read
timeout and never blocking the device long enough to wedge the following
write.

Non-sparse, non-NAND, VIP-disabled programs are still the only ones
eligible, and a region smaller than one chunk behaves exactly as the
previous whole-region compare did.

Closes: https://github.com/linux-msm/qdl/issues/261

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-06-21 10:30:25 +02:00
Igor Opaniuk a6053af16b tree: drop qdl-ks binary
With kickstart now available as `qdl ks`, the standalone qdl-ks binary
is the same redundancy qdl-ramdump was: a second entry point, meson
target and manpage for code the main binary already carries. Removing
it completes the consolidation onto a single qdl binary.

Drop the binary and its build/doc scaffolding, and rewrite the README
kickstart section to use `qdl ks`. The description is kept, not
removed -- the feature lives on, only its entry point changed.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-06-20 14:47:46 -05:00
Igor Opaniuk 7d3d56e90f qdl: introduce ks subcommand
Kickstart (loading images to a flashless-boot device such as the
Qualcomm Cloud AI 100 over Sahara) only exists as the standalone
qdl-ks binary.

Fold it into the single qdl binary the way ramdump already is.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-06-20 14:47:46 -05:00
Igor Opaniuk 89e2823ed7 tree: drop qdl-ramdump binary
qdl-ramdump exposed crash-dump collection as its own binary, but the
exact same code is already reachable as `qdl ramdump`. Keeping a second
entry point for it costs a meson target, a manpage and README real
estate, and gives users a second name to learn, all for functionality
the main binary already provides. Consolidating on a single qdl binary
(as suggested in review) removes that duplication.

Drop the standalone binary and everything that only existed to build
and document it; the `qdl ramdump` subcommand is unchanged.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-06-20 14:47:46 -05:00
Igor Opaniuk 0122582b1c build: point meson at the new src/ layout
Every source change funnells through the single ~200-line
root meson.build that enumerated the sources for all binaries,
so touching one area means editing a file that mostly described
unrelated code.

Declare the source lists in src/meson.build and the tests in
tests/meson.build and pull them in with subdir(). The root meson.build
is then left to compose (dependencies, version header, targets) while
the list of files to build lives next to the files themselves, so
adding or removing a source is a one-line edit in one obvious place.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-06-20 14:47:46 -05:00
Igor Opaniuk 9f0a50bc4b tree: reorganize sources
The repository root had accumulated ~45 .c/.h files sitting directly
next to the project files (meson.build, scripts/, tests/, docs).

Move all sources under src/ and the public header to include/ so the
top level reflects the shape of the project rather than its source
file count.

This commit is a pure git mv with no content changes, so git log
--follow keeps working across the move.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-06-20 14:47:46 -05:00
Igor Opaniuk a34e571bf4 github: request reviews from qdl-maintainers via CODEOWNERS
Add a CODEOWNERS file so every pull request automatically requests a
review from the @linux-msm/qdl-maintainers team, instead of relying on
contributors to add reviewers by hand. The wildcard pattern covers the
whole tree; ownership can later be narrowed per path if needed.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-06-19 11:40:40 +02:00
Igor Opaniuk 74d810ad5d firehose: report progress through the skipblock fast-path
The --skipblock=sha256 fast-path could appear to hang before a large
<program> such as rootfs: qdl_should_skipblock() reads the whole file to
build a local SHA-256, then waits on the device's <getsha256digest> over
the same flash region, all without printing anything. On a multi-hundred
MiB partition both steps take a noticeable while and look like a stall.

Narrate each phase so the wait is explained:

 - "hashing ... locally" before the local read/hash,
 - "requesting flash digest" before the device digest query,
 - "sha256 mismatch ..., flashing partition" when the digests differ
   and the partition falls back to the normal program path.

The messages are emitted only once the skipblock guards have passed, so
runs with skipblock disabled or not applicable (sparse/NAND/VIP) stay
quiet as before. The existing "skipped ... (sha256 match)" message
covers the match case.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-06-17 08:50:16 +02:00
Igor Opaniuk d790275613 firehose: zero-pad local digest so short/offset files can be skipped
qdl_should_skipblock() builds a local SHA-256 over the bytes that would
be written and compares it against the device's <getsha256digest> of the
same flash region, skipping the <program> on a match.

For the comparison to be valid the local digest must cover exactly what
the program path puts on flash. That path always sends a full
num_sectors-sized region and zero-pads any residue past EOF (see the
memset() in firehose_program()). This happens whenever the file size is
not a sector multiple, or a non-zero file_sector_offset is used while
num_sectors still spans the whole file. The skipblock path instead
treated that short read as "cannot build a matching digest" and returned
0 before ever issuing <getsha256digest>, so those partitions were always
reprogrammed and never compared.

Mirror the write path: hash the file bytes within the region, then hash
zeros for the remainder up to num_sectors * sector_size. Files that fill
the region exactly are unchanged; short or offset files now produce a
digest that can match flash, letting xbl/tz/rpm/hyp-style partitions be
skipped like boot/dtb already were.

Reported-by: Milosz Wasilewski <milosz.wasilewski@oss.qualcomm.com>
Closes: https://github.com/linux-msm/qdl/issues/258
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-06-11 15:40:43 -05:00
Igor Opaniuk a5953c4c2e tree: drop unused .gitreview
The .gitreview file configured git-review to push changes for code
review to a Gerrit instance at review.linaro.org. The project's review
workflow has since moved to GitHub pull requests against
github.com/linux-msm/qdl, and the Linaro Gerrit project is no longer
used. Remove the stale config so contributors are not misled into
sending patches to an abandoned review server.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-06-01 23:49:40 -05:00
Igor Opaniuk d2a737cfac program: Cast away const when freeing start_sector in erase
The recently added free() calls in load_erase_tag() pass
program->start_sector directly to free(), but the field is declared
as const char * in struct firehose_op. GCC emits a
-Wdiscarded-qualifiers warning for both call sites.

Cast through (void *) to match the pattern already used at the other
free sites for the same field (program.c, patch.c, firehose.c).

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-06-01 23:48:19 -05:00
Igor Opaniuk 2576983692 firehose: detect VIP mode mismatch from programmer startup logs
Both directions of host/device VIP misconfiguration currently surface
as opaque failures that operators in the field have to diagnose from
raw byte-stream errors, when the right answer is fully derivable from
the programmer's own startup logs. Do the detection here so customers
don't have to debug this manually on every new platform.

The Firehose programmer announces VIP activation by emitting an
"INFO: VIP is enabled, receiving the signed table of size N" log line
during startup. Watch for that marker in firehose_read() and use it in
firehose_detect_and_configure() to handle the two mismatch cases:

  - --vip-table-path missing, programmer requires VIP: previously qdl
    would send configure, the programmer would wait silently for a
    signed table that never comes, and the user would see only a
    generic 5s configure timeout. Now bail out immediately with an
    explicit error pointing at the missing option.

  - --vip-table-path given, programmer not in VIP mode: previously qdl
    would push the signed digest table at the programmer, which tried
    to parse those binary bytes as XML and bailed out with "Failed to
    parse xml at offset 4090, 4096". Now detect the absence of the
    marker after the startup-log drain, print a warning, demote
    vip_data.state to VIP_DISABLED so the table is never sent, and
    continue flashing normally.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-06-01 23:47:51 -05:00
Igor Opaniuk 702abb441f qdl: add --skipblock=sha256 to skip programs already on flash
Automated flashing pipelines often re-program identical content on every
run, which dominates total flash time on slow storage like spinor. With
the <getsha256digest> primitive now exposed, qdl can ask the device for
the SHA-256 of the target region before each <program> and skip the
write when it already matches what would have been sent.

Add --skipblock=<none|sha256>, defaulting to none. When sha256 is
selected, firehose_program() hashes the bytes it would otherwise stream
to the device - reading num_partition_sectors * SECTOR_SIZE_IN_BYTES
from the file with zero-padding for the trailing partial sector, exactly
matching what the existing chunked write path puts on the wire - then
asks the device for the digest of the same region via
firehose_getsha256digest(), reusing the op->digest field that the
sha256 subcommand path also reads. On a match, the <program> is skipped
entirely and "skipped \"<label>\" (sha256 match)" is logged in place of
the usual flashed-at-rate line.

The optimization is gated on three conditions, all enforced silently:

  - !program->sparse: each sparse chunk would need its own digest;
    keeping sparse images on the normal program path avoids per-chunk
    round trips and the bookkeeping that would come with them.
  - !program->is_nand: NAND spare/OOB byte semantics differ across
    programmers, so the digest the device returns is not guaranteed to
    correspond to the bytes qdl would compute locally.
  - VIP disabled: <getsha256digest> is not part of pre-built VIP
    digest tables, so issuing it under VIP would cause a hash mismatch
    on the device.

Any failure on the optimization path - file read error, query failure,
digest absent, or digest mismatch - falls through to the normal program
flow. The file position is restored unconditionally by the existing
qdl_file_seek() at the top of the write loop, so the fall-through path
is byte-identical to --skipblock=none.

[1] https://github.com/linux-msm/qdl/issues/237

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-05-16 16:35:15 -05:00
Igor Opaniuk 7a7e02aac8 firehose: carry sha256 digest in op; print from qdl.c
firehose.c is the firehose protocol layer; deciding what to do with a
digest (print it for the user, compare it to a locally-computed one)
is qdl policy. Move the result back onto struct firehose_op so the
parser can fill it directly, drop the local intermediate result struct,
and let firehose_run() return without emitting anything to stdout.

For the `sha256` subcommand, format and print each filled digest in
qdl_flash() once firehose_run() succeeds. The visible output for the
subcommand is unchanged.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-05-16 16:35:15 -05:00
Igor Opaniuk 05e4bb9904 qdl: add sha256 subcommand
Expose the FIREHOSE_OP_GET_SHA256_DIGEST primitive as a user-facing
verb. The syntax mirrors the existing read / write / erase verbs and
reuses parse_storage_address(), so addresses can be expressed as
<P>/<S>+<L>, by GPT partition name, or any of the other supported
forms.

Each invocation prints one lowercase 64-character hex digest per region
to stdout, so the output can be diffed against a sha256sum(1) line or
piped into other tooling without further parsing.

Typical use is verifying that what is on the device matches a known
local image - useful on shared bench machines where multiple people
flash the same board:

$ qdl prog_firehose_ddr.elf sha256 xbl_a
...
c288e50a94687a130a64db608c83678777ce5e7f9229248696c6f0dc41406cc9

$ qdl prog_firehose_ddr.elf read xbl_a xbl_a.bin
...
$ sha256sum xbl_a.bin
c288e50a94687a130a64db608c83678777ce5e7f9229248696c6f0dc41406cc9

Multiple sha256 verbs can be chained in a single invocation, with one
digest line emitted per region in order.

[1] https://github.com/linux-msm/qdl/issues/237

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-05-14 21:41:45 -05:00
Igor Opaniuk 8d27dc4f61 firehose: add support for <getsha256digest>
Firehose programmers can compute a SHA-256 over a region of flash on the
target side via the <getsha256digest> command, but qdl currently has no
way to issue it. Bench workflows that want to answer "what's actually
flashed on this board?" today have to read the partition back over USB
and hash it locally - slow on every storage type and particularly bad on
spinor.

Introduce a new firehose op type, FIREHOSE_OP_GET_SHA256_DIGEST, and a
matching firehose_getsha256digest() that builds the XML request with the
same address attributes used by <read> / <program> / <erase>
(SECTOR_SIZE_IN_BYTES, num_partition_sectors, physical_partition_number,
start_sector, and slot when applicable).

The digest response format is not consistent across Firehose variants.
Most implementations emit the digest inside a <log value="..."/> stream
prior to the ACK, but some attach it as an attribute on the <response>
itself. Add firehose_sha256_parser() which scans every incoming log
payload for a 64-hex-character run bounded by non-hex characters and,
as a fallback, probes the response node for Digest / SHA256 / sha256
attributes. The parser still defers ACK / NAK / rawmode handling to
firehose_generic_parser() so behaviour is identical to other commands
on the control path.

On success the digest is printed to stdout as a single lowercase
64-character hex line, separate from the ux_info / ux_log channels, so
it can be cleanly captured or piped without stripping prefixes.

gpt_resolve_deferrals() learns about the new op type so callers can
address regions by GPT partition name, matching the ergonomics of
<read>.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-05-14 21:41:45 -05:00
Igor Opaniuk 7a9169b61d 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 <igor.opaniuk@oss.qualcomm.com>
2026-05-14 21:40:37 -05:00
Igor Opaniuk 8e9082bc21 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 <igor.opaniuk@oss.qualcomm.com>
2026-05-14 21:40:37 -05:00
Igor Opaniuk 0512c0b983 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 <read> ACK) can be spliced onto the same buffer right after
the closing </data> tag.

Walk the read buffer using "<?xml" as the document start and
"</data>" as the envelope end, then dispatch each XML chunk through
the existing firehose_response_parse() + response_parser path.
<log/> and <response/> 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 <igor.opaniuk@oss.qualcomm.com>
2026-05-14 21:40:37 -05:00
Igor Opaniuk 1599b880fc 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 <igor.opaniuk@oss.qualcomm.com>
2026-05-14 21:40:37 -05:00
Igor Opaniuk 78d791c622 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 <igor.opaniuk@oss.qualcomm.com>
2026-05-14 21:40:37 -05:00
Igor Opaniuk 2e4c66ac5c sahara: auto-skip when Firehose programmer is already running
When chaining flash invocations - typically with --skip-reset to leave
the programmer alive between calls - re-uploading the programmer via
Sahara is unnecessary, and re-entering Sahara on a device that is
already serving Firehose simply hangs until timeout.

Detect this on the very first read of sahara_run(): if the device emits
a Firehose XML banner ("<?xml") instead of a Sahara HELLO, or if the
read times out, assume the programmer is already running and return
success so the caller proceeds straight to the Firehose phase.

The detection is gated on the call site: ramdump mode, where Sahara is
the only valid protocol, retains the previous strict behaviour.

Discarding the early Firehose banner is harmless - firehose_run() drives
its own request/response exchange and does not depend on prior data.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-05-12 09:01:00 -05:00
Igor Opaniuk 86864dec4e qdl: add --skip-reset to suppress post-flash device reset
Some workflows require the device to remain in the programmer state
after flashing - for example, when chaining multiple flash operations,
running post-flash verification, or debugging with the programmer still
responsive. The unconditional reset at the end of firehose_run() and
firehose_provision() makes this impossible without patching the binary.

Model the reset as a regular firehose op (FIREHOSE_OP_RESET) that
firehose_execute_ops() dispatches like any other operation. qdl.c then
appends a reset op at the end of the ops list, gated on the new -R /
--skip-reset CLI flag, instead of teaching firehose_run() a special
case. firehose_run() no longer issues a reset on its own.

The UFS provisioning path does not consume a firehose ops list, so it
keeps a dedicated reset call; firehose_provision() now takes the
skip_reset boolean as a parameter rather than reading it back off the
qdl_device, keeping all CLI-level state out of struct qdl_device.

Default behaviour is unchanged.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-05-12 09:01:00 -05:00
Igor Opaniuk e40a2b07f6 meson: avoid duplicate library entries on executable link lines
Both the qdl_common static library and each of the
qdl/qdl-ramdump/qdl-ks executables were declared with the same
`dependencies: common_dep` (libusb-1.0, libxml-2.0, libzip,
plus ws2_32 on Windows). Meson propagates a static library's
dependencies to its consumers via `link_with`, so listing those
dependencies again on each executable caused every common library to
appear twice on the final link line. Apple's ld surfaced this as:

  ld: warning: ignoring duplicate libraries: '-lxml2'

The other dependencies are passed by absolute dylib path on macOS
(libusb-1.0, libzip), so the linker silently deduplicated those while
still warning about libxml2, which is referenced as `-lxml2` from the
system path.

Fix it by introducing a compile-only view of the common dependencies
using `partial_dependency(compile_args: true, includes: true)` and
attaching that to the executables instead of the full deps. The
executables still see the headers needed to compile their own sources
(e.g. usb.c -> libusb.h, file.c -> zip.h), while link arguments now
flow to them exactly once via the static library's automatic
propagation.

Fixes: ff189b1a12 ("build: migrate from GNU Make to Meson build system")
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-05-11 10:32:13 -05:00
Igor Opaniuk fa274ea2db ux: Route libxml2 parser diagnostics through ux_err()
libxml2 prints warnings and errors directly to stderr by default. This
leaks raw parser diagnostics into both test output and user-facing
output when an XML file is missing or malformed, bypassing the tool's
ux_err() output layer.

Install a structured error handler via xmlSetStructuredErrorFunc() in
ux_init() that formats each diagnostic as

    libxml2 <level>: <file>:<line>: <message>

and routes it through ux_err(). This preserves the file, line and
reason detail libxml2 produces for malformed XML while keeping output
flow consistent with the rest of the tool, and applies uniformly to
every xmlReadFile()/xmlReadMemory() call without per-site flag
changes.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-05-11 10:17:33 -05:00
Igor Opaniuk e08f0eb42b commitlint: Recognize kernel-style trailers as footer items
The config relaxes footer-max-line-length to 200 specifically to
accommodate long Signed-off-by addresses, but commitlint's default
parser only treats `BREAKING CHANGE` as a note keyword. Other
kernel-style trailers (Fixes, Reviewed-by, Tested-by, ...) get
classified as body lines, so a trailer that exceeds 72 characters --
typically a `Fixes:` quoting a long commit subject -- trips
body-max-line-length even though the relaxed footer limit was meant
to cover exactly this case.

Configure parserPreset.parserOpts.noteKeywords with the trailer
tokens this project uses, so those lines move into the footer
section and inherit the existing footer-max-line-length=200 rule.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-05-11 10:15:48 -05:00
Igor Opaniuk 21c7f22183 tests: give each integration test its own fixture directory
test_flashmap.sh and test_vip_generation.sh both wrote fixtures to
${builddir}/tests/data, and meson runs the two tests in parallel.
Their generate_flat_build.sh invocations therefore raced on dd, cp
and the final `zip flashmap.zip ...`, while test_vip_generation.sh's
EXIT trap could rm -rf the directory out from under its peer.

The race was masked while zip took several seconds on the unshrunk
1 GB fixtures; once fixture generation dropped to ~30 ms it started
firing on the macos-14 arm64 runner as zip exit status 18 ("could
not open a specified file to read").

Point each test at its own directory (data-flashmap / data-vip) so
they no longer share mutable state.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-05-11 10:15:23 -05:00
Igor Opaniuk aebce7311a tests: shrink oversized fixture binaries from 1 GB to 16 MB total
The fixture generator was creating efi.bin (512 MB) and rootfs.img
(500 MB) -- sizes inherited from a real-world device layout but with
no bearing on what the integration tests actually exercise. Both the
flashmap dry-run and the VIP digest test only need qdl to parse the
XML/JSON descriptors, walk the program list, and (for VIP) hash the
backing files. Whether the backing files are 8 MB or 512 MB makes no
difference to the code paths under test.

The previous switch to sparse files removed the dd write cost, but
zip still has to scan and CRC32 every byte of input regardless of
whether the bytes come from a hole. On the slow macos-15-intel CI
runner that scan was the new dominant cost and continued to push
both tests past meson's default 30 s timeout.

Drop efi.bin and rootfs.img to 8 MB each. Adjust rawprogram0.xml so
that the efi partition descriptor matches the new file size
(size_in_KB="8192.0", num_partition_sectors="2048") and rootfs's
start_sector / start_byte_hex follow on at sector 2054 (0x806000).
The other partition descriptors and patch entries are unaffected.

Local test run drops from ~5 s to ~0.25 s (combined). The slow
Intel CI runner should now have plenty of headroom under the 30 s
default timeout.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-05-06 09:51:56 -05:00
Igor Opaniuk b551397355 tests: generate fixture files as sparse to fix slow CI runners
The integration tests' fixture generator dd'd ~1 GB of zeros to disk
for every test run (efi.bin = 512 MB, rootfs.img = 500 MB, plus
smaller files), and the script is invoked twice per `meson test`
invocation (once per test, with cleanup in between). Each test is
then capped by meson's default 30 s timeout. On the macos-15-intel
GitHub runner that combination was enough to push both tests over
the limit:

  1/2 integration - qdl:VIP table generation TIMEOUT  30.05s
  2/2 integration - qdl:Flashmap dry-run     TIMEOUT  30.53s

while macos-14 arm64 (10 s / 16 s) and the Windows runners (14-23 s)
were already trending uncomfortably close. The tests don't actually
care about file *contents*: qdl --dry-run and the SHA256 of the VIP
digest both consume the byte stream as zeros either way, and zip
deflates zero blocks trivially.

Switch the generator from `dd ... bs=1024 count=N` (which writes
N KB) to `dd ... bs=1024 count=0 seek=N` (which extends the file to
N KB via lseek and writes nothing). On filesystems that support
sparse files (APFS, ext4, NTFS) the resulting file has zero
allocated blocks. Verified with `stat`:

  count=0 seek=N: size=1048576 blocks=0     (sparse)

Reads of unallocated regions return zeros from the kernel without
disk I/O, so SHA256 input bytes are identical and the existing
EXPECTED_DIGEST in test_vip_generation.sh stays valid. Local test
time on Apple Silicon dropped from ~5-7 s to ~3-5 s; the much
larger gain is on the slow Intel CI runner where the writes were
the dominant cost.

Fixes: 6c0fbcce08 ("tests: add initial test set")
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-05-06 09:51:56 -05:00
Igor Opaniuk d3d806cbe3 checkpatch: Run per-commit range checks locally and in CI
Until now checkpatch had two divergent code paths: the local meson
'check' target ran 'checkpatch.pl --no-tree -f' against every tracked
source file, while the CI workflow used the webispy/checkpatch-action
action, which runs checkpatch in patch mode against each commit in the
PR. The two modes report different defects -- in particular, file mode
flags style issues in long-untouched code and skips commit-log checks,
while patch mode validates the commit message (Signed-off-by, subject
length, ...) and only inspects added/changed lines. Contributors could
end up with a green local 'check' but a red CI run, and vice versa.

Introduce a 'check-range' mode in scripts/checkpatch_wrapper.sh that
mirrors what the GitHub Action does: iterate over every commit in
CHECKPATCH_BASE..CHECKPATCH_HEAD (defaulting to origin/master..HEAD),
feed each commit through 'git format-patch -1 --stdout' and pipe the
result into checkpatch.pl. All commits are processed even on failure
so the user sees every offending commit in one go.

Expose the new mode as a meson run_target ('meson compile check-range
-C build') and switch the CI workflow to invoke the wrapper directly
with the PR base/head SHAs, so local and CI invocations exercise the
exact same code path. Document the new target in README under the
Contributing section.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-05-04 21:25:07 -05:00
Igor Opaniuk 00f1b9bb0f markdown-lint: Add wrapper script and meson target
The markdown lint check previously only ran in CI via the
markdownlint-cli2 GitHub Action, which meant developers had no
straightforward way to reproduce the check locally before pushing. As a
result, contributors only discovered lint errors after opening a PR.

Introduce scripts/markdownlint_wrapper.sh that drives mdl on the same
set of files the CI workflow lints. mdl is shipped as a Debian/Ubuntu
package, so contributors can install it with 'sudo apt install mdl'
without pulling in a Node.js toolchain. Wire the wrapper up as a meson
run_target so it can be invoked with:

  meson compile markdown-lint -C build

Switch the markdown-lint workflow to install mdl from apt and invoke
the wrapper directly, making the script the single source of truth for
both local and CI runs. Document the usage in README under the
Contributing section.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-05-04 21:25:07 -05:00
Igor Opaniuk be4f6c3b9c meson: guard manpages alias_target against missing help2man
The alias_target('manpages', manpage_targets) call lived outside the
'if help2man.found()' / 'can_run_exe' block, but manpage_targets is
only populated inside it. When help2man is absent (or this is a cross
build), manpage_targets is undefined and meson setup fails. The same
call would also fail if reached with an empty list, since alias_target
requires at least one dependency.

Move the alias_target declaration inside the same guard so it is only
emitted when there are actual manpage targets to alias.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-04-20 17:39:31 -05:00
Igor Opaniuk 30c328b16e README: document qdl-ramdump standalone binary
The crash dump section only mentioned the 'qdl ramdump' subcommand,
but the build also produces a standalone qdl-ramdump executable that
exposes the same functionality. Mention it so users who only need
crash-dump collection know they can use it directly.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-04-20 17:39:31 -05:00
Igor Opaniuk fd4a4651d0 README: document qdl-ks Sahara kickstart helper
Add a section describing qdl-ks, the "kickstart" Sahara helper used to
load images from the host to flashless-boot devices such as the
Qualcomm Cloud AI 100, which fetch their runtime firmware from the
host on every boot rather than storing it on-device.

Explain why qdl-ks is a separate tool (no USB, no Firehose; talks to a
kernel-provided device node via open/read/write), describe the two
required arguments, and show a sample invocation. Note that mapped
files do not need to exist at invocation time -- if qdl-ks cannot open
a requested file the device decides the next action, which is useful
for wiring qdl-ks into a single udev rule covering multiple device
configurations (e.g. an optional DDR training image).

The description is based on the commit bdcf93b4a3 ("Add ks utility")
message.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-04-20 17:39:31 -05:00
Igor Opaniuk 8c1a822369 tests: generate test fixtures in the build directory
Place generated test artifacts (binaries, images) under the build
directory instead of the source tree, keeping the source tree clean
and following Meson's out-of-tree build convention.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-04-15 16:42:55 -05:00
Igor Opaniuk a42f803c2c tests: replace monolithic test wrapper with per-suite Meson tests
Drop the run_tests.sh wrapper script and register each test suite
directly with Meson's test() function. This leverages Meson's native
test infrastructure for parallel execution, filtering by suite, and
per-test timeout/reporting instead of reimplementing it in shell.

Each test script is now self-contained: it generates its own fixtures
and cleans up via an EXIT trap, so tests can run independently and
in any order without relying on a shared setup/teardown wrapper.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-04-15 16:42:55 -05:00
Igor Opaniuk 0ace7d868a README: change instructions to meson
Provide meson instructions instead of GNU-make for
building/testing qdl.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-04-06 10:16:17 -05:00
Igor Opaniuk ff189b1a12 build: migrate from GNU Make to Meson build system
Replace the existing Makefile-based build system with Meson to improve
build performance, cross-platform support, and maintainability.

Key improvements:

* Build performance: Shared source files (sahara.c, util.c, oscompat.c,
  ux.c, sim.c, io.c) are now compiled once and linked into multiple
  targets instead of being compiled separately for each executable.
  This reduces total compilation units from ~30 to ~20 and speeds up
  build.

  Full build benchmark results below

  Before (Make): $ time make
  ...
  real    0m1.668s
  user    0m1.424s
  sys     0m0.242s

  After (Meson): $ time bash -c '
  meson setup build
  meson compile -C build
  '
  ...
  real    0m0.759s
  user    0m0.877s
  sys     0m0.237s

* Flexible build directory: Unlike Make which builds in-tree, Meson
  supports out-of-tree builds with custom build directories.

  Example workflows:
    meson setup builddir              # Default debug build
    meson setup build-release --buildtype=release
    meson setup build-asan -Db_sanitize=address
    meson setup build-arm64 --cross-file=aarch64-linux.txt

  This enables:
  - Multiple build configurations simultaneously
  - Clean source tree (no .o files polluting git status)
  - Easy comparison between debug/release builds
  - Separate builds for different architectures

* Cross-compilation: Native support for cross-compilation via Meson
  cross-files enables building for multiple architectures without
  requiring separate build runners for each target platform.

  Before (Make): Requires native ARM64 runner in GitHub Actions
    runs-on: ubuntu-24.04-arm  # Dedicated ARM64 hardware
    run: make

  After (Meson): Cross-compile from x64 host
    runs-on: ubuntu-24.04  # x64 host
    run: |
      meson setup build-arm64 --cross-file=ci/aarch64-linux.txt
      meson compile -C build-arm64

* Automatic dependency tracking: Meson automatically tracks header
  dependencies, eliminating the need for manual dependency declarations
  and preventing missed recompilation on header changes.

  Before (Make): Manual dependency tracking
    util.o: version.h  # Must be manually maintained

  After (Meson): Automatic header scanning
    # No manual declarations needed
    # Changing qdl.h automatically rebuilds all files that include it

  Result: Prevents subtle bugs where changing a header doesn't trigger
  necessary recompilation.

* Platform handling: Windows-specific linking (ws2_32) and platform
  detection are now handled declaratively without shell conditionals.

* Testing integration: Test suite is now integrated into the build
  system with proper dependency tracking and timeout handling.

  Before (Make):
    make tests  # Shells out to bash script

  After (Meson):
    meson test -C builddir
    meson test -C builddir --verbose
    meson test -C builddir --wrapper='valgrind --leak-check=full'

  Result: Better test output formatting, timeout handling, and ability
  to run tests under valgrind or sanitizers.

* Installation: Standard installation paths and packaging support
  through Meson's built-in install infrastructure.

  Before (Make):
    make install prefix=/usr/local DESTDIR=/tmp/package

  After (Meson):
    meson setup builddir --prefix=/usr/local
    DESTDIR=/tmp/package meson install -C builddir
    # Correctly installs to:
    # - /usr/local/bin/{qdl,qdl-ramdump,ks}
    # - /usr/local/share/man/man1/{qdl.1,qdl-ramdump.1,ks.1}

  Result: Better integration with distribution packaging (Debian .deb,
  RPM) and standard FHS paths.

Migration maintains full feature parity with the existing build system:
- All three executables (qdl, qdl-ramdump, ks) build correctly
- Version string generation from git tags
- Optional man page generation
- Support for Linux, macOS, and Windows (MSYS2/MinGW)
- Recreated tests integration via Meson’s test() function,
  including support for script arguments (e.g. --builddir)

Usage:
$ meson setup build
$ meson compile -C build
$ meson test -C build
$ meson compile check -C build

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-04-06 10:16:17 -05:00
Igor Opaniuk 2116ac1ba7 sim: fix indentation alignment in sim_vip_signed_payload()
Fix indentation alignment in sim_vip_signed_payload().

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-04-06 10:16:17 -05:00
Igor Opaniuk da5f887e16 sahara: fix indentation alignment in sahara_read64 declaration
Fix indentation alignment in sahara_read64() declaration.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-04-06 10:16:17 -05:00
Igor Opaniuk 1cdefb30ad README: document VIP dry-run validation workflow
Add a subsection under the VIP section that explains the workflow,
what QDL verifies, and why running a dry-run validation before touching
hardware saves time and avoids leaving devices in a broken state.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-04-06 09:10:10 -05:00
Igor Opaniuk 75ac9bcb20 sim: validate VIP digest tables and chunk hashes in dry-run mode
Without any verification on the host side, a dry-run with pre-built VIP
tables (-n -D <path>) would silently succeed even when the digest tables
did not match the data being flashed. The only way to catch a mismatch
was to attempt a real flash and wait for the device to reject it, which
wastes time and risks leaving a device in a broken state.

Add hash validation to the simulator so mismatches are caught at dry-run
time, before any hardware is involved.

The signed initial table (DigestsToSign.bin.mbn) and all chained tables
(ChainedTableOfDigests<n>.bin) are parsed as they arrive. Chain hashes
linking each table to the next are verified to ensure table integrity.
Per-chunk SHA256 hashes are checked in order against every XML command
write and raw payload write; any mismatch is reported via ux_err()
without aborting so all mismatches remain visible at the end of a run.

Validation is active in exactly the intended modes:
  -n             plain dry-run, no tables sent -- no-op
  -n -D <path>   dry-run with VIP tables -- full hash validation
  -n -t <dir>    digest generation -- validation disabled

MAX_DIGESTS_PER_SIGNED_FILE and MAX_DIGESTS_PER_CHAINED_FILE are moved
from vip.c to vip.h so sim.c can reference them. sha2.c is added to
RAMDUMP_SRCS because qdl-ramdump links sim.o which now calls SHA256.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-04-06 09:10:10 -05:00
Igor Opaniuk ec8dc8d332 sim: implement proper Firehose protocol state machine
Replace the stub sim_read()/sim_write() with a simulator that
parses incoming XML commands and enqueues matching XML responses,
mirroring the real Firehose device protocol. This decreases divergence
in behavior between the real HW and the dry-run mode, giving better
validation of the flashing process in dry-run mode.

sim_write() now dispatches based on the command tag:
 - configure: enqueue log + ACK echoing the same max payload size so
   firehose_try_configure() does not trigger a redundant exchange
 - program/read: enqueue log + rawmode=true ACK and transition to
   SIM_STATE_RAW_IN/RAW_OUT to track the raw binary transfer
 - erase/patch/setbootablestoragedrive/ufs: enqueue log + ACK
 - power: enqueue log + ACK and mark the simulator as closed

sim_read() first drains the XML response queue (ensuring protocol
responses are delivered before any raw data), then serves zero-filled
sector data in SIM_STATE_RAW_OUT, and returns -EIO once closed so
the trailing drain in firehose_reset() completes immediately.

Since the simulator now produces correct protocol responses, remove the
QDL_DEVICE_SIM early-return guards in firehose_read() and
firehose_try_configure() that previously bypassed response parsing.

Pre-initialize qdl->sector_size = 4096 in sim_init() and only probe
sector size in firehose_try_configure() if unset. Previously, the
probe <read> commands were executed during --createdigest dry-runs and
included in the VIP digest table, but not issued during real VIP
flashing (where probing is gated on VIP_DISABLED). This caused all
subsequent digest entries to shift, resulting in hash mismatches on
the device.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
2026-03-30 19:36:24 +02:00