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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3849 Β· 11 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download raw

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_newbuf if_dc.c:2321-2324 m_new = m_getcl(M_NOWAIT, MT_DATA, M_PKTHDR); m_new->m_len = m_new->m_pkthdr.len = MCLBYTES (= 2048), then if_dc.c:2331 m_adj(m_new, sizeof(u_int64_t)) β‡’ 2040 usable bytes, posted to the descriptor with c->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:2523 total_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 tolerates DC_RXSTAT_GIANT without other error bits ("could be vlans") β€” i.e. the length is gated by the same producer that wrote it.
  • if_dc.c:2568 total_len -= ETHER_CRC_LEN; β€” total_len is int; FRAMESZ < 4 goes negative.
  • if_dc.c:2581-2584 (x86_64 quick path, dc_quick=1 default, 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 = 16383 yields m_len = 16379 on 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..3 yields a negative m_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 mainline dc_rxeof trust, 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.ko exists;
  • no supported PCI NIC: ifconfig -l β†’ vtnet0 lo0; pciconf -lv shows 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=16379 on the 2040-byte cluster (14,339 bytes OOB); FRAMESZ 2 β†’ m_len=-2 (unsigned ~4294967294). Compiled with the macros copied verbatim from if_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_testable
baseline no→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-3849

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