amrr ath_rate_getxtxrates() writes hardware rate codes into ath_rc_series.rix (rate-table INDEX) fields and clobbers the correct series-0 index β wrong-entry rt->info[] derefs on every multi-rate-retry frame, HT mis-programming on 11n HALs, and a second OOB read sink when compounded with DF-1931
Summary
getxtxrates stores cached hardware RATE CODES into the index-typed rc[].rix fields (violating the documented contract), discarding the caller's correct index - every consumer then indexes rt->info[] with a rate code: in-bounds WRONG-entry read that on 11n HALs flips legacy-chosen series to HT MCS rows with wrong flags, on pre-11n HALs reads the zero tail (rate 0 series), and compounds with DF-1931's 0xff sentinel into a second OOB read (~68-190 entries past info[64]). Unauth WiFi peer drives the rate set; non-default config (GENERIC uses sample). Cache-the-indices fix mirroring sample.c.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4850 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.0 KB | β raw | |
| VERDICT.md | β | 4.2 KB | β raw | |
| verdict.json | β | 3.2 KB | view raw | |
| fix.diff | β | 3.4 KB | view raw |
DF-4850 β amrr ath_rate_getxtxrates() writes hardware rateCodes into ath_rc_series.rix (rate-table index) fields
Status: untested (hardware-gated β no Atheros radio on the QEMU guest; amrr is not
compiled into GENERIC, which uses ath_rate_sample). Static verification only,
performed at line level against sys/.
What the bug is (static, certain)
struct ath_rc_series.rixis documented "ratetable index, not rate code" (sys/dev/netif/ath/ath/if_athrate.h:91).- amrr's per-node cache fields
amn_tx_rate0..3hold hardware rate codes β they are assignedrt->info[x].rateCodeinath_rate_update()(sys/dev/netif/ath/ath_rate/amrr/amrr.c:247-249, 258-260, 267-269, 275-277). ath_rate_getxtxrates()(amrr.c:127-144) stores those rate codes intorc[0..3].rixβ including clobbering the correct index that the caller had just stored atif_ath_tx.c:1392(bfs_rc[0].rix = rix) before callingath_rate_getxtxrates()atif_ath_tx.c:1397. Therix0argument (amrr.c:129) is ignored entirely.- Every consumer that treats
rc[i].rixas a table index then dereferences the wrong entry: ath_tx_rate_fill_rcflags():rt->info[rc[i].rix].rateCode/shortPreamble(if_ath_tx_ht.c:254, 260) β runs on every data-frame tx dispatch (if_ath_tx.c:1531, 2298, 3034, 5525, 5553, 5740)- A-MPDU delimiter padding:
rix = bfs_rc[0].rix; rt->info[rix].rateCode(if_ath_tx_ht.c:457-458) - 11n descriptor series:
rt->info[rc[i].rix].rateCode(if_ath_tx_ht.c:579-580) - RTS/CTS + duration paths:
if_ath_tx.c:1064, 1128, 1260, 1282
Why it is in-bounds (standalone) but still wrong
amrr only ever selects legacy negotiated rates from ni->ni_rates
(amrr.c:245-267; IEEE80211_RATE_VAL = 0x7f strips the basic bit, and HT rows
are keyed >= 0x80 in sc_rixmap, if_ath.c:6203-6208), so the stored rate
codes are β€ 0x1b (27) < info[64] (ah.h:681) β an in-bounds read of the
wrong entry, never OOB by itself.
Concrete wrong entries:
- 11n-capable HALs (
ar5416_11ng_tablerateCount=28,ar5416_11na_tablerateCount=24,ar5416_phy.c:31/69): legacy rate codes as indices land on HT MCS rows β e.g. 11ng: rateCode 0x1b (1 Mbps CCK) βinfo[27]= MCS15 (rateCode 0x8f); 11na: rateCode 0x0b (6 Mbps) βinfo[11]= MCS3. TheIS_HT_RATE()logic inath_tx_rate_fill_rcflags(if_ath_tx_ht.c:259-318) then setsATH_RC_HT_FLAG/ATH_RC_CW40_FLAG/ATH_RC_SGI_FLAG/STBC on frames whose rate series was chosen as legacy β frames are programmed at MCS rates the peer may not support, with 40 MHz/SGI flags the channel may not allow. - Pre-11n HALs (e.g.
ar5212_11a_tablerateCount=8): indices 8..27 read the zero-filled static tail βrc[i].ratecode = 0β MRR series / completionts_rateof 0, wrong RTS/CTS durations, wrong A-MPDU padding.
Amplification of known DF-1931
When sc_rixmap[] returns the 0xff not-found sentinel (DF-1931, already filed
against this file), amn_tx_rate0..2 are garbage bytes read OOB from
rt->info[255] (amrr.c:246-267); those garbage bytes then flow into
rc[i].rix here and become a second OOB read (rt->info[β€255] at
if_ath_tx_ht.c:254 etc.) ~68-190 entries past the 64-entry table. Same run,
two sinks; DF-1931's fix (bounds-check the sentinel) removes that path; the fix
below removes the confusion independently.
How it would be exercised (needs hardware)
Unauthenticated WiFi peer within radio range on a kernel built with
device ath_rate_amrr (sys/conf/files:264; GENERIC selects
ath_rate_sample β sys/config/X86_64_GENERIC:266): the peer's rate set drives
ath_rate_update; every multi-rate-retry data frame then walks the wrong-index
derefs above. No QEMU-runnable trigger exists without an Atheros NIC.
Fix
fix.diff in this directory (never applied to sys/): store per-series
rate-table indices in amrr_node and return them from
ath_rate_getxtxrates(), mirroring sample.c's contract-correct
sn->rates[i].rix usage.
DF-4850 β VERDICT
- status: untested
- reproduced: 0
- impact: none (demonstrated statically; class is logic/correctness with a latent second-OOB amplifier for DF-1931 β no memory-safety violation on its own)
- confidence: certain (code mechanism proven at line level; impact characterization from the HAL rate tables in-tree)
Why untested (not skipped carelessly β it cannot run on the guest)
- The trigger requires an Atheros NIC. The QEMU guest has none
(
vm.sh run_user 'pciconf -l | grep ath'would be empty; ath is not even compiled for a NIC-less VM). Every sink is on the ath tx path. - amrr is not the default rate-control module: GENERIC selects
device ath_rate_sample(sys/config/X86_64_GENERIC:266); amrr requires a custom kernel config (optional ath_rate_amrr,sys/conf/files:264-265), and only one of amrr/onoe/sample can be linked at a time (all three define the sameath_rate_*symbols andDECLARE_MODULE(ath_rate, ...)βamrr.c:513). - Fix validation likewise requires the hardware tx path, so
fix_status: not_testable.
The chain, statically proven
ath_rate_update()caches rate codes inamrr_node:amrr.c:247amn->amn_tx_rate0 = rt->info[amn->amn_tx_rix0].rateCode; likewise series 1/2 atamrr.c:258-260, 267-269and series 3 atamrr.c:275-277(rt->info[0].rateCode).ath_tx_do_ratelookup()stores the correct series-0 index (if_ath_tx.c:1392bfs_rc[0].rix = rix), then callsath_rate_getxtxrates()(if_ath_tx.c:1396-1398, gated only onbfs_ismrr && try0 != ATH_TXMAXTRY).ath_rate_getxtxrates()overwritesrc[0].rixand fillsrc[1..3].rixwith the cached rate codes (amrr.c:135-138), ignoring itsrix0parameter (amrr.c:129). This violates the field contract (if_athrate.h:91β "ratetable index, not rate code").- Sinks dereference
rt->info[rc[i].rix]unguarded (onlytries == 0entries are skipped,if_ath_tx_ht.c:251-254):if_ath_tx_ht.c:254, 260(every tx dispatch βif_ath_tx.c:1531, 2298, 3034, 5525, 5553, 5740),if_ath_tx_ht.c:457-458(A-MPDU pad),if_ath_tx_ht.c:579-580(11n descriptor series),if_ath_tx.c:1064, 1128, 1260, 1282(RTS/CTS durations). - Value domain: amrr maps only legacy dot11 rates (
IEEE80211_RATE_VAL=0x7f; HT rows keyed β₯0x80 insc_rixmap,if_ath.c:6203-6208), so codes β€ 0x1b (27) < 64 (ah.h:681) β in-bounds wrong entry, never standalone OOB. - Wrong-entry consequences, from the in-tree tables:
-
ar5416_11ng_table(rateCount 28,ar5416_phy.c:31): code 0x1b βinfo[27]= MCS15/0x8f; code 0x18 βinfo[24]= MCS12/0x8c; HT rows triggerATH_RC_HT_FLAG/CW40/SGI/STBC (if_ath_tx_ht.c:278-317) on legacy-chosen series β frames transmitted at HT rates to legacy peers. -ar5416_11na_table(rateCount 24): code 0x0b βinfo[11]= MCS3/0x83. -ar5212_11a_table(rateCount 8): indices 8..27 hit the zero-filled tail βratecode = 0in the MRR series. - DF-1931 amplification:
sc_rixmap0xff sentinel (already filed, this file,amrr.c:246-267) makes the cached "rate codes" garbage bytes read fromrt->info[255]; step 4 then re-dereferencesrt->info[garbageβ€255]β a second OOB read pastinfo[64](~68-190 entries).
Why severity is Low
- Standalone: no OOB β a wrong in-bounds entry; effect is mis-programmed tx rates/flags (availability/interop of the link, remotely influenceable via the peer's advertised rate set).
- Requires a non-default kernel config (
ath_rate_amrr) plus Atheros radio. - The OOB variant rides on DF-1931's already-known unchecked sentinel.
Fix validation
Not possible without hardware β fix_status: not_testable. The authored
fix.diff (indices stored/returned per series, mirroring sample.c) is
compile-checked by inspection only; sys/ remains untouched per audit rules.
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 read-only sys/ (store per-series indices in amrr_node; return them from ath_rate_getxtxrates). Fix validation requires the hardware tx path (Atheros NIC + ath_rate_amrr kernel) absent from the guest.
findings/poc/DF-4850/fix.diff
Confirmed kernel references
- sys/dev/netif/ath/ath_rate/amrr/amrr.c:135
- sys/dev/netif/ath/ath_rate/amrr/amrr.c:247
- sys/dev/netif/ath/ath/if_athrate.h:91
- sys/dev/netif/ath/ath/if_ath_tx.c:1392
- sys/dev/netif/ath/ath/if_ath_tx.c:1397
- sys/dev/netif/ath/ath/if_ath_tx_ht.c:254
- sys/dev/netif/ath/ath/if_ath_tx_ht.c:457
- sys/dev/netif/ath/ath/if_ath_tx_ht.c:579
- sys/dev/netif/ath/ath_hal/ah.h:681
- sys/dev/netif/ath/ath_hal/ar5416/ar5416_phy.c:31
- sys/dev/netif/ath/ath/if_ath.c:6203
Detail
Evidence (decisive lines)
findings/poc/DF-4850/VERDICT.md (step-by-step chain with path:line); README.md (wrong-entry table walkthrough); fix.diff (index/code separation, mirrors sample.c)
PoC changes
No seed PoC existed; pack authored fresh. No runnable trigger is possible without Atheros radio hardware.
Verified recommended fix
Cache per-series rate-table indices in amrr_node and return those from ath_rate_getxtxrates() instead of rate codes (see fix.diff); bounds-check sc_rixmap sentinel per DF-1931.
Verdict
Mechanism proven statically at line level: amrr.c:135-138 stores hardware rateCodes (cached at amrr.c:247-277) into ath_rc_series.rix fields that every consumer dereferences as a rate-table index (if_athrate.h:91 contract), clobbering the correct index placed at if_ath_tx.c:1392; rt->info[rc[i].rix] is then read at if_ath_tx_ht.c:254/260/457-458/579-580 and if_ath_tx.c:1064/1128/1260/1282 on every MRR data frame. Standalone the index stays <= 0x1b < info[64] (amrr selects only legacy rates; sc_rixmap keys HT rows >= 0x80), so it is an in-bounds WRONG-entry read: on 11n HALs legacy rateCodes land on HT MCS rows (ar5416_11ng: 0x1b -> info[27] = MCS15) and flip legacy series to HT rates with CW40/SGI/STBC flags; on pre-11n HALs they read zero-filled table tails yielding ratecode 0. Combined with the known unchecked sc_rixmap 0xff sentinel (DF-1931, same file), the garbage byte becomes the index for a second OOB read past info[64]. Not run: requires an Atheros NIC and a kernel built with ath_rate_amrr (GENERIC uses ath_rate_sample, sys/config/X86_64_GENERIC:266); the QEMU guest has no ath hardware.
No comments yet.