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.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0015 Β· 17 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 | PoC final successful build, full output | 71 B | view raw |
| baseline_run.log | run-log | BASELINE #0 hardened (ps_argsopen=0): pathname.1 leaks /sbin/init, args/cwd gated | 1.2 KB | view raw |
| run.log | run-log | prior decisive run with kern.ps_argsopen=0 on baseline | 1.2 KB | view raw |
| run.default.log | run-log | prior baseline run with ps_argsopen=1: all three leak | 1.2 KB | view raw |
| fix_run.log | fix-run-log | PATCHED #1 hardened (ps_argsopen=0): pathname.1 returns rc=-1 EPERM (denied); self still ok | 1.2 KB | view raw |
| fix_build.log | fix-build-log | FULL nativekernel build output, single-fix kernel, rc=0 | 5.6 MB | β download |
| 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 |
| leak_sample.txt | leak-sample | leaked exe paths of all root daemons + gate trace | 2.5 KB | view raw |
| env.txt | environment | guest uname (baseline #0), cc version, sysctls | 385 B | view raw |
| env_patched.txt | environment | guest uname (single-fix #1) + kernel sha256 | 313 B | view raw |
| VERDICT.md | verdict | full mechanism, ps_argsopen nuance, proof, impact, Phase-8 fix-validation before/after | 6.6 KB | β raw |
| README.md | readme | build/run/expected for humans | 2.2 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 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.
Fix validation (Phase 8) β VALIDATED
Built a single-fix kernel from with-src baseline + fix.diff only, booted
it, and re-ran the same PoC. Clean before/after contrast:
| kernel | build | kern.ps_argsopen |
pathname.1 (root init) |
args.1 |
cwd.1 |
pathname.<self> |
|---|---|---|---|---|---|---|
| #0 unpatched (base) | 6cc⦠| 0 (hardened) |
rc=0 len=11 /sbin/init (LEAK) |
rc=0 len=0 | rc=0 len=0 | rc=0 ok (own path) |
| #0 unpatched (base) | 6cc⦠| 1 (default) |
rc=0 /sbin/init (all three leak β gate disabled) |
rc=0 | rc=0 | rc=0 ok |
| #1 single-fix | this | 0 (hardened) |
rc=-1 errno=EPERM "Operation not permitted" | rc=0 len=0 | rc=0 len=0 | rc=0 ok (own path) |
| #1 single-fix | this | 1 (default) |
rc=0 /sbin/init (no regression β gate disabled) |
rc=0 | rc=0 | rc=0 ok |
- Before (baseline #0, hardened): unprivileged
maxxreads/sbin/initfor pid 1 and/sbin/hammer2for pid 68 etc. β disclosure of every root daemon's exe path (saved inbaseline_run.log). - After (single-fix #1, hardened): the same query returns
EPERM (Operation not permitted), deterministic across 3 consecutive runs. Self pid query still works (/home/maxx/poc/DF-0015/leak_pathname). Siblingargs/cwdbehavior unchanged (still gated). Default-config behavior unchanged (gate disabled byps_argsopen=1, all three return data β no regression). Full output:fix_run.log.
Build / install / boot details
- Applied
fix.diff(2 hunks, both succeeded at lines 2086/2096) to/usr/src/sys/kern/kern_proc.c. Verified patch landed. - Build:
cd /usr/src && make -j6 nativekernel KERNCONF=X86_64_GENERICβ=== NK_DONE rc=0 ===(full log infix_build.log). - Install: copied
kernel.stripped(15.7 MB) +kernel.debug(119 MB) to/boot/kernel/,sync. - Reboot:
vm.sh down && vm.sh up.kern.version:DragonFly 6.5-DEVELOPMENT #1: Thu Jul 2 19:06:46 UTC 2026(sha2561d6fb7c8β¦7597665202β seeenv_patched.txt). - fix_status = fixed. Bad behavior gone on patched, present on baseline; self-path query (no-regression) and default-config all preserved.
Fix verification
fixedVALIDATED. fix.diff applied cleanly (2 hunks at kern_proc.c:2086/2096), built single-fix kernel #1 (nativekernel rc=0), installed kernel.stripped+debug to /boot/kernel, rebooted. BASELINE #0 (unpatched, kern.ps_argsopen=0): pathname.1 leaked '/sbin/init' to unprivileged maxx while args.1/cwd.1 were correctly gated. SINGLE-FIX #1 (same hardened config): pathname.1 now returns rc=-1 errno=EPERM 'Operation not permitted' (deterministic across 3 runs); self-path query still works (no regression); default ps_argsopen=1 mode returns data normally for all three nodes (no regression). => fix closes the disclosure.
baseline (#0 hardened): kern.proc.pathname.1 = rc=0 len=11 '/sbin/init' (LEAK), kern.proc.args.1/.cwd.1 = rc=0 len=0 (gated) patched (#1 hardened): kern.proc.pathname.1 = rc=-1 errno=1 (Operation not permitted) (DENIED), self pathname = rc=0 '/home/maxx/poc/DF-0015/leak_pathname' (no regression) patched (#1 default ps_argsopen=1): pathname.1/args.1/cwd.1 all return data normally (no regression) nativekernel build: === NK_DONE rc=0 ===
Confirmed kernel references
Detail
Exploit chain
unprivileged exe-path disclosure of arbitrary processes via kern.proc.pathname.
Evidence (decisive lines)
BASELINE #0 (kern.ps_argsopen=0, as maxx uid 1001): kern.proc.pathname.1 : rc=0 len=11 '/sbin/init' <-- LEAKED (no gate) kern.proc.args.1 : rc=0 len=0 (blocked/empty) <-- sibling gate works kern.proc.cwd.1 : rc=0 len=0 (blocked/empty) <-- sibling gate works kern.proc.pathname.68 : rc=0 len=14 '/sbin/hammer2' (root daemon, leaked) PATCHED #1 (kern.ps_argsopen=0, as maxx uid 1001): kern.proc.pathname.1 : rc=-1 errno=1 (Operation not permitted) <-- DENIED kern.proc.pathname.<self> : rc=0 '/home/maxx/poc/DF-0015/leak_pathname' <-- no regression kern.proc.args.1 / cwd.1 : rc=0 len=0 (gated, unchanged)
PoC changes
No source change to leak_pathname.c (already correct). Added baseline_run.log (decisive hardened-config run on #0), env_patched.txt (single-fix kernel #1 uname + sha256), fix_run.log (full patched-kernel PoC output), fix_build.log (full 35456-line nativekernel output, rc=0). Updated VERDICT.md with the Phase-8 before/after table and manifest.json with the fix-validation fields and new artifacts.
Verified recommended fix
Add the same inter-process visibility gate the args (kern_proc.c:1897) and cwd (kern_proc.c:2052) handlers use to sysctl_kern_proc_pathname: declare struct ucred cr1 = curproc->p_ucred; then immediately after pfind(pidp) succeeds in the else branch (kern_proc.c:2096), if ((!ps_argsopen) && p_trespass(cr1, p->p_ucred)) { PRELE(p); return (EPERM); }. Matches the finding markdown's proposal verbatim. Full git-apply-able diff in findings/poc/DF-0015/fix.diff.
Verdict
REPRODUCED + FIX VALIDATED. sysctl_kern_proc_pathname (sys/kern/kern_proc.c:2080-2117) resolves and returns the executable path of any pid with no p_trespass/ps_argsopen gate, unlike its siblings sysctl_kern_proc_args (:1897) and sysctl_kern_proc_cwd (:2052). Confirmed on the unpatched master DEV #0 kernel: as unprivileged maxx (uid 1001) with kern.ps_argsopen=0, kern.proc.pathname.1 returned '/sbin/init' (root's init, len=11) while kern.proc.args.1 and kern.proc.cwd.1 were correctly gated to len=0 -- pathname is the lone un-gated sibling. fix.diff adds the identical (!ps_argsopen) && p_trespass(cr1, p->p_ucred) gate to the pathname handler; built single-fix kernel #1, rebooted, re-ran the SAME PoC: pathname.1 now returns rc=-1 errno=EPERM 'Operation not permitted' (deterministic across 3 consecutive runs), self-path query still works, args/cwd behavior unchanged, and default-config (ps_argsopen=1) is unaffected. fix_status=fixed.
No comments yet.