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

ng_iface (v1): first node creation panics in kfree(NULL) - bcopy() UB makes GCC drop the NULL guard on ng_iface_units

Summary

Creating the first ng_iface node deterministically panics the kernel: 'panic: trying to free NULL pointer' from ng_iface_constructor+0x3a2. ng_iface_get_unit() passes the initially-NULL ng_iface_units to bcopy(); systm.h maps bcopy onto __builtin_memmove and documents that GCC then assumes the operand non-NULL (even for len 0) and optimizes out later NULL tests - so the NULL guard at :294 is dead code in the binary and kfree(NULL) runs on the first array-growth. VERIFIED live 3/3 boots (plain root ngctl mkpeer -> instant panic, guest dies) and by disassembly of the shipped AND freshly-rebuilt module (unguarded kfree call, relocations prove _kfree/M_NETGRAPH). Unconditional panic - production kernels affected. Root-only trigger (ng_socket SYSCAP_RESTRICTEDROOT EPERM verified as uid 1001); no memory corruption. Fix validated module-only (guard the bcopy itself): guard present in machine code, free-NULL panic gone (creation then reaches the independent DF-3486 defect; with both fixes the full lifecycle works).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3485 Β· 16 files
FileTypeDescriptionSize
trigger.sh β€” 362 B view raw
run.sh β€” 186 B view raw
build.sh β€” 184 B view raw
panic.txt β€” 1.3 KB view raw
panic.after_fix.txt β€” 1.1 KB view raw
disasm.stock.txt β€” 2.4 KB view raw
disasm.fixed.txt β€” 1.0 KB view raw
unpriv_check.log β€” 609 B view raw
env.txt β€” 762 B view raw
run.log β€” 1.1 KB view raw
build.log β€” 1.1 KB view raw
fix_build.log β€” 1.2 KB view raw
fix_run.log β€” 1.9 KB view raw
fix.diff β€” 990 B view raw
VERDICT.md β€” 4.2 KB ↓ raw
verdict.json β€” 3.7 KB view raw
VERDICT.md
↓ download raw

DF-3485 VERDICT β€” REPRODUCED (panic, deterministic, root-only)

Bottom line

Creating the first ng_iface node on the stock kernel panics instantly and 100% deterministically: panic: trying to free NULL pointer from ng_iface_constructor+0x3a2 (= the kfree(ng_iface_units, M_NETGRAPH) at sys/netgraph/iface/ng_iface.c:295, reached through the inlined ng_iface_get_unit()). Root cause is a compiler-visible UB pattern, not a stale binary: bcopy(ng_iface_units, ...) at ng_iface.c:290 feeds __builtin_memmove() a pointer that is NULL on the first call; per the in-tree contract in sys/sys/systm.h:257-260 GCC then assumes the pointer is non-NULL and deletes the if (ng_iface_units != NULL) guard at :294, so the compiled module calls kfree(NULL) unconditionally on the array-growth path β€” which is exactly the first-creation path. kfree(NULL) is an unconditional panic() (sys/kern/kern_slaballoc.c:1406-1407), so production (non-INVARIANTS) kernels are equally affected.

How it was proven

  1. Live reproduction (fresh boot, stock modules): kldload ng_socket ng_iface; ngctl mkpeer . iface inet inet β†’ instant panic, guest dead, trace in panic.txt. Reproduced on every attempt (3/3 across independent boots; also independently observed during the DF-3482 ng_ppp run).
  2. Machine-code proof: objdump of the shipped module (disasm.stock.txt) shows the load of ng_iface_units at +0x31d flowing into call _kfree at +0x39d with no NULL test in between (relocations identify _kfree, M_NETGRAPH, and the .bss globals). The return address +0x3a2 in the panic trace matches this exact call. DWARF maps the call to ng_iface.c:295 via the inlined ng_iface_get_unit.
  3. Not a stale binary: rebuilding the module from the unmodified /usr/src (md5-identical to the audit tree) reproduces the identical unguarded pattern β€” this is what the current tree compiles to.
  4. Why the guard vanished: systm.h:257-260 documents that bcopy()β†’__builtin_memmove() lets GCC "optimize-out later NULL tests" on the operands β€” precisely what happened to the :294 guard.
  5. Fix validation (fix.diff, module rebuild only): - disasm.fixed.txt: a real test/je now dominates both bcopy and kfree. - Live: with only this fix, the free-NULL panic is GONE; creation now panics later at the if_attach() call (ng_iface.c:605) with the distinct DF-3486 signature (panic.after_fix.txt). - With DF-3486's fix too, the full ng_iface lifecycle (create, list, ifconfig ng0, shutdown incl. deferred if_detach, 22+ rapid create/destroy cycles) succeeds without any panic (findings/poc/DF-3486/fix_run.log).

