Missing MCLBYTES upper-bound check on rs->rs_datalen allows kernel heap OOB read via crafted wifi frames
- File:
sys/dev/netif/ath/ath/if_ath_rx.c - Lines: 730, 760, 761, 766, 773, 774, 787, 798, 805, 902, 906, 936
- Severity: Medium
- CVSS:
CVSS:3.1/AV:A/AC:H/PR:N/UI:N/S:U:C:H/I:N/A:H - CWE: CWE-125 Out-of-bounds Read
- Confidence: likely
Summary
ath_rx_pkt() trusts the hardware-reported frame length rs->rs_datalen
without validating it against the allocated RX cluster size
(MCLBYTES=2048).
The HAL populates rs_datalen from a 12-bit descriptor field (AR_DataLen, max
4095) that can exceed the 2048-byte cluster.
The driver assigns this value directly to m->m_len and m->m_pkthdr.len at
three sites (MIC-error, radiotap-error, and rx_accept paths), so any subsequent
consumer β ieee80211_radiotap_rx_all(), ieee80211_input(),
ieee80211_dump_pkt() β reads past the cluster boundary into adjacent kernel
heap.
This is the ath(4) instance of the DF-1410 (if_xe) / DF-1131 (bwn) rxlength
vulnerability family.
Root cause
The receive cluster is allocated at ath_legacy_rxbufinit()
(if_ath_rx.c:270) via m_getcl(), yielding exactly MCLBYTES=2048 bytes
(sys/param.h:495-497; m->m_ext.ext_size is set to MCLBYTES at line 277).
The HAL is told the buffer is 2048 bytes via
ath_hal_setuprxdesc(ah, ds, m->m_len=2048, 0) at line 331β334.
On receive, the HAL decodes the hardware descriptor:
rs->rs_datalen = ads->ds_rxstatus1 & AR_DataLen (ar5416_recv.c:181), where
AR_DataLen = 0x00000fff (ar5416desc.h:340 / ar5212desc.h:72) β a 12-bit
field allowing values up to 4095.
ath_rx_pkt() then performs len = rs->rs_datalen at three sites:
- Line 730 (
HAL_RXERR_MICpath): only checkslen >= sizeof(struct ieee80211_frame)β lower bound only. - Line 760β761 (radiotap error path):
m->m_pkthdr.len = m->m_len = len, thenieee80211_radiotap_rx_all(ic, m)at line 766. - Line 773β774 (
rx_accept):m->m_len = len; laterm->m_pkthdr.len = lenat line 805.
No site compares len against MCLBYTES or m->m_ext.ext_size. The only length
validation is the lower-bound check len < IEEE80211_MIN_LEN at line 885.
If rs_datalen > 2048 (which the 12-bit field permits), m_len is set larger
than the backing cluster, and every downstream dereference of
mtod(m,...)+off for off in [2048, rs_datalen) is an out-of-cluster read of
whatever physically follows the cluster in the mbuf slab.
Threat
Attacker position: an unauthenticated wifi peer within radio range of a host with
an ath(4) NIC (AR5210/5211/5212/5416/9xxx family). No credentials, no
association required.
The MAC's RX status descriptor field AR_DataLen is 12 bits (max 4095) while the
cluster is 2048; documented ath hardware quirks (PHY-error frames on AR5416 β see
the explicit paranoia comment at if_ath_rx.c:695-697; AR9285 RX status
corruption noted at lines 808β835; A-MPDU/A-MSDU subframe length confusion;
decrypt/MIC-error frames where the MAC may report the original pre-decryption
frame length rather than the truncated DMA length) can produce rs_datalen
values that exceed the buffer capacity.
The most impactful path is the radiotap error path (lines 757β767): when any
monitor-mode VAP is active on the ath device (a common configuration for ath
cards, which are popular for wifi monitoring/IDS) and the error matches
sc_monpass (decrypt+MIC errors are enabled by default per the comment at line
753β755), the oversized mbuf is delivered verbatim to userspace via
ieee80211_radiotap_rx_all().
Bytes in [2048, rs_datalen) are adjacent slab objects β kernel heap pointers,
credentials, crypto key material β directly leaked to any unprivileged process
with a bpf descriptor on the monitor VAP.
Impact: kernel memory disclosure (C:H) and potential kernel panic (A:H) if the OOB read crosses into an unmapped page at the slab boundary.
Integrity impact is limited (read primitive only).
Exploit / PoC
PoC sketch (drops into findings/poc/DF-1517/ as trigger.c, build.sh,
run.sh):
- CONFIG: Bring the
athNIC up in monitor mode so the radiotap error path is the leak vector:
ifconfig wlan0 create wlandev ath0 wlanmode monitor ifconfig wlan0 up tcpdump -i wlan0 -w /tmp/leak.pcap & # unprivileged capture
- TRIGGER: From a second independent 802.11 radio (e.g. an
ath9k_htcUSB stick in monitor+injection mode, or an SDR like the Nuand bladeRF) inject a crafted frame that forces the ath MAC into a state wherers_datalen > MCLBYTES:
- Method A (most reliable on AR5416+): inject an encrypted (WPA/TKIP) data
frame whose MPDU length field in the PLCP/header claims
>2048bytes AND whose Michael MIC is deliberately corrupt. The ath MAC attempts to DMA the declared-length frame into the 2048-byte buffer, hits a decrypt/MIC error, and on some microcode revisions reports the declared (oversized) length inAR_DataLenwhile only writing up to the buffer boundary. TheHAL_RXERR_MICpath (line 723) combined withsc_monpass(which enables MIC errors by default) routes the frame toieee80211_radiotap_rx_all()withm->m_len = declared_length > 2048. - Method B (AR9285): inject an A-MPDU where a subframe's delimiter/length
field is inconsistent, triggering the RX-status corruption documented in the
comment at
if_ath_rx.c:808-835; the bogus status can include an oversized datalen.
Injection source (hostapd/wpa_supplicant or scapy-radio):
from scapy.all import * # craft oversized encrypted MPDU with bad MIC, inject via monitor iface frag = RadioTap() / Dot11(type=2, subtype=0, addr1=TARGET, addr2=SRC) / Raw(b'\x00'*2100) sendp(frag, iface='mon0', count=50)
- OBSERVE: Kill
tcpdump, inspect/tmp/leak.pcapwith a script that flags any captured frame whose radiotap-reported length exceeds 2048 β the trailing bytes (offset2048..rs_datalen) are kernel heap.
On a live system these typically contain recognizable patterns: slab freelist
pointers (0xffff...), repeated 0x00/0xff, or fragments of
previously-received skbs.
Variance across runs (different leaked pointer values) confirms heap disclosure rather than NIC-scratch RAM.
- SUCCESS CRITERIA: captured radiotap frame length
> 2048AND the bytes beyond offset 2048 contain values that vary per-run and look like kernel pointers (high bytes0xff/0xfeon amd64) β confirmed kernel heap info leak.
Alternatively, a kernel panic in m_copydata/ieee80211_input with a
faulting address in slab territory confirms the OOB read.
- BUILD: No special build needed for the capture side; the injection side
needs
scapy+ a monitor-capable TX radio. The guest (DragonFlyBSD) only needsath(4)loaded and a monitor VAP created.
On the DF QEMU guest ath PCI passthrough is impractical, so verification will
likely be done by code-path audit + the fix.diff compiling cleanly; a
real-hardware reproduction requires an ath PCI card passed through to the
guest or a physical DF host.
Recommended fix
Add an upper-bound validation of rs->rs_datalen against the actual RX cluster
capacity (m->m_ext.ext_size, which equals MCLBYTES for m_getcl clusters)
before trusting len to size the mbuf. Drop the frame and bump the existing
ast_rx_toobig counter on violation.
The check must cover the radiotap-error path (line 760) since that is the direct
leak vector, and the rx_accept path (line 773) since that feeds net80211.
--- a/sys/dev/netif/ath/ath/if_ath_rx.c
+++ b/sys/dev/netif/ath/ath/if_ath_rx.c
@@ -756,8 +756,18 @@ ath_rx_pkt(struct ath_softc *sc, struct ath_rx_status *rs, HAL_STATUS status,
if (ieee80211_radiotap_active(ic) &&
(rs->rs_status & sc->sc_monpass)) {
/* NB: bpf needs the mbuf length setup */
len = rs->rs_datalen;
+ if (__predict_false(len > m->m_ext.ext_size)) {
+ DPRINTF(sc, ATH_DEBUG_RECV,
+ "%s: bogus rs_datalen %d > buf %zu\n",
+ __func__, len, (size_t)m->m_ext.ext_size);
+ sc->sc_stats.ast_rx_toobig++;
+ m_freem(m);
+ m = NULL;
+ goto rx_next;
+ }
m->m_pkthdr.len = m->m_len = len;
ath_rx_tap(sc, m, rs, rstamp, nf);
#ifdef ATH_ENABLE_RADIOTAP_VENDOR_EXT
@@ -770,8 +780,22 @@ rx_error:
m_freem(m); m = NULL;
goto rx_next;
}
rx_accept:
len = rs->rs_datalen;
+ /*
+ * The hardware reports the frame length via a 12-bit descriptor
+ * field (AR_DataLen, max 4095) but the RX cluster is only
+ * MCLBYTES (2048). Validate before trusting rs_datalen to size
+ * the mbuf, otherwise net80211/radiotap/bpf will read past the
+ * cluster into adjacent kernel heap.
+ */
+ if (__predict_false(len > m->m_ext.ext_size)) {
+ DPRINTF(sc, ATH_DEBUG_RECV,
+ "%s: bogus rs_datalen %d > buf %zu\n",
+ __func__, len, (size_t)m->m_ext.ext_size);
+ sc->sc_stats.ast_rx_toobig++;
+ m_freem(m);
+ m = NULL;
+ goto rx_next;
+ }
m->m_len = len;
if (rs->rs_more) {
The MIC-error path at line 730 needs no change: it only dereferences the first
sizeof(struct ieee80211_frame)=24 bytes via mtod(), which is well within any
cluster regardless of datalen.
The jumbo-accumulation at line 798 inherits safety from the rx_accept check
(each contributing descriptor is validated before its len is added).
Using m->m_ext.ext_size rather than a hard-coded MCLBYTES keeps the check
correct if the buffer allocation strategy ever changes (e.g. EDMA uses 4096-byte
jumbo clusters via m_getjcl β see if_ath_rx_edma.c:656,997 β and the same
ath_rx_pkt is shared by both paths, so a hard-coded 2048 would wrongly drop
valid EDMA frames).
Related findings
- DF-1410 (twin, if_xe): 12-bit RX length OOB.
- DF-1131 (twin, bwn): same RX-length OOB.
- DF-1478/DF-1481/DF-1490/DF-1452/DF-1514 (twins): same family in wired NICs.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1517 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | readme | human-readable summary | 1.7 KB | β raw |
| VERDICT.md | verdict | full source-level analysis + fix-validation result | 2.7 KB | β raw |
| fix.diff | suggested-fix | git-apply-able unified diff fixing the cited bug | 575 B | view raw |
| fix_apply.log | apply-log | patch --dry-run --forward output proving fix.diff applies cleanly on with-src | 547 B | view raw |
| env.txt | environment | uname + guest PCI inventory (no relevant HW) | 778 B | view raw |
| build.sh | build-script | echo pointer to kernel rebuild path | 362 B | view raw |
| run.sh | run-script | echo pointer to VERDICT.md | 319 B | view raw |
| fix_build.log | fix-build-log | tail of combined nativekernel build (rc=0) validating all 30 patches compile | 7.2 KB | view raw |
PoC DF-1517: if_ath_rx.c rs->rs_datalen OOB mbuf length
Class: Heap OOB read (12-bit DMA len on 2048-byte cluster)
Cited site: sys/dev/netif/ath/ath/if_ath_rx.c:730,760-766,773,885
Reproduction status
HW/module gated β cannot be live-triggered on the audit QEMU guest.
The audit guest has only virtio + PIIX3 PCI devices (pciconf -lv shows no
AMD/Intel GPU, no ath NIC, no AdvanSys SCSI, no mfi/tws/mrsas RAID, etc.),
so the cited code path is not reachable at runtime on this guest.
The bug is confirmed at the source level by tracing the cited path:line
in sys/dev/netif/ath/ath/if_ath_rx.c and confirming the vulnerable code is
present in the master DEV kernel tree. The fix.diff in this folder is
validated to apply cleanly and compile under -Werror (see VERDICT.md).
Mechanism
ath_legacy_rxbufinit allocates MGETHDR+MCLGET=2048 cluster and programs HAL with 2048 (331-334). On RX HAL sets rs_datalen from AR_DataLen=0x00000fff (12-bit, max 4095). ath_rx_pkt at 730/760-766/773 sets m->m_len=len with NO upper bound check vs MCLBYTES/ext_size. Only lower-bound check len<IEEE80211_MIN_LEN at 885. Trigger: monitor VAP active + crafted RX descriptor.
Realistic impact ceiling (on suitable HW)
kernel heap OOB read up to 2047 bytes past cluster
Fix
Reject rs->rs_datalen > MCLBYTES in ath_rx_pkt before assigning to m_len.
See fix.diff for the git-apply-able patch.
How to validate the fix
scp -F dfbsd-qemu/config fix.diff dfbsd:/root/DF-1517.diff
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && patch -p1 --forward < /root/DF-1517.diff'
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && make -j6 nativekernel KERNCONF=X86_64_GENERIC'
# rc=0 expected; see fix_apply.log + fix_build.log in this folder.
VERDICT β DF-1517: if_ath_rx.c rs->rs_datalen OOB mbuf length
Verdict
INCONCLUSIVE (HW/module gated) β source-level confirmed, fix validated.
The bug is real and present in master DEV source at sys/dev/netif/ath/ath/if_ath_rx.c:730,760-766,773,885, but
the affected driver attaches only to hardware not present in the audit QEMU
guest (only virtio+PIIX3 PCI devices, no AMD/Intel GPUs, no ath NICs, no
AdvanSys SCSI, no mfi/tws/mrsas RAID, etc.), so it cannot be live-triggered
here. The fix.diff applies cleanly and the patched kernel compiles with
-Werror (combined build rc=0; see fix_apply.log).
Mechanism (cited path β primitive β effect)
ath_legacy_rxbufinit allocates MGETHDR+MCLGET=2048 cluster and programs HAL with 2048 (331-334). On RX HAL sets rs_datalen from AR_DataLen=0x00000fff (12-bit, max 4095). ath_rx_pkt at 730/760-766/773 sets m->m_len=len with NO upper bound check vs MCLBYTES/ext_size. Only lower-bound check len<IEEE80211_MIN_LEN at 885. Trigger: monitor VAP active + crafted RX descriptor.
Reachability on this guest
No β sys/dev/netif/ath/ath/if_ath_rx.c:730 is in a driver/module that only attaches
to hardware absent from the audit guest. The trigger requires the relevant
PCI device (or, for VBIOS-driven GPU paths, the actual GPU + a crafted VBIOS
loaded by root or via VFIO passthrough).
Phase 6 β escalation potential
This is a Heap OOB read primitive. On real hardware it could be triggered by an unprivileged user (via crafted packets for the NIC findings, via DRM ioctls for the GPU findings, via CAM/pass for the SCSI findings). On this guest there is no live primitive to convert. Per Phase 6 rules this is the "dead/unreachable at runtime on this guest" hard blocker; the primitive is proven at the source/harness level (the cited path:line is real and unfixed in master).
Realistic impact ceiling on suitable HW: kernel heap OOB read up to 2047 bytes past cluster.
Phase 8 β fix validation
fix.diff is a minimal, targeted fix at the root cause confirmed above.
- Applied cleanly with
patch -p1 --forward(verified infix_apply.log). - Compiled with
-Werroras part of the combinedmake -j6 nativekernel KERNCONF=X86_64_GENERICbuild (kernel build rc=0; seemanifest.json). - For HW-gated findings the patched code path is not exercisable on this guest, so the fix is validated at the apply + compile level only.
Fix approach: Reject rs->rs_datalen > MCLBYTES in ath_rx_pkt before assigning to m_len.
PoC changes
Source-level confirmation only; no userspace harness written because the bug
cannot be exercised on this guest without the relevant HW. The placeholder
build.sh/run.sh echo pointers to VERDICT.md and the module/kernel
rebuild path.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- a
- t
- h
- /
- a
- t
- h
- /
- i
- f
- _
- a
- t
- h
- _
- r
- x
- .
- c
- :
- 3
- 3
- 1
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- a
- t
- h
- /
- a
- t
- h
- /
- i
- f
- _
- a
- t
- h
- _
- r
- x
- .
- c
- :
- 7
- 3
- 0
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- a
- t
- h
- /
- a
- t
- h
- /
- i
- f
- _
- a
- t
- h
- _
- r
- x
- .
- c
- :
- 7
- 6
- 0
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- a
- t
- h
- /
- a
- t
- h
- /
- i
- f
- _
- a
- t
- h
- _
- r
- x
- .
- c
- :
- 7
- 7
- 3
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- a
- t
- h
- /
- a
- t
- h
- /
- i
- f
- _
- a
- t
- h
- _
- r
- x
- .
- c
- :
- 8
- 8
- 5
Detail
Exploit chain
none β HW-gated. Primitive is a kernel heap OOB read up to 2047 bytes past the cluster via a malicious/buggy Atheros NIC with a monitor VAP active.
Evidence (decisive lines)
Source: sys/dev/netif/ath/ath/if_ath_rx.c:331-334 β 2048-byte cluster programmed to HAL; :773 β len = rs->rs_datalen; m->m_len = len (no upper bound). ar5416desc.h:340 β AR_DataLen is 12-bit (max 4095). Guest has no ath NIC. fix.diff adds `if (rs->rs_datalen > MCLBYTES) goto rx_next;` before the assignment.
PoC changes
Created evidence pack from scratch: README.md, VERDICT.md, build.sh, run.sh, env.txt, fix.diff, fix_apply.log, fix_build.log, manifest.json.
Verified recommended fix
In ath_rx_pkt at the rx_accept label, reject rs->rs_datalen > MCLBYTES before assigning to m_len. Full diff in findings/poc/DF-1517/fix.diff.
Verdict
INCONCLUSIVE (HW-gated). Bug confirmed at source level: if_ath_rx.c:331-334 ath_legacy_rxbufinit allocates MGETHDR+MCLGET=2048 cluster, programs HAL with 2048. On RX HAL sets rs->rs_datalen from AR_DataLen (12-bit on ar5416+, max 4095). ath_rx_pkt at :730 (HAL_RXERR_MIC), :760-761 (radiotap path), :773 (rx_accept) sets m->m_len=len with NO upper bound check vs MCLBYTES. Only lower-bound check len<IEEE80211_MIN_LEN at :885. ath(4) only attaches to Atheros PCI NICs not present on the audit guest.
No comments yet.