β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5538

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5538 Β· 11 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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):

  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):

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

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Baseline 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)
↓ fix.diffper-fix-DF-5538

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