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

Division by zero in iwn4965_set_txpower(): interpolate() divisor from unvalidated EEPROM channel samples

Summary

iwn4965_read_eeprom() bulk-loads sc->bands from EEPROM with no validation; iwn4965_set_txpower() then calls interpolate() whose macro divides by chans[1].num - chans[0].num. A malicious/corrupt EEPROM storing the same channel number in both sample slots makes the divisor zero -> #DE -> kernel panic at every device bring-up and on each auth/recalibration. Malicious/tampered Mini-PCIe card surface; persistent DoS. VERIFIED: verbatim-macro harness with volatile-fed EEPROM values - control prints power=40, malicious (36/36) dies SIGFPE exit 136, 4/4 runs (hardened against gcc constant-folding; documented pitfall). Virtio-only guest -> inconclusive/panic.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3578 Β· 12 files
FileTypeDescriptionSize
README.md β€” 2.5 KB ↓ raw
VERDICT.md β€” 3.0 KB ↓ raw
iwn_eeprom_div0.c β€” 3.4 KB view raw
build.sh β€” 104 B view raw
run.sh β€” 98 B view raw
build.log β€” 14 B view raw
run.log β€” 186 B view raw
run.2.log β€” 516 B view raw
env.txt β€” 321 B view raw
fix.diff β€” 707 B view raw
verdict.json β€” 3.3 KB view raw
manifest.json β€” 879 B view raw

DF-3578 β€” Divide-by-zero kernel panic in iwn4965_set_txpower() via malicious EEPROM

What this is

