Multi-segment RX reads overwritten descriptor length inflating m_len past cluster for heap OOB read
Summary
age_rxeof at if_age.c:2025: age_newbuf(rxd,0) called BEFORE mp->m_len=AGE_RX_BYTES(desc->len) at :2037. age_newbuf (:2783) overwrites desc->len lower 16 bits -> AGE_RX_BYTES (upper 16 bits) returns 0 for all non-first segments -> pktlen=0. Then :2061 pktlen-=ETHER_CRC_LEN -> -4. :2079 m->m_len=age_rxlen-pktlen=age_rxlen+4. For jumbo frame (9018B): m_len=9018 on 2046B cluster -> ~6972B heap OOB read. Requires jumbo MTU (ifconfig age0 mtu 9000) + remote sender of >2046B frame. UNAUTHENTICATED REMOTE. Fix: read desc->len BEFORE age_newbuf.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1292 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | readme | finding summary + race explanation | 3.0 KB | β raw |
| VERDICT.md | verdict | mechanism + citations + fix | 2.1 KB | β raw |
| fix.diff | suggested-fix | snapshot desc->len into local segsz BEFORE age_newbuf() | 1.1 KB | view raw |
| build.sh | build-script | no PoC binary | 357 B | view raw |
| run.sh | run-script | no runtime PoC (NIC absent) | 323 B | view raw |
| env.txt | environment | guest PCI/kld/uname | 862 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-1292 β age (Attansic L1) RX descriptor-length race β OOB heap read
Finding
age_rxeof at sys/dev/netif/age/if_age.c:2025 calls age_newbuf(sc, rxd, 0)
before reading desc->len at line 2037. age_newbuf (line 2783-2785)
reprograms the descriptor for the next receive and overwrites desc->len:
desc->len = htole32((segs[0].ds_len & AGE_RD_LEN_MASK=0xFFFF)
<< AGE_RD_LEN_SHIFT=0);
The post-recycle descriptor's upper 16 bits are zero, so
AGE_RX_BYTES(le32toh(desc->len)) (which masks 0xFFFF0000 >> 16) returns 0
for every non-first segment. pktlen stays 0, then line 2061 does
pktlen -= ETHER_CRC_LEN β pktlen = -4, so line 2079 computes
m->m_len = age_rxlen - pktlen = age_rxlen + 4. For a 9018-byte jumbo frame
the first mbuf (a 2046-byte MCLBYTES cluster) is assigned m_len = 9022,
producing a ~6976-byte OOB read when upper layers consume the mbuf.
The attack is unauthenticated remote: send a multi-segment jumbo frame to
a victim age0 interface configured for mtu 9000.
Why we did not reproduce at runtime
The audit guest has only a virtio-net NIC (virtio_pci0, vendor 1af4). The
age driver attaches only to Attansic L1 (Marvell) Gigabit PCI NICs
(vendor 1969). kldstat -v shows pci/if_age is compiled into GENERIC but it
has no HW to probe, so it does not attach and age_rxeof is never called.
Source-level confirmation
if_age.c:2025βif (age_newbuf(sc, rxd, 0) != 0)called BEFORE reading desc->len.if_age.c:2037βmp->m_len = AGE_RX_BYTES(le32toh(desc->len));reads desc->len AFTER newbuf overwrote it.if_age.c:2784-2785βdesc->addr = ...; desc->len = htole32((segs[0].ds_len & AGE_RD_LEN_MASK) << AGE_RD_LEN_SHIFT);β confirmed overwrite.if_agereg.h:601-603βAGE_RX_BYTES(x)extracts bits 16-31;AGE_RRD_LEN_MASK=0xFFFF0000,AGE_RRD_LEN_SHIFT=16.if_agereg.h:612-614βAGE_RD_LEN_MASK=0x0000FFFF,AGE_RD_LEN_SHIFT=0β the WRITE side (inage_newbuf) puts the buffer size in the LOW 16 bits β upper 16 bits are 0 after recycle.if_age.c:2058-2079βage_rxlen -= ETHER_CRC_LEN; ...; m->m_len = sc->age_cdata.age_rxlen - pktlen;β produces the oversize m_len.
Bug is real and the order-of-operations root cause is unambiguous. The driver
author intended desc->len to be the receive-side length but recycled the
descriptor too early.
Realistic impact ceiling
Remote OOB heap read of up to ~7 KB per packet on a host with an age NIC
running jumbo frames. Could leak kernel heap contents (info disclosure) and
will very likely panic on INVARIANTS-enabled kernels when m_len overshoots
the cluster. Not directly an escalation primitive; with the leaked bytes an
attacker may defeat KASLR-equivalent information, but DragonFly disables
vm.randomize_mmap and there is no kernel-text KASLR here anyway.
Fix
fix.diff snapshots desc->len into a local segsz before the
age_newbuf call, and uses segsz at line 2037 instead of re-reading
desc->len. Adds int segsz; to age_rxeof's locals.
DF-1292 β age (Attansic L1) RX descriptor-length race
Verdict
NOT REPRODUCED β real source-level bug confirmed; unreachable on this guest (no Attansic L1 NIC β only virtio-net).
Mechanism (verified)
if_age.c:2025βage_newbuf(sc, rxd, 0)recycles the descriptor.if_age.c:2784-2785βage_newbufoverwritesdesc->lenwith the new buffer size in the LOW 16 bits.if_age.c:2037βmp->m_len = AGE_RX_BYTES(le32toh(desc->len));βAGE_RX_BYTES(if_agereg.h:601-603) extracts the HIGH 16 bits, which are now 0 βm_len = 0for non-first segments βpktlen = 0.if_age.c:2061βpktlen -= ETHER_CRC_LEN;β pktlen = -4.if_age.c:2079βm->m_len = sc->age_cdata.age_rxlen - pktlen;βm_len = age_rxlen + 4β 9022 for a 9018-B jumbo frame on a 2046-B cluster β ~6976-B OOB read.
Root cause is unambiguous: the descriptor's receive length is read AFTER
age_newbuf has destroyed it. The fix is to snapshot the length before the
recycle.
Why not triggered on this guest
pciconf -l(env.txt): only virtio-net (vendor 1af4). No vendor-1969 Attansic device.ageis in GENERIC but does not attach.
Phase 4(d): genuinely not reachable on this kernel. The bug is in a remote attack surface (RX path of a Gigabit NIC) so it is realistically exploitable only on hardware that has an Attansic L1 NIC; the audit guest is not such a host.
Fix
fix.diff:
1. Add int segsz; to age_rxeof locals.
2. Snapshot segsz = AGE_RX_BYTES(le32toh(desc->len)); immediately after the
desc = rxd->rx_desc; fetch, BEFORE age_newbuf.
3. Replace mp->m_len = AGE_RX_BYTES(le32toh(desc->len)); with
mp->m_len = segsz;.
This is the smallest change that preserves the buffer-recycle optimisation while restoring correct length accounting.
Fix validation
Compiles cleanly in the unified 5-fix kernel build (fix_build.log).
Runtime before/after is not_testable β no age NIC.
Realistic impact
Remote OOB heap read of up to ~7 KB on a jumbo-frame age NIC. Likely panic
on INVARIANTS kernels; info disclosure on non-debug kernels.
Fix verification
not_testablecompile validated
nativekernel rc=0
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
Source-confirmed. age_rxeof age_newbuf overwrites desc->len before read -> m_len overflow ~7KB OOB read. age module, no Attansic NIC.
No comments yet.