# DF-5538 VERDICT — vkernel64 `fuwordadd32()`/`swapu32()` over-strict page-tail straddle guard

## Status: REPRODUCED (vkernel uaccess semantic divergence, userland-visible) — fix validated

## The bug

`sys/platform/vkernel64/platform/copyio.c`:

* `swapu32()` — guard at **copyio.c:165**: `if (PAGE_SIZE - ((vm_offset_t)p & PAGE_MASK) < sizeof(uint64_t)) return -1;`
* `fuwordadd32()` — guard at **copyio.c:229**: same, `sizeof(uint64_t)`

Both are **32-bit** operations. A 32-bit word at page offset `0xFFC` has
`PAGE_SIZE - 0xFFC == 4` bytes of room — it fits entirely in-page — but
`4 < 8` aborts with the fault sentinel `-1` even though the page is mapped
and writable. Compare the correct siblings in the same file:
`casu32` uses `sizeof(u_int)` (copyio.c:96); `swapu64`/`fuwordadd64`/`casu64`
use `sizeof(uint64_t)` for real 64-bit ops (copyio.c:58/133/197).

## Why it matters (live caller)

`umtx_sleep(2)` reads the user word with `fuwordadd32(uptr, 0)`
(sys/kern/kern_umtx.c:146, also :172, :224). Consequences inside a vkernel
for a 32-bit-aligned word in the last 4 bytes of a page (any mapping,
unprivileged, only requires the 4-byte alignment check at kern_umtx.c:127):

1. `umtx_sleep(w, v, t)` with `*w == v` → instant **EBUSY** instead of
   sleeping: the futex never sleeps on a matching word.
2. `umtx_sleep(w, -1, t)` with `*w != -1` → **sleeps the full timeout** on
   the phantom `-1` and returns (EAGAIN): lost wakeups, hangs.

On native pc64 the same word is read by a real `lock xaddl` + onfault and
works at offset 0xFFC (verified: native reference run, all probes OK). The
vkernel twin breaks the MI uaccess contract for page-tail 32-bit words.
No memory-unsafety: the guard is over-strict, never under-strict (verified
for all six guards in the file). `swapu32` is latent (no in-tree caller);
`fuwordadd64`/`swapu64`/`casu64`/`casu32` also have no in-tree callers —
only `fuwordadd32` is live, via umtx.

## How it was verified

Guest: DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC 2026 (X86_64_GENERIC).
`df5538.c` — one static binary, run in two environments:

* **Native reference (guest host kernel, pc64)**:
  `A off0x000 → OK(slept 1.491s)`, `B off0xFFC → OK(slept 1.500s)`,
  `C off0xFFC → OK(instant EBUSY)` — `RESULT: OK (mask=0)`.
* **vkernel baseline** (`/usr/obj/usr/src/sys/VKERNEL64/kernel.debug -m 512m
  -r /vkroot.img -n 2`, unprivileged root shell inside, binary from
  `/root/df5538`):
  `A off0x000 → OK(slept 1.460s)` — plumbing fine at page offset 0;
  `B off0xFFC → EBUSY elapsed 0.000` — kernel never saw the word;
  `C off0xFFC → slept 0.300s, EAGAIN` — slept on a value that isn't there;
  `RESULT: BROKEN (mask=3)`.

The 0.000s-vs-1.5s / 0.300s-vs-0.000s splits are three orders of magnitude
apart — deterministic, not timing noise. Full console in `run.log`
(baseline) and `run.fixed.log` (post-fix).

## Fix validation

`fix.diff` (authored after verification, applied only to the guest's
`/usr/src` copy; the audit tree's `sys/` is untouched — `git apply --check`
only on the host):

```diff
-copyio.c:165  if (PAGE_SIZE - ((vm_offset_t)p & PAGE_MASK) < sizeof(uint64_t))
+copyio.c:165  if (PAGE_SIZE - ((vm_offset_t)p & PAGE_MASK) < sizeof(uint32_t))
-copyio.c:229  if (PAGE_SIZE - ((vm_offset_t)p & PAGE_MASK) < sizeof(uint64_t))
+copyio.c:229  if (PAGE_SIZE - ((vm_offset_t)p & PAGE_MASK) < sizeof(uint32_t))
```

Incremental `make nativekernel KERNCONF=VKERNEL64` (fix-build.log,
NK_RC=0), exact same PoC re-run inside the patched vkernel:

`A → OK(slept 1.470s)`, `B off0xFFC → OK(slept 1.510s)` (word now read
correctly), `C off0xFFC → OK(instant EBUSY)` (phantom gone) —
`RESULT: OK (mask=0)`, `VKPROBE_RC=0`.

Baseline-bad behavior is GONE after the one-line-per-function fix.

## Negative results (classes hunted and killed in this pass-2)

* **exec/exit vs `curproc->p_vmspace` UAF** (copyio.c:49,87,125,157,189,221,313,353
  deref with no hold): killed — `exec_new_vmspace()` calls `killalllwps(1)`
  *before* replacing `p_vmspace` (kern_exec.c:922-926), and `killlwps()`
  blocks until `p_nthreads == 1` (kern_exit.c:280-281); `exit1()` likewise
  tears down only after every LWP is gone. A thread mid-`copyin()` must
  finish its syscall and exit first. No window.
* **fault-recovery/onfault chains**: vkernel64 has no onfault trampoline
  here; `vm_fault_page()` error paths return NULL+error → EFAULT/-1
  correctly at every call site (copyio.c:65-66,103-104,139-140,171-172,
  204-205,236-237,326-327,367-368).
* **busy-vs-hold page release contract**: matches vm_fault.c:1100-1107 /
  1498-1510 exactly — copyin passes busyp=NULL and always `vm_page_unhold`
  (copyio.c:325,338); copyout/casu*/swap*/fuwordadd* pass `&busy` and do
  `busy ? vm_page_wakeup : vm_page_unhold` (copyio.c:76-79,114-117,146-149,
  178-181,210-213,242-245,379-382). `pmap_fault_page_quick()` is a NULL
  stub in vkernel64 (pmap.c:703-708), so the formal path (and its COW
  handling incl. the vkernel-specific `pmap_enter` at vm_fault.c:1455-1466)
  is always taken.
* **subword/cross-page copies**: `copyin`/`copyout` page-loop math is exact
  (n = min(page-remainder, len)); `suword*`/`fuword*` inherit it and work
  cross-page. Only the cas/swap/fetchadd family over-rejects (this finding).
  No under-strict guard exists → no OOB.
* **under-strict guards**: none — all six guards reject ≥ the access size
  required.
* **lwbuf failure path**: lwbuf on x86-64/vkernel64 is a pure inline
  DMAP arithmetic wrapper (sys/cpu/x86_64/include/lwbuf.h:77-92) — cannot
  fail, cannot sleep; `lwbuf_free` is a no-op.
* **`-1` sentinel ambiguity** (fuword*/casu*/fubyte returning -1 for a real
  -1 word): inherited BSD-wide MI contract semantics, same on pc64 — not a
  finding.
* **kernel-address confusion**: `vm_fault_page` on the user map cannot
  reach vkernel kernel KVA (no entries); growstack side effects match
  native fault semantics.
* **`copystr` on user pointers**: kernel-only MI contract (same as pc64);
  caller misuse is the DF-3222 class, already filed against dirfs.

## Impact ceiling

Application-level malfunction inside a vkernel: futexes at page tail never
block correctly (busy-spin / lost sleep / phantom sleep). No kernel memory
corruption, no privilege boundary crossed (vkernel user → vkernel user).
Severity Low. Guest kernel unaffected (pc64 path correct — verified
natively).

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