Single-segment RX frame length taken unclamped from the return descriptor: m_len/m_pkthdr.len up to 65535 on a 2046-byte cluster β heap OOB read (kernleak)
Summary
age_rxlen is the raw 16-bit NIC-controlled length; in the single-segment completion path m->m_pkthdr.len = m->m_len = age_rxlen with pktlen==0 and no clamp to MCLBYTES-ETHER_ALIGN, so a forged length walks the stack ~63KB past the cluster - ether_input/BPF read kernel heap contents as packet payload. Distinct from DF-1292 (multi-seg). Clamp fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3933 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.1 KB | β raw | |
| VERDICT.md | β | 3.0 KB | β raw | |
| fix.diff | β | 828 B | view raw | |
| env.txt | β | 543 B | view raw | |
| verdict.json | β | 2.8 KB | view raw |
DF-3933 β age: single-segment RX frame length taken from return descriptor without clamp β m_len/m_pkthdr.len past the cluster (heap OOB read)
Build / Run (on hardware)
Requires an Attansic L1 NIC (age(4)) whose RX return descriptor reports a frame length larger than the receive buffer (malicious/buggy NIC, or a NIC descriptor corrupted by DMA faults). No user action beyond the interface being up; the forged RRD arrives in the interrupt path.
Lab: QEMU model of the L1 writing rx_rdesc->len = 0xFFFF<<16 with nsegs=1.
Expected result
For a single-segment frame the driver sets
m->m_pkthdr.len = m->m_len = sc->age_cdata.age_rxlen β the raw 16-bit
hardware length β on an mbuf whose usable capacity is MCLBYTES-ETHER_ALIGN
(2046). A length up to 65535 makes ether_input/BPF walk up to ~63KB past
the cluster: kernel heap contents get parsed as an Ethernet frame and can
surface in packet data (info leak) or panic the stack.
Hardware-gated (no L1 in the audit guest): source-certain, not runtime-reproduced. See VERDICT.md. (Distinct from DF-1292, which is the multi-segment desc->len path.)
DF-3933 verdict narrative
Reproduced?
No (hardware-gated: the audit guest has no Attansic L1 / age(4) NIC). Source-level certainty: certain β the arithmetic has no runtime dependency.
Root cause chain (path:line)
- age_rxeof() takes the frame length from the hardware-written return
descriptor, unclamped:
if_age.c:2000
sc->age_cdata.age_rxlen = AGE_RX_BYTES(le32toh(rxrd->len));β AGE_RX_BYTES is bits 16..31 of the DMA'd word (if_agereg.h:601-602): 0..65535, fully NIC-controlled. - The receive buffers are MCLBYTE clusters with 2 bytes of headroom:
age_newbuf() if_age.c:2742-2747
(
m_getcl,m_adj(m, ETHER_ALIGN)) and the per-buffer DMA tag is capped at MCLBYTES (if_age.c:1066-1076) β hardware may not write past 2048 bytes, and the usable payload region is 2046. - Single-segment completion path (count==0==nsegs-1):
if_age.c:2074-2079
c m = sc->age_cdata.age_rxhead; m->m_flags |= M_PKTHDR; m->m_pkthdr.rcvif = ifp; m->m_pkthdr.len = sc->age_cdata.age_rxlen; m->m_len = sc->age_cdata.age_rxlen - pktlen; /* pktlen == 0 here */Withage_rxlenup to 65535 and the cluster holding at most 2046 valid bytes,m_len/m_pkthdr.lenclaim up to ~63KB of payload that is not there.ifp->if_input(ifp, m, NULL, -1)(if_age.c:2121) then lets ether_input, BPF listeners and the IP stack walk that entire span β heap over-read; the "packet" payload beyond byte 2046 is whatever sits after the cluster in the slab (adjacent mbufs, freed data, pointers). - Error-status frames are skipped early (if_age.c:2001-2016) but a clean
status with a bogus length is not: nothing between :2000 and :2079
compares
age_rxlenagainst the buffer capacity. (The nsegs-vs-pktlen consistency check lives in age_rxintr, if_age.c:2161-2164, and only cross-checks nsegs β the length itself is still trusted.)
Relation to knowns
DF-1292 (known) is the multi-segment path: mp->m_len =
AGE_RX_BYTES(desc->len) at if_age.c:2037 per chained mbuf. This finding is
the single-segment path assigning the RRD frame length to the one and only
mbuf (:2077-2079) β same trust, different source field and line, explicitly
in scope per pass-2 instruction (a).
Threat model
Malicious/buggy NIC (DMA-fed descriptor), same boundary as DF-1292/DF-1293.
Impact
Kernel heap over-read surfaced as packet payload (kernel memory disclosure to anyone able to receive/bpf the frame) or stack panic on malformed "headers" β Medium, hardware-gated.
Fix
Clamp the single-seg length to MCLBYTES - ETHER_ALIGN (fix.diff); a full fix
also validates age_rxlen >= ETHER_HDR_LEN + ETHER_CRC_LEN.
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 not validated on the guest: the trigger path requires mly/age hardware absent from the audit guest. fix.diff is mechanical (bounds/termination-only) and authored post-verification against the cited lines.
findings/poc/DF-3933/fix.diff + VERDICT.md
Confirmed kernel references
Detail
Exploit chain
malicious/buggy L1 writes RRD len with nsegs=1 -> m_len/m_pkthdr.len up to 65535 on a 2046-byte cluster -> heap OOB read delivered to the network stack/BPF
Evidence (decisive lines)
findings/poc/DF-3933/VERDICT.md; fix.diff (MCLBYTES-ETHER_ALIGN clamp)
PoC changes
No seed PoC; hardware-gated RX path - lab reproduction documented (QEMU L1 model writing len=0xFFFF<<16 with nsegs=1).
Verified recommended fix
Clamp single-segment m_len/m_pkthdr.len to MCLBYTES - ETHER_ALIGN.
Verdict
Hardware-gated (no Attansic L1 NIC in the audit guest); source-certain, and explicitly the single-segment complement of known DF-1292 (which covers the multi-segment desc->len path at if_age.c:2037). age_rxeof takes the frame length from the DMA'd return descriptor without any clamp: sc->age_cdata.age_rxlen = AGE_RX_BYTES(rxrd->len) (if_age.c:2000), a full 16-bit NIC-controlled value. Receive buffers are MCLBYTES clusters with ETHER_ALIGN headroom (age_newbuf, if_age.c:2742-2747; rx tag capped at MCLBYTES, if_age.c:1066-1076 - 2046 usable bytes). In the single-segment completion path (count==nsegs-1==0) the driver assigns m->m_pkthdr.len = m->m_len = age_rxlen - 0 (if_age.c:2077-2079) and hands the mbuf to if_input (:2121): a hostile/buggy NIC reporting up to 65535 makes ether_input/BPF walk up to ~63KB past the cluster - kernel heap over-read surfaced as packet payload (info leak) or stack panic. The nsegs-vs-length cross-check in age_rxintr (:2161-2164) constrains nsegs only, not the length. Filed not_reproduced (family convention DF-1292), certain at source.
No comments yet.