World-readable sysctl kern.file exports raw kernel heap pointers (kinfo_file.f_file/f_data) and cross-user fd state (types/flags/offsets) to unprivileged users
| Field | Value |
|---|---|
| ID | DF-2684 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N |
| CWE | CWE-200 Exposure of Sensitive Information |
| File | sys/kern/kern_descrip.c |
| Lines | 3490 (sink subr_kcore.c:64/69) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-29 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | kernleak |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
sysctl_kern_file_callback (:3453-3503) filters by prison only;
sysctl_root gates writes only, so kern.file is world-readable.
kcore_make_file copies ufile->f_file = kfile and
ufile->f_data = kfile->f_data β direct kernel-heap addresses of every
open struct file and its vnode/socket β plus f_offset/f_flag/f_count of
every process's fds. Userland sysctl(8) discards CTLTYPE_OPAQUE output,
hiding the leak from casual inspection; a direct sysctl(3) read returns
it all.
Threat model & preconditions
Unprivileged local info leak: defeats KASLR (were it enabled) and reveals objcache/slab layout for heap-grooming against UAF-class bugs (e.g. DF-2682); separately discloses other users' β including root's β descriptor types, flags and file offsets (demonstrated: root's read position on a password file visible to uid 1001).
Proof of concept
VERIFIED (findings/poc/DF-2684/kfile_leak.c): direct sysctl as uid 1001 β "entries with kernel pointers: 136 / entries belonging to OTHER uids: 120 / RESULT: LEAK CONFIRMED" with direct-map f_file addresses. Fix validated in-guest (nativekernel + install + reboot): post-fix uid 1001 sees f_file=0x0/f_data=0x0 on all rows (0 pointers) while root still sees 143 β deterministic baseline-vs-patched.
Recommended fix
Mask kf.f_file/kf.f_data for readers with td_ucred->cr_uid != 0 in sysctl_kern_file_callback (validated fix.diff in the pack); consider p_cansee()-style row filtering for the cross-user fd-offset disclosure as a follow-up.
Timeline
- 2026-08-29 Discovered during pass-2 audit of kern_descrip.c (GLM 5.3); leak reproduced + fix validated same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2684 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| kfile_leak.c | β | 2.4 KB | view raw | |
| build.sh | β | 110 B | view raw | |
| run.sh | β | 74 B | view raw | |
| run.log | β | 1.1 KB | view raw | |
| run_fixed.log | β | 731 B | view raw | |
| leak_sample.txt | β | 460 B | view raw | |
| env.txt | β | 326 B | view raw | |
| VERDICT.md | β | 3.2 KB | β raw | |
| fix.diff | β | 515 B | view raw | |
| verdict.json | β | 3.0 KB | view raw | |
| manifest.json | β | 774 B | view raw |
DF-2684 β VERDICT
status: reproduced (impact: leak); fix validated (fixed).
Root cause
sysctl_kern_file (sys/kern/kern_descrip.c:3410-3450) and its callback
sysctl_kern_file_callback (kern_descrip.c:3453-3503) export every
process's descriptor table through kcore_make_file()
(sys/kern/subr_kcore.c:58-77), which copies raw kernel pointers into
the exported structure:
ufile->f_file = kfile; /* address of struct file */ ufile->f_data = kfile->f_data; /* vnode / socket / pipe */
The sysctl read path applies no privilege gate: sysctl_root()
(sys/kern/kern_sysctl.c:1410+) privilege-checks only writes
(CTLFLAG_WR / SYSCAP_NOSYSCTL_WR); kern.file is CTLFLAG_RD, and the
callback's only filter is the prison check at kern_descrip.c:3464.
Any unprivileged local user therefore receives, for every process in
their jail (i.e. the whole system for the common non-jailed case):
- the kernel-heap address of every open
struct file(objcache slab layout disclosure -- defeats any future KASLR, aids heap grooming for UAF/overflow exploits elsewhere); - the kernel address of the underlying vnode/socket/pipe;
- other users' fd state: type, flags, reference count and FILE OFFSET (e.g. root's read position on a sensitive file), an information disclosure across uid boundaries.
(Note: the base-system sysctl(8) tool discards CTLTYPE_OPAQUE
output, which is why this is not commonly noticed -- a direct
sysctl(3) call reads it fine.)
Reproduction
kfile_leak.c (unprivileged, uid 1001) calls
sysctl({CTL_KERN, KERN_FILE}, ...) and parses struct kinfo_file.
On the stock INVARIANTS guest:
uid=1001 fetched 136 kinfo_file entries (9792 bytes) pid=278 uid=0 fd=0 type=1 f_file=0xfffff80116c49380 f_data=0xfffff8008f521b00 off=112 fl=3 ... entries with kernel pointers: 136 entries belonging to OTHER uids: 120 RESULT: LEAK CONFIRMED
120 of 136 rows belong to other uids (0, 77, ...) -- root's fd offsets included (off=112 on root's open of the password file in the demonstration).
Fix validation (fixed)
fix.diff masks kf.f_file / kf.f_data for readers whose
td_ucred->cr_uid != 0 in sysctl_kern_file_callback. Built into
the guest kernel (make -j6 nativekernel KERNCONF=X86_64_GENERIC,
rc=0, installed, rebooted; the same kernel also carried the
independent sigio restructure under test for DF-2682/2683 -- the
sysctl mask change is in a different function and branch) and
re-ran the exact PoC:
as uid 1001: entries with kernel pointers: 0 (f_file=0x0, f_data=0x0) as root : entries with kernel pointers: 143 (unchanged behavior)
Baseline leak vs patched mask, deterministic. A complete upstream fix
may additionally want p_cansee()-style filtering of other users'
rows (as this pack's cross-uid offset disclosure documents); the
shipped minimal fix addresses the kernel-pointer disclosure.
Bottom line
World-readable kern.file hands unprivileged users kernel heap pointers for every open file in the system plus cross-user descriptor state (including file offsets). Pointer disclosure confirmed and masked by the validated fix.
Fix verification
not_testableConfirmed kernel references
Detail
Exploit chain
unprivileged sysctl(3) {CTL_KERN, KERN_FILE} -> kernel-heap addresses of every struct file and its vnode/socket + other users' fd offsets; the addresses are direct-map slab pointers usable to defeat KASLR and to aim UAF-reclaim grooming (e.g. for DF-2682-class bugs) -- no further chain needed for the leak itself.
Evidence (decisive lines)
["run.log: 'entries with kernel pointers: 136 / entries belonging to OTHER uids: 120 / RESULT: LEAK CONFIRMED' as uid 1001", 'leak_sample.txt: pointer values across runs (0xfffff8xxxxxxxxxx direct-map slab addresses)', "run_fixed.log: post-fix 'entries with kernel pointers: 0' as uid 1001 and '143' as root", 'VERDICT.md: root cause, why sysctl(8) hides it (opaque type), fix validation narrative']
PoC changes
Direct sysctl(3) consumer written from scratch (userland sysctl(8) discards CTLTYPE_OPAQUE output, so the seed idea of shelling out to sysctl(8) showed nothing).
Verified recommended fix
Mask kinfo_file.f_file/f_data for unprivileged readers in sysctl_kern_file_callback (validated fix.diff); consider p_cansee()-based row filtering for cross-user fd state.
Verdict
sysctl kern.file (CTL_KERN/KERN_FILE) is world-readable: sysctl_root() gates only writes, and sysctl_kern_file_callback (kern_descrip.c:3453-3503) filters by prison only. kcore_make_file (subr_kcore.c:58-77) copies ufile->f_file = kfile and ufile->f_data = kfile->f_data -- raw kernel heap pointers -- into every exported kinfo_file. Unprivileged uid 1001 read 136 entries: 136 carried non-NULL kernel pointers (struct file and vnode/socket addresses) and 120 belonged to other uids including root's fd types/flags/offsets (off=112 on root's password-file read). Pointer disclosure aids KASLR defeat and heap grooming for memory-corruption exploits; cross-uid fd-offset disclosure is an information leak across uid boundaries. Validated fix masks f_file/f_data for uid!=0: post-fix unprivileged read shows 0 pointers while root still sees 143 -- baseline-vs-patched deterministic.
No comments yet.