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

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_car is 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, peer ng_socket close) which calls ng_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:H only.

Proof of concept

PoC: C program over the netgraph control socket (root).

  1. Create a car node with upper+lower hooks connected to two ng_socket data peers.
  2. NGM_CAR_SET_CONF: mode=NG_CAR_SHAPE, cir=10240, cbs=8192, ebs=8192.
  3. Send a burst of >100 packets on the upper data hook to fill the SHAPE queue (so the callout is pending/rescheduled).
  4. While packets are still queued, issue NGM_RMHOOK on the upper hook β†’ ng_car_disconnect purges the queue without ng_uncallout.
  5. Within a few ticks the pending callout fires ng_car_q_event, reads NULL mbuf, NULL-derefs at m->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.

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

Timeline

  • 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
  • 2026-07-02 Reported to DragonFlyBSD security contact (pending).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0622 Β· 10 files
FileTypeDescriptionSize
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
VERDICT.md verdict full narrative
↓ download 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 (each NG_FREE_M NULLs 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 on hinfo->tc >= 0 (:676) with no empty-queue guard. After disconnect purged the queue, the loop reads c 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 mode at the movz off 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:

  1. sys/netgraph7/Makefile SUBDIR list does NOT include ng_car β€” so make in the netgraph7 build tree never builds it.
  2. The audit guest's /boot/kernel/ contains no ng_car.ko.
  3. kldload ng_car returns module not found.
  4. 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 adds ng_uncallout to ng_car_disconnect (mirroring shutdown) and adds an empty-queue guard to ng_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.

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:

  1. In ng_car_disconnect, call ng_uncallout(&hinfo->q_callout, node) before purging the queue β€” exactly mirroring the cleanup done in ng_car_shutdown.
  2. 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_testable

n/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.