wpi(4): divide-by-zero in wpi_get_power_index() β interpolate() divisor is the difference of two unvalidated int8 EEPROM TX-power sample bytes (wpi twin of DF-3578)
Summary
wpi_get_power_index() computes idx = interpolate(pwr, sample[0].power, sample[0].index, sample[1].power, sample[1].index, 19) where the macro expands to fdivround with divisor b = sample[1].power - sample[0].power - the difference of two raw int8_t EEPROM bytes bulk-loaded by wpi_read_eeprom_group() with no monotonicity/distinctness validation. Equal adjacent sample powers (crafted device) or an all-zero corrupt EEPROM group (the pwr-walk falls through to samples[3]/[4] which are then equal) make the divisor zero: integer division by zero, kernel #DE, panic. Reached at first device bring-up (wpi_init -> ... -> wpi_set_txpower -> wpi_get_power_index, 12 rate iterations per call), again on every RXON/channel change, and every 60s via wpi_power_calibration. VERIFIED: verbatim-arithmetic harness on the guest - control (monotonic samples) -> idx=77 exit 0; all-zero EEPROM group -> SIGFPE exit 136 (0/0); crafted adjacent-equal powers -> SIGFPE exit 136; deterministic 6/6 faulting runs. Fix validated at harness level (degenerate-sample guard; control output byte-identical). Crafted or corrupt EEPROM on the Intel 3945ABG (malicious PCIe device, physical tampering, flash corruption); panic-only primitive, no memory corruption; matches iwn twin DF-3578's Low rating. Also killed this pass: set_timing bintval=0 (unreachable - net80211 rejects bintval outside [25,1000] at parse AND every consumer drops the frame AND ic_bintval ioctl-range-checked - this ALSO retroactively strengthens DF-3576's net80211 fix scope), firmware TLV parse sound, EEPROM table reads memory-safe, tx_done/cmd_done indices bounded by construction, no ioctl surface.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3662 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| wpi_pwr_div0.c | β | 7.0 KB | view raw | |
| wpi_pwr_div0_fixed.c | β | 7.1 KB | view raw | |
| build.sh | β | 157 B | view raw | |
| run.sh | β | 162 B | view raw | |
| build.log | β | 48 B | view raw | |
| run.log | β | 1.1 KB | view raw | |
| run.stress.log | β | 716 B | view raw | |
| env.txt | β | 236 B | view raw | |
| VERDICT.md | β | 4.8 KB | β raw | |
| fix.diff | β | 1.0 KB | view raw | |
| verdict.json | β | 4.1 KB | view raw |
VERDICT β DF-3662 (wpi_get_power_index interpolate divide-by-zero)
Bottom line
Code-certain, hardware-gated: inconclusive (kernel-level), arithmetic
reproduced 6/6 in the verbatim harness. The divide-by-zero is real,
deterministic, and sits on the mandatory bring-up path of the driver; the
only reason the kernel itself cannot be crashed on our guest is that the
guest has no Intel 3945ABG PCI device (verified: pciconf -l shows zero
vendor=0x8086 entries; ifconfig -l = vtnet0 lo0). This mirrors the
treatment of the iwn twins DF-3576/DF-3578 (same class, same guest
limitation, same harness methodology).
Root cause (path:line)
sys/dev/netif/wpi/if_wpi.c:3812-3813:
idx = interpolate(pwr, sample[0].power, sample[0].index,
sample[1].power, sample[1].index, 19);
interpolate() (if_wpi.c:3782-3783) expands to fdivround(a, b, 19) =
(((1<<19)*(a))/(b) + (1<<19)/2) / (1<<19) β an integer division whose
denominator b = sample[1].power - sample[0].power is the difference of
two raw int8_t EEPROM bytes. wpi_read_eeprom_group() (if_wpi.c:1574-1610)
bulk-loads the five groups with no monotonicity/distinctness validation, and
the sample-walk (if_wpi.c:3808-3810) only guarantees the loop stops inside
group->samples[3..4]; it does not guarantee the two selected samples differ
in power.
Two concrete zero-divisor shapes:
- All-zero / corrupt EEPROM group (flash corruption on a genuine card):
every
sample[].power == 0βpwr (=0) > sample[1].power (=0)never true β walk falls through tosamples[3]β denominatorsamples[4].power - samples[3].power == 0β0/0. - Adjacent equal powers (crafted device): e.g. powers
{20,20,12,8,4}withpwr=28β walk breaks atsamples[0]β denominatorsamples[1].power - samples[0].power == 0.
In-kernel this is a #DE on the DIV instruction β kern_trap/trap fatal
β kernel panic on ifconfig wpi0 up, on every channel change, and every
60 s calibration tick (wpi_calib_timeout β wpi_power_calibration β
wpi_set_txpower, if_wpi.c:1887-1897, 3714).
Reachability / threat model
- Requires the EEPROM content of the card to be crafted or corrupt. The EEPROM is on-device; a local unprivileged user cannot write it through any driver path (no ioctl surface at all in wpi; sysctl is debug-only and compiled out without WPI_DEBUG). β attacker = malicious PCIe device masquerading as 3945ABG, physical EEPROM tampering, or flash corruption.
- Severity therefore Low (crafted-hardware DoS), matching DF-3578.
What was run (guest: DragonFly 6.5-DEVELOPMENT, cc 8.3)
build.shβcc -O2both variants (build.log, zero warnings).run.sh+ 3-run stress (run.log,run.stress.log):- baseline case A (realistic monotonic samples):
idx=77, exit 0 β driver behaves normally on valid EEPROM. - baseline case B (all-zero group): SIGFPE, exit 136 (0/0).
- baseline case C (adjacent equal powers): SIGFPE, exit 136.
- Deterministic 6/6 faulting runs, 3/3 control survivals.
wpi_pwr_div0_fixed.c(fix.diff guard applied): A/B/C all exit 0, control output identical (idx=77) β fix changes nothing for valid EEPROM and removes the fault for degenerate samples. Fix validated at arithmetic-harness level only (no kernel build possible without the device; the guard is 4 lines whose kernel effect is exactly the tested arithmetic).
Verdict fields
- status:
inconclusive(kernel-level repro impossible: no wpi hardware; arithmetic-level repro deterministic β iwn-twin convention DF-3576/3578) - impact:
panic - confidence:
certain(code path + arithmetic both proven from source) - fix_status:
not_testableat kernel level; harness-level fix validation passed (see run.stress.log== fixed ==block).
Not this finding (checked and killed)
wpi_set_timing% valdiv0 (if_wpi.c:3673-3674):ni->ni_intval == 0is unreachable in this tree β net80211 rejects beacon bintval outside [25,1000] at parse time (ieee80211_input.c:700-707, IEEE80211_BPARSE_BINTVAL_INVALID; every consumer drops the frame: ieee80211_sta.c:1332, ieee80211_adhoc.c:701, ieee80211_hostap.c:1707, ieee80211_mesh.c:1904) and the only other writer,ic->ic_bintval, is ioctl-range-checked (ieee80211_ioctl.c:3021-3023, default 100 at ieee80211.c:372). The DF-3576 premise ("scan consumer stores it anyway") does not hold in the current tree. Driver-side zero-guard still recommended as defense-in-depth.
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_testableKernel-level fix build impossible on this guest (no wpi PCI device; a kernel with the fix cannot attach it). Harness-level fix validation performed: wpi_pwr_div0_fixed.c applies the exact fix.diff guard and survives all three cases (A/B/C rc=0) while the baseline faults 6/6 on B/C; control case A output identical (idx=77), so valid EEPROM behavior is unchanged by the fix.
run.stress.log '== fixed ==' block: fixed A/B/C rc=0 vs baseline run1-3 B/C rc=136; run.log fixed-case outputs 'SURVIVED (no SIGFPE)'
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log: baseline case B 'Floating point exception (core dumped) -- exit 136' (0/0 from all-zero EEPROM group); case C exit 136 (adjacent equal powers); case A survives idx=77. run.stress.log: 6/6 faulting runs across 3 iterations, fixed variant A/B/C all rc=0. build.log: clean cc -O2 -Wall. env.txt: DragonFly 6.5-DEVELOPMENT, cc 8.3, ifconfig vtnet0 lo0 only, pciconf intel count 0 (hardware absent), kldstat pci/wpi present.
PoC changes
Original sketch (none existed - new finding) authored directly as verbatim-arithmetic harness: fdivround/interpolate macros, sample walk, ridx switch, clamp and EEPROM group bulk-load copied 1:1 from if_wpi.c:3773-3835 and 1574-1610; three cases (realistic control / all-zero corrupt group / crafted adjacent-equal powers).
Verified recommended fix
Guard degenerate samples before interpolate(): if (sample[1].power == sample[0].power) idx = sample[0].index; else idx = interpolate(...); (see fix.diff; validated at harness level - fixed variant survives all cases, control output unchanged)
Verdict
Code-certain, hardware-gated: wpi_get_power_index() (if_wpi.c:3812) feeds interpolate()/fdivround() (if_wpi.c:3782-3783) two raw int8_t EEPROM sample powers as (x2)-(x1); when they are equal (all-zero corrupt EEPROM group, or crafted adjacent-equal powers) the integer division by zero raises #DE and panics DragonFly at device bring-up (wpi_config -> wpi_set_txpower), on every channel change and every 60s calibration tick. Guest has the wpi driver compiled in (kldstat pci/wpi) but zero Intel PCI devices (pciconf vendor=0x8086 count 0), so no in-kernel repro is possible; verbatim-arithmetic harness reproduces deterministically 6/6 (SIGFPE exit 136 cases B/C; control case A survives idx=77), matching the DF-3576/DF-3578 iwn-twin methodology. Threat requires a crafted/corrupt EEPROM (malicious PCIe device masquerading as 3945ABG or flash corruption) - Low severity crafted-hardware DoS.
No comments yet.