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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3079 Β· 9 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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

  1. open("/proc/self/mem", O_RDWR) succeeds; procfs_open executes pfs->pfs_flags = ap->a_mode & (FWRITE|O_EXCL) = FWRITE (sys/vfs/procfs/procfs_vnops.c:204-205).
  2. close() β†’ procfs_close case Pmem (sys/vfs/procfs/procfs_vnops.c:239-241): the clear condition (ap->a_fflag & FWRITE) && (pfs->pfs_flags & O_EXCL) is false because O_EXCL was never set β†’ flags stay FWRITE.
  3. 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.
  4. 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)

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

fix.diff authored against read-only sys/ tree; kernel not rebuilt (non-memory-corruption Low finding, rebuild not mandated by contract).

['fix.diff']
↓ fix.diffper-fix-DF-3079

Confirmed kernel references

Detail

Exploit chain

open(own /proc/self/mem, O_RDWR); close(fd); every subsequent open(/proc//mem, O_RDWR|O_EXCL) by any user returns EBUSY indefinitely (pfsnode persists while the vnode is cached).

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//mem: procfs_open latches FWRITE into the shared pfs_flags on any write open (vnops.c:204-205) but procfs_close only clears the pair when O_EXCL was set (vnops.c:240-241), so a plain O_RDWR open+close leaves FWRITE latched and all later O_RDWR|O_EXCL opens return EBUSY (vnops.c:186-190) until the vnode is reclaimed. Reproduced 3/3 deterministic on the stock INVARIANTS guest as uid 1001; fresh-pid control open succeeds. No memory corruption; ceiling is a local DoS of a debugging facility.