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

rssadapt_rate returns/stores rix=-1 (fresh node RSSI=0 or converged thresholds): rs_rates[-1] read, iwm_setrates KASSERT panic on INVARIANTS kernels, and u_int-indexed ~+8GiB wild read+write in rssadapt_lower_rate β€” the negative-side residual of DF-0730

Summary

The rate-pick loop exits with rix=-1 whenever no threshold is STRICTLY below rssi<<8 - deterministically for any fresh node (RSSI 0 vs M_ZERO'd thresholds) and after threshold convergence under signal decay. rix=-1 then reads rs_rates[-1] (in-struct), is stored in ra_rix and returned raw to ratectl callers - iwm_setrates KASSERTs (INVARIANTS panic); and on the next failed TX rssadapt_lower_rate - which declares 'u_int rix' - turns -1 into 0xffffffff so (*thrs)[rix] reads AND writes at chunk+~8GiB: wild 2-byte access (page-fault panic or genuine 2-byte wild heap write where the direct map covers it). LATENT: wlan_rssadapt optional, not in GENERIC, no in-tree driver selects it (default AMRR); guest has no radio. Proven via a code-level harness replicating the exact layout (with DF-0730's resize applied so only the negative index is under test): rix=-1, rs_rates[-1] read, KASSERT condition VIOLATED, wild access at chunk+0x200000032 caught by a SIGSEGV trapper ('RESULT: BUG REPRODUCED'); fixed variant clean. Fix: clamp rix to 0 at loop exhaustion + refuse to adapt when ra_rix<0 in lower_rate (the u_int hazard).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3459 Β· 11 files
FileTypeDescriptionSize
README.md β€” 3.2 KB ↓ raw
VERDICT.md β€” 5.8 KB ↓ raw
df3459_harness.c β€” 10.8 KB view raw
build.sh β€” 93 B view raw
run.sh β€” 89 B view raw
run.log β€” 945 B view raw
run.fixed.log β€” 734 B view raw
fix.diff β€” 1.2 KB view raw
fix_module_build.log β€” 7.4 KB view raw
env.txt β€” 377 B view raw
verdict.json β€” 5.7 KB view raw

DF-3459 β€” rssadapt rix = -1: negative rate index β†’ ~+8GiB wild read+write (u_int) in rssadapt_lower_rate

File: sys/netproto/802_11/wlan/ieee80211_rssadapt.c Verdict: REPRODUCED (code-level harness; module latent on this guest β€” no WiFi radio, no in-tree driver selects RSSADAPT, identical reachability profile as DF-0730/DF-0731). Severity filed: Medium (latent) β€” would be High if a driver wired RSSADAPT up.

What the bug is

rssadapt_rate() (ieee80211_rssadapt.c:253-255) picks the rate index with

for (rix = rs->rs_nrates-1; rix >= 0; rix--)
        if ((*thrs)[rix] < (rssi << 8))
                break;

