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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5808 Β· 13 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download raw

DF-5808 β€” VERDICT

Status: reproduced (deterministic unprivileged OOB kernel heap read; impact = leak).

Root cause chain

  1. sys/kern/vfs_syscalls.c:1281-1284 β€” MOUNTCTL_MOUNTFLAGS is the ONLY mountctl op exempt from caps_priv_check_td(SYSCAP_RESTRICTEDROOT): any local user may call it on any mount-root path.
  2. 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 reads usedbytes = *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.
  3. sys/kern/vfs_syscalls.c:1331-1332 β€” sys_mountctl trusts the VOP's count: copyout(buf, uap->buf, sysmsg->sysmsg_result) with no clamp against uap->buflen; buf is kmalloc(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 string read-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 via buflen; on a busier heap the same primitive samples adjacent M_TEMP allocations. No marker-byte placement was achieved with the in-syscall ctllen groom (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=85 on all runs; wide probe ret=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

fixed
baseline reproduced→ patch + rebuild →patched clean

baseline: 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
↓ fix.diffper-fix-DF-5808

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.