ng_fec_get_unit: bcopy(NULL, newarray, 0) makes the compiler elide the explicit NULL guard -> unconditional kfree(NULL): deterministic kernel panic on the FIRST fec node creation
Summary
First-ever ng_fec_get_unit() call runs the extend branch with ng_fec_units==NULL/len==0 and executes bcopy(NULL, newarray, 0). bcopy is __builtin_memmove whose header documents that the compiler assumes non-NULL src even for len==0 and optimizes out later NULL tests - the explicit guard at :280 is absent from the compiled object (verified by disassembly: ctor+0x39e = 'mov %r9,%rdi; call kfree' with no test/branch). kfree(NULL) panics. The v1 ng_fec module cannot create a single node at HEAD. REPRODUCED 3x across boots on both the stale installed module and the freshly built HEAD module ('panic: trying to free NULL pointer', backtrace ng_fec_constructor <- ng_mkpeer; one ngctl mkpeer suffices). Same bug family as DF-3485 (ng_iface). Fix validated (guard the bcopy itself): creation proceeds past get_unit. Notably this also masks the known DF-0529 constructor double-kfree on stock builds.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3499 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.3 KB | β raw | |
| VERDICT.md | β | 3.3 KB | β raw | |
| panic.txt | β | 517 B | view raw | |
| fix.diff | β | 452 B | view raw | |
| build.sh | β | 91 B | view raw | |
| run.sh | β | 167 B | view raw | |
| verdict.json | β | 2.6 KB | view raw | |
| manifest.json | β | 691 B | view raw |
DF-3499 β ng_fec_get_unit: bcopy(NULL,β¦,0) elides the NULL guard β unconditional kfree(NULL) panic on first node creation
- File:
sys/netgraph/fec/ng_fec.c(v1 netgraph, LIVE moduleng_fec.ko) - Lines: 276-283 (
ng_fec_get_unit) - Severity: Medium (deterministic kernel panic / local DoS; trigger = one
netgraph control message, which requires the netgraph control socket
(root-only on stock DFly via
caps_priv_check(SYSCAP_RESTRICTEDROOT))) - Bucket: base:netgraph (DoS)
Build
In-guest, stock module (no patches):
cd /usr/src/sys/netgraph/fec && make
Run
kldload ng_socket kldload /usr/src/sys/netgraph/fec/ng_fec.ko ngctl mkpeer fec c0 c1 # any fec node creation
Expected
Immediate kernel panic:
panic: trying to free NULL pointer _kfree() at _kfree+0x558 _kfree() at _kfree+0x558 ng_fec_constructor() at ng_fec_constructor+0x39e ng_mkpeer() ...
The module is unusable at HEAD: the very first ng_fec_get_unit() call takes
the "extend array" branch with ng_fec_units == NULL / ng_fec_units_len == 0
and executes bcopy(NULL, newarray, 0). bcopy is __builtin_memmove
(sys/sys/systm.h:262-265), whose header comment states the compiler "assumes
that (to) and (from) cannot be NULL (even when (len) might be 0), and will
optimize-out later NULL tests" β the explicit if (ng_fec_units != NULL)
guard at ng_fec.c:280 is optimized out of the shipped object code (verified by
disassembly: mov %r9,%rdi; call kfree with no test/branch; DWARF maps the
call to ng_fec.c:281), so kfree(NULL) executes and panics.
Root cause chain
ng_fec.c:266-267loop exits immediately (ng_fec_units_len == 0),index == ng_fec_units_lenβ extend branch.ng_fec.c:276bcopy(ng_fec_units /* NULL */, newarray, 0)β UB per DFly's own bcopy macro documentation.ng_fec.c:280-281guard elided by the compiler βkfree(NULL).sys/kern/kern_slaballoc.c:1406-1407βpanic("trying to free NULL pointer").
Fix
Guard the bcopy itself (see fix.diff). Validated in-guest: with the guard the
constructor proceeds past ng_fec_get_unit (creation then trips the separate,
pre-existing netisr/ifnet_lock defect filed as DF-3500; driven from thread
context, node creation fully succeeds with this fix applied).
DF-3499 VERDICT β reproduced (panic)
What was run
Guest: DragonFly 6.5-DEVELOPMENT #0 (INVARIANTS) x86_64, vtnet0+lo0, /usr/src == audit tree HEAD (md5 of ng_fec.c verified identical: 5aeb55ff27bef163dd94950a9cbbe294).
cd /usr/src/sys/netgraph/fec && make(stock source, module rebuilt from HEAD β the installed /boot/kernel/ng_fec.ko is a stale build; both the stale one and the fresh HEAD build panic identically)kldload ng_socket; kldload ./ng_fec.kongctl mkpeer fec c0 c1
Result: immediate kernel panic (see panic.txt):
panic: trying to free NULL pointer _kfree() at _kfree+0x558 _kfree() at _kfree+0x558 ng_fec_constructor() at ng_fec_constructor+0x39e ng_mkpeer() at ng_mkpeer+0x4c ng_generic_msg() ...
Reproduced 3+ times across separate boots (two with the stale installed module at ctor+0x3ae, one with the freshly built HEAD module at ctor+0x39e; module base addresses confirm which build).
Why (path:line)
- ng_fec.c:266-268: with ng_fec_units==NULL / len==0 the scan loop is a no-op and index==len β the "extend array" branch runs.
- ng_fec.c:276:
bcopy(ng_fec_units, newarray, 0)with a NULL source. - sys/sys/systm.h:256-265: bcopy is __builtin_memmove; the header comment warns that the compiler assumes non-NULL (even for len 0) and "will optimize-out later NULL tests".
- ng_fec.c:280-281: the source-level
if (ng_fec_units != NULL)guard aroundkfree(ng_fec_units)is therefore NOT present in the object code. Verified by disassembly of the freshly built module (constructor+0x39e = 0xc89:mov %r9,%rdi; call kfreewith no preceding test/branch on r9) and by DWARF line info mapping the stale build's call site to ng_fec.c:281. - sys/kern/kern_slaballoc.c:1406-1407: kfree(NULL) β panic.
Impact
Deterministic kernel panic (local DoS) on the first fec node creation from
any netgraph control socket. On stock DFly the control socket requires
root (caps_priv_check(SYSCAP_RESTRICTEDROOT), sys/netgraph/socket/
ng_socket.c:172-176), so this is a root-triggerable panic / completely
broken module rather than an unprivileged primitive. The module cannot
create even a single node at HEAD β every other ng_fec code path is
reachable only after working around this bug.
Fix validation
fix.diff (guard the bcopy call itself) applied to the guest's /usr/src copy,
module rebuilt: ngctl mkpeer fec c0 c1 no longer panics in
ng_fec_get_unit; creation proceeds (and then hits the separate DF-3500
netisr/ifnet_lock defect, as expected). Driven from thread context
(fecdrive.ko), creation fully succeeds with only this fix applied.
fix_status: fixed.
Honest notes
- The pre-installed /boot/kernel/ng_fec.ko (Jun 29 2026 build) panics at the same site with a slightly different offset (ctor+0x3ae); the freshly built HEAD module panics at ctor+0x39e. Both are the kfree(NULL); the HEAD source contains the NULL guard that the compiler elides.
- Attempts: 3 distinct reproductions across boots.
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 (kfree(NULL) backtrace through ng_fec_constructor+0x39e <- ng_mkpeer); VERDICT.md (disassembly + DWARF line-mapping proof that the source guard is compiled out); fix.diff validated in-guest (ctor proceeds past get_unit)
PoC changes
none: stock module rebuilt from HEAD /usr/src in-guest; md5 of ng_fec.c verified identical to audit tree
Verified recommended fix
Guard the bcopy itself: if (ng_fec_units != NULL) bcopy(...) in ng_fec_get_unit()
Verdict
Deterministic kernel panic on first ng_fec node creation at HEAD: ng_fec_get_unit() executes bcopy(NULL, newarray, 0) (ng_fec.c:276); DFly's bcopy macro (__builtin_memmove, sys/sys/systm.h:262) lets the compiler assume a non-NULL source even for zero length and elide the explicit NULL guard at ng_fec.c:280, so kfree(NULL) runs and panics (kern_slaballoc.c:1407). Verified by disassembly of the freshly built module (no test/branch before the kfree call mapped by DWARF to ng_fec.c:281) and reproduced 3x across boots, including with the stale installed module. With fix.diff (guard the bcopy) creation passes get_unit. Trigger is a single netgraph control message (root-only control socket on stock DFly), so impact is a root-triggerable local DoS / completely unusable module rather than an unprivileged primitive.
No comments yet.