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)
PoC verification
Evidence pack
findings/poc/DF-0565 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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:
- Sets
node->private = NULL(soNG_NODE_PRIVATE(node)returns NULL). - Unrefs the node (drops one reference; may not be the last).
kfree(sc, M_NETGRAPH)β frees the softc without stoppingsc->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 atsc->flagsaccess. - If
scis 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.cand friends) is opt-in viaoptions NETGRAPH7in the kernel config. The defaultX86_64_GENERICdoes NOT enable it. - The ng7_lmi module is NOT shipped in
/boot/kernel/. The shippedng_lmi.kois the OLDERsys/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.
Related shipped-code observation
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.
Recommended fix (validated compile-only)
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_drainblocks 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)
sys/netgraph7/lmi/ng_lmi.c:264-282βLMI_tickerself-reschedules.sys/netgraph7/lmi/ng_lmi.c:1053-1062βnglmi_shutdownfrees sc without stopping the callout.sys/netgraph7/lmi/ng_lmi.c:1069-1085βnglmi_disconnectuses non-syncng_uncallout.sys/netgraph7/netgraph/ng_base.c:3273-3281βng_uncallout=callout_stop(non-synchronizing).sys/kern/kern_timeout.c:1047βcallout_drain(synchronizing).sys/conf/options:284βNETGRAPH7opt-in.
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_testablecompile 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.
No comments yet.