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

stge_rxeof trusts the NIC-DMA'd 16-bit RxDMAFrameLen with no per-buffer bound: jumbo-MTU FrameEnd claim yields m_len up to 9022 on the 2048-byte cluster (up to 6974-byte heap OOB read into delivered frames); framelen < rxlen yields negative tail m_len -> m_copydata unsigned count -> ~4GB bcopy panic

Summary

stge_rxeof applies the RFD status quadword's raw 16-bit frame-length field with only a per-interface check (> framesize) AFTER m_len has been assigned. With jumbo MTU a FrameEnd descriptor claiming 9022 sets m_len=9022 on the MCLBYTES cluster - 6974-byte OOB read past the cluster into every consumer trusting m_len (heap bytes delivered inside received frames). Independently at ANY MTU, framelen < accumulated rxlen gives a NEGATIVE tail m_len that passes the framesize check; m_copydata computes unsigned count on it -> ~4GB bcopy panic. Legit frames never violate either bound, so any violation is a device lie. stge(4) instance of the DF-3604/3759/3933/3989/4024 family. Guest-built geometry model prints exact extents. Chain-geometry validation fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4045 Β· 11 files
FileTypeDescriptionSize
README.md β€” 1.6 KB ↓ raw
VERDICT.md β€” 4.8 KB ↓ raw
trigger.c β€” 4.5 KB view raw
build.sh β€” 106 B view raw
run.sh β€” 93 B view raw
build.log β€” 11 B view raw
run.log β€” 1.4 KB view raw
env.txt β€” 210 B view raw
fix.diff β€” 1.0 KB view raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 4.9 KB view raw

DF-4045 β€” stge_rxeof trusts the device-fed 16-bit RxDMAFrameLen (no per-buffer bound)

  • File: sys/dev/netif/stge/if_stge.c (stge(4), Sundance/IC Plus IP1000A, TC9021)
  • Severity: Medium Β· Confidence: certain Β· hardware-gated runtime (no stge(4) on the QEMU guest β€” virtio-net only)
  • Family: DF-3604 (bce) / DF-3759 (alc) / DF-3933 (age) / DF-3989 (ale) / DF-4024 (re) β€” device-fed RX length trust

Build

cc -O2 -Wall -Wextra -o /root/tr4045 /root/tr4045.c

Run

/root/tr4045          # userspace geometry model, RC=0

Expected output

The model prints the OOB extents implied by the driver's masks/geometry:

  • Case A (jumbo MTU 9004, SIOCSIFMTU-legal): a single FrameEnd descriptor claiming framelen = 9022 builds m_len = 9022 on the 2048-byte cluster from stge_newbuf β†’ 6974-byte heap OOB read into a delivered frame (readable by any local user with a socket/BPF on stgeX).
  • Case B (any MTU, incl. default): framelen < rxlen at FrameEnd β†’ negative tail m_len β†’ m_copydata computes unsigned count = min(m->m_len - off, len) β†’ ~4 GB bcopy β†’ panic/UB as soon as any consumer walks the chain (BPF attached); the first mbuf is full-sized so ether_input's m_len < ETHER_HDR_LEN guard does not stop it.

Runtime trigger precondition (NOT met on this guest): a faulty or malicious TC9021/IP1000A DMA'ing a lying RFD status quadword β€” see trigger.c header.

Artifacts

trigger.c, build.sh, run.sh, build.log, run.log, env.txt, fix.diff, VERDICT.md, manifest.json, verdict.json.

VERDICT.md
↓ download raw

DF-4045 VERDICT β€” stge_rxeof device-fed length trust (source-certain, runtime hardware-gated)

Reproduced?

Not on hardware β€” the guest has no stge(4) device (env.txt: vtnet0 lo0, NO_STGE_HW, DragonFly 6.5-DEVELOPMENT). Per the established family convention (DF-3989/DF-4024) this is recorded as inconclusive at runtime, certain at source level. The geometry model (trigger.c) builds and runs on the guest with RC=0 (run.log) and prints the exact extents below.

Root cause (path:line)

