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)
PoC verification
Evidence pack
findings/poc/DF-4270 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 β 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)
- 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).
- First FIXED-kernel run: the rig dispatched
wb_txeof.part.4unguarded; on the fixed kernelwb_tx_headis NULL and the.part.4clone's NULL guard lives in the CALLER (wb_intrchecks 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
fixedfixed: 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
Confirmed kernel references
- sys/dev/netif/wb/if_wb.c:1327
- sys/dev/netif/wb/if_wb.c:1248
- sys/dev/netif/wb/if_wb.c:1251
- sys/dev/netif/wb/if_wb.c:1253
- sys/dev/netif/wb/if_wb.c:1255
- sys/dev/netif/wb/if_wb.c:1258
- sys/dev/netif/wb/if_wb.c:1283
- sys/dev/netif/wb/if_wb.c:1354
- sys/dev/netif/wb/if_wb.c:1355
- sys/dev/netif/wb/if_wb.c:1062
- sys/kern/uipc_mbuf.c:1249
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.
No comments yet.