dc_rxeof trusts the NIC-DMA'd 14-bit RX frame length with no bound against the 2040-byte posted cluster: heap OOB read up to ~14 KB handed to the stack, and negative m_len on FRAMESZ<4 (DF-3604 family, dc(4) instance)
Summary
dc_rxeof derives the received frame length exclusively from the descriptor status word the device DMAs: a 14-bit field, then -= ETHER_CRC_LEN, then either assigns it straight to the re-posted mbuf (quick path, default on x86_64) or copies it out with m_devget. The posted cluster holds only 2040 usable bytes. The only sanity gate reads error bits from the SAME device-produced word and deliberately tolerates GIANT frames without other errors. A device-written descriptor with RXERR clear and FRAMESZ=16383 yields m_len=16379 on a 2040-byte cluster: ether_input/BPF/socket paths read up to 14,339 bytes past the cluster (kernel heap info leak - BPF taps copy it to unprivileged listeners). FRAMESZ=0..3 wraps negative (~4GB) -> immediate OOB walk/panic from the frame's mere presence. VERIFIED: trigger executes the exact macro math on the guest. Same trust boundary as DF-3604; dc binds ~15 real-hardware tulip-workalike families (21143, PNIC, Macronix, ADMtek, ASIX, Xircom, Conexant, Davicom). Clamp fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3849 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.4 KB | β raw | |
| VERDICT.md | β | 4.9 KB | β raw | |
| trigger.c | β | 3.0 KB | view raw | |
| build.sh | β | 111 B | view raw | |
| run.sh | β | 177 B | view raw | |
| build.log | β | 827 B | view raw | |
| run.log | β | 805 B | view raw | |
| fix.diff | β | 827 B | view raw | |
| manifest.json | β | 1.4 KB | view raw | |
| verdict.json | β | 3.9 KB | view raw | |
| env.txt | β | 189 B | view raw |
DF-3849 β dc_rxeof trusts NIC-DMA'd RX frame length (heap OOB read)
Severity: Medium Β· confidence: certain (source) Β· hardware-gated at runtime (inconclusive per DF-3604 family convention)
What this is
sys/dev/netif/dc/if_dc.c:2523 β dc_rxeof() uses the 14-bit FRAMESZ
field from the device-written RX descriptor status as the mbuf length,
with no bound against the 2040-byte posted cluster. A misbehaving or
malicious dc(4) device (21143 + ~15 clone families) can make the kernel
hand the stack an mbuf claiming up to 16379 bytes (14 KB heap over-read,
BPF-visible info leak) or a negative length (β4 GB unsigned, panic).
Reproduce (audit guest)
./build.sh # cc -O2 -o /root/trigger /root/trigger.c ./run.sh # hardware-absence probe + exact integer mechanics
Success criterion (observed 2026-09-16):
FRAMESZ=16383 -> m_len=16379 on a 2040-byte cluster => OOB READ 14339 bytes past the cluster (14.0 KB) FRAMESZ= 2 -> m_len=-2 ... NEGATIVE m_len (unsigned ~4294967294): stack OOB walk guest: pci/if_dc in kernel; NICs: vtnet0 lo0 only (no dc hardware)
Files
trigger.c arithmetic replica (macros verbatim from if_dcreg.h) build.sh / run.sh build.log / run.log full untrimmed guest output fix.diff git-apply-able clamp vs the read-only sys/ tree VERDICT.md full narrative verdict.json machine verdict
DF-3849 β VERDICT
Status: inconclusive (runtime) Β· source-level: certain
Impact ceiling: kernel heap OOB read (info leak, up to ~14.0 KB per
frame past a 2040-byte RX cluster) handed to the network stack / BPF; and
negative m_len (unsigned β4 GB) on FRAMESZ < 4 β stack OOB walk / panic.
Local DoS + limited info leak from a malicious or misbehaving dc(4) NIC
(any of the 21143/PNIC/PNICII/Macronix/ADMtek/ASIX/Xircom/Conexant/Davicom
clones this driver binds).
What was verified, and how
1. Source-level verification (certain)
sys/dev/netif/dc/if_dc.c read end-to-end; guest /usr/src copy
verified byte-identical (md5 f396b494478178deff15f9ba999ef0b9).
- RX buffers are single mbuf clusters trimmed by 8 bytes:
dc_newbufif_dc.c:2321-2324m_new = m_getcl(M_NOWAIT, MT_DATA, M_PKTHDR);m_new->m_len = m_new->m_pkthdr.len = MCLBYTES(= 2048), thenif_dc.c:2331m_adj(m_new, sizeof(u_int64_t))β 2040 usable bytes, posted to the descriptor withc->dc_ctl = DC_RXCTL_RLINK | DC_RXLEN(:2343,DC_RXLEN = 1536,if_dcreg.h:465).- The only length source for the handed-up frame is the device-DMA'd descriptor status word:
if_dc.c:2523total_len = DC_RXBYTES(rxstat);(if_dcreg.h:415-418:DC_RXSTAT_RXLEN 0x3FFF0000β a 14-bit field, 0..16383).- The only sanity gate is
if_dc.c:2548-2565, which reads error bits out of the same device-produced status word and explicitly toleratesDC_RXSTAT_GIANTwithout other error bits ("could be vlans") β i.e. the length is gated by the same producer that wrote it. if_dc.c:2568total_len -= ETHER_CRC_LEN;βtotal_lenisint;FRAMESZ < 4goes negative.if_dc.c:2581-2584(x86_64 quick path,dc_quick=1default,if_dc.c:317-318)m->m_pkthdr.len = m->m_len = total_len;β the freshly re-posted 2040-byte cluster is handed up with the trusted length.if_dc.c:2590-2591(copy path)m_devget(mtod(m, char *) - ETHER_ALIGN, total_len + ETHER_ALIGN, 0, ifp)β copies the trusted length out of the cluster.- No clamp exists anywhere on this path. A descriptor with RXERR
clear and
FRAMESZ = 16383yieldsm_len = 16379on a 2040-byte cluster β up to 14,339 bytes of heap over-read when the stack consumes the mbuf (BPF taps copy it to userspace; ether_input walks headers within the claimed length).FRAMESZ = 0..3yields a negativem_len(β4 GB unsigned) β immediate OOB walk. - The setup-frame RX path (
dc_pnic_rx_bug_war,if_dc.c:2402-2457) has its own unbounded-copy/length-trust issues already filed as the known DF-1249 β this finding is the mainlinedc_rxeoftrust, which every supported chip hits.
Ring math cross-checked so nothing else bounds it: the walk is
ring-bounded (DC_INC, β€ DC_RX_LIST_CNT iterations, each processed
descriptor re-posted with OWN), every path either advances i or
returns β the defect is purely the missing length-vs-cluster check.
2. Guest runtime verification (why inconclusive, not reproduced)
Audit guest: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC), virtio-net
only. Evidence in run.log:
- driver present in the running kernel:
kldstat -vβpci/if_dc,cardbus/if_dc;/boot/kernel/if_dc.koexists; - no supported PCI NIC:
ifconfig -lβvtnet0 lo0;pciconf -lvshows only QEMU i440fx/PIIX4 + virtio devices β the descriptor-DMA precondition cannot be produced on this guest; - the exact integer mechanics executed on the guest via
trigger.c(build.log): FRAMESZ 16383 βm_len=16379on the 2040-byte cluster (14,339 bytes OOB); FRAMESZ 2 βm_len=-2(unsigned ~4294967294). Compiled with the macros copied verbatim fromif_dcreg.h.
Threat model / preconditions
Requires a dc(4)-attached device that DMA-writes a descriptor with RXERR clear and a FRAMESZ field that exceeds the posted buffer (or < 4): buggy clone silicon (this driver binds ~15 cheap clone families), a compromised/attacker-controlled PCIe device (Thunderbolt/external-chassis DMA), or platform DMA corruption. Same trust boundary as DF-3604 (bce_rx_intr) β the dc(4) instance of that family.
Fix
fix.diff (git apply --check clean against the read-only tree): clamp
after total_len -= ETHER_CRC_LEN β drop frames with
total_len <= 0 || total_len > MCLBYTES - sizeof(u_int64_t), re-posting
the cluster (same treatment as the error path). Covers both the quick
path and the m_devget path, and re-covers the PNIC-war re-entry since
it re-enters above the clamp.
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 authored against the read-only sys/ tree (git apply --check clean). Cannot be driver-level validated on this guest (no dc hardware); the clamp is a static bounds check identical in shape to the error-path handling already in dc_rxeof.
findings/poc/DF-3849/fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
findings/poc/DF-3849/VERDICT.md; run.log (hardware-absence probe: ifconfig/pciconf/kldstat + trigger output); build.log (trigger built and executed on guest); fix.diff (clamp: drop frames with total_len<=0 || total_len > MCLBYTES-8, re-post cluster)
PoC changes
No seed PoC. Authored trigger.c (userspace arithmetic replica using the macros verbatim from if_dcreg.h) + hardware-absence probe, mirroring the DF-3604 (bce) family convention.
Verified recommended fix
Clamp total_len after the CRC subtraction: drop (ierrors++, re-post cluster) frames with total_len <= 0 or total_len > MCLBYTES - sizeof(u_int64_t).
Verdict
Source-level certain: dc_rxeof (sys/dev/netif/dc/if_dc.c:2500-2607) takes the received frame length solely from the NIC-DMA'd descriptor status word - total_len = DC_RXBYTES(rxstat) at :2523, a 14-bit field (DC_RXSTAT_RXLEN 0x3FFF0000, if_dcreg.h:415) - subtracts ETHER_CRC_LEN at :2568 and assigns it to the freshly re-posted mbuf at :2583 (quick path, dc_quick=1 default) or copies it out via m_devget at :2590, with NO bound against the posted cluster (2040 usable bytes: MCLBYTES=2048 minus the m_adj(8) in dc_newbuf, if_dc.c:2331). The only gate (:2548-2565) reads error bits from the same device-produced word and explicitly tolerates GIANT. A device-written descriptor with RXERR clear and FRAMESZ=16383 yields m_len=16379 on a 2040-byte cluster: up to 14339 bytes of heap over-read handed to the stack (BPF taps -> userspace info leak; header walks within the claimed length). FRAMESZ<4 wraps total_len negative (m_len ~4GB unsigned) -> stack OOB walk/panic. Precondition: malicious or misbehaving dc(4) device (buggy clone silicon among the ~15 families this driver binds, attacker-controlled PCIe/Thunderbolt DMA, or platform DMA corruption) - same trust boundary as DF-3604 (bce). Guest runtime: pci/if_dc present in kernel and /boot/kernel/if_dc.ko exists, but the audit guest has only virtio hardware (ifconfig: vtnet0 lo0; pciconf: QEMU i440fx + virtio only), so the descriptor-DMA precondition cannot be produced -> inconclusive per family convention. The exact integer mechanics were executed on the guest via trigger.c (build.log/run.log): FRAMESZ=16383 -> m_len=16379 (OOB READ 14339 bytes); FRAMESZ=2 -> m_len=-2 (unsigned ~4294967294).
No comments yet.