Write-path SSTOP check diverges from LSSTOP check used by fpregs/dbregs siblings
| Field | Value |
|---|---|
| ID | DF-0937 |
| 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-362 Race Condition |
| File | sys/vfs/procfs/procfs_regs.c |
| Lines | 68, 71 |
| Area | vfs |
| Confidence | likely |
| Discovered | 2026-07-05 |
| Reported | pending |
| Known CVE | none |
| CVE match | dfly_specific |
Summary
procfs_doregs gates PT_SETREGS-style writes on
p->p_stat == SSTOP, whereas the immediately adjacent
procfs_dofpregs (sys/vfs/procfs/procfs_fpregs.c:67) and
procfs_dodbregs (sys/vfs/procfs/procfs_dbregs.c:69) gate on
lp->lwp_stat == LSSTOP. proc_stop (sys/kern/kern_sig.c:1565-1641)
sets p->p_stat = SSTOP at line 1584 BEFORE iterating LWPs, and for
LWPs that are LSRUN merely calls lwp_signotify (line 1619) so
they stop asynchronously. The chosen first LWP
(sys/vfs/procfs/procfs_subr.c:375) can therefore still be LSRUN (or
LSSLEEP with LWP_MP_WSTOP, not yet at a tstop()) at the moment
p_stat is observed SSTOP, so the write at line 71 can hit a
trapframe whose owning LWP is not yet actually stopped.
Root cause
At sys/vfs/procfs/procfs_regs.c:67-72 the gate is:
if (p->p_stat != SSTOP)
error = EBUSY;
else
error = procfs_write_regs(lp, &r);
The sibling implementations at
sys/vfs/procfs/procfs_fpregs.c:66-71 and
sys/vfs/procfs/procfs_dbregs.c:68-73 instead use:
if (lp->lwp_stat != LSSTOP)
error = EBUSY;
proc_stop() at sys/kern/kern_sig.c:1584 sets p_stat=SSTOP and
then, in the loop at lines 1586-1624, only schedules still-running
LWPs to stop (LSRUN β lwp_signotify at :1619; LSSLEEP β set
LWP_MP_WSTOP at :1609). Thus there is a real window in which
p_stat==SSTOP holds but the first LWP is LSRUN.
The machine-dependent contract documented at the top of
sys/platform/pc64/x86_64/procfs_machdep.c (lines 45 and 53:
"The process is stopped at the time read_regs/write_regs is called")
is LWP-local, so the LWP-level check is the correct one.
Threat model & preconditions
- Attacker position: Local, same-uid (must pass
CHECKIO+p_trespassat line60). - Privileges gained or impact: No privilege boundary is crossed
and no information is leaked beyond what same-uid ptrace already
grants. Demonstrable impact is correctness, not
confidentiality/integrity of another uid: the trapframe written via
set_regs(sys/platform/pc64/x86_64/machdep.c:3020-3031) is concurrent with the LWP still running on another CPU, so when that LWP eventually returns to userspace its registers reflect the write rather than the state it was actually stopped in. - Required config or capabilities: Same-uid + the ability to send
SIGSTOPto the target. - Reachability:
fork()+pthread_create()(second LWP) +kill(child, SIGSTOP)+pwrite(/proc/<child>/regs, β¦). P_SUGIDinCHECKIOandP_TRACEDin exec (sys/kern/kern_exec.c:487) rule out using this to corrupt a setuid exec.
Proof of concept
No privilege-gain exploit exists. To demonstrate the divergence vs. the
siblings, build and run a small program on DragonFlyBSD that forks a
child which creates a second LWP (pthread_create + pthread_spin
lock to keep the new LWP running), then has the parent send SIGSTOP
to the child and immediately pwrite() into /proc/<child>/regs at
offset offsetof(struct reg, r_rip) (struct reg from
<machine/reg.h>). On a sufficiently loaded box the write succeeds
even though the first LWP's lwp_stat is not yet LSSTOP, whereas
writing /proc/<child>/fpregs or /proc/<child>/dbregs at the same
instant returns EBUSY.
Reproducibility is timing-dependent and the observable effect is only
that RIP/RSP are now whatever the parent chose (which the parent
was already authorized to do via set_regs once the LWP settles). This
is reported as Info because no uid/capability boundary is crossed.
Impact
No confidentiality or integrity impact beyond same-uid ptrace's existing capabilities. The write can land in a trapframe whose LWP is not yet stopped β a correctness issue for the tracing API, not a security primitive.
Recommended fix
Make the gate LWP-local to match the documented machine-dependent contract and the fpregs/dbregs siblings.
--- a/sys/vfs/procfs/procfs_regs.c
+++ b/sys/vfs/procfs/procfs_regs.c
@@ -65,7 +65,7 @@ procfs_doregs(struct proc *curp, struct lwp *lp, struct pfsnode *pfs,
if (error == 0)
error = uiomove_frombuf(&r, sizeof(r), uio);
if (error == 0 && uio->uio_rw == UIO_WRITE) {
- if (p->p_stat != SSTOP)
+ if (lp->lwp_stat != LSSTOP)
error = EBUSY;
else
error = procfs_write_regs(lp, &r);
References
sys/vfs/procfs/procfs_fpregs.c:66-71andsys/vfs/procfs/procfs_dbregs.c:68-73β siblings that use the correctLSSTOPgate.sys/kern/kern_sig.c:1565-1641βproc_stopsetsp_stat=SSTOPbefore LWPs have actually stopped.sys/platform/pc64/x86_64/procfs_machdep.c:45,53β the machine-dependent contract that mandates the LWP be stopped.
Timeline
- 2026-07-05 Discovered during automated audit.
- pending Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0937 Β· 1 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | Write-path SSTOP check diverges from LSSTOP check used by fpregs/dbregs siblings | 396 B | view raw |
Fix verification
not_testablefix.diff authored but did not apply cleanly; needs context rework
fix.diff authored but did not apply cleanly; needs context rework
Confirmed kernel references
β
Detail
Exploit chain
none (Info severity)
Evidence (decisive lines)
Source-confirmed at sys/vfs/procfs/procfs_regs.c:68: write-path SSTOP check diverges from LSSTOP check used by fpregs/dbregs
Verified recommended fix
Source-confirmed at sys/vfs/procfs/procfs_regs.c:68: write-path SSTOP check diverges from LSSTOP check used by fpregs/dbregs
Verdict
Source-confirmed at sys/vfs/procfs/procfs_regs.c:68: write-path SSTOP check diverges from LSSTOP check used by fpregs/dbregs
No comments yet.