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

wb_start() ignores wb_encap() failure: failure paths leak the dequeued packet chain and leave a half-armed, unterminated OWN-marked descriptor chain for the NIC

Summary

wb_start calls wb_encap without checking the result. In the >16-fragment coalesce branch, both failure paths return without freeing m_head - the packet already dequeued. Per failure: (1) full mbuf-chain leak (leak spiral toward kernel memory exhaustion, unprivileged via >16-fragment TX chains under memory pressure); (2) fragments 0..k-1 already programmed with OWN but LASTFRAG never set - the NIC DMA-walks an unterminated chain via stale next words; (3) the txeof walk wedges until the 5s watchdog. Distinct from DF-1487. Check-and-clean fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4270 Β· 12 files
FileTypeDescriptionSize
dfwb3_rig.c β€” 40.9 KB view raw
Makefile β€” 105 B ↓ download
kld_run.sh β€” 2.8 KB view raw
kld_build.log β€” 768 B view raw
run.kld.log β€” 8.2 KB view raw
run.kld_fixed.log β€” 4.0 KB view raw
wb_start.disasm β€” 6.1 KB ↓ download
panic_rig_txeof_guard.txt β€” 750 B view raw
fix_validated.diff β€” 2.2 KB view raw
env.kld.txt β€” 491 B view raw
VERDICT.md β€” 5.0 KB ↓ raw
verdict.json β€” 5.4 KB view raw
VERDICT.md
↓ download raw

VERDICT β€” DF-4270 (wb_start ignores wb_encap failure, if_wb.c:1327)

Status: reproduced (deterministic, production code) β€” fix validated: fixed.

Method

DF-4266 KLD dispatch rig: resolve the running kernel's real static wb_start (0xffffffff808d9e90; wb_encap is inlined into it), fabricate the proven post-attach softc, enqueue a crafted packet on the production classic ifq, dispatch. The failure precondition the finding requires ("M_NOWAIT allocations fail under mbuf pressure") is forced exactly: drain the cluster pool (MGETHDR+MCLGET until MCLGET fails β€” 34,655 clusters) so the coalesce branch's MCLGET(m_new, M_NOWAIT) fails while MGETHDR still succeeds. The crafted chain (1 pkthdr + 16 data mbufs, 128 B each, pkthdr.len 2176) is precisely the >16-fragment shape that enters the coalesce branch and requires a cluster.

