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

oce_rx builds mbuf chain with m_pkthdr.len from the CQE 14-bit pkt_size unclamped to num_fragments x frag_size: chain up to 2047 bytes shorter than claimed, handed to if_input β€” stack offset-walkers (m_copydata KASSERT) panic, TCP desynchronizes

Summary

oce_rx takes pkt_size (14-bit) verbatim, loops num_fragments (3-bit, max 7) posting min(len,2048) per cluster, then sets the first mbuf's m_pkthdr.len to the full pkt_size. 7x2048=14336 < 16383 max pkt_size, so a firmware-shaped CQE (pkt_size=16383, num_fragments=1) delivers a chain claiming up to 2047 bytes more than it contains; m_copydata/m_copym consumers KASSERT-panic and tcp_input's tif_len desyncs. Distinct from DF-1371 (needs no RQ-array inconsistency). Residual-check drop fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4079 Β· 16 files
FileTypeDescriptionSize
README.md β€” 3.3 KB ↓ raw
VERDICT.md β€” 4.2 KB ↓ raw
df4079_oce_poc.c β€” 13.6 KB view raw
Makefile β€” 163 B ↓ download
build.sh β€” 179 B view raw
run.sh β€” 206 B view raw
build.log β€” 848 B view raw
run.log β€” 1.5 KB view raw
panic.txt β€” 643 B view raw
panic_console_full.log β€” 2.6 KB view raw
fix.diff β€” 727 B view raw
fix_build.log β€” 5.8 MB ↓ download
fix_run.log β€” 2.9 KB view raw
env.txt β€” 423 B view raw
manifest.json β€” 1.7 KB view raw
verdict.json β€” 4.9 KB view raw

DF-4079 β€” oce_rx: CQE pkt_size vs num_fragmentsΓ—frag_size desync builds an

inconsistent mbuf chain (m_pkthdr.len > Ξ£ m_len) handed to the network stack

File: sys/dev/netif/oce/oce_if.c (pass 2) Class: memory-safety contract violation β†’ stack offset-walker panic (m_copydata KASSERT) / TCP sequence desync VERIFIED: reproduced (baseline + panic) on the DFly QEMU guest via the production-handler KLD dispatch rig; fix validated on a rebuilt kernel.

How it was verified without OneConnect hardware

