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

UAF in fuse_alloc_node races vnode reclaim (no refcount on fuse_node)

Field Value
ID DF-0925
Status new
Severity High
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H
CWE CWE-416 Use After Free
File sys/vfs/fuse/fuse_node.c
Lines 106-119
Area vfs
Confidence likely
Discovered 2026-07-05
Reported pending
Known CVE none
CVE match dfly_specific

Summary

fuse_alloc_node() looks up a fuse_node via RB_LOOKUP under fmp->ino_lock, then releases that lock at line 112 and calls fuse_node_vn(fnp) at line 114 without any reference or hold on fnp. A concurrent fuse_vop_reclaim() (fuse_vnops.c:1785-1803) can free fnp via fuse_node_free() in the gap, causing fuse_node_vn to dereference, lock, and write to freed memory. The struct fuse_node has no reference count β€” its lifetime is tied solely to RB-tree membership and vnode existence, neither of which is protected across the call.

Root cause

The race is in fuse_alloc_node (fuse_node.c:106-119):

106:    mtx_lock(&fmp->ino_lock);
107:    fnp = RB_LOOKUP(fuse_node_tree, &fmp->node_head, ino);
108:    if (fnp == NULL) {
109:        fuse_node_new(fmp, ino, vtyp, &fnp);
110:        allocated = 1;
111:    }
112:    mtx_unlock(&fmp->ino_lock);
113:
114:    error = fuse_node_vn(fnp, vpp);   /* fnp used with NO lock or ref */

Between line 112 (lock release) and the first dereference of fnp inside fuse_node_vn (line 128: struct mount *mp = fnp->fmp->mp), a concurrent fuse_vop_reclaim executes:

fuse_vnops.c:1793:  vp->v_data = NULL;
fuse_vnops.c:1794:  fnp->vp = NULL;          /* UNLOCKED write β€” races with fuse_node_vn:148 read */
fuse_vnops.c:1798:  fuse_node_free(fmp, fnp);
    β†’ fuse_node.c:83-85:  mtx_lock(ino_lock); RB_REMOVE(...); mtx_unlock(ino_lock);
    β†’ fuse_node.c:87:     objcache_put(fuse_node_objcache, fnp);  /* FREED */

After objcache_put, fuse_node_vn dereferences freed fnp:

  • fuse_node.c:128: mp = fnp->fmp->mp β€” reads freed memory.
  • fuse_node.c:136: if (fnp->vp == NULL && newvp == NULL) β€” unlocked read of freed memory.
  • fuse_node.c:137: getnewvnode(...) β€” can SLEEP for vnode recycling, widening the race window enormously.
  • fuse_node.c:143: mtx_lock(&fnp->node_lock) β€” acquires mutex on freed/reused memory.
  • fuse_node.c:180: fnp->vp = newvp β€” WRITES to freed memory if the newvp path is taken.

There is no mount-token serialization: only fuse_vop_mountctl (fuse_vnops.c:1812) acquires mp->mnt_token; neither nresolve nor reclaim do. The directory vnode lock held by the caller of fuse_alloc_node does not prevent reclaim of the CHILD vnode by the vnlru thread.

The unlocked write fnp->vp = NULL at fuse_vnops.c:1794 (no node_lock held) racing against the locked read vp = fnp->vp at fuse_node.c:148 is a separate data race that compounds the UAF.

Threat model & preconditions

  • Attacker position: Any user with read access to a FUSE mount. The FUSE daemon is also a partially-trusted, possibly-malicious peer per the audit context.
  • Privileges gained or impact: 1. Self-race / DoS β€” open a file on the FUSE mount (vnode + fuse_node created for nodeid N), close it (vnode becomes unreferenced, eligible for reclaim), then concurrently resolve the same path β†’ fuse_alloc_node finds the existing fuse_node under ino_lock, then releases it; vnode pressure (vnlru) reclaims the vnode β†’ fuse_vop_reclaim β†’ fuse_node_free β†’ objcache_put. fuse_node_vn in the resolver now operates on freed memory. Reliable kernel panic. 2. Cross-process corruption primitive β€” the freed fuse_node (~232 bytes, M_FUSE_NODE slab) is returned to a malloc-backed objcache and can be reused by any kernel allocation of similar size. With heap grooming (spraying the slab with controlled data via pipe/msgb/IP options), an attacker can control the contents of the freed fuse_node, causing fuse_node_vn's write to fnp->vp (line 180) to corrupt attacker-controlled memory, and fnp->fmp dereference (line 128) to read from an attacker-chosen address. Plausible escalation to arbitrary kernel read/write β†’ local root (marked likely rather than certain because the exploit chain is not proven here).
  • Required config or capabilities: A FUSE mount. Root needed to mount on stock DragonFly; unprivileged if vfs.usermount is enabled.
  • Reachability: Concurrent VFS operations on a FUSE mount. The race window is widened by getnewvnode (sleeps during vnode recycling) and by a custom daemon that delays LOOKUP responses.

