# 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()`:

```c
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`:

```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.