Reachability / severity

  • Trigger: root only. The only entry to node creation is a netgraph control message; AF_NETGRAPH control sockets require caps_priv_check(SYSCAP_RESTRICTEDROOT) (ng_socket.c:172-176) β€” verified live: uid-1001 ngctl gets EPERM (unpriv_check.log).
  • Impact: deterministic kernel panic (DoS) by an already-privileged user; no memory corruption occurs (the allocator checks NULL before touching anything). Severity Low.

Relation to DF-3482's lead

The DF-3482 lead said ng_eiface panics with "trying to free NULL pointer at ng_eiface_constructor+0xf6". That was an extrapolation from a truncated console window; the actual ng_eiface panic is a different defect (DF-3486). This pack fixes the iface-only NULL-free. ng_eiface has no get_unit bitmap and is not affected by DF-3485's root cause.

Residual notes

  • ng_iface_units bitmap itself remains unsynchronized, but v1 node creation is serialized on the netisr control path, so no concurrent access exists today.
  • The B-only regression observed mid-validation (free-NULL panic returning after a crash) was HAMMER2 write-loss reverting /usr/src edits made ~60s before a panic β€” an environment artifact, documented here so nobody chases it as a "fix regression".

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

Single-fix module validated: disasm.fixed.txt shows a real test/je guarding bcopy+kfree, and the 'trying to free NULL pointer' panic no longer occurs on the trigger. The remaining panic seen with only this fix is the independent DF-3486 defect (if_attach from netisr, ng_iface.c:605), whose own fix is validated in findings/poc/DF-3486 (with both fixes: ng0 created/listed/shutdown cleanly, 22+ create-destroy cycles, no panic).

disasm.fixed.txt; panic.after_fix.txt; findings/poc/DF-3486/fix_run.log
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (stock kernel; fix applied as module rebuild only)

Confirmed kernel references

Detail

Evidence (decisive lines)

panic.txt (stock trace, ng_iface_constructor+0x3a2); disasm.stock.txt (unguarded kfree with relocations) vs disasm.fixed.txt (test/je guard); panic.after_fix.txt (panic signature changes -> defect eliminated); findings/poc/DF-3486/fix_run.log (full lifecycle with both fixes); unpriv_check.log (EPERM for uid 1001)

PoC changes

none needed β€” plain ngctl reproduces on the stock kernel; trigger is 3 lines

Verified recommended fix

Guard the bcopy itself: move it (and the kfree) inside the existing 'if (ng_iface_units != NULL)' so no potentially-NULL pointer is ever passed to __builtin_memmove (see fix.diff).

Verdict

Deterministic kernel panic on the first ng_iface (v1 netgraph) node creation: ng_iface_get_unit() passes the initially-NULL ng_iface_units pointer to bcopy(), which sys/sys/systm.h:257-265 maps onto __builtin_memmove() with the documented consequence that GCC assumes the pointer non-NULL and deletes the later 'if (ng_iface_units != NULL)' guard, so the compiled module executes kfree(NULL) -> panic("trying to free NULL pointer") (kern_slaballoc.c:1406-1407). Proven by live reproduction (3/3 boots, plain root ngctl mkpeer), by disassembly of both the shipped and freshly rebuilt modules (unguarded call, relocations identify _kfree/M_NETGRAPH), and by DWARF line mapping of the panic return address. Root-only reachability (ng_socket.c:172 caps_priv_check); no memory corruption; Low severity. fix.diff guards the bcopy itself; validated: machine-code guard present, the free-NULL panic disappears (creation then reaches the independent DF-3486 defect; with both fixes the full lifecycle works).