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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4850 Β· 4 files
FileTypeDescriptionSize
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.rix is documented "ratetable index, not rate code" (sys/dev/netif/ath/ath/if_athrate.h:91).
  • amrr's per-node cache fields amn_tx_rate0..3 hold hardware rate codes β€” they are assigned rt->info[x].rateCode in ath_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 into rc[0..3].rix β€” including clobbering the correct index that the caller had just stored at if_ath_tx.c:1392 (bfs_rc[0].rix = rix) before calling ath_rate_getxtxrates() at if_ath_tx.c:1397. The rix0 argument (amrr.c:129) is ignored entirely.
  • Every consumer that treats rc[i].rix as 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_table rateCount=28, ar5416_11na_table rateCount=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. The IS_HT_RATE() logic in ath_tx_rate_fill_rcflags (if_ath_tx_ht.c:259-318) then sets ATH_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_table rateCount=8): indices 8..27 read the zero-filled static tail β†’ rc[i].ratecode = 0 β†’ MRR series / completion ts_rate of 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.

VERDICT.md
↓ download raw

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)

  1. 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.
  2. 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 same ath_rate_* symbols and DECLARE_MODULE(ath_rate, ...) β€” amrr.c:513).
  3. Fix validation likewise requires the hardware tx path, so fix_status: not_testable.

The chain, statically proven

  1. ath_rate_update() caches rate codes in amrr_node: amrr.c:247 amn->amn_tx_rate0 = rt->info[amn->amn_tx_rix0].rateCode; likewise series 1/2 at amrr.c:258-260, 267-269 and series 3 at amrr.c:275-277 (rt->info[0].rateCode).
  2. ath_tx_do_ratelookup() stores the correct series-0 index (if_ath_tx.c:1392 bfs_rc[0].rix = rix), then calls ath_rate_getxtxrates() (if_ath_tx.c:1396-1398, gated only on bfs_ismrr && try0 != ATH_TXMAXTRY).
  3. ath_rate_getxtxrates() overwrites rc[0].rix and fills rc[1..3].rix with the cached rate codes (amrr.c:135-138), ignoring its rix0 parameter (amrr.c:129). This violates the field contract (if_athrate.h:91 β€” "ratetable index, not rate code").
  4. Sinks dereference rt->info[rc[i].rix] unguarded (only tries == 0 entries 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).
  5. Value domain: amrr maps only legacy dot11 rates (IEEE80211_RATE_VAL=0x7f; HT rows keyed β‰₯0x80 in sc_rixmap, if_ath.c:6203-6208), so codes ≀ 0x1b (27) < 64 (ah.h:681) β†’ in-bounds wrong entry, never standalone OOB.
  6. 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 trigger ATH_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 = 0 in the MRR series.
  7. DF-1931 amplification: sc_rixmap 0xff sentinel (already filed, this file, amrr.c:246-267) makes the cached "rate codes" garbage bytes read from rt->info[255]; step 4 then re-dereferences rt->info[garbage≀255] β€” a second OOB read past info[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_testable

fix.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
↓ fix.diffper-fix-DF-4850

Confirmed kernel references

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.