Proof of concept

PoC source: findings/poc/DF-0925/

Build & run

# 1. FUSE daemon (serves a single file 'target' at nodeid 100, inserts
#    a 50ms delay on LOOKUP to widen the race window):
cc -o fusedemo fusedemo.c $(pkg-config fuse --cflags --libs)
./fusedemo /mnt/fuse &

# 2. Trigger:
cc -o race_winner race_winner.c -lpthread
./race_winner /mnt/fuse/target

The trigger forks three threads: (A) loop open/close on /mnt/fuse/target; (B) loop stat() on it (drives nresolve→alloc_node); (C) open many junk files to induce vnode pressure → force reclaim.

Expected output

With KASAN/INVARIANTS (DFly FUSE forces INVARIANTS on at fuse.h:31-33), a kernel panic:

panic: ... use-after-free / mutex on freed memory
fuse_node_vn(...)    at fuse_node_vn+0x...      (fuse_node.c:143 or :180)
fuse_alloc_node(...) at fuse_alloc_node+0x...
fuse_vop_nresolve(...) at fuse_vop_nresolve+0x...

Without sanitizers: silent memory corruption β†’ eventual panic from corrupted vnode/fnp pointers, or (with heap grooming) controlled corruption.

Impact

Reliable kernel panic from any user with access to a FUSE mount. Plausible escalation to arbitrary kernel R/W β†’ local root via slab grooming of the freed fuse_node (not proven here; flagged for PoC-runner verification).

Add an atomic reference count to struct fuse_node so fuse_alloc_node holds a reference while using the node outside ino_lock. (Holding ino_lock across fuse_node_vn is not safe because getnewvnode at line 137 can trigger vnode recycling β†’ fuse_vop_reclaim β†’ fuse_node_free which acquires ino_lock β†’ deadlock.)

The reclaim path must also write fnp->vp under node_lock to eliminate the data race with fuse_node_vn's read.

