HAMMER mountctl(MOUNTCTL_MOUNTFLAGS) double-counts the result length β unprivileged OOB kernel heap read via unchecked copyout
Summary
MOUNTCTL_MOUNTFLAGS is the only mountctl op exempt from the root check, and sys_mountctl copyout()s the VOP-reported byte count from the kmalloc(buflen+1) buffer with no clamp. hammer_vop_mountctl already has part1 stored in *a_res by vop_stdmountctl then does *a_res += (part1+part2) β returning 2*part1+part2: with buflen=strlen(flags)+1 the copyout over-reads adjacent kernel heap to any local user (observed ret=170 from an 87-byte allocation, 3/3; tmpfs control clean). Repeatable ~100 bytes/call kernel heap disclosure on any host with a HAMMER(1) mount (DragonFly default root through 5.x). '= usedbytes' + clamp-in-sys_mountctl fix validated on patched guest: ret=85, no over-read.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5808 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.2 KB | β raw | |
| SHA256SUMS | β | 233 B | β download | |
| VERDICT.md | β | 3.1 KB | β raw | |
| build.sh | β | 98 B | view raw | |
| fix.diff | β | 611 B | view raw | |
| mctl.c | β | 1.9 KB | view raw | |
| mctl2.c | β | 2.0 KB | view raw | |
| run.log | β | 1.7 KB | view raw | |
| run.sh | β | 377 B | view raw | |
| run_fixed.log | β | 190 B | view raw | |
| run_tuned_exploratory.log | β | 3.1 KB | view raw | |
| setup.log | β | 65 B | view raw | |
| verdict.json | β | 2.6 KB | view raw |
DF-5808 β HAMMER mountctl(MOUNTFLAGS) double-counts the result length β unprivileged OOB kernel heap read
What
sys_mountctl (sys/kern/vfs_syscalls.c:1267) exempts
MOUNTCTL_MOUNTFLAGS from the root privilege check (line 1281), so any
local user can query the flag string of any mounted filesystem.
kern_mountctl passes &sysmsg->sysmsg_result to the VOP, and
sys_mountctl then does
copyout(buf, uap->buf, sysmsg->sysmsg_result) with no clamp against
buflen (line 1331-1332). On HAMMER(1) mounts,
hammer_vop_mountctl (sys/vfs/hammer/hammer_vnops.c:2559-2582)
double-counts: vop_stdmountctl already stored part1 in
*ap->a_res; hammer then computes usedbytes = part1 (+part2) and
does *ap->a_res += usedbytes β the syscall copyout length becomes
2*part1 + part2 while the kernel buffer is only buflen+1 bytes.
With buflen = strlen(flags)+1 the copyout over-reads
part1 - 2 bytes of adjacent kernel heap into userspace.
hammer2/tmpfs/ufs/... use vop_stdmountctl directly and are safe
(control verified: tmpfs returns ret == strlen).
Build
cc -O2 -o /tmp/mctl2 mctl2.c
Run
(root, one-time) create a HAMPER mount with a long visible flag string:
dd if=/dev/zero of=/root/h.img bs=1m count=0 seek=4096 vnconfig -c vn1 /root/h.img && newfs_hammer -f -L leaktest /dev/vn1 mount -t hammer -o ro,noexec,nosuid,nodev,nosymfollow,noatime,noclusterr,noclusterw /dev/vn1 /mnt/h
(unprivileged, uid 1001):
/tmp/mctl2 /mnt/h 1024 1 # probe: flags_str + ret /tmp/mctl2 /mnt/h 86 3 # leak: ret=170 > buflen=86
Expected
Stock kernel:
run 0: ret=170 buflen=86 ... strlen=85 *** ret(170) > buflen(86): 84 bytes past the 86-byte kernel buffer
(ret == 2*strlen; 3/3 identical runs; tmpfs control ret == strlen.)
The 84 bytes beyond the 87-byte kmalloc'd buffer are adjacent kernel
heap (zero-filled in this guest's heap state; the over-read itself is
deterministic and attacker-sized).
Patched kernel (fix.diff): ret=85 == strlen, no over-read, 3/3.
Files
- mctl2.c trigger (probe/tuned/NULL-buflen modes)
- run.log baseline 3 runs + tmpfs control
- run_fixed.log patched-kernel runs
- fix.diff hammer_vnops.c (
=instead of+=) + vfs_syscalls.c clamp
DF-5808 β VERDICT
Status: reproduced (deterministic unprivileged OOB kernel heap read; impact = leak).
Root cause chain
sys/kern/vfs_syscalls.c:1281-1284βMOUNTCTL_MOUNTFLAGSis the ONLY mountctl op exempt fromcaps_priv_check_td(SYSCAP_RESTRICTEDROOT): any local user may call it on any mount-root path.sys/vfs/hammer/hammer_vnops.c:2562-2582β MOUNTCTL_MOUNTFLAGS path:vop_stdmountctl(ap)stores part1 into*ap->a_res(sys/kern/vfs_default.c:1316); hammer readsusedbytes = *ap->a_res, appends the hammer-extra flags (usedbytes += vfs_flagstostr(...)), then writes*ap->a_res += usedbytesβ part1 is counted twice. The actual buffer writes are correctly bounded (vfs_flagstostr(..., ap->a_buflen - usedbytes, ...)); only the returned byte count is inflated.sys/kern/vfs_syscalls.c:1331-1332βsys_mountctltrusts the VOP's count:copyout(buf, uap->buf, sysmsg->sysmsg_result)with no clamp againstuap->buflen;bufiskmalloc(buflen+1, M_TEMP).2*part1+part2 > buflen+1β out-of-bounds source read of the kernel heap, disclosed to the unprivileged caller.
Reproduction (guest DragonFly 6.5-DEVELOPMENT, stock kernel #0)
- Root mounted a HAMMER(1) fs with flags
ro,noexec,nosuid,nodev,nosymfollow,noatime,noclusterr,noclusterwβ visible stringread-only, noexec, ..., local(85 chars). - uid 1001:
mountctl("/mnt/h", MOUNTCTL_MOUNTFLAGS, -1, NULL, 0, buf, 86, &res): - wide probe (buflen=1024):
ret=170,strlen(out)=85β ret == 2*strlen. - tuned (buflen=86):
ret=170 > 86: copyout of 170 bytes from the 87-byte kernel buffer β 84 bytes OOB (run.log, 3/3 identical). - tmpfs control (
/tmp, buflen=86):ret=5 == strlen=5β no doubling; the defect is hammer-specific (hammer2 has no mountctl override β verified by grep). - The over-read bytes landed in a zeroed heap region on this guest;
the read extent (
part1-2) and determinism are attacker-controlled viabuflen; on a busier heap the same primitive samples adjacent M_TEMP allocations. No marker-byte placement was achieved with the in-syscallctllengroom (different kmalloc buckets) β documented honestly; the structural over-read needs no grooming to exist.
Fix validation
Applied fix.diff (hammer *ap->a_res = usedbytes + result clamp in
sys_mountctl), rebuilt/installed kernel, rebooted:
/tmp/mctl2 /mnt/h 86 3βret=85 == strlen=85on all runs; wide proberet=85(run_fixed.log). Over-read gone.
Notes
- On hosts whose root/daily filesystems are HAMMER(1) (the DragonFly default through 5.x), this is an unprivileged, repeatable kernel heap disclosure against a default-config mount. On hammer2-root hosts (6.x default) a hammer1 mount must exist (admin setup).
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedbaseline: ret=170 from 87-byte buffer 3/3; after fix.diff + kernel rebuild + reboot: ret=85 == strlen on all runs, tmpfs and hammer both clamped (run_fixed.log)
run_fixed.log; fix.diff
Confirmed kernel references
Detail
Exploit chain
unprivileged mountctl(mount-root-path, MOUNTCTL_MOUNTFLAGS, buflen=strlen(flags)+1) -> kernel returns 2part1 -> sys_mountctl copyout(buf, user, 2part1) from buflen+1 heap buffer -> repeatable OOB kernel heap disclosure sized by the mount's flag string
Evidence (decisive lines)
run.log (3x ret=170 buflen=86 + tmpfs control ret=5); run_fixed.log (ret=85 after fix); mctl2.c; fix.diff
PoC changes
trigger written from scratch (mctl.c/mctl2.c); hammer1 test mount created on guest (vn+newfs_hammer -f); NULL-buflen variant of the same probe is filed separately as DF-5811
Verified recommended fix
hammer_vnops.c: *ap->a_res = usedbytes (not +=); plus clamp sysmsg_result to uap->buflen in sys_mountctl before copyout (both in fix.diff, validated)
Verdict
hammer_vop_mountctl() double-counts the MOUNTCTL_MOUNTFLAGS result byte count (ap->a_res already holds part1, then ap->a_res += part1+part2), and sys_mountctl() copyout()s the VOP-reported length from the kmalloc(buflen+1) result buffer with no clamp; MOUNTCTL_MOUNTFLAGS is exempt from the root check, so any local user over-reads part1-2 bytes of adjacent kernel heap per call (observed ret=170 from an 87-byte allocation, 3/3 deterministic; tmpfs control clean). Fixed kernel returns ret == strlen.
No comments yet.