# DF-5799 VERDICT — REPRODUCED (protection removal; fix validated)

## Bottom line

`hammer2_cleanup_devvp()` (`sys/vfs/hammer2/hammer2_ondisk.c:225-226`) clears
`v_rdev->si_mountpoint` unconditionally for every device in the list it is
handed. Two reachable call sites hand it devices whose markers belong to
someone else or to a still-live hammer2 mount:

* `sys/vfs/hammer2/hammer2_vfsops.c:1358-1360` — secondary-PFS mount path
  (devvp matched an existing hmp → `hammer2_cleanup_devvp(&devvpl)` before the
  label lookup). Runs on **every** secondary hammer2 mount attempt from a
  device that is already mounted — success OR failure of the label lookup.
* `sys/vfs/hammer2/hammer2_vfsops.c:1092-1099` — `vfs_mountedon()` EBUSY path:
  the devvpl devices belong to a foreign filesystem (ufs/msdos/ext2/cd9660/
  ntfs/hpfs/udf); cleanup strips *their* marker.
* (aggravating, same lifecycle) `sys/vfs/hammer2/hammer2_ondisk.c:691` parks
  the *first* mount's `struct mount *` in the marker; unmounting that mount
  while later mounts keep the device leaves a stale pointer that the devfs
  write gate dereferences (`sys/vfs/devfs/devfs_vnops.c:1019` reads
  `si_mountpoint->mnt_flag`) — a use-after-free read of `struct mount`.

`vfs_mountedon()` (`sys/kern/vfs_subr.c:1887-1895`) is a bare
`si_mountpoint != NULL` test, and it is the only double-mount guard for most
filesystems *and* the only thing behind the devfs raw-write gate. HAMMER1
does the ownership check correctly at `sys/vfs/hammer/hammer_ondisk.c:362-366`
(`si_mountpoint == hmp->mp`), which is the upstream-blessed shape of the fix.

## How it was reproduced (baseline, stock kernel #0, 2026-10-12)

Run `./run.sh` (transcript: `run.log`):

1. vnconfig a 200 MB file as `/dev/vn0`; `newfs_hammer2 -L ROOT`.
2. `mount -t hammer2 /dev/vn0@ROOT /mnt/a` → rc=0.
3. `dd if=/dev/zero of=/dev/vn0 bs=64k count=1` → **"Device busy"**
   (devfs gate active; `si_mountpoint` set by `hammer2_init_volumes`).
4. `mount -t hammer2 /dev/vn0@LOCAL /mnt/b` → rc=0 (routine secondary PFS
   mount). Console shows `hmp=… matched` then `pmp=…` — the
   `hammer2_cleanup_devvp(&devvpl)` at vfsops.c:1360 has already run.
5. Same `dd` → **succeeds** (65536 bytes). The write protection of a device
   backing TWO live read-write hammer2 mounts is gone.
6. `umount /mnt/b`; `mount -t hammer2 /dev/vn0@NOSUCH /mnt/b` → fails
   ("not found") — yet the same `dd` **still succeeds**: even a *failed*
   secondary mount attempt strips the gate (cleanup precedes the label
   lookup).
7. Cross-fs variant: UFS on `/dev/vn1` (`mount -t ufs` rc=0);
   `dd of=/dev/vn1` → **Device busy**.
