NULL-pointer dereference panic: ng_car_disconnect does not cancel the pending SHAPE callout, ng_car_q_event dereferences purged queue
| Field | Value |
|---|---|
| ID | DF-0622 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-476 NULL Pointer Dereference |
| File | sys/netgraph7/ng_car.c |
| Lines | 553-580 (disconnect missing uncallout); 665-707 (q_event NULL deref) |
| Area | netgraph7 (CAR shaping queue lifecycle) |
| Confidence | certain |
| Discovered | 2026-07-02 |
| Reported | pending |
Summary
When a hook operating in NG_CAR_SHAPE mode has packets queued and a
queue-processing callout pending, disconnecting that hook purges the mbuf
queue (NULLing each slot) but never cancels the pending callout. When
the callout fires it enters ng_car_q_event, whose drain loop is gated
only on tc >= 0 with no check that the queue is non-empty, and
dereferences the now-NULL mbuf pointer at m->m_pkthdr.len, panicking the
kernel.
Root cause
ng_car_shutdown correctly cancels both callouts with ng_uncallout
(ng_car.c:539-540), but ng_car_disconnect (ng_car.c:553-580) does
NOT β it only purges the queue (lines 562-567) and clears hook refs. The
netgraph callout framework takes its own references on the node and hook,
so the pending callout item survives the disconnect and fires later.
The hookinfo and privdata are still alive (node still referenced), but the
queue has been emptied: the purge loop NG_FREE_M's each entry which
writes NULL back into the slot. ng_car_q_event (ng_car.c:665-707) then
runs:
673: ng_car_refillhook(hinfo); /* tc = cbs (>= 0) */
676: while (hinfo->tc >= 0) { /* enters loop β NO empty-queue guard */
679: m = hinfo->q[hinfo->q_first]; /* NULL β queue was purged */
680: NG_SEND_DATA_ONLY(error, hinfo->dest, m); /* benign with NULL m */
686: hinfo->q_first++; /* advances past q_last */
691: if (hinfo->q_first == hinfo->q_last) break; /* MISSED */
695: m = hinfo->q[hinfo->q_first]; /* another NULL slot */
699: hinfo->tc -= m->m_pkthdr.len; /* NULL DEREF β PANIC */
Threat model & preconditions
- Deterministic trigger (privileged): an operator issuing
NGM_RMHOOK(ngctl rmhook) on a shaping hook while it is congested panics the kernel β a kernel must not crash from a legitimate configuration operation. - Network-reachable variant (likely): in a real deployment
ng_caris wired between interface nodes (e.g.ng_ether β ng_car β ng_iface) for traffic shaping; an unauthenticated adjacent peer can induce a peer-hook teardown (PPP/PPPoE session close, carrier/link flap on the shaped interface, peerng_socketclose) which callsng_car_disconnect. Under congestion the SHAPE queue is non-empty and the callout is continuously rescheduled, so the disconnect-while-callout-pending window is wide. - Impact: hard kernel panic (local or adjacent-network DoS). No code
execution;
A:Honly.
Proof of concept
PoC: C program over the netgraph control socket (root).
- Create a
carnode with upper+lower hooks connected to twong_socketdata peers. NGM_CAR_SET_CONF: mode=NG_CAR_SHAPE, cir=10240, cbs=8192, ebs=8192.- Send a burst of >100 packets on the upper data hook to fill the SHAPE queue (so the callout is pending/rescheduled).
- While packets are still queued, issue
NGM_RMHOOKon the upper hook βng_car_disconnectpurges the queue withoutng_uncallout. - Within a few ticks the pending callout fires
ng_car_q_event, reads NULL mbuf, NULL-derefs atm->m_pkthdr.lenβFatal trap 12: page fault while in kernel mode.
Expected output
Kernel panic; dmesg shows faulting RIP inside ng_car_q_event at the
mov off the NULL m->m_pkthdr.len.
Recommended fix
Two coordinated changes (defense in depth):
(a) Cancel the callout on disconnect, mirroring shutdown:
--- a/sys/netgraph7/ng_car.c
+++ b/sys/netgraph7/ng_car.c
@@ -557,6 +557,11 @@ ng_car_disconnect(hook_p hook)
if (hinfo) {
+ /*
+ * Cancel any pending queue-processing callout before
+ * tearing down the queue state it depends on.
+ */
+ ng_uncallout(&hinfo->q_callout, node);
/* Purge queue if not empty. */
while (hinfo->q_first != hinfo->q_last) {
NG_FREE_M(hinfo->q[hinfo->q_first]);
(b) Guard the drain loop against an out-of-band-emptied queue:
@@ -672,6 +677,12 @@ ng_car_q_event(node_p node, hook_p hook, void *arg, int arg2)
/* Refill tokens for time we have slept. */
ng_car_refillhook(hinfo);
+ /*
+ * The queue may have been drained/purged out-of-band (e.g. hook
+ * disconnect) since this callout was scheduled; bail before touching mbufs.
+ */
+ if (hinfo->q_first == hinfo->q_last)
+ return;
+
/* If we have some tokens */
while (hinfo->tc >= 0) {
References
sys/netgraph7/ng_car.c:539-540βng_car_shutdowncorrectly callsng_uncallouton both callouts.sys/netgraph7/ng_car.c:553-580βng_car_disconnectmissingng_uncallout.sys/netgraph7/ng_car.c:665-707βng_car_q_event, the NULL-deref site.
Timeline
- 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
- 2026-07-02 Reported to DragonFlyBSD security contact (pending).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0622 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| args_overflow.c | trigger-source | C stub documenting the bug and the dead-code situation | 3.8 KB | view raw |
| ng_car_trigger.sh | trigger-source | ngctl sequence that would fire the panic on a kernel with a working ng_car module | 2.1 KB | view raw |
| build.sh | build-script | compiles the C stub | 175 B | view raw |
| run.sh | run-script | runs the stub; on a ported kernel, invokes ng_car_trigger.sh | 323 B | view raw |
| run.log | run-log | stub output | 431 B | view raw |
| env.txt | environment | uname, kern.version | 209 B | view raw |
| fix.diff | suggested-fix | ng_uncallout in disconnect + empty-queue guard in q_event | 1.1 KB | view raw |
| VERDICT.md | verdict | full narrative | 4.8 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-0622 β VERDICT
Verdict: NOT REPRODUCED β bug pattern is real, but the trigger path is DEAD CODE on this kernel.
Mechanism (the bug pattern IS real in source)
sys/netgraph7/ng_car.c is a FreeBSD-derived netgraph node type for
Committed Access Rate shaping. It is correctly identified by the finding
as having a NULL-pointer-deref panic window:
ng_car_shutdown(ng_car.c:539-540) cancels both pending callouts:c ng_uncallout(&priv->upper.q_callout, node); ng_uncallout(&priv->lower.q_callout, node);ng_car_disconnect(ng_car.c:553-580) does NOT β it only purges the queue (eachNG_FREE_MNULLs the mbuf slot at:563) and clears hook refs (:569-573).- The netgraph callout framework takes its own references on the node and
hook (
sys/netgraph7/netgraph/ng_base.c:3257-3262,ng_callout):c NG_NODE_REF(node); NGI_SET_NODE(item, node); if (hook) { NG_HOOK_REF(hook); NGI_SET_HOOK(item, hook); }so a pending callout item survives disconnect and later fires. - When the surviving callout fires
ng_car_q_event(ng_car.c:665-707), its drain loop is gated only onhinfo->tc >= 0(:676) with no empty-queue guard. After disconnect purged the queue, the loop readsc 679: m = hinfo->q[hinfo->q_first]; // NULL 695: m = hinfo->q[hinfo->q_first]; // another NULL 699: hinfo->tc -= m->m_pkthdr.len; // NULL DEREF -> panicβFatal trap 12: page fault while in kernel modeat themovzoff the NULL mbuf.
This bug-pattern analysis is correct.
Why it CANNOT be triggered on this kernel
sys/netgraph7/ng_car.c does not compile against the DragonFlyBSD
master kernel. It references three FreeBSD types/functions that do not
exist anywhere in sys/:
| Reference in ng_car.c | Exists in DragonFly sys/? |
|---|---|
struct bintime (line 62, 588) |
NO β grep -rln "struct bintime" sys/ returns ONLY sys/netgraph7/ng_car.c |
getbinuptime() (line 210, 592) |
NO β DragonFly has getnanouptime(struct timespec *) instead |
bintime_sub() (line 596) |
NO β no bintime arithmetic in this kernel |
Consequences:
sys/netgraph7/MakefileSUBDIR list does NOT includeng_carβ somakein the netgraph7 build tree never builds it.- The audit guest's
/boot/kernel/contains nong_car.ko. kldload ng_carreturnsmodule not found.- Attempting to build it standalone (from a copied Makefile) fails at
compile time:
ng_car.c:596:2: error: implicit declaration of function 'bintime_sub' ng_car.c:588:17: error: unused variable 'newt'
ng_car.c is therefore dead code on this kernel. The runtime trigger
path (ng_car_disconnect reachable via netgraph NGM_RMHOOK) is
unreachable. The valid hard blocker applies: the vulnerable code
path is dead/unreachable at runtime on this guest AND no harness can
exercise it without first porting ng_car.c to DragonFly.
PoC
The PoC directory contains:
args_overflow.cβ a C stub that documents the bug and explains the dead-code situation. (Originally a placeholder name from the empty PoC scaffold; renamed-in-place would have been nicer but the directory contents are clear.)ng_car_trigger.shβ the ngctl sequence that would trigger the panic on a kernel with a working ng_car module. It documents the trigger logic for whenever the module is ported.fix.diffβ git-apply-able fix that addsng_uncallouttong_car_disconnect(mirroring shutdown) and adds an empty-queue guard tong_car_q_event(defense in depth).
The PoC does NOT trigger a panic on this guest because the module cannot
be loaded. Honest classification: not_reproduced due to dead code.
Recommended fix
fix.diff makes two coordinated changes in sys/netgraph7/ng_car.c,
both of which are valid regardless of whether the module ever compiles
upstream:
- In
ng_car_disconnect, callng_uncallout(&hinfo->q_callout, node)before purging the queue β exactly mirroring the cleanup done inng_car_shutdown. - In
ng_car_q_event, add an empty-queue guard (if (hinfo->q_first == hinfo->q_last) return;) right after the refillhook call, so the drain loop is not entered on a queue that was purged out-of-band.
These are defense-in-depth. Either change alone closes the panic window; both together are belt-and-braces.
This matches the finding markdown's proposal (which suggested the same two changes).
Caveats
The netgraph7 Car module being unported dead code is itself an observation worth flagging upstream: either the file should be ported to DragonFly's time API (and the build Makefile updated to include it), or it should be removed. As-is, it is a footgun for anyone who tries to build it.
Fix verification
not_testablen/a
see evidence pack
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
Dead code. ng_car.c uses unported FreeBSD bintime/getbinuptime -> doesn't compile. Bug pattern real in source.
No comments yet.