ng_callout() discards the previously-armed callout item (oitem) - re-arming a pending ng_callout leaks the item and permanently pins the node reference (unfreeable node, unloadable type)
Summary
ng_callout() reads oitem = callout_arg(c) and drops it on the floor before callout_reset() replaces the argument. If a prior ng_callout() item is still armed, it is orphaned: ng_uncallout() can no longer reach it, so the item plus the NG_NODE_REF/NG_HOOK_REF taken for it leak forever - the node never reaches refs==0 and its type stays permanently unloadable (kldunload EBUSY). API trap for node types; no in-tree caller currently re-arms a still-pending callout (all re-arms occur inside the fired callback after the item was consumed) - hardening. The FreeBSD-style oitem reclaim was lost in the port. Fix: commit the orphaned item when replacing an armed callout (refs==0 caveat per DF-3474).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3475 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.4 KB | β raw | |
| VERDICT.md | β | 1.2 KB | β raw | |
| fix.diff | β | 737 B | view raw | |
| verdict.json | β | 1.4 KB | view raw | |
| manifest.json | β | 356 B | view raw |
DF-3475 β ng_callout() silently discards the previously-armed item (leak on re-arm while pending)
Where
sys/netgraph7/netgraph/ng_base.c:3247-3269 (ng_callout()):
oitem = callout_arg(c);
callout_reset(c, ticks, &ng_callout_trampoline, item);
return (0);
oitem is read and then dropped on the floor. If the callout was still
pending (armed by a previous ng_callout() and not yet fired), the old item
is orphaned: nothing owns it, ng_uncallout() can no longer reach it, and
the NG_NODE_REF/NG_HOOK_REF taken for it in ng_callout() are never
released.
Impact
- One leaked
struct ng_itemper re-arm-while-pending. - Worse: the leaked item pins a node reference β the node can never reach
refs==0, so
ng_unref_node()never frees it (M_NETGRAPH_NODE leak) and the node type'srefsnever returns to 1, making the type permanently unloadable (kldunloadreturns EBUSY forever).
Severity Info: requires a node type that re-arms the same struct callout
via ng_callout() while it is still pending (an API misuse, but one the API
silently accepts β FreeBSD's ng_callout historically freed/reused oitem,
and the read of oitem here shows the intent was to handle it). In-tree
users re-arm from inside the timeout callback (safe: item already consumed)
or on fresh callouts, so there is no known in-tree trigger; filed as a
hardening/robustness defect of the core API.
Fix (sketch, included in fix.diff)
oitem = callout_arg(c);
callout_reset(c, ticks, &ng_callout_trampoline, item);
+ if (oitem != NULL && oitem != item)
+ NG_FREE_ITEM(oitem); /* drop our orphaned reference */
with the same refs==0 caveat as DF-3474 (use ng_free_item() for a
never-sent item; a fired-but-not-yet-applied item must instead be left to
the queue/apply path β distinguishing those needs a flag, so the safe
minimal fix is to only free when the callout was still PENDING, which
callout_stop()-style return of callout_reset ... simplest correct check:
if (oitem != NULL && callout_pending item was replaced) β see fix.diff for
the conservative variant that only frees items that never entered the queue,
tracked via NGQF_FREE already being clear and refs==0).
PoC
Skipped (Info severity β no demonstrated security impact; per audit contract Low/Info hardening findings are not QEMU-verified). Evidence pack documents the code path only.
DF-3475 β VERDICT
status: untested / impact: none (hardening) / confidence: certain (code defect), speculative (exploitability)
Pure code-analysis finding: ng_callout() (ng_base.c:3247-3269) reads
oitem = callout_arg(c) and never uses it. A re-arm while the callout is
still pending orphans the old item together with its node reference β the
node becomes unfreeable and the type unloadable. No in-tree caller currently
re-arms a pending ng_callout (grep over sys/netgraph7: all re-arms happen
from inside the fired callback, where the old item was already consumed by
the apply path), so there is no reachable trigger today; it is an API trap,
not a live bug. Not verified on the guest (Info severity; skipped per
contract). fix.diff carries the conservative one-liner (free only when the
old item was never queued β refs==0 β mirroring the DF-3474 idiom).
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_testableConfirmed kernel references
Detail
Evidence (decisive lines)
['findings/poc/DF-3475/README.md', 'findings/poc/DF-3475/VERDICT.md', 'findings/poc/DF-3475/fix.diff']
PoC changes
n/a - analysis-only finding
Verified recommended fix
Free/commit the previously armed oitem in ng_callout() (see fix.diff).
Verdict
ng_callout() (ng_base.c:3247-3269) reads callout_arg(c) into oitem and drops it; re-arming a still-pending ng_callout orphans the prior item together with its NG_NODE_REF (node never freed, type never unloadable, M_NETGRAPH item leak). Certain code defect by inspection; no in-tree caller currently re-arms a pending ng_callout (all re-arms occur inside the fired callback after the item was consumed), so no reachable trigger exists today - filed as Info hardening. Phase V skipped per contract (Info severity).
No comments yet.