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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3475 Β· 5 files
FileTypeDescriptionSize
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_item per 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's refs never returns to 1, making the type permanently unloadable (kldunload returns 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.

VERDICT.md
↓ download raw

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_testable
↓ fix.diffper-fix-DF-3475

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