# DF-2788 — callout_stop/cancel/drain/stop_async return 0 after dequeuing a pending callout

## What

`_callout_cancel_or_stop()` (sys/kern/kern_timeout.c:859-930) processes a
STOP/CANCEL request synchronously via `_callout_update_spinlocked()`
(:881). When the callout was queued (CALLOUT_SET) and not executing, the
update dequeues it from the callwheel and sets CALLOUT_PREVENTED **on the
backend `_callout`** (kern_timeout.c:229 for CANCEL, :276 for STOP).
Immediately after, the fast path at **kern_timeout.c:888-891**

```c
if (sync == 0 || (c->flags & (CALLOUT_INPROG | CALLOUT_SET)) == 0) {
        exis_drop_gd(gd);
        return 0;                      /* <-- unconditionally */
}
```

sees `(INPROG|SET) == 0` (we just cleared SET by dequeuing) and returns
**0**, never reading the PREVENTED bit it just caused to be set.  The
near-identical recheck a few lines later (:895-901) shows the intended
semantics — `return ((c->flags & CALLOUT_PREVENTED) != 0)` — but is only
reachable if the flags changed between :888 and :895 (a race window a few
instructions wide), i.e. it is effectively dead code.

Consequently, on a stock kernel `callout_stop()`, `callout_stop_async()`,
`callout_cancel()` and `callout_drain()` return 0 for the *common case*
"this call removed a pending callout from the wheel", violating the
documented contract at kern_timeout.c:1052-1058, :1027-1030, :1080-1094
("Returns whether the STOP operation was responsible for removing a queued
or pending callout" / "Returns 1 if the cancel/drain is responsible for
stopping the callout").

This is the *second half* of the broken-return family; DF-0048 (known,
not re-reported here) is the other half: on the INPROG-completion path the
handler records PREVENTED on `c->verifier->flags` (the frontend
`struct callout`, :600-601, :623-624) while the reader looks at the
backend `c->flags`.  **Fixing DF-0048 alone does not restore correct
returns** — the :888 fast path still swallows the dequeue case fixed by
this finding's patch (proven by the patched-kernel run below, which fixes
only this half and flips all four returns to 1).

## Impact

Correctness / API-contract, Low.  Return-value-checking callers take the
wrong branch.  In-tree example: sys/netgraph/pptpgre/ng_pptpgre.c:786/889
`if (callout_stop(&a->rackTimer)) { kfree(a->rackTimerPtr, ...);
ng_unref(node); }` — the free/unref arm is dead code on a stock kernel
(reference + memory leak while timers were armed; the timeout callback
still frees its own arg, so this direction is a leak, not a UAF).
No false "1" is possible: PREVENTED-on-backend is only set at dequeue
time under both `c->spin` and `wheel->spin` with INPROG clear, so a
return of 1 can never coincide with a queued or executing callback — no
firing-after-free primitive exists in either half of the family.

## Reproduce (guest: DragonFly 6.5-DEVELOPMENT, X86_64_GENERIC, INVARIANTS)

```
scp df2788.c Makefile build.sh run.sh dfbsd:/root/df2788/
ssh dfbsd: cd /root/df2788 && sh build.sh    # KLD, -Werror clean
ssh dfbsd: cd /root/df2788 && sh run.sh      # kldload + sysctl -n debug.df2788=1
```

Expected (stock kernel, bug): dmesg

```
DF2788 A stop:      pending 1 -> 0, ret=0 (contract: 1)
DF2788 B stop_async: pending 1 -> 0, ret=0 (contract: 1)
DF2788 C cancel:    pending 1 -> 0, ret=0 (contract: 1)
DF2788 D drain:     pending 1 -> 0, ret=0 (contract: 1)
DF2788 E control:   fired=1, ret=0 (contract: 0)
DF2788 RESULT: stop=0 async=0 cancel=0 drain=0 ctl_fired=1 ctl_ret=0 BUG=1
```

(pending flips 1 -> 0 proves *this* call dequeued the callout; ret=0 is
the contract violation; control case E shows 0 is the correct answer only
when nothing was pending.)

Deterministic — no race, no special setup, root only (KLD).

## Fix validation

`fix.diff` (git-apply clean against sys/): the :888 fast path returns
`PREVENTED` from the backend for both sync and async when the request was
already processed (PREVENTED was cleared at :879, so it cannot be stale);
the async still-pending case keeps returning 0.

* Baseline (kernel #0, stock): BUG=1 (run.log, run.2.log)
* Patched  (kernel #1, `make -j6 nativekernel KERNCONF=X86_64_GENERIC` +
  installkernel + reboot, fix_build.log): BUG=0, all four ret=1 with
  pending 1->0, control ret=0 (run_patched.log)

## Files

df2788.c, Makefile — trigger module; build.sh / run.sh — exact commands;
build.log — full clean KLD build (-Werror, RC=0); run.log / run.2.log —
stock kernel runs; fix.diff — the fix; fix_build.log — patched kernel
build; run_patched.log — patched kernel re-run of the identical PoC.
