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

et_rxeof/et_txeof completion walks have no termination bound, and the device-supplied index fields are WIDER than the software rings (RX stat 12-bit vs ET_RX_NSTAT=1024; TX done 10-bit vs ET_TX_NDESC=512): a value in the unreachable range spins forever under the driver serializer / KKASSERT-panics (DF-3761 family instance)

Summary

The loop exit predicates compare unvalidated device-DMA indices against narrower software ring counters: RX STATRING_INDEX is 12-bit vs a 1024-entry ring - any value in 1024..4095 makes equality unreachable, spinning forever holding if_serializer while spraying stale buffers into the stack. TX DONE_INDEX is 10-bit vs a 512-ring: index 512..1023 never matches, tbd_used hits 0, KKASSERT panics (or negative-forever with asserts off). One-register-write trigger once the device lies. Index-validation + loop-bound fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4056 Β· 18 files
FileTypeDescriptionSize
dfet_poc.c β€” 32.0 KB view raw
Makefile β€” 103 B ↓ download
build.sh β€” 91 B view raw
run.sh β€” 203 B view raw
run_tx.sh β€” 239 B view raw
run_rx.sh β€” 368 B view raw
s3_evidence.sh β€” 467 B view raw
panic_tx.txt β€” 1.1 KB view raw
run_rx_livelock.log β€” 2.0 KB view raw
fix.diff β€” 3.6 KB view raw
fix_build.log β€” 1.1 KB view raw
fix_run_s2.log β€” 1.4 KB view raw
fix_run_s2_dmesg.txt β€” 498 B view raw
fix_run_s3.log β€” 912 B view raw
VERDICT.md β€” 3.9 KB ↓ raw
env.txt β€” 1.3 KB view raw
verdict.json β€” 4.4 KB view raw
manifest.json β€” 1.0 KB view raw
VERDICT.md
↓ download raw

VERDICT β€” DF-4056 (unbounded rxeof/txeof walks; device indices wider than the SW rings)

Status: reproduced β€” both halves (production-handler KLD dispatch, DF-4106 vge / DF-4266 wb evidence class) β€” and fix validated on a rebuilt kernel.

