amrr never revalidates stale amn_rix against the active rateset: HTβlegacy flip gives OOB reads of node memory (incl. ni_chan kernel pointer bytes) disclosed via ni_txrate/rate_stats, and hands drivers an unbounded/negative rate index
Summary
amrr_node_init computes amn_rix once against whichever rateset amrr_node_is_11n() selects at that moment - the 77-entry ni_htrates for an 11n node (rix up to 76), or -1 when both ratesets are empty. amrr_node_is_11n() is re-evaluated per call and flips to the 15-entry legacy ni_rates whenever ni_chan loses HT (channel switch, e.g. DFS radar CSA) or ni_htrates.rs_nrates is cleared (reassoc without HT IEs) - but amn_rix is never re-clamped. amrr_update, amrr_rate and amrr_node_stats then index rs_rates[76] on the 15-byte legacy array; the byte flows masked (&0x7f) into ni_txrate -> SIOCG80211NODE isi_txrate and the world-readable net.wlanX.rate_stats sysctl. amrr_rate returns the raw rix to drivers: run(4) indexes its 12-entry rt2860_rates[] (static OOB read), bwn reads rs_rates[rix-1], iwm_setrates KASSERT panics INVARIANTS kernels with the -1 tmp-node value. amrr is the DEFAULT ratectl and compiled into the base kernel. REPRODUCED on QEMU guest via a KLD harness driving only the public ratectl API: climb rix to 76 on 11n, flip ni_chan to legacy -> ratectl_rate() returns rix=76 vs rs_nrates=12; failure-driven walk-down reads every OOB byte - all 8 bytes of the ni_chan kernel heap pointer (masked to 7 bits in the sysctl) and planted markers disclosed; empty-rateset tmp node returns rix=-1. Fix validated on rebuilt kernel: rix 76->11 and -1->0, zero OOB reads. Read-only intra-object (12,208-byte node); no write primitive.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3454 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| amrroob.c | β | 10.7 KB | view raw | |
| Makefile | β | 238 B | β download | |
| build.sh | β | 153 B | view raw | |
| run.sh | β | 166 B | view raw | |
| build.log | β | 5.9 KB | view raw | |
| run.log | β | 582 B | view raw | |
| run.2.log | β | 10.3 KB | view raw | |
| fix.diff | β | 2.0 KB | view raw | |
| fix_module_only.log | β | 7.4 KB | view raw | |
| fix_module_only_attempt1.log | β | 3.7 KB | view raw | |
| fix_run.log | β | 1.8 KB | view raw | |
| fix_build.log | β | 12.7 KB | view raw | |
| env.txt | β | 212 B | view raw | |
| VERDICT.md | β | 6.2 KB | β raw | |
| README.md | β | 2.3 KB | β raw | |
| manifest.json | β | 1.5 KB | view raw | |
| verdict.json | β | 6.6 KB | view raw |
DF-3454 β amrr stale rate index (amn_rix) never revalidated against the active rateset
File: sys/netproto/802_11/wlan/ieee80211_amrr.c
Class: CWE-125 out-of-bounds read (intra-object, kernel pointer bytes disclosed)
Severity: Low Β· Confidence: certain (reproduced on QEMU guest)
Build
scp amrroob.c Makefile dfbsd:/root/amrroob/ guest# cd /root/amrroob && make SYSDIR=/usr/src/sys # -> amrroob.ko
Run
guest# kldload /root/amrroob/amrroob.ko # pulls in wlan_amrr (or uses in-kernel amrr) guest# sysctl amrroob.run=1 # ~60s; all output to dmesg/console guest# dmesg | grep amrroob guest# kldunload amrroob
(see run.sh; output lands in dmesg, captured in run.2.log / run.log)
Expected (vulnerable kernel)
PHASE2 ... -> amn_rix=76β 11n rate index driven to 76 through the publicieee80211_ratectl_*API (77-entryni_htratestable).PHASE3 ... ratectl_rate() returned rix=76; legacy rs_nrates=12β after flipping the node to a legacy channel, the stale index 76 is used against the 15-byteni_rates.rs_ratesarray: OOB read at rs_rates[76] (array ends at rs_rates[14]).PHASE4walk-down: every failure interval reads one more OOB byte; the value lands inni->ni_txrate(masked& 0x7f) β i.e. inSIOCG80211NODEisi_txrateand the world-readablenet.wlanX.rate_statssysctl. rs_rates[18..25] overlay theni_chankernel heap pointer (e.g.0xfffff8008d403a30β bytes40 3a 40 8d 00 f8 ff ffread OOB,0xA5meshid marker bytes likewise disclosed as0x25).PHASE5 tmp-node ... amn_rix=-1 ratectl_rate() returned -1β the empty-rateset path (ieee80211_tmp_node): drivers receive rix = -1 (iwm_setratesKASSERT(rix >= 0 && rix < nrates)β INVARIANTS panic).
Expected (patched kernel, fix.diff applied)
PHASE3returnsrix=11(clamped tors_nrates-1), no reads pastrs_rates[14], walk-down runs 11β0 in bounds, no0xA5/pointer-byte lines,PHASE5returns0.
Fix validation
fix.diff applied to guest /usr/src, make nativekernel, installkernel,
reboot, same PoC β see fix_run.log / VERDICT.md.
NOTE: ieee80211_amrr.c is compiled into the base kernel when wlan is
built in (sys/conf/files:1630) β patching only wlan_amrr.ko has no effect;
a kernel rebuild is required.
VERDICT β DF-3454 (ieee80211_amrr.c stale amn_rix OOB read)
Status: reproduced (2 baseline runs on the QEMU guest, full pointer-byte
disclosure captured) Β· Impact: leak (7 of 8 bits of arbitrary intra-node
bytes, incl. the ni_chan kernel heap pointer, via ni_txrate/rate_stats)
plus an unbounded/negative index handed to drivers (run(4) rt2860_rates[12]
static OOB read, bwn(4) rs_rates[rix-1], iwm(4) INVARIANTS KASSERT panic).
No write primitive: not an escalation candidate β read-only intra-object
OOB inside a 12 KB struct ieee80211_node allocation.
Root cause (path:line)
amrr_node_init(ieee80211_amrr.c:208-218) computesamn_rixonce, against whichever ratesetamrr_node_is_11n()selects at that moment. For an 11n node that is the 77-entryni_htrates(MCS0..76 across 3 streams), soamn_rixcan legitimately be up to 76; for the empty-rateset tmp-node path (ieee80211_node.c ieee80211_tmp_node β ieee80211_ratectl_node_init) it becomes-1.amrr_node_is_11n()(ieee80211_amrr.c:145-155) is re-evaluated on every call: it flips to "legacy" as soon asni_chanloses HT (channel switch β e.g. DFS radar CSA to a legacy channel β or reassoc) orni_htrates.rs_nratesis cleared (reassoc without HT IEs).- Nothing ever re-clamps
amn_rixto the newly selected rateset: amrr_updatereadsrs->rs_rates[rix]at :259 (NOTE arg), :280, :301;amrr_ratewritesni->ni_txrate = rs->rs_rates[rix]at :339 and returnsrixat :350 to the driver (bwn:6304-6308, run:2617/3345-3346, iwm:3540βiwm_setrates:4366 KASSERT);amrr_node_statsreadsrs->rs_rates[amn->amn_rix]at :434/:438 and prints it β this is exactly what the world-readablenet.wlanX.rate_statssysctl (ieee80211_ratectl.c:129-135, CTLFLAG_RD, attached for every vap) dumps per node.- amrr is the default ratectl (ieee80211_ratectl.c:119-120 forces AMRR
whenever the algorithm is NONE), and
ieee80211_amrr.cis built into the base kernel (sys/conf/files:1630 optional wlan) β no module opt-in needed for any vap. - Under success the stale index never comes down (
rix+1 < rs_nratesis false, so no increment, no reset), under failure it walks down at one byte per update interval (500 ms default) β the disclosure is slow but deterministic, and the attacker (remote 11n peer controlling its own IEs / an HTβlegacy channel change) chooses where the 11n era ends, i.e. which byte index gets read.
Reproduction
Harness: amrroob.c β a KLD that exercises ONLY the public
ieee80211_ratectl_set/_node_init/_tx_update/_rate/_node_stats wrappers on a
synthetic vap/node (iv_rs set up exactly as amrr_init does; sysctl attach
replicated because it needs a full vap sysctl tree β everything the vulnerable
functions touch is real). No wireless hardware needed; the guest's in-kernel
amrr is the code under test (verified: ops table at 0xffffffff80d69ea0, inside
the static kernel image).
Decisive output (run 2, run.2.log):
node=0xfffff80118800400 size=12208 legacy rs_rates[0..14]=0xfffff8011880066e..0xfffff8011880067c
&ni_chan=0xfffff80118800680 (rs_rates +18..+25) <- pointer overlays rs_rates[18..25]
ni_chan pointer value = 0xfffff8008d403a30
PHASE1 11n node_init -> amn_rix=67 (htrates nrates=77, legacy nrates=12)
PHASE2 after 9 successful intervals -> amn_rix=76
PHASE3 after HT->legacy flip: ratectl_rate() returned rix=76; legacy rs_nrates=12
rix=76 indexes legacy rs_rates[76] => 0xfffff801188006ba is OOB (array ends at rs_rates[14])
PHASE4 rix=25 -> read ...687 = 0xff (ni_txrate/sysctl show 0x7f) [ni_chan pointer byte]
rix=24 -> read ...686 = 0xff ... rix=18 -> ...680 = 0x40 [ni_chan pointer byte]
rix=68/67/... -> 0xa5 -> 0x25 [0xA5 meshid marker]
PHASE5 tmp-node (empty ratesets): amn_rix=-1 ratectl_rate() returned -1
DONE - OOB reads demonstrated via public ratectl API only; guest still alive
All 8 bytes of the ni_chan kernel heap pointer were read out-of-bounds
(7 bits each after the IEEE80211_RATE_VAL mask) and the 0xA5 marker
planted in ni_meshid was disclosed as 0x25 through ni_txrate β the same
value SIOCG80211NODE/rate_stats hand to unprivileged users. Guest stayed
alive both runs (read-only bug).
Fix validation
fix.diffclampsamn_rix/rixagainst the currently selected rateset at the three consumption sites (amrr_update,amrr_rate,amrr_node_stats) and neutralises the empty-rateset-1.- Applied in-guest to
/usr/src(patch -p1, 3 hunks),make nativekernel(RC=0),make installkernel, reboot into kernel#1 Sun Sep 13 02:35:55 UTC 2026, identical PoC re-run (fix_run.log):
| observation | baseline kernel #0 | patched kernel #1 |
|---|---|---|
ratectl_rate() after flip |
rix=76 (OOB) |
rix=11 (= rs_nrates-1) |
rate_stats output |
leaked OOB bytes | rate: 54 Mbit (real 108/2) |
| walk-down OOB reads | 8 pointer bytes + 34 marker bytes | none |
| tmp-node rix | -1 |
0 |
Bad behavior gone; guest stable before and after.
* (Lesson learned the hard way and recorded here: the file is compiled into
the base kernel via optional wlan (sys/conf/files:1630); rebuilding only
wlan_amrr.ko has no effect β see fix_module_only.log. Also: this
guest's vm.sh down hard-kills after a timeout, so file installs must be
sync'd or they are lost β the first module install was truncated to 0
bytes by exactly that.)
Not an escalation
The OOB read is confined to the 12,208-byte struct ieee80211_node
allocation (index β€ 76 vs array end +61 bytes, struct end +14,535 bytes).
No OOB write, no freed-object access, no cross-object read. Combined with
the 7-bit mask and the 500 ms/byte pacing, the realistic ceiling is a
KASLR-defeating pointer nibble harvest and driver-side garbage metadata
(wrong ack durations on run(4), INVARIANTS panic on iwm(4)) β hence
severity Low, bucket kernleak/memcorrupt-read.
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
fixedfix.diff applied to guest /usr/src (patch -p1, 3 hunks), make nativekernel (RC=0, fix_build.log), make installkernel, reboot into kernel #1. Identical PoC re-run (fix_run.log): PHASE3 ratectl_rate() returns rix=11 = rs_nrates-1 (was 76); rate_stats prints 'rate: 54 Mbit' = the genuine top legacy rate 108/2 (was leaked OOB bytes); PHASE4 walk-down produces zero OOB reads (loop never reaches the pointer/marker range 18..76); PHASE5 empty-rateset tmp node yields amn_rix=0 and ratectl_rate() returns 0 (was -1, the iwm KASSERT panic trigger). Bad behavior gone; guest stable before/after.
fix_run.log (patched dmesg: 'returned rix=11', 'rate: 54 Mbit', 'amn_rix=0 ... returned 0', no PHASE4 OOB lines) vs run.2.log (baseline: rix=76, pointer bytes 40 3a 40 8d 00 f8 ff ff at rs_rates[18..25], 0xA5 markers as 0x25, rix=-1); fix.diff (git-apply clean); fix_build.log (nativekernel tail, RC=0); fix_module_only.log (module-only patch attempt - no effect, proves in-kernel amrr).
Confirmed kernel references
- sys/netproto/802_11/wlan/ieee80211_amrr.c:145
- sys/netproto/802_11/wlan/ieee80211_amrr.c:208
- sys/netproto/802_11/wlan/ieee80211_amrr.c:259
- sys/netproto/802_11/wlan/ieee80211_amrr.c:339
- sys/netproto/802_11/wlan/ieee80211_amrr.c:350
- sys/netproto/802_11/wlan/ieee80211_amrr.c:434
- sys/netproto/802_11/wlan/ieee80211_ratectl.c:119
- sys/netproto/802_11/wlan/ieee80211_node.c:1276
- sys/netproto/802_11/_ieee80211.h:375
- sys/netproto/802_11/_ieee80211.h:388
- sys/dev/netif/bwn/bwn/if_bwn.c:6304
- sys/bus/u4b/wlan/if_run.c:2617
- sys/bus/u4b/wlan/if_run.c:3345
- sys/dev/netif/iwm/if_iwm.c:4366
Detail
Exploit chain
remote 11n peer (or DFS/CSA legacy channel switch) flips node HT status while amn_rix is an htrates index (<=76) -> stale index used against 15-entry legacy rs_rates in wlan core (OOB read, value -> ni_txrate) -> unprivileged local user reads net.wlanX.rate_stats (CTLFLAG_RD) / SIOCG80211NODE isi_txrate -> 7 of 8 bits per disclosed byte incl. ni_chan heap pointer bytes; driver-side: run(4) indexes rt2860_rates[12] with rix<=76 (static OOB read), bwn(4) reads rs_rates[rix-1], iwm(4) KASSERT(rix>=0&&rix<nrates) panics INVARIANTS kernels. Ceiling: KASLR-defeating pointer-byte harvest + driver metadata corruption/panic - no write primitive.
Evidence (decisive lines)
run.2.log: PHASE3 'returned rix=76; legacy rs_nrates=12' + 'rs_rates[76] ... is OOB (array ends at rs_rates[14])'; PHASE4 lines 'rix=25..18 ... [ni_chan pointer byte]' showing bytes ff ff f8 00 8d 40 3a 40 of ni_chan=0xfffff8008d403a30 and 34 x '[0xA5 meshid marker]' lines reading 0xa5 disclosed as 0x25; PHASE5 'amn_rix=-1 ratectl_rate() returned -1'. fix_run.log (patched kernel #1): 'returned rix=11', rate_stats 'rate: 54 Mbit' (=108/2, real top legacy rate), zero PHASE4 OOB lines, 'amn_rix=0 ... returned 0'. fix_module_only.log documents the failed module-only patch attempt (amrr is compiled into the base kernel via sys/conf/files:1630 'optional wlan').
PoC changes
Harness authored fresh (no seed PoC existed). Two build fixes vs first draft (missing net/if_media.h include for ifmedia in ieee80211_var.h; NULL->0 in DEV_MODULE). One harness logic fix after run 1 (walk-down step budget exhausted early - steps counter reuse; run 2 walks the full 76->17 range exposing all 8 ni_chan pointer bytes). Cosmetic: harness 'is OOB' label prints unconditionally for PHASE3 even when the index is in-bounds after the fix (fix_run.log rix=11 vs array end 14 - in bounds). iv_rs setup replicates amrr_init()'s 4 statements because ir_init's sysctl attach requires a fully constructed vap sysctl tree; the vulnerable functions exercised (node_init/update/rate/tx_update/node_stats/setinterval) are the real in-kernel ones, reached through the public wrappers.
Verified recommended fix
Clamp amn_rix/rix against the currently selected rateset at every use (amrr_update, amrr_rate, amrr_node_stats) - see fix.diff; also make drivers treat rix<0 as 'use lowest'.
Verdict
Reproduced on the QEMU guest via a KLD harness that drives only the public ieee80211_ratectl_* API against the in-kernel amrr (the DEFAULT ratectl, ratectl.c:119). After an 11n association is driven to amn_rix=76 through real success intervals, flipping the node to a legacy channel makes amrr_rate()/amrr_update()/amrr_node_stats() index the 15-byte ni_rates.rs_rates with the stale 76 (array ends at 14): all 8 bytes of the ni_chan kernel heap pointer (rs_rates[18..25]) and planted 0xA5 meshid markers are read out of bounds and disclosed 7-bits-masked through ni_txrate -> SIOCG80211NODE isi_txrate and the world-readable net.wlanX.rate_stats sysctl. The empty-rateset tmp-node path yields amn_rix=-1 returned to drivers (iwm_setrates KASSERT -> INVARIANTS panic). Read-only intra-object OOB inside the 12208-byte ieee80211_node allocation: no write primitive, not an escalation candidate. Fix (clamping amn_rix/rix against the active rateset at amrr_update/amrr_rate/amrr_node_stats) validated on a rebuilt kernel: rix 76->11, -1->0, zero OOB reads, identical PoC, guest stable.
No comments yet.