VFS_CONF (vfs.generic) sysctl leaks kernel pointers (vfc_vfsops, vfc_next) to unprivileged users
| Field | Value |
|---|---|
| ID | DF-0009 |
| 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-200 Exposure of Sensitive Information to an Unauthorized Actor |
| File | sys/kern/vfs_subr.c |
| Lines | 1839-1845, 1850, 1863 |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-06-29 |
| Reported | pending |
Summary
vfs_sysctl() handles the VFS_CONF request by copying the entire
struct vfsconf to userspace via SYSCTL_OUT(req, vfsp, sizeof *vfsp).
struct vfsconf (sys/sys/mount.h:477-484) contains struct vfsops
*vfc_vfsops (a kernel pointer to a function-pointer ops table) and
STAILQ_ENTRY(vfsconf) vfc_next (a kernel list pointer). The vfs.generic
sysctl node is registered CTLFLAG_RD with no privilege gate
(vfs_subr.c:1850), and sysctl reads are not privilege-gated in DragonFlyBSD,
so any local user can read these raw kernel addresses โ defeating KASLR, a
prerequisite primitive for privilege-escalation chains. The legacy
sysctl_ovfs_conf_iter() path (vfs_subr.c:1863) likewise copies
vfc_vfsops verbatim.
Root cause
sys/kern/vfs_subr.c:1839-1845:
case VFS_CONF:
if (namelen != 3)
return (ENOTDIR);
vfsp = vfsconf_find_by_typenum(name[2]);
if (vfsp == NULL)
return (EOPNOTSUPP);
return (SYSCTL_OUT(req, vfsp, sizeof *vfsp)); /* copies the WHOLE struct */
struct vfsconf (sys/sys/mount.h:477-484):
struct vfsconf {
struct vfsops *vfc_vfsops; /* :478 kernel .text (ops vector) */
...
STAILQ_ENTRY(vfsconf) vfc_next; /* :483 kernel .data (list pointer) */
...
};
Both are kernel pointers copied raw to userspace. The node is
SYSCTL_NODE(_vfs, VFS_GENERIC, generic, CTLFLAG_RD, vfs_sysctl, ...)
(vfs_subr.c:1850); CTLFLAG_RD is read-by-anyone, and sysctl_root
privilege-checks only writes, so the read is ungated. The user-side mib is
{CTL_VFS, VFS_GENERIC, VFS_CONF, typenum} (the handler's name = arg1 - 1 /
namelen = arg2 + 1 hack at :1810-1811 re-includes VFS_GENERIC as
name[0]). The legacy iterator at :1863 copies vfc_vfsops into the
ovfsconf shadow struct verbatim (with a /* XXX used as flag */ comment
noting userland abuses it as an "is-configured" flag).
Threat model & preconditions
- Attacker position: any local unprivileged user.
- Privileges gained or impact: information disclosure โ live kernel
.textaddresses (per-filesystemvfsopsvectors) and.dataaddresses (thevfsconflist). A reliable KASLR-bypass / kernel-ASLR-defeat primitive, used to relocate gadgets/RIP for a second bug. Standalone impact is info-leak only. - Required config or capabilities: none; default kernel.
- Reachability:
sysctlon{CTL_VFS, VFS_GENERIC, VFS_CONF, typenum}(and the legacyvfs.genericiteration).
Proof of concept
PoC source: findings/poc/DF-0009/leak_vfsconf.c
Iterates filesystem type numbers, reads each struct vfsconf via the mib, and
prints vfc_vfsops / vfc_next.
Build & run
cc -o leak_vfsconf findings/poc/DF-0009/leak_vfsconf.c ./leak_vfsconf # as a non-root user
Expected output
type 0 ufs vfc_vfsops=0xffffffff80abcdef vfc_next=0xffff8000xxxxxxxx ... kernel .text pointers leaked (vfc_vfsops): N result: LEAK CONFIRMED (KASLR-defeat primitive)
Impact
Lowers the bar for exploiting any future local kernel memory-corruption bug by defeating KASLR and revealing kernel data layout. Information disclosure only (addresses, not arbitrary memory); rated Low.
Recommended fix
Redact the kernel pointers before copyout (preserves struct ABI/size), and do
the same in the legacy ovfsconf path. Userland that abused vfc_vfsops as an
"is-configured" flag should switch to vfc_typenum/vfc_refcount.
--- a/sys/kern/vfs_subr.c
+++ b/sys/kern/vfs_subr.c
@@ -1842,7 +1842,14 @@
vfsp = vfsconf_find_by_typenum(name[2]);
if (vfsp == NULL)
return (EOPNOTSUPP);
- return (SYSCTL_OUT(req, vfsp, sizeof *vfsp));
+ {
+ struct vfsconf vfc;
+ vfc = *vfsp;
+ /* do not disclose kernel pointers to unprivileged readers */
+ vfc.vfc_vfsops = NULL;
+ vfc.vfc_next.stqe_next = NULL;
+ return (SYSCTL_OUT(req, &vfc, sizeof(vfc)));
+ }
}
And in sysctl_ovfs_conf_iter() (:1863) set ovfs.vfc_vfsops from a non-
pointer configured-flag rather than copying the raw vfsops address. A
stronger long-term fix is to expose a dedicated, pointer-free kinfo_vfsconf.
References
sys/kern/vfs_subr.c:1845โVFS_CONFraw copyout ofstruct vfsconf.sys/kern/vfs_subr.c:1863โ legacy ovfsconf copiesvfc_vfsops.sys/sys/mount.h:477-484โstruct vfsconfpointer fields.- CWE-200 Exposure of Sensitive Information to an Unauthorized Actor.
Timeline
- 2026-06-29 Discovered during automated file-by-file audit of
sys/kern/vfs_subr.c. - pending Reported to DragonFlyBSD security contact.
PoC verification
Evidence pack
findings/poc/DF-0009 ยท 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| leak_vfsconf.c | trigger-source | unprivileged sysctl reader of VFS_CONF; prints vfc_vfsops/vfc_next per fs type | 2.5 KB | view raw |
| VERDICT.md | verdict | full narrative: reproduced, line-by-line trace, evidence table vs nm | 5.7 KB | โ raw |
| README.md | readme | human build/run/expected summary | 2.6 KB | โ raw |
| build.sh | build-script | cc -o leak_vfsconf leak_vfsconf.c | 235 B | view raw |
| run.sh | run-script | ./leak_vfsconf as unprivileged user | 330 B | view raw |
| build.log | build-log | final successful build, full output | 70 B | view raw |
| run.log | run-log | decisive run 1, full output incl 11 leaked .data pointers | 1.0 KB | view raw |
| run.2.log | run-log | stability run 2 (byte-identical) | 1.0 KB | view raw |
| run.3.log | run-log | stability run 3 (byte-identical) | 1.0 KB | view raw |
| leak_sample.txt | leak-sample | nm cross-ref: each leaked vfc_vfsops matches an exact kernel symbol; kernel text/data bounds | 484 B | view raw |
| env.txt | environment | uname, cc version, nm symbol table | 665 B | view raw |
| fix.diff | suggested-fix | redact vfc_vfsops/vfc_next in VFS_CONF and ovfs_conf paths (git apply --check passes) | 1.2 KB | view raw |
| manifest.json | manifest | this catalog | 2.4 KB | view raw |
DF-0009 โ PoC
leak_vfsconf.c โ unprivileged leak of kernel .data pointers via the
VFS_CONF (vfs.generic) sysctl.
Verdict
REPRODUCED on DragonFly master DEV (v6.5.0.1712.g89e6a-DEVELOPMENT,
2026-06-29). As the unprivileged maxx user (uid 1001, not in wheel), the PoC
dumps all 11 filesystem-type struct vfsconf records. Each leaks two kernel
.data pointers โ vfc_vfsops (the per-filesystem ops vector) and vfc_next
(the vfsconf list link) โ 11 + 10 raw kernel addresses total. Every
vfc_vfsops value matches an exact symbol in nm /boot/kernel/kernel
(e.g. devfs_vfsops=0xffffffff81111ae0, hammer_vfsops=0xffffffff81112000,
tmpfs_vfsops=0xffffffff81117200). Byte-identical across 3 runs โ deterministic,
reliable KASLR-defeat. See VERDICT.md for the full line-by-line trace and the
evidence table.
The issue
vfs_sysctl()'s VFS_CONF handler (sys/kern/vfs_subr.c:1845) copies the
whole struct vfsconf to userspace:
return (SYSCTL_OUT(req, vfsp, sizeof *vfsp)); /* :1845 whole struct */
struct vfsconf (sys/sys/mount.h:477-484) embeds struct vfsops *vfc_vfsops
(kernel .data ops vector) and STAILQ_ENTRY(vfsconf) vfc_next (kernel .data
list pointer). The vfs.generic node is CTLFLAG_RD with no privilege gate
(vfs_subr.c:1850), and sysctl reads are not privilege-gated, so any
unprivileged local user can dump these addresses โ a reliable KASLR-bypass
primitive. The legacy ovfsconf path (sysctl_ovfs_conf_iter, :1863) copies
vfc_vfsops verbatim too.
Build
./build.sh # cc -o leak_vfsconf leak_vfsconf.c
Run (as an UNPRIVILEGED user)
./run.sh # ./leak_vfsconf
Expected output (bug present)
sizeof(struct vfsconf) = 48 type 1 hammer vfc_vfsops=0xffffffff81112000 vfc_next=0xffffffff8110fa40 type 2 mfs vfc_vfsops=0xffffffff8110fa80 vfc_next=0xffffffff810ebb40 ... type 11 tmpfs vfc_vfsops=0xffffffff81117200 vfc_next=0x0 filesystem types dumped: 11 kernel .text pointers leaked (vfc_vfsops): 11 kernel .data pointers leaked (vfc_next) : 10 result: LEAK CONFIRMED (KASLR-defeat primitive)
On a fixed kernel the printed vfc_vfsops/vfc_next would be 0x0 and the
PoC exits 2 (no kernel pointers observed).
Impact
Information disclosure only (kernel .data addresses, not memory contents).
Standalone impact is Low, but it is a prerequisite primitive for exploiting any
future local kernel memory-corruption bug (KASLR defeat / gadget relocation).
Reachable by any unprivileged local user on a default kernel; no config.
DF-0009 โ VFS_CONF (vfs.generic) sysctl leaks kernel pointers to unprivileged users
Verdict
REPRODUCED โ deterministic, unprivileged disclosure of kernel .data
addresses (the per-filesystem struct vfsops instances and the vfsconf
linked-list pointers) via the VFS_CONF sysctl handler. 11 filesystem types โ
11 distinct kernel .data pointers leaked on every run, byte-identical across
runs, every one confirmed against nm /boot/kernel/kernel. This is a reliable
KASLR-defeat / pointer-disclosure primitive, reachable by any local user
with no privileges and no special setup.
The bug โ confirmed line-by-line
vfs_sysctl() (sys/kern/vfs_subr.c:1839-1845):
case VFS_CONF:
if (namelen != 3)
return (ENOTDIR); /* :1841 overloaded */
vfsp = vfsconf_find_by_typenum(name[2]);
if (vfsp == NULL)
return (EOPNOTSUPP); /* :1844 */
return (SYSCTL_OUT(req, vfsp, sizeof *vfsp)); /* :1845 whole struct */
SYSCTL_OUT(req, vfsp, sizeof *vfsp) copies the entire struct vfsconf
(sys/sys/mount.h:477-484) to userspace, including two kernel-pointer fields:
struct vfsconf {
struct vfsops *vfc_vfsops; /* :478 -> .data (ops vector) */
char vfc_name[MFSNAMELEN];
int vfc_typenum;
int vfc_refcount;
int vfc_flags;
STAILQ_ENTRY(vfsconf) vfc_next; /* :483 -> .data (list link) */
};
The sysctl node is registered with no privilege gate
(sys/kern/vfs_subr.c:1850):
SYSCTL_NODE(_vfs, VFS_GENERIC, generic, CTLFLAG_RD, vfs_sysctl,
"Generic filesystem");
CTLFLAG_RD means readable by any user, and sysctl_root() privilege-checks
only writes, so the read path is ungated โ confirmed by the PoC running as
uid=1001(maxx) (not in wheel) and succeeding.
The legacy sysctl_ovfs_conf_iter() path (sys/kern/vfs_subr.c:1863) copies
vfc_vfsops verbatim into struct ovfsconf (whose vfc_vfsops is a raw
void *, sys/sys/mount.h:487) โ same leak via the older mib.
Evidence โ the leak is real
Running ./leak_vfsconf as the unprivileged maxx user leaked 11 filesystem
entries. Each vfc_vfsops value matches an exact symbol in
nm /boot/kernel/kernel:
| type | fsname | leaked vfc_vfsops |
exact kernel symbol (nm) |
|---|---|---|---|
| 1 | hammer | 0xffffffff81112000 | hammer_vfsops |
| 2 | mfs | 0xffffffff8110fa80 | mfs_vfsops |
| 3 | msdos | 0xffffffff810ebb80 | (msdos_vfsops, in .data) |
| 4 | hammer2 | 0xffffffff81114a80 | hammer2_vfsops |
| 5 | cd9660 | 0xffffffff810c5f00 | cd9660_vfsops |
| 6 | procfs | 0xffffffff810eb3a0 | procfs_vfsops |
| 7 | null | 0xffffffff810ea880 | null_vfsops |
| 8 | devfs | 0xffffffff81111ae0 | devfs_vfsops |
| 9 | ufs | 0xffffffff8110f080 | ufs_vfsops |
| 10 | nfs | 0xffffffff81102f40 | nfs_vfsops |
| 11 | tmpfs | 0xffffffff81117200 | tmpfs_vfsops |
Plus 10 vfc_next linked-list pointers (the 11th, tmpfs, is the list tail so
its vfc_next is correctly 0x0). Kernel segment bounds from nm:
btext=0xffffffff802aa4a0, etext=0xffffffff80c369d1 โ every leaked address
lands in the kernel .data segment immediately above etext, confirming they
are genuine in-kernel addresses, not garbage.
The output is byte-identical across three consecutive runs (see
run.log, run.2.log, run.3.log) โ this is a deterministic leak of static
addresses, not stack-residue noise, which makes it a particularly reliable
KASLR-defeat: the relative offsets between the leaked symbols are constant and
directly reveal the kernel's load base.
Impact
Information disclosure only (no memory contents beyond the pointer values themselves). Standalone impact is Low, but it is a prerequisite primitive for exploiting any future local kernel memory-corruption bug on this kernel: once the kernel text/data base is known, gadgets/RIP/jop-frames can be relocated precisely. Reachable by any unprivileged local user, default kernel, no config.
Exploit chain
None for this class (pure info-leak). The leaked addresses feed a second bug (gadget relocation for an arbitrary-write/UAF), they do not themselves corrupt memory. No further primitive is derivable from this node alone.
PoC changes
None to the source โ the supplied leak_vfsconf.c compiled and ran correctly
on the first attempt. Added the repro glue (build.sh, run.sh) and the full
evidence logs (build.log, run.log, run.2.log, run.3.log,
leak_sample.txt, env.txt, manifest.json, fix.diff).
How to reproduce
./build.sh # as maxx: cc -o leak_vfsconf leak_vfsconf.c ./run.sh # as maxx: dumps vfc_vfsops / vfc_next per fs type
On a fixed kernel the printed vfc_vfsops/vfc_next would be 0x0 and the
PoC exits 2 (no kernel pointers observed).
References
sys/kern/vfs_subr.c:1845โVFS_CONFrawSYSCTL_OUTof wholestruct vfsconf.sys/kern/vfs_subr.c:1850โvfs.genericnodeCTLFLAG_RD, no privilege gate.sys/kern/vfs_subr.c:1863โ legacysysctl_ovfs_conf_itercopiesvfc_vfsopsverbatim.sys/sys/mount.h:478โstruct vfsops *vfc_vfsops(kernel.datapointer).sys/sys/mount.h:483โSTAILQ_ENTRY(vfsconf) vfc_next(kernel.datalist pointer).sys/sys/mount.h:487โstruct ovfsconf.vfc_vfsops(void *, legacy leak).- CWE-200 Exposure of Sensitive Information to an Unauthorized Actor.
Confirmed kernel references
Detail
Exploit chain
none (pure info-leak; no memory corruption). The leaked kernel .data addresses are a prerequisite primitive for a SECOND bug (gadget/RIP relocation for an arbitrary-write/UAF), not themselves a corruption. No further primitive derivable from this node alone.
Evidence (decisive lines)
sizeof(struct vfsconf) = 48 type 1 hammer vfc_vfsops=0xffffffff81112000 vfc_next=0xffffffff8110fa40 type 8 devfs vfc_vfsops=0xffffffff81111ae0 vfc_next=0xffffffff8110f040 type 9 ufs vfc_vfsops=0xffffffff8110f080 vfc_next=0xffffffff81102f00 type 10 nfs vfc_vfsops=0xffffffff81102f40 vfc_next=0xffffffff811171c0 type 11 tmpfs vfc_vfsops=0xffffffff81117200 vfc_next=0x0 filesystem types dumped: 11 kernel .text pointers leaked (vfc_vfsops): 11 kernel .data pointers leaked (vfc_next) : 10 result: LEAK CONFIRMED (KASLR-defeat primitive) --- exact nm /boot/kernel/kernel matches --- ffffffff81111ae0 d devfs_vfsops ffffffff81112000 d hammer_vfsops ffffffff81117200 d tmpfs_vfsops ffffffff81102f40 d nfs_vfsops ffffffff8110f080 d ufs_vfsops (btext=0xffffffff802aa4a0, etext=0xffffffff80c369d1 -> leaked addrs are in .data)
PoC changes
none to the trigger source (leak_vfsconf.c compiled and ran correctly first try). Added repro glue: build.sh, run.sh, VERDICT.md, refreshed README.md, and full evidence logs (build.log, run.log, run.2.log, run.3.log [byte-identical stability runs], leak_sample.txt [nm cross-ref], env.txt, manifest.json, fix.diff).
Verified recommended fix
Redact the kernel pointer fields before copyout in BOTH the VFS_CONF path (vfs_subr.c:1845) and the legacy ovfs_conf path (:1863): in VFS_CONF, copy vfsp into a local struct vfsconf, set vfc_vfsops=NULL and vfc_next.stqe_next=NULL, then SYSCTL_OUT the redacted copy (preserves struct size/ABI); in sysctl_ovfs_conf_iter, set ovfs.vfc_vfsops = (vfsp->vfc_vfsops != NULL) ? (void)1 : NULL (expose only a non-NULL is-configured flag, not the address). A stronger long-term fix is a dedicated pointer-free kinfo_vfsconf. fix.diff implements both; git apply --check passes. Supersedes the finding proposal (adds the ovfs_conf redaction the proposal only mentioned in prose).
Verdict
REPRODUCED. As unprivileged maxx (uid 1001, not in wheel), the VFS_CONF sysctl handler (vfs_subr.c:1845) copies the whole struct vfsconf to userspace, exposing two raw kernel .data pointers per filesystem type: vfc_vfsops (the per-fs struct vfsops, mount.h:478) and vfc_next (the vfsconf list link, mount.h:483). 11 fs types -> 11 vfc_vfsops + 10 vfc_next non-NULL = 21 kernel pointers (168 bytes). The vfs.generic node is CTLFLAG_RD with no privilege gate (vfs_subr.c:1850) and sysctl reads are not privilege-gated. Every leaked vfc_vfsops value matches an EXACT symbol in nm /boot/kernel/kernel (e.g. devfs_vfsops=0xffffffff81111ae0, hammer_vfsops=0xffffffff81112000, tmpfs_vfsops=0xffffffff81117200), all landing in the kernel .data segment just above etext=0xffffffff80c369d1. Output is byte-identical across 3 runs -> deterministic, reliable KASLR-defeat primitive (the relative offsets between the leaked symbols are constant and reveal the kernel load base). The legacy sysctl_ovfs_conf_iter path (:1863) copies vfc_vfsops verbatim too. No code changes were needed to the supplied PoC.