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)
PoC verification
Evidence pack
findings/poc/DF-4045 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 claimingframelen = 9022buildsm_len = 9022on the 2048-byte cluster fromstge_newbufβ 6974-byte heap OOB read into a delivered frame (readable by any local user with a socket/BPF onstgeX). - Case B (any MTU, incl. default):
framelen < rxlenat FrameEnd β negative tailm_lenβm_copydatacomputesunsigned count = min(m->m_len - off, len)β ~4 GBbcopyβ panic/UB as soon as any consumer walks the chain (BPF attached); the first mbuf is full-sized soether_input'sm_len < ETHER_HDR_LENguard 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.
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), andSIOCSIFMTUaccepts up toSTGE_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_newbufusesm_getcland setsm_len = MCLBYTES(if_stge.c:2326-2329); 2046 after theETHER_ALIGNm_adjwhen 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 sotx_prodcan never laptx_cons, init marks all TFDs done with an empty busyq (:2280) and thetx_cnt <= 0guard precedes every control read. stge_encapfreeq 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_testablefix.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
Confirmed kernel references
- sys/dev/netif/stge/if_stge.c:1606
- sys/dev/netif/stge/if_stge.c:1607
- sys/dev/netif/stge/if_stge.c:1608
- sys/dev/netif/stge/if_stge.c:1623
- sys/dev/netif/stge/if_stge.c:1625
- sys/dev/netif/stge/if_stge.c:1261
- sys/dev/netif/stge/if_stge.c:2013
- sys/dev/netif/stge/if_stge.c:2326
- sys/dev/netif/stge/if_stge.c:2329
- sys/dev/netif/stge/if_stge.c:2335
- sys/dev/netif/stge/if_stgereg.h:167
- sys/dev/netif/stge/if_stgevar.h:55
- sys/kern/uipc_mbuf.c:1689
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.
No comments yet.