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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3499 Β· 8 files
FileTypeDescriptionSize
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 module ng_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

  1. ng_fec.c:266-267 loop exits immediately (ng_fec_units_len == 0), index == ng_fec_units_len β†’ extend branch.
  2. ng_fec.c:276 bcopy(ng_fec_units /* NULL */, newarray, 0) β€” UB per DFly's own bcopy macro documentation.
  3. ng_fec.c:280-281 guard elided by the compiler β†’ kfree(NULL).
  4. 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).

VERDICT.md
↓ download raw

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

  1. 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)
  2. kldload ng_socket; kldload ./ng_fec.ko
  3. ngctl 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 around kfree(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 kfree with 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

fixed
↓ fix.diffper-fix-DF-3499

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