DragonFlyBSD Kernel Audit
← triage · dashboard
DF-2828

Multi-threaded CKPT_THAW violates vmspace_exec()/pmap_replacevm()'s single-thread contract: deterministic INVARIANTS panic; released-vmspace UAF for other LWPs on production kernels

Field Value
ID DF-2828
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:H
CWE CWE-667 / CWE-416 (consequence)
File sys/kern/kern_checkpoint.c
Lines 556 (contrast freeze :696-702; contract pmap.c:6378)
Area kern
Confidence certain
Discovered 2026-08-31
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

elf_gettextvp() calls vmspace_exec(p, NULL) while the process's other LWPs are still running — ckpt_thaw_proc() never stops them (contrast ckpt_freeze_proc, and kern_exec which guarantees a single thread before vmspace_exec). pmap_replacevm() asserts p->p_nthreads == 1 and only re-points RB_ROOT(&p->lwp_tree); vmspace_exec() then releases the old vmspace. On non-INVARIANTS kernels the assert is compiled out and the other LWPs keep lwp_vmspace pointing into the released vmspace/pmap (UAF — traced, not run). Wheel-gated.

Proof of contest

VERIFIED on the stock INVARIANTS kernel (findings/poc/DF-2828/): 2 threads + THAW → panic: assertion "p->p_nthreads == 1" failed in pmap_replacevm (pmap_replacevm←vmspace_exec←sys_sys_checkpoint← syscall2). Fix: enforce the precondition — EINVAL when p_nthreads > 1 at the top of CKPT_THAW, or proc_stop(SCORE)+wait+unstop mirroring the freeze path (policy choice best made upstream; fix_status=not_testable with reasoning).

See VERDICT.md design options in findings/poc/DF-2828/.

Timeline

  • 2026-08-31 Discovered during pass-2 audit of kern_checkpoint.c (GLM 5.3); deterministic panic reproduced same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2828 · 5 files
FileTypeDescriptionSize
README.md 2.5 KB ↓ raw
verdict.json 2.8 KB view raw
panic.txt 2.2 KB view raw
trigger_mt.c 761 B view raw
gen.c 7.4 KB view raw
README.md
↓ download raw

DF-2828 — CKPT_THAW from a multi-threaded process violates vmspace_exec()'s single-thread contract

  • File: sys/kern/kern_checkpoint.c (call site) / sys/vm/vm_map.c / sys/platform/pc64/x86_64/pmap.c (victim contract)
  • Bug line: kern_checkpoint.c:556elf_gettextvp() calls vmspace_exec(p, NULL) while other threads of p are still running. vmspace_exec()pmap_replacevm() begins with KKASSERT(p->p_nthreads == 1) (pmap.c:6378) and only re-points the first LWP (lp = RB_ROOT(&p->p_lwp_tree)), then releases the old vmspace (vmspace_rel(oldvmspace), vm_map.c:4330). kern_exec guarantees single-threadedness before calling it; ckpt_thaw_proc() does not — it never stops the process's other LWPs (contrast ckpt_freeze_proc() at kern_checkpoint.c:696-702 which does proc_stop(p, SCORE) + waits).

Run (guest, root; stock INVARIANTS kernel)

cc -O -o trigger_mt trigger_mt.c -lpthread
./gen clean.ckpt stage2.bin normal 0x600000f0 0     # any parseable image
./trigger_mt                                          # 2 threads + THAW

Expected

panic: assertion "p->p_nthreads == 1" failed in pmap_replacevm at /usr/src/sys/platform/pc64/x86_64/pmap.c:6378
pmap_replacevm() at pmap_replacevm+0xb9
vmspace_exec() at vmspace_exec+0x6a
sys_sys_checkpoint() at sys_sys_checkpoint+0x6a1
sys_xsyscall() at sys_xsyscall+0x89
syscall2() at syscall2+0x11e
Debugger("panic")

Impact

  • INVARIANTS kernels (incl. the stock audit guest): deterministic local kernel panic (DoS), gated by kern.ckptgroup (wheel by default).
  • Non-INVARIANTS production kernels (analysis, not run here): the assertion is compiled out; pmap_replacevm updates only the first LWP's lwp_vmspace, while vmspace_exec() drops the process's reference on the old vmspace. Other LWPs keep lwp_vmspace pointing into the released vmspace/pmap and can still take user faults or context-switch through it → use-after-free of struct vmspace/pmap.

Reject (or serialize) multi-threaded THAW before vmspace_exec() in ckpt_thaw_proc()/elf_gettextvp():

    vmspace_exec(p, NULL);

either return EINVAL when p->p_nthreads > 1 at the top of sys_sys_checkpoint()'s CKPT_THAW case, or stop the other LWPs the way ckpt_freeze_proc() does (proc_stop(p, SCORE) + wait for p->p_nstopped == p->p_nthreads - 1, then unstop after the restore).

Fix not kernel-validated in this run (the assertion fix is a policy change best decided upstream); the panic reproduction is in panic.txt.

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

Not kernel-validated: the natural fix is a policy change (reject or serialize multi-threaded THAW) rather than a mechanical one-line correctness fix; recommend upstream decide. The baseline panic reproduction is captured in panic.txt.

findings/poc/DF-2828/panic.txt; findings/poc/DF-2828/README.md (recommended fix)
per-fix-DF-2828

Confirmed kernel references

Detail

Evidence (decisive lines)

['panic.txt: assertion failure with full kernel trace through sys_sys_checkpoint', 'trigger_mt.c: 2-thread THAW trigger', 'gen.c: image generator (any parseable image suffices; nfiles=0 used)']

PoC changes

Simple pthread trigger; reuses the DF-2826 generator.

Verified recommended fix

Ensure single-threadedness before vmspace_exec() in the CKPT_THAW path: return EINVAL if p->p_nthreads > 1, or proc_stop(p, SCORE) + wait (mirroring ckpt_freeze_proc) and unstop after the restore.

Verdict

ckpt_thaw_proc() calls vmspace_exec(p, NULL) (kern_checkpoint.c:556) without first making the process single-threaded. pmap_replacevm() asserts p->p_nthreads == 1 (pmap.c:6378) and only re-points the first LWP; vmspace_exec() then drops the old vmspace reference. A 2-thread process calling sys_checkpoint(CKPT_THAW,...) on any parseable image panics deterministically on the stock INVARIANTS kernel: 'panic: assertion "p->p_nthreads == 1" failed in pmap_replacevm' with the trace pmap_replacevm<-vmspace_exec<-sys_sys_checkpoint<-sys_xsyscall<-syscall2. On non-INVARIANTS production kernels the KKASSERT is compiled out and the remaining LWPs keep lwp_vmspace pointing at the released vmspace (vmspace UAF) - analysis only, not executed. Gate: kern.ckptgroup (wheel by default).