Type-confusion panic in fuse_alloc_node when daemon reuses nodeid with conflicting type
| Field | Value |
|---|---|
| ID | DF-0926 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-843 Type Confusion |
| File | sys/vfs/fuse/fuse_node.c |
| Lines | 106-114 |
| Area | vfs |
| Confidence | certain |
| Discovered | 2026-07-05 |
| Reported | pending |
| Known CVE | none |
| CVE match | dfly_specific |
Summary
When fuse_alloc_node finds an existing fuse_node via RB_LOOKUP, it
completely ignores the vtyp argument and proceeds to use the existing
node with its original type. A malicious FUSE daemon can return an
existing nodeid of one type (e.g., VREG) in response to a
CREATE/MKDIR/SYMLINK request that expects a different type. The
caller then calls fuse_set_attr with the daemon's conflicting type,
hitting KKASSERT(vap->va_type == fnp->type) (fuse_vnops.c:81).
Because INVARIANTS is unconditionally #define'd (fuse.h:31-33), this
KKASSERT always fires in production, causing an immediate kernel
panic β a reliable denial-of-service triggered by any user performing a
create/mkdir/symlink on the malicious mount.
Root cause
In fuse_alloc_node (fuse_node.c:106-114):
107: fnp = RB_LOOKUP(fuse_node_tree, &fmp->node_head, ino);
108: if (fnp == NULL) {
109: fuse_node_new(fmp, ino, vtyp, &fnp); /* vtyp used for new nodes */
110: allocated = 1;
111: }
114: error = fuse_node_vn(fnp, vpp); /* existing fnp->type used; vtyp IGNORED */
When fnp is found (non-NULL), the vtyp parameter β which the caller
derived from the daemon's feo->attr.mode β is never compared to
fnp->type. The callers (fuse_vnops.c:717 ncreate, :791 nmknod,
:920 nmkdir, :1289 nsymlink) then call fuse_set_attr(fnp,
&feo->attr), which executes:
fuse_vnops.c:56: vap->va_type = IFTOVT(fat->mode); /* daemon's claimed type */
fuse_vnops.c:81: KKASSERT(vap->va_type == fnp->type); /* panics if mismatch */
fuse.h:31-33 unconditionally forces INVARIANTS on:
#ifndef INVARIANTS
#define INVARIANTS
#endif
so this KKASSERT fires even in release/production kernels. The daemon
controls fat->mode, so it can trivially create a type mismatch for any
existing nodeid.
Note:
fuse_vop_nresolve(fuse_vnops.c:571) does NOT callfuse_set_attrafterfuse_alloc_node, so thenresolvepath silently returns a vnode of the wrong type without panicking β a silent type confusion that could cause incorrect VOP behavior ifINVARIANTSwere ever removed.
Threat model & preconditions
- Attacker position: The FUSE daemon (explicitly treated as a
partially-trusted, possibly-malicious peer per the audit context). Any
local user accessing the mount triggers the attack by performing a
normal
create/mkdir/symlinkoperation. The daemon simply returns an existing nodeid (e.g., the nodeid of a previously created regular file) in itsCREATEresponse withmode=S_IFDIR. The kernel panics deterministically β no race, no timing, 100% reliable. - Privileges gained or impact: Kernel panic (denial of service). On a multi-user system, a malicious FUSE mount can crash the kernel whenever any user creates a file on it.
- Required config or capabilities: A FUSE mount.
- Reachability:
mkdir /mnt/fuse/anything(ortouch,ln -s) against a malicious mount.
If INVARIANTS were removed from fuse.h (which a developer might do
for performance), the KKASSERT becomes a no-op and the type confusion
proceeds silently: vp->v_type (from fnp->type) would disagree with
fnp->attr.va_type (from daemon). This could enable confused-deputy
attacks where directory operations are performed through a regular-file
vnode or vice versa, potentially corrupting kernel state.
Proof of concept
PoC source: findings/poc/DF-0926/
Build & run
# 1. Malicious FUSE daemon: cc -o fusedemo fusedemo.c $(pkg-config fuse --cflags --libs) ./fusedemo /mnt/fuse & # 2. Trigger (any user with access to the mount): ls /mnt/fuse/baitfile # creates fuse_node(100, VREG) mkdir /mnt/fuse/crashdir # daemon returns nodeid=100 as VDIR β KKASSERT panic
Expected output
panic: vap->va_type == fnp->type cpuid = ... Trace begins at ... fuse_set_attr(...) at fuse_set_attr+0x... (fuse_vnops.c:81) fuse_vop_nmkdir(...) at fuse_vop_nmkdir+0x... vop_nmkdir(...) at vop_nmkdir+0x... ...
The system halts. 100% reproducible, no race required.
Impact
Reliable kernel panic (denial of service) from any user with access to a
malicious FUSE mount. Silent type confusion if INVARIANTS were removed.
Recommended fix
Add a type-consistency check in fuse_alloc_node when an existing node
is found. If the daemon's claimed type conflicts with the existing node's
type, return EINVAL (do not proceed to fuse_set_attr where the
KKASSERT would panic).
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
@@ -106,8 +106,14 @@ 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 if (fnp->type != vtyp) {
+ /*
+ * The daemon returned an existing nodeid with a conflicting
+ * type. Reject instead of proceeding to fuse_set_attr which
+ * would KKASSERT-panic the kernel.
+ */
+ mtx_unlock(&fmp->ino_lock);
+ return EINVAL;
}
mtx_unlock(&fmp->ino_lock);
This prevents both the panic (current INVARIANTS-on behavior) and the
silent type confusion (hypothetical INVARIANTS-off behavior). The caller
already handles EINVAL from fuse_alloc_node.
References
sys/vfs/fuse/fuse.h:31-33βINVARIANTSunconditionally forced on.sys/vfs/fuse/fuse_vnops.c:56,81βfuse_set_attrKKASSERT.sys/vfs/fuse/fuse_vnops.c:717,791,920,1289β callers passing the daemon's claimedvtyp.
Timeline
- 2026-07-05 Discovered during automated audit.
- pending Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0926 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fusedev.c | trigger-source | standalone malicious FUSE daemon (no libfuse), reuses nodeid 100 across VREG lookup and VDIR mkdir | 8.0 KB | view raw |
| fusedemo.c | trigger-source | original libfuse-based daemon (does not build on guest: no libfuse installed) | 2.5 KB | view raw |
| trigger.sh | run-script | unprivileged ls+mkdir sequence that fires the panic | 511 B | view raw |
| build.sh | build-script | cc -o fusedev fusedev.c | 224 B | view raw |
| run.sh | run-script | kldload fuse, start daemon, trigger as maxx | 1.2 KB | view raw |
| panic.txt | panic-signature | KKASSERT in fuse_set_attr called from fuse_vop_nmkdir | 568 B | view raw |
| env.txt | environment | uname/kern.version/cc/sysctl at verification time | 315 B | view raw |
| fix.diff | suggested-fix | reject nodeid-reuse with conflicting type in fuse_alloc_node (validated) | 609 B | view raw |
| fix_build.log | build-log | full build of patched fuse.ko module, BUILD_RC=0 | 10.3 KB | view raw |
| fix_run.log | run-log | 3x deterministic post-fix PoC runs (mkdir returns EINVAL, no panic) | 1.0 KB | view raw |
| fix_env.txt | environment | patched-kernel environment: kern.version, fuse.ko sha256, patched source excerpt | 907 B | view raw |
| run.log | run-log | decisive baseline reproduction run output incl panic | 1.2 KB | view raw |
| VERDICT.md | verdict | full narrative analysis | 9.1 KB | β raw |
| README.md | readme | initial PoC README from finding scaffolding | 1.4 KB | β raw |
| manifest.json | manifest | this catalog | 3.7 KB | view 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-0926 β PoC: type-confusion panic via daemon nodeid reuse
Goal
Trigger a deterministic kernel panic by serving a FUSE mount that returns
the same nodeid for two different operations with conflicting types
(e.g., S_IFREG for one file, S_IFDIR for a directory created later).
The kernel hits KKASSERT(vap->va_type == fnp->type) at
fuse_vnops.c:81, which fires unconditionally because INVARIANTS is
forced on at fuse.h:31-33.
Build & run
cc -o fusedemo fusedemo.c $(pkg-config fuse --cflags --libs) -D_FILE_OFFSET_BITS=64 mkdir -p /mnt/fuse ./fusedemo /mnt/fuse & # In another shell (any user with access to /mnt/fuse): ls /mnt/fuse/baitfile # creates fuse_node(100, VREG) mkdir /mnt/fuse/crashdir # daemon returns nodeid=100 as VDIR -> KKASSERT panic
Expected output
panic: vap->va_type == fnp->type cpuid = ... Trace begins at ... fuse_set_attr(...) at fuse_set_attr+0x... (fuse_vnops.c:81) fuse_vop_nmkdir(...) at fuse_vop_nmkdir+0x... vop_nmkdir(...) at vop_nmkdir+0x...
The system halts. 100% reproducible, no race required.
Notes
- Any user with read+execute on the FUSE mount can trigger this; the malicious behavior is entirely on the daemon side.
- If
INVARIANTSwere ever removed fromfuse.h, theKKASSERTbecomes a no-op and the type confusion proceeds silently β at which point it becomes a confused-deputy vector rather than a panic.
DF-0926 β VERDICT
Verdict: REPRODUCED. Type-confusion panic in fuse_alloc_node when a
malicious FUSE daemon reuses a previously-allocated nodeid for a node of a
different type (fileβdir or dirβfile). Trigger is unprivileged (any user
with access to the mount). Impact is kernel panic / DoS (no memory
corruption primitive; the KKASSERT halts the kernel before any further
state damage).
Mechanism (every hop cited)
-
Daemon creates a VREG node at ino=100. A user performs
ls /mnt/fuse/baitfile; the kernel sendsFUSE_LOOKUP("baitfile")and the daemon replies withfuse_entry_out{nodeid=100, attr.mode=S_IFREG}. The kernel'sfuse_vop_nresolve(sys/vfs/fuse/fuse_vnops.c:571) callsfuse_alloc_node(fmp, dfnp, 100, VREG, &vp). -
fuse_alloc_node(sys/vfs/fuse/fuse_node.c:106-114) is buggy. It doesRB_LOOKUPfor the nodeid, and when found it proceeds with the existing node βvtypis silently ignored:c fnp = RB_LOOKUP(fuse_node_tree, &fmp->node_head, ino); if (fnp == NULL) { fuse_node_new(fmp, ino, vtyp, &fnp); /* vtyp used only for new nodes */ allocated = 1; } mtx_unlock(&fmp->ino_lock); error = fuse_node_vn(fnp, vpp); /* uses existing fnp->type */ -
Daemon returns the same nodeid for an
MKDIR. User runsmkdir /mnt/fuse/crashdir; the kernel sendsFUSE_MKDIR("crashdir")and the daemon replies withfuse_entry_out{nodeid=100, attr.mode=S_IFDIR}.fuse_vop_nmkdir(sys/vfs/fuse/fuse_vnops.c:915) verifiesIFTOVT(feo->attr.mode) == VDIR(passes), then callsfuse_alloc_node(fmp, dfnp, 100, VDIR, &vp)at:920. This returns the existing VREG vnode withvp->v_type = fnp->type = VREG. -
fuse_set_attrpanics.fuse_vop_nmkdirthen callsfuse_set_attr(fnp, &feo->attr)(sys/vfs/fuse/fuse_vnops.c:927). Insidefuse_set_attr:c vap->va_type = IFTOVT(fat->mode); /* fuse_vnops.c:56 -> VDIR */ ... KKASSERT(vap->va_type == fnp->type); /* fuse_vnops.c:81 -> VDIR != VREG */TheKKASSERTalways fires in production becauseINVARIANTSis unconditionally forced on atsys/vfs/fuse/fuse.h:31-33. -
Kernel halts. Captured panic signature (from
dfbsd-qemu/boot.log):panic: assertion "vap->va_type == fnp->type" failed in fuse_set_attr at /usr/src/sys/vfs/fuse/fuse_vnops.c:81 fuse_set_attr() at fuse_set_attr+0x199 fuse_vop_nmkdir() at fuse_vop_nmkdir+0x1b3 vop_nmkdir() at vop_nmkdir+0x5b kern_mkdir() at kern_mkdir+0xf2 sys_mkdir() at sys_mkdir+0x51 Stopped at Debugger+0x7c: movb $0,0xbdaf09(%rip)
Why this is a valid DoS finding
- Trigger is unprivileged. Only the daemon side requires root/operator
privilege (to open
/dev/fuseand callmount); the triggering syscall (mkdir) is performed by the unprivilegedmaxxuser (uid=1001). - Realistic precondition. A FUSE filesystem daemon is, by the DragonFly
FUSE design contract, a partially-trusted userspace peer (see comments in
fuse_vnops.cand the FUSE protocol). Any malicious or compromised daemon can mount this attack and crash the kernel whenever any user creates a directory on the mount. No race, no timing β 100% reliable. - Reachability is the default. The bug is compiled in unconditionally
via
fuse.h:31-33's forcedINVARIANTS. No custom kernel build is required.mount_fusefsandfuse.koship with the master DEV build.
Why this is DoS-only (no escalation attempted)
The primitive is a panic halt, not a memory write. The KKASSERT
expands to panic() before fuse_set_attr returns, so no caller ever
sees the type-confused vnode pointer as a usable primitive. There is no
heap/stack corruption, no UAF, no function-pointer overwrite. The bug
class is type confusion but the effect in production is purely DoS
because INVARIANTS traps it at the assertion boundary. The runner
instructions' Phase-6 escalation bar (write/UAF/double-free/type-confusion
with a write primitive) does not apply β the write-able corruption
exists only on a hypothetical INVARIANTS-OFF kernel, which is a
non-default build; on default GENERIC the bug is DoS-only. The finding
markdown itself classifies this correctly as Medium severity CVSS
AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H (DoS only).
PoC changes from initial scaffolding
- The provided
fusedemo.cusedlibfuse(the<fuse.h>userland library andfuse_main()), which is not installed on the DragonFly master DEV guest (no pkgsrc, no/usr/local/lib/libfuse*, nopkg-config). Rewrote as a standalone daemon (fusedev.c) that speaks the FUSE kernel ABI directly over/dev/fuseβ open the device, fork/execmount_fusefs fd mountpoint, then read/write the FUSE protocol structures fromfuse_abi.hinline. - Initial trigger attempt returned
EEXISTbecause the daemon'sFUSE_LOOKUPhandler also answeredcrashdir(pre-populating the namecache as a positive entry). Fixed:FUSE_LOOKUP("crashdir")now returnsENOENT; onlyFUSE_MKDIR("crashdir")returns the conflictingnodeid=100/VDIRentry. - Second attempt returned
EACCESbecause the daemon reported the root dir asmode=0755, blocking the unprivileged trigger user's write via the kernel-sidenaccess_lva(sys/kern/vfs_nlookup.c:1780) pre-check. Fixed: root inode reportsmode=S_IFDIR|0777so the unprivileged user can attempt the mkdir.
Reproduction
- Build:
cc -o fusedev fusedev.c(succeeds on DragonFly gcc 8.3). - Setup (as root):
kldload fuse; ./fusedev /mnt/fuse &(mounts the malicious daemon). - Trigger (as any user):
ls /mnt/fuse/baitfile && mkdir /mnt/fuse/crashdir. - Expected: kernel panic at
fuse_set_attr+0x199, full halt at DDB prompt. Serial console (dfbsd-qemu/boot.log) captures the trace.
Exploit chain
Not applicable (DoS-only primitive β see above). The bug is a KKASSERT
panic; there is no write primitive on the default INVARIANTS-on GENERIC
kernel to convert into uid=0.
Files in this evidence pack
fusedemo.cβ original libfuse-based daemon (kept for reference; does not build on this guest because libfuse is absent).fusedev.cβ working standalone FUSE daemon (no libfuse), the one that actually triggers the panic.trigger.shβ the unprivilegedls && mkdirsequence.build.sh/run.shβ exact reproducible build/run commands.panic.txtβ the captured panic stack trace fromdfbsd-qemu/boot.log.env.txtβ guestuname/kern.version/cc/sysctl at verification time.fix.diffβ minimal git-apply-able fix (see below).fix_build.logβ full build output of the patchedfuse.komodule.fix_run.logβ decisive post-fix PoC run (3Γ β deterministic, no panic).fix_env.txtβ patched-kernel environment (kern.version, fuse.ko sha256, patched source excerpt).manifest.jsonβ machine-readable catalog.
Fix validation (Phase 8)
The bug lives in the FUSE module (fuse.ko), not the kernel image
proper. So instead of rebuilding the entire kernel, I rebuilt only
fuse.ko from the patched source and swapped it in via kldload. This
validates the same source code change as a full kernel rebuild would β
the loader just gets the patched module through a different mechanism.
Setup:
1. Reset to with-src baseline (unpatched 6.5-DEVELOPMENT #0, source
6cc80ee9, warm obj).
2. Confirmed baseline reproduction: the panic at fuse_set_attr+0x199
fires (see panic.txt) β fix_baseline_reproduced=1.
3. Applied fix.diff to /usr/src/sys/vfs/fuse/fuse_node.c
(patch -p1 --forward, "Hunk #1 succeeded at 108").
4. Built only the patched module: cd /usr/src/sys/vfs/fuse && make
(fix_build.log, BUILD_RC=0, produced
/usr/obj/usr/src/sys/vfs/fuse/fuse.ko sha256
5a4e2a701cdeb13065cd1daafa8482a09b541f7fb278341175ea905bfdc18ee1).
5. Installed it: cp fuse.ko /boot/kernel/fuse.ko; kldload fuse.
6. Re-ran the same trigger (same daemon, same trigger.sh).
Before (unpatched baseline):
[trigger] mkdir /mnt/fuse/crashdir (daemon reuses ino=100 as VDIR -> KKASSERT panic)
<ssh session dies; guest down>
panic: assertion "vap->va_type == fnp->type" failed in fuse_set_attr
at /usr/src/sys/vfs/fuse/fuse_vnops.c:81
fuse_set_attr() at fuse_set_attr+0x199
fuse_vop_nmkdir() at fuse_vop_nmkdir+0x1b3
After (patched fuse.ko, 3 deterministic runs):
[trigger] mkdir /mnt/fuse/crashdir (daemon reuses ino=100 as VDIR -> KKASSERT panic) mkdir: /mnt/fuse/crashdir: Invalid argument [trigger] mkdir rc=1 [trigger] if you read this, the kernel did not panic
The patched kernel returns EINVAL (the value the fix returns from
fuse_alloc_node when the daemon's claimed type conflicts with the
existing node's type) β exactly the contract the finding markdown's
recommended fix describes ("The caller already handles EINVAL from
fuse_alloc_node"). The guest stays up; the panic is gone.
fix_patched_reproduced=0, fix_status=fixed.
Verdict: the fix closes the bug. It matches the finding markdown's recommended fix exactly.
Fix verification
fixedVALIDATED the fix: the same trigger ('ls /mnt/fuse/baitfile; mkdir /mnt/fuse/crashdir' as maxx) on the unpatched 6.5-DEVELOPMENT #0 baseline reproducibly panics with 'assertion vap->va_type == fnp->type failed in fuse_set_attr at fuse_vnops.c:81' (panic.txt). After applying fix.diff to sys/vfs/fuse/fuse_node.c:108-111 (reject nodeid-reuse with conflicting type via EINVAL) and rebuilding+installing the patched fuse.ko module, the same trigger returns 'mkdir: /mnt/fuse/crashdir: Invalid argument' (EINVAL) and the guest stays up -- confirmed deterministic across 3 consecutive runs (fix_run.log). The fix closes the bug; it matches the finding markdown's recommended fix.
BEFORE (unpatched #0 baseline): [trigger] mkdir /mnt/fuse/crashdir ... <ssh dies, guest down> panic: assertion "vap->va_type == fnp->type" failed in fuse_set_attr at fuse_vnops.c:81 fuse_set_attr() at fuse_set_attr+0x199 fuse_vop_nmkdir() at fuse_vop_nmkdir+0x1b3 AFTER (fuse_node.c patched, fuse.ko rebuilt & loaded, 3 deterministic runs): [trigger] mkdir /mnt/fuse/crashdir ... mkdir: /mnt/fuse/crashdir: Invalid argument [trigger] mkdir rc=1 [trigger] if you read this, the kernel did not panic Guest stayed up after all 3 runs.
Confirmed kernel references
- sys/vfs/fuse/fuse_node.c:106
- sys/vfs/fuse/fuse_node.c:107
- sys/vfs/fuse/fuse_node.c:108
- sys/vfs/fuse/fuse_node.c:111
- sys/vfs/fuse/fuse_node.c:114
- sys/vfs/fuse/fuse_vnops.c:56
- sys/vfs/fuse/fuse_vnops.c:81
- sys/vfs/fuse/fuse_vnops.c:915
- sys/vfs/fuse/fuse_vnops.c:920
- sys/vfs/fuse/fuse_vnops.c:927
- sys/vfs/fuse/fuse.h:31
- sys/vfs/fuse/fuse.h:32
- sys/vfs/fuse/fuse.h:33
Detail
Exploit chain
Not applicable (DoS-only primitive, no escalation attempted). The bug is a KKASSERT panic -- the assertion halts the kernel via panic() BEFORE fuse_set_attr returns, so no caller ever receives a usable type-confused vnode pointer. There is no heap/stack write, no UAF, no function-pointer overwrite, no attacker-controlled corruption primitive on the default INVARIANTS-on GENERIC kernel. Phase 6 escalation bar (write/UAF/double-free with a write primitive) does not apply; impact ceiling is DoS.
Evidence (decisive lines)
Decisive panic signature from dfbsd-qemu/boot.log (unpatched baseline): panic: assertion "vap->va_type == fnp->type" failed in fuse_set_attr at /usr/src/sys/vfs/fuse/fuse_vnops.c:81 fuse_set_attr() at fuse_set_attr+0x199 fuse_vop_nmkdir() at fuse_vop_nmkdir+0x1b3 vop_nmkdir() at vop_nmkdir+0x5b kern_mkdir() at kern_mkdir+0xf2 sys_mkdir() at sys_mkdir+0x51 Stopped at Debugger+0x7c: movb $0,0xbdaf09(%rip) Trigger was 'mkdir /mnt/fuse/crashdir' as maxx (uid=1001); ssh died with timeout=124 and guest entered DDB.
PoC changes
Rewrote the libfuse-based fusedemo.c as a standalone fusedev.c daemon that speaks the FUSE kernel ABI directly over /dev/fuse (libfuse is not installed on the master DEV guest). Two trigger-path fixes during iteration: (1) FUSE_LOOKUP handler initially also answered 'crashdir' which pre-cached it as a positive entry -> mkdir returned EEXIST; fixed by returning ENOENT for 'crashdir'. (2) Initial trigger from maxx got EACCES because the daemon reported the root inode as mode 0755; fixed by reporting root inode mode 0777 so the trigger user can attempt mkdir. Added build.sh, run.sh, trigger.sh, VERDICT.md, manifest.json, fix.diff, fix_build.log, fix_run.log, fix_env.txt, run.log, panic.txt, env.txt to the evidence pack.
Verified recommended fix
Add an else if (fnp->type != vtyp) { mtx_unlock(&fmp->ino_lock); return EINVAL; } clause to fuse_alloc_node at sys/vfs/fuse/fuse_node.c:108-111 so a daemon that returns an existing nodeid with a conflicting type is rejected (EINVAL) before reaching fuse_set_attr which would KKASSERT-panic the kernel at fuse_vnops.c:81. The callers (ncreate/nmknod/nmkdir/nsymlink at fuse_vnops.c:717/791/920/1289) already propagate EINVAL cleanly. This matches the finding markdown's proposed fix exactly. The full git-apply-able diff lives in findings/poc/DF-0926/fix.diff.
Verdict
REPRODUCED. Type-confusion KKASSERT panic in fuse_alloc_node confirmed on default GENERIC 6.5-DEVELOPMENT #0 kernel. The malicious FUSE daemon (root/operator setup; the daemon is the attacker per the finding's threat model) returns nodeid=100 first as VREG (FUSE_LOOKUP for 'baitfile'), then as VDIR (FUSE_MKDIR for 'crashdir'). fuse_alloc_node at sys/vfs/fuse/fuse_node.c:106-114 RB_LOOKUP's the existing node and silently ignores the vtyp argument, returning the VREG vnode. fuse_vop_nmkdir at fuse_vnops.c:920 then calls fuse_set_attr (fuse_vnops.c:927) which executes vap->va_type = IFTOVT(fat->mode) = VDIR (fuse_vnops.c:56) and KKASSERT(vap->va_type == fnp->type) at fuse_vnops.c:81 fires (VDIR != VREG). INVARIANTS is unconditionally forced on by sys/vfs/fuse/fuse.h:31-33 so this always panics in production. Captured panic: 'panic: assertion "vap->va_type == fnp->type" failed in fuse_set_attr at fuse_vnops.c:81' with stack fuse_set_attr+0x199 <- fuse_vop_nmkdir+0x1b3 <- vop_nmkdir+0x5b <- kern_mkdir+0xf2 <- sys_mkdir+0x51. Trigger is the unprivileged user (maxx uid=1001) issuing 'mkdir /mnt/fuse/crashdir' on the malicious mount. 100% reliable, no race.
No comments yet.