β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2704

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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2704 Β· 4 files
FileTypeDescriptionSize
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 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).

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.md
↓ download raw

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:

  1. Reader: vop_cache_operate_ap() sys/kern/vfs_vopops.c:1774-1786 β€” ops->head.vv_mount->mnt_vn_journal_ops loaded twice (:1781 test, :1782 call) with no token/refcount. (vop_journal_operate_ap :1793-1803 has the same shape but only reads mnt_vn_norm_ops, which is never freed while the mount lives.)
  2. Swapper/free-er: journal_attach/journal_detach sys/kern/vfs_jops.c:233-252 β†’ vfs_add_vnodeops/vfs_rm_vnodeops sys/kern/vfs_init.c:113-162 β€” *ops_pp = NULL then kfree(ops, M_VNODEOP), no synchronization with in-flight VOP threads (vnodes reach the ops via *(vp)->v_ops which 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).
  3. Gate: sys_mountctl root-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_testable
per-fix-DF-2704

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