The comparison is strict. The loop falls out with rix == -1 whenever no rate row has a threshold strictly below rssi<<8:

  • rssi == 0 β€” the normal state of every freshly created node: node_getrssi() (ieee80211_node.c:1146-1155) returns 0 while ni_avgrssi is still IEEE80211_RSSI_DUMMY_MARKER, and clamps negatives to 0. All thresholds are 0 (M_ZERO allocation) β†’ 0 < 0 is false for every row β†’ deterministic rix=-1 on the first rate query of any new node.
  • threshold convergence β€” rssadapt_lower_rate() raises thresholds toward rssi<<8 with integer interpolation that can converge to exactly rssi<<8 (e.g. a station's signal decays below every previously-raised threshold).

With rix == -1:

  • :258 ni->ni_rates.rs_rates[-1] β€” in-struct OOB read (returns the rs_nrates byte) β†’ bogus ni_txrate;
  • :259 ra->ra_rix = -1 is stored; -1 is returned to every caller of ieee80211_ratectl_rate() β€” e.g. if_iwm.c:3540/3574 feed it straight into iwm_setrates(), whose first line is KASSERT(rix >= 0 && rix < nrates) (if_iwm.c:4366) β†’ guaranteed panic on INVARIANTS kernels;
  • a subsequent failed TX completion (rssadapt_tx_complete β†’ rssadapt_lower_rate) hits the killer: :279 declares u_int rix;, so rix = ra->ra_rix makes 0xffffffff, and (*thrs)[rix] at :284/:285 addresses

    chunk + 2 * 0xffffffff = chunk + 0x1fffffffe (~ +8 GiB)

β†’ a WILD 2-byte read followed by a WILD 2-byte write at a fixed ~+8GiB offset from the per-node heap chunk. Unmapped there (the usual case) β†’ kernel page fault β†’ panic. On boxes whose direct map covers chunk+8GiB β†’ genuine 2-byte wild heap corruption with a semi-attacker-influenced value (interpolate(last_thr, rssi<<8)).

This is the negative-side residual of DF-0730 (which fixed the high-side [8] vs rs_nrates<=15 bound; its fix.diff leaves rix<0 untouched).

Reproduce

./build.sh          # buggy (as shipped)
./run.sh            # β†’ RESULT: BUG REPRODUCED …
./build.sh fixed    # applies this pack's fix.diff logic
./run.sh            # β†’ RESULT: no negative-index effect observed (fix active)

Build/run on the audit guest (DragonFly 6.5-DEVELOPMENT, stock INVARIANTS #0 kernel): run.log (buggy) vs run.fixed.log.

Fix validation

  • fix.diff β€” git apply-clean against the sys/ tree; the patched wlan_rssadapt module builds clean with full kernel -Werror CFLAGS (fix_module_build.log, rc=0).
  • Harness fixed mode shows all three effects gone (no rix<0, no KASSERT violation, no wild access).
VERDICT.md
↓ download raw

DF-3459 β€” VERDICT

Status: REPRODUCED (code-level harness, latent module on this guest) Impact: panic (INVARIANTS via iwm KASSERT; production via ~+8GiB wild access in rssadapt_lower_rate) / 2-byte wild heap write where chunk+8GiB is mapped Confidence: certain (source-level arithmetic; harness run on the audit guest)

How it reproduces

df3459_harness.c replicates the exact struct layout (with DF-0730's resize already applied β€” 15 columns β€” so the only negative-index effect demonstrated is this finding's) and the exact code paths:

  • rssadapt_rate() loop as shipped (ieee80211_rssadapt.c:253-255);
  • :258 rs_rates[rix] read; :259 ra_rix store;
  • rssadapt_lower_rate() as shipped (:279 u_int rix, :283 rix = ra->ra_rix, :284/:285 (*thrs)[rix] read+write).

Buggy run (run.log, guest DragonFly dfbsd 6.5-DEVELOPMENT #0, cc 8.3):

rssi=0 fresh node : rssadapt_rate -> rix=-1  ra_rix=-1  ni_txrate=0x08 (bogus: rs_nrates byte 8)
  -> rs_rates[-1] read returned the rs_nrates byte (1): in-struct OOB read confirmed
  -> iwm_setrates KASSERT(rix >= 0 && rix < nrates): VIOLATED  (INVARIANTS kernel would PANIC here)
lower_rate with ra_rix=-1: u_int rix = 0xffffffff -> access at chunk+0x200000032 (wild +7 GiB)
  -> WILD ACCESS confirmed: kernel equivalent is a ~+8GiB READ+WRITE from the rssadapt_node chunk -> kernel page fault -> PANIC
RESULT: BUG REPRODUCED β€” rix=-1: in-struct rs_rates[-1] read, iwm KASSERT violation, and u_int-indexed ~+8GiB wild read+write in rssadapt_lower_rate()

The wild access is performed in a forked child with a SIGSEGV trapper so the harness can report the fault the kernel cannot catch. The computed address chunk+0x200000032 = offsetof(ra_rate_thresh)=52 + 2Γ—0xffffffff β€” exactly the kernel's arithmetic.

Why rix = -1 is reachable in the field (not just theory)

  • node_getrssi() (ieee80211_node.c:1146-1155) returns 0 while ni_avgrssi == IEEE80211_RSSI_DUMMY_MARKER (node created, no RX frame sampled yet) and clamps negative RSSI to 0. Fresh node + zeroed thresholds (M_ZERO, ieee80211_rssadapt.c:184-185) β†’ (*thrs)[rix] < (rssi << 8) is 0 < 0 for every row β†’ loop exhausts β†’ rix = -1 on the first rate query of every new node with an unsampled RSSI.
  • Sustained-decay scenario: rssadapt_lower_rate raises thresholds toward the failure-time rssi<<8 with interpolate() = (4*old+4*new)/8, which converges to exactly rssi<<8; the comparison is strict <, so once every row β‰₯ current rssi<<8 the next rssadapt_rate again exhausts the loop.

Consequence chain (as shipped)

  1. rix = -1 returned by .ir_rate β†’ iwm_setrates(sc, in, -1) (if_iwm.c:3540-3544, :3574-3578, :4646) β†’ KASSERT(rix >= 0 && rix < nrates) (if_iwm.c:4366) β†’ fatal kernel panic on INVARIANTS kernels. Other drivers ignore the return ((void) ieee80211_ratectl_rate(...) β€” rt2560/2661/2860, wpi, iwn).
  2. ra_rix = -1 stored β†’ next failed TX β†’ rssadapt_lower_rate: u_int rix = 0xffffffff β†’ (*thrs)[rix] = read+write at chunk+0x1fffffffe (~+8 GiB). Unmapped KVA β†’ page fault β†’ panic on production kernels. Mapped (large-RAM direct map) β†’ 2-byte wild write with value interpolate(last_thr, rssi<<8) (rssi is the remote peer's signal strength β€” semi-attacker-influenced).
  3. ni_txrate = rs_nrates & 0x7f (bogus rate code, in-struct read at :206 for empty ratesets too).

Latency caveat (why Medium, not High)

Identical to DF-0730/DF-0731: wlan_rssadapt is optional (sys/conf/files:1655), NOT in X86_64_GENERIC, no in-tree driver calls ieee80211_ratectl_set(vap, RSSADAPT) (default is AMRR, ieee80211_ratectl.c:121-122), and the audit guest has no WiFi radio (ifconfig -l β†’ vtnet0 lo0). Runtime verification on this guest is therefore a hard blocker; the code-level harness plus source trace is the strongest demonstration available. If a driver wires RSSADAPT up, this is a remote-triggerable (any TX failure on a fresh/zero-RSSI node) ~+8GiB wild write β€” High.

Exploit chain

Not developed: module cannot be loaded/selected on the audit guest (no radio, no selector). In a hypothetical wired-up deployment the primitive would be: fresh-node RSSI=0 β†’ forced TX failure (remote deauth/flood) β†’ wild 2-byte write at ra+0x1fffffffe β€” a fixed-offset corruption with a bounded controlled value; on typical RAM footprints the offset lands in unmapped KVA β†’ DoS (panic) is the realistic ceiling.

Fix validation

  • fix.diff: clamp rix = 0 when the pick loop exhausts (:253), bail on empty ratesets in rssadapt_node_init, and defensively refuse to adapt when ra_rix < 0 in rssadapt_lower_rate (the u_int hazard).
  • Applied to the guest's /usr/src copy: wlan_rssadapt module rebuilds clean (make rc=0, full kernel -Werror CFLAGS β€” fix_module_build.log).
  • Harness fixed mode: rix=0, no KASSERT violation, no fault, canaries intact (run.fixed.log).
  • Guest source restored to pristine afterwards (sha256 verified against the audit tree).

Kernel references

sys/netproto/802_11/wlan/ieee80211_rssadapt.c:202-206 (node_init pick loop / empty rateset) sys/netproto/802_11/wlan/ieee80211_rssadapt.c:253-259 (rate pick loop, rs_rates[rix], ra_rix store) sys/netproto/802_11/wlan/ieee80211_rssadapt.c:279,283-286 (u_int rix; wild read+write) sys/netproto/802_11/wlan/ieee80211_node.c:1146-1155 (node_getrssi returns 0 / clamps) sys/dev/netif/iwm/if_iwm.c:3540,3574,4366,4646 (KASSERT consumer) sys/netproto/802_11/ieee80211_ratectl.h:91-95 (unclamped ir_rate return)

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applies cleanly (git apply --check on the guest's /usr/src); the patched wlan_rssadapt module rebuilds with full kernel -Werror CFLAGS (rc=0, fix_module_build.log); harness fixed-mode shows rix=0, no iwm KASSERT violation, and no wild access (run.fixed.log). Module cannot be exercised at runtime on this guest (no radio), so validation is compile + harness-level, matching the DF-0730 precedent.

['findings/poc/DF-3459/fix.diff', 'findings/poc/DF-3459/fix_module_build.log', 'findings/poc/DF-3459/run.fixed.log']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

Hypothetical wired-up deployment only (module unselectable in-tree): remote peer associates (fresh node, RSSI unsampled -> 0) -> attacker forces TX failure (deauth/RTS flood) -> rssadapt_lower_rate wild 2-byte write at ra+0x1fffffffe with value interpolate(last_thr, rssi<<8). Fixed-offset, bounded-value corruption; realistic ceiling is DoS panic (offset usually unmapped). No chain developed on the guest: hard blocker (module cannot be loaded/selected β€” no radio, no ratectl selector).

Evidence (decisive lines)

['findings/poc/DF-3459/run.log β€” buggy run: rix=-1, rs_rates[-1] read confirmed, iwm KASSERT condition VIOLATED, wild access at chunk+0x200000032 (~+8GiB) caught by SIGSEGV trapper', 'findings/poc/DF-3459/run.fixed.log β€” fix.diff logic active: rix=0, no KASSERT violation, no fault, canaries intact', 'findings/poc/DF-3459/fix_module_build.log β€” patched wlan_rssadapt.ko builds clean, full kernel -Werror CFLAGS, rc=0', 'findings/poc/DF-3459/df3459_harness.c β€” exact struct layout (DF-0730 resize applied) + exact kernel code paths']

PoC changes

Harness is self-authored (no seed). Two iterations were needed: (1) canary region initially misaligned the struct (zone+1) causing an unrelated SSE fault β€” moved to a 32-byte-aligned slot; (2) the wild access is performed in a forked child with a SIGSEGV trapper (_exit(42)) so the fault the kernel cannot catch is reported instead of silently killing the harness; buggy/fixed modes selected by build.sh arg, following the DF-0730 pack conventions.

Verified recommended fix

Clamp rix to 0 when the threshold pick loop exhausts, refuse to adapt when ra_rix < 0 in rssadapt_lower_rate (u_int hazard), and bail out of rssadapt_node_init on empty ratesets β€” see findings/poc/DF-3459/fix.diff.

Verdict

REPRODUCED (code-level harness; module latent on this guest). rssadapt_rate (ieee80211_rssadapt.c:253-255) exhausts its pick loop with rix=-1 whenever no threshold is STRICTLY below rssi<<8 β€” deterministically for any fresh node because node_getrssi (ieee80211_node.c:1146-1155) returns 0 while ni_avgrssi is IEEE80211_RSSI_DUMMY_MARKER and all M_ZERO'd thresholds are 0 (0<0 false for every row), and after threshold convergence under signal decay. rix=-1 then (a) reads rs_rates[-1] at :258 (in-struct, returns the rs_nrates byte into ni_txrate), (b) is stored into ra_rix and returned to callers β€” if_iwm.c:3540/3574 feeds it to iwm_setrates whose KASSERT(rix>=0&&rixra_rix) turns -1 into 0xffffffff so (*thrs)[rix] at :284/:285 reads AND writes at chunk+0x1fffffffe (~+8GiB) β€” a wild 2-byte read+write: page-fault panic on unmapped KVA, genuine wild heap corruption where the direct map covers chunk+8GiB. Negative-side residual of DF-0730 (its fix resized the array but left rix<0 untouched). Latent: wlan_rssadapt is optional, not in GENERIC, no in-tree driver selects it, guest has no radio β€” same blocker class as DF-0730/DF-0731.