vkernel64 fuwordadd32()/swapu32() page-tail straddle guard copy-pasted from the 64-bit twins (sizeof(uint64_t)): a 32-bit word at page offset 0xFFC that fits in-page is spuriously failed with the -1 fault sentinel β umtx_sleep(2) futexes at page tail never sleep on matching values and sleep full timeouts on a phantom -1
Summary
The 32-bit user-atomics reject their own in-page addresses: the guard tests 4 < 8 and returns the -1 fault sentinel for a 4-byte word at offset 0xFFC even though it fits (casu32 in the same file correctly uses sizeof(u_int)). Via the only live caller umtx_sleep(2): inside a vkernel any 32-bit lockword in the last 4 bytes of a page is permanently seen as -1 - umtx_sleep with a matching value returns EBUSY instantly and with a non-matching value sleeps the full timeout on the phantom (lost wakeups, busy-spin, hangs). Reproduced with an identical static binary: native pc64 all-OK vs buggy vkernel BROKEN mask=3; sizeof(uint32_t) fix re-run all-OK. Over-strict only - no memory unsafety, vkernel-scoped DoS.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5538 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df5538.c | β | 3.7 KB | view raw | |
| build_image.sh | β | 1.1 KB | view raw | |
| run.sh | β | 218 B | view raw | |
| build.log | β | 933.8 KB | β download | |
| fix-build.log | β | 934.0 KB | β download | |
| run.log | β | 1.3 KB | view raw | |
| run.fixed.log | β | 1.3 KB | view raw | |
| env.txt | β | 224 B | view raw | |
| fix.diff | β | 947 B | view raw | |
| VERDICT.md | β | 6.6 KB | β raw | |
| verdict.json | β | 4.2 KB | view raw |
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):
umtx_sleep(w, v, t)with*w == vβ instant EBUSY instead of sleeping: the futex never sleeps on a matching word.umtx_sleep(w, -1, t)with*w != -1β sleeps the full timeout on the phantom-1and 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):
-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_vmspaceUAF (copyio.c:49,87,125,157,189,221,313,353 deref with no hold): killed βexec_new_vmspace()callskillalllwps(1)before replacingp_vmspace(kern_exec.c:922-926), andkilllwps()blocks untilp_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&busyand dobusy ? 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-specificpmap_enterat vm_fault.c:1455-1466) is always taken. - subword/cross-page copies:
copyin/copyoutpage-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_freeis a no-op. -1sentinel 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_pageon the user map cannot reach vkernel kernel KVA (no entries); growstack side effects match native fault semantics. copystron 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.
Fix verification
fixedBaseline vkernel: probe B off0xFFC instant EBUSY (0.000s) and probe C slept 0.300s on phantom -1 (BROKEN mask=3). After applying fix.diff to guest /usr/src, incremental make nativekernel KERNCONF=VKERNEL64 (NK_RC=0), exact same PoC: probe B sleeps the full 1.510s (EAGAIN after sleep = word read correctly), probe C instant EBUSY, RESULT OK mask=0, VKPROBE_RC=0. Bad behavior gone; no regressions in control probe A.
fix.diff; fix-build.log; run.fixed.log (post-fix console); run.log (baseline console)
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log (vkernel baseline: probe B off0xFFC EBUSY elapsed 0.000, probe C sleeps 0.300s on phantom -1, control A off0x000 OK; plus native pc64 reference run all-OK in build/run transcript), run.fixed.log (post-fix: B sleeps 1.510s, C instant EBUSY, VKPROBE_RC=0), fix-build.log (incremental rebuild NK_RC=0, copyio.o recompiled), fix.diff (two-line change), env.txt (guest uname + image md5)
PoC changes
Seed had no code: wrote df5538.c from scratch (three umtx_sleep probes: page-offset-0 control, page-tail 0xFFC matching-value, page-tail 0xFFC sentinel-value, classified by elapsed time because DF umtx_sleep returns EAGAIN after sleeping out its timeout). Initial probe classification wrongly required rc==0 for 'slept' β fixed to time-based classification after the native reference run revealed the EAGAIN-on-timeout convention. Run rig (build_kernel/image/run.sh) adapted from the proven DF-5337 vkernel rig.
Verified recommended fix
copyio.c:165 and :229: change the 32-bit ops' straddle guard from sizeof(uint64_t) to sizeof(uint32_t)
Verdict
REPRODUCED + FIX VALIDATED. sys/platform/vkernel64/platform/copyio.c:165 (swapu32) and :229 (fuwordadd32) use the 64-bit twins' page-straddle guard PAGE_SIZE - (p & PAGE_MASK) < sizeof(uint64_t) for 32-bit operations, so a 4-byte-aligned word at page offset 0xFFC (which fits entirely in-page) is spuriously rejected with the fault sentinel -1. Live via umtx_sleep(2) (kern_umtx.c:146/172/224 reads the word with fuwordadd32): inside a vkernel, a page-tail futex NEVER sleeps on a matching value (instant EBUSY, verified elapsed=0.000s vs control 1.460s at offset 0) and sleeps the FULL timeout on a phantom -1 that is not the word's value (verified 0.300s sleep with word=0x41). Native pc64 reference with the identical binary: all probes correct. No memory unsafety (guard is over-strict, never under-strict); impact ceiling is broken userland synchronization inside a vkernel (lost wakeups / busy-fail / phantom sleeps). Applying fix.diff (sizeof(uint64_t) -> sizeof(uint32_t) at both sites), rebuilding VKERNEL64 and re-running the exact PoC: all probes pass (B sleeps 1.510s, C instant EBUSY, RESULT OK mask=0).
No comments yet.