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

kern_timeout _callout_cancel_or_stop returns 0 for inline-completed stops (violating the documented contract) -> netgraph7 ng_uncallout misreports every successful cancellation; ng_callout/ng_uncallout also leak callout items + node references (HCI command-complete machine permanently wedged)

Summary

Live-instrumented while verifying DF-3513: a 500-tick-armed callout with valid toc and func=ng_callout_trampoline returns rval=0 from callout_stop() - because _callout_update_spinlocked() completes the STOP inline and _callout_cancel_or_stop's early exit 'return 0' fires before the PREVENTED-reporting path. netgraph7's ng_uncallout (and every consumer distinguishing 0 from 1) is broken: demonstrated on ng_hci - complete_command's untimeout always returns ETIMEDOUT, so every Command_Complete arriving while the timeout is pending (the normal case) is silently dropped; the unit can NEVER initialize (bdaddr zero, NGM_HCI_NODE_INIT ENXIO forever) - the entire ng7 bluetooth stack is wedged on this kernel; same contract used by ng_l2cap/ng_car/ng_pptpgre/ng_l2tp timers. Additionally ng_callout leaks the overwritten callout item (oitem) and ng_uncallout frees the cancelled item with flag-only NG_FREE_ITEM - both pin ng_node references (DF-3514 feedstock). Observed with guest-local kprintf probes (ARM rv=0 / UNC rval=0 / state stuck 0x5, bdaddr 00:00:00:00:00:00, INIT ENXIO). ng7-side fix module-validated within DF-3514's 20-run test; kern_timeout.c fix authored (report PREVENTED in the inline-completed early exit) - needs kernel rebuild, not rebuilt (documented).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3515 Β· 6 files
FileTypeDescriptionSize
README.md β€” 3.6 KB ↓ raw
VERDICT.md β€” 1.9 KB ↓ raw
fix.diff β€” 866 B view raw
fix.diff.ng7 β€” 1.6 KB ↓ download
manifest.json β€” 842 B view raw
verdict.json β€” 2.1 KB view raw

DF-3515 β€” callout_stop() returns 0 for a synchronously-stopped armed callout; breaks netgraph7 ng_uncallout contract (HCI CC machine wedged; callout items+noderefs leak)

What

Two coupled defects:

  1. sys/kern/kern_timeout.c _callout_cancel_or_stop() (lines 859-901): when the STOP request is completed inline by _callout_update_spinlocked() (callout removed from the wheel, SET cleared, CALLOUT_PREVENTED set), execution falls into the early exit if (sync == 0 || (c->flags & (CALLOUT_INPROG | CALLOUT_SET)) == 0) return 0; β€” returning 0 even though OUR stop succeeded and PREVENTED is set. The documented contract (kern_timeout.c:1029-1044: "Returns 1 if the cancel is responsible for stopping the callout") is violated for the synchronous-stop case. callout_stop/cancel/drain are all affected.

  2. netgraph7's ng_uncallout() (sys/netgraph7/netgraph/ng_base.c:3273-3296) relies on that return value (rval = callout_stop(c); ... return rval), with consumers treating 0 as "not cancelled". Live-instrumented on the guest (kprintf probes): a callout armed for 500 ticks (default HCI command timeout), stopped 300ms in with toc valid and func=ng_callout_trampoline, returned rval=0. Consequence for ng_hci_command_untimeout() (ng_hci_misc.c:390-392): ETIMEDOUT for a successful cancellation -> complete_command() (ng_hci_cmds.c:368-369) silently drops every Command_Complete that arrives while the timeout is pending (the normal case) -> the HCI unit can NEVER initialize (Read_BD_ADDR CC dropped -> bdaddr stays 00:00:00:00:00:00 -> NGM_HCI_NODE_INIT returns ENXIO) -> the whole ng7 bluetooth stack is wedged on this kernel. Same contract is used by ng_l2cap RTX/ERTX, ng_car, ng_pptpgre, ng_l2tp timers.

Additionally, both callout paths leak in netgraph7: * ng_callout() (ng_base.c:3266-3268) reads oitem = callout_arg(c) and never frees it when overwriting a pending callout's item (item + its node/hook refs leak). * ng_uncallout() frees the cancelled item with flag-only NG_FREE_ITEM(item) (ng_base.c:3291) β€” the item and its node reference are never released on the rval>0 path; on the rval=0 path the item stays armed as the callout arg and callout_set_arg(c,NULL) orphans it. Both routes permanently pin ng_node references (feeds DF-3514's rehash panic threshold).

