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

Unprivileged kernel panic: unvalidated non-canonical TLS base reaches wrmsr(MSR_FSBASE/MSR_KGSBASE) in set_user_TLS()

Summary

sys_set_tls_area validates only which and size - the 64-bit base is installed verbatim into pcb_fsbase/gsbase then wrmsr'd: WRMSR of a non-canonical FS.base/GS.base raises #GP(0) IN KERNEL MODE (SDM 17.17), a fatal trap inside the crit section. One syscall(472,0,{base=0x0000800000000000},16) as nobody deterministically panics the kernel (reproduced twice on fresh boots: 'Fatal trap 9 ... Stopped at set_user_TLS+0x68: wrmsr', RIP confirmed by objdump). The which=1 variant identical; vmspace_ctl reaches the same sink with a crafted vextframe because pc64 cpu_sanitize_tls is a no-op. 100% reliable unpriv local DoS, default config, no setup. canonicality-check fix validated on a rebuilt kernel (both variants return EINVAL, boundary matrix passes, normal TLS unregressed); cpu_sanitize_tls hardened for the vkernel path.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5639 Β· 14 files
FileTypeDescriptionSize
tls_noncanon.c β€” 1.6 KB view raw
tls_edge.c β€” 8.0 KB view raw
build.sh β€” 102 B view raw
run.sh β€” 114 B view raw
build.log β€” 1.2 KB view raw
run.log β€” 848 B view raw
run.2.log β€” 963 B view raw
run.3.log β€” 2.0 KB view raw
panic.txt β€” 1.1 KB view raw
env.txt β€” 737 B view raw
fix.diff β€” 1.5 KB view raw
verdict.json β€” 5.5 KB view raw
VERDICT.md β€” 5.1 KB ↓ raw
README.md β€” 3.6 KB ↓ raw

DF-5639 β€” Unprivileged kernel panic via non-canonical TLS base in sys_set_tls_area(2) / set_user_TLS()

VERDICT: reproduced (impact: panic) β€” deterministic, single syscall, unprivileged (tested as uid 65534 "nobody").

WHAT HAPPENS

sys_set_tls_area (sys/platform/pc64/x86_64/tls.c:70-110) copies a user-supplied struct tls_info and performs exactly two validations: which in {0,1} (tls.c:83) and size >= -1 (tls.c:100). The 64-bit base field is never validated. tls.c:106 stores it into td_tls.info[i] and tls.c:107 calls set_user_TLS(), which writes

  td->td_pcb->pcb_fsbase = info[0].base;           (tls.c:163)
  ...
  wrmsr(MSR_FSBASE,  gd->gd_user_fs);              (tls.c:167)
  wrmsr(MSR_KGSBASE, gd->gd_user_gs);              (tls.c:171)

with the raw user value. WRMSR to MSR_FSBASE (0xc0000100) / MSR_KGSBASE (0xc0000102) raises #GP(0) when the value is not a canonical address (Intel SDM Vol. 3, 17.17 / MSR canonical-address requirement). The fault is taken in KERNEL mode, inside the crit_enter_quick() section (tls.c:162), and DragonFly treats it as a fatal trap:

  Fatal trap 9: general protection fault while in kernel mode
  current process        = <poc pid>
  current thread         = pri 6 (CRIT)
  Stopped at      set_user_TLS+0x68:  wrmsr

RIP 0xffffffff80bd4c48 = the first wrmsr (MSR_FSBASE, ecx=0xc0000100), confirmed by objdump -d of the guest /boot/kernel/kernel.

REPRODUCE (guest: DragonFly 6.5-DEVELOPMENT x86_64, QEMU/KVM on Intel)

build.sh: cc -O1 -o tls_noncanon tls_noncanon.c run.sh (as any unprivileged user, e.g. nobody): su -m nobody -c /tmp/tls_noncanon expected: console: "Fatal trap 9: general protection fault while in kernel mode" ... "Stopped at set_user_TLS+0x68: wrmsr"; system halts in DDB; ssh dies.

First hit during the pass-2 boundary-matrix run (tls_edge.c), then reproduced minimally 1x1 with tls_noncanon.c on a fresh vm.sh reset with-src boot: panic, current process = the PoC pid.

VARIANTS

  • which=1 (gsbase, tls.c:171 wrmsr MSR_KGSBASE) is equally unvalidated β€” same #GP. Primary PoC uses which=0.
  • vmspace_ctl(VMSPACE_CTL_RUN) reaches the same sink with a user-supplied vextframe: vm/vm_vmspace.c:296-307 copies vx_tls into td_tls and calls set_user_TLS(); platform/pc64/x86_64/machdep.c:924 cpu_sanitize_tls() is a NO-OP ("we don't have to do anything"). The fix below covers both entry paths. (vkernel_enable gate applies to this variant only; syscall 472 is unconditional.)

WHY NOT ROOT

#GP is a fault, not a controlled write: there is no corruption primitive, the trap is fatal (DDB/reboot). Ceiling = reliable unprivileged local DoS (kernel panic). No escalation path identified; the wrmsr operands (ecx=MSR index, edx:eax=the base) are otherwise fixed by the code path.

