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

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 call fuse_set_attr after fuse_alloc_node, so the nresolve path silently returns a vnode of the wrong type without panicking β€” a silent type confusion that could cause incorrect VOP behavior if INVARIANTS were 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/symlink operation. The daemon simply returns an existing nodeid (e.g., the nodeid of a previously created regular file) in its CREATE response with mode=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 (or touch, 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.

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

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-0926 Β· 17 files
FileTypeDescriptionSize
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
README.md readme initial PoC README from finding scaffolding
↓ download 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 INVARIANTS were ever removed from fuse.h, the KKASSERT becomes a no-op and the type confusion proceeds silently β€” at which point it becomes a confused-deputy vector rather than a panic.
VERDICT.md verdict full narrative analysis
↓ download raw

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)

  1. Daemon creates a VREG node at ino=100. A user performs ls /mnt/fuse/baitfile; the kernel sends FUSE_LOOKUP("baitfile") and the daemon replies with fuse_entry_out{nodeid=100, attr.mode=S_IFREG}. The kernel's fuse_vop_nresolve (sys/vfs/fuse/fuse_vnops.c:571) calls fuse_alloc_node(fmp, dfnp, 100, VREG, &vp).

  2. fuse_alloc_node (sys/vfs/fuse/fuse_node.c:106-114) is buggy. It does RB_LOOKUP for the nodeid, and when found it proceeds with the existing node β€” vtyp is 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 */

  3. Daemon returns the same nodeid for an MKDIR. User runs mkdir /mnt/fuse/crashdir; the kernel sends FUSE_MKDIR("crashdir") and the daemon replies with fuse_entry_out{nodeid=100, attr.mode=S_IFDIR}. fuse_vop_nmkdir (sys/vfs/fuse/fuse_vnops.c:915) verifies IFTOVT(feo->attr.mode) == VDIR (passes), then calls fuse_alloc_node(fmp, dfnp, 100, VDIR, &vp) at :920. This returns the existing VREG vnode with vp->v_type = fnp->type = VREG.

  4. fuse_set_attr panics. fuse_vop_nmkdir then calls fuse_set_attr(fnp, &feo->attr) (sys/vfs/fuse/fuse_vnops.c:927). Inside fuse_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 */ The KKASSERT always fires in production because INVARIANTS is unconditionally forced on at sys/vfs/fuse/fuse.h:31-33.

  5. 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/fuse and call mount); the triggering syscall (mkdir) is performed by the unprivileged maxx user (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.c and 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 forced INVARIANTS. No custom kernel build is required. mount_fusefs and fuse.ko ship 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.c used libfuse (the <fuse.h> userland library and fuse_main()), which is not installed on the DragonFly master DEV guest (no pkgsrc, no /usr/local/lib/libfuse*, no pkg-config). Rewrote as a standalone daemon (fusedev.c) that speaks the FUSE kernel ABI directly over /dev/fuse β€” open the device, fork/exec mount_fusefs fd mountpoint, then read/write the FUSE protocol structures from fuse_abi.h inline.
  • Initial trigger attempt returned EEXIST because the daemon's FUSE_LOOKUP handler also answered crashdir (pre-populating the namecache as a positive entry). Fixed: FUSE_LOOKUP("crashdir") now returns ENOENT; only FUSE_MKDIR("crashdir") returns the conflicting nodeid=100/VDIR entry.
  • Second attempt returned EACCES because the daemon reported the root dir as mode=0755, blocking the unprivileged trigger user's write via the kernel-side naccess_lva (sys/kern/vfs_nlookup.c:1780) pre-check. Fixed: root inode reports mode=S_IFDIR|0777 so 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 unprivileged ls && mkdir sequence.
  • build.sh / run.sh β€” exact reproducible build/run commands.
  • panic.txt β€” the captured panic stack trace from dfbsd-qemu/boot.log.
  • env.txt β€” guest uname/kern.version/cc/sysctl at verification time.
  • fix.diff β€” minimal git-apply-able fix (see below).
  • fix_build.log β€” full build output of the patched fuse.ko module.
  • 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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED 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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 (base kernel unchanged; fix validated via patched fuse.ko module swap, sha256 5a4e2a701cdeb13065cd1daafa8482a09b541f7fb278341175ea905bfdc18ee1, since the bug lives in the FUSE module not the kernel image)

Confirmed kernel references

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.