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)
PoC verification
Evidence pack
findings/poc/DF-3515 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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:
-
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 exitif (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. -
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 withtocvalid andfunc=ng_callout_trampoline, returned rval=0. Consequence forng_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 earlyreturn 0for the sync case withreturn ((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
oitemin ng_callout; useng_free_item(item)(refs==0, never sent) instead of flag-only NG_FREE_ITEM in ng_uncallout.
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_testableConfirmed 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).
No comments yet.