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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3841 Β· 11 files
FileTypeDescriptionSize
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
README.md file
↓ download 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).

VERDICT.md file
↓ download raw

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_testable
baseline no→ patch + rebuild →patched clean

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

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