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_nodecreated for nodeid N), close it (vnode becomes unreferenced, eligible for reclaim), then concurrently resolve the same path βfuse_alloc_nodefinds the existingfuse_nodeunderino_lock, then releases it; vnode pressure (vnlru) reclaims the vnode βfuse_vop_reclaimβfuse_node_freeβobjcache_put.fuse_node_vnin the resolver now operates on freed memory. Reliable kernel panic. 2. Cross-process corruption primitive β the freedfuse_node(~232 bytes,M_FUSE_NODEslab) 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 viapipe/msgb/IP options), an attacker can control the contents of the freedfuse_node, causingfuse_node_vn's write tofnp->vp(line 180) to corrupt attacker-controlled memory, andfnp->fmpdereference (line 128) to read from an attacker-chosen address. Plausible escalation to arbitrary kernel read/write β local root (markedlikelyrather thancertainbecause the exploit chain is not proven here). - Required config or capabilities: A FUSE mount. Root needed to
mount on stock DragonFly; unprivileged if
vfs.usermountis 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).
Recommended fix
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
sys/vfs/fuse/fuse_node.c:106-119βfuse_alloc_node(the race).sys/vfs/fuse/fuse_node.c:75-88βfuse_node_free(no refcount).sys/vfs/fuse/fuse_vnops.c:1785-1803βfuse_vop_reclaim(the freer).sys/vfs/fuse/fuse_node.c:128,136,143,180βfuse_node_vndereferences after the lock drop.
Timeline
- 2026-07-05 Discovered during automated audit.
- pending Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0925 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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_NODEslab is not proven by this PoC; the demonstrated impact is a reliable kernel panic.
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)
-
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. -
Race window:
fuse_alloc_nodetakesfmp->ino_lock, doesRB_LOOKUPfor the inode, and either creates a newfuse_nodeor finds an existing one. It then releasesino_lock(line 112) and callsfuse_node_vn(fnp)(line 114) with no reference onfnp. -
Concurrent reclaim:
vnlrureclaims the vnode for the same inode viavclean β VOP_RECLAIM β fuse_vop_reclaim β fuse_node_free. Insidevclean(vfs_subr.c:1314),cache_inval_vpinvalidates the namecache entry BEFOREVOP_RECLAIM(line 1402). A concurrentnresolvecan miss the cache, enterfuse_alloc_node, find thefuse_nodein the RB tree (still present betweencache_inval_vpandfuse_node_free), dropino_lock, and callfuse_node_vn.fuse_node_vn'svget(vp)blocks on the VX lock held byvclean. WhenVOP_RECLAIMruns and freesfnp,vgetwakes up and the retry loop reads freedfnpβ UAF. -
Panic evidence: With diagnostic delays widening the race window (2s in
vcleanfor FUSE vnodes, 2s infuse_vop_reclaimafterfuse_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) andmount_fusefs(root, sincevfs.usermount=0)./dev/fuseis 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) withobjcache_malloc_alloc_zerobacking. The objcache magazine layer keeps the freed object type-stable β onlyM_FUSE_NODEallocations 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) andrace_winner.ckept 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
fixedVALIDATED 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.
Confirmed kernel references
- sys/vfs/fuse/fuse_node.c:106
- sys/vfs/fuse/fuse_node.c:107
- sys/vfs/fuse/fuse_node.c:112
- sys/vfs/fuse/fuse_node.c:114
- sys/vfs/fuse/fuse_node.c:78
- sys/vfs/fuse/fuse_node.c:83
- sys/vfs/fuse/fuse_node.c:87
- sys/vfs/fuse/fuse_node.c:128
- sys/vfs/fuse/fuse_node.c:136
- sys/vfs/fuse/fuse_node.c:143
- sys/vfs/fuse/fuse_node.c:148
- sys/vfs/fuse/fuse_node.c:180
- sys/vfs/fuse/fuse_vnops.c:1786
- sys/vfs/fuse/fuse_vnops.c:1792
- sys/vfs/fuse/fuse_vnops.c:1794
- sys/vfs/fuse/fuse_vnops.c:1798
- sys/kern/vfs_subr.c:1314
- sys/kern/vfs_subr.c:1402
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.
No comments yet.