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)
PoC verification
Evidence pack
findings/poc/DF-3578 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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_txpoweris called fromiwn_config(if_iwn.c:6798, every device bring-up),iwn_auth(if_iwn.c:7199) andiwn4965_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_testablefix.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
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.
No comments yet.