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

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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2913 Β· 16 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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_testable
baseline no→ patch + rebuild →patched reproduced

p_spin-guarded stores compiled and booted; setrlimit/fork exercised throughout the A/B suite without regression.

↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Mon Oct 26 01:57:23 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (h41 fix kernel, all 41 hardening fixes applied)

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.