stge_rxeof applies the NIC-DMA'd frame length with only a per-interface bound, never a per-buffer bound:

  • sys/dev/netif/stge/if_stge.c:1606-1607 β€” on a FrameEnd descriptor: mp->m_len = RFD_RxDMAFrameLen(status) - sc->sc_cdata.stge_rxlen;
  • if_stge.c:1608 β€” sc->sc_cdata.stge_rxlen += mp->m_len;
  • if_stge.c:1623-1629 β€” the ONLY check: m->m_pkthdr.len = stge_rxlen; if (m->m_pkthdr.len > sc->sc_if_framesize) drop;
  • RFD_RxDMAFrameLen(x) = x & 0xffff β€” raw 16-bit device-fed field, sys/dev/netif/stge/if_stgereg.h:167.
  • sc_if_framesize = if_mtu + ETHER_HDR_LEN + ETHER_CRC_LEN (if_stge.c:2013), and SIOCSIFMTU accepts up to STGE_JUMBO_MTU = 9004 (if_stge.c:1261, if_stgevar.h:55-57) β†’ framesize up to 9022.
  • Every RX buffer is one MCLBYTES (2048) cluster: stge_newbuf uses m_getcl and sets m_len = MCLBYTES (if_stge.c:2326-2329); 2046 after the ETHER_ALIGN m_adj when framesize ≀ 2046 (:2335-2336).

Intermediate (non-FrameEnd) descriptors contribute their FULL buffer length (2046/2048) to stge_rxlen; the frame's total length appears only in the FrameEnd descriptor's 16-bit field.

Consequence geometry (model output, run.log)

Case A β€” jumbo MTU, over-long last buffer (heap OOB read): framelen = framesize = 9022 with k intermediate buffers: last m_len = 9022 βˆ’ 2048Β·k, i.e. up to 9022 on a 2048-byte cluster β†’ 6974 bytes read past the cluster (k=1: 4926; k=2: 2878; k=3: 830). The only driver check passes (9022 ≀ framesize). Every consumer trusting m_len/pkthdr.len β€” BPF tap, m_copydata pulls, protocol header reads past 2 KB β€” reads adjacent kernel heap into a delivered network frame: a kernel-heap info leak readable by any local user with a socket on the interface. Needs jumbo MTU (a documented, SIOCSIFMTU-legal config).

Case B β€” any MTU (incl. default 1500), negative tail m_len: k intermediates then FrameEnd with framelen < rxlen (e.g. k=1, framelen=100): mp->m_len = 100 βˆ’ 2046 = βˆ’1946; pkthdr.len = 100 passes the check; the first mbuf is full-sized so ether_input's m_len < ETHER_HDR_LEN guard (sys/net/if_ethersubr.c) does not drop it. m_copydata (sys/kern/uipc_mbuf.c:1671,1689) computes unsigned count = min(m->m_len - off, len) on the negative tail β†’ count β‰ˆ 4 GB β†’ bcopy overflow β†’ panic / memory corruption as soon as a consumer walks the chain (BPF attached; the KASSERTs at uipc_mbuf.c:1680+ are compiled out of GENERIC).

Threat model

Device-fed bug: requires a faulty or malicious TC9021/IP1002A (compromised NIC firmware, malicious PCIe/Thunderbolt NIC, or a second kernel bug that corrupts the coherent RX ring). Inbound-only: read primitive (Case A) / DoS-grade UB (Case B); no write primitive. Same threat framing as the rest of the DF-3604 family.

Fix validation

fix.diff bounds the FrameEnd m_len to [0, MCLBYTES] and drops the frame using the driver's own error-drop idiom (free chain + reset + continue, mirroring the existing iqdrops path at if_stge.c:1595-1602). git apply --check passes against the read-only tree; runtime validation would require stge(4) hardware (not present on the guest) β€” fix_status: not_testable.

