Missing visibility/privilege check in kern.proc.pathname -> exe-path disclosure of arbitrary processes
| Field | Value |
|---|---|
| ID | DF-0015 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N |
| CWE | CWE-862 Missing Authorization |
| File | sys/kern/kern_proc.c |
| Lines | 2080-2117 |
| Area | kern |
| Confidence | likely |
| Discovered | 2026-06-29 |
| Reported | pending |
Summary
sysctl_kern_proc_pathname() resolves and returns the executable path of an
arbitrary pid without any p_trespass()/PRISON_CHECK()/ps_argsopen
authorization check. Its sibling handlers enforce visibility:
sysctl_kern_proc_args gates on (!ps_argsopen) && p_trespass(cr1, p->p_ucred)
(kern_proc.c:1897) and sysctl_kern_proc_cwd uses the identical gate
(:2052). The KERN_PROC_PATHNAME node (:2212) is a plain CTLFLAG_RD
sysctl (world-readable), so any unprivileged user can read the executable path
of any process โ including root-owned daemons โ leaking what is running and
its exact install path. This is an unnecessary disclosure inconsistent with
the cwd/args policy.
Root cause
sys/kern/kern_proc.c:2080-2117 (sysctl_kern_proc_pathname):
p = pfind(*pidp);
if (p == NULL) return (ESRCH);
...
lwkt_gettoken_shared(&p->p_token);
if (p->p_textnch.ncp) {
cache_copy(&p->p_textnch, &nch);
error = cache_fullpath(p, &nch, NULL, &retbuf, &freebuf, 0);
...
}
...
error = SYSCTL_OUT(req, retbuf, strlen(retbuf) + 1);
There is no p_trespass(cr1, p->p_ucred) / ps_argsopen gate, unlike args
(:1897) and cwd (:2052).
Threat model & preconditions
- Attacker position: any local unprivileged user.
- Privileges gained or impact: low-grade information disclosure โ the full resolved executable pathname of every process system-wide (other users' and root's). Aids attacker reconnaissance/target selection; inconsistent privilege model vs cwd/args.
- Required config or capabilities: none; default kernel.
- Reachability:
sysctl kern.proc.pathname.<any-pid>.
Proof of concept
PoC source: findings/poc/DF-0015/leak_pathname.sh
Run (unprivileged)
sh findings/poc/DF-0015/leak_pathname.sh
Expected output
kern.proc.pathname.1: /sbin/init kern.proc.pathname.<sshd>: /usr/sbin/sshd ...
Impact
Information disclosure of per-process executable paths to any local user, contradicting the cwd/args visibility policy. Rated Low.
Recommended fix
Apply the same visibility gate used by the args and cwd handlers:
--- a/sys/kern/kern_proc.c
+++ b/sys/kern/kern_proc.c
@@ -2083,6 +2083,7 @@ sysctl_kern_proc_pathname(SYSCTL_HANDLER_ARGS)
char *retbuf, *freebuf;
int error = 0;
struct nchandle nch;
+ struct ucred *cr1 = curproc->p_ucred;
if (arglen != 1)
return (EINVAL);
@@ -2095,6 +2096,10 @@ sysctl_kern_proc_pathname(SYSCTL_HANDLER_ARGS)
p = pfind(*pidp);
if (p == NULL)
return (ESRCH);
+ if ((!ps_argsopen) && p_trespass(cr1, p->p_ucred)) {
+ PRELE(p);
+ return (EPERM);
+ }
}
References
sys/kern/kern_proc.c:2080-2117โsysctl_kern_proc_pathname(no gate).sys/kern/kern_proc.c:1897/:2052โargs/cwdvisibility gate.- CWE-862 Missing Authorization.
Timeline
- 2026-06-29 Discovered during automated file-by-file audit of
sys/kern/kern_proc.c. - pending Reported to DragonFlyBSD security contact.
PoC verification
Evidence pack
findings/poc/DF-0015 ยท 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| leak_pathname.c | trigger-source | reads kern.proc.pathname.<pid> + gated args/cwd contrast via sysctlnametomib+MIB | 3.8 KB | view raw |
| build.sh | build-script | cc -o leak_pathname leak_pathname.c | 180 B | view raw |
| run.sh | run-script | runs leak_pathname as unprivileged user (target pid 1) | 378 B | view raw |
| build.log | build-log | final successful build, full output | 71 B | view raw |
| run.log | run-log | decisive run with kern.ps_argsopen=0: pathname leaks /sbin/init, args/cwd blocked | 1.2 KB | view raw |
| run.default.log | run-log | baseline run with ps_argsopen=1: all three leak | 1.2 KB | view raw |
| leak_sample.txt | leak-sample | leaked exe paths of all root daemons + gate trace | 2.5 KB | view raw |
| VERDICT.md | verdict | full mechanism, ps_argsopen nuance, proof, impact | 4.3 KB | โ raw |
| README.md | readme | build/run/expected for humans | 2.2 KB | โ raw |
| fix.diff | suggested-fix | git-apply-able: add (!ps_argsopen)&&p_trespass() gate to sysctl_kern_proc_pathname (kern_proc.c:2096) | 904 B | view raw |
| env.txt | environment | guest uname, cc version, sysctls | 385 B | view raw |
DF-0015 โ PoC
leak_pathname.c โ unprivileged disclosure of every process's executable
path via kern.proc.pathname.<pid>.
The bug
sysctl_kern_proc_pathname() (sys/kern/kern_proc.c:2080-2117) resolves and
returns the executable path of an arbitrary pid without the
p_trespass/ps_argsopen visibility gate that its siblings
sysctl_kern_proc_args (:1897) and sysctl_kern_proc_cwd (:2052) apply.
The KERN_PROC_PATHNAME node (:2212-2214) is CTLFLAG_RD (world-readable),
so any unprivileged user can read the resolved exe path of any process,
including other users' and root's.
ps_argsopen defaults to 1 (kern_exec.c:103) which disables the args/cwd
gate; an admin restricts inter-process visibility with sysctl
kern.ps_argsopen=0. That hides args/cwd but not pathname โ proving pathname
is the lone un-gated sibling.
Build
cc -o leak_pathname leak_pathname.c # or: ./build.sh
Run
As an unprivileged user (e.g. maxx):
./leak_pathname 1 # or: ./run.sh (default target pid = 1 = init)
For the decisive contrast (args/cwd gated, pathname not), as root first do
sysctl kern.ps_argsopen=0, then run the PoC as the unprivileged user:
kern.proc.pathname.1 returns /sbin/init while kern.proc.args.1 and
.cwd.1 return nothing.
Expected output (bug present)
With kern.ps_argsopen=0:
kern.ps_argsopen = 0 kern.proc.pathname .1 : rc=0 len= 11 '/sbin/init' <-- LEAKED (no gate) kern.proc.args .1 : rc=0 len=0 (blocked/empty) <-- gate works kern.proc.cwd .1 : rc=0 len=0 (blocked/empty) <-- gate works kern.proc.pathname .699 : '/usr/sbin/sshd' (root daemon, leaked) ...
With the default kern.ps_argsopen=1, all three return data (gate disabled).
On a fixed kernel, pathname.1 is also blocked when ps_argsopen=0.
Access-pattern note
These are "node-with-pid-child" sysctls; the pid is passed as a trailing MIB
element via sysctlnametomib("kern.proc.pathname", mib, &len) then
mib[len]=pid, not via the dotted sysctlbyname("kern.proc.pathname.1") form
(which returns ENOENT on DragonFly โ a resolution quirk, not a privilege
control).
DF-0015 โ kern.proc.pathname. discloses the executable path of any process with no visibility check
Verdict
REPRODUCED โ sysctl_kern_proc_pathname resolves and returns the executable
path of any pid with no p_trespass/ps_argsopen gate. Confirmed on
DragonFly master DEV v6.5.0.1712.g89e6a-DEVELOPMENT: as unprivileged maxx
(uid 1001, not in wheel) I read the resolved exe path of root's init (/sbin/init)
and every other root daemon, and proved the sibling args/cwd handlers are
gated while pathname is not.
Mechanism (confirmed by source + run)
sysctl_kern_proc_pathname (sys/kern/kern_proc.c:2080-2117):
pid_t *pidp = (pid_t *)arg1; /* the requested pid */
...
p = pfind(*pidp); /* :2095 -- finds ANY pid */
if (p == NULL) return (ESRCH);
...
if (p->p_textnch.ncp) {
cache_copy(&p->p_textnch, &nch);
error = cache_fullpath(p, &nch, NULL, &retbuf, &freebuf, 0); /* :2102 */
}
...
error = SYSCTL_OUT(req, retbuf, strlen(retbuf) + 1); /* :2110 */
There is no p_trespass(cr1, p->p_ucred) / ps_argsopen authorization
check. Its two siblings enforce it:
sysctl_kern_proc_args(kern_proc.c:1897):if ((!ps_argsopen) && p_trespass(cr1, p->p_ucred)) goto done;sysctl_kern_proc_cwd(kern_proc.c:2052): same gate.
The node (kern_proc.c:2212-2214) is CTLFLAG_RD | CTLFLAG_NOLOCK; as shown in
DF-0006, sysctl reads are not privilege-gated by the framework
(kern_sysctl.c:1446-1450 checks only writes). So reads reach the handler as
any user, and the handler has no internal gate โ the exe path of any process is
disclosed.
The ps_argsopen nuance (and why this is still a real gap)
ps_argsopen defaults to 1 (sys/kern/kern_exec.c:103), which disables
the args/cwd gate (the condition (!ps_argsopen) && ... is false). So in
the default config all three of args/cwd/pathname are open. The
documented way an admin restricts inter-process visibility is
sysctl kern.ps_argsopen=0. With that set:
| node | as maxx, target pid 1 (root init) |
gate line |
|---|---|---|
kern.proc.pathname.1 |
rc=0, /sbin/init LEAKED |
(none) |
kern.proc.args.1 |
rc=0, len=0 blocked | kern_proc.c:1897 |
kern.proc.cwd.1 |
rc=0, len=0 blocked | kern_proc.c:2052 |
i.e. the admin's hardening hides args/cwd but cannot hide pathname โ proving
pathname is the lone un-gated sibling. (run.log = hardened; run.default.log
= ps_argsopen=1 baseline where all three leak.) Every root daemon's exe path was
recovered: init, hammer2, dhclient, devd, syslogd, sshd, cron (see
leak_sample.txt).
Access-pattern note (PoC fix)
These are "node-with-pid-child" sysctls. The original leak_pathname.sh used the
dotted sysctlbyname("kern.proc.pathname.1") form, which returns ENOENT on
DragonFly โ that is a sysctl(8)/sysctlbyname resolution quirk, not a
privilege control. The pid must be passed as a trailing MIB element:
sysctlnametomib("kern.proc.pathname", mib, &len); /* mib=[1,14,9] */
mib[len] = pid;
sysctl(mib, len+1, buf, &buflen, NULL, 0); /* succeeds as any user */
The rewritten leak_pathname.c uses this form, prints pathname + the gated
args/cwd contrast, reads kern.ps_argsopen to label the run, and walks several
root daemons.
Impact
Low-grade information disclosure to any local unprivileged user: the resolved
executable path (including install path / chroot/jail root) of every process
system-wide, even when the admin has set kern.ps_argsopen=0 to restrict
inter-process visibility. Useful attacker reconnaissance / target selection;
inconsistent with the explicit args/cwd privilege policy. Rated Low.
PoC changes
Replaced leak_pathname.sh (which used the non-resolving dotted name and
relied on ps -ax which needs privileges) with leak_pathname.c using the
correct sysctlnametomib+MIB form, reading pathname + gated args/cwd for
contrast, reporting kern.ps_argsopen, and iterating several root daemons.
Added build.sh/run.sh.
Recommended fix
Matches the finding markdown's proposal. Authoritative fix.diff in this folder
adds the same (!ps_argsopen) && p_trespass(...) gate to
sysctl_kern_proc_pathname that the args/cwd siblings use.
Confirmed kernel references
Detail
Exploit chain
none (low-grade authorization/info-disclosure; reveals resolved executable paths / install paths / chroot roots of every process, no memory primitive). Reconnaissance aid for target selection; inconsistent with the args/cwd privilege policy.
Evidence (decisive lines)
running as uid=1001; target pid=1 (not ours) kern.ps_argsopen = 0 === exe-path leak via kern.proc.pathname.<target> === kern.proc.pathname .1 : rc=0 len= 11 '/sbin/init' (LEAKED - no gate) === contrast: gated siblings on the same target === kern.proc.args .1 : rc=0 len=0 (blocked/empty) (gate :1897 works) kern.proc.cwd .1 : rc=0 len=0 (blocked/empty) (gate :2052 works) kern.proc.pathname .699 : '/usr/sbin/sshd' (root daemon, leaked) RUN_EXIT=0
PoC changes
Replaced leak_pathname.sh: (a) the dotted sysctlbyname('kern.proc.pathname.1') form returns ENOENT on DragonFly -- the pid must be passed as a trailing MIB element via sysctlnametomib()+sysctl(); (b) the .sh relied on ps -ax which needs privileges. New leak_pathname.c uses the correct MIB form, prints pathname + the gated args/cwd contrast, reports kern.ps_argsopen to label the run, and iterates root daemons. Added build.sh/run.sh. Full packs in findings/poc/DF-0015/ (VERDICT.md, leak_sample.txt, run.log=hardened, run.default.log=ps_argsopen=1, fix.diff).
Verified recommended fix
fix.diff (git apply --check OK) adds the identical gate the args/cwd siblings use to sysctl_kern_proc_pathname (kern_proc.c:2096, right after pfind): struct ucred *cr1 = curproc->p_ucred; ... if((!ps_argsopen) && p_trespass(cr1, p->p_ucred)) { PRELE(p); return EPERM; }. Matches finding markdown proposal. After the fix, ps_argsopen=0 also hides pathname.
Verdict
REPRODUCED. sysctl_kern_proc_pathname (sys/kern/kern_proc.c:2080-2117) does pfind(*pidp) (:2095) and cache_fullpath+SYSCTL_OUT (:2102/:2110) with NO p_trespass/ps_argsopen gate, unlike siblings sysctl_kern_proc_args (:1897) and sysctl_kern_proc_cwd (:2052) which gate with if((!ps_argsopen) && p_trespass(cr1,p->p_ucred)) goto done;. The KERN_PROC_PATHNAME node (:2212-2214) is CTLFLAG_RD and reads are not framework-gated (kern_sysctl.c:1446-1450). Decisive proof with kern.ps_argsopen=0 (admin hardening): as unprivileged maxx, kern.proc.pathname.1 returns '/sbin/init' (root's init) while kern.proc.args.1 and .cwd.1 return len=0 (blocked) -- proving pathname is the lone un-gated sibling that cannot be restricted. All root daemons' exe paths leaked (init, hammer2, dhclient, devd, syslogd, sshd, cron). Nuance: ps_argsopen defaults to 1 (kern_exec.c:103) which disables the args/cwd gate, so in the default config all three leak; the gap is that pathname leaks even when the admin hardens with ps_argsopen=0. No master code change closes this.