# DF-5631 — kqueue_del() latent hazards: EV_DELETE-ENOENT panic + delete-vs-dispatch UAF

**File:** `sys/platform/vkernel64/platform/kqueue.c` (pass 2, GLM 5.3)
**Severity:** Low · **Bucket:** base:hw (vkernel platform) · **PoC status:** untested —
latent dead-code API hazard, **zero in-tree callers**; no trigger exists to build.

## What the defect is (code-level, deterministic)

`kqueue_del()` (sys/platform/vkernel64/platform/kqueue.c:157-169) has two hazards
that bite the moment any future caller uses the API in the natural way:

1. **ENOENT → panic (deterministic, close-before-delete ordering).**
   The host kqueue auto-detaches an `EVFILT_READ` knote when the watched fd's
   file description is closed. A caller that does the natural
   `close(fd); kqueue_del(info);` teardown gets `ENOENT` from `kevent()` at
   kqueue.c:165 and hits `panic("kevent: failed to delete descriptor %d")`
   at kqueue.c:166 — taking down the whole vkernel.

2. **UAF window vs kqueue_intr() (racy, multi-vCPU).**
   `kqueue_intr()` (kqueue.c:177-196) runs concurrently on any vCPU host
   thread: the SIGIO handler `kqueuesig()` calls it directly from signal
   context (sys/platform/vkernel64/x86_64/exception.c:144-158) and `splz()`
   re-enters it via RQF_KQUEUE (sys/platform/vkernel64/platform/machintr.c:154-157);
   SIGIO is process-directed (init_kqueue, kqueue.c:84-87). If thread A has
   already copied the event out at kqueue.c:188 with
   `udata = info`, and thread B executes `kqueue_del(info)` — EV_DELETE
   succeeds (the knote still exists; dequeue does not remove it) and
   `kfree(info)` at kqueue.c:168 frees the object — then thread A's
   `info->func(info->data, frame)` at kqueue.c:193 calls through freed
   memory. Use-after-free of a vkernel-process heap object (`M_DEVBUF`).

3. **EV_ADD re-registration orphaning (API foot-gun).**
   `kqueue_add()` (kqueue.c:105-120) performs no duplicate-(ident,filter)
   guard; a second `kqueue_add` on the same fd silently rebinds the knote's
   udata and orphans the old `info` (leak), after which a `kqueue_del` of the
   OLD info EV_DELETEs the knote now owned by the NEW registration.

## Why no PoC was built

`rg 'kqueue_del' sys/` — the only references are the definition
(kqueue.c:158) and the prototype (sys/platform/vkernel64/include/md_var.h:133).
**No code in the entire tree calls it.** The single live `kqueue_add` caller is
console.c:116 (fd 0, once, guarded by `kqueue_console_info == NULL` under
`tp->t_token`), and `vcons_close()` (console.c:127-139) never deletes — so
hazard 1 and 2 are unreachable today. Reproducing would require writing a new
kernel caller into `sys/`, which the audit contract forbids. Class: latent API
defect / hardening, filed so the first future caller (e.g. a vke detach path)
doesn't trip it.

## Cross-checks performed from the owner side (pass 2, checklist a–d)

- **DF-5537 "concurrent-first-open double-add is benign" — CONFIRMED.**
  `ttymalloc(&dev->si_tty)` caches the tty in the cdev under `tty_token`
  (sys/kern/tty.c:2841-2855), so concurrent `vcons_open` races serialize on
  the same `tp->t_token` (console.c:90) and the second opener sees
  `kqueue_console_info != NULL` (console.c:114-117). Single EV_ADD; no
  re-registration; no orphaned info.
- **kqueue fd lifecycle / ordering — sound.** `init_kqueue()` (init.c:449)
  runs after `cpu_disable_intr()` (init.c:443, blocks SIGIO — machintr.c:192-196)
  and before `init_exceptions()` (init.c:494) installs `kqueuesig`; no window
  where O_ASYNC SIGIO fires unhandled. `KQueueFd` is never closed in-tree.
- **kqueue_intr loop — safe.** `kevary[8]`, host `kevent()` returns ≤
  nevents; `n == -1` skips the dispatch loop and exits the do-while
  (kqueue.c:187-195); spurious wakeups (event already drained by another
  vCPU) yield n==0 and exit. Concurrent `kqueue_intr` on multiple vCPU
  threads is safe today because the only dispatch target,
  `vcons_hardintr` (console.c:203-209), is a leaf that does
  `signalintr(4)` → atomic bit-set deferral (machintr.c:169-186).
- **signalintr deferral — sound.** Both entry paths hold a critical section
  (kqueuesig: crit_enter_quick at exception.c:152; splz: machintr.c:130), so
  `signalintr()` always sees `td_critcount != 0` and defers via
  `gd_fpending` + `RQF_INTPEND` + `umtx_wakeup` (machintr.c:174-177),
  drained at machintr.c:144-148.
- **Not re-filed (family findings):** SIGIO deliverability on %gs=0 cothreads
  is the DF-5587/DF-5607 chain (cothread mask discipline), owned by
  cothread.c/exception.c. `init_kqueue`'s unchecked `kqueue()` return
  (kqueue.c:83, -1 under RLIMIT_NOFILE exhaustion → misleading panic at
  kqueue.c:85) fails loudly at boot and is recorded in negative notes only.

## Fix sketch

See `fix.diff` — tolerate `ENOENT` on EV_DELETE (knote already gone), and
close the UAF window by fencing deletes against in-flight dispatch.

## Reproduce

Nothing to run: latent dead-code hazard (no caller). Static verification:

    rg -n 'kqueue_del' sys/          # definition + prototype only
    sed -n '157,196p' sys/platform/vkernel64/platform/kqueue.c
