ng7 ng_iface: first node creation panics in kfree(NULL) β bcopy() UB makes GCC drop the NULL guard on ng_iface_units (DF-3485's ng7 twin)
Summary
ng_iface_get_unit() passes the initially-NULL ng_iface_units to bcopy(). systm.h maps bcopy onto __builtin_memmove and documents that GCC may then assume the operand non-NULL and delete later NULL tests, so the 'if (ng_iface_units != NULL)' guard at :300 is dead code in the compiled ng7 module and the first array growth - the FIRST ng7 iface node ever created - calls kfree(NULL), an unconditional panic. Machine-code proof: freshly-built module's ng_iface_constructor has memmove flowing into call _kfree with no intervening test; panic return address ng_iface_constructor+0x312 matches. REPRODUCED 3/3 across independent fresh-boot/rebuild rounds; production (non-INVARIANTS) kernels equally affected. Root-only trigger (ng7 control sockets require SYSCAP_RESTRICTEDROOT; verified EPERM for uid 1001). DF-3486 twin REFUTED for ng7: ng_snd_item funnels control to the dedicated ngthread kernel-thread context where if_attach is legal (proven live). Fix validated at module level (guard the bcopy on ng_iface_units_len > 0): full lifecycle + 20/20 serial + parallel create/destroy clean. No write primitive - clean panic at allocator argument validation.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3526 Β· 22 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.9 KB | β raw | |
| VERDICT.md | β | 4.5 KB | β raw | |
| iface_con.c | β | 4.8 KB | view raw | |
| build.sh | β | 193 B | view raw | |
| run.sh | β | 165 B | view raw | |
| stress.sh | β | 494 B | view raw | |
| build.log | β | 6.6 KB | view raw | |
| build.depmods.log | β | 14.0 KB | view raw | |
| fix_build.log | β | 719 B | view raw | |
| run.log | β | 731 B | view raw | |
| run.2.log | β | 636 B | view raw | |
| run.3.log | β | 600 B | view raw | |
| fix_run.log | β | 734 B | view raw | |
| panic.txt | β | 558 B | view raw | |
| console_full.txt | β | 2.1 KB | view raw | |
| unpriv_check.txt | β | 508 B | view raw | |
| env.txt | β | 150 B | view raw | |
| disasm.stock.txt | β | 26.1 KB | view raw | |
| reloc.stock.txt | β | 8.9 KB | view raw | |
| disasm.fix26.txt | β | 26.3 KB | view raw | |
| fix.diff | β | 482 B | view raw | |
| verdict.json | β | 3.5 KB | view raw |
DF-3526 β netgraph7 ng_iface: first node creation panics in kfree(NULL)
(bcopy() UB makes GCC drop the NULL guard on ng_iface_units)
What
Creating the first netgraph7 ng_iface node panics the kernel deterministically:
panic: trying to free NULL pointer _kfree() at _kfree+0x558 ng_iface_constructor() at ng_iface_constructor+0x312 ng_make_node() -> ng_apply_item() -> ngthread()
ng_iface_get_unit() (sys/netgraph7/iface/ng_iface.c:292-303) passes the
initially-NULL ng_iface_units to bcopy(). sys/systm.h maps bcopy onto
__builtin_memmove and documents that GCC may then assume the operand
non-NULL and delete later NULL tests β the if (ng_iface_units != NULL)
guard at :300 is dead code in the compiled module, so the first
array-growth (i.e. the FIRST node ever created) calls kfree(NULL), which
is an unconditional panic() (sys/kern/kern_slaballoc.c:1406-1407).
Production (non-INVARIANTS) kernels are equally affected.
This is the ng7 twin of v1 DF-3485 (sys/netgraph/iface/ng_iface.c:290-295): same code pattern, separate file/module (ng7 module set), separate fix.
NOTE: the OTHER half of the v1 lead does NOT carry over β ng7 does NOT
share DF-3486 (if_attach from netisr control path). ng7 funnels every
first-hop control message to the dedicated ngthread via
lwkt_sendmsg(ng_cpuport(0)) (ng_base.c:1956-1961), a kernel-thread
context in which if_attach() is legal: with only the bcopy fix applied,
the full ng_iface lifecycle (mkpeer -> ng0 -> GET_IFNAME -> shutdown)
succeeds, 20/20 + a parallel pair. See VERDICT.md.
Trigger privilege
ROOT ONLY (verified live): ng7 ng_socket control sockets require
caps_priv_check(SYSCAP_RESTRICTEDROOT) (sys/netgraph7/socket/ng_socket.c:182-185);
uid 1001 gets EPERM (unpriv_check.txt). Severity Low (privileged DoS).
Build / Run (in-guest, root)
# 1. build ng7 modules (netgraph core, ng_socket, ng_iface) cd /usr/src/sys/netgraph7/netgraph && make cd ../socket && make cd ../iface && make cc -O2 -o iface_con iface_con.c # 2. load and fire kldload /usr/src/sys/netgraph7/netgraph/netgraph.ko kldload /usr/src/sys/netgraph7/socket/ng_socket.ko kldload /usr/src/sys/netgraph7/iface/ng_iface.ko ./iface_con create # -> instant panic, guest dead
Expected
Stock modules: panic: trying to free NULL pointer at
ng_iface_constructor+0x312 (the unguarded kfree; see panic.txt,
disasm.stock.txt + reloc.stock.txt: +0xd76 memmove flows into
+0xdce _kfree with no NULL test). Reproduced 3/3 on independent
boots (run.log, run.2.log, run.3.log).
With fix.diff (module rebuild only): creation succeeds
(disasm.fix26.txt: real test/je before kfree), ifname=ng0,
clean shutdown, 20/20 lifecycle + parallel stress clean (fix_run.log).
Fix validation
cd /usr/src && patch -p1 < fix.diff # bcopy call guarded by units_len>0 (rebuild ng_iface module only, reload, run stress.sh)
DF-3526 VERDICT β REPRODUCED (panic, deterministic 3/3, root-only)
Bottom line
Creating the first netgraph7 ng_iface node on the stock kernel panics
instantly and 100% deterministically: panic: trying to free NULL pointer
from ng_iface_constructor+0x312, reached through the inlined
ng_iface_get_unit() (sys/netgraph7/iface/ng_iface.c:292-303).
Root cause is a compiler-visible UB pattern, proven not a stale
binary: bcopy(ng_iface_units, ...) at ng_iface.c:296 feeds
__builtin_memmove() a pointer that is NULL on the first call; per the
in-tree contract (sys/sys/systm.h:257-260) GCC then assumes the pointer
non-NULL and deletes the if (ng_iface_units != NULL) guard at :300, 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),
so non-INVARIANTS production kernels are equally affected. This is the
ng7 twin of v1 DF-3485, independently verified against the ng7 module set.
How it was proven
- Machine code, freshly built (disasm.stock.txt, reloc.stock.txt,
GCC 8.3 DragonFly): in
ng_iface_constructor, the load ofng_iface_unitsat +0xd4d flows throughmemmove(reloc at +0xd76) intocall _kfree(reloc at +0xdce) with no NULL test in between. The panic return address+0x312matches this exact call. - Live reproduction, 3/3 across independent
vm.sh reset with-srcboots with freshly built stock modules:./iface_con create(root, ng7 control socket, NGM_MKPEER type "iface") β guest dead every time with the identical trace (panic.txt, run.log, run.2.log, run.3.log): _kfree+0x558 <- ng_iface_constructor+0x312 <- ng_make_node+0x5f <- ng_apply_item+0x666 <- ngthread+0x18 - Trigger is root-only: ng7 ng_socket control-socket attach is
gated by
caps_priv_check(SYSCAP_RESTRICTEDROOT | __SYSCAP_NULLCRED)(sys/netgraph7/socket/ng_socket.c:182-185); uid 1001 gets EPERM (unpriv_check.txt). Severity: Low (privileged deterministic DoS). - Fix validation (fix.diff, module rebuild only, no kernel rebuild):
disasm.fix26.txt shows a real
test %r10,%r10 / jenow dominating the kfree, and theng_iface_units_len > 0guard around the bcopy. Live: full lifecycle succeeds βmkpeersurvives, GET_IFNAME returns "ng0", NGM_SHUTDOWN clean; 20/20 serial + 2 parallel create/destroy cycles, zero panics (fix_run.log).
The DF-3486 half of the lead is REFUTED for ng7
With only the bcopy fix applied (DF-3526), node creation proceeds to
if_attach(ifp, NULL) (ng_iface.c:624) and succeeds β no
"try holding ifnet lock in netisr" panic, no deadlock. Mechanically:
ng_snd_item() queues every first-hop item (refs == 1 β which is
every control message entering netgraph from ng_socket) to cpu0's
netgraph port: lwkt_sendmsg(ng_cpuport(0), msg) (ng_base.c:1956-1961),
where the dedicated ngthread (a kernel thread, not a netisr) applies
it. The DF-3486 v1 panic came from v1's control path running on a
TD_TYPE_NETISR thread; ng7's thread model differs (exactly the audit
lead's "token model differs" hint), and in ngthread context
if_attach()'s ifnet lock and netmsg_service_sync() are legal.
Empirical + mechanical: ng7 ng_iface does NOT share DF-3486.
Side benefit: because all ng7 control messages (constructors and
shutdowns alike) funnel through the single cpu0 ngthread, the
unsynchronized ng_iface_units bitmap is serialized by construction β
the parallel create test (fix_run.log) exercised exactly that.
Impact characterization
kfree(NULL) is caught by the allocator's explicit NULL check before
any memory is touched β a clean, deterministic panic (DoS). There is no
write primitive, no double-free, no corruption: the stop is inside
kfree's argument validation. No escalation path exists from this bug.
Relation to prior findings
- DF-3485 (v1 sys/netgraph/iface/ng_iface.c): same root cause, v1 module set; verified 3/3 there too. DF-3526 is the independent ng7 copy β separate file, separate module, separate fix commit.
- DF-3486: NOT shared (see above).
- DF-0607 (BPF write uninit sa_data on the same ng7 file): unrelated, not re-reported here.
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
fixedConfirmed kernel references
Detail
Evidence (decisive lines)
panic.txt (trace _kfree <- ng_iface_constructor+0x312 <- ng_make_node <- ng_apply_item <- ngthread); disasm.stock.txt + reloc.stock.txt (memmove@+0xd76 flows into _kfree@+0xdce, no NULL test); run.log/run.2.log/run.3.log (3/3); unpriv_check.txt (EPERM for uid 1001); disasm.fix26.txt + fix_run.log (fixed module: guard restored, lifecycle + stress clean)
PoC changes
None β harness written fresh for this finding (ng7 control-socket ABI adapted from the DF-3524 pattern); no seed PoC existed.
Verified recommended fix
Guard the bcopy call with 'if (ng_iface_units_len > 0)' so the NULL-pointer operand never reaches __builtin_memmove and GCC keeps the kfree NULL test (see fix.diff).
Verdict
Creating the first netgraph7 ng_iface node panics the stock kernel deterministically (3/3 independent boots): 'panic: trying to free NULL pointer' at ng_iface_constructor+0x312. Root cause is the compiler-visible UB pattern in ng_iface_get_unit() (sys/netgraph7/iface/ng_iface.c:296-301): bcopy() of the initially-NULL ng_iface_units maps onto __builtin_memmove, GCC deletes the NULL guard at :300, and the first array growth calls kfree(NULL), which is an unconditional panic even on non-INVARIANTS kernels (kern_slaballoc.c:1406). Proven live and by disassembly of the freshly-built module (reloc +0xd76 memmove -> +0xdce _kfree, no intervening test). ng7 twin of v1 DF-3485; separate file and module set. Trigger is root-only (ng7 ng_socket ngc_attach requires caps_priv_check(SYSCAP_RESTRICTEDROOT), uid 1001 gets EPERM), so impact is a privileged deterministic DoS with no write primitive (kfree validates NULL before touching memory). fix.diff (guard the bcopy call) validated at module level: guard restored in machine code, full mkpeer/GET_IFNAME=ng0/shutdown lifecycle plus 20/20 serial and parallel create/destroy cycles clean.
No comments yet.