# DF-2763 — journal_mountctl attach/detach lifecycle is unsynchronized → panic on INVARIANTS, double-kfree/UAF of mnt_jbitmap on release kernels

## What
`sys/kern/vfs_jops.c` manages the per-mount journal lifecycle state
machine (`mnt_vn_journal_ops` installed ⇔ `mnt_jbitmap` allocated ⇔
`mnt_jlist` entries) with **zero locking**:

- `journal_mountctl()` (vfs_jops.c:153-231) reads
  `mp->mnt_vn_journal_ops` lock-free and calls
  `journal_attach()`/`journal_detach()` on that basis.
- `journal_attach()` (vfs_jops.c:235-243) allocates `mnt_jbitmap` and
  installs the journal vop vector (`KKASSERT(mp->mnt_jbitmap == NULL)`).
- `journal_detach()` (vfs_jops.c:245-253) frees both
  (`KKASSERT(mp->mnt_jbitmap != NULL)`, `kfree(mp->mnt_jbitmap)`).
- `journal_remove_all_journals()` (vfs_jops.c:422-430) is called from
  `dounmount()` **holding `mp->mnt_token`** (vfs_syscalls.c:814,1037),
  but `journal_mountctl()` itself takes no token at all.

Two concurrent `mountctl` operations (or mountctl racing unmount)
interleave the state machine.  Reproduced interleave: thread A runs
`journal_attach()` (installs ops + bitmap) while thread B's
`MOUNTCTL_REMOVE` sees a stale non-NULL `mnt_vn_journal_ops`, empties
`mnt_jlist`, and calls `journal_detach()` (frees/NULLs the bitmap A just
installed).  The *next* remove then calls `journal_detach()` on an
already-NULL bitmap:

```
panic: assertion "mp->mnt_jbitmap != NULL" failed in journal_detach at vfs_jops.c:248
journal_detach() <- journal_mountctl() <- vop_mountctl() <- kern_mountctl() <- sys_mountctl()
```

On a release kernel (no INVARIANTS) the same interleaving where both
threads observe a non-NULL bitmap is a **double `kfree()` of the
1024-byte M_JOURNAL block**, and the detach-vs-in-flight-VOP variants
give a UAF bit-RMW in `jreclist_init()` (:547 read of a freed/NULL
`mnt_jbitmap`, :556 bit-set) and `jreclist_done()` (:613 bit-clear)
— the bitmap is freed while a streamid is still owned.  Related family:
DF-2704 (vop-vector dispatch UAF), DF-2747 (memfifo RMW), DF-2748
(jo/fifo free under sleepers).

## Who can trigger
`mountctl` is SYSCAP_RESTRICTEDROOT-gated (vfs_syscalls.c:1281-1285),
so the *trigger* is root-only; the racing *victim* side can be any
unprivileged VOP on the journaled mount.  Severity Low accordingly.

## PoC (reproduced 3/3, root-only minimal config)
```
# two concurrent install/remove churn loops on /tmp (tmpfs), ~90 s
sh df2763_run4.sh        # two loops: mountctl -a -w ... /tmp:cN ; mountctl -d /tmp:cN
```
Result (stock INVARIANTS kernel, run1/run3 with 3 unpriv writers,
run4 writers-free): panic at vfs_jops.c:248 within 25-90 s.

Files: `df2763.c` (unpriv writer stress, used in runs 1/3),
`df2763_root.sh`/`df2763_run2.sh`/`df2763_run3.sh`/`df2763_run4.sh`
(churn configs), `panic.txt` (all three captures), `run.log` narrative.

## Fix (validated)
Serialize the lifecycle: `lwkt_gettoken(&mp->mnt_token)` around the
whole `journal_mountctl()` body (pairs with `dounmount()`, which
already holds `mnt_token` across `journal_remove_all_journals()`).
See `fix.diff` (also carries the DF-2765/DF-2766 hunks; the DF-2763
hunks are the token pair at :168/:234).
