Firmware-image walks lack bounds: iwi_load_ucode odd-size size_t underflow (1-2 β SIZE_MAX) walks kernel memory past the image under IWI_LOCK until a page fault; iwi_load_firmware command-block walk reads up to 7 bytes past the DMA buffer and emits up to ~2^19 MMIO command blocks from unvalidated len (DF-3573/3591/3631 firmware-trust family)
Summary
fw->size reaches iwi_load_ucode unvalidated (pre-3.0: datasize-8; v3: arbitrary hdr->usize - the sum check is sound but parity is unchecked). With odd size the loop reads *w one byte past the image and size -= 2 wraps size_t to SIZE_MAX -> unbounded kernel OOB read + permanent lock-held spin until an unmapped page panics the kernel (same pattern as DF-3631 iwm). Separately, iwi_load_firmware's walk never checks 8 header bytes remain (tail reads up to 7 bytes past the DMA allocation) and never bounds len (0xffffffff drives ~524k MMIO command-block writes under the lock). Root-gated (installing a crafted firmware image requires root) - the accepted trust boundary of this audit's firmware-trust family; deterministic panic/hang once loaded. mkbadfw image generator executed on the guest.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3841 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | file | 1.3 KB | β raw | |
| VERDICT.md | file | 3.1 KB | β raw | |
| build.log | file | 85 B | view raw | |
| build.sh | file | 82 B | view raw | |
| env.txt | file | 286 B | view raw | |
| fix.diff | file | 895 B | view raw | |
| manifest.json | file | 612 B | view raw | |
| mkbadfw.c | file | 2.1 KB | view raw | |
| run.log | file | 959 B | view raw | |
| run.sh | file | 102 B | view raw | |
| verdict.json | file | 2.8 KB | view raw |
DF-3841 β iwi(4) firmware-image walks lack bounds validation: (a) iwi_load_ucode() size_t underflow on odd ucode size -> unbounded kernel OOB read + permanent lock-held spin (if_iwi.c:2446-2447); (b) iwi_load_firmware() command-block walk: GETLE32 tail reads up to 7 bytes past the DMA buffer and unvalidated block length drives up to ~2^19 MMIO command-block writes under IWI_LOCK (if_iwi.c:2506-2527). Firmware-trust family: DF-3573 / DF-3591 / DF-3631.
WHAT TO LOOK AT mkbadfw.c userspace generator for a combined (v3) image with usize=9 (odd) β runs anywhere, output blob is the trigger artifact. fix.diff even-size rejection + walk bounds checks. run.log generator executed on the audit guest (RUN_OK).
BUILD cc -O2 -o mkbadfw mkbadfw.c RUN ./mkbadfw iwi_bss_bad EXPECT On hardware with the iwi driver: installing the blob as the iwi_bss firmware (root: fwget/kldload path) and bringing the interface up -> iwi_init_locked -> iwi_load_ucode walks off the image: size becomes SIZE_MAX -> kernel spins reading memory past the firmware buffer until it faults -> panic/hang.
SCOPE NOTE: installing a crafted firmware image requires root, exactly the trust boundary of the DF-3573/3591/3631 family findings (Low).
DF-3841 VERDICT β status: inconclusive (hardware-gated), source-level: CERTAIN
(a) iwi_load_ucode() (if_iwi.c:2404-2473):
2408-2409 const char uc = fw->data; size_t size = fw->size;
2446-2447 for (w = (const uint16_t )uc; size > 0; w++, size -= 2)
MEM_WRITE_2(sc, 0x200010, htole16(w));
size comes, unvalidated, from the image:
pre-3.0 images : fw->size = fp->datasize - sizeof(struct
iwi_firmware_ohdr) (if_iwi.c:2228) β any parity;
3.0+ images : fw_uc.size = le32toh(hdr->usize) (if_iwi.c:2362) β
any u32 (the :2352 size check is computed in size_t
and so is NOT overflowable; parity is still free).
With odd size the final iteration has size == 1: w reads 2 bytes at
uc+size-1 β one byte past the image β and size -= 2 wraps size_t to
SIZE_MAX. The loop then walks the kernel address space after the
firmware buffer, word by word, MMIO-writing each value, holding IWI_LOCK
inside iwi_init, until it hits an unmapped page -> kernel page-fault
panic (or an effectively permanent spin if it maps through). Same
pattern as DF-3631 (iwm_read_firmware TLV len -= roundup2 underflow).
(b) iwi_load_firmware() (if_iwi.c:2478-2527):
2501-2506 src = sc->fw_physaddr; p = sc->fw_virtaddr; end = p + fw->size;
while (p < end) {
dst = GETLE32(p); p += 4; src += 4;
len = GETLE32(p); p += 4; src += 4;
p += len;
- No check that 8 header bytes remain: with < 8 bytes left, GETLE32
reads up to 7 bytes past end β past the fw DMA allocation when this
image is the largest of the three (fw_dma_size == fw->size,
if_iwi.c:3139-3141) β heap OOB read whose value is programmed into
NIC command blocks.
- len is unvalidated: len = 0xffffffff makes the inner while emit
len/IWI_CB_MAXDATALEN (~524k) command blocks Γ 4 MMIO writes under
the lock β a multi-second soft hang from a single image byte pattern.
Why not reproduced: guest has no iwi NIC (run.log) and loading replacement firmware requires root, which is the family's accepted threat model (DF-3573/3591/3631 all filed Low/certain on the same trust boundary). The generator ran successfully on the guest and produced the malformed blob (run.log: "wrote /tmp/iwi_bss_bad: version=300 bsize=16 usize=9 (odd!) fsize=256"). The underflow itself is plain arithmetic: (size_t)1 - 2 == SIZE_MAX.
Severity: Low (root-gated trigger, deterministic panic/hang once triggered).
Fix: fix.diff β reject odd ucode sizes; require 8 remaining bytes for a command-block header and len <= remaining data. Fix validation: not possible on this guest β fix_status not_applicable.
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
not_testableFix validation impossible on this guest: the vulnerable device (AMD radeon CS / Intel 2200BG) does not exist, so neither baseline nor patched kernel can exercise the path. fix.diff authored against the read-only sys/ tree, never applied.
fix.diff in this pack; structurally mirrors the bounds checks the same driver family already enforces elsewhere (radeon_cs_packet_parse fit check; iwn ring-index masking).
Confirmed kernel references
Detail
Evidence (decisive lines)
README.md (what to look at), VERDICT.md (line-by-line trace), trigger.c/mkbadfw.c, build.log (clean guest compile, gcc 8.3), run.log (honest ENOENT / generator RUN_OK), env.txt (guest has no DRI, no iwi NIC), fix.diff
PoC changes
PoC authored fresh for this finding (cross-file family seed): raw DRM ioctl ABI (cmd low byte 0x66 dispatches RADEON_CS on DF drm_ioctl.c:809), chunk structs and packet encodings verified against sys/dev/drm/include/uapi/drm/radeon_drm.h; iwi mkbadfw.c emits the v3 combined-image header per if_iwireg.h:161-166.
Verified recommended fix
Reject odd ucode sizes; require 8 remaining bytes for a command-block header and len <= remaining data.
Verdict
Source-level certain, runtime inconclusive: no iwi NIC on the guest, and installing replacement firmware requires root (the DF-3573/3591/3631 family trust boundary). The malformed-image generator ran on the guest (run.log: wrote /tmp/iwi_bss_bad with usize=9). With odd fw->size the ucode loop's size -= 2 wraps size_t to SIZE_MAX -> unbounded kernel OOB read + lock-held spin until a page fault panics; the load_firmware walk has no tail or len bounds. Same pattern as DF-3631 (iwm TLV underflow).
No comments yet.