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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3514 Β· 6 files
FileTypeDescriptionSize
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 of ng_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-only LIST_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.

VERDICT.md
↓ download raw

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_testable
↓ fix.diffper-fix-DF-3514

Confirmed 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.