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

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.

--- 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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2698 Β· 12 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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 */

sys/kern/vfs_syscalls.c:

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 its len budget on both calls; the hammer caller correctly reduces the second call's length by usedbytes. 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 from ml2. 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Single-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.
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #2: Mon Aug 31 00:53:26 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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.