vop_cache_operate_ap dispatches through mnt_vn_journal_ops unsynchronized against journal_detach kfree (UAF window, root-gated)
| Field | Value |
|---|---|
| ID | DF-2704 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:L/A:L |
| CWE | CWE-367 TOCTOU / CWE-416 UAF (ops-vector lifetime) |
| File | sys/kern/vfs_vopops.c |
| Lines | 1781-1782 (free side vfs_init.c:145-162, vfs_jops.c:247-252) |
| Area | kern |
| Confidence | speculative |
| Discovered | 2026-08-30 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
The cache-coherency dispatch layer loads mp->mnt_vn_journal_ops twice with no token or refcount and VOCALLs through it; journal_detach() via vfs_rm_vnodeops() NULLs and kfrees that same struct vop_ops, also lock-free. A racing VOP thread can indirect-call through freed M_VNODEOP memory (VOCALL = offset-cast indirect call). Worst case kernel UAF function dispatch β but the free side requires root (mountctl is SYSCAP_RESTRICTEDROOT-gated) and no in-tree workload detaches journals under I/O load; the lock-protocol flaw itself is real and spans every VOP on a journalled mount.
Recommended fix
Serialize the swap with the dispatch layer β take lwkt_gettoken(&mp->mnt_token) (or refcount struct vop_ops) around the mnt_vn_journal_ops read in vop_cache_operate_ap and around the NULL+kfree in vfs_rm_vnodeops (paired token in vfs_add_vnodeops).
Timeline
- 2026-08-30 Discovered during pass-2 audit of vfs_vopops.c (GLM 5.3).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2704 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.9 KB | β raw | |
| VERDICT.md | β | 2.2 KB | β raw | |
| verdict.json | β | 2.3 KB | view raw | |
| manifest.json | β | 697 B | view raw |
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:
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 andcaps_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 failedMOUNTCTL_INSTALL_VFS_JOURNALalso 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_vnodeopsin 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.
VERDICT β DF-2704
status: untested (Phase V deliberately skipped: Low, speculative, root-gated trigger; deterministic reproduction would need a root mountctl storm plus scheduler manipulation).
Analysis
Race protocol violation spanning three files; the reader is in the audited file:
- Reader:
vop_cache_operate_ap()sys/kern/vfs_vopops.c:1774-1786 βops->head.vv_mount->mnt_vn_journal_opsloaded twice (:1781 test, :1782 call) with no token/refcount. (vop_journal_operate_ap:1793-1803 has the same shape but only readsmnt_vn_norm_ops, which is never freed while the mount lives.) - Swapper/free-er:
journal_attach/journal_detachsys/kern/vfs_jops.c:233-252 βvfs_add_vnodeops/vfs_rm_vnodeopssys/kern/vfs_init.c:113-162 β*ops_pp = NULLthenkfree(ops, M_VNODEOP), no synchronization with in-flight VOP threads (vnodes reach the ops via*(vp)->v_opswhich points at&mp->mnt_vn_use_ops, sys/kern/vfs_mount.c:209, re-read per call, so the use pointer swap itself is safe; it is the journal_ops pointer lifetime that is not). - Gate:
sys_mountctlroot-only (sys/kern/vfs_syscalls.c:1277-1283, SYSCAP_RESTRICTEDROOT + no-jail), so only root can start the free side.
Worst case is a function-pointer call through freed M_VNODEOP memory
(VOCALL = cast-of-offset indirect call, sys/sys/vnode.h:407) β i.e. a
kernel-controlled-but-not-attacker-shaped UAF dispatch, triggered by root action
racing user-triggerable VOP traffic.
Honest classification
- Real protocol flaw: yes β read the three call sites; nothing serializes them.
- Exploitable by an unprivileged user alone: no β the free requires root mountctl (journal remove or failed install auto-detach).
- Deterministic PoC cost vs. severity class: disproportionate (needs root helper
- schedule-injection). Per audit contract, Low speculative findings are not taken to the guest.
Recommended follow-up: file the serialization fix (mnt_token or ops refcount)
with upstream; if the team wants a live demo, a root loop of
mountctl install/remove journalling on a tmpfs while a user hammers
write(2) under debug.cpumask pinning is the cheapest stress shape.
Fix verification
not_testableConfirmed kernel references
Detail
Evidence (decisive lines)
['sys/kern/vfs_vopops.c:1774-1786 unlocked mnt_vn_journal_ops read + VOCALL', 'sys/kern/vfs_jops.c:247-252 journal_detach -> vfs_rm_vnodeops', 'sys/kern/vfs_init.c:143-162 vfs_rm_vnodeops: *ops_pp=NULL then kfree, no lock', 'sys/kern/vfs_syscalls.c:1277-1283 mountctl root gate', 'findings/poc/DF-2704/VERDICT.md full protocol analysis']
PoC changes
no PoC built: race window is a single load-to-call sequence and the free side needs root; not reproducible honestly without scheduler injection
Verified recommended fix
Serialize the journal-ops swap with the dispatch layer (mnt_token around the mnt_vn_journal_ops read in vop_cache_operate_ap and the NULL+ kfree in vfs_rm_vnodeops), or refcount struct vop_ops before freeing.
Verdict
vop_cache_operate_ap (sys/kern/vfs_vopops.c:1781-1782) loads and dispatches through mp->mnt_vn_journal_ops with no token or refcount; journal_detach (sys/kern/vfs_jops.c:247-252) via vfs_rm_vnodeops (sys/kern/vfs_init.c:143-162) NULLs and kfrees that same struct vop_ops, also unsynchronized. A racing VOP thread can VOCALL through freed M_VNODEOP memory (use-after-free function dispatch). The free side is reachable only by root (mountctl gated by SYSCAP_RESTRICTEDROOT, sys/kern/vfs_syscalls.c:1277-1283), and no in-tree workload detaches journals under I/O load, so this is a genuine protocol flaw with low practical exploitability. Phase V skipped per contract (Low/speculative/root-gated); would need a root mountctl storm plus scheduler injection to demo.
No comments yet.