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 | 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 |
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_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)
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 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.