Evidence (guest, instrumented ng7 core + hci modules)

ARM: rv=0 ticks=500                <- ng_callout armed (default 5s timeout)
CC: state=0x5 q=1                  <- complete_command: PENDING set, cmd queued
CC: op=0x1009 qop=0x1009           <- opcode MATCHES
UNC: rval=0 toc=0xfffff800897a4f80 func=0xffffffff82603460   <- ARMED callout, callout_stop()==0
UT: rval=0                          <- command_untimeout => ETIMEDOUT, CC dropped
state = 0x5 forever; bdaddr stays 00:00:00:00:00:00

Reproduce

Same harness as DF-3513 (poc_leak handshake stage): the Read_BD_ADDR handshake never completes; GET_STATE shows COMMAND_PENDING (0x4) stuck indefinitely; GET_BDADDR returns zeros. (This is also why DF-3513's measurement needed a guest-local INITED bypass.)

Fixes

  • kern_timeout.c (needs kernel rebuild β€” authored, not rebuilt here): in _callout_cancel_or_stop, replace the early return 0 for the sync case with return ((c->flags & CALLOUT_PREVENTED) != 0); (the same expression the later path already uses).
  • ng7 side (module-level, VALIDATED as part of the 20-run survival test in DF-3514's pack): free oitem in ng_callout; use ng_free_item(item) (refs==0, never sent) instead of flag-only NG_FREE_ITEM in ng_uncallout.
VERDICT.md
↓ download raw

DF-3515 β€” VERDICT

Status: reproduced (impact: dos β€” netgraph7 timer subsystem contract broken; HCI initialization wedged; callout items/node refs leak). ng7-side fix validated (module); kern_timeout.c fix authored, NOT kernel-rebuilt (fix_status: not_testable for that half, documented).

Live proof on guest with instrumented modules: armed 500-tick callout, valid toc, func=trampoline, callout_stop() returns 0 (UNC probe); command completion then always takes the silent ETIMEDOUT path (complete_command, ng_hci_cmds.c:368) β€” reproduced on every run; unit state stuck at 0x5, bdaddr never set, NODE_INIT ENXIO forever.

Root cause in kern_timeout.c verified by code-reading: inline-completed STOP hits the (flags & (INPROG|SET)) == 0 early return with plain return 0, despite PREVENTED being set β€” contradicting the documented "returns 1 if the cancel is responsible" contract.

ng7-side leaks (ng_callout oitem; ng_uncallout flag-only free) are source-certain and were fixed + built + survived the 20-run stress (together with DF-3514's fix) β€” but their individual leak counts were not separately instrumented (the surviving-run criterion is absence of the DF-3514 panic; slow noderef pinning from the still-broken callout_stop return remains until the kernel-side fix lands, as expected and documented).

Classification: status=reproduced, reproduced=1, impact=dos, confidence=certain (behavior) / certain (kern_timeout root cause by trace + source), fix_status=fixed (ng7 half, module-validated) / not_testable (kern_timeout half, requires full kernel rebuild).

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-3515

Confirmed kernel references

Detail

Exploit chain

armed callout + cancel -> callout_stop()==0 -> ng_uncallout reports not-cancelled -> caller takes timeout path while callout is actually stopped -> state machines wedge + items/noderefs leak permanently

Evidence (decisive lines)

VERDICT.md: ARM/CC/UNC/UT kprintf probe output captured on guest (rval=0 with toc valid, func=trampoline); state probes 0x5 stuck; bdaddr zeros

PoC changes

kernel-side probes added to guest-local module copies (ARM/UNC/UT kprintf); no workspace changes

Verified recommended fix

kern_timeout: report PREVENTED in the inline-completed early exit; ng7: ng_free_item for callout items (oitem overwrite + cancelled item)

Verdict

kern_timeout's _callout_cancel_or_stop returns 0 when the STOP request is completed inline (SET cleared by _callout_update_spinlocked, PREVENTED set), violating the documented 'returns 1 if responsible' contract; netgraph7's ng_uncallout therefore reports failure for successful cancellations, so every ng7 timer consumer misbehaves β€” demonstrated live: HCI Read_BD_ADDR Command_Complete always dropped via the silent ETIMEDOUT path, unit wedged in COMMAND_PENDING forever, bluetooth stack cannot initialize. ng_callout additionally leaks the overwritten callout item (oitem) and ng_uncallout leaks the cancelled item via flag-only NG_FREE_ITEM; both pin ng_node references (DF-3514 feedstock).