Negative space checked this pass on the same surface

  • txeof producer/consumer arithmetic sound (if_stge.c:1455-1495): tx_cnt == len(busyq) invariant, HIWAT 248 < RING_CNT 256 so tx_prod can never lap tx_cons, init marks all TFDs done with an empty busyq (:2280) and the tx_cnt <= 0 guard precedes every control read.
  • stge_encap freeq non-empty guaranteed by the HIWAT gate (:1186); error path frees the mbuf before any queue mutation (:1114-1118).
  • dma_alloc failure paths pair with dma_free (:849-1036).
  • Residual (not filed): stge_tx_error's register-drain loop (:1356-1381) is bounded only by hardware clearing TS_TxComplete β€” device-gated DoS, weaker than the DF-3761 unbounded-walk shape (no counter bound applies to a status register).

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.diff applies clean (git apply --check) and uses the driver's own chain-free + STGE_RXCHAIN_RESET + continue idiom on the already-compiled path; runtime validation would need stge(4) hardware.

findings/poc/DF-4045/fix.diff
↓ fix.diffper-fix-DF-4045

Confirmed kernel references

Detail

Exploit chain

On real (faulty/malicious) IP1000A/TC9021 hardware: DMA an RFD status quadword with RFDDone|FrameEnd and a lying 16-bit DMAFrameLen -> case A (jumbo MTU) m_len up to 9022 over a 2048-byte cluster -> leaked kernel-heap bytes ride into received packet payloads readable by any local user with a socket on the interface; case B (default MTU) negative tail m_len -> m_copydata unsigned count -> ~4GB bcopy -> panic (BPF attached). Inbound-only: read/DoS primitive, no write.

Evidence (decisive lines)

run.log (model output: 6974/4926/2878/830-byte OOB extents, negative m_len -1946 -> ~4GB bcopy, RC=0); build.log; env.txt (vtnet0+lo0 only, NO_STGE_HW); VERDICT.md (path:line trace); trigger.c; fix.diff (FrameEnd m_len bounded to [0, MCLBYTES] with driver's own drop idiom)

PoC changes

No seed PoC (pass 1 filed nothing). Authored trigger.c as hardware-precondition + cluster-geometry model per the DF-3989/DF-4024 family convention; honest hardware-absent verification.

Verified recommended fix

In stge_rxeof drop the frame when the FrameEnd mp->m_len is < 0 or > MCLBYTES (see fix.diff); legit frames never trip either bound.

Verdict

Source-level certain, runtime hardware-gated (inconclusive per family convention). stge_rxeof takes the received frame length from the NIC-DMA'd 16-bit RFD DMAFrameLen field (RFD_RxDMAFrameLen(x) = x & 0xffff, if_stgereg.h:167) and applies it with only a per-interface bound: on the FrameEnd descriptor if_stge.c:1606-1607 sets mp->m_len = RFD_RxDMAFrameLen(status) - sc->sc_cdata.stge_rxlen and the ONLY check (:1623-1625) is m->m_pkthdr.len > sc->sc_if_framesize afterwards. (1) With jumbo MTU (SIOCSIFMTU accepts up to STGE_JUMBO_MTU=9004, :1261; framesize=9022, :2013) a single FrameEnd claiming framelen=9022 yields m_len=9022 on the 2048-byte MCLBYTES cluster from stge_newbuf (m_getcl, :2326-2329) -> 6974-byte heap OOB read into a delivered frame (BPF tap, m_copydata pulls, header reads past 2KB) -> kernel-heap info leak to any local user with a socket/BPF on stgeX (k=1/2/3 intermediates: 4926/2878/830 bytes). (2) At ANY MTU including default, framelen < rxlen gives a NEGATIVE tail m_len that passes the framesize check (pkthdr.len = framelen) and survives ether_input's m_len < ETHER_HDR_LEN first-mbuf guard; m_copydata (uipc_mbuf.c:1689) computes unsigned count = min(m->m_len - off, len) on it -> ~4GB bcopy -> panic/UB when any consumer walks the chain. Legit frames never violate either bound (intermediates contribute full 2046/2048-byte buffers, remainder < 2048), so any violation is a device lie. Runtime: guest has virtio-net only, zero Sundance/Tamarack PCI devices (env.txt); trigger.c (geometry model) builds and runs on the guest RC=0 (run.log). stge(4) instance of the DF-3604(bce)/3759(alc)/3933(age)/3989(ale)/4024(re) family; pass 1 filed nothing against this file.