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)
PoC verification
Evidence pack
findings/poc/DF-4056 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 β 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 isET_RX_NSTAT= 1024 entries (if_etvar.h:45). Values 1024..4095 can never equalrsr_index.ET_TX_DONE_POS_INDEX=__BITS(9,0)β 10-bit device field (if_etreg.h:139); the TX ring isET_TX_NDESC= 512 (if_etvar.h:42). Values 512..1023 can never equaltbd_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_newbufreallocations, mbuf frees and CSR writes while the interface serializer stays held; - serializer capture proven: a second thread's
lwkt_serialize_enteron the same serializer blocked forever (kern.dfet.grabnever returned; zeroACQUIREDlines) β in production every later ioctl/if_start on the interface blocks; - one CPU pegged, caller unkillable,
ps -axitself 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);
psreplaced 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
fixedKernel #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
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.
No comments yet.