4-byte heap OOB read in iwn_read_firmware_tlv(): IWN_FW_TLV_PHY_CALIB dereferences *ptr without a len>=4 check
Summary
The TLV loop's only bound check accepts payloads of 0..3 bytes, but the IWN_FW_TLV_PHY_CALIB case unconditionally executes tmp = le32toh(*ptr). A zero/short PHY_CALIB TLV terminating the firmware image reads up to 4 bytes past the end of the kernel heap allocation holding the image. The sibling IWN_FW_TLV_FLAGS case shows the intended guard - PHY_CALIB simply lacks it. VERIFIED: guard-page harness on the guest - baseline dies SIGSEGV exit 139 (4/4), fixed variant parses the identical image cleanly. Whoever controls the root-installed firmware file; the 4 adjacent-heap bytes only condition radio gain values; no userspace-visible disclosure path.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3577 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.6 KB | β raw | |
| VERDICT.md | β | 3.1 KB | β raw | |
| iwn_tlv_oob.c | β | 5.8 KB | view raw | |
| iwn_tlv_oob_fixed.c | β | 5.8 KB | view raw | |
| build.sh | β | 153 B | view raw | |
| run.sh | β | 127 B | view raw | |
| build.log | β | 292 B | view raw | |
| run.log | β | 439 B | view raw | |
| run.2.log | β | 516 B | view raw | |
| env.txt | β | 321 B | view raw | |
| fix.diff | β | 427 B | view raw | |
| verdict.json | β | 3.9 KB | view raw | |
| manifest.json | β | 1.0 KB | view raw |
DF-3577 β 4-byte heap OOB read in iwn_read_firmware_tlv() (IWN_FW_TLV_PHY_CALIB)
What this is
Evidence pack for an out-of-bounds read in the iwn firmware TLV parser,
sys/dev/netif/iwn/if_iwn.c:8315-8320:
case IWN_FW_TLV_PHY_CALIB:
tmp = le32toh(*ptr); /* if_iwn.c:8316 β no len check! */
if (tmp < 253) {
sc->reset_noise_gain = tmp;
sc->noise_gain = tmp + 1;
}
break;
The generic TLV bound check (ptr + len > end, if_iwn.c:8280) accepts any
len, including 0..3 β but the case unconditionally loads 4 bytes at ptr.
When a PHY_CALIB TLV with len < 4 sits at the end of the firmware image,
the load reads up to 4 bytes past the end of the kernel heap allocation
holding the image (fw->data from firmware_get(), if_iwn.c:8375-8385).
The sibling case IWN_FW_TLV_FLAGS (if_iwn.c:8327-8331) shows the intended
pattern β it rejects len < sizeof(uint32_t).
What this pack proves
iwn_tlv_oob.c is a line-for-line copy of the parsing loop of
iwn_read_firmware_tlv() (if_iwn.c:8271-8358; diagnostics dropped,
sc-> fields modelled as locals). The crafted image is 96 bytes:
88-byte TLV header + one 8-byte TLV (type=15 PHY_CALIB, alt=0, len=0)
ending exactly at the end of the buffer.
- guard-page mode (default): the image is placed flush against a
PROT_NONEpage β the 4-byte load atendfaults: SIGSEGV, exit 139, deterministic 4/4 runs. - ASAN mode: provided for environments with libasan (the audit guest's
base gcc 8.3 has none β
cannot find -lasanβ so guard mode is primary). - fixed variant (
iwn_tlv_oob_fixed.c): adds the one-line guardif (len < sizeof(uint32_t)) break;(mirroring the FLAGS case). It parses the same image and returns cleanly β no fault (seerun.log). This is the fix diff validated at harness level.
Build / run (on the DragonFly guest)
cc -O2 -Wall -o iwn_tlv_oob iwn_tlv_oob.c cc -O2 -Wall -o iwn_tlv_oob_fixed iwn_tlv_oob_fixed.c ./iwn_tlv_oob guard # expect: exit 139 (SIGSEGV at guard page) ./iwn_tlv_oob_fixed guard # expect: clean return, no fault
Impact
Kernel heap OOB read of β€4 bytes; the value only conditions
sc->reset_noise_gain / sc->noise_gain (calibration command codes sent to
the NIC) β no demonstrated path back to userspace. Attacker = whoever
controls the firmware file (/boot/firmware/iwn*-fw, root-installed) or a
corrupted download. Severity Low, but it is a genuine memory-safety defect
and a correctness bug (garbage calib codes).
Fix
fix.diff β add the same length guard the FLAGS case already has.
DF-3577 β VERDICT
Status: reproduced (parser logic exercised verbatim at userland, fault deterministic). Impact ceiling on its own: none demonstrated beyond the OOB read (Low). Confidence: certain.
Root cause
iwn_read_firmware_tlv() walks TLVs with the single generic bound check
ptr += sizeof (*tlv);
if (ptr + len > end) /* if_iwn.c:8280 β accepts len == 0..3 */
return EINVAL;
and then dispatches on tlv->type. The IWN_FW_TLV_PHY_CALIB case
(if_iwn.c:8315-8320) executes tmp = le32toh(*ptr) without checking
len >= 4, so a TLV whose payload is shorter than 4 bytes β or has zero
length and terminates the image β makes the 4-byte load run past end
(the end of the firmware image resident in a kernel heap buffer obtained
from firmware_get(), if_iwn.c:8375-8385). The IWN_FW_TLV_FLAGS case
(if_iwn.c:8327-8331) proves the author knew the pattern: it rejects
len < sizeof(uint32_t) and len % sizeof(uint32_t) != 0.
The parser runs at every ifconfig wlan0 up (iwn_init_locked β
iwn_read_firmware β iwn_read_firmware_tlv, if_iwn.c:8933β8362β8397).
Reproduction
Harness iwn_tlv_oob.c copies the parsing loop line-for-line (structs from
if_iwnreg.h:1721-1754). Crafted 96-byte image: valid TLV header (signature
"IWL\n") + one TLV {type=PHY_CALIB(15), alt=0, len=0} as the final 8 bytes.
- baseline: image mapped to end flush at a
PROT_NONEguard page βtmp = le32toh(*ptr)reads atendβ SIGSEGV, exit 139 β 4/4 runs (run.log, run.2.log stress 3/3). - ASAN variant exists; the guest's gcc 8.3 lacks libasan
(
ld.gold: cannot find -lasan), so guard-page mode is the recorded evidence. (build.log) - fixed variant (
iwn_tlv_oob_fixed.c, one added guard): same image parsed, returns cleanly, no fault β the fix eliminates the OOB at harness level. Kernel-level fix validation is impossible on this guest (no iwn hardware to load a patched kernel for), hence fix_status=not_testable with harness-level baseline/patched evidence recorded.
Why this is faithful
The vulnerable code is pure parsing over a kernel-resident buffer; the
harness executes the identical statements on an identical input. The only
difference is the allocator (kmalloc vs malloc/mmap), which affects only
whether the OOB hits unmapped memory β the read past end is the defect,
and it is unconditional.
Exploit chain
None developed: the 4 adjacent-heap bytes only influence calibration command codes sent to the radio. No userspace-visible disclosure path exists in the driver. Reported as a memory-safety defect (CWE-125) with Low impact.
Fix
fix.diff: mirror the FLAGS-case guard:
case IWN_FW_TLV_PHY_CALIB:
+ if (len < sizeof (uint32_t))
+ break;
tmp = le32toh(*ptr);
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.diff authored against the read-only sys/ tree and validated at harness level: baseline harness faults (SIGSEGV 139) on the crafted TLV, fixed harness (identical code + the one-line guard) parses the same image cleanly. Kernel-build validation impossible on this guest (no iwn hardware to load a patched kernel against; the PoC is a userland model of pure parsing code), so fix_status is not_testable rather than fixed.
run.log decisive run C (fixed variant, no fault) vs run B (baseline, SIGSEGV); findings/poc/DF-3577/fix.diff
Confirmed kernel references
Detail
Exploit chain
root-controlled or corrupted iwn firmware file with trailing PHY_CALIB TLV len<4 -> firmware_get loads image into kernel heap -> iwn_read_firmware_tlv reads 4 bytes past allocation end at if_iwn.c:8316 -> OOB heap read (value conditions sc->reset_noise_gain/sc->noise_gain; no leak path to userspace).
Evidence (decisive lines)
run.log (baseline: 'guard mode ... TLV payload len=0' then exit=139 SIGSEGV; fixed variant: 'UNEXPECTED: parser returned without OOB' = clean return, no fault), run.2.log (stress 3/3 baseline faults), build.log (clean build; ASAN unavailable: cannot find -lasan), iwn_tlv_oob.c (verbatim parser copy), iwn_tlv_oob_fixed.c (fix applied), fix.diff.
PoC changes
No seed existed; harness authored in this run: verbatim copy of iwn_read_firmware_tlv parsing loop with struct defs from if_iwnreg.h:1721-1754, guard-page detection mode (ASAN unavailable on guest), plus a fixed variant carrying the proposed one-line guard.
Verified recommended fix
Add 'if (len < sizeof(uint32_t)) break;' to the IWN_FW_TLV_PHY_CALIB case, mirroring IWN_FW_TLV_FLAGS (if_iwn.c:8328).
Verdict
4-byte heap OOB read in the iwn firmware TLV parser: the IWN_FW_TLV_PHY_CALIB case (if_iwn.c:8315-8320) executes 'tmp = le32toh(*ptr)' with no 'len >= sizeof(uint32_t)' check, although the generic bound check (if_iwn.c:8280) accepts TLV payloads of 0..3 bytes; a zero-length PHY_CALIB TLV terminating the image makes the load run past the end of the kernel heap buffer holding the firmware (firmware_get, if_iwn.c:8375). Reproduced with a line-for-line copy of the parser loop on the DragonFly guest: image placed flush against a PROT_NONE page, process dies SIGSEGV exit 139, deterministic 4/4 (guest gcc 8.3 lacks libasan, so guard-page mode is the recorded evidence; ASAN variant provided). The one-line fix (mirror the IWN_FW_TLV_FLAGS length guard, if_iwn.c:8328) was validated in the same harness: fixed variant parses the identical image and returns cleanly. No userspace-visible disclosure path exists (the value only feeds calibration command codes), so impact is rated none/Low despite the memory-safety violation.
No comments yet.