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

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_MIC path): only checks len >= sizeof(struct ieee80211_frame) β€” lower bound only.
  • Line 760–761 (radiotap error path): m->m_pkthdr.len = m->m_len = len, then ieee80211_radiotap_rx_all(ic, m) at line 766.
  • Line 773–774 (rx_accept): m->m_len = len; later m->m_pkthdr.len = len at 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):

  1. CONFIG: Bring the ath NIC 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
  1. TRIGGER: From a second independent 802.11 radio (e.g. an ath9k_htc USB stick in monitor+injection mode, or an SDR like the Nuand bladeRF) inject a crafted frame that forces the ath MAC into a state where rs_datalen > MCLBYTES:
  • Method A (most reliable on AR5416+): inject an encrypted (WPA/TKIP) data frame whose MPDU length field in the PLCP/header claims >2048 bytes 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 in AR_DataLen while only writing up to the buffer boundary. The HAL_RXERR_MIC path (line 723) combined with sc_monpass (which enables MIC errors by default) routes the frame to ieee80211_radiotap_rx_all() with m->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)
  1. OBSERVE: Kill tcpdump, inspect /tmp/leak.pcap with a script that flags any captured frame whose radiotap-reported length exceeds 2048 β€” the trailing bytes (offset 2048..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.

  1. SUCCESS CRITERIA: captured radiotap frame length > 2048 AND the bytes beyond offset 2048 contain values that vary per-run and look like kernel pointers (high bytes 0xff/0xfe on 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.

  1. BUILD: No special build needed for the capture side; the injection side needs scapy + a monitor-capable TX radio. The guest (DragonFlyBSD) only needs ath(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.

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).

  • 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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1517 Β· 8 files
FileTypeDescriptionSize
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
README.md readme human-readable summary
↓ download 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.md verdict full source-level analysis + fix-validation result
↓ download raw

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 in fix_apply.log).
  • Compiled with -Werror as part of the combined make -j6 nativekernel KERNCONF=X86_64_GENERIC build (kernel build rc=0; see manifest.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

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.