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

Callout lifetime: ng_uncallout non-synchronizing + nglmi_shutdown omits ng_uncallout β€” ng7 twin of DF-0557

Summary

ng7 twin of DF-0557 (slightly worse). (1) nglmi_disconnect(:1079) ng_uncallout -> callout_stop(ng_base.c:3281) non-synchronizing. LMI_ticker self-reschedules(:270/:279) -> new callout after uncallout -> ng_rmnode_self(:1083) -> nglmi_shutdown frees sc(:1060) -> ticker derefs freed/NULL NG_NODE_PRIVATE. (2) nglmi_shutdown(:1053-1062) does NOT call ng_uncallout at all β€” unconditionally frees sc with NO timer cleanup. NGM_SHUTDOWN or forced rmnode bypasses disconnect -> timer still armed -> fires into freed memory. Fix: ng_uncallout at top of shutdown before private=NULL+kfree; consider callout_stop_sync in ng_uncallout.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0565 Β· 9 files
FileTypeDescriptionSize
df0565.c trigger-source race-test for shipped ng_lmi non-sync callout_stop (milder variant) 3.5 KB view raw
fix.diff suggested-fix ng_uncallout + callout_drain at top of nglmi_shutdown 815 B view raw
build.sh build-script cc -O2 -o df0565 df0565.c 122 B view raw
run.sh run-script kldload + run race-test 393 B view raw
build.log build-log patched ng7_lmi.ko build, full output 7.0 KB view raw
VERDICT.md verdict full narrative + shipped-twin observation 7.7 KB ↓ raw
env.txt environment guest uname, ng7 unshipped status 1007 B 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
VERDICT.md verdict full narrative + shipped-twin observation
↓ download raw

DF-0565 β€” ng7_lmi callout lifetime UAF (twin of DF-0557)

Verdict

NOT REPRODUCED LIVE β€” bug confirmed in source. The targeted code is in sys/netgraph7/lmi/ng_lmi.c, which is not shipped on the default GENERIC kernel (netgraph7 is opt-in via options NETGRAPH7, sys/conf/options:284). The bug pattern is genuine; the module just isn't reachable on the audit guest.

The shipped ng_lmi.ko (built from sys/netgraph/lmi/ng_lmi.c) has a related but milder form of the bug (non-synchronizing callout_stop in nglmi_disconnect); see "Related shipped-code observation" below.

Mechanism (cited path:line, confirmed by trace)

The targeted ng7_lmi code at sys/netgraph7/lmi/ng_lmi.c:

1049: /*
1050:  * Do local shutdown processing..
1051:  * Cut any remaining links and free our local resources.
1052:  */
1053: static int
1054: nglmi_shutdown(node_p node)
1055: {
1056:     const sc_p sc = NG_NODE_PRIVATE(node);
1057:
1058:     NG_NODE_SET_PRIVATE(node, NULL);
1059:     NG_NODE_UNREF(sc->node);
1060:     kfree(sc, M_NETGRAPH);           <-- freed without stopping the callout
1061:     return (0);
1062: }

Compare with LMI_ticker at :264-282, which self-reschedules via ng_callout(&sc->handle, node, ..., LMI_ticker, ..., 0) at :270/279 whenever the node is alive. When NGM_SHUTDOWN (or a forced rmnode) arrives, the generic netgraph shutdown dispatcher invokes the type's shutdown handler = nglmi_shutdown, which:

  1. Sets node->private = NULL (so NG_NODE_PRIVATE(node) returns NULL).
  2. Unrefs the node (drops one reference; may not be the last).
  3. kfree(sc, M_NETGRAPH) β€” frees the softc without stopping sc->handle (the callout).

The callout is still armed. The next time it fires, LMI_ticker runs:

