ng_ID_rehash/ng_name_rehash migrate hash entries only under #ifdef INVARIANTS (double-insert on production/module builds) and ng_ID_rehash destroys the old table with the WRONG mask -> 'hashdestroy: hash not empty' kernel panic (list-corruption UAF) β root cause of DF-3271's fix_failed mystery
Summary
netgraph7's hash-growth paths unlink nodes from the old table only when compiled with INVARIANTS. Production kernels - and ANY standalone KLD module build (which does not inherit the kernel's INVARIANTS flag) - leave every entry double-inserted; hashdestroy on the still-populated old table panics. ng_ID_rehash additionally passes ng_name_hmask to hashdestroy instead of ng_ID_hmask (OOB walk). REPRODUCED 4x on the guest (3x ng_make_node_common, 1x ng_name_node after the ID half was fixed); one run also hit slab-heap corruption (chunk_mark_allocated assertion) consistent with the double-insert UAF. Node-count threshold permanently crossed by the DF-3513/DF-3515 node-reference leaks, converting routine traffic into a reliable panic. THIS resolves the mystery behind DF-3271's honest fix_failed verdict: the earlier one-line mask fix was correct but insufficient - the #ifdef INVARIANTS double-insert was the deeper cause. Fix validated (unconditional LIST_REMOVE in both migration loops + correct mask, module-level, 20/20 survival).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3514 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.5 KB | β raw | |
| VERDICT.md | β | 1.7 KB | β raw | |
| panic.txt | β | 1.4 KB | view raw | |
| fix.diff | β | 1.6 KB | view raw | |
| manifest.json | β | 831 B | view raw | |
| verdict.json | β | 2.0 KB | view raw |
DF-3514 β ng_ID_rehash/ng_name_rehash: entries not unlinked (non-INVARIANTS) + wrong destroy mask -> kernel panic
What
sys/netgraph7/netgraph/ng_base.c:
ng_ID_rehash()(lines 973-999): the migration loop unlinks nodes from the old ID-hash only under#ifdef INVARIANTS(lines 988-990). On production (non-INVARIANTS) kernels β and in any standalone KLD module build, which does not inherit the kernel's INVARIANTS flag β every node stays linked into the OLD table and is additionally inserted into the new one.hashdestroy()on the still-populated old table then panics ("hashdestroy: hash not empty"). Independently, line 996 passes the WRONG mask:hashdestroy(ng_ID_hash, M_NETGRAPH_NODE, ng_name_hmask)β the name-hash mask instead ofng_ID_hmask; if the name hash is smaller than the ID hash the walk under-runs, if larger it walks OOB past the old table.ng_name_rehash()(lines 941-967): same INVARIANTS-onlyLIST_REMOVE(lines 956-958); destroy mask is correct here but the un-migrated old table still triggers the same panic.
Double-insertion is also a list-corruption UAF: when such a node is later freed via one list, the other list keeps a dangling pointer.
Reachability
Rehash fires when ng_nodes * 2 > ng_ID_hmask (8 live/leaked nodes for the
initial 16-bucket table) at ng_make_node_common() (line 657-658), and when
named nodes cross the equivalent threshold in ng_name_node(). Node counts
inflate permanently via the reference leaks in DF-3513 (drained conq items
pin their el_dest node) and DF-3515 (callout items pin their node while
armed/stopped) β so routine bluetooth traffic or any repeated graph
build/teardown eventually crosses the threshold; the NEXT node creation
(its caller can be any subsystem creating a netgraph node, e.g. a socket)
panics the kernel.
Reproduced
4 panics on the QEMU guest (3x via ng_make_node_common during
ngc_attach, 1x via ng_name_node after only the ID-rehash half was
fixed), all "hashdestroy: hash not empty". Traces in panic.txt.
Fix (unconditional LIST_REMOVE + correct mask, both functions) validated at
module level: after the fixed core module was loaded, 20 consecutive
DF-3513 PoC cycles (each previously sufficient to cross the threshold and
panic) completed with exit 0 and no panic.
Reproduce (same harness as DF-3513)
build+load ng7 modules (unfixed), then repeatedly: ./poc_leak 500
(each run pins nodes via the leaks). Panic occurs within ~3-8 runs.
With the fixed core module: 20/20 runs survive.
DF-3514 β VERDICT
Status: reproduced (impact: panic). Fix validated (module-level).
Reproduced 4x on the guest, all panic: hashdestroy: hash not empty:
* 3x ng_make_node_common() path (ID-hash rehash at node creation),
* 1x ng_name_node() path (name-hash rehash β same bug class; first
appeared after the ID-half fix, confirming both halves are live).
Root cause certain from source + traces: #ifdef INVARIANTS-gated
LIST_REMOVE in the rehash migration loops (ng_base.c:956-958, 988-990)
leaves every entry double-inserted on production/standalone-module builds;
hashdestroy() then trips. Additionally ng_ID_rehash() destroys the old
ID table with ng_name_hmask (ng_base.c:996) β wrong mask (OOB/under-run
walk).
Driver for crossing the rehash threshold in practice: permanent node-ref inflation from DF-3513 (drained conq items pin el_dest) and DF-3515 (callout items pin their node) β both reproduced in the same session; also plain repeated named-graph build/teardown.
Fix validated: unconditional LIST_REMOVE in both rehash functions +
hashdestroy(ng_ID_hash, M_NETGRAPH_NODE, ng_ID_hmask). Guest module
rebuild; the previously-panicking workload (20 PoC cycles) then survived
20/20 with exit 0.
Classification: status=reproduced, reproduced=1, impact=panic, confidence=certain, fix_status=fixed.
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
not_testableConfirmed kernel references
Detail
Exploit chain
DF-3513/DF-3515 leaks pin ng_nodes -> ng_nodes*2 > ng_ID_hmask -> ng_make_node_common calls ng_ID_rehash on next node creation (any subsystem) -> double-insert + hashdestroy -> kernel panic; related slab-heap corruption observed once (chunk_mark_allocated assertion) consistent with the double-insert UAF
Evidence (decisive lines)
panic.txt (4 distinct traces); fix.diff; VERDICT.md baseline-vs-fixed run counts
PoC changes
reuses DF-3513 poc_leak harness; panic reproduction needed no extra code beyond repeated runs
Verified recommended fix
unconditional LIST_REMOVE in both rehash loops; hashdestroy(..., ng_ID_hmask)
Verdict
ng_ID_rehash/ng_name_rehash migrate hash entries with LIST_REMOVE guarded by #ifdef INVARIANTS; on production kernels and standalone KLD builds every entry remains double-inserted, so hashdestroy on the old table panics ('hashdestroy: hash not empty'); ng_ID_rehash also destroys the old ID table with the name-hash mask. Reproduced 4x on the guest (3x ID path, 1x name path); node-count threshold is permanently crossed by the DF-3513/DF-3515 node-reference leaks, so routine traffic converts this into a reliable kernel panic. Fix (unconditional LIST_REMOVE + correct mask) validated at module level: the previously-panicking workload survived 20/20 cycles.
No comments yet.