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.
Recommended fix
--- 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)
PoC verification
Evidence pack
findings/poc/DF-2667 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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)
sys_mountctl()(sys/kern/vfs_syscalls.c:1267) only privilege-checks ops other thanMOUNTCTL_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.- Argument validation accepts
buflen == 0(:1292only rejects< 0or> 16*1024), and the result buffer is only allocatedif (uap->buflen)(:1311-1312) β sobuf == NULLis forwarded tokern_mountctl()together withbuflen == 0. kern_mountctl()(:1354) requires the path to resolve to a mount root;/is one (VROOT) and is look-up-able by any user. It dispatchesvop_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 tovop_stdmountctl(sys/vfs/hammer2/hammer2_vnops.c:2296).vfs_stdmountctlcaseMOUNTCTL_MOUNTFLAGScallsvfs_flagstostr(mp->mnt_flag & MNT_VISFLAGMASK, NULL, ap->a_buf, ap->a_buflen, &error)(sys/kern/vfs_default.c:1316-1317) witha_buf == NULL,a_buflen == 0.vfs_flagstostr()computesactsize = strlen(buf)atsys/kern/vfs_subr.c:1996before its own degenerate-buffer guardif (bleft < 0)at:2004-2007(the developer clearly intended to handle the 0-length case, but ordered the check after thestrlen).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
fixedPatched 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']
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.
No comments yet.