264: LMI_ticker(node_p node, hook_p hook, void *arg1, int arg2)
265: {
266:     sc_p sc = NG_NODE_PRIVATE(node);   <-- NULL or freed
267:
268:     if (sc->flags & SCF_AUTO) {        <-- deref freed/NULL sc
  • If NG_NODE_PRIVATE(node) is NULL (the path set it to NULL): NULL-deref panic at sc->flags access.
  • If sc is freed but the slab is reused (kfree returns memory to the slab pool; if reallocated for another type, we get type confusion): UAF with attacker-controlled content.

The path is deterministic (no race needed) for the basic NULL-deref case: any shutdown of an ng7_lmi node whose timer is armed triggers it.

nglmi_disconnect at :1069-1085 (the hook-disconnect path) DOES call ng_uncallout(&sc->handle, sc->node) at :1079, but ng_uncallout just calls callout_stop (sys/netgraph7/netgraph/ng_base.c:3273-3281), which is non-synchronizing β€” if LMI_ticker is currently executing, callout_stop returns without waiting. LMI_ticker then reschedules, ng_rmnode_self runs, nglmi_shutdown frees sc, next firing β†’ UAF.

Why not testable on this guest

  • The ng7 stack (netgraph7/netgraph/ng_base.c and friends) is opt-in via options NETGRAPH7 in the kernel config. The default X86_64_GENERIC does NOT enable it.
  • The ng7_lmi module is NOT shipped in /boot/kernel/. The shipped ng_lmi.ko is the OLDER sys/netgraph/lmi/ng_lmi.c.
  • The two files share the same Makefile target name (ng_lmi.ko), so they cannot coexist; only the older one ships.

Standalone make in /usr/src/sys/netgraph7/lmi succeeds (the file is valid kernel C), but loading the resulting ng_lmi.ko requires the netgraph7 base, which is also not shipped.

The shipped sys/netgraph/lmi/ng_lmi.c has a related-but-milder race:

1063: nglmi_rmnode(node_p node) {              <-- shutdown handler
1065:     const sc_p sc = node->private;
1067:     node->flags |= NG_INVALID;
1068:     ng_cutlinks(node);                   <-- tears down hooks
1069:     ng_unname(node);
1070:     node->private = NULL;
1071:     ng_unref(sc->node);
1072:     kfree(sc, M_NETGRAPH);
1073: }

1081: nglmi_disconnect(hook_p hook) {
1090:     if (sc->flags & SCF_CONNECTED)
1091:         callout_stop(&sc->timeout);       <-- non-sync!
1094:     ng_rmnode(hook->node);
1095: }

nglmi_rmnode does not call callout_stop itself, but transitively calls nglmi_disconnect via ng_cutlinks β†’ ng_destroy_hook. The callout_stop at :1091 is non-synchronizing, so if LMI_ticker is currently running (inside crit_enter() at :277), it doesn't wait; the ticker reschedules at :280/289; kfree(sc) then frees the softc; the rescheduled callout fires into freed memory β†’ UAF.

I attempted to trigger this race with a 500-iteration create+destroy loop (df0565.c) on the shipped ng_lmi.ko β€” no panic in 60s. The race window is microseconds and LMI_ticker fires every 10s, so the per- iteration hit probability is roughly 1us / 10s β‰ˆ 1e-7; 500 iters gives ~5e4 odds against. Reproducing it uninstrumented would need ~1e7 iterations or a tighter LMI_ticker interval.

The fix for the shipped twin is the same pattern (callout_drain before kfree); I have NOT included that hunk in fix.diff because the finding is specifically against the ng7 file.

fix.diff adds an explicit ng_uncallout + callout_drain block at the top of nglmi_shutdown in sys/netgraph7/lmi/ng_lmi.c:1055:

if (sc->flags & SCF_CONNECTED) {
    ng_uncallout(&sc->handle, sc->node);
    callout_drain(&sc->handle);
}
  • ng_uncallout (callout_stop) cancels any pending firing.
  • callout_drain blocks until any currently-running LMI_ticker has finished, so the kfree below is safe.

Validated by: 1. patch -p4 < fix.diff β†’ Hunk #1 succeeded at 1055. 2. make in /usr/src/sys/netgraph7/lmi β†’ builds cleanly (/usr/obj/usr/src/sys/netgraph7/lmi/ng_lmi.ko, 15768 bytes).

Cannot load+test on the audit guest (ng7 base not shipped), so fix_status: not_testable.

The fix matches the intent of the finding proposal ("ng_uncallout at top of shutdown before private=NULL+kfree"). The proposal also suggests "consider callout_stop_sync in ng_uncallout" β€” that would be a broader change to sys/netgraph7/netgraph/ng_base.c:3273 (change callout_stop to callout_stop_sync or callout_drain), which I have NOT included because (a) it affects every netgraph7 node type, not just lmi, and (b) callout_drain from inside a callout's own context would deadlock, so a blanket change needs careful audit of every ng_uncallout caller. The local fix in nglmi_shutdown is sufficient to close DF-0565 itself.

Kernel references (verified by source trace)

Threat model

Local privilege-relevant UAF. The trigger (NGM_SHUTDOWN to an active ng7_lmi node) requires control-socket privilege (root, or an unprivileged user with admin-exported hook access). On a default install the path is dead code (ng7 not enabled). On a custom kernel with NETGRAPH7 enabled, the bug is deterministic for the NULL-deref case and probabilistic for the UAF case. No write primitive is immediately derivable, but a UAF into a reclaimed slab object is a textbook escalation primitive.

PoC

df0565.c hammers the shipped ng_lmi's non-sync callout_stop race (the milder shipped-code variant) β€” 500 iterations on the audit guest did not trigger. The cleaner ng7_lmi UAF (the actual finding) cannot be triggered on this guest because the module is not shipped.

Fix verification

not_testable

compile validated

see evidence pack

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

Source-confirmed. ng7_lmi shutdown no ng_uncallout before kfree -> callout fires into freed memory. netgraph7 not shipped. Shipped twin raced 500x no panic.