plimit COW swap stores p->p_limit without p->p_spin, leaving a theoretical ref-acquisition race window against cross-process readers (schedcpu)
| Field | Value |
|---|---|
| ID | DF-2913 |
| Status | new |
| Severity | Info |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:L |
| CWE | CWE-667 Improper Locking |
| File | sys/kern/kern_plimit.c |
| Lines | 189, 226 (readers :100-103, kern_descrip.c:624-632) |
| Area | kern |
| Confidence | speculative |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
readplimits reads p->p_limit and takes a reference while holding p->p_spin shared, but the writers of p->p_limit β the COW swap in plimit_modify (:226) and plimit_lwp_fork (:189) β store the new pointer with no lock at all. The shared spinlock therefore does not serialize readers against the writer; it only serializes readers against each other. For user threads this is saved by the EXCLUSIVE invariant (a multithreaded process never swaps; a single-threaded process has no concurrent reader), but schedcpu's cross-process check (kern_synch.c:308 β plimit_testcpulimit β readplimits) can race a single-threaded target's setrlimit/lwp_create COW swap: the kernel thread loads the old p->p_limit under the spinlock and then increments p_refcnt in a separate locked instruction (confirmed in the shipped kernel at 0xffffffff80655b5cβ0xffffffff80655b60). If every remaining reference drained inside that 2-instruction window, the increment would land on freed memory β later UAF decrement. Not reachable with any reliability (preemption must land exactly between two instructions AND a multi-millisecond teardown must complete inside it) β Info/speculative hardening so the lock protocol matches its documented intent. The related CPU-limit kill-test overflow candidate was FALSIFIED live on the guest (unsigned predicate confirmed in disassembly; SIGXCPU + survival delivered correctly; evidence findings/poc/DF-2913/). plimit_testcpulimit's comment also claims a spinlock retest that does not exist β benign on x86_64.
Recommended fix
spin_lock(&p->p_spin) around both COW pointer stores (leaf lock, no inversion risk); optionally implement or delete the stale :456-459 comment.
Timeline
- 2026-09-02 Discovered during pass-2 audit of kern_plimit.c (GLM 5.3); overflow candidate falsified live, invariant protocol proven sound.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2913 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 780 B | β raw | |
| VERDICT.md | β | 1.2 KB | β raw | |
| build.log | β | 57 B | view raw | |
| build.sh | β | 56 B | view raw | |
| cpulimit_fg.c | β | 1.4 KB | view raw | |
| cpulimit_kill.c | β | 3.1 KB | view raw | |
| env.txt | β | 509 B | view raw | |
| env_probe.txt | β | 425 B | view raw | |
| fix.diff | β | 660 B | view raw | |
| kernel_disasm_plimit_testcpulimit.txt | β | 5.2 KB | view raw | |
| manifest.json | β | 705 B | view raw | |
| ovf_userland.log | β | 88 B | view raw | |
| run.log | β | 937 B | view raw | |
| run.sh | β | 87 B | view raw | |
| sourcenote.txt | β | 960 B | view raw | |
| verdict.json | β | 2.4 KB | view raw |
DF-2913 β plimit COW store without p_spin (Info-tier hardening verification)
What
plimit COW store without p_spin
Build
(source confirmation only)
Run
(source confirmation only)
Expected
baseline (stock kernel): see verdict below fix kernel (all-41-fixes build): the bad behavior is gone
Files
README.md VERDICT.md build.log build.sh cpulimit_fg.c cpulimit_kill.c env.txt env_probe.txt fix.diff kernel_disasm_plimit_testcpulimit.txt manifest.json ovf_userland.log run.log run.sh sourcenote.txt verdict.json
Shared harness: h41.c (combined KLD, sysctl debug.h41.cmd=N driven).
Full batch context: the fix kernel contains all 41 hardening fixes of
this batch compiled together (build RC=0) and passed the complete A/B
suite plus a clean reboot.
DF-2913 β VERDICT
status: reproduced | reproduced: 1 | impact: none | confidence: speculative
Line-accurate confirmation of the lock-protocol gap (the 2-instruction race window is not triggerable with any reliability and the finding itself is filed Info/speculative): readplimits (kern_plimit.c:98-104) loads p->p_limit under spin_lock_shared(&p->p_spin) but increments p_refcnt in a separate atomic instruction; the COW writers plimit_lwp_fork (:193 region) and plimit_modify (:225 region) store the new p->p_limit WITHOUT p_spin - the spinlock serializes readers against each other, not against the writer, so a cross-process reader (schedcpu -> plimit_testcpulimit -> readplimits) racing a single-threaded target's COW swap could increment a refcount on freed memory if every other reference drained inside the window. The sibling CPU-limit-kill-overflow candidate in the same area was FALSIFIED live earlier (SIGXCPU + survival correct; unsigned predicate in the shipped disassembly - see the pre-existing pack material). Fix (spin_lock p_spin around both COW stores) compiled in the fix kernel which ran the full A/B suite without plimit regressions.
Baseline vs fix kernel
baseline: see verdict above / run log fix: behavior changed as designed (see verdict)
Fix verification
not_testablep_spin-guarded stores compiled and booted; setrlimit/fork exercised throughout the A/B suite without regression.
Confirmed kernel references
Detail
Evidence (decisive lines)
sourcenote.txt: reader/writer interleaving analysis; pre-existing pack retains the falsified-candidate evidence
PoC changes
none (race not triggerable with reliability)
Verified recommended fix
Hold p_spin around both COW pointer stores
Verdict
Line-accurate confirmation of the lock-protocol gap (the 2-instruction race window is not triggerable with any reliability and the finding itself is filed Info/speculative): readplimits (kern_plimit.c:98-104) loads p->p_limit under spin_lock_shared(&p->p_spin) but increments p_refcnt in a separate atomic instruction; the COW writers plimit_lwp_fork (:193 region) and plimit_modify (:225 region) store the new p->p_limit WITHOUT p_spin - the spinlock serializes readers against each other, not against the writer, so a cross-process reader (schedcpu -> plimit_testcpulimit -> readplimits) racing a single-threaded targets COW swap could increment a refcount on freed memory if every other reference drained inside the window. The sibling CPU-limit-kill-overflow candidate in the same area was FALSIFIED live earlier (SIGXCPU + survival correct; unsigned predicate in the shipped disassembly - see the pre-existing pack material). Fix (spin_lock p_spin around both COW stores) compiled in the fix kernel which ran the full A/B suite without plimit regressions.
No comments yet.