β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3631

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3631 Β· 13 files
FileTypeDescriptionSize
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:

  1. Trailing unaligned TLV: a final TLV whose length is not 4-aligned and whose data ends exactly at end-of-file passes the len < tlv_len check (len == tlv_len), then len -= roundup2(tlv_len, 4) subtracts more than remains β†’ len wraps to ~SIZE_MAX and data points past the buffer. The loop keeps parsing TLV headers from kernel heap memory beyond the firmware image; whatever it "finds" can pass the per-type checks (e.g. IWM_UCODE_TLV_SEC_RT only needs tlv_len >= 4), causing iwm_firmware_store_section() to record a data pointer outside the image which iwm_pcie_load_section() later memcpys and DMAs to the device (:2396-2446) β€” heap OOB read with attacker-steered length.
  2. Short file: datasize < sizeof(struct iwm_tlv_ucode_header) (88) underflows len immediately (:620); and the magic check at :607 dereferences *(const uint32_t *)fw->fw_fp->data before 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: check len < 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).

VERDICT.md
↓ download raw

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)

  1. tlv_underflow.c β€” port of iwm_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 from if_iwmreg.h (iwm_tlv_ucode_header 88 bytes, iwm_ucode_tlv 8-byte header, IWM_TLV_UCODE_MAGIC 0x0a4c5749, IWM_UCODE_TLV_SEC_RT 19, IWM_UCODE_TLV_NUM_OF_CPU 27).
  2. 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 the firmware image; a garbage TLV of type SEC_*/PAGING etc. can then store an out-of-image pointer that iwm_pcie_load_section() (if_iwm.c:2427-2428) later memcpys and DMAs (OOB read, attacker-sized).
  3. Case B β€” 40-byte file (< sizeof(header)): len = datasize - 88 underflows immediately (:620), SIGSEGV, exit 139 (run.log). (Related: the magic check at :607 reads 4 bytes before any datasize check.)
  4. Fix validation β€” tlv_underflow_fixed.c applies 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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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)
↓ fix.diffper-fix-DF-3631

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).