Evidence pack for a division by zero in sys/dev/netif/iwn/if_iwn.c:5707-5715 (iwn4965_set_txpower()), reachable with attacker-controlled EEPROM contents (malicious/corrupt card β€” the pass-2 strategy's "EEPROM parse" surface).

Root cause

iwn4965_read_eeprom() bulk-loads the power-group sample table straight from EEPROM with no validation (if_iwn.c:2314):

iwn_read_prom_data(sc, IWN4965_EEPROM_BANDS, sc->bands, sizeof sc->bands);

iwn4965_set_txpower() then interpolates between the two per-band channel samples (if_iwn.c:5707-5715):

power = interpolate(chan, chans[0].num, chans[0].samples[c][1].power,
                           chans[1].num, chans[1].samples[c][1].power, 1);

with (if_iwn.c:5632-5637):

#define fdivround(a, b, n)   ((((1 << n) * (a)) / (b) + (1 << n) / 2) / (1 << n))
#define interpolate(x, x1, y1, x2, y2, n) \
        ((y1) + fdivround(((int)(x) - (x1)) * ((y2) - (y1)), (x2) - (x1), n))

The divisor is (x2) - (x1) = chans[1].num - chans[0].num, both raw EEPROM bytes. If a malicious EEPROM stores the same channel number in both sample slots, the divisor is 0 β†’ integer divide error β†’ kernel panic on ifconfig wlan0 up (iwn_config β†’ ops->set_txpower, if_iwn.c:6798) and again from iwn_auth (if_iwn.c:7199) / 4965 temperature recalibration.

Contrast: the neighboring EEPROM values are range-checked (maxpwr5GHz/maxpwr2GHz, if_iwn.c:2305-2309) β€” the band sample channels are not.

What this pack proves

iwn_eeprom_div0.c runs the verbatim macros with EEPROM-fed values routed through volatile storage (so the compiler cannot constant-fold the division β€” the first, non-volatile harness version was folded by gcc -O2 and did not fault; see VERDICT.md):

  • control (chans[].num = 36/64): interpolates fine, prints power=40 OK.
  • malicious (chans[].num = 36/36): SIGFPE, exit 136, deterministic 4/4.

In the kernel the same idiv raises #DE β†’ panic. Full in-kernel repro needs a 4965 card with a re-flashed EEPROM, which the audit guest (virtio-only, no Intel WiFi device) cannot provide.

Build / run (on the DragonFly guest)

cc -O2 -Wall -o iwn_eeprom_div0 iwn_eeprom_div0.c
./iwn_eeprom_div0       # expect: control OK, then SIGFPE (exit 136)

Fix

fix.diff β€” guard the interpolation divisor (skip/clamp when chans[1].num <= chans[0].num), and/or validate the band table after iwn_read_prom_data.

VERDICT.md
↓ download raw

DF-3578 β€” VERDICT

Status: inconclusive (runtime; no iwn/4965 hardware on guest), source-certain. Impact: kernel panic (#DE) at device bring-up. Reproduced: the fault primitive.

Root cause (verified in-tree)

  • EEPROM bulk-read without validation: if_iwn.c:2314 (iwn_read_prom_data(sc, IWN4965_EEPROM_BANDS, sc->bands, sizeof sc->bands)).
  • Interpolation divisor from two raw EEPROM bytes: if_iwn.c:5707-5715 calls interpolate(chan, chans[0].num, ..., chans[1].num, ..., 1); the macro (if_iwn.c:5633-5637) divides by (x2) - (x1) β€” zero when both sample slots carry the same channel number.
  • Reachability: iwn4965_set_txpower is called from iwn_config (if_iwn.c:6798, every device bring-up), iwn_auth (if_iwn.c:7199) and iwn4965_power_calibration (if_iwn.c:5619, on temperature change). Nothing between the EEPROM read and the division validates the sample channel numbers.

Threat model: the malicious input is the card's EEPROM/OTPROM β€” i.e. a malicious or tampered Mini-PCIe card inserted into the machine, matching the audit strategy's "EEPROM parse β€” attacker-controlled on malicious card" surface. Severity Low (physical insertion / root-controlled flashing), but the fault itself is unconditional once such EEPROM contents exist.

Reproduction

iwn_eeprom_div0.c executes the verbatim fdivround/interpolate macros with the two divisor operands taken from volatile stores (modeling iwn_read_prom_data() output β€” runtime-opaque to the compiler):

  • control (36/64): prints control (nums 36/64): power=40 OK.
  • malicious (36/36): prints the marker, then SIGFPE exit 136, 4/4 runs (run.log + run.2.log stress 3/3).

PoC iteration note (recorded for honesty): the first harness version kept the sample values in a plain local struct; gcc 8.3 -O2 scalar-replaced the whole struct, constant-folded the compile-time-visible 0/0 UB, and the process survived β€” a classic way a div-by-zero PoC silently degrades. Moving the values behind volatile restored the real hardware idiv fault. The kernel always performs the real division (values come from DMA/MMIO reads).

Why not "reproduced"

Same reason as DF-3576: the guest is virtio-only (no Intel WiFi hardware), so the in-kernel path (EEPROM read β†’ set_txpower) cannot be driven. The arithmetic primitive is proven deterministically; the chain is proven by source.

Fix

fix.diff guards the interpolation: only interpolate when chans[1].num > chans[0].num, else fall back to chans[0]'s sample values. (Belt-and-braces: validate the whole sc->bands table after iwn4965_read_eeprom β€” check lo <= chans[i].num <= hi and increasing sample channel numbers.)

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; kernel-build validation impossible on the virtio-only guest (no 4965 hardware, and the trigger is EEPROM contents). Baseline fault primitive reproduced (SIGFPE exit 136); the guard returns EINVAL before the division.

findings/poc/DF-3578/fix.diff
↓ fix.diffper-fix-DF-3578

Confirmed kernel references

Detail

Exploit chain

malicious/tampered 4965 EEPROM with chans[0].num == chans[1].num in any power group -> machine boots / ifconfig wlan0 up -> iwn4965_read_eeprom loads sc->bands unvalidated -> iwn_config -> iwn4965_set_txpower -> interpolate() divides by 0 -> #DE -> kernel panic (persistent DoS: faults again on every bring-up).

Evidence (decisive lines)

run.log (control OK; 'malicious EEPROM (nums 36/36)' then exit=136 SIGFPE), run.2.log (3/3 stress), iwn_eeprom_div0.c (verbatim macros, volatile-fed operands), VERDICT.md (source trace + constant-folding pitfall), fix.diff.

PoC changes

No seed existed; harness authored in this run. Values fed through volatile after the plain-struct version was constant-folded by gcc -O2 (folded 0/0 does not fault; the hardened version faults 4/4).

Verified recommended fix

Validate chans[1].num > chans[0].num (return EINVAL otherwise) before interpolate() in iwn4965_set_txpower, or validate the whole sc->bands table after the EEPROM bulk read.

Verdict

Division by zero in iwn4965_set_txpower(): interpolate() (if_iwn.c:5636-5637) divides by chans[1].num - chans[0].num, two raw EEPROM bytes bulk-loaded without validation at if_iwn.c:2314; a malicious/corrupt 4965 EEPROM carrying equal sample channel numbers makes the divisor zero and the kernel takes a #DE at device bring-up (iwn_config -> ops->set_txpower, if_iwn.c:6798). The verbatim macros were executed on the DragonFly guest with EEPROM values routed through volatile stores: control (36/64) survives, malicious (36/36) dies SIGFPE exit 136, deterministic 4/4. First harness iteration was silently constant-folded by gcc -O2 (documented in VERDICT.md) and had to be hardened - a real hardware idiv, not a compiler fold. In-kernel execution impossible on the virtio-only guest (no 4965 card), hence inconclusive-at-runtime / certain-at-source.