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)
PoC verification
Evidence pack
findings/poc/DF-3485 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
- 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). - Machine-code proof: objdump of the shipped module
(disasm.stock.txt) shows the load of
ng_iface_unitsat +0x31d flowing intocall _kfreeat +0x39d with no NULL test in between (relocations identify_kfree,M_NETGRAPH, and the.bssglobals). The return address+0x3a2in the panic trace matches this exact call. DWARF maps the call to ng_iface.c:295 via the inlinedng_iface_get_unit. - 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.
- 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. - Fix validation (fix.diff, module rebuild only):
- disasm.fixed.txt: a real
test/jenow 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-1001ngctlgets 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_unitsbitmap 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
fixedSingle-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
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).
No comments yet.