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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3577 Β· 13 files
FileTypeDescriptionSize
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_NONE page β†’ the 4-byte load at end faults: 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 guard if (len < sizeof(uint32_t)) break; (mirroring the FLAGS case). It parses the same image and returns cleanly β€” no fault (see run.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.

VERDICT.md
↓ download raw

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_NONE guard page β†’ tmp = le32toh(*ptr) reads at end β†’ 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_testable
baseline reproduced→ patch + rebuild →patched clean

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

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.