That the :1255-1258 path was taken is proven three ways: (a) mbstat.m_drops +1 attributable to m_mclget (uipc_mbuf.c:1249) at dispatch time; (b) the post-dispatch probe "MGETHDR ok; MCLGET FAILS" (the packet pool is fine, only clusters are dry); (c) the descriptor state is the failure state (wb_mbuf NULL β€” :1283 never ran; frag[0] ctl still carries the frag-loop's m_len=128, not the coalesce rewrite of 2176; LASTFRAG nowhere).

Baseline β€” stock kernel #0 (run.kld.log)

claim observation
packet chain leaked mbtypes[MT_DATA] accounting: exactly +17 outstanding vs the chain-free expectation (self-tested Β±1-accurate); queue len 0; chain[0].wb_mbuf == NULL; mbuf delta across wb_start = 0 (driver freed nothing); chain walk: 17 mbufs, links + markers intact (never m_freem'd β€” m_free NULLs m_next); *** DF-4270 LEAK CONFIRMED ***; and after the REAL wb_watchdog (wb_stop+wb_reset+wb_init): "leaked mbufs STILL leaked: YES"
half-armed descriptor armed for the NIC frag[0].status=0x80000000 (OWN, set :1354), ctl=0xa1000080 (FINT|FIRSTFRAG|TLINK|128), frags 1..15 OWN (:1232), LASTFRAG 0/16, frag[15].wb_next == vtophys(&frag[16]) (one-past), WB_TXSTART=0xffffffff (:1355), if_timer=5
TX completion wedges REAL wb_txeof.part.4 dispatched with a chip-completed head frag: opackets +0, wb_tx_head unchanged, :1062 walk never entered β€” *** DF-4270 WEDGE CONFIRMED ***

The compiled wb_start disassembly (wb_start.disasm) confirms the binary is faithful to the source here (unlike DF-4268's case): MGETHDR-fail jumps straight back to the caller flow without freeing anything; MCLGET-fail calls m_freem(m_new) only; m_freem(m_head) exists solely on the success path after m_copydata. So the leak is real in the shipped binary.

Impact: per-failure leak of a full packet chain (leak spiral toward kernel memory exhaustion β€” the same pressure that causes the failures worsens them), an OWN-marked unterminated fragment chain started on the NIC via TXSTART (on real hardware the chip DMA-walks stale wb_next words past the one-past frag[16]), and TX wedged until the 5 s watchdog. Trigger for an unprivileged local user: ordinary TX traffic shaped into >16-fragment chains under cluster pressure (16 small writes per packet is enough shape; the pressure can be the system's own).

Fix validation β€” kernel #1 (fix_validated.diff, combined 3-fix build)

go4270 re-run on the patched kernel (run.kld_fixed.log):

observable stock #0 fixed #1
mbuf delta across wb_start 0 (leak +17) βˆ’17 (fix frees the chain)
LEAK accounting 17 0
frags with OWN set 16/16 0/16 (statuses zeroed)
WB_TXSTART 0xffffffff 0 (never issued)
wb_tx_head / wedge set, wedged NULL β€” nothing armed; no wedge possible
if_timer 5 0

Accounting closes exactly: final mbtypes = baseline βˆ’ 17 (chain freed by the fix) βˆ’ 64 (RX ring freed by wb_stop).

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

Rig incidents (documented, not the finding)

  1. First baseline run's leak arithmetic under-counted by exactly the chain (baseline read AFTER chain build); the underlying data was correct and the corrected formula prints the LEAK CONFIRMED marker. No kernel misbehaviour was involved; v1 cleanup freed the genuinely-leaked chain (no double-free β€” confirmed by exact final accounting).
  2. First FIXED-kernel run: the rig dispatched wb_txeof.part.4 unguarded; on the fixed kernel wb_tx_head is NULL and the .part.4 clone's NULL guard lives in the CALLER (wb_intr checks before each call β€” visible in its disassembly), so the rig faulted AFTER all fixed-kernel evidence had printed (panic_rig_txeof_guard.txt). Guard added (mirroring wb_intr); the clean re-run SURVIVED.

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

fixed: on the patched kernel the failure path frees the chain (mbuf delta -17, LEAK 0), disarms all 16 fragment statuses, never writes TXSTART, leaves wb_tx_head NULL (no wedge), and if_timer 0; accounting closes exactly (baseline - 17 chain - 64 rx-ring)

run.kld_fixed.log; fix_validated.diff; VERDICT.md 'Fix validation' table
↓ fix.diffper-fix-DF-4270

Confirmed kernel references

Detail

Exploit chain

unprivileged local user shapes TX into >16-fragment chains (e.g. many small writes per packet) and applies ordinary cluster pressure (the system's own traffic suffices) -> per-failure leak of a full packet chain (leak spiral toward kernel memory exhaustion) + NIC started on an unterminated OWN-marked fragment chain + TX wedged until the 5s watchdog

Evidence (decisive lines)

run.kld.log ('cluster pool exhausted after 34655 clusters', 'wb_start mbuf delta: 0', 'post-dispatch probe: MGETHDR ok; MCLGET FAILS', '*** DF-4270 LEAK CONFIRMED: exactly the 17 mbufs ... ***', 'frags with OWN set: 16/16; frags with LASTFRAG set: 0/16', 'MMIO WB_TXSTART=0xffffffff', '*** DF-4270 WEDGE CONFIRMED ***', 'after watchdog ... leaked mbufs STILL leaked: YES'); run.kld_fixed.log ('wb_start mbuf delta: -17', 'LEAK 0', 'frags with OWN set: 0/16', 'MMIO WB_TXSTART=0000000000', 'wb_tx_head == NULL ... FIXED behaviour'); wb_start.disasm (failure paths faithful to source; m_freem(m_head) only on success)

PoC changes

Replaced the seed's 'no hardware -> analysis pack' with a deterministic kernel-level reproduction: forced the exact MCLGET-failure precondition by draining mclmeta via MGETHDR+MCLGET (~34.6k clusters) inside the rig, enqueued the crafted 17-mbuf chain on the production classic ifq, dispatched the REAL wb_start, and observed leak (mbtypes accounting via nm-resolved statics), descriptor state, TXSTART MMIO, the REAL wb_txeof.part.4 wedge, and the REAL wb_watchdog path; added the fixed-kernel A/B. Rig incidents: first-run leak arithmetic under-counted by the chain-in-baseline (corrected; no kernel misbehaviour); unguarded txeof.part.4 dispatch panicked on the FIXED kernel after evidence capture (guard added mirroring wb_intr's caller-side NULL check, panic_rig_txeof_guard.txt).

Verified recommended fix

Check wb_encap()'s return in wb_start; on failure free the dequeued m_head, zero the partially programmed fragment statuses, restore wb_tx_free, and bail β€” validated as fix_validated.diff hunk 4 (the seed's fix, applied verbatim)

Verdict

Reproduced deterministically on production code: with the cluster pool drained (MGETHDR+MCLGET until MCLGET fails) so that the inlined wb_encap inside the REAL wb_start takes the :1255-1258 MCLGET-failure path on a 17-mbuf (>16-fragment, len 2176 > MHLEN) chain, the ignored return at if_wb.c:1327 leaves: (1) the dequeued packet chain leaked β€” mbtypes[MT_DATA] accounting (method self-tested) shows exactly +17 mbufs allocated and referenced by nothing (queue empty, chain[0].wb_mbuf NULL, wb_start freed nothing, chain links/markers intact), and the leak persists across the REAL wb_watchdog recovery; (2) the half-programmed descriptor ARMED for the NIC β€” frag[0] OWN-set (:1354), frags 1..15 OWN (:1232), LASTFRAG on 0/16 fragments, frag[15].wb_next one-past-the-array, and WB_TXSTART=0xFFFFFFFF written (:1355): the chip is started on an unterminated chain over stale next words; (3) TX completion wedged β€” the REAL wb_txeof.part.4 with a chip-completed head descriptor advances nothing (:1062 walk never enters), only the 5s watchdog recovers. Fix validated on the combined-fix kernel: wb_start mbuf delta -17 (chain freed), LEAK 0, fragments disarmed (0/16 OWN), TXSTART never issued, wb_tx_head NULL (no wedge), if_timer 0 β€” every defect marker gone, accounting closes exactly.