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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1292 Β· 8 files
FileTypeDescriptionSize
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
README.md readme finding summary + race explanation
↓ download 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 (in age_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.

VERDICT.md verdict mechanism + citations + fix
↓ download raw

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_newbuf overwrites desc->len with 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 = 0 for 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.
  • age is 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_testable

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