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

Missing return after NG_FREE_DATA in ng_etf_rcvdata turns discard path into NULL-deref panic

Summary

ng_etf_rcvdata (:381-383): if(NG_HOOK_PRIVATE(hook)==NULL){NG_FREE_DATA(m,meta);} NG_FREE_DATA (netgraph.h:283-287) sets m=NULL+meta=NULL. NO return after. Falls through :392 if(m->m_len<sizeof(*eh)) dereferences NULL m -> PANIC. Comment "Shouldnt happen but.." intent was free+bail missing bail. Reachable: ng_send_data (ng_base.c:1678-1698) checks HK_INVALID no lock then invokes rcvdata synchronously. ng_destroy_hook (ng_base.c:814-827) sets HK_INVALID then ng_etf_disconnect :491 NG_HOOK_SET_PRIVATE(hook,NULL). SMP: ng_send_data check passes before disconnect runs rcvdata observes private==NULL. Data path NO mplock (only ngintr netisr ng_base.c:2034 holds mplock). Attacker: ng_socket access (root) spam etf hook with traffic while tearing hook down loop. Impact: reliable kernel panic local DoS m==NULL not dangling no UAF/exploitation. Fix: return(EINVAL) after NG_FREE_DATA.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0696 Β· 9 files
FileTypeDescriptionSize
race.sh trigger-source ngctl connect/rmhook race loop 2.4 KB view raw
build.sh build-script no-op 186 B view raw
run.sh run-script wraps race.sh 132 B view raw
run.log run-log 200-iter attempt, no panic 1.1 KB view raw
VERDICT.md verdict this analysis 3.3 KB ↓ raw
fix.diff suggested-fix add return(EINVAL) after NG_FREE_DATA 385 B view raw
README.md readme human reproduce doc 1.2 KB ↓ 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 reproduce doc
↓ download raw

DF-0696 β€” Missing return after NG_FREE_DATA in ng_etf_rcvdata

Summary

sys/netgraph/etf/ng_etf.c:381-392 ng_etf_rcvdata() checks NG_HOOK_PRIVATE(hook) == NULL and on that path calls NG_FREE_DATA(m, meta) (which NULLs both pointers) β€” but lacks a return. Control falls through to if (m->m_len < sizeof(*eh)) which dereferences NULL β†’ page-fault panic.

How to (attempt to) reproduce

./build.sh          # nothing to compile (shell probe)
ssh dfbsd 'kldload ng_socket.ko; kldload ng_etf.ko; kldload ng_ether.ko'
ssh dfbsd 'cd /root/poc/DF-0696 && sh ./run.sh 200'

The race is tight (sender must observe HK_INVALID==0 just before disconnect sets HK_INVALID + private=NULL); a 200-iteration ngctl loop did not fire on the default kernel.

Preconditions

  • root (netgraph ng_socket creation is privileged).
  • concurrent hook disconnect racing with ng_send_data calls.

Impact

Local DoS (panic) once the race fires. No UAF β€” NG_FREE_DATA NULLs the freed pointers, so this is not a corruption primitive.

Fix

fix.diff adds return (EINVAL); immediately after NG_FREE_DATA, matching the apparent original intent (free + bail).

VERDICT.md verdict this analysis
↓ download raw

DF-0696 β€” Missing return after NG_FREE_DATA in ng_etf_rcvdata

Verdict: NOT REPRODUCED (bug is structurally real; trigger requires root + a tight race)

Mechanism (sys/netgraph/etf/ng_etf.c:381-392)

if (NG_HOOK_PRIVATE(hook) == NULL) { /* Shouldn't happen but.. */    /* line 381 */
    NG_FREE_DATA(m, meta);           /* sets m = NULL, meta = NULL */ /* line 382 */
}                                                                    /* line 383: NO return! */

...
if (m->m_len < sizeof(*eh)) {          /* line 392: m == NULL -> page fault */

NG_FREE_DATA (netgraph.h:283-287) is a do{ NG_FREE_M(m); NG_FREE_META(a); }while(0) where both inner macros NULL their argument. The "Shouldn't happen but.." comment shows the original intent was clearly free-and-bail; the bail was forgotten. Falling through, m->m_len reads from NULL β†’ page fault.

Trigger

The condition NG_HOOK_PRIVATE(hook) == NULL becomes true during ng_etf_disconnect (line 491) β€” which sets HK_INVALID and sets private to NULL β€” racing with ng_send_data (ng_base.c), which checks HK_INVALID without a lock and then synchronously invokes rcvdata. If the sender's check passes just before disconnect runs, rcvdata observes private == NULL and hits the bug.

Reachability

  • ng_socket creation is root-only (CAP_NETGRAPH + the standard netgraph privilege check). An unprivileged user cannot create the etf node or drive data through it.
  • The race window is narrow: the sender must read HK_INVALID==0, then ng_etf_disconnect must set HK_INVALID and private=NULL, then rcvdata must observe private==NULL β€” all between two specific instructions.

A simple ngctl-based loop (race.sh) did not fire in 200 iterations. Driving raw mbuf data into an etf match hook from userspace would require either an ng_socket data socket or a real ng_ether push path, which ngctl doesn't expose directly; a focused C PoC using socket(AF_NETGRAPH, SOCK_DGRAM, ...) with two parallel threads (producer + teardown) is the natural next iteration.

Why I stopped here

Even if the race fired: - It is a DoS (panic) only — NG_FREE_DATA NULLs both pointers, so there is no UAF or arbitrary-write primitive. The freed mbuf is gone, not dangling. - The path is root-only (netgraph setup). Root→kernel is game-over. - An unprivileged user cannot reach this code at all.

Per the realistic-threat test, this is a root→kernel DoS hardening gap, not an unprivileged escalation. The bug is unambiguous from the source (missing return after NG_FREE_DATA) and is fixed trivially.

Fix

fix.diff adds return (EINVAL); immediately after NG_FREE_DATA:

@@ -380,6 +380,7 @@

    if (NG_HOOK_PRIVATE(hook) == NULL) { /* Shouldn't happen but.. */
        NG_FREE_DATA(m, meta);
+       return (EINVAL);            /* DF-0696: missing bail-out */
    }

Matches the apparent original intent (free + bail with EINVAL, same as the short-packet branch at line 396).

Files

  • race.sh β€” ngctl-based tight connect/disconnect loop (control-msg only; did not fire in 200 iterations on the default kernel)
  • build.sh / run.sh β€” shell-only PoC wrappers
  • run.log β€” sample race attempt output (no panic)
  • VERDICT.md β€” this analysis
  • fix.diff β€” return(EINVAL) after NG_FREE_DATA

Fix verification

not_testable

compile validated

see evidence pack

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

Source-confirmed. ng_etf_rcvdata missing return after NG_FREE_DATA -> fallthrough m->m_len on NULL. Root-only race not won.