hammer(1) MOUNTCTL_MOUNTFLAGS result-length double-count: unprivileged copyout reads up to ~U bytes past a kmalloc'd M_TEMP buffer (kernel heap pointer disclosure)
| Field | Value |
|---|---|
| ID | DF-2698 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N |
| CWE | CWE-125 OOB Read (kernel heap info leak) |
| File | sys/vfs/hammer/hammer_vnops.c |
| Lines | 2580 (root cause); sink vfs_syscalls.c:1334-1335 |
| Area | vfs |
| Confidence | certain |
| Discovered | 2026-08-30 |
| Pass | 2 (GLM 5.3 second pass β surfaced during vfs_subr.c audit, sibling of DF-2667) |
| Bucket | kernleak |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
hammer_vop_mountctl() sets usedbytes=*a_res (U bytes written by
vop_stdmountctl), adds the hammer-flag suffix (R), then executes
*ap->a_res += usedbytes, reporting 2U+R instead of U+R.
sys_mountctl() uses that number directly as the copyout length from
buf=kmalloc(buflen+1), and MOUNTCTL_MOUNTFLAGS is deliberately
unprivileged. With buflen=U+1 the kernel copies 2U bytes out of a
U+1-byte object: demonstrated 73 bytes of adjacent kernel heap,
containing live 0xfffff801β¦ pointers, on every one of 4 runs as uid
1001 against a hammer(1) mount. vfs_flagstostr itself is in-bounds;
only the reported length is wrong.
Threat model & preconditions
Any local user, no privileges, repeatable at will while any hammer(1) filesystem is mounted (hammer1 fully supported in 6.5). Discloses adjacent slab contents β kernel pointers and whatever shares the M_TEMP zone.
Proof of concept
VERIFIED (findings/poc/DF-2698/): root one-time newfs_hammer+mount with
a flag-laden remount (U=74); unpriv syscall(468,"/hmnt",18,-1,NULL,0,
buf,75) β kernel returns 148 for a 74-byte string and copies 148 bytes
out of the 75-byte allocation; the 73 OOB bytes contain kernel pointers
(varying across runs). Patched kernel: returns 74==U, zero OOB bytes,
flag string identical.
Recommended fix
--- a/sys/vfs/hammer/hammer_vnops.c
+++ b/sys/vfs/hammer/hammer_vnops.c
@@ -2577,7 +2577,7 @@ hammer_vop_mountctl(struct vop_mountctl_args *ap)
&error);
}
- *ap->a_res += usedbytes;
+ *ap->a_res = usedbytes;
break;
Validated on guest with a combined single-fix kernel (fix.both.diff).
Timeline
- 2026-08-30 Surfaced during pass-2 audit of vfs_subr.c (GLM 5.3); unpriv leak reproduced + fix validated same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2698 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| mountctl_leak.c | β | 2.9 KB | view raw | |
| setup.sh | β | 879 B | view raw | |
| run.sh | β | 228 B | view raw | |
| build.sh | β | 111 B | view raw | |
| run.log | β | 836 B | view raw | |
| run.23.log | β | 2.5 KB | view raw | |
| panic.txt | β | 1.2 KB | view raw | |
| env.txt | β | 750 B | view raw | |
| fix.log | β | 669 B | view raw | |
| fix.diff | β | 384 B | view raw | |
| fix.both.diff | β | 1.0 KB | view raw | |
| VERDICT.md | β | 4.3 KB | β raw |
DF-2698 β hammer(1) MOUNTCTL_MOUNTFLAGS result double-count β unprivileged kernel heap OOB read
Reproduced? YES (reproduced, impact = leak)
Guest: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, INVARIANTS), run as uid 1001 (maxx) β no privileges used for the read itself.
Root cause (arithmetic), line-accurate
sys/vfs/hammer/hammer_vnops.c, hammer_vop_mountctl():
2567: error = vop_stdmountctl(ap); /* writes U bytes, sets *a_res = U */
2571: usedbytes = *ap->a_res; /* usedbytes = U */
2573: if (usedbytes > 0 && usedbytes < ap->a_buflen) {
2574: usedbytes += vfs_flagstostr(hmp->hflags, extraopt,
2575: ap->a_buf,
2576: ap->a_buflen - usedbytes,
2577: &error); /* usedbytes = U + R */
2578: }
2580: *ap->a_res += usedbytes; /* *a_res = U + (U + R) = 2U + R β BUG */
1312: if (uap->buflen)
1313- buf = kmalloc(uap->buflen + 1, M_TEMP, M_WAITOK|M_ZERO);
1334: if (error == 0 && sysmsg->sysmsg_result > 0)
1335- error = copyout(buf, uap->buf, sysmsg->sysmsg_result); /* 2U+R bytes from U+R-byte buffer */
1281: if ((uap->op != MOUNTCTL_MOUNTFLAGS) &&
1282: (error = caps_priv_check_td(td, SYSCAP_RESTRICTEDROOT)) != 0)
i.e. MOUNTCTL_MOUNTFLAGS is deliberately unprivileged, and the over-counted
result is used directly as the copyout length. With the queue
buflen = U+1 the kernel reads 2U bytes out of a U+1-byte heap object
(the writer, vfs_flagstostr() in sys/kern/vfs_subr.c, stays within its
budget β the overflow is purely the reported length).
Reproduction (guest)
Root setup once (setup.sh): vn0 + newfs_hammer + mount -t hammer /dev/vn0
/hmnt + mount -u -o noatime,noexec,nosuid,nosymfollow,nodev,noclusterr,
noclusterw /hmnt β flag string U=74 ("noexec, nosuid, ... local").
Unprivileged run of /tmp/ml /hmnt (raw syscall 468, op=18):
pass1: kernel returned 148 bytes; string in buffer is 74 bytes β 2U signature pass2: buflen=75 β kernel kmalloc'd 75 bytes, copied out 148 bytes bytes [75..148) PAST the kernel allocation: 0000: 00 00 00 00 00 80 7f 9e 8d 00 f8 ff ff ... β 0xfffff8018f9e7f80 etc.
73 bytes beyond the allocation are disclosed; multiple live kernel heap pointers (0xfffff801β¦-canonical) present on every run; addresses vary between runs (run.log, run.23.log β 4 total runs). Repeatable at will by any user while a hammer(1) mount exists; ~U bytes per call (U is the mount's visible-flag string length; a flag-laden mount maximizes it; hammer1 mounts are still fully supported in 6.5 and trivially created by root, incl. by auto-mounting a crafted image in setups that allow it).
Impact: unprivileged kernel heap disclosure incl. kernel text/heap pointers (> DFly has no KASLR by default, but the leaked objects themselves β adjacent M_TEMP allocations β can contain file paths, credentials-derived data, whatever shares the slab).
Not-a-bug cross-check
vfs_flagstostr()itself (sys/kern/vfs_subr.c:1954-2037) stays within itslenbudget on both calls; the hammer caller correctly reduces the second call's length byusedbytes. Only the+=at hammer_vnops.c:2580 is wrong. (The strlen-before-length-check wart at vfs_subr.c:1996 is already known finding DF-2667 β our first PoC attempt passed buflen=0, tripped it, and panicked the guest:panic.txt,Stopped at strlen: cmpb $0,(%rdi), fault address 0x0 fromml2. Independent live confirmation of DF-2667, not re-reported here.)- nullfs's MOUNTCTL_MOUNTFLAGS (null_vnops.c:220-222) delegates straight to vop_stdmountctl β no double-count. Only hammer(1) has the custom path; hammer2 does not override vop_mountctl.
Fix validation
fix.both.diff (this finding's one-liner + the DF-2697 vaccess one-liner)
applied to guest /usr/src, make nativekernel KERNCONF=X86_64_GENERIC,
installkernel, reboot:
- baseline:
kernel returned 148 bytes; string ... 74 bytes, OOB tail full of kernel pointers - patched:
kernel returned 74 bytes(== U, correct), zero bytes past the string β see fix.log. mount(8) output and flag string unchanged.
Verdict
Real, unprivileged, repeatable kernel heap info leak (bucket: kernleak), Medium severity; one-line fix validated on the guest.
Fix verification
fixedSingle-fix kernel (fix.both.diff: this one-liner + the DF-2697 vaccess one-liner) built with make nativekernel KERNCONF=X86_64_GENERIC and booted. Baseline: result=148 vs 74-byte string, 73 OOB bytes with kernel pointers. Patched: result=74 == U, zero bytes past the allocation, flag string identical. Leak gone, mount(8) behaviour unchanged.
fix.log / fix_run.log (before/after); fix_build.log (nativekernel rc=0, build completed, no errors); uname -v #2 boot identity.
Confirmed kernel references
Detail
Exploit chain
root one-time: vn0 + newfs_hammer + mount -t hammer /hmnt + flag-laden remount (U=74). Then unprivileged, repeatable at will: syscall(468, "/hmnt", 18(MOUNTCTL_MOUNTFLAGS), -1, NULL, 0, buf, 75) -> kernel kmallocs 75 bytes, writes the 74-byte flag string + NUL, reports 148 -> copyout drags 73 bytes of adjacent kernel heap (pointers) into the user buffer.
Evidence (decisive lines)
["run.log: pass1 'kernel returned 148 bytes; string in buffer is 74 bytes' (2U signature); pass2 OOB dump with 0xfffff801...-canonical kernel pointers", 'run.23.log: three further runs, pointers present each time with varying addresses', "fix.log (patched kernel): 'kernel returned 74 bytes', bytes-past-allocation range empty", 'panic.txt: accidental independent confirmation of known DF-2667 (strlen(NULL)) from attempt #1', 'VERDICT.md: full path:line trace incl. why vfs_flagstostr itself is in-bounds']
PoC changes
Two fixes vs the sketch: (1) mountctl(2) takes 7 args and returns the byte count itself (no res pointer arg); (2) the heap-spray helper must never pass buflen=0 -- the first attempt did and panicked the guest via the already-known DF-2667 strlen(NULL); switched to buflen=1.
Verified recommended fix
hammer_vnops.c:2580: replace 'ap->a_res += usedbytes;' with 'ap->a_res = usedbytes;'.
Verdict
hammer_vop_mountctl() (sys/vfs/hammer/hammer_vnops.c:2571-2580) sets usedbytes = ap->a_res (U bytes written by vop_stdmountctl), adds the hammer-flag suffix (R), then does ap->a_res += usedbytes, so the mountctl result becomes 2U+R instead of U+R. sys_mountctl() (vfs_syscalls.c:1334-1335) uses that result directly as the copyout length out of buf = kmalloc(buflen+1, M_TEMP) (line 1312), and MOUNTCTL_MOUNTFLAGS is explicitly unprivileged (line 1281). With buflen = U+1 the kernel copies 2U bytes out of a U+1-byte heap object. Reproduced on the stock guest as uid 1001 against a hammer(1) mount: mountctl returned 148 bytes for a 74-byte string and the 73 bytes past the allocation (copied to userland) contained live kernel heap pointers on every one of 4 runs. One-line fix (*ap->a_res = usedbytes) validated with an in-guest nativekernel build: result == U, zero bytes past the allocation, mount flag string unchanged.
No comments yet.