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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3933 Β· 5 files
FileTypeDescriptionSize
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.)

VERDICT.md
↓ download raw

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)

  1. 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.
  2. 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.
  3. 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 */ With age_rxlen up to 65535 and the cluster holding at most 2046 valid bytes, m_len/m_pkthdr.len claim 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).
  4. 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_rxlen against 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_testable
baseline no→ patch + rebuild →patched clean

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

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.