The guest kernel is built with device oce (X86_64_GENERIC), so the driver's production code β€” including the static oce_rx() β€” lives in the RUNNING kernel image. The KLD rig (df4079_oce_poc.c):

  • references the kernel's own oce_rq_handler(), oce_alloc_rx_bufs(), oce_create_ring_buffer(), oce_destroy_ring_buffer() and oce_free_posted_rxbuf() as extern symbols (kernel-loader bound β€” nothing recompiled);
  • fabricates the post-attach softc exactly as production leaves it (BE3 flags, frag_size 2048, verbatim oce_rq_init DMA replay, real busdma rings from the kernel's own ring allocator, MMIO BAR stand-in pages);
  • posts real RX clusters via the REAL oce_alloc_rx_bufs();
  • crafts the CQE bitfields the way firmware DMA-writes them: pkt_size=16383, num_fragments=1 and pkt_size=16383, num_fragments=7;
  • dispatches the REAL oce_rq_handler(rq) (static oce_rx runs inside);
  • captures the delivered chain in an if_input shim and prints claimed (m_pkthdr.len) vs actual (Ξ£ m_len).

Results (stock kernel #0 Thu Jul 2 06:02:54 UTC 2026)

  • case 1 (pkt_size=16383, nf=1): chain 2048 bytes, claims 16383 β†’ 14335 phantom bytes
  • case 2 (pkt_size=16383, nf=7): chain 7Γ—2048=14336 bytes, claims 16383 β†’ 2047 phantom bytes (the finding's headline gap)
  • panic mode: the real stack offset-walker m_copydata(m, off=2056, 8) β€” the exact consumer cited (sys/kern/uipc_mbuf.c:1679, kernel built with INVARIANTS) β€” panics: panic: m_copydata: offset > size of mbuf chain with backtrace m_copydata() ← df4079_input() ← oce_rx.isra.10() ← oce_rq_handler() β€” production oce_rx built the chain, production m_copydata died on it.

Fix validation (fix.diff, fix_build.log, fix_run.log)

fix.diff adds the residual-length check to oce_rx: if len != 0 after the fragment loop, the chain is freed and dropped. Applied to the guest's /usr/src (identical to the audit tree), kernel rebuilt (make -j6 nativekernel KERNCONF=X86_64_GENERIC) and installed, booted into #1: Thu Oct 15 13:06:11 UTC 2026. The SAME rig re-run against the patched kernel's own oce_rq_handler (address re-resolved at module load): both crafted CQEs dropped β€” chains delivered to if_input: 0; invariant violations: 0; rx_pkts=0 β†’ FIX CONFIRMED; panic mode on the patched kernel SURVIVES (no chain β†’ no m_copydata dispatch β†’ no panic).

Reproduce

vm.sh run_root 'sh /root/df4079/build.sh'    # build the KLD
vm.sh run_root 'sh /root/df4079/run.sh baseline'   # survives, prints violations
vm.sh reset with-src                          # fresh boot for the panic run
vm.sh run_root 'sh /root/df4079/run.sh panic'      # kernel panic

Threat model unchanged from the finding: pkt_size/num_fragments are firmware/DMA-controlled; the driver must not trust the completion.

VERDICT.md
↓ download raw

DF-4079 VERDICT β€” oce_rx pkt_size/fragment-count desync

Status: REPRODUCED (baseline invariant-violation + m_copydata panic; fix validated on rebuilt kernel)

Guest: DragonFly dfbsd 6.5-DEVELOPMENT #0 (stock) and #1 (patched), x86_64. Method: production-handler KLD dispatch rig (the DF-4106/DF-4266/if_et evidence class) β€” no OneConnect hardware needed because device oce is compiled into the guest's X86_64_GENERIC kernel.

What ran (all production code, kernel-loader bound)

  1. Rig externs: oce_rq_handler, oce_alloc_rx_bufs, oce_create_ring_buffer, oce_destroy_ring_buffer, oce_free_posted_rxbuf β€” resolved against the running kernel's symbol table at kldload (verified: dispatched handler address 0xffffffff805c9e50 = nm(1) T oce_rq_handler).
  2. Fabricated post-attach state: BE3 softc (flags, ring sizes, rq_frag_size=2048 per oce_if.c:236), verbatim oce_rq_init DMA replay (oce_queue.c:429-455: same tag params, 1024 dmamaps, real ring from the kernel's own oce_create_ring_buffer), RX CQ ring (256Γ—16B), real ifnet with an if_input inspection shim, MMIO BAR stand-in pages (register offsets all < 0x140, oce_hw.h:64-88).
  3. Posted 16 real clusters via REAL oce_alloc_rx_bufs (m_getcl + bus_dmamap_load_mbuf_segment + RQEs + doorbell to the stand-in page).
  4. Crafted CQEs as firmware would DMA them (DW_SWAP is a no-op on LE, oce_if.h:1114): case 1 pkt_size=16383, num_fragments=1, valid=1; case 2 pkt_size=16383, num_fragments=7, valid=1.
  5. Dispatched REAL oce_rq_handler(rq) β†’ static oce_rx (confirmed in the panic backtrace as oce_rx.isra.10) β†’ sc->ifp->if_input(...).

Observed (stock kernel, run.log / panic.txt)

  • case 1: delivered chain [m_len=2048] with m_pkthdr.len=16383 β†’ 14335 phantom bytes.
  • case 2: delivered chain of 7Γ—2048=14336 bytes with m_pkthdr.len=16383 β†’ 2047 phantom bytes (max representable chain data 7Γ—2048=14336 < 16383=max 14-bit pkt_size: the trust-boundary gap).
  • rq->rx_stats.rx_bytes += 32766 β€” stats count phantom bytes (oce_if.c:1381).
  • Deterministic: baseline run twice, identical results.
  • panic mode (panic.txt): panic: m_copydata: offset > size of mbuf chain β€” the exact cited KASSERT (sys/kern/uipc_mbuf.c:1679-1680, INVARIANTS kernel), backtrace m_copydata() ← df4079_input() ← oce_rx.isra.10() ← oce_rq_handler(). The rig's m_copydata call models any offset-walking stack consumer (forwarding/fragmentation/ICMP-error paths) that trusts pkthdr.len.

Impact ceiling on this guest

Kernel panic (DoS) demonstrated. Escalation beyond DoS would require the frame bytes (also firmware-controlled) to steer a stack consumer into an OOB copy β€” the primitive is the 2047-byte phantom region the stack will read past the chain's real end; on this guest the walk hits the KASSERT first. Threat model: buggy/malicious NIC firmware or device DMA; not remotely reachable without the NIC writing the CQE.

Fix validation (fix.diff / fix_build.log / fix_run.log)

fix.diff = residual-length check in oce_rx (drop when len != 0 after the fragment loop). Applied to guest /usr/src (md5-identical to audit tree before patch), make -j6 nativekernel KERNCONF=X86_64_GENERIC + installkernel (full 41592-line log: fix_build.log), rebooted into #1: Thu Oct 15 13:06:11 UTC 2026.

  • baseline on patched kernel: both crafted CQEs dropped β€” chains delivered to if_input: 0; invariant violations: 0; rx_pkts=0; rx_bytes=0 β†’ FIX CONFIRMED (handler address moved to 0xffffffff805c9e80 = the rebuilt kernel's own oce_rq_handler).
  • panic mode on patched kernel: SURVIVED (no chain ever reaches if_input, so the m_copydata dispatch never happens).

Distinct from DF-1371 (pckts[] array over-walk): DF-4079 needs no RQ-array inconsistency β€” the RQ walk here is perfectly legal; only the chain length contract is violated.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff (residual-length drop in oce_rx) applied to guest /usr/src, kernel rebuilt via make -j6 nativekernel KERNCONF=X86_64_GENERIC (full log fix_build.log) and rebooted (#1 Thu Oct 15 13:06:11 UTC 2026); identical rig re-dispatched against the patched kernel's own oce_rq_handler: both crafted CQEs dropped (chains delivered=0, violations=0, rx_pkts=0, rx_bytes=0, FIX CONFIRMED) and panic mode SURVIVES. Every defective behavior gone.

fix_run.log (FIX CONFIRMED markers on kernel #1); fix_build.log (BUILD_DONE_OK, kernel install completed 13:11:22); panic.txt vs fix survival (no second panic.txt exists β€” the patched panic-mode run completed with GO_RC=0).
↓ fix.diffper-fix-DF-4079

Confirmed kernel references

Detail

Exploit chain

firmware/DMA-shaped RX CQE (pkt_size=16383, num_fragments=1..7) -> production oce_rq_handler/oce_rx builds a legal-RQ mbuf chain whose m_pkthdr.len exceeds its data by up to 2047 bytes -> if_input -> any offset-walking consumer (m_copydata/m_copym: forwarding, fragmentation, ICMP-error) reads past the chain's real end -> KASSERT panic (demonstrated); with frame bytes also firmware-controlled, silent OOB reads of adjacent cluster data / TCP sequence desync are the same primitive short of the assert.

Evidence (decisive lines)

run.log (stock baseline: both CHAIN INVARIANT VIOLATION markers with per-mbuf m_len dumps); panic.txt (panic: m_copydata: offset > size of mbuf chain + backtrace oce_rx.isra.10 <- oce_rq_handler); fix_run.log (patched kernel: chains=0 violations=0 FIX CONFIRMED); fix_build.log (full 41592-line nativekernel build, BUILD_DONE_OK); panic_console_full.log (full serial console of the panic run)

PoC changes

Seed pack had no PoC (marked hardware-gated/untested). Replaced with the production-handler KLD dispatch rig: extern-binds the kernel's own oce_rq_handler + posting/ring helpers, fabricates the post-attach softc (verbatim oce_rq_init DMA replay), posts real clusters via REAL oce_alloc_rx_bufs, crafts CQEs in DMA memory, dispatches the real handler; if_input shim proves pkthdr.len > sum(m_len); panic mode dispatches the real m_copydata.

Verified recommended fix

In oce_rx, after the fragment loop, drop the chain when residual len != 0 (pkt_size exceeded num_fragments x frag_size); never set m_pkthdr.len above the chain's real data.

Verdict

REPRODUCED via the production-handler KLD dispatch rig (DF-4106/DF-4266 evidence class): the guest kernel ships device oce, so the rig binds the kernel's OWN oce_rq_handler()/oce_alloc_rx_bufs()/ring-allocator as externs over a fabricated post-attach softc (BE3 flags, verbatim oce_rq_init DMA replay, real busdma rings, real posted clusters, MMIO BAR stand-ins) and dispatches it on firmware-shaped CQEs. Stock kernel: pkt_size=16383/num_fragments=1 delivered a 2048-byte chain claiming 16383 (14335 phantom bytes); pkt_size=16383/num_fragments=7 delivered 14336 bytes claiming 16383 (2047 phantom bytes β€” the max 7x2048<16383 trust-boundary gap); rx_bytes counted the phantom length. Panic mode: the cited stack offset-walker m_copydata (sys/kern/uipc_mbuf.c:1679 KASSERT, INVARIANTS kernel) panicked with backtrace m_copydata() <- df4079_input() <- oce_rx.isra.10() <- oce_rq_handler() β€” production oce_rx built the chain, production m_copydata died on it. Fix (residual-length drop in oce_rx) validated on a rebuilt+rebooted kernel: both crafted CQEs dropped (chains=0, violations=0, rx_pkts=0, FIX CONFIRMED) and the panic mode survives. Threat model unchanged: firmware/DMA-controlled CQE; not remotely reachable without the NIC.