β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2667

Unprivileged kernel NULL-pointer panic via mountctl(2) MOUNTCTL_MOUNTFLAGS with buflen=0: vfs_flagstostr() strlens a NULL buffer before its degenerate-length guard

Field Value
ID DF-2667
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
CWE CWE-476 NULL Pointer Dereference
File sys/kern/vfs_syscalls.c
Lines 1281-1285, 1311-1312 (sink vfs_subr.c:1996)
Area kern
Confidence certain
Discovered 2026-08-29
Pass 2 (GLM 5.3 second pass)
Bucket base:kern
Reported pending
Known CVE none
CVE match novel

Summary

Any non-jailed local user can panic the kernel with one syscall: mountctl("/", MOUNTCTL_MOUNTFLAGS, -1, NULL, 0, NULL, 0). sys_mountctl() skips the privilege check for op==MOUNTCTL_MOUNTFLAGS (vfs_syscalls.c:1281-1285) and only allocates the result buffer if (uap->buflen), forwarding buf==NULL/buflen==0 (:1311-1312); kern_mountctl() dispatches to vop_stdmountctl (vfs_default.c) whose MOUNTCTL_MOUNTFLAGS case calls vfs_flagstostr(flags, NULL, NULL, 0, &error); vfs_flagstostr() executes actsize = strlen(buf) at vfs_subr.c:1996 BEFORE its if (bleft < 0) degenerate case guard at :2004-2007 β€” strlen(NULL) faults.

Proof of concept

Reproduced live as uid 1001 on the stock INVARIANTS guest (findings/poc/DF-2667/mountctl_null.c): Fatal trap 12 ... fault virtual address = 0x0, Stopped at strlen: cmpb $0,(%rdi), faulting process df2667, guest frozen in DDB. 100% reliable unprivileged local DoS. Fix validated on a rebuilt INVARIANTS kernel: EINVAL plus a working positive control (256-byte buffer returns "local"). No memory-write primitive (NULL-page read), so no escalation route.

--- a/sys/kern/vfs_subr.c
+++ b/sys/kern/vfs_subr.c
@@ vfs_flagstostr()
    *errorp = 0;
+
+   /*
+    * Degenerate case: missing or zero-length buffer.  This must
+    * be checked before strlen(buf); sys_mountctl() leaves buf
+    * NULL when buflen == 0.
+    */
+   if (buf == NULL || len == 0) {
+       *errorp = EINVAL;
+       return(0);
+   }
+
    bwritten = 0;
    bleft = len - 1;    /* leave room for trailing \0 */

(full diff in findings/poc/DF-2667/fix.diff; validated)

Timeline

  • 2026-08-29 Discovered during pass-2 audit of vfs_syscalls.c (GLM 5.3); reproduced unprivileged + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2667 Β· 12 files
FileTypeDescriptionSize
mountctl_null.c β€” 1.3 KB view raw
build.sh β€” 79 B view raw
run.sh β€” 174 B view raw
run.log β€” 891 B view raw
panic.txt β€” 1.1 KB view raw
env.txt β€” 334 B view raw
fix.diff β€” 1016 B view raw
run.patched.log β€” 589 B view raw
fix_validation.log β€” 2.5 KB view raw
VERDICT.md β€” 3.0 KB ↓ raw
verdict.json β€” 3.0 KB view raw
manifest.json β€” 830 B view raw
VERDICT.md
↓ download raw

DF-2667 VERDICT β€” REPRODUCED (unprivileged local kernel panic / DoS)

Bottom line

One unprivileged syscall β€” mountctl("/", MOUNTCTL_MOUNTFLAGS, -1, NULL, 0, NULL, 0) β€” reliably panics the stock INVARIANTS kernel with a NULL-pointer read in strlen(). Verified live on the audit guest as uid 1001: the syscall never returns, the console shows Fatal trap 12 ... fault virtual address = 0x0, Stopped at strlen: cmpb $0,(%rdi), Fatal user address access from kernel mode from df2667, and the guest freezes in DDB.

