β¬’ 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 116 B view raw
run.sh run-script kldload + run race-test 146 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 8.0 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_terminate in ng_uncallout" β€” that would be a broader change to sys/netgraph7/netgraph/ng_base.c:3273 (change callout_stop to callout_terminate 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 status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

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.