โฌข DragonFlyBSD Kernel Audit
โ† dashboard
DF-0015

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.

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

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
FileTypeDescriptionSize
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
README.md readme build/run/expected for humans
โ†“ download 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).

VERDICT.md verdict full mechanism, ps_argsopen nuance, proof, impact
โ†“ download raw

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.

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.