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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2980 Β· 16 files
FileTypeDescriptionSize
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) loops profil(0x400000000000, 4 GiB, 0, 1.0) into an unmapped window; the tracer parent (same uid) hammers PT_ATTACH/stop/PT_DETACH. Hit signatures, both false-positive-free:
  • HIT1 PT_DETACH == EPERM after a successful PT_ATTACH + observed stop β‡’ the |= P_TRACED set was annihilated (sys_process.c:268-271 gate);
  • HIT2 PT_ATTACH == EBUSY immediately after a successful PT_DETACH β‡’ the detach's &= ~(P_TRACED|P_WAITED) clear was annihilated and P_TRACED resurrected (state-verified: child kp_stat=SACTIVE with P_TRACED still set β€” no tstop() can be in flight while the process is fully running, so the only tokenless p_flags writer in that window is addupc_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.

VERDICT.md
↓ download raw

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() calls stopprofclock(p) with no p->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-atomic p->p_flags &= ~P_PROFIL. p_flags is a plain int (sys/sys/proc.h:241); RMWs are plain loads/stores (no lock prefix), 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 a P_PROFIL process) and trap.c:507 (RQF_AST_OWEUPC AST); vkernel mirrors at platform/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 at kern_exec.c:493), kern_sig.c:1286 (|= P_CONTINUED), kern_exit.c:201 (|= P_WEXIT), etc. (Also note tstop() at kern_synch.c:1368 clears 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 inside setsugid() during exec; chain plausible β€” exec has no synchronous LWP teardown before kern_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

  1. Built poc.c in guest (cc, unprivileged-capable). Victim child: two threads looping syscall(SYS_profil, (void*)0x400000000000, 0x100000000, 0, 0x10000); tracer parent (same uid): PT_ATTACH β†’ waitpid(stop) β†’ PT_DETACH loop. - First attempt failed: PT_ATTACH re-attach got EBUSY (traced relationship persists until DETACH) β†’ reworked to attach/detach cycle. - Second failure mode (understood, not a hit): multi-threaded victim's straggler tstop() re-clears P_WAITED after waitpid consumed the stop β†’ DETACH EBUSY artifact; handled by re-consuming the re-posted stop event (waitpid(WNOHANG|WUNTRACED)).
  2. Baseline (kernel #0, stock INVARIANTS): - run.log: HIT2 after 99310 cycles / 1.9 s — PT_ATTACH EBUSY right after a successful PT_DETACH; state dump kp_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 no tstop() can be in flight; the only tokenless p_flags writer active is addupc_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).
  3. Fix validation (fix.diff: lwkt_gettoken/reltoken(&p->p_token) around the stopprofclock() in addupc_task): - make nativekernel + make installkernel in 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).
  4. Guest reset to pristine baseline afterwards (vm.sh reset with-src, verified #0 kernel and pristine subr_prof.c md5 974b1875e9983feeb4595c295cd0ad40).

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Applied 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)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Fri Sep 4 14:50:31 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

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.