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

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 .text addresses (per-filesystem vfsops vectors) and .data addresses (the vfsconf list). 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: sysctl on {CTL_VFS, VFS_GENERIC, VFS_CONF, typenum} (and the legacy vfs.generic iteration).

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.

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

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

VERDICT.md verdict full narrative: reproduced, line-by-line trace, evidence table vs nm
โ†“ download raw

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

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.