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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3526 Β· 22 files
FileTypeDescriptionSize
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)
VERDICT.md
↓ download raw

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

  1. Machine code, freshly built (disasm.stock.txt, reloc.stock.txt, GCC 8.3 DragonFly): in ng_iface_constructor, the load of ng_iface_units at +0xd4d flows through memmove (reloc at +0xd76) into call _kfree (reloc at +0xdce) with no NULL test in between. The panic return address +0x312 matches this exact call.
  2. Live reproduction, 3/3 across independent vm.sh reset with-src boots 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
  3. 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).
  4. Fix validation (fix.diff, module rebuild only, no kernel rebuild): disasm.fix26.txt shows a real test %r10,%r10 / je now dominating the kfree, and the ng_iface_units_len > 0 guard around the bcopy. Live: full lifecycle succeeds β€” mkpeer survives, 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

fixed
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0 (stock kernel; ng_iface module rebuilt with fix.diff only)

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