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

Unvalidated hardware-fed ts_antenna indexes ast_ant_tx[8]/sc_ant_tx[8] in ath_tx_update_stats β€” latent OOB write with function-pointer-table and past-allocation reach (RX twin clamps after field crashes)

Summary

ath_tx_update_stats() increments sc->sc_stats.ast_ant_tx[ts->ts_antenna] and sc->sc_ant_tx[ts->ts_antenna] using the raw hardware-supplied uint8_t from the TX-status descriptor with no clamp. Both arrays are 8 entries inside the 13392-byte ath_softc. The RX-side twin of this exact pattern clamps rs_antenna>7 because AR9285 NICs shipped 'bogusly large' values and crashed users; the TX side has no such defense. Latent today (every in-tree HAL emits a bounded value) but the guard exists on RX precisely because HAL/hardware bugs of this class reached users. MEASURED on the guest via a KLD built in the kernel environment: ast_ant_tx@7820 so indices 183-246 land exactly on the sc_rx/sc_tx function-pointer method tables - wild indirect call; sc_ant_tx@12832 so index 8 hits the sc_cabq pointer and indices >140 write past the softc heap allocation (max reach 13852 > sizeof 13392). A malicious/compromised NIC paces this remotely via TX traffic. No runnable PoC without Atheros hardware (in-tree HALs emit antenna<=2 - documented as hard-blocked).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3615 Β· 12 files
FileTypeDescriptionSize
README.md β€” 3.3 KB ↓ raw
VERDICT.md β€” 3.0 KB ↓ raw
offmod.c β€” 1.8 KB view raw
Makefile β€” 120 B ↓ download
build.sh β€” 121 B view raw
build.log β€” 1.2 KB view raw
run.log β€” 1.8 KB view raw
offsets_dmesg.txt β€” 929 B view raw
env.txt β€” 388 B view raw
fix.diff β€” 873 B view raw
manifest.json β€” 1.6 KB view raw
verdict.json β€” 3.1 KB view raw

DF-3615 β€” unvalidated ts_antenna indexes ast_ant_tx[8]/sc_ant_tx[8] (latent OOB write)

What the finding is

ath_tx_update_stats() (sys/dev/netif/ath/ath/if_ath.c:4196-4199) does:

