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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3662 Β· 11 files
FileTypeDescriptionSize
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.md
↓ download 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:

  1. 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 to samples[3] β‡’ denominator samples[4].power - samples[3].power == 0 β‡’ 0/0.
  2. Adjacent equal powers (crafted device): e.g. powers {20,20,12,8,4} with pwr=28 β‡’ walk breaks at samples[0] β‡’ denominator samples[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 -O2 both 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_testable at kernel level; harness-level fix validation passed (see run.stress.log == fixed == block).

Not this finding (checked and killed)

  • wpi_set_timing % val div0 (if_wpi.c:3673-3674): ni->ni_intval == 0 is 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_testable
baseline reproduced→ patch + rebuild →patched clean

Kernel-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)'
↓ fix.diffper-fix-DF-3662

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.