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

vkernel sys_set_tls_area accepts a non-canonical TLS base that crosses to the host kernel via go_user()/vmspace_ctl() and panics the HOST in wrmsr(MSR_KGSBASE) β€” unprivileged in-vkernel user β†’ host kernel panic (DF-5639 vkernel twin, boundary-crossing)

Summary

The vkernel validates which and size but never base: set_user_TLS is a NOP here so the stored td_tls aliases td_mtd.mtd_savevext.vx_tls, which go_user hands verbatim to the HOST's vmspace_ctl on every return to emulated user mode - the host's set_user_TLS executes wrmsr(MSR_FSBASE/KGSBASE) and a non-canonical base raises #GP(0) IN HOST KERNEL MODE. Verified: in-vkernel uid=65534 single syscall -> host 'Fatal trap 9 ... Stopped at set_user_TLS+0x94: wrmsr' (RIP objdump-attributed to MSR_KGSBASE), 2/2 deterministic - the vkernel containment boundary is the only separation and it is crossed. Pair with DF-5639's pc64 fixes to seal both sides. canonicality-check fix validated (both variants EINVAL, vkernel+host survive, rtld TLS unregressed).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5655 Β· 15 files
FileTypeDescriptionSize
vk_tls_noncanon.c β€” 2.7 KB view raw
as_nobody.c β€” 551 B view raw
vk_image.sh β€” 1.2 KB view raw
vk_run.sh β€” 1.9 KB view raw
fix.diff β€” 970 B view raw
run.log β€” 2.0 KB view raw
panic.txt β€” 701 B view raw
run.2.log β€” 856 B view raw
run.3.log β€” 856 B view raw
run.3.console.log β€” 2.1 KB view raw
build.log β€” 934.4 KB ↓ download
env.txt β€” 2.1 KB view raw
VERDICT.md β€” 6.6 KB ↓ raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 7.0 KB view raw
VERDICT.md
↓ download raw

DF-5655 β€” VERDICT

Status: reproduced (host-kernel panic, deterministic; 2/2 baseline fires). Impact: panic β€” an unprivileged user inside a vkernel panics the host kernel (the kernel running the vkernel process), with one syscall. Confidence: certain. Fix validated (vkernel rejects the value, everything survives).

The finding

sys/platform/vkernel64/x86_64/tls.c β€” the VKERNEL's sys_set_tls_area validates which (tls.c:79) and info.size (tls.c:96) but never validates info.base before storing it (tls.c:102). This is the vkernel twin of the pc64 gap filed as DF-5639, but the sink is different β€” and the threat model is worse: the vkernel's set_user_TLS() is a NOP (tls.c:142-145), so the poisoned base is not programmed locally; it is handed to the host kernel on the very next return to emulated user mode and programmed by the host's wrmsr:

  1. In-vkernel unprivileged user calls set_tls_area(1, {base:0x0000800000000000, size:-1}, 16) (SYS_set_tls_area = 472).
  2. The vkernel's sys_set_tls_area stores info into curthread->td_tls.info[1] (vkernel64 tls.c:102) and returns 0. td_tls is #defined as td_mach.mtd_savevext.vx_tls (vkernel64/include/thread.h:50) β€” the same memory that struct vextframe wraps (cpu/x86_64/include/vframe.h:44-47).
  3. Return to emulated user mode: go_user() calls vmspace_ctl(id, VMSPACE_CTL_RUN, tf, &curthread->td_savevext) (vkernel64 trap.c:1407-1408) β€” a host syscall made by the vkernel process.
  4. Host sys_vmspace_ctl copyins the vframe's vx_tls into the host thread's td_tls (sys/vm/vm_vmspace.c:293-295), calls cpu_sanitize_tls() β€” a no-op on pc64 (sys/platform/pc64/x86_64/machdep.c:924-927, "For x86_64 we don't have to do anything") β€” then set_user_TLS() (vm_vmspace.c:311), which executes wrmsr(MSR_KGSBASE, 0x0000800000000000) (pc64 tls.c:163-172).
  5. WRMSR with a non-canonical FS.base/GS.base raises #GP(0) in host kernel mode (Intel SDM Vol.3 17.17) β†’ Fatal trap 9 β†’ the host kernel halts in DDB. The vkernel's containment is fully defeated: code with no privilege on the host (any account inside the vkernel) kills the host kernel.

Proof (baseline, stock kernels)

  • Trigger: /bin/as_nobody /bin/vk_tls_noncanon 1 inside the vkernel (in-vkernel uid=gid=65534), poisoning GS (fs left valid).
  • Serial transcript (run.log / panic.txt): the DF5655: uid=65534 ... base=0x0000800000000000 marker is immediately followed by Fatal trap 9: general protection fault while in kernel mode and Stopped at set_user_TLS+0x94: wrmsr.
  • RIP attribution: objdump of the guest /boot/kernel/kernel places set_user_TLS+0x94 (0xffffffff80bd4c74) on the wrmsr preceded by mov $0xc0000102,%ecx β€” MSR_KGSBASE, i.e. pc64 tls.c:171, the emulated-user GS install. The FSBASE wrmsr (+0x68) was skipped because only GS was poisoned β€” proving the fault is the vframe-supplied base, not the vkernel's own rtld TLS.
  • 2/2 deterministic baseline fires (two separate boots); guest wedged in DDB both times, ssh dead, vm.sh status => down.
  • The fault is a fixed #GP β€” no write primitive, no resumable path β€” so the impact ceiling is a reliable host-kernel DoS from inside the virtualization boundary (no uid=0 route exists for this class).

Fix validation

fix.diff (this pack) adds the canonicality check to the vkernel's sys_set_tls_area (reject non-canonical info.base with EINVAL), so the value is stopped at the source, before any vmspace_ctl handoff:

  • Patched-vkernel run (run.2.log): identical trigger, identical unprivileged uid β†’ DF5655: RETURNED rc=22 (EINVAL), MARKER_AFTER_TRIGGER prints, vkernel stays alive, host stays alive (ssh fine).
  • FS variant (which=0) equally rejected (run.3.log).
  • Legit TLS regression: the fixed vkernel booted normally β€” every binary exec'd inside it (sh, vkinit, the static trigger) performs rtld TLS bootstrap through this same syscall with canonical bases.

The host-side counterpart (real validation in pc64 cpu_sanitize_tls, covering hostile/other vkernel binaries) is the second hunk of DF-5639's fix; the two fixes are complementary: DF-5639's seals the host boundary against any vkernel, this one stops the stock DragonFly vkernel from ever carrying an in-vkernel user's poison across it (and avoids the vkernel's own panic("vmspace_ctl failed") at trap.c:1427-1430 that would otherwise fire once hosts adopt DF-5639's fix).

Rig notes (DF-5474)

The stock VKERNEL64 platform does not link (known finding DF-5474: vkernel64 SYSTEM_LD drops ${ELDFLAGS}, so __build_id_start/end are unresolved). The DF-5474 defsym workaround was applied to the guest's /usr/src copy, identically in baseline and fixed phases; it does not touch this finding's code paths.

Negative results (checked and killed)

  • which/index bounds β€” i = uap->which; 0..1 else ERANGE (tls.c:78-80, 126-128); syscallarg(int) is signed; no truncation.
  • infosize β€” uap->infosize < 0 (tls.c:81-82, 129-130) is dead code (size_t, sysproto.h:1045,1050 β€” always false), but every copy is bounded by min(sizeof(info), infosize) = ≀16 bytes (tls.c:87-93, 134); infosize==0 copies nothing. No overflow, no uninit read: the partial-copy path pre-zeroes info (tls.c:88).
  • copyout leak β€” sys_get_tls_area copies td_tls.info[i], which is only ever user-set, fork-inherited (vkernel64 vm_machdep.c:182-183) or zero-allocated; no kernel pointers are reachable through it.
  • cothread / %gs=0 discipline (DF-5607 interplay) β€” this file never touches MSRs (set_user_TLS() is a NOP, tls.c:142-145) and the swtch.s call site for it is inside #if 0 (vkernel64 swtch.s:481-486); no clobber of cothread %gs is possible from this file.
  • vmspace_ctl TLS save/restore β€” host saves/restores the vkernel's kernel TLS around VMSPACE_CTL_RUN (vm_vmspace.c:288-290, 301-306, 884) and the error path restores both frame and TLS; the bug is solely the unvalidated input accepted by this file, not the save/restore.
  • Races β€” the td_tls store is same-thread; consumers (go_user on the same thread, fork copy under fork's serialization) cannot observe a torn 16-byte struct in a way this file controls.

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

fixed: baseline (stock vkernel) β€” in-vkernel uid-65534 trigger panics the host at set_user_TLS+0x94 (MSR_KGSBASE wrmsr), guest dead in DDB; patched vkernel (fix.diff, canonicality check in vkernel64 tls.c) β€” identical trigger returns rc=22/EINVAL for both gs and fs variants, vkernel stays up (MARKER_AFTER_TRIGGER), host stays up (ssh fine), and legitimate TLS (rtld bootstrap of every binary exec'd in the vkernel) is unaffected.

run.log + panic.txt (baseline panic) vs run.2.log / run.3.log (patched: 'DF5655: RETURNED rc=22', alive); build.log (patched VKERNEL64 build incl. tls.c recompile); env.txt (RIP->wrmsr attribution).
↓ fix.diffper-fix-DF-5655

Confirmed kernel references

Detail

Exploit chain

in-vkernel unprivileged user (uid 65534) -> syscall 472 set_tls_area(1, {base=0x0000800000000000, size=-1}, 16) -> vkernel stores td_tls.info[1] with no base validation (vkernel64 tls.c:102; set_user_TLS is a NOP, tls.c:142-145) -> syscall return: go_user() passes &td_savevext (== td_tls) to host vmspace_ctl(VMSPACE_CTL_RUN) (trap.c:1407-1408) -> host copyin vx_tls (vm_vmspace.c:293) -> cpu_sanitize_tls NOP (machdep.c:924) -> host set_user_TLS wrmsr(MSR_KGSBASE, non-canonical) (pc64 tls.c:171) -> #GP(0) in host kernel mode -> Fatal trap 9 -> host kernel dead in DDB. Stop at panic: no memory-corruption primitive (#GP is a fault, not a controlled write); impact ceiling = unprivileged in-vkernel -> host-kernel DoS, crossing the virtualization boundary.

Evidence (decisive lines)

panic.txt (verbatim serial capture: DF5655 uid=65534 marker then 'Fatal trap 9 ... Stopped at set_user_TLS+0x94: wrmsr'); run.log (baseline narrative + RIP attribution via objdump: +0x94 == wrmsr after mov $0xc0000102,%ecx = MSR_KGSBASE); run.2.log (fixed vkernel, gs variant: 'RETURNED rc=22' = EINVAL, MARKER_AFTER_TRIGGER prints, vkernel+host alive); run.3.log (fixed, fs variant: rc=22); env.txt (kernel uname, KVM/GenuineIntel, rig + DF-5474 build-workaround notes, sha256s); build.log (full VKERNEL64 nativekernel build incl. tls.c recompile with the fix).

PoC changes

No seed PoC existed (pass-2 discovery). Trigger written from scratch as a static in-vkernel binary using a raw syscall-472 stub (no libc TLS dependence, raw write(2) + manual hex so nothing reads %fs/%gs after the poisoning); as_nobody wrapper proves in-vkernel uid/gid 65534; rig (vkroot image + VKERNEL64 boot) adapted from the DF-5607/DF-5579 rigs, with the vkernel console tee'd onto the guest serial console so the unprivileged marker and the host panic land in one transcript. The stock VKERNEL64 does not link without the DF-5474 defsym workaround (applied to the guest's /usr/src only, identically in baseline and fixed phases).

Verified recommended fix

Reject non-canonical info.base in the vkernel's sys_set_tls_area with EINVAL (see fix.diff); pair with DF-5639's pc64 cpu_sanitize_tls validation to seal the host boundary against any vkernel binary.

Verdict

REPRODUCED (panic, 2/2 deterministic): the vkernel's sys_set_tls_area performs no canonicality validation of the user-supplied 64-bit tls_info.base (sys/platform/vkernel64/x86_64/tls.c:96-102 β€” only info.size < -1 is checked) and stores it into td_tls, which aliases td_mach.mtd_savevext.vx_tls (vkernel64/include/thread.h:50). On the syscall-return path go_user() hands &curthread->td_savevext verbatim to the HOST vmspace_ctl(VMSPACE_CTL_RUN) (vkernel64 trap.c:1407-1408); the host copies it in (vm/vm_vmspace.c:293-295), pc64 cpu_sanitize_tls() is a no-op (pc64 machdep.c:924-927), and host set_user_TLS() executes wrmsr(MSR_KGSBASE, 0x0000800000000000) (pc64 tls.c:163-172) -> #GP(0) in HOST kernel mode (Intel SDM Vol.3 17.17) -> 'Fatal trap 9 ... Stopped at set_user_TLS+0x94: wrmsr', host halts in DDB. RIP 0xffffffff80bd4c74 is the wrmsr after 'mov $0xc0000102,%ecx' (MSR_KGSBASE) per objdump of the guest kernel; the FSBASE wrmsr was skipped because the trigger poisoned only GS. Consequence: ANY unprivileged user INSIDE a vkernel (demonstrated uid=65534 in-vkernel) panics the HOST kernel with one syscall β€” the vkernel's containment is defeated; this is the vkernel twin of DF-5639 (pc64 direct syscall, RIP +0x68/FSBASE) reached through a different file and a different entry chain. Ceiling is a reliable host-kernel DoS: #GP is a fixed fault with no write primitive and no resumable path, so no uid=0 route exists. fix.diff (canonicality check in this file) was built into the vkernel in-guest and re-run: identical trigger now returns EINVAL (rc=22), vkernel and host stay alive, and legitimate TLS (rtld bootstrap of every exec, both fs and gs variants) is unaffected.