addupc_task() calls stopprofclock() without p->p_token: non-atomic p->p_flags RMW race annihilating concurrent token-held flag updates (P_TRACED/P_SUGID security gates)
| Field | Value |
|---|---|
| ID | DF-2980 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:L |
| CWE | CWE-667 / CWE-362 |
| File | sys/kern/subr_prof.c |
| Lines | 145 (contract: kern_clock.c:1303-1310) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | privesc |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
stopprofclock() documents 'caller must hold p->p_token' and performs a non-atomic read-modify-write p->p_flags &= ~P_PROFIL (p_flags is a plain int). addupc_task() is the sole tokenless caller β both its call sites (userret at trap.c:231 and the RQF_AST_OWEUPC AST) hold no token. Because userret runs addupc_task on EVERY syscall exit of a P_PROFIL process, an unprivileged process looping profil(2) with an unmapped sample base makes every copyin fault, generating tokenless p_flags RMWs at syscall rate (MHz). A stale store from this stream annihilates any concurrent token-held p_flags update: reproduced state-verified corruption of P_TRACED (PT_DETACH's &= ~(P_TRACED|P_WAITED) overwritten, P_TRACED resurrected on a running child; symmetric flavor: PT_ATTACH's |= P_TRACED lost β PT_DETACH EPERM). The same defect races |= P_SUGID (kern_prot.c:1304, set during setuid exec) whose loss defeats the ptrace-attach gate at sys_process.c:202-204, in principle allowing a same-ruid parent to ptrace a setuid-root image (speculative uid=0 chain, not demonstrated); also affects P_CONTINUED/P_WAITED wait accounting. Broader class noted: p_flags writers are not mutually serialized either (tstop clears P_WAITED under the parent's token). Demonstrated on the stock INVARIANTS guest: P_TRACED resurrection hits at 16K/99K cycles, 2/3 runs. Fix validated (p_token around the call): 3Γ1M cycles clean, profiling regression-clean.
Timeline
- 2026-09-02 Discovered during pass-2 audit of subr_prof.c (GLM 5.3); race reproduced + fix validated. DF-0251/0252 re-verified still present, not re-reported.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2980 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc.c | β | 5.9 KB | view raw | |
| sanity.c | β | 993 B | view raw | |
| build.sh | β | 88 B | view raw | |
| run.sh | β | 320 B | view raw | |
| build.log | β | 42 B | view raw | |
| run.log | β | 391 B | view raw | |
| run.2.log | β | 500 B | view raw | |
| run.3.log | β | 275 B | view raw | |
| sanity-baseline.log | β | 89 B | view raw | |
| fix-patched.log | β | 4.6 KB | view raw | |
| fix-kernel-uname.txt | β | 111 B | view raw | |
| fix.diff | β | 224 B | view raw | |
| verdict.json | β | 5.0 KB | view raw | |
| VERDICT.md | β | 5.1 KB | β raw | |
| README.md | β | 4.1 KB | β raw | |
| env.txt | β | 352 B | view raw |
DF-2980 β tokenless stopprofclock() in addupc_task() (non-atomic p_flags RMW race)
File: sys/kern/subr_prof.c (pass-2 finding; DF-0251 / DF-0252 known, not affected here)
Severity: Medium Β· Confidence: certain (mechanism reproduced + fix-validated)
Class: CWE-667 (improper locking) / CWE-362 (race) β concurrent non-atomic RMW on
struct proc::p_flags from the profiling AST/syscall-return path.
What it is
addupc_task() calls stopprofclock(p) at sys/kern/subr_prof.c:145 without holding
p->p_token, violating stopprofclock()'s documented contract
(sys/kern/kern_clock.c:1303-1305 "caller must hold p->p_token"). stopprofclock() does a
non-atomic p->p_flags &= ~P_PROFIL (kern_clock.c:1310; p_flags is a plain int,
sys/sys/proc.h:241). Every other caller (sys_profil subr_prof.c:61-76, exec
kern_exec.c:226β444, exit kern_exit.c:304β365) holds the token; addupc_task is the
sole violator, and both of its call sites (platform/pc64/x86_64/trap.c:231 in userret()
and trap.c:507 on the T_ASTFLT/RQF_AST_OWEUPC path) are tokenless.
Amplifier: userret() calls addupc_task() on every syscall exit of a P_PROFIL
process. A thread that keeps re-arming profil(2) with an unmapped sample buffer
(pr_base in dead user space) makes every copyin() in addupc_task() fault, so the
tokenless p_flags &= ~P_PROFIL executes at syscall rate (MHz), entirely unprivileged.
When one of those stale RMW stores lands on top of a token-held writer's store β e.g.
PT_ATTACH's p->p_flags |= P_TRACED (sys/kern/sys_process.c:305) or its
&= ~(P_TRACED|P_WAITED) clear at detach (sys_process.c:362), or setsugid()'s
|= P_SUGID (kern_prot.c:1304, called on setuid exec at kern_exec.c:493) β the
concurrent update is annihilated. P_SUGID gates ptrace attach (sys_process.c:202-204),
sugid coredumps (kern_sig.c:2543) and ktrace (kern_ktrace.c:680); P_TRACED gates the
whole debug relationship; P_CONTINUED gates WCONTINUED reporting. Losing the P_SUGID set on
a setuid-root exec would let a same-ruid parent ptrace a root-euid image β a speculative
full privesc chain (nanosecond window inside setsugid(); not demonstrated).
PoC (100% unprivileged)
poc.cβ victim child (2 threads) loopsprofil(0x400000000000, 4 GiB, 0, 1.0)into an unmapped window; the tracer parent (same uid) hammersPT_ATTACH/stop/PT_DETACH. Hit signatures, both false-positive-free:- HIT1
PT_DETACH == EPERMafter a successfulPT_ATTACH+ observed stop β the|= P_TRACEDset was annihilated (sys_process.c:268-271gate); - HIT2
PT_ATTACH == EBUSYimmediately after a successfulPT_DETACHβ the detach's&= ~(P_TRACED|P_WAITED)clear was annihilated and P_TRACED resurrected (state-verified: childkp_stat=SACTIVEwith P_TRACED still set β notstop()can be in flight while the process is fully running, so the only tokenlessp_flagswriter in that window isaddupc_taskβstopprofclock).
Build / run (guest)
cc -O2 -Wall -pthread -o poc poc.c ./poc 300000 # exit 0 = RACE HIT, 2 = no hit within budget
Results
| kernel | result |
|---|---|
baseline #0 Jul 2 2026 |
HIT2 after 99310 cycles (1.9 s); 300000-cycle run no-hit; HIT2 after 16166 cycles (0.33 s) β 2/3 runs |
patched #1 Sep 4 2026 (fix.diff: lwkt_gettoken(&p->p_token) around the stopprofclock() in addupc_task) |
sanity: profiling still accumulates samples; 3 Γ 1,000,000 cycles (~49 s, β10Γ baseline exposure) β zero hits |
Fix
fix.diff β take p->p_token around the stopprofclock() call in addupc_task()
(thread context only; both call sites hold no conflicting token; userret() itself
acquires p_token a few lines later at trap.c:247, so this is an established pattern).
Suggested hardening beyond this finding: p_flags has several RMW writers that are not
mutually serialized (tstop() clears P_WAITED under the parent's token at
kern_synch.c:1368 while ptrace writes under the child's token) β a wider cleanup would
make p_flags updates atomic (e.g. atomic_clear_int) or consistently token-interlocked.
DF-2980 β VERDICT
Status: reproduced (unprivileged local race; state corruption of security-relevant
p_flags bits demonstrated; fix validated) Β· Confidence: certain (mechanism),
exploit escalation speculative.
Root cause (path:line)
sys/kern/subr_prof.c:145βaddupc_task()callsstopprofclock(p)with nop->p_token. Contract:sys/kern/kern_clock.c:1303-1305("caller must hold p->p_token"). All other callers comply:subr_prof.c:61-63(sys_profil),kern_exec.c:226/444,kern_exit.c:304/365.sys/kern/kern_clock.c:1308-1311βstopprofclock()does read + non-atomicp->p_flags &= ~P_PROFIL.p_flagsis a plainint(sys/sys/proc.h:241); RMWs are plain loads/stores (nolockprefix), so a competing CPU's store landing between a tokenless clear's load and store is silently overwritten.- Tokenless call sites:
platform/pc64/x86_64/trap.c:231(userret()β runs on every syscall exit of aP_PROFILprocess) andtrap.c:507(RQF_AST_OWEUPCAST); vkernel mirrors atplatform/vkernel64/x86_64/trap.c:197,405. - Racing token-held writers on the same word:
sys_process.c:305/313(|= P_TRACED),sys_process.c:362(&= ~(P_TRACED|P_WAITED)),kern_prot.c:1304(|= P_SUGID, reached from setuid exec atkern_exec.c:493),kern_sig.c:1286(|= P_CONTINUED),kern_exit.c:201(|= P_WEXIT), etc. (Also notetstop()atkern_synch.c:1368clears P_WAITED under the parent's token β p_flags writers are not even mutually serialized; that broader issue is out of scope for this finding.)
Threat
Any unprivileged process can generate a MHz-rate stream of tokenless
p->p_flags &= ~P_PROFIL RMWs: loop profil(2) with pr_base in unmapped user VA β
every syscall exit runs userret β addupc_task β copyin(EFAULT) β stopprofclock
(trap.c:225-232, subr_prof.c:140-145). Each stale store can annihilate a concurrent
token-held update of any other p_flags bit. Security gates affected:
- P_SUGID loss β ptrace attach check passes (
sys_process.c:202-204) for a same-ruid setuid-root image β speculative full privesc (window ~ns insidesetsugid()during exec; chain plausible β exec has no synchronous LWP teardown beforekern_exec.c:493β but not demonstrated). - P_TRACED loss β tracee escapes its tracer / debugger state corrupted.
- P_CONTINUED / P_WAITED loss β wait4(WCONTINUED) accounting corruption.
- P_WEXIT/P_POSTEXIT loss β exit-path correctness.
Demonstrated impact: deterministic-within-seconds corruption of kernel process state
(P_TRACED resurrection / annihilation) by an unprivileged user. No memory corruption:
addupc_task's copyin/copyout targets are range-checked by std_copyout/std_copyin
(platform/pc64/x86_64/support.s:245-258,295-307), so escalation beyond flag corruption
was not pursued further.
Reproduction narrative
- Built
poc.cin guest (cc, unprivileged-capable). Victim child: two threads loopingsyscall(SYS_profil, (void*)0x400000000000, 0x100000000, 0, 0x10000); tracer parent (same uid):PT_ATTACH β waitpid(stop) β PT_DETACHloop. - First attempt failed:PT_ATTACHre-attach got EBUSY (traced relationship persists until DETACH) β reworked to attach/detach cycle. - Second failure mode (understood, not a hit): multi-threaded victim's stragglertstop()re-clears P_WAITED after waitpid consumed the stop β DETACH EBUSY artifact; handled by re-consuming the re-posted stop event (waitpid(WNOHANG|WUNTRACED)). - Baseline (kernel #0, stock INVARIANTS):
- run.log: HIT2 after 99310 cycles / 1.9 s β
PT_ATTACHEBUSY right after a successfulPT_DETACH; state dumpkp_stat=2 (SACTIVE),kp_flags=0x1c20(P_TRACED|P_WAITED|P_PROFIL set) β the child was running with P_TRACED still set. Attribution: while the process is fully running notstop()can be in flight; the only tokenless p_flags writer active isaddupc_taskβstopprofclock. - run.2.log: 300000 cycles, no hit (race is probabilistic). - run.3.log: HIT2 after 16166 cycles / 0.33 s (kp_flags=0xc20). - Fix validation (
fix.diff:lwkt_gettoken/reltoken(&p->p_token)around thestopprofclock()inaddupc_task): -make nativekernel+make installkernelin guest β kernel #1 Sep 4 2026. - sanity: normal profiling still accumulates samples (sanity.c, sum > 0). - 3 Γ 1,000,000 cycles (~49 s β 10Γ the exposure in which baseline hit twice): zero hits (fix-patched.log). - Guest reset to pristine baseline afterwards (
vm.sh reset with-src, verified #0 kernel and pristinesubr_prof.cmd5974b1875e9983feeb4595c295cd0ad40).
Honest limits
- HIT1 (EPERM flavor: annihilated set rather than annihilated clear) was not observed in the recorded runs; HIT2 is the same defect in the stale-store direction and is state-verified. The EPERM flavor remains expected under longer runs.
- The P_SUGID β ptrace-a-setuid-root-image privesc chain is argued from source, not demonstrated.
- 1-in-3 baseline runs produced no hit within 300K cycles β the defect is a race; reproduction is probabilistic but fast (sub-2 s in both hitting runs).
Fix verification
fixedApplied fix.diff (lwkt_gettoken/reltoken around stopprofclock in addupc_task) to a clean /usr/src in the guest, make -j6 nativekernel + installkernel, rebooted into kernel #1. sanity.c confirms profiling still accumulates samples; the exact PoC ran 3 x 1,000,000 attach/detach cycles (~49 s, ~10x the exposure in which the baseline hit twice) with ZERO hits, versus baseline hits at 16166 and 99310 cycles. Bad behavior gone, no regression.
fix.diff, fix-patched.log (3x1M cycles clean + sanity pass on #1), fix-kernel-uname.txt, baseline run.log/run.3.log (hits on #0)
Confirmed kernel references
- sys/kern/subr_prof.c:145
- sys/kern/kern_clock.c:1303-1311
- sys/platform/pc64/x86_64/trap.c:225-232
- sys/platform/pc64/x86_64/trap.c:500-510
- sys/kern/sys_process.c:268-271
- sys/kern/sys_process.c:305
- sys/kern/sys_process.c:362
- sys/kern/kern_prot.c:1298-1307
- sys/kern/kern_exec.c:493
- sys/sys/proc.h:241,353
- sys/kern/kern_synch.c:1368
Detail
Exploit chain
unpriv: loop profil(2) with pr_base in unmapped user VA -> every syscall exit runs userret->addupc_task->copyin(EFAULT)->stopprofclock (tokenless p_flags RMW at MHz) -> stale store annihilates a concurrent token-held RMW: reproduced on PT_DETACH's &=~(P_TRACED|P_WAITED) (P_TRACED resurrected on a running child, state-verified via kinfo kp_flags) and expected on |= P_TRACED (PT_DETACH EPERM) and |= P_SUGID during setuid exec (kern_exec.c:493) -> P_SUGID loss would let a same-ruid parent PT_ATTACH a setuid-root image (sys_process.c:202-204) -> PT_WRITE code injection -> uid0 (speculative; not demonstrated)
Evidence (decisive lines)
run.log (HIT2 after 99310 cycles/1.9s, kp_stat=2 SACTIVE kp_flags=0x1c20 P_TRACED resurrected), run.3.log (HIT2 after 16166 cycles/0.33s, kp_flags=0xc20), run.2.log (300K-cycle no-hit - probabilistic), fix-patched.log (3x1M cycles zero hits on patched kernel #1), sanity-baseline.log, build.log, VERDICT.md full narrative
PoC changes
Seed concept kept, execution rewritten: victim uses an unmapped 0x400000000000 window with 4 GiB size and 1.0 scale (copyin fault on every userret -> tokenless stopprofclock per syscall exit); tracer cycles PT_ATTACH/waitpid/PT_DETACH (naive re-attach gets EBUSY - relationship persists); multi-threaded victim straggler tstop() re-clears P_WAITED (kern_synch.c:1368) causing a benign DETACH EBUSY artifact, handled by re-consuming the re-posted stop with waitpid(WNOHANG|WUNTRACED); two false-positive-free hit signatures (DETACH EPERM = annihilated set; ATTACH EBUSY after successful detach = annihilated clear) plus kinfo kp_stat/kp_flags state dump for attribution.
Verified recommended fix
Hold p->p_token across the stopprofclock() call in addupc_task() (sys/kern/subr_prof.c:145), per stopprofclock()'s documented locking contract.
Verdict
addupc_task() (sys/kern/subr_prof.c:145) calls stopprofclock() without p->p_token, violating the documented contract (kern_clock.c:1303-1305); the resulting non-atomic p->p_flags &= ~P_PROFIL runs at syscall rate via userret (trap.c:231) whenever profil(2) is armed with an unmapped sample base, and a tokenless stale store was reproduced (2/3 runs, sub-2 s) annihilating PT_DETACH's token-held P_TRACED clear on the stock INVARIANTS guest by an unprivileged user. Demonstrated impact is kernel process-state corruption of security-gated bits (P_TRACED; same defect races |= P_SUGID at kern_prot.c:1304 which gates ptrace attach at sys_process.c:202-204 - speculative uid0 chain, not demonstrated); no memory corruption (copy targets are VM_MAXUSER_ADDRESS-checked). fix.diff (take p->p_token around the stopprofclock call) rebuilt as kernel #1: profiling still works and 3x1,000,000 cycles (~10x baseline exposure) produced zero hits.
No comments yet.