Geometry (headers, confirmed in the running kernel's disassembly)

  • ET_RXS_STATRING_INDEX = __BITS(27,16) β€” 12-bit device field (if_etvar.h:107); the software stat ring is ET_RX_NSTAT = 1024 entries (if_etvar.h:45). Values 1024..4095 can never equal rsr_index.
  • ET_TX_DONE_POS_INDEX = __BITS(9,0) β€” 10-bit device field (if_etreg.h:139); the TX ring is ET_TX_NDESC = 512 (if_etvar.h:42). Values 512..1023 can never equal tbd_start_index.

TX half β€” KKASSERT panic (panic_tx.txt)

Dispatched the real et_txeof(sc, 0) (the et_start:1223 call shape; symbol t et_txeof) with tbd_used=1 and the MMIO stand-in's ET_TX_DONE_POS = index 600, wrap 0:

panic: assertion "tbd->tbd_used > 0" failed in et_txeof at
/usr/src/sys/dev/netif/et/if_et.c:2050
et_txeof() at et_txeof+0x1cb 0xffffffff804b690b

Exactly as predicted: the loop predicate at :2029 can never be satisfied, the walk decrements tbd_used past zero, the KKASSERT at :2050 fires. Guest died at the ddb prompt.

RX half β€” livelock under the serializer (run_rx_livelock.log)

Dispatched the real et_intr (inlined et_rxeof) under the rig's serializer with rxs_stat_ring = STATRING index 2000:

  • the completion walk (if_et.c:1851) never terminates: ierrors 4,230,460,413 β†’ 4,253,528,605 in ~2 s (~11.5M iterations/s), still growing linearly 9 minutes later (6.6e9+ β†’ +11.7M/s) β€” the walk sprays rbd_newbuf reallocations, mbuf frees and CSR writes while the interface serializer stays held;
  • serializer capture proven: a second thread's lwkt_serialize_enter on the same serializer blocked forever (kern.dfet.grab never returned; zero ACQUIRED lines) β€” in production every later ioctl/if_start on the interface blocks;
  • one CPU pegged, caller unkillable, ps -ax itself hangs, and the guest could not be shut down cleanly afterwards (host force-killed QEMU) β€” same shutdown wedge as the DF-3715/4025/4046/4051 family.

Rig iteration notes

  • The first S1 run (DF-4055 pack) also panicked at m_free β€” sink adjusted to bound the demonstration; unrelated to this finding.
  • S3 launched via a backgrounded sysctl; observation from a second session (stats/grab sysctls); ps replaced by sysctl probes after it proved to hang while the spinner runs.

Fix validation (kernel #1, Oct 14 2026, same tree + fix.diff)

fix.diff validates both device indices against the ring geometry before the walks (rxst_index >= ET_RX_NSTAT β†’ ierrors + return; end >= ET_TX_NDESC β†’ oerrors + return). Identical rig on the rebuilt kernel:

scenario baseline (#0) fixed (#1)
TX: DONE index 600 panic "tbd_used > 0" (if_et.c:2050) dfet0: invalid TX done index 600, et_txeof RETURNED (tbd_used=1), no panic
RX: STATRING index 2000 livelock, ~11.5M iter/s forever, serializer held dfet0: invalid RX stat ring index 2000, et_intr RETURNED (ierrors=4), no livelock

fix_status: fixed (baseline_reproduced=1, patched_reproduced=0).

Impact

One device register/DMA write by a faulty or hostile NIC, then every TX interrupt panics the kernel (stock KKASSERT) or every RX interrupt permanently livelocks a CPU with the interface serializer held. Both observed deterministically. Severity Medium stands (hardware-mediated trigger).

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

Kernel #1 (same tree + fix.diff) A/B with the identical rig: TX - baseline panic 'tbd_used > 0' at if_et.c:2050 vs patched 'invalid TX done index 600' + et_txeof returned (tbd_used=1, no panic); RX - baseline 11.5M-iter/s unbounded livelock vs patched 'invalid RX stat ring index 2000' + et_intr returned (ierrors flat). Defective behaviors gone.

findings/poc/DF-4056/fix.diff; fix_run_s2.log; fix_run_s2_dmesg.txt; fix_run_s3.log; fix_build.log
↓ fix.diffper-fix-DF-4056

Confirmed kernel references

Detail

Exploit chain

Device/DMA attacker writes one status word: TX -> deterministic KKASSERT panic on the next TX interrupt; RX -> permanent livelock of one CPU holding the interface serializer (every later ioctl/if_start blocks) plus that context's callout starvation. DoS class; no memory-corruption primitive.

Evidence (decisive lines)

panic_tx.txt (KKASSERT panic backtrace); run_rx_livelock.log (ierrors growth ~11.5M/s across two sessions, grab-probe serializer capture, ps hang, forced shutdown); fix_run_s2.log + fix_run_s2_dmesg.txt + fix_run_s3.log (patched kernel: both walks return with validation messages, no panic/livelock); VERDICT.md; env.txt

PoC changes

Authored the production-dispatch rig fresh (pack was 'untested/hardware-gated'). et_rxeof has no standalone symbol - RX half dispatched through the real et_intr with ET_INTR_STATUS=ET_INTR_RXEOF. S3 observation via sysctl probes (kern.dfet.stats / kern.dfet.grab) after ps -ax proved to hang against the spinner.

Verified recommended fix

Validate device-supplied indices against ring geometry before the completion walks: reject rxst_index >= ET_RX_NSTAT and TX DONE end >= ET_TX_NDESC (error counter + return); optionally add loop work budgets

Verdict

REPRODUCED, BOTH HALVES, via production-handler KLD dispatch (DF-4106 vge / DF-4266 wb class). TX half: dispatched the real et_txeof(sc,0) (et_start:1223 call shape) with ET_TX_DONE_POS index 600 - representable by the 10-bit register field (if_etreg.h:139) but unreachable by tbd_start_index which wraps at ET_TX_NDESC=512 - with tbd_used=1: the walk (if_et.c:2029) decrements tbd_used past zero and the kernel panicked exactly as predicted: panic: assertion 'tbd->tbd_used > 0' failed in et_txeof at if_et.c:2050, backtrace et_txeof+0x1cb (panic_tx.txt). RX half: dispatched the real et_intr (inlined et_rxeof) with rxs_stat_ring index 2000 - representable by the 12-bit field (if_etvar.h:107) but unreachable by rsr_index which wraps at ET_RX_NSTAT=1024: the completion walk (if_et.c:1851) never terminates, spinning ~11.5 MILLION iterations/s under the interface serializer (ierrors 4.23e9 -> 4.25e9 in ~2s, still linear 9 min later), serializer capture proven (second thread's lwkt_serialize_enter blocked forever), caller unkillable, ps -ax itself hangs, guest unshutdownable afterwards. FIX VALIDATED: kernel #1 rebuilt with fix.diff (index-vs-ring-geometry validation before both walks): identical rig - TX: 'invalid TX done index 600', et_txeof RETURNED, no panic; RX: 'invalid RX stat ring index 2000', et_intr RETURNED, no livelock.