iwm_read_firmware TLV walk: size_t underflow on len -= roundup2(tlv_len,4) (and datasize < header) walks the parser past the firmware image
Summary
The TLV loop only checks len < tlv_len but subtracts roundup2(tlv_len,4) and advances data by sizeof(*tlv)+roundup2(tlv_len,4): a final TLV with unaligned length ending exactly at the image end underflows size_t len to ~SIZE_MAX and the parser keeps reading TLV headers past the firmware buffer; garbage TLVs can pass per-type checks (SEC_* only needs len>=4) and store out-of-image section pointers later memcpy'd/DMA'd by iwm_pcie_load_section. Additionally len = datasize - sizeof(*uhdr) underflows for files < 88 bytes and the magic check dereferences 4 bytes before any size validation. REPRODUCED at logic level on the guest with a verbatim userland port: case A (trailing SEC_RT TLV len=5) prints len = 0xfffffffffffffffd then SIGSEGV on the guard page; case B (40-byte file) SIGSEGVs; fixed arithmetic returns clean errors. iwm counterpart of DF-3577 by a different mechanism. Root-installed firmware file / supply chain precondition.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3631 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.4 KB | β raw | |
| VERDICT.md | β | 3.3 KB | β raw | |
| tlv_underflow.c | β | 6.0 KB | view raw | |
| tlv_underflow_fixed.c | β | 6.1 KB | view raw | |
| build.sh | β | 100 B | view raw | |
| run.sh | β | 117 B | view raw | |
| build.log | β | 147 B | view raw | |
| run.log | β | 615 B | view raw | |
| run_fixed.log | β | 474 B | view raw | |
| env.txt | β | 740 B | view raw | |
| fix.diff | β | 752 B | view raw | |
| manifest.json | β | 1.2 KB | view raw | |
| verdict.json | β | 3.5 KB | view raw |
DF-3631 β iwm_read_firmware TLV walk: size_t underflow on len -= roundup2(tlv_len, 4) walks the parser past the firmware image
Where
sys/dev/netif/iwm/if_iwm.c:563-910 (iwm_read_firmware), specifically
:620 (len = fw->fw_fp->datasize - sizeof(*uhdr)), :606-608
(unvalidated 4-byte magic read), :630-638 (the TLV walk).
What
len = fw->fw_fp->datasize - sizeof(*uhdr); /* :620 β underflows if datasize < 88 */
while (len >= sizeof(*tlv)) {
len -= sizeof(*tlv);
tlv = (const void *)data;
tlv_len = le32toh(tlv->length);
...
if (len < tlv_len) { ...error... } /* :630 β the ONLY bound check */
len -= roundup2(tlv_len, 4); /* :637 β UNDERFLOW:
len == tlv_len and tlv_len%4 != 0
-> len = ~SIZE_MAX */
data += sizeof(*tlv) + roundup2(tlv_len, 4); /* :638 β advances past end */
switch (...) { ... } /* next iteration reads tlv header OOB */
}
Two reachable underflows, both leaving the firmware image buffer:
- Trailing unaligned TLV: a final TLV whose
lengthis not 4-aligned and whose data ends exactly at end-of-file passes thelen < tlv_lencheck (len == tlv_len), thenlen -= roundup2(tlv_len, 4)subtracts more than remains βlenwraps to ~SIZE_MAX anddatapoints past the buffer. The loop keeps parsing TLV headers from kernel heap memory beyond thefirmwareimage; whatever it "finds" can pass the per-type checks (e.g.IWM_UCODE_TLV_SEC_RTonly needstlv_len >= 4), causingiwm_firmware_store_section()to record a data pointer outside the image whichiwm_pcie_load_section()later memcpys and DMAs to the device (:2396-2446) β heap OOB read with attacker-steered length. - Short file:
datasize < sizeof(struct iwm_tlv_ucode_header)(88) underflowslenimmediately (:620); and the magic check at:607dereferences*(const uint32_t *)fw->fw_fp->databefore any size check (fault/OOB read on a <4-byte file).
This is the iwm counterpart of iwn's DF-3577 (TLV len>=4 lesson), with a
different mechanism (roundup2 underflow rather than missing per-case check).
Threat model
Crafted firmware file (firmware_get(sc->cfg->fw_name), :585). Installing
one requires root (kld module under /boot, or loader(8) paths) β a
privileged-file-replacement / supply-chain trust boundary, hence Low
severity, consistent with DF-3577 (Low).
Reproduce (logic-level, executed on the audit guest)
tlv_underflow.c in this pack is a faithful userland port of the exact
loop (:619-638 verbatim, same types, same checks) with the crafted image
placed at a guard page:
- Case A: 113-byte image, final TLV type=SEC_RT(19) len=5 at image end β
iter 2 prints
remaining len field = 18446744073709551613 (0xfffffffffffffffd)then SIGSEGV (exit 139) reading the next TLV header past the image (run.log). - Case B: 40-byte file (< 88-byte header) β immediate underflow β SIGSEGV (139) (run.log).
- Fixed variant (
tlv_underflow_fixed.c: checklen < roundup2(tlv_len,4) - header-size guard): both cases return clean errors, exit 0 (run_fixed.log) β the exact arithmetic of fix.diff validated.
Fix
fix.diff β header-size guard before the magic read, and
if (len < roundup2(tlv_len, 4)) as the bound check (subsumes the
len < tlv_len check).
DF-3631 VERDICT
Status of the kernel path: inconclusive (no iwm hardware on the guest to make the driver load a firmware file). Status of the parser primitive: REPRODUCED on the guest toolchain via a verbatim userland port of the exact loop β both underflow cases fault reading past the image, and the fix arithmetic eliminates the faults. Confidence: certain.
What was run (guest: DragonFly 6.5-DEVELOPMENT, cc 8.3)
tlv_underflow.cβ port ofiwm_read_firmware()'s walk (sys/dev/netif/iwm/if_iwm.c:619-638), byte-for-byte same arithmetic:len -= sizeof(*tlv);if (len < tlv_len);len -= roundup2(tlv_len,4);data += sizeof(*tlv) + roundup2(tlv_len,4). Structs and constants copied fromif_iwmreg.h(iwm_tlv_ucode_header88 bytes,iwm_ucode_tlv8-byte header,IWM_TLV_UCODE_MAGIC 0x0a4c5749,IWM_UCODE_TLV_SEC_RT 19,IWM_UCODE_TLV_NUM_OF_CPU 27).- Case A β crafted 113-byte image whose final TLV is SEC_RT with
length 5 ending exactly at the image end, image backed by a PROT_NONE
guard page:
iter 2: ... type 19 len 5 -> remaining len field = 18446744073709551613 (0xfffffffffffffffd)then SIGSEGV, exit 139 (run.log) β the size_t wrapped and the very next header read is out of bounds. In the kernel this is a walk through kernel heap past thefirmwareimage; a garbage TLV of type SEC_*/PAGING etc. can then store an out-of-image pointer thatiwm_pcie_load_section()(if_iwm.c:2427-2428) later memcpys and DMAs (OOB read, attacker-sized). - Case B β 40-byte file (< sizeof(header)):
len = datasize - 88underflows immediately (:620), SIGSEGV, exit 139 (run.log). (Related: the magic check at:607reads 4 bytes before any datasize check.) - Fix validation β
tlv_underflow_fixed.capplies exactly the arithmetic of fix.diff (len < roundup2(tlv_len,4)guard + header-size guard): both cases return proper errors, exit 0 (run_fixed.log).
Kernel-path honesty
iwm_read_firmware runs from iwm_attach and needs the driver bound to a
real iwm PCI device (firmware file name comes from sc->cfg). The guest has
no such device (env.txt), so the in-kernel manifestation could not be
observed. The port proves the loop arithmetic; nothing in the kernel path
changes it (same size_t len, same expressions β if_iwm.c:573-579,622-638).
Impact ceiling
Kernel heap OOB read walk (firmware parse) + out-of-image section pointers
later DMA-read by iwm_pcie_load_section β potential kernel-memory
disclosure to the device / crash. Precondition: attacker-supplied firmware
file (root-installed or supply chain) β Low severity, matching DF-3577.
Fix status
fix.diff authored and its arithmetic validated on the guest (see #4). The
kernel build itself cannot be exercised without the device β fix_status
recorded as fixed with the ported-logic evidence noted (fix_baseline_reproduced=true:
crash reproduced pre-fix; fix_patched_reproduced=false: crash gone post-fix).
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedfix.diff (header-size guard + padded-length check) applies cleanly to sys/dev/netif/iwm/if_iwm.c; validated at ported-logic level on the guest: baseline harness crashes (run.log), fixed arithmetic exits 0 (run_fixed.log). In-kernel build validation impossible - no iwm device on guest.
run.log vs run_fixed.log + fix.diff (patch --dry-run clean against sys/dev/netif/iwm/if_iwm.c)
Confirmed kernel references
Detail
Exploit chain
crafted firmware file -> firmware_get() at attach -> TLV walk underflow -> OOB TLV parse -> (e.g.) IWM_UCODE_TLV_SEC_RT with garbage header stored as fw section pointing outside the image -> iwm_pcie_load_section() memcpy+DMA of out-of-image memory (if_iwm.c:2427) -> kernel heap disclosure to device / crash. Precondition: root-installed or supply-chain firmware file.
Evidence (decisive lines)
run.log (both cases SIGSEGV exit 139, len wrap visible: 0xfffffffffffffffd); run_fixed.log (both cases clean, exit 0); tlv_underflow.c / tlv_underflow_fixed.c (verbatim port + fixed variant); build.log; env.txt; VERDICT.md
PoC changes
PoC authored from scratch in verify phase (no seed): verbatim port of the :619-638 loop with structs/constants from if_iwmreg.h; guard-page placement to turn the OOB walk into a deterministic fault
Verified recommended fix
fix.diff (header-size guard + padded-length check) applies cleanly to sys/dev/netif/iwm/if_iwm.c
Verdict
iwm_read_firmware()'s TLV walk subtracts roundup2(tlv_len,4) from size_t len after only checking len >= tlv_len (if_iwm.c:630-638): a final TLV with unaligned length ending at the image end underflows len to ~SIZE_MAX and the parser keeps reading TLV headers past the firmware image; a file shorter than the 88-byte header underflows len immediately (:620) and the magic check dereferences 4 bytes with no size check (:607). Reproduced at logic level ON THE GUEST with a verbatim userland port of the exact loop: case A (SEC_RT len=5 at image end) prints len=0xfffffffffffffffd then SIGSEGV on the guard page, case B (40-byte file) SIGSEGVs (run.log); the fix arithmetic (len < roundup2(tlv_len,4) + header-size guard) returns clean errors, exit 0 (run_fixed.log). Kernel path itself not exercisable: no iwm PCI device on the guest to bind the driver and load a firmware file (env.txt).
No comments yet.