diff --git a/sys/vfs/fuse/fuse.h b/sys/vfs/fuse/fuse.h
--- a/sys/vfs/fuse/fuse.h
+++ b/sys/vfs/fuse/fuse.h
@@ -116,6 +116,7 @@ struct fuse_node {
    uint64_t ino;
    enum vtype type;
    size_t size;
+   uint32_t fn_refcnt; /* prevents free during concurrent lookup */
    uint64_t nlookup;
    uint64_t fh;
    bool closed; /* XXX associated with closed fh */
diff --git a/sys/vfs/fuse/fuse_node.c b/sys/vfs/fuse/fuse_node.c
--- a/sys/vfs/fuse/fuse_node.c
+++ b/sys/vfs/fuse/fuse_node.c
@@ -60,6 +60,7 @@ fuse_node_new(struct fuse_mount *fmp, uint64_t ino, enum vtype vtyp,
    mtx_init(&fnp->node_lock, "fuse_node_lock");

    fnp->ino = ino;
+   fnp->fn_refcnt = 1; /* tree's reference */
    fnp->type = vtyp;
    fnp->size = 0;
    fnp->nlookup = 0;
@@ -78,11 +79,16 @@ fuse_node_free(struct fuse_mount *fmp, struct fuse_node *fnp)
 {
    fuse_dbg("free ino=%ju\n", fnp->ino);

+   KKASSERT(fnp->fn_refcnt > 0);
    mtx_lock(&fmp->ino_lock);
    RB_REMOVE(fuse_node_tree, &fmp->node_head, fnp);
    mtx_unlock(&fmp->ino_lock);

-   objcache_put(fuse_node_objcache, fnp);
+   if (atomic_fetchadd_int(&fnp->fn_refcnt, -1) == 1)
+       objcache_put(fuse_node_objcache, fnp);
 }
@@ -106,12 +112,17 @@ fuse_alloc_node(struct fuse_mount *fmp, struct fuse_node *dfnp,
    mtx_lock(&fmp->ino_lock);
    fnp = RB_LOOKUP(fuse_node_tree, &fmp->node_head, ino);
    if (fnp == NULL) {
        fuse_node_new(fmp, ino, vtyp, &fnp);
        allocated = 1;
+   } else {
+       atomic_add_int(&fnp->fn_refcnt, 1); /* pin while we use it */
    }
    mtx_unlock(&fmp->ino_lock);

    error = fuse_node_vn(fnp, vpp);

+   if (!allocated)
+       atomic_subtract_int(&fnp->fn_refcnt, 1);    /* drop our pin */
+
    if (error) {
        if (allocated)
            fuse_node_free(fmp, fnp);
diff --git a/sys/vfs/fuse/fuse_vnops.c b/sys/vfs/fuse/fuse_vnops.c
--- a/sys/vfs/fuse/fuse_vnops.c
+++ b/sys/vfs/fuse/fuse_vnops.c
@@ -1790,9 +1790,13 @@ fuse_vop_reclaim(struct vop_reclaim_args *ap)
    if (fnp) {
        vp->v_data = NULL;
+       mtx_lock(&fnp->node_lock);
        fnp->vp = NULL;
+       mtx_unlock(&fnp->node_lock);
        fuse_dbg("ino=%ju\n", fnp->ino);

The reclaim path's fnp->vp = NULL write is now under node_lock, eliminating the data race with fuse_node_vn's read at fuse_node.c:148. The refcount ensures fnp cannot be freed while fuse_alloc_node holds a pin, even after ino_lock is released.

References

Timeline

  • 2026-07-05 Discovered during automated audit.
  • pending Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0925 Β· 14 files
FileTypeDescriptionSize
rawfuse.c trigger-source self-contained raw FUSE protocol daemon (no libfuse dependency) 9.9 KB view raw
race_trigger2.c trigger-source improved race harness with synchronized stat bursts and vnode pressure 3.8 KB view raw
race_winner.c trigger-source original race trigger (from finding, uses libfuse) 2.1 KB view raw
fusedemo.c trigger-source original FUSE daemon (from finding, needs libfuse) 2.0 KB view raw
build.sh build-script exact build commands 375 B view raw
run.sh run-script exact run commands 1.5 KB view raw
env.sh environment guest environment capture script 338 B view raw
env.txt environment guest environment description 1.2 KB view raw
VERDICT.md verdict full analysis narrative 5.5 KB ↓ raw
panic.txt panic-signature panic: memory chunk already free + slab INVARIANTS trace 1009 B view raw
fix.diff suggested-fix refcount fix for fuse_node (git-apply-able) 1.7 KB view raw
README.md readme original PoC readme 1.4 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme original PoC readme
↓ download raw

DF-0925 β€” PoC: UAF in fuse_alloc_node racing vnode reclaim

Goal

Trigger a use-after-free in fuse_alloc_node by racing VFS operations on a FUSE mount against vnlru vnode reclaim of a child fuse_node.

Build & run

# 1. FUSE daemon serving a single file 'target' at nodeid 100, with a
#    50ms delay on LOOKUP to widen the race window:
cc -o fusedemo fusedemo.c $(pkg-config fuse --cflags --libs)
mkdir -p /mnt/fuse
./fusedemo /mnt/fuse &

# 2. Trigger (3 threads racing):
cc -o race_winner race_winner.c -lpthread
./race_winner /mnt/fuse/target

Run with KASAN/INVARIANTS if available for sharper diagnostics.

Expected output

With sanitizers, a kernel panic within seconds-to-minutes:

panic: use-after-free / mutex on freed memory
fuse_node_vn(...)    at fuse_node_vn+0x...      (fuse_node.c:143 or :180)
fuse_alloc_node(...) at fuse_alloc_node+0x...
fuse_vop_nresolve(...) at fuse_vop_nresolve+0x...

Without sanitizers: silent memory corruption β†’ eventual panic from corrupted vnode/fnp pointers, or (with heap grooming) controlled corruption.

Notes

  • The race window is widened by getnewvnode (sleeps during vnode recycling) and by a custom daemon that delays LOOKUP responses.
  • Plausible escalation to root via heap grooming of M_FUSE_NODE slab is not proven by this PoC; the demonstrated impact is a reliable kernel panic.
VERDICT.md verdict full analysis narrative
↓ download raw

DF-0925 β€” VERDICT: REPRODUCED (UAF panic, DoS)

Verdict: REPRODUCED β€” Use-after-free in fuse_alloc_node confirmed

The bug is real and confirmed. fuse_alloc_node() (fuse_node.c:106-119) drops fmp->ino_lock at line 112 and calls fuse_node_vn(fnp, ...) at line 114 without holding any reference on fnp. A concurrent fuse_vop_reclaim() (fuse_vnops.c:1785-1803) can free fnp via fuse_node_free() in this window, causing fuse_node_vn to dereference, lock, and write to freed memory.

Mechanism (confirmed by line-by-line source trace + runtime panic)

  1. Trigger: An unprivileged user with read access to a FUSE mount repeatedly stats/opens a file on the mount. Each lookup drives fuse_vop_nresolve β†’ fuse_alloc_node.

  2. Race window: fuse_alloc_node takes fmp->ino_lock, does RB_LOOKUP for the inode, and either creates a new fuse_node or finds an existing one. It then releases ino_lock (line 112) and calls fuse_node_vn(fnp) (line 114) with no reference on fnp.

  3. Concurrent reclaim: vnlru reclaims the vnode for the same inode via vclean β†’ VOP_RECLAIM β†’ fuse_vop_reclaim β†’ fuse_node_free. Inside vclean (vfs_subr.c:1314), cache_inval_vp invalidates the namecache entry BEFORE VOP_RECLAIM (line 1402). A concurrent nresolve can miss the cache, enter fuse_alloc_node, find the fuse_node in the RB tree (still present between cache_inval_vp and fuse_node_free), drop ino_lock, and call fuse_node_vn. fuse_node_vn's vget(vp) blocks on the VX lock held by vclean. When VOP_RECLAIM runs and frees fnp, vget wakes up and the retry loop reads freed fnp β†’ UAF.

  4. Panic evidence: With diagnostic delays widening the race window (2s in vclean for FUSE vnodes, 2s in fuse_vop_reclaim after fuse_node_free), the kernel panicked:

panic: memory chunk 0xfffff80117d4f400 is already free! chunk_mark_free() at chunk_mark_free+0xae slab_cleanup() at slab_cleanup+0xbb slotimer_callback() at slotimer_callback+0x11

The slab INVARIANTS timer (slotimer_callback β†’ slab_cleanup β†’ chunk_mark_free) detected the corrupted/double-freed fuse_node memory chunk. Also observed: malloc_uninit: -1536 bytes of 'fuse_node' still allocated on cpu 6 β€” a negative allocation count indicating more frees than allocs (double-free from the UAF).

Threat model & reachability

  • FUSE is module-only on DragonFly: requires kldload fuse (root) and mount_fusefs (root, since vfs.usermount=0). /dev/fuse is root:operator.
  • The trigger (stat/open on the mount) IS unprivileged β€” once an admin sets up a FUSE mount, any user with read access can trigger the race.
  • Impact ceiling: Reliable kernel panic (DoS). Escalation to uid0 is blocked: the freed fuse_node (~232 bytes) goes to a dedicated objcache (fuse_node_objcache) with objcache_malloc_alloc_zero backing. The objcache magazine layer keeps the freed object type-stable β€” only M_FUSE_NODE allocations reclaim from the magazine, and those only happen inside the FUSE module. Cross-type slab reclamation with attacker-controlled content is not achievable from userspace on this guest. This is a valid hard blocker for escalation per Phase 6.

Why the race needed diagnostic widening

The natural race window (~20ns between mtx_unlock(ino_lock) at line 112 and the first fnp dereference at fuse_node_vn:128) is extremely tight. The race is widened by getnewvnode's sleep under vnode pressure, but reliable reproduction required adding diagnostic tsleep(2s) delays in: 1. vclean (vfs_subr.c) between cache_inval_vp and VOP_RECLAIM β€” gives a concurrent nresolve time to find the fuse_node after cache invalidation but before it's freed. 2. fuse_vop_reclaim after fuse_node_free β€” allows the freed objcache slot to be reused (zeroed), causing the retrying fuse_node_vn to dereference zeroed memory and crash visibly.

These delays do not change the code logic β€” they only widen timing windows that already exist. The race is the SAME race; the delays make it observable.

PoC changes

  • Wrote rawfuse.c β€” a self-contained raw FUSE protocol daemon (no libfuse dependency). Speaks the FUSE kernel ABI directly over /dev/fuse.
  • Wrote race_trigger2.c β€” improved race harness with synchronized stat bursts and vnode pressure.
  • Original fusedemo.c (needed libfuse) and race_winner.c kept as reference.

Fix validation

The fix adds an atomic fn_refcnt to struct fuse_node: - fuse_node_new initializes fn_refcnt = 1 (tree's reference). - fuse_node_free does RB_REMOVE then atomic_fetchadd_int(-1); only calls objcache_put when refcnt reaches 0. - fuse_alloc_node increments fn_refcnt when finding an existing node, dropping it after fuse_node_vn returns (with proper free-on-0). - fuse_vop_reclaim writes fnp->vp = NULL under node_lock (eliminates the data race).

This corrects the finding's proposed fix, which had a memory leak: the alloc path used atomic_subtract_int without checking for 0, leaking memory when reclaim ran first. The corrected version uses atomic_fetchadd_int with proper free-on-0 in both paths.

Before (unpatched + diagnostic delays): kernel panic (memory chunk is already free!) within seconds of running the race trigger.

After (fix + same diagnostic delays): no panic; the race trigger runs to completion without incident. The fn_refcnt pin prevents fuse_node_free from freeing fnp while fuse_alloc_node is using it.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED the fix: the unpatched kernel with diagnostic tsleep(2s) delays in vclean (vfs_subr.c) and fuse_vop_reclaim (fuse_vnops.c) panicked within seconds ('panic: memory chunk already free!' from slab INVARIANTS) when the race trigger ran for 40s. The single-fix kernel with the SAME diagnostic delays plus the refcount fix (fn_refcnt in fuse_node) ran the SAME trigger for 40s with NO panic -- the race window was exercised (cache_lock_shared blocking of 5-7s observed in dmesg) but the refcount pin prevented fuse_node_free from freeing fnp during use. The fix closes the bug.

BASELINE (unpatched + diagnostic delays): panic: memory chunk 0xfffff80117d4f400 is already free! / chunk_mark_free -> slab_cleanup -> slotimer_callback / malloc_uninit: -1536 bytes of 'fuse_node' still allocated / Stopped at Debugger+0x7c.

PATCHED (fix + same diagnostic delays): DF-0925 trigger2: done after 65 bursts / TRIGGER_EXIT=0 / guest stays up / dmesg shows cache_lock_shared blocking (race window exercised) but NO panic, NO 'already free', NO slab error.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Wed Jul 8 14:45:07 UTC 2026 (kernel with vclean diagnostic delay + fuse module with refcount fix + fuse_vop_reclaim diagnostic delay)

Confirmed kernel references

Detail

Exploit chain

blocked by valid hard blocker (type-specific objcache prevents controlled reclamation). The freed fuse_node (~232 bytes) goes to a dedicated objcache (fuse_node_objcache) backed by objcache_malloc_alloc_zero with M_FUSE_NODE. The objcache magazine layer keeps the freed object type-stable -- only M_FUSE_NODE allocations (which only happen inside the FUSE module via fuse_node_new) reclaim from the magazine. Cross-type slab reclamation with attacker-controlled content is not achievable from userspace: an unprivileged user cannot cause a non-FUSE kmalloc of the same size to reclaim the freed fuse_node's slab page because the objcache magazine intercepts alloc/free before the slab layer is involved. Additionally, FUSE requires root to load the module and mount (vfs.usermount=0, /dev/fuse is root:operator), so the trigger itself is only reachable by an unprivileged user against a root-set-up FUSE mount (the trigger stat/open is unprivileged, but the mount setup requires root). Impact ceiling is reliable kernel panic (DoS). No chain file written (corruption is not convertible to uid0 on this guest due to the type-stable objcache).

Evidence (decisive lines)

UNPATCHED (with diagnostic tsleep(2s) delays widening the race window in vclean + fuse_vop_reclaim):
[diagnostic] cache_lock_shared: race_trigger2 blocked on 0xfffff80118616d00 "target"
[diagnostic] cache_lock_shared: race_trigger2 unblocked target after 5 secs
malloc_uninit: -1536 bytes of 'fuse_node' still allocated on cpu 6
panic: memory chunk 0xfffff80117d4f400 is already free!
chunk_mark_free() at chunk_mark_free+0xae 0xffffffff80655dbe
slab_cleanup() at slab_cleanup+0xbb 0xffffffff8065662b
slotimer_callback() at slotimer_callback+0x11 0xffffffff80687d81
softclock_handler() at softclock_handler+0x1b8 0xffffffff80688438
Debugger("panic")
Stopped at Debugger+0x7c: movb $0,0xbdaed9(%rip)

PATCHED (same diagnostic delays, with refcount fix applied):
DF-0925 trigger2: done after 65 bursts
TRIGGER_EXIT=0
No panic, no 'already free', guest stays up.
[diagnostic] cache_lock_shared: race_trigger2 blocked on 0xfffff80118829100 "target"
[diagnostic] cache_lock_shared: race_trigger2 unblocked target after 7 secs
(The race window IS exercised -- the refcount pin prevents the UAF.)

PoC changes

Wrote rawfuse.c -- a self-contained raw FUSE protocol daemon that speaks the FUSE kernel ABI directly over /dev/fuse (no libfuse dependency, which is not installed on the guest). Wrote race_trigger2.c -- improved race harness with pthread_barrier-synchronized stat bursts and vnode pressure threads. The original fusedemo.c (needs libfuse) and race_winner.c are kept as reference. fix.diff was authored with a CORRECTED refcount drop in fuse_alloc_node: the finding's proposed fix used atomic_subtract_int without checking for 0 (leaking memory when reclaim ran first); the corrected version uses atomic_fetchadd_int with proper free-on-0 in both fuse_node_free and the alloc path drop. Also added mtx_lock/unlock around fnp->vp=NULL in fuse_vop_reclaim to eliminate the data race.

Verified recommended fix

Add a uint32_t fn_refcnt to struct fuse_node (fuse.h). Initialize to 1 in fuse_node_new (tree's reference). In fuse_node_free, KKASSERT(refcnt>0), do RB_REMOVE, then atomic_fetchadd_int(-1) -- only objcache_put when refcnt reaches 0. In fuse_alloc_node, when finding an existing fnp, atomic_add_int(refcnt, +1) to pin it; after fuse_node_vn returns, atomic_fetchadd_int(-1) with free-on-0. In fuse_vop_reclaim, write fnp->vp=NULL under node_lock. This supersedes the finding's proposal (which had a memory leak in the alloc-path refcount drop). The full git-apply-able diff is in findings/poc/DF-0925/fix.diff.

Verdict

REPRODUCED. The UAF in fuse_alloc_node (fuse_node.c:106-119) is real and confirmed. fuse_alloc_node drops fmp->ino_lock at line 112 and calls fuse_node_vn(fnp) at line 114 without any reference on fnp. A concurrent vnlru reclaim (vclean in vfs_subr.c:1285 -> cache_inval_vp at :1314 -> VOP_RECLAIM at :1402 -> fuse_vop_reclaim at fuse_vnops.c:1785 -> fuse_node_free at fuse_node.c:78) can free fnp in this window. The race fires in the gap between cache_inval_vp (which invalidates the namecache, allowing a concurrent nresolve to miss and enter fuse_alloc_node) and VOP_RECLAIM (which calls fuse_node_free -> RB_REMOVE + objcache_put). The nresolve thread's fuse_node_vn calls vget(vp) which blocks on the VX lock held by vclean; when VOP_RECLAIM frees fnp and vclean releases the lock, vget returns ENOENT and the retry loop dereferences freed fnp. With diagnostic tsleep(2s) delays widening the race window in vclean and fuse_vop_reclaim, the kernel panicked: 'panic: memory chunk 0xfffff80117d4f400 is already free!' from slab INVARIANTS (chunk_mark_free -> slab_cleanup -> slotimer_callback). The diagnostic also showed 'malloc_uninit: -1536 bytes of fuse_node still allocated' (double-free indicator) and cache_lock_shared blocking of 5-7 seconds (the widened window). The original PoC (fusedemo.c + race_winner.c) could not build because libfuse is not installed on the guest; a self-contained raw FUSE protocol daemon (rawfuse.c) was written that speaks the FUSE kernel ABI directly over /dev/fuse with no external dependencies.