8. `mount -t hammer2 /dev/vn1@X /mnt/b2` → "Device busy" (the
   vfs_mountedon path at vfsops.c:1092-1099; its cleanup clears UFS's marker).
9. Same `dd of=/dev/vn1` → **succeeds** — raw write-open under a live
   read-write UFS mount. (A *second* UFS mount is still blocked by ffs's
   independent `vcount(devvp)` guard at `sys/vfs/ufs/ffs_vfsops.c:608-609`;
   the universal raw-write gate is what hammer2 broke.)

Full-device impact demo (first exploratory run, transcript in this audit
session): after step 5, `dd if=/dev/zero of=/dev/vn0 bs=1m count=200`
completed — 200 MB of the live-mounted device zeroed with no EBUSY; the
mounted fs keeps serving from cached chains, on-disk state destroyed
(corruption surfaces on cache eviction/remount; ffs softdep's
`si_mountpoint` consumers at `ffs_softdep.c:4184,4266` also lose their
mount pointer).

## Exploit chain / impact ceiling

* Trigger: one hammer2 mount attempt touching the victim device (root, or a
  `vfs.usermount` user for devices they own; on multi-PFS hammer2 systems the
  trigger is *routine admin behavior*, and it also fires on failures).
* Effect: `open(O_WRONLY)` of the mounted raw device becomes possible for
  ANY credential with node write permission (the devfs gate is uid-blind).
  That converts "mounted filesystem media" into attacker-writable state:
  crafted metadata under live kernel parsers (hammer2/ffs parsing bugs become
  memory-corruption primitives), softdep state corruption, silent data
  destruction; and un-blocks double-mounts for filesystems lacking ffs's
  vcount guard.
* Not a direct uid=0 chain by itself — it is a protection-removal that
  upgrades the reachable attack surface of every filesystem mounted on the
  device. Classified impact: dos (device/fs destruction) with
  privesc-facilitation.

## Fix validation (mandatory per contract — done)

`fix.diff` (git-applyable, in this pack):

* removes the unconditional clear from `hammer2_cleanup_devvp`;
* manages the marker explicitly: `hammer2_devvp_mount(hmp, mp)` (re-own on
  every mount / cluster-join in `hammer2_mount_helper` and `hammer2_pfsalloc`),
  `hammer2_devvp_unmount(hmp, mp)` (ownership-checked clear on last unmount),
  `hammer2_devvp_unmount_all(hmp)` (final device teardown), plus marker
  cleanup in `hammer2_init_volumes`' error path. This also eliminates the
  stale-`struct mount *` dereference window.

Applied inside the guest (`git apply`), `make -j6 nativekernel
KERNCONF=X86_64_GENERIC` → rc=0 (`build.log`), `make installkernel`, reboot
into kernel #1 (2026-10-12 21:32:58), re-ran the identical `poc.sh`
(`run.fixed.log`):

| step | stock kernel #0 | patched kernel #1 |
|---|---|---|
| [3] gate before secondary mount | Device busy | Device busy |
| [4] secondary mount @LOCAL | rc=0 | rc=0 (no regression) |
| [5] dd after secondary mount | **rc=0 (BUG)** | **Device busy** |
| [6] dd after FAILED secondary mount | **rc=0 (BUG)** | **Device busy** |
| [9] dd under UFS after failed h2 mount | **rc=0 (BUG)** | **Device busy** |

Fix status: **fixed** (baseline reproduced, patched kernel suppresses all
three write-gate bypasses; mounts/unmounts still function).

## Negative results (pass-3 candidates investigated and killed)

* multi-volume sparse `volu_id` → `hammer2_get_volume` panic: killed —
  `hammer2_verify_volumes_2` (`hammer2_ondisk.c:390-405`) forces
  `volu_loff[i]==-1` for `i >= nvolumes`, and `verify_common` rejects
  `offset == -1`, so ids are provably dense `{0..nvolumes-1}`.
* unbounded `bzero(f_mntfromname + size, MNAMELEN - size)` at
  `hammer2_vfsops.c:1497-1499` on copyinstr fault: killed — DFly
  `std_copyinstr` (`sys/platform/pc64/x86_64/support.s:655-718`) always
  writes `*lencopied` (0 on immediate fault, ≤ maxlen otherwise).
* `hammer2_vfs_vget` "uninitialized ip": killed — `ip` is initialized by
  `hammer2_inode_lookup()` at `hammer2_vfsops.c:1915`.
* `!hmp->devvp` teardown of zero-initialized vchain/fchain
  (`hammer2_vfsops.c:1161-1167`): unreachable — `hammer2_init_volumes`
  returns EINVAL ("root volume not found") whenever `*rootvoldevvp` unset.
* `statfs`/`statvfs` leaving `*sbp` unwritten when `nchains == 0`
  (`hammer2_vfsops.c:2047-2081`): unreachable while a pmp is mounted
  (mount requires ≥1 chain; the `pfs_nmasters > nchains` wedge is DF-2630).
* `hammer2_remount` iterating `hmp->nvolumes` vs sparse `hmp->volumes[]`:
  same density proof as above kills it.
* `hammer2_pfsdealloc` not clearing `iroot->cluster.focus`/`pfs_names`:
  only reachable for unmounted pmps via ioctl; no deref path found
  (speculative, not reported).

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