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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0759 Β· 13 files
FileTypeDescriptionSize
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
README.md readme human reproduction guide
↓ download 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 out hook requires a netgraph topology, which requires the privileged netgraph control socket.
  • Non-default module. ng_split lives only in sys/netgraph7/ (the opt-in parallel netgraph stack). It is NOT in X86_64_GENERIC and NOT in the default module build. The guest ships the old sys/netgraph/ stack, which has no split node at all.
  • Not memory corruption. NG_FREE_ITEM only sets a mark-for-later-free flag; the actual ng_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
VERDICT.md verdict full narrative: mechanism, reachability, before/after, no-corruption rationale
↓ download raw

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 out hook β€” can only be set up through the privileged netgraph control socket);
  • the module is non-default β€” ng_split exists only in sys/netgraph7/, the opt-in parallel netgraph stack; it is neither in X86_64_GENERIC nor in the default module build (Makefile.modules picks sys/netgraph/ unless WANT_NETGRAPH7 is set, and ng_split has no upstream module Makefile / SUBDIR entry);
  • the primitive is a flag-double-set caught by an assertion, NOT a memory double-free β€” NG_FREE_ITEM only sets the NGQF_FREE "mark for later free" flag; the actual ng_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:

  1. netgraph7 core + ng_socket7 modules built from sys/netgraph7/netgraph/ and sys/netgraph7/socket/ (stock).
  2. ng_split built from sys/netgraph7/ng_split.c with KCFLAGS=-DINVARIANTS (a hand-authored module Makefile; bsd.kmod.mk does not inherit the kernel's INVARIANTS define, so a stock kld build has KKASSERT compiled out β€” this KCFLAGS reproduces the INVARIANTS-kernel behaviour the finding describes, equivalent to options NETGRAPH7_SPLIT in an INVARIANTS kernel).
  3. libnetgraph7 (NG_VERSION=8) built/installed so the userland injector speaks the netgraph7 wire ABI (the stock ngctl/libnetgraph is NG_VERSION=2 and is rejected by netgraph7's ng_socket at ng_socket.c:264-268).
  4. The injector (inject.c) creates a socket node, peers its tx hook to a new split node's out hook, and writes one data byte. The mbuf is delivered to split: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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: 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!'.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 (kernel unchanged; module-level validation via ng_split.ko built with KCFLAGS=-DINVARIANTS)

Confirmed kernel references

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.