# DF-2704 — vop_cache_operate_ap reads mnt_vn_journal_ops without synchronization (UAF window vs journal_detach kfree)

File: sys/kern/vfs_vopops.c (pass 2, GLM 5.3)
Severity: Low (speculative, root-gated trigger) — Confidence: speculative

## What

`vop_cache_operate_ap()` (sys/kern/vfs_vopops.c:1774-1786) — the cache-coherency
dispatch layer that every VOP on a journalled mount flows through — reads the
mount's journal-ops pointer with no lock:

```c
ops = ap->a_ops;
if (ops->head.vv_mount->mnt_vn_journal_ops)          /* :1781 unlocked read */
        error = VOCALL(ops->head.vv_mount->mnt_vn_journal_ops, ap);  /* :1782 */
else
        error = VOCALL(ops->head.vv_mount->mnt_vn_norm_ops, ap);
```

The teardown side, `journal_detach()` (sys/kern/vfs_jops.c:247-252), calls
`vfs_rm_vnodeops(mp, &journal_vnode_vops, &mp->mnt_vn_journal_ops)`
(sys/kern/vfs_init.c:143-162), which does `*ops_pp = NULL; ... kfree(ops, M_VNODEOP)`
— also with no lock that a concurrent VOP thread holds.

A thread that passes the NULL check at :1781 and then loses the CPU can
`VOCALL` through a `struct vop_ops` that has just been kfree()d → use-after-free
function-pointer dispatch (kernel-controlled contents of the freed M_VNODEOP
slab slot; on this 64-bit build `VOCALL` is `(*(vocall_func_t *)((char *)vops + sd_offset))(ap)`
— sys/sys/vnode.h:407).

## Trigger path and why it is only Low/speculative here

- Attaching/detaching journals is only possible through the `mountctl(2)` syscall,
  which is gated: `sys_mountctl()` requires no jail and
  `caps_priv_check_td(td, SYSCAP_RESTRICTEDROOT)` (sys/kern/vfs_syscalls.c:1277-1283).
  So the free side needs **root** (the journaling rc scripts run it at mount time;
  a failed `MOUNTCTL_INSTALL_VFS_JOURNAL` also auto-detaches, vfs_jops.c:176-179).
- The racing reader side is any in-flight VOP on the same mount from any user.
- Window is one load-to-indirect-call sequence; no known in-tree workload detaches
  journals under load. Practical exploitability is low, but the protocol is
  genuinely unsynchronized (no mnt_token, no refcount on vop_ops —
  `vfs_add_vnodeops`/`vfs_rm_vnodeops` in sys/kern/vfs_init.c:113-162 are lock-free).

## Recommended fix (upstream-appropriate direction)

Serialize journal ops swap against the dispatch layer — e.g. take
`lwkt_gettoken(&mp->mnt_token)` around the `mnt_vn_journal_ops` read in
`vop_cache_operate_ap`/`vop_journal_operate_ap` and around the pointer swap+free
in `vfs_rm_vnodeops` (journal ops themselves already run under mnt_token in
tmpfs/hammer handlers), or refcount the `struct vop_ops` (ops_pp) with
`waitrefs` before kfree.

## Reproduction status

Not executed (Phase V skipped): speculative root-gated race, Low severity; a
deterministic reproduction would require a root-driven journal
install/remove storm plus injected scheduling delays, which is out of proportion
to the finding's severity class per the audit contract.