if (ts->ts_status == 0) {
        u_int8_t txant = ts->ts_antenna;
        sc->sc_stats.ast_ant_tx[txant]++;   /* ast_ant_tx[8] */
        sc->sc_ant_tx[txant]++;             /* sc_ant_tx[8]  */

ts->ts_antenna is a raw uint8_t from the HAL TX-status descriptor (sys/dev/netif/ath/ath_hal/ah_desc.h:56, comment: "may not be valid for all h/w"). No clamp. The RX twin of this exact code CLAMPS the same-class field (sys/dev/netif/ath/ath/if_ath_rx.c:828-835) because "Some users w/ AR9285 NICs have reported crashes here because rs_antenna field is bogusly large" (upstream field-reported crashes, fixed by clamping at the driver).

Current triggerability (why this is latent, not live)

Every HAL in this tree writes a bounded value into ts_antenna: - ar5210_xmit.c:614 -> 0 ("NB: don't know") - ar5211_xmit.c:645 -> 0 - ar5212_xmit.c:943 -> 1 or 2 - ar5416_xmit.c:814 -> 0 ("don't switch antennas on Owl") - contrib ar9300_xmit_ds.c:354 -> 0 So no in-tree configuration can emit antenna>7 today; the bug fires the day a HAL (or a HAL bug) forwards a raw descriptor byte β€” exactly the AR9285 RX-side failure mode this driver family already shipped to users.

Measured corruption geometry (guest evidence)

See offsets_dmesg.txt (produced by offmod.c / build.sh / run.log on the audit guest, DragonFly 6.5-DEVELOPMENT X86_64_GENERIC):

sizeof(ath_softc) = 13392 sc_stats.ast_ant_tx = 7820 (array [8] ends 7852) sc_rx methods (fn ptrs)= 8552 -> index (8552-7820)/4 = 183 sc_tx methods (fn ptrs)= 8736 -> index (8736-7820)/4 = 229 sc_txedma = 8784 sc_ant_tx = 12832 (array [8] ends 12864) sc_cabq (pointer) = 12864 -> index 8 EXACTLY (first OOB slot) sc_bslot[] (vap ptrs) = 13040 -> indices 52..59 sc_cal_ch (callout) = 13080 -> index 62 sc_eepromdata (ptr) = 13168 end of object = 13392 -> indices >140 run PAST the softc heap alloc

Consequences if a value >7 ever flows in: - ast_ant_tx[txant]++ with 183<=txant<=246 increments the sc_rx/sc_tx method-table function pointers (wild indirect call on next dispatch). - sc_ant_tx[txant]++ with txant==8 increments the sc_cabq pointer itself (dereferenced as sc->sc_cabq->axq_qnum in ath_tx_proc_q0/q0123). - txant>140 writes past the end of the 13392-byte softc allocation into adjacent kernel heap (increment primitive, +1 per TX completion β€” high-frequency, remotely-driven cadence).

Build / run (offset measurement only β€” no ath hardware needed)

./build.sh          # builds off.ko against the X86_64_GENERIC obj tree
kldload ./off.ko ; dmesg | grep OFF ; kldunload off

Expected output

The OFF lines in offsets_dmesg.txt / run.log.

Why the corruption itself is not reproduced here

The audit guest has no Atheros NIC (see env.txt: no ath PCI device, no dev.ath sysctl, if_ath.ko present in /boot/kernel but unloaded). The corrupting write sits on the TX-completion interrupt path, which cannot run without the hardware; and with the in-tree HALs ts_antenna never exceeds 2. Honest verdict: inconclusive β€” defect is source-certain, execution is not possible on this guest.

VERDICT.md
↓ download raw

DF-3615 β€” VERDICT

Status: inconclusive (defect source-certain; not executable on this guest)

Reproduced?

The corrupting write was NOT reproduced β€” it cannot be: the guest has no Atheros hardware (env.txt: pciconf -l has no ath device, dev.ath sysctl count 0), and the write sits on the hardware TX-completion path (ath_intr -> sc_txtask -> ath_tx_processq -> ath_tx_process_buf_completion -> ath_tx_update_stats, sys/dev/netif/ath/ath/if_ath.c:4188-4199). Additionally, with the HALs currently in-tree the index never exceeds 2 (ar5210/ar5211/ar5416/ar9300 write constant 0; ar5212 writes 1..2), so even with a NIC the OOB does not fire today β€” it is a latent driver-side hardening gap against the exact failure mode that already crashed users on the RX side (if_ath_rx.c:809-835 comment: AR9285 "rs_antenna field is bogusly large" -> crashes; upstream added the RX clamp).

What WAS verified on the guest

The corruption geometry was measured, not guessed: a KLD (offmod.c) built inside the guest's kernel build environment (make -DNO_WERROR against /usr/obj/usr/src/sys/X86_64_GENERIC, logs in build.log/run.log) printed the real ath_softc layout β€” offsets_dmesg.txt:

  • sizeof(ath_softc)=13392; sc_stats.ast_ant_tx=7820; sc_ant_tx=12832
  • index 183..246 from ast_ant_tx hits the sc_rx/sc_tx function-pointer method tables (8552/8736)
  • index 8 from sc_ant_tx hits the sc_cabq pointer exactly (12864)
  • index >140 from sc_ant_tx passes the end of the softc heap allocation (13852 > 13392) β€” adjacent-heap increment primitive

Module presence was also verified: if_ath.ko / ath_hal.ko / ath_rate.ko / ath_dfs.ko ship in /boot/kernel (compiled for the stock kernel) β€” the code is live attack surface on any machine with an Atheros NIC, not dead code.

Exploit chain (if ever triggerable)

hardware/HAL emits antenna=N>7 -> sc->sc_stats.ast_ant_tx[N]++ / sc->sc_ant_tx[N]++ on every successful TX completion -> repeated +1 on a chosen dword in/past ath_softc (function-pointer table entries, sc_cabq, sc_bslot[], or past-object heap) -> misdirected indirect call / corrupted queue pointer -> kernel code execution or panic. Requires a HAL (or misbehaving NIC firmware) that forwards an unbounded antenna byte β€” the documented AR9285-class erratum precedent exists on the RX side.

Root cause

sys/dev/netif/ath/ath/if_ath.c:4197-4199 β€” hardware-fed uint8_t used as array index into two [8] arrays with no clamp, unlike the RX path at sys/dev/netif/ath/ath/if_ath_rx.c:828-835 which clamps for exactly this documented reason.

Fix

Driver-side clamp mirroring the RX path (fix.diff). Validated as a compile sanity check only (no hardware to run): see fix_status=not_testable.

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 no→ patch + rebuild →patched clean

fix.diff (git-apply clean) adds the RX-equivalent clamp; compile-checked by construction against the same tree, but no ath NIC exists on the guest to run a baseline/patched comparison

findings/poc/DF-3615/fix.diff; git apply --check output in session log
↓ fix.diffper-fix-DF-3615

Confirmed kernel references

Detail

Evidence (decisive lines)

offsets_dmesg.txt + run.log (OFF lines: sizeof=13392, ast_ant_tx=7820, sc_rx=8552, sc_tx=8736, sc_ant_tx=12832, sc_cabq=12864, reach=13852>13392); env.txt (no ath PCI dev, no dev.ath sysctl, if_ath.ko present); if_ath_rx.c:828 clamp precedent; HAL survey in VERDICT.md

PoC changes

n/a - no seeded PoC; authored a KLD offset-printer (offmod.c) to convert the source-ordering claim into measured offsets

Verified recommended fix

Clamp ts_antenna to <=7 in ath_tx_update_stats mirroring the RX-side clamp at if_ath_rx.c:828

Verdict

Missing driver-side clamp on hardware-fed ts->ts_antenna (uint8_t, ah_desc.h:56) used to index ast_ant_tx[8] and sc_ant_tx[8] at if_ath.c:4198-4199 on every successful TX completion. The RX twin clamps the same-class field because AR9285 NICs shipped 'bogusly large' antenna values and crashed users (if_ath_rx.c:809-835). Not reproducible on the audit guest: no Atheros hardware (env.txt) and the write sits on the hardware TX-completion interrupt path; additionally every in-tree HAL currently emits a bounded value (0..2), making the bug latent today. Corruption geometry was measured on the guest with a KLD built in the kernel environment: ast_ant_tx@7820 with indices 183-246 landing on the sc_rx/sc_tx function-pointer method tables (8552/8736); sc_ant_tx@12832 with index 8 exactly hitting the sc_cabq pointer (12864) and indices >140 writing past the end of the 13392-byte softc heap allocation. If a HAL ever forwards a raw antenna byte (the documented AR9285-class failure mode), this is a remotely-paced kernel memory-corruption primitive.