Double NG_FREE_ITEM on out-hook data path panics INVARIANTS kernels
Summary
ng_split.c:134-137 out-hook branch: NG_FREE_ITEM(item) at :136 frees item sets NGQF_FLAG. Falls through to :144-145 if(item) NG_FREE_ITEM(item). In non-debug builds NG_FREE_ITEM (netgraph.h:816-820) does NOT null item pointer (contrast debug :791 which does). Second NG_FREE_ITEM always executes. KKASSERT(!(item->el_flags & NGQF_FREE)) at netgraph.h:818 fires because flag already set by first call at :136 = panic on INVARIANTS kernels. Production kernels: second NG_FREE_ITEM is harmless idempotent flag-set (KKASSERT compiled out). Trigger: data on out hook (topology-dependent may face untrusted traffic on network-facing split node). Compare ng_hub.c:71-72 avoids this with return(0) after NG_FREE_ITEM. Fix: remove explicit NG_FREE_ITEM from out-hook branch let catch-all :144-145 handle it.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0759 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| inject.c | trigger-source | userland trigger: socket:tx -> split:out topology + 1 data byte via libnetgraph7 (NG_VERSION=8) | 2.9 KB | view raw |
| build.sh | build-script | compiles inject against libnetgraph | 387 B | view raw |
| run.sh | run-script | builds netgraph7 core+socket+ng_split(KCFLAGS=-DINVARIANTS), loads, fires inject | 3.8 KB | view raw |
| fix.diff | suggested-fix | drop redundant NG_FREE_ITEM in out-hook branch; catch-all at :144-145 frees once | 378 B | view raw |
| panic.txt | panic-signature | baseline panic: KKASSERT '!(item->el_flags & NGQF_FREE)' at ng_split.c:145 (INVARIANTS) | 527 B | view raw |
| run.log | run-log | decisive FIXED run: NO_PANIC, guest up, out-hook kprintf still fires | 581 B | view raw |
| build.log | build-log | netgraph7 + ng_split (INVARIANTS) + injector build output | 961 B | view raw |
| env.txt | environment | 6.5-DEVELOPMENT #0 X86_64_GENERIC, INVARIANTS on, gcc 8.3 | 330 B | view raw |
| VERDICT.md | verdict | full narrative: mechanism, reachability, before/after, no-corruption rationale | 6.3 KB | β raw |
| README.md | readme | human reproduction guide | 3.7 KB | β raw |
| manifest.json | manifest | this catalog | 3.2 KB | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0759 β PoC & reproduction
Double NG_FREE_ITEM on the ng_split out-hook data path (Low severity,
CWE-672). A redundant NG_FREE_ITEM(item) in the out-hook branch of
ng_split_rcvdata() is followed unconditionally by a catch-all
if (item) NG_FREE_ITEM(item). The non-debug NG_FREE_ITEM does not null
item, so the catch-all always runs and trips KKASSERT(!(NGQF_FREE)) β
panic on INVARIANTS kernels. On production (non-INVARIANTS) kernels the
second call is a harmless idempotent flag-set.
Reachability & threat model
- Root-only. Delivering data to a split node's
outhook requires a netgraph topology, which requires the privileged netgraph control socket. - Non-default module.
ng_splitlives only insys/netgraph7/(the opt-in parallel netgraph stack). It is NOT inX86_64_GENERICand NOT in the default module build. The guest ships the oldsys/netgraph/stack, which has nosplitnode at all. - Not memory corruption.
NG_FREE_ITEMonly sets a mark-for-later-free flag; the actualng_free_item()runs once. No slab double-free, no UAF, no write primitive β hence no escalation path.
Files
| file | purpose |
|---|---|
inject.c |
userland trigger: builds socketβsplit:out topology, sends 1 data byte |
build.sh |
compiles inject against libnetgraph |
run.sh |
builds netgraph7 core + ng_socket7 + ng_split (KCFLAGS=-DINVARIANTS), loads them, fires inject |
fix.diff |
git-apply-able fix: drop the redundant NG_FREE_ITEM |
panic.txt |
baseline panic signature from boot.log (unpatched) |
VERDICT.md |
full narrative + before/after |
manifest.json |
machine-readable catalog |
How to reproduce (as root, on the DragonFly audit guest)
The trigger must run as root (netgraph control socket). run.sh does
everything from a clean boot:
ssh -F dfbsd-qemu/config dfbsd # root cd /home/maxx/poc/DF-0759 && sh run.sh
What run.sh does:
1. builds netgraph7 core + ng_socket7 from /usr/src/sys/netgraph7/;
2. builds ng_split from /usr/src/sys/netgraph7/ng_split.c with
KCFLAGS=-DINVARIANTS so the KKASSERT inside NG_FREE_ITEM is live
(bsd.kmod.mk does not inherit the kernel's INVARIANTS, so a stock kld
build would have the assertion compiled out β this reproduces the
options NETGRAPH7_SPLIT + INVARIANTS kernel the finding describes);
3. builds/installs libnetgraph7 (NG_VERSION=8 β the stock ngctl/libnetgraph
is NG_VERSION=2 and is rejected by netgraph7's ng_socket);
4. loads the netgraph7 stack + ng_split;
5. compiles inject and fires one data byte at split:out.
Expected output
Bug present (unpatched, INVARIANTS active): the guest panics β ssh dies and
the serial log (dfbsd-qemu/boot.log) shows:
ng_split: got packet from out hook! panic: assertion "!(item->el_flags & NGQF_FREE)" failed in ng_split_rcvdata at .../ng_split.c:145 ng_split_rcvdata() at ng_split_rcvdata+0xfd ng_apply_item() at ng_apply_item+0x105 ngthread() at ngthread+0x18
After fix.diff (same trigger): inject prints NO_PANIC, the guest stays
up, dmesg shows ng_split: got packet from out hook! (the path still runs) but
no panic.
Validating the fix
scp fix.diff dfbsd:/root/fix.diff ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && patch -p1 < /root/fix.diff' # apply ssh -F dfbsd-qemu/config dfbsd 'cd /home/maxx/poc/DF-0759 && sh run.sh' # rebuild + fire # -> NO_PANIC, guest up
DF-0759 β Double NG_FREE_ITEM on ng_split out-hook data path
Verdict: REPRODUCED (panic on INVARIANTS kernels) β root-only, non-default module, NO memory corruption
The double NG_FREE_ITEM described in the finding is real and reproduced as a
kernel panic (KKASSERT) on INVARIANTS builds. It is, however, a narrow defect:
- reachability is root-only (a netgraph topology β needed to deliver data to
a split node's
outhook β can only be set up through the privileged netgraph control socket); - the module is non-default β
ng_splitexists only insys/netgraph7/, the opt-in parallel netgraph stack; it is neither inX86_64_GENERICnor in the default module build (Makefile.modulespickssys/netgraph/unlessWANT_NETGRAPH7is set, andng_splithas no upstream module Makefile / SUBDIR entry); - the primitive is a flag-double-set caught by an assertion, NOT a memory
double-free β
NG_FREE_ITEMonly sets theNGQF_FREE"mark for later free" flag; the actualng_free_item()runs once, later, in the message-reply path. There is no slab double-free, no UAF, no write primitive β no escalation path (Phase 6 does not apply: this is not memory corruption).
Root cause (confirmed by code + live reproduction)
sys/netgraph7/ng_split.c ng_split_rcvdata():
if (hook == priv->out) {
kprintf("ng_split: got packet from out hook!\n");
NG_FREE_ITEM(item); /* :136 marks NGQF_FREE (does NOT null item) */
error = EINVAL;
} else if ((hook == priv->in) && (priv->mixed != NULL)) {
NG_FWD_ITEM_HOOK(error, item, priv->mixed); /* nulls item on success */
} else if ((hook == priv->mixed) && (priv->out != NULL)) {
NG_FWD_ITEM_HOOK(error, item, priv->out);
}
if (item) /* :144 item still non-NULL in the out-hook case */
NG_FREE_ITEM(item); /* :145 second NG_FREE_ITEM -> KKASSERT trips */
The non-debug NG_FREE_ITEM (sys/netgraph7/netgraph.h:816-820):
#define NG_FREE_ITEM(item) \
do { \
KKASSERT(!(item->el_flags & NGQF_FREE)); /* :818 fires on the 2nd call */ \
item->el_flags |= NGQF_FREE; /* does NOT null `item` */ \
} while (0)
KKASSERT (sys/sys/systm.h:94-101) is gated on options INVARIANTS, which the
default X86_64_GENERIC ships (sys/config/X86_64_GENERIC:56). So on an
INVARIANTS build the second NG_FREE_ITEM evaluates !(item->el_flags &
NGQF_FREE) to FALSE (the flag was already set by the first call at :136),
tripping the assertion β panic. On a non-INVARIANTS (production) build the
KKASSERT compiles to do {} while(0) and the second call is an idempotent
flag-set β harmless.
(The debug NG_FREE_ITEM, netgraph.h:787-792, does null item, so under
NETGRAPH_DEBUG the if(item) guard at :144 prevents the second call β another
reason this only bites the non-debug INVARIANTS path.)
How it was reproduced
The guest runs the old sys/netgraph/ netgraph (the default); ng_split is
absent from both the kernel and /boot/kernel/. To exercise the bug the
netgraph7 stack was built from the in-guest /usr/src:
netgraph7core +ng_socket7modules built fromsys/netgraph7/netgraph/andsys/netgraph7/socket/(stock).ng_splitbuilt fromsys/netgraph7/ng_split.cwithKCFLAGS=-DINVARIANTS(a hand-authored module Makefile;bsd.kmod.mkdoes not inherit the kernel'sINVARIANTSdefine, so a stock kld build hasKKASSERTcompiled out β thisKCFLAGSreproduces the INVARIANTS-kernel behaviour the finding describes, equivalent tooptions NETGRAPH7_SPLITin an INVARIANTS kernel).libnetgraph7(NG_VERSION=8) built/installed so the userland injector speaks the netgraph7 wire ABI (the stockngctl/libnetgraphis NG_VERSION=2 and is rejected by netgraph7'sng_socketatng_socket.c:264-268).- The injector (
inject.c) creates a socket node, peers itstxhook to a newsplitnode'southook, and writes one data byte. The mbuf is delivered tosplit:outβng_split_rcvdata(hook == priv->out)β the out-hook branch.
Observed on the unpatched code (INVARIANTS active)
login: ng_split: got packet from out hook!
panic: assertion "!(item->el_flags & NGQF_FREE)" failed in ng_split_rcvdata at /usr/src/sys/netgraph7/split/ng_split.c:145
cpuid = 0
Trace beginning at frame 0xfffff801185299f0
ng_split_rcvdata() at ng_split_rcvdata+0xfd 0xffffffff8260b0fd
ng_split_rcvdata() at ng_split_rcvdata+0xfd 0xffffffff8260b0fd
ng_apply_item() at ng_apply_item+0x105 0xffffffff82601a55
ngthread() at ngthread+0x18 0xffffffff82603488
Debugger("panic")
Stopped at Debugger+0x7c: movb $0,0xbdaf09(%rip)
db>
(The trace also confirms the data path the finding describes: the kprintf at
:135 fires, then the panic cites :145 β the catch-all second NG_FREE_ITEM.)
Impact / escalation
This is a KKASSERT panic (local DoS), root-only, in a non-default module. It
is not memory corruption: NG_FREE_ITEM sets a mark-for-later-free flag, the
actual free happens once. There is no double-free primitive, no UAF, no write
β no privilege-escalation chain (Phase 6 is not applicable). On a production
(non-INVARIANTS) kernel the second call is a no-op flag-set and the defect is
silent.
Fix (validated)
Remove the redundant NG_FREE_ITEM(item) from the out-hook branch; the existing
catch-all if (item) NG_FREE_ITEM(item) at :144-145 then frees the (un-forwarded)
item exactly once. This matches the finding's recommendation and the
ng_hub.c:71-72 early-free pattern. See fix.diff.
Before / after (both with KCFLAGS=-DINVARIANTS, same trigger)
| build | out-hook kprintf |
result |
|---|---|---|
| unpatched ng_split | fires | panic: assertion "!(item->el_flags & NGQF_FREE)" β guest down |
| patched ng_split | fires | NO_PANIC, guest stays up, no assertion |
Both runs drove the identical trigger (data byte β split:out); the patched
module still reaches the out-hook branch (the kprintf fires in dmesg) but
frees the item once instead of twice.
Fix verification
fixedVALIDATED: baseline panic at ng_split.c:145 (KKASSERT); patched NO_PANIC, guest up, out-hook path ran.
BEFORE: panic assertion !(NGQF_FREE) at ng_split.c:145. AFTER: NO_PANIC, guest up, dmesg 'ng_split: got packet from out hook!'.
Confirmed kernel references
- sys/netgraph7/ng_split.c:134
- sys/netgraph7/ng_split.c:136
- sys/netgraph7/ng_split.c:144
- sys/netgraph7/ng_split.c:145
- sys/netgraph7/netgraph.h:816
- sys/netgraph7/netgraph.h:818
- sys/netgraph7/netgraph.h:819
- sys/netgraph7/netgraph/ng_base.c:1974
- sys/sys/systm.h:94
- sys/sys/systm.h:98
- sys/config/X86_64_GENERIC:56
- sys/conf/options:331
Detail
Exploit chain
none -- flag double-set, not slab double-free. No write, no UAF, no corruption. KKASSERT panic only.
Evidence (decisive lines)
BASELINE (INVARIANTS ng_split): panic assertion !(NGQF_FREE) at ng_split.c:145, guest down. PATCHED: inject -> 'NO_PANIC', guest up, dmesg shows out-hook path ran.
PoC changes
Authored from scratch: inject.c (netgraph7 topology trigger), run.sh (builds netgraph7+ng_split with KCFLAGS=-DINVARIANTS), build.sh, VERDICT.md, fix.diff (delete redundant NG_FREE_ITEM at :136), manifest.json.
Verified recommended fix
Delete redundant NG_FREE_ITEM(item) inside out-hook branch at ng_split.c:136. Catch-all at :144-145 frees exactly once. Matches finding proposal. Full git-apply-able diff in findings/poc/DF-0759/fix.diff.
Verdict
REPRODUCED. ng_split.c:136 frees item in out-hook branch (sets NGQF_FREE); catch-all NG_FREE_ITEM at :144-145 runs again -> KKASSERT(!(NGQF_FREE)) trips -> panic. NOT memory corruption (NG_FREE_ITEM only sets flag; real free runs once). Harmless on non-INVARIANTS builds. Root-only, netgraph7 opt-in.
No comments yet.