β¬’ DragonFlyBSD Kernel Audit
← triage Β· 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.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0015 Β· 17 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 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
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, Phase-8 fix-validation before/after
↓ 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.


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 maxx reads /sbin/init for pid 1 and /sbin/hammer2 for pid 68 etc. β€” disclosure of every root daemon's exe path (saved in baseline_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). Sibling args/cwd behavior unchanged (still gated). Default-config behavior unchanged (gate disabled by ps_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 in fix_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 (sha256 1d6fb7c8…7597665202 β€” see env_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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. 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 ===
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Thu Jul 2 19:06:46 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (sha256 /boot/kernel/kernel = 1d6fb7c8ab4704babd76f98d4c92cf6a279d272dbe027e03ab5cad7597665202)

Confirmed kernel references

Detail

Exploit chain

unprivileged exe-path disclosure of arbitrary processes via kern.proc.pathname.: no privilege/visibility check in the handler, so any local user reads the resolved executable path (including install path / chroot root) of every other process (root daemons, other users) even when the admin has set kern.ps_argsopen=0 to restrict inter-process visibility. Low-grade info disclosure (reconnaissance / target selection); not a memory-corruption primitive so no further chain to develop.

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.