FIX VALIDATION (see VERDICT.md tail, fix logs)

fix.diff adds canonicality validation of info.base in sys_set_tls_area (tls.c) and makes pc64 cpu_sanitize_tls() reject non-canonical bases (machdep.c), closing the vmspace_ctl route. Patched kernel built in-guest, rebooted: baseline: panic at set_user_TLS+0x68 (fatal trap 9) patched : tls_noncanon returns -1/EINVAL, guest stays up, full tls_edge.c matrix passes, normal TLS (libc/rtld, threaded build) unaffected.

FILES

tls_noncanon.c minimal deterministic trigger (syscall 472) tls_edge.c full boundary matrix (discovered the bug) build.sh run.sh exact commands build.log run.log run.2.log panic.txt panic.matrix.txt env.txt fix.diff git-apply-able unified diff (both entry paths) manifest.json verdict.json

VERDICT.md
↓ download raw

DF-5639 β€” VERDICT: REPRODUCED (impact: panic)

Finding

sys_set_tls_area(2) (sys/platform/pc64/x86_64/tls.c:70-110) accepts an arbitrary 64-bit base from an unprivileged user β€” the only validations are which in {0,1} (tls.c:83-84) and size >= -1 (tls.c:100-101); the base is deliberately unvalidated ("For x86_64 we can only adjust FSBASE and GSBASE", tls.c:103-105). The value flows unmodified:

uap->info --copyin--> info (tls.c:93-96)
curthread->td_tls.info[i] = info          (tls.c:106)
set_user_TLS()                            (tls.c:107)
  td->td_pcb->pcb_fsbase = info[0].base   (tls.c:163)
  wrmsr(MSR_FSBASE,  gd->gd_user_fs)      (tls.c:167)   <-- #GP(0)
  wrmsr(MSR_KGSBASE, gd->gd_user_gs)      (tls.c:171)   <-- same, which=1

Intel SDM Vol.3 17.17 requires FS.base/GS.base to hold canonical addresses; WRMSR of a non-canonical value raises #GP(0). The fault is taken in KERNEL mode inside crit_enter_quick() (tls.c:162) β€” a fatal trap, not a recoverable signal:

Fatal trap 9: general protection fault while in kernel mode
current process = <poc pid>; current thread = pri 6 (CRIT)
Stopped at set_user_TLS+0x68: wrmsr

RIP 0xffffffff80bd4c48 identified against the guest's /boot/kernel/kernel: it is the FIRST wrmsr (mov $0xc0000100,%ecx = MSR_FSBASE). Guest: QEMU/KVM on GenuineIntel host, so the guest wrmsr executes natively and Intel's #GP check applies.

How it was found / reproduced

  1. Boundary matrix tls_edge.c as uid 65534 discovered the panic during the wild-base phase (console: current process 922, same RIP; the preceding "pid 898 (tls_edge) exited on signal 11" line was an earlier harness iteration dying in USER mode after the same wild base install).
  2. Minimal deterministic trigger tls_noncanon.c β€” a single syscall(472, 0, {base=0x0000800000000000, size=-1}, 16) β€” on a fresh vm.sh reset with-src boot as nobody: immediate Fatal trap 9 at set_user_TLS+0x68, current process = PoC pid (853), guest halts in DDB, ssh dead. panic.txt is the console capture.

Exploitability ceiling

GP is an uncontrolled fault: no write primitive, no register control

beyond the wrmsr operand the code path already fixes (ecx=MSR index, edx:eax=the base). The result is a 100%-reliable unprivileged local kernel panic (DoS). No route to uid=0 was identified; the trap handler is trap_fatal -> DDB/reboot, nothing resumable. Impact classified "panic".

  • which=1: wrmsr(MSR_KGSBASE) at tls.c:171 β€” identical #GP.
  • vmspace_ctl(VMSPACE_CTL_RUN) as used by virtual kernels: user-supplied vextframe.vx_tls is copied into td_tls (sys/vm/vm_vmspace.c:293-299) and installed by set_user_TLS() (vm_vmspace.c:304); the intended guard, cpu_sanitize_tls() (sys/platform/pc64/x86_64/machdep.c:924-928), is a no-op on pc64. Same panic without ever calling set_tls_area. (Requires vm.vkernel_enable=1; the syscall-472 route has no gate.)
  • Context-switch restore path (swtch.s:515-535) wrmsr's PCB_FSBASE/ PCB_GSBASE β€” post-fix these are always pre-validated values; the syscall-time validation is the single choke point that covers fork inheritance (vm_machdep.c:137,229) too.

Fix validation (mandatory for memory-safety/panic findings)

fix.diff (in this pack, git-apply-able; never applied to the read-only audit tree β€” applied only inside the guest /usr/src copy):

  1. tls.c: reject non-canonical info.base with EINVAL before storing td_tls (canonical iff ((base + 0x0000800000000000) >> 48) == 0).
  2. machdep.c cpu_sanitize_tls(): validate both savetls bases, closing the vmspace_ctl route (error path already restores saved TLS, vm_vmspace.c:302-306).

