# DF-2737 verdict — dead `oid_running` drain (Info / defense-in-depth)

## Classification

- Finding: Info, confidence **certain** (static proof), no demonstrated
  impact today — hence no standalone exploit PoC (Phase V not warranted
  for an Info finding; the prober/race-hammer runs here are the
  negative-result record for the whole file audit).
- PoC machine status: `untested` (nothing to reproduce; this is dead-code
  hardening, not a triggerable bug).

## Why certain (static)

`rg -n oid_running sys/` over the entire kernel tree returns exactly:

- `sys/sys/sysctl.h:168` — struct field declaration
- `sys/kern/kern_sysctl.c:391` — `while (oidp->oid_running > 0)`
- `sys/kern/kern_sysctl.c:393/395` — tsleep on `&oidp->oid_running`

No increment, no decrement, no wakeup counterpart anywhere. Dynamic oids
are allocated with `M_ZERO` (kern_sysctl.c:463-464), so `oid_running`
stays 0 for the oid's entire life; the drain loop body never executes;
`CTLFLAG_DYING` (kern_sysctl.c:392) is never set; the
`KASSERT((oid->oid_kind & CTLFLAG_DYING) == 0)` in `sysctl_find_oid`
(kern_sysctl.c:1386-1387, 1395-1396) can therefore never fire.

## Why it is (currently) harmless

Reader/dispatcher sites hold the per-cpu shared topology lock across the
whole dispatch **including the handler**:
- `userland_sysctl` kern_sysctl.c:1569-1578 (`SYSCTL_SLOCK()` …
  `sysctl_root()` … `SYSCTL_SUNLOCK()`)
- `kernel_sysctl` kern_sysctl.c:1274-1276 (same pattern)

Teardown sites take the exclusive topology lock over ALL cpus
(`_sysctl_xlock`, kern_sysctl.c:1641-1651) before unlink/free
(`sysctl_remove_oid_locked` kern_sysctl.c:336-408, `sysctl_ctx_free`
kern_sysctl.c:213-261, `sysctl_unregister_oid` …). `lockinit(…,
LK_CANRECURSE)` (sys/kern/lwkt_thread.c:340) makes the recursive
XLOCK inside those paths (e.g. kern_sysctl.c:383 → 173) legal.
Mutual exclusion between in-flight handlers and oid free is therefore
complete **as long as** every future dispatch keeps the SLOCK-across-handler
property — exactly the invariant the dead drain's comment claims to
guarantee independently. The danger is latent: a refactor that drops the
SLOCK across handlers (e.g. for latency), trusting the "drain", silently
converts module unload into a use-after-free of `struct sysctl_oid` and
the handler text.

Runtime corroboration on the INVARIANTS guest (see race_hammer.txt):
12 `kldload/kldunload coretemp` cycles (coretemp allocates/frees dynamic
oids via sysctl ctx) while an unprivileged user ran `sysctl -aN` full-tree
walks (4245 completed): no panic, guest up, `vmstat -m` sysctloid arena
fully drained afterwards.

## Fix

`fix.diff` (git-apply-able, `git apply --check` clean) does two things:

1. `sysctl_root`: `atomic_add_int(&oid->oid_running, 1)` around the
   handler invocation, `wakeup(&oid->oid_running)` on the 1→0 transition —
   makes the existing drain at kern_sysctl.c:391-398 actually function.
2. `sysctl_find_oid`: return `ENOENT` for `CTLFLAG_DYING` nodes instead of
   falling through — mandatory companion, because once the drain can run,
   it drops the XLOCK while sleeping, and a fresh reader could otherwise
   walk into a DYING node and hit the (now reachable) INVARIANTS KASSERT
   at kern_sysctl.c:1386 — a panic an unprivileged user could race for.

`fix_status: not_testable` — Info-severity finding; a full kernel rebuild
cycle for a defense-in-depth change with no runtime trigger was not
warranted under the single-tenant guest.

## Negative results established by the same run (for the file record)

1. `sysctl_old_user` (kern_sysctl.c:1332-1345) `oldlen - oldidx` size_t
   underflow after a truncated (ENOMEM) SYSCTL_OUT: an ignoring handler
   would copy out at `oldptr + oldidx` past the user's declared buffer —
   but `oldptr` is the *attacker's own* pointer in its own address space,
   so this is at worst a self-overwrite, not a kernel violation.
   In-tree handlers were scanned; every multi-OUT handler checks the
   first error (devstat, collect, cputimer, tcp_subr, in_pcb, rman,
   vm_machdep, acpi, clockmod, kern_proc). Prober truncation matrix
   (`run_user_prober.log`) confirms byte-exact clamping.
2. Exact-path `{0,4}`/`{0,5}` oidfmt/oiddescr queries make
   `sysctl_find_oid` read *uninitialized in-bounds* entries of the
   caller's `int name[CTL_MAXNAME]` stack array (kern_sysctl.c:1372-1374
   with arg2==0 via the node dispatch at 1477): no kernel data is
   disclosed (the stale ints are only used as a lookup key; prior-syscall
   stack spray leaves attacker-chosen values anyway). 64 sprayed
   iterations on the INVARIANTS kernel: no panic.
3. `sysctl_sysctl_next_ls` writes into `int newoid[CTL_MAXNAME]`
   (kern_sysctl.c:786) at recursion `level` — depth-bounded by tree
   depth; full `sysctl -aN` walks over the stock tree (4245 iterations)
   never exceeded it. Overflow needs a >12-deep handler-less dynamic
   chain, creatable only by privileged module code.
4. Write gating re-verified at runtime: unprivileged writes to
   `debug.sysctl` and `kern.hostname` → EPERM (kern_sysctl.c:1438-1450).
   The `CTLFLAG_ANYBODY` write-bypass population (name2oid query-only;
   kern.proc.args/cwd handler-gated in kern_proc.c:1897-1903; psm knobs
   hardware-local) contains no unguarded kernel-state writer.
5. Reader-gating model: the dispatcher gates *writes only*
   (SYSCAP_NOSYSCTL_WR, kern_sysctl.c:1446-1449); reads are unconfined
   with zero jail/PRISON visibility filtering — sensitivity is a
   per-handler responsibility (per audit instructions this known model is
   not re-reported; DF-2684 & friends are the handler-side instances).
6. Mid-handler CPU migration vs `mycpu`-based `SYSCTL_SLOCK/SUNLOCK`
   (release-on-wrong-cpu → shared-count theft, kern_lock.c:886+):
   not demonstrable — LWKT threads are home-cpu scheduled while blocked
   in-kernel; dfly usched migration happens at the user boundary. Left as
   a design observation, no trigger found.
7. `sysctl_ctx_free` double-entry double-free (same oid appearing twice
   in one ctx → second `sysctl_remove_oid_locked` on freed memory,
   kern_sysctl.c:249-258): requires a module adding the same node name
   twice to one ctx (sysctl_add_oid:448-456 happily ctx-adds an existing
   node). No in-tree instance; module-bug-dependent → recorded, not filed.
