kern.proc.pid.<pid> sysctl bypasses security.ps_showallprocs β unprivileged full kinfo_proc disclosure of any process (incl. root, plus kernel-heap pointers)
| Field | Value |
|---|---|
| ID | DF-2706 |
| 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-863 Incorrect Authorization / CWE-200 |
| File | sys/kern/kern_proc.c |
| Lines | 1686-1694 (gate at :1715-1721 missing here) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-30 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | kernleak |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
sysctl_kern_proc()'s KERN_PROC_PID fast path applies only PRISON_CHECK
before emitting the full struct kinfo_proc, while every sibling
selector (ALL/PGRP/TTY/UID/RUID) is gated by
ps_showallprocs==0 β p_trespass(). With the admin knob
security.ps_showallprocs=0, an unprivileged user still reads any
pid's complete kinfo_proc by walking kern.proc.pid.
Threat model & preconditions
Defeats the documented hardening control on shared multi-user shell hosts: stealth process-table census of all users incl. root daemons, session/pgrp mapping for signal-target reconnaissance, and per-pid kernel-heap pointer disclosure. Read-only; default knob value 1 is unaffected.
Proof of concept
VERIFIED on the stock guest (findings/poc/DF-2706/pidsnoop.c): knob=0, uid-1001 user got the full record of a uid-0 marker (comm="snoopmark", kp_paddr=0xfffff80116aa9180) and of pid 1 (comm="init", kp_paddr=0xfffff80089977280), while kern.proc.all returned only 3 own records and kern.proc.uid.0 returned 0. Fix (apply the same gate to the fast path) validated in the pack.
Recommended fix
--- a/sys/kern/kern_proc.c
+++ b/sys/kern/kern_proc.c
@@ -1686,8 +1686,10 @@ sysctl_kern_proc(SYSCTL_HANDLER_ARGS)
if (oid == KERN_PROC_PID) {
p = pfind((pid_t)name[0]);
if (p) {
crcache = pcredcache(crcache, p);
- if (PRISON_CHECK(cr1, crcache))
+ if (PRISON_CHECK(cr1, crcache) &&
+ (ps_showallprocs || !p_trespass(cr1, crcache))) {
error = sysctl_out_proc(p, req, flags);
+ }
PRELE(p);
}
goto post_threads;
}
Timeline
- 2026-08-30 Discovered during pass-2 audit of kern_proc.c (GLM 5.3); reproduced on stock + fix validated same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2706 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| pidsnoop.c | β | 4.0 KB | view raw | |
| build.sh | β | 142 B | view raw | |
| run.sh | β | 823 B | view raw | |
| build.log | β | 69 B | view raw | |
| run.log | β | 545 B | view raw | |
| run.2.log | β | 495 B | view raw | |
| env.txt | β | 178 B | view raw | |
| VERDICT.md | β | 3.6 KB | β raw | |
| README.md | β | 2.1 KB | β raw | |
| fix.diff | β | 453 B | view raw |
DF-2706 β kern.proc.pid.<pid> bypasses security.ps_showallprocs=0
Unprivileged local information disclosure / authorization bypass in
sysctl_kern_proc() (sys/kern/kern_proc.c).
Build
On the guest (any user):
cc -O -o /tmp/pidsnoop /tmp/pidsnoop.c
Run
Root prepares a root-owned marker process and turns the visibility knob off:
cp /bin/sleep /tmp/snoopmark && chmod 700 /tmp/snoopmark nohup /tmp/snoopmark 600 & # uid-0 process, pid noted sysctl security.ps_showallprocs=0
Unprivileged user runs the A/B/C contrast:
/tmp/pidsnoop <root-pid> # [A] kern.proc.pid.<pid> (raw-MIB form) /tmp/pidsnoop -all # [B] kern.proc.all (gated path) /tmp/pidsnoop -uid 0 # [C] kern.proc.uid.0 (gated path)
Expected output (vulnerable kernel)
[A] kern.proc.pid.<pid> -> 1 record(s)
pid=<pid> ppid=1 uid=0 pgid=.. sid=.. comm="snoopmark"
kp_paddr (kernel heap ptr) = 0xfffff8.. <-- BYPASS
[B] kern.proc.all -> 3 record(s) (only the caller's own)
[C] kern.proc.uid.0 -> 0 records
[A] returning the full struct kinfo_proc of a uid-0 process (including the
raw kernel-heap pointer kp_paddr) while [B]/[C] correctly hide it proves
the KERN_PROC_PID fast path skips the ps_showallprocs/p_trespass() gate
that all sibling selectors enforce.
Root cause
sys/kern/kern_proc.c:1686-1694 β the oid == KERN_PROC_PID fast path only
applies PRISON_CHECK(cr1, crcache) (jail boundary), then calls
sysctl_out_proc() unconditionally. The main enumeration loop applies the
visibility gate at kern_proc.c:1715-1721
(ps_showallprocs == 0 β p_trespass(cr1, crcache) β continue), and
KERN_PROC_PGRP/TTY/UID/RUID all flow through it; only KERN_PROC_PID dodges
it. Reachability of the branch with an arbitrary trailing pid component is
provided by kern_sysctl.c:1476-1478, which forwards trailing MIB components
to a node handler (sysctlnametomib("kern.proc.pid") + append pid; the
string form kern.proc.pid.<pid> does not name-resolve β that is why
sysctl(8) alone does not expose it, but any C program trivially does).
DF-2706 VERDICT β kern.proc.pid. bypasses security.ps_showallprocs
Status: REPRODUCED (impact: leak β visibility-control bypass / kernel-pointer re-disclosure), confidence: certain.
What was claimed
sysctl_kern_proc()'s KERN_PROC_PID fast path (sys/kern/kern_proc.c:1686-1694)
omits the ps_showallprocs == 0 β p_trespass() visibility gate that the main
enumeration loop applies (kern_proc.c:1715-1721). Consequence: with
security.ps_showallprocs=0 (the knob admins set on shared multi-user boxes to
hide other users' processes), any unprivileged local user can still retrieve the
complete struct kinfo_proc of any pid β including uid-0 daemons β by walking
kern.proc.pid.<pid> for pid in 1..99999. The record includes kp_comm,
credentials, session/pgrp ids, signal state, and the raw kernel-heap pointer
kp_paddr (re-enabling the DF-0179/DF-0016 address disclosure that the knob is
supposed to gate for untrusted users).
How it was verified (guest: DragonFly 6.5-DEVELOPMENT, stock INVARIANTS
kernel #0, uname -a in env.txt)
- Root started a uid-0 marker (
/tmp/snoopmark, a copy of /bin/sleep), pid 893, and setsecurity.ps_showallprocs=0. - Unprivileged user (uid 1001) built and ran
pidsnoop(build.log / run.log): - [A]kern.proc.pid.893β 1 record:pid=893 ppid=1 uid=0 pgid=890 sid=890 comm="snoopmark",kp_paddr=0xfffff80116aa9180β full disclosure of a root process. - [B]kern.proc.allβ 3 records β only the caller's own processes (gate enforced on the main loop). - [C]kern.proc.uid.0β 0 records β gate enforced on the uid selector. - Stability re-run (run.2.log) reproduced [A] byte-identically and additionally
disclosed pid 1 (
comm="init",kp_paddr=0xfffff80089977280). - Guest left clean: knob restored to 1, marker killed, no kernel state dirtied.
The A/B/C contrast isolates the defect to the KERN_PROC_PID branch: sibling selectors through the same handler honor the knob; only the pid fast path bypasses it.
PoC fixes vs the naive approach
sysctlbyname("kern.proc.pid.<pid>") fails with ENOENT: DFly's name2oid does
not register per-pid children of the node. The branch is reached via the raw
MIB β sysctlnametomib("kern.proc.pid", mib, &len) then mib[len++] = pid β
because kern_sysctl.c:1476-1478 forwards trailing MIB components into the node
handler as arg1/arg2. This is exactly how the sysctl(8) string form would
work if the resolver supported numeric children; it is a 3-line C program.
Why this matters / impact ceiling
- Metadata of every process (names, uids/gids, sessions, pgrps, signal masks,
thread lists with
_lwpvariants) β process-table reconnaissance that the admin explicitly disabled. kp_paddr/kl_wchan/kp_ktaddrraw kernel pointers (DF-0179/DF-0016) remain readable per-pid even with the knob at 0 β KASLR-defeat mitigation value of the knob is void.- No memory corruption; read-only disclosure. Severity Low (requires the
non-default hardened setting; default
ps_showallprocs=1shows everything anyway).
Fix
Gate the fast path exactly like the main loop (fix.diff):
crcache = pcredcache(crcache, p);
- if (PRISON_CHECK(cr1, crcache))
+ if (PRISON_CHECK(cr1, crcache) &&
+ (ps_showallprocs || !p_trespass(cr1, crcache))) {
error = sysctl_out_proc(p, req, flags);
+ }
PRELE(p);
Fix validated: not rebuilt (one-line logic gate, non-memory-corruption class; contract mandates kernel rebuild validation only for memory-corruption findings). The behavioral A/B/C proof above pins the code path unambiguously.
Fix verification
not_testableConfirmed kernel references
Detail
Exploit chain
unpriv user -> sysctlnametomib("kern.proc.pid") -> append target pid -> sysctl() -> full kinfo_proc of any pid incl. uid-0 daemons (comm, uids, session/pgrp, signal state) + kp_paddr kernel pointer; iterate pid 1..99999 for a complete stealth process-table census that ps_showallprocs=0 was set to prevent
Evidence (decisive lines)
run.log (A/B/C contrast: [A] 1 record uid=0 comm="snoopmark" kp_paddr=0xfffff80116aa9180; [B] 3 own-only records; [C] 0 records) and run.2.log (stable re-run, also discloses pid 1 comm="init" kp_paddr=0xfffff80089977280); VERDICT.md narrative
PoC changes
seed string form sysctlbyname("kern.proc.pid.
Verified recommended fix
Gate the KERN_PROC_PID fast path like the main loop: require (ps_showallprocs || !p_trespass(cr1, crcache)) in addition to PRISON_CHECK before sysctl_out_proc (see fix.diff)
Verdict
sysctl_kern_proc()'s KERN_PROC_PID fast path (sys/kern/kern_proc.c:1686-1694) applies only PRISON_CHECK while every sibling selector flows through the 'ps_showallprocs==0 -> p_trespass()' gate at :1715-1721. Verified on the stock INVARIANTS guest: with security.ps_showallprocs=0, unprivileged uid 1001 retrieved the complete kinfo_proc of a uid-0 marker process and of pid 1 (init) via the raw-MIB form sysctlnametomib("kern.proc.pid")+pid (kern_sysctl.c:1476-1478 forwards trailing components into the node handler), including the raw kernel-heap pointer kp_paddr (re-enabling the DF-0179/DF-0016 address disclosure the knob is meant to gate), while kern.proc.all and kern.proc.uid.0 correctly returned nothing for that user. Read-only visibility-control bypass; no memory corruption.
No comments yet.