procfs Pmem exclusive-open latch: plain write-open+close permanently denies O_EXCL opens of /proc/<pid>/mem (EBUSY until vnode reclaim)
Summary
procfs_open latches the open mode into the per-pfsnode (shared) flag word: pfs->pfs_flags = a_mode & (FWRITE|O_EXCL) - a plain O_RDWR open latches FWRITE without O_EXCL. procfs_close only clears the pair when (pfs->pfs_flags & O_EXCL) is already set, so the last non-exclusive writer leaves FWRITE latched forever; every subsequent open(O_RDWR|O_EXCL) trips the exclusivity test and returns EBUSY until the vnode is reclaimed. Any user who can legitimately write a pid's mem once (own processes; root for all) permanently wedges exclusive mem access for everyone on that pid, cross-uid within the allowed tracer set. VERIFIED guest uid 1001 3/3 deterministic (open+close then O_EXCL -> EBUSY forever; fresh pid accepts). No corruption; facility DoS. Fix: clear the latch on the last write close via the v_opencount<2 test.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3079 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| procfs_sticky_excl.c | β | 2.4 KB | view raw | |
| build.sh | β | 119 B | view raw | |
| run.sh | β | 97 B | view raw | |
| run.log | β | 234 B | view raw | |
| run.2.log | β | 450 B | view raw | |
| env.txt | β | 272 B | view raw | |
| fix.diff | β | 655 B | view raw | |
| VERDICT.md | β | 2.3 KB | β raw | |
| verdict.json | β | 2.3 KB | view raw |
DF-3079 β VERDICT
Status: reproduced (3/3 runs, deterministic, unprivileged, guest stayed up).
Impact: dos β permanent denial of exclusive (O_EXCL) opens of
/proc/<pid>/mem for a pid, from an unprivileged user, until vnode reclaim.
Not memory corruption; ceiling is a local DoS of a debugging facility.
How it reproduces
open("/proc/self/mem", O_RDWR)succeeds;procfs_openexecutespfs->pfs_flags = ap->a_mode & (FWRITE|O_EXCL)=FWRITE(sys/vfs/procfs/procfs_vnops.c:204-205).close()βprocfs_closecasePmem(sys/vfs/procfs/procfs_vnops.c:239-241): the clear condition(ap->a_fflag & FWRITE) && (pfs->pfs_flags & O_EXCL)is false becauseO_EXCLwas never set β flags stayFWRITE.open("/proc/self/mem", O_RDWR|O_EXCL)β the exclusivity test at sys/vfs/procfs/procfs_vnops.c:186-190((pfs->pfs_flags & FWRITE) && (ap->a_mode & O_EXCL))β EBUSY.- Control: a freshly-forked child pid never opened before accepts
O_RDWR|O_EXCL(3/3) β proves the latch is per-pfsnode state left by the prior non-exclusive write open, not a system-wide condition.
Observed on guest (uid 1001 maxx):
open(/proc/self/mem, O_RDWR) = 3 OK open(/proc/self/mem, O_RDWR|O_EXCL) = -1 Device busy open(/proc/child/mem, O_RDWR|O_EXCL) = 3 OK BUG-REPRODUCED: EBUSY on O_EXCL after non-EXCL write open+close
Why it is what it is (path:line)
- latch set: sys/vfs/procfs/procfs_vnops.c:204-205
- latch cleared only if O_EXCL: sys/vfs/procfs/procfs_vnops.c:240-241
- EBUSY test: sys/vfs/procfs/procfs_vnops.c:186-190
pfs_flagsis per-pfsnode, i.e. shared by all openers of that (pid, Pmem, mount) node β sys/vfs/procfs/procfs.h:71; the pfsnode survives as long as the vnode is cached (procfs_subr.c:88-133).
Exploit chain
Not applicable (logic/lifecycle bug, no corruption): the primitive is "permanently EBUSY an exclusive mem open on a pid I am allowed to write once". Cross-user effect requires sharing the target pid legitimately (same real uid, or non-setuid targets of one's own uid).
Fix validation
Not rebuilt: non-memory-corruption Low finding; fix.diff is the surgical
state-machine fix (clear on last write close using the v_opencount < 2
test already present at :254, dropping the bogus O_EXCL requirement).
fix_status: not_testable (authored only).
Fix verification
not_testablefix.diff authored against read-only sys/ tree; kernel not rebuilt (non-memory-corruption Low finding, rebuild not mandated by contract).
['fix.diff']
Confirmed kernel references
Detail
Exploit chain
open(own /proc/self/mem, O_RDWR); close(fd); every subsequent open(/proc/
Evidence (decisive lines)
["run.log and run.2.log: 'open(/proc/self/mem, O_RDWR|O_EXCL) = -1 Device busy' after plain write open+close, while 'open(/proc/child/mem, O_RDWR|O_EXCL) = 3 OK' control succeeds; BUG-REPRODUCED marker 3/3"]
PoC changes
Written fresh for this finding; no seed existed.
Verified recommended fix
In procfs_close Pmem case, clear FWRITE|O_EXCL on the last write close: condition on (a_fflag & FWRITE) && a_vp->v_opencount < 2 instead of requiring pfs_flags & O_EXCL.
Verdict
Unprivileged local user permanently denies exclusive (O_EXCL) opens of /proc/
No comments yet.