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).
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2828 · 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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:556 —
elf_gettextvp()callsvmspace_exec(p, NULL)while other threads ofpare still running.vmspace_exec()→pmap_replacevm()begins withKKASSERT(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_execguarantees single-threadedness before calling it;ckpt_thaw_proc()does not — it never stops the process's other LWPs (contrastckpt_freeze_proc()at kern_checkpoint.c:696-702 which doesproc_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_replacevmupdates only the first LWP'slwp_vmspace, whilevmspace_exec()drops the process's reference on the old vmspace. Other LWPs keeplwp_vmspacepointing into the released vmspace/pmap and can still take user faults or context-switch through it → use-after-free ofstruct vmspace/pmap.
Recommended fix
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_testableNot 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)
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).
No comments yet.