# DF-5631 — VERDICT

**Status: untested** (legitimately untriggerable: the defective API has zero
in-tree callers; building a trigger would require modifying `sys/`, which the
audit forbids). Impact: none today; latent vkernel-panic / vkernel-process
heap UAF for the first future caller.

## The defect, traced line by line

### 1. Deterministic panic on natural teardown order (kqueue.c:157-169)

```c
157 kqueue_del(struct kqueue_info *info)
158 {
160     struct timespec ts = { 0, 0 };
163     KKASSERT(info->fd >= 0);
164     EV_SET(&kev, info->fd, EVFILT_READ, EV_DELETE, 0, 0, NULL);
165     if (kevent(KQueueFd, &kev, 1, NULL, 0, &ts) < 0)
166         panic("kevent: failed to delete descriptor %d", info->fd);
167     info->fd = -1;
168     kfree(info, M_DEVBUF);
```

Host-kqueue semantics (the vkernel calls the HOST kqueue via libc — kqueue.c:83,
117, 165, 188): an `EVFILT_READ` knote is detached when the watched file
description is last-closed. Any caller doing `close(fd)` before `kqueue_del()`
— the natural detach order — makes `kevent(EV_DELETE)` return `ENOENT`, and
kqueue.c:166 panics the entire vkernel. The return of `kevent()` is -1 on any
error including ENOENT; the code treats *every* failure as fatal.

### 2. UAF window: delete vs in-flight dispatch (kqueue.c:187-195 vs 168)

`kqueue_intr()` runs on ANY vkernel host thread:

- SIGIO is process-directed (`fcntl(KQueueFd, F_SETOWN, getpid())` +
  `O_ASYNC`, kqueue.c:84-87), so the host may deliver SIGIO to any thread with
  SIGIO unblocked — each vCPU host thread as it passes through
  `cpu_enable_intr()` windows (machintr.c:198+).
- `kqueuesig()` (exception.c:144-163) calls `kqueue_intr(NULL)` **directly in
  signal-handler context** when `td_critcount == 0`.
- `splz()` (machintr.c:154-157) defers into `kqueue_intr()` via RQF_KQUEUE on
  whatever CPU observed the flag.

There is no lock shared by `kqueue_intr()` and `kqueue_del()`. Timeline:

| Thread A (vCPU0, SIGIO ctx) | Thread B (vCPU1) |
|---|---|
| kqueue.c:188 `kevent()` copies out event `{ident=fd, udata=info}` | |
| | kqueue.c:164 `EV_DELETE` succeeds (knote exists; dequeue ≠ removal) |
| | kqueue.c:168 `kfree(info)` |
| kqueue.c:193 `info->func(info->data, frame)` — **call through freed `M_DEVBUF` object** | |

Because the vkernel disables host hardening for its own image but is a plain
userspace process to the host, this is a userspace heap UAF *of the kernel
process* — i.e., guest-controlled corruption of the vkernel itself, same class
the family tracks as memcorrupt/`base:hw`, but **only reachable once a caller
of kqueue_del exists** — none does today:

```
$ rg -n 'kqueue_del' sys/
sys/platform/vkernel64/include/md_var.h:133: void kqueue_del(struct kqueue_info *);
sys/platform/vkernel64/platform/kqueue.c:158: kqueue_del(struct kqueue_info *info)
```

The single live `kqueue_add` caller is console.c:116 (fd 0, once — the
`kqueue_console_info == NULL` guard is under `tp->t_token`, and `tp` is the
stable cached `dev->si_tty` from ttymalloc, sys/kern/tty.c:2841-2855), and
`vcons_close()` (console.c:127-139) never deletes. The knote and
`kqueue_console_info` live for the whole vkernel process — udata is always
valid on the live path, confirming (from the owner side) that DF-5537's
"concurrent-first-open double-add is benign" assessment holds.

### 3. EV_ADD re-registration orphaning (kqueue.c:105-120)

No duplicate-(fd,EVFILT_READ) guard: a second `kqueue_add` on the same fd
rebinds udata and leaks the old info; a subsequent `kqueue_del(old_info)`
would delete the NEW registration's knote. Latent API foot-gun only.

## Why untested, honestly

- No in-tree trigger: hazard 1 requires a `close(fd)`-then-`kqueue_del`
  caller, hazard 2 requires a `kqueue_del` racing `kqueue_intr` — no such
  code exists anywhere in `sys/` (verified by full-tree rg).
- A vkernel boot on the guest cannot exercise the path without patching
  `sys/`, which the audit contract forbids (read-only tree). Static line
  trace above is the evidence.

## Fix

`fix.diff` in this directory: tolerate ENOENT (fd already closed → knote
already auto-deleted), and serialize delete against in-flight dispatch with a
simple in-use counter drained before `kfree` (the vkernel process is
cooperatively threaded; a crit-section-free atomic counter suffices because
the dispatcher's kevent copy-out and the delete both run on vkernel host
threads).

## Cross-references

- DF-5537 (console.c) — double-add benign claim: **confirmed** from owner side.
- DF-5586/5587, DF-5607-5612 (exception/cothread/machintr) — SIGIO
  deliverability on %gs=0 cothreads belongs to the DF-5607 mask-discipline
  finding, not re-filed here.

## 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.