Why it happens (line-accurate)

  1. sys_mountctl() (sys/kern/vfs_syscalls.c:1267) only privilege-checks ops other than MOUNTCTL_MOUNTFLAGS (:1281-1285): c if ((uap->op != MOUNTCTL_MOUNTFLAGS) && (error = caps_priv_check_td(td, SYSCAP_RESTRICTEDROOT)) != 0) Only a jail check precedes it (:1279). Any non-jailed user passes.
  2. Argument validation accepts buflen == 0 (:1292 only rejects < 0 or > 16*1024), and the result buffer is only allocated if (uap->buflen) (:1311-1312) β€” so buf == NULL is forwarded to kern_mountctl() together with buflen == 0.
  3. kern_mountctl() (:1354) requires the path to resolve to a mount root; / is one (VROOT) and is look-up-able by any user. It dispatches vop_mountctl(mp->mnt_vn_use_ops, vp, op, fp, ctl, 0, NULL, 0, res) (:1408). On this system the root mount is hammer2; hammer2 (and tmpfs, hammer, fuse β€” all of them) route unknown ops to vop_stdmountctl (sys/vfs/hammer2/hammer2_vnops.c:2296).
  4. vfs_stdmountctl case MOUNTCTL_MOUNTFLAGS calls vfs_flagstostr(mp->mnt_flag & MNT_VISFLAGMASK, NULL, ap->a_buf, ap->a_buflen, &error) (sys/kern/vfs_default.c:1316-1317) with a_buf == NULL, a_buflen == 0.
  5. vfs_flagstostr() computes actsize = strlen(buf) at sys/kern/vfs_subr.c:1996 before its own degenerate-buffer guard if (bleft < 0) at :2004-2007 (the developer clearly intended to handle the 0-length case, but ordered the check after the strlen). strlen(NULL) β†’ page fault at address 0 β†’ panic.

Exploitability

  • DoS: 100% reliable, unprivileged, one line of C.
  • Not memory corruption β€” a NULL-page read; on this platform page 0 is unmapped in kernel mode, so the ceiling is a panic (and the same on any realistic config). No privilege-escalation route.

Fix validation

fix.diff (in this directory) moves the degenerate rejection ahead of the strlen() in vfs_flagstostr() (sys/kern/vfs_subr.c). Applied to the guest's /usr/src together with DF-2668's fix, kernel rebuilt (make nativekernel KERNCONF=X86_64_GENERIC, INVARIANTS), rebooted: - baseline (stock kernel): panic (above); - patched kernel: PoC returns cleanly, mountctl = EINVAL, guest stays up (run.patched.log, fix_validation.log).

Artifacts

mountctl_null.c (PoC), build.sh, run.sh, run.log, panic.txt, env.txt, fix.diff, run.patched.log, fix_validation.log, verdict.json, manifest.json.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched kernel returns EINVAL for the degenerate mountctl instead of panicking; positive control (valid 256-byte buffer) still returns the flags string; guest stable.

['fix_validation.log', 'run.patched.log', 'fix.diff']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Sun Aug 30 10:16:19 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

['run.log (unprivileged run + console panic text)', 'panic.txt (Fatal trap 12, fault address 0x0, Stopped at strlen, from df2667)', 'fix_validation.log + run.patched.log (patched kernel returns EINVAL, positive control works)', 'fix.diff']

PoC changes

PoC written fresh for this finding (no seed); raw syscall stubs used because libc does not export mountctl(); syscall number 468 taken from sys/sys/syscall.h and verified in-guest.

Verified recommended fix

In vfs_flagstostr(), reject buf == NULL || len == 0 before strlen(buf) (see fix.diff).

Verdict

Unprivileged (uid 1001, non-jailed) local kernel panic reproduced on the stock INVARIANTS guest with a single mountctl("/", MOUNTCTL_MOUNTFLAGS, -1, NULL, 0, NULL, 0) syscall: sys_mountctl() skips the privilege check for MOUNTCTL_MOUNTFLAGS (sys/kern/vfs_syscalls.c:1281-1285), forwards a NULL result buffer when buflen==0 (:1311-1312), and vfs_flagstostr() executes strlen(buf) at sys/kern/vfs_subr.c:1996 BEFORE its degenerate 0-length guard (:2004-2007), faulting at address 0x0 (console: 'Fatal trap 12 ... fault virtual address = 0x0', 'Stopped at strlen: cmpb $0,(%rdi)', faulting process df2667). NULL-page read only: the ceiling is a reliable unprivileged DoS, no escalation path. fix.diff moves the NULL/zero-length rejection ahead of the strlen(); validated on a rebuilt kernel (#1): the same syscall returns EINVAL and the guest stays up, while a proper 256-byte buffer still returns the flags string ('local') β€” no regression.