Patched kernel built in-guest (make nativekernel) and booted:

BASELINE (stock kernel): su -m nobody -c /tmp/tls_noncanon -> Fatal trap 9 GPF kernel mode, set_user_TLS+0x68 wrmsr, guest halted in DDB, ssh dead. (run.log / panic.txt) PATCHED (fix.diff kernel): su -m nobody -c /tmp/tls_noncanon -> syscall returns -1/EINVAL ("kernel rejected non-canonical base, errno=22"), guest stays up, no console messages. (run.2.log) full tls_edge.c matrix re-run as nobody: completes end-to-end, wild bases now EINVAL, all legitimate TLS behavior unchanged (round-trip, fork inheritance, exec reset, libc TLS of the freshly exec'd matrix binary itself). (run.3.log)

Conclusion: root cause confirmed at the exact cited lines; fix is minimal, complete over all writers of td_tls/pcb_fsbase/gsbase, and behavior-neutral for canonical bases.

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 kernel panics at set_user_TLS+0x68 (wrmsr) on the PoC; patched kernel returns -1/EINVAL for both fs and gs non-canonical bases, guest stays up, full boundary matrix passes, and all legitimate TLS semantics (round-trip, fork inheritance, exec reset, rtld TLS bootstrap of fresh binaries) are preserved.

run.2.log and run.3.log (patched runs, no panic; 'set fs noncanon rc=-1 errno=22'); build.log (patch application, nativekernel build completion NATIVEKERNEL_DONE/INSTALL_DONE, rebooted kernel uname #1: Oct 9 03:00:09); contrast run.log/panic.txt (baseline Fatal trap 9).
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Fri Oct 9 03:00:09 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

unprivileged user -> syscall(472, 0, {base=0x0000800000000000, size=-1}, 16) -> sys_set_tls_area stores td_tls.info[0] (tls.c:106) -> set_user_TLS writes pcb_fsbase (tls.c:163) -> wrmsr(MSR_FSBASE, non-canonical) (tls.c:167) -> #GP(0) in kernel mode inside crit section -> trap_fatal -> Fatal trap 9 -> kernel halts in DDB. Stop at panic: no memory-corruption primitive exists (#GP is a fault, not a controlled write), impact ceiling = permanent unprivileged local DoS of the whole system.

Evidence (decisive lines)

panic.txt (Fatal trap 9 console capture, current process = PoC pid, 'Stopped at set_user_TLS+0x68: wrmsr'); run.log (baseline PoC run: output stops at the syscall, ssh dead); run.2.log (patched kernel: same PoC returns -1/EINVAL, guest up, gs-variant also EINVAL); run.3.log (patched full tls_edge.c matrix, completes end-to-end with noncanon->EINVAL and all legitimate TLS behavior intact); fix.diff (two hunks: tls.c + machdep.c cpu_sanitize_tls); build.log (patch applied in-guest, nativekernel build, install, reboot); env.txt (uname, QEMU/KVM on Intel host).

PoC changes

No seed PoC existed (pass-2 discovery). tls_noncanon.c written from scratch as a minimal single-syscall trigger; syscall number taken from sys/sys/syscall.h (SYS_set_tls_area=472); output uses raw write(2) because libc errno is %fs-relative and dies once a wild base is installed (that quirk itself is what produced the first, user-mode SIGSEGV harness iteration).

Verified recommended fix

Validate canonicality of info.base in sys_set_tls_area (return EINVAL) and implement the same check for both savetls bases in pc64 cpu_sanitize_tls so the vmspace_ctl route is covered (see fix.diff).

Verdict

REPRODUCED (panic): sys_set_tls_area(2) performs no canonicality validation of the user-supplied 64-bit tls_info.base (sys/platform/pc64/x86_64/tls.c:100-108) and set_user_TLS() programs it directly into MSR_FSBASE/MSR_KGSBASE via wrmsr (tls.c:163-171). WRMSR with a non-canonical FS.base/GS.base raises #GP(0) in kernel mode (Intel SDM Vol.3 17.17), a fatal trap: any unprivileged user calling set_tls_area(0, {base=0x0000800000000000, size=-1}, 16) panics the kernel instantly ('Fatal trap 9 ... Stopped at set_user_TLS+0x68: wrmsr'; RIP 0xffffffff80bd4c48 = the MSR_FSBASE wrmsr, identified by objdump of the guest kernel). Deterministic single-syscall repro as uid 65534 on a fresh vm.sh reset boot; guest halted in DDB. The same unvalidated value is also installable through vmspace_ctl(VMSPACE_CTL_RUN) because pc64 cpu_sanitize_tls() (machdep.c:924-928) is a no-op. Ceiling is a reliable unprivileged local kernel panic (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 sys_set_tls_area + real validation in cpu_sanitize_tls) was built in-guest (make nativekernel), installed, rebooted: PoC now returns -1/EINVAL with the guest healthy, the full boundary matrix passes, and normal TLS (rtld of every freshly exec'd binary, round-trip, fork inheritance, exec reset) is unchanged.