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)
PoC verification
Evidence pack
findings/poc/DF-0696 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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_datacalls.
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).
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, thenng_etf_disconnectmust set HK_INVALID and private=NULL, thenrcvdatamust 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 wrappersrun.logβ sample race attempt output (no panic)VERDICT.mdβ this analysisfix.diffβ return(EINVAL) after NG_FREE_DATA
Fix verification
not_testablecompile 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.
No comments yet.