# DF-2941 VERDICT — put-side `count==0` acceptance in `_sysref_put`

## Finding

`_sysref_put()` (`sys/kern/kern_sysref.c:297-360`) has no floor guard on
the put side. The branch

```c
} else if (count > -0x40000000) {          /* kern_sysref.c:329 */
	if (atomic_cmpset_int(&sr->refcnt, count, count - 1))
```

also accepts `count == 0`, so a double-release (a second `sysref_put()` for
a reference that was already dropped) silently decrements a fully-released
object `0 -> -1 -> -2 -> ...` with no structural rejection. The only
barrier is the debug-only `KKASSERT((sr->flags & SRF_PUTAWAY) == 0)` at
`kern_sysref.c:303` (and the same assert inlined in `sysref_put`,
`sys/sysref2.h:85`) — and that assert reads `sr->flags` non-atomically
with respect to the release sequence in the final branch:

```c
if (atomic_cmpset_int(&sr->refcnt, count, 0)) {   /* :346 count -> 0  */
	KKASSERT(sr->flags & SRF_ALLOCATED);
	sr->flags |= SRF_PUTAWAY;                 /* :348 flags RMW     */
```

A double-put racing into `_sysref_put` between `:346` and `:348` observes
`refcnt == 0` with `SRF_PUTAWAY` not yet visible, passes the entry assert,
and is accepted by the `:329` branch — **even on INVARIANTS kernels**.
After `:348` the assert does catch it, but only on debug builds; production
kernels compile `KKASSERT` out entirely and accept every double-release
silently.

## Reproduction (guest, stock INVARIANTS kernel #0)

`sysref_probe.c` defines a synthetic `sysref_class` (lockmgr-based
lock/unlock, terminate callback mirroring `devfs_cdev_terminate`'s
protocol: unlock, then drop the terminal ref) and drives the layer:

- **B1** — normal `sysref_alloc → sysref_activate → get → put → put`
  cycle completes to putaway (`refcnt=0`, `flags=0x6` =
  `SRF_ALLOCATED|SRF_PUTAWAY`). Run log:
  `run.log:4` "B1: normal cycle refcnt=0 flags=0006 term_count=1 (putaway)".
- **B2** — fault injection clearing `SRF_PUTAWAY` to emulate the exact
  state a racing put sees between `:346` and `:348`, then one extra
  `sysref_put()`. Observed:
  `run.log:6` "B2: layer ACCEPTED double-release: refcnt=-1 (walked 0 ->
  -1; `count > -0x40000000` branch, no floor guard)" — the underflow was
  accepted on the INVARIANTS kernel. State was then repaired by the probe.
- **B3** — a real post-putaway double-put (no injection). Observed:
  `panic: assertion "(sr->flags & SRF_PUTAWAY) == 0" failed in
  _sysref_put at /usr/src/sys/kern/kern_sysref.c:303` (`panic.txt`,
  backtrace `_sysref_put ← sysref_probe_modevent ← linker_load_module`).

## Impact assessment (honest ceiling)

- The layer offers **no structural defense** against double-releases; the
  debug-only assert is the whole barrier and is racy w.r.t. its own
  release sequence.
- Consequences of an accepted double-release on a production kernel: the
  object's refcount is driven into permanent negative limbo (the object
  has already terminated and been returned to the objcache magazine; the
  spurious negative count is only observable at the next
  `sysref_alloc()` of the chunk, where `KKASSERT(sr->refcnt == 0)`
  (`kern_sysref.c:163`) fires on debug builds and is overwritten silently
  on production builds). Reaching a *second* `objcache_put()` of the same
  object requires ~2^30 additional puts, so the realistic ceiling of a
  single double-release is silent state corruption / loss of detection,
  not an immediate double-free.
- **Attacker reachability: none found in-tree.** No syscall path performs
  an unpaired `sysref_put()`; the finding is that any future caller bug
  of that shape is silently swallowed by the layer on production builds
  instead of being caught at the earliest point. This is why the finding
  is filed as Info (hardening), severity consistent with the existing
  DF-0169 (get-side overflow guard absence).

## Kernel references

- `sys/kern/kern_sysref.c:305-336` — the accepting branch
- `sys/kern/kern_sysref.c:345-355` — final release; `:346` cmpset → `:348`
  non-atomic `flags |= SRF_PUTAWAY` window
- `sys/kern/kern_sysref.c:303` — the only guard (debug-only, racy)
- `sys/sys/sysref2.h:79-88` — inline `sysref_put` entry assert (same)
- `sys/kern/kern_sysref.c:163` — recycle-time detection point

## Fix

Reject `count == 0` loudly in `_sysref_put` (see `fix.diff`): a dedicated
branch for `count == 0` that panics on INVARIANTS and, on all builds,
does not decrement — this converts the silent underflow into a hard,
immediately-diagnosable failure at the point of misuse. Fix authored
post-verification; not build-validated (Info hardening finding; the fix
is assert/plumbing-level and the behavior change is the panic itself).
