Skip to main content

FUSE: Filesystem in Userspace Explained

Summary
Learn FUSE (Filesystem in Userspace) for building custom filesystems. Understand how NTFS-3G, SSHFS, and cloud storage work.

What is FUSE?

FUSE (Filesystem in Userspace) is a Linux kernel interface that lets a userspace program implement filesystem operations. The kernel exposes a special device, /dev/fuse. A userspace daemon reads filesystem requests from this device, executes them, and writes responses back. From a calling application's perspective, the mount behaves like any other filesystem.

This boundary moves the implementation out of the kernel: filesystem code runs as an ordinary process, in any language with FUSE bindings (C, Python, Go, Rust, and more). The trade-off is performance—every operation makes extra trips across the kernel/userspace boundary.

Why Userspace?

Writing a kernel filesystem is expensive in time and risk. A bug can panic the system; iteration requires a reboot or module reload; only C is practical; and only root can load the module.

A FUSE filesystem inverts these constraints. The implementation is a normal process:

  • A crash terminates the daemon, not the kernel. The mount goes stale and can be unmounted and restarted.
  • Iteration is a process restart, not a reboot.
  • Any language that can read and write the FUSE protocol over /dev/fuse works. Bindings exist for C, Python, Go, Rust, and others.
  • A non-root user can mount a FUSE filesystem without sudo because fusermount is setuid-root.

The cost is performance, which the rest of this page measures.

The Kernel/Userspace Boundary

This is THE key insight to understanding FUSE. Every Unix system has two worlds: kernel space (privileged, dangerous) and userspace (safe, restricted). FUSE bridges them.

A FUSE filesystem lives entirely on the userspace side of this boundary. The kernel-side participation is bounded to the FUSE module and /dev/fuse.

Request Flow

When an application reads a file on a FUSE mount, the request passes through the VFS layer, into the FUSE kernel module, out to the userspace daemon, and back. The diagram below walks through each step.

Each step of the path is:

  1. The application makes a syscall (open, read, write).
  2. The VFS layer receives the call and routes it.
  3. The FUSE kernel module intercepts requests for FUSE mounts and queues them on /dev/fuse.
  4. libfuse, running in userspace, reads from the queue.
  5. The user-written filesystem handler executes.
  6. The response travels back through the same path.

Notice the four kernel/userspace crossings—twice the round-trip cost of a native filesystem call.

# The key components: # 1. FUSE kernel module - handles kernel-side communication lsmod | grep fuse # 2. /dev/fuse - the bridge between kernel and userspace ls -l /dev/fuse # 3. Your FUSE daemon - runs in userspace, handles requests ps aux | grep myfs

The Performance Cost

The boundary crossings have a measurable cost. The demo below shows how syscall latency changes as the userspace handler's own work scales.

A native filesystem call crosses the kernel/userspace boundary twice (in and out). A FUSE call crosses four times:

  1. App → Kernel (syscall)
  2. Kernel → FUSE daemon (via /dev/fuse)
  3. FUSE daemon → Kernel (response)
  4. Kernel → App (return)

Compare to native filesystems: only two crossings (syscall in, return out).

FUSE in Practice

Several widely-used filesystems are built on FUSE. The gallery below shows the categories and representative implementations.

Implementing a FUSE Filesystem

A minimal FUSE filesystem needs to answer a handful of operations. The checklist below covers the required and commonly-overridden ones.

A Minimal Python Example

The following is a complete read-only FUSE filesystem that exposes a single file, /hello.txt.

#!/usr/bin/env python3 from fuse import FUSE, Operations import stat import errno class HelloFS(Operations): def getattr(self, path, fh=None): if path == '/': return {'st_mode': stat.S_IFDIR | 0o755, 'st_nlink': 2} if path == '/hello.txt': content = b'Hello from FUSE!\n' return { 'st_mode': stat.S_IFREG | 0o444, 'st_nlink': 1, 'st_size': len(content) } raise OSError(errno.ENOENT) def readdir(self, path, fh): return ['.', '..', 'hello.txt'] def open(self, path, flags): return 0 def read(self, path, length, offset, fh): if path == '/hello.txt': content = b'Hello from FUSE!\n' return content[offset:offset + length] raise OSError(errno.ENOENT) if __name__ == '__main__': import sys FUSE(HelloFS(), sys.argv[1], foreground=True)

Mount it:

pip install fusepy mkdir /tmp/hello python hello_fs.py /tmp/hello

Use it from another terminal:

ls /tmp/hello # hello.txt cat /tmp/hello/hello.txt # Hello from FUSE! fusermount -u /tmp/hello # unmount when done

That's the entire filesystem. Around thirty lines of Python is enough to expose a file in a mountable view.

Performance Optimization

Kernel Caching

FUSE can cache data in the kernel to reduce round-trips:

# Enable kernel page cache (default) ./myfs /mountpoint -o kernel_cache # Enable attribute caching ./myfs /mountpoint -o attr_timeout=60 # Disable caching for always-fresh data ./myfs /mountpoint -o direct_io

Batch Operations

For better throughput:

# Increase max read/write size ./myfs /mountpoint -o max_read=131072 -o max_write=131072 # Enable multi-threading ./myfs /mountpoint -o max_threads=16

Common Mount Options

# Allow other users to access the mount mount -o allow_other /dev/fuse /mountpoint # Set ownership mount -o uid=1000,gid=1000 ... # Read-only mount mount -o ro ... # Debug mode (foreground with verbose output) ./myfs -f -d /mountpoint

Debugging FUSE Filesystems

Verbose Logging

import logging logging.basicConfig(level=logging.DEBUG) def read(self, path, length, offset, fh): logging.debug(f"READ: {path}, len={length}, off={offset}") # ... implementation

Strace the FUSE Process

# Trace all system calls strace -p $(pidof myfs) -f # Just file operations strace -e open,read,write,close -p $(pidof myfs)

Common Issues

"Transport endpoint is not connected"

# FUSE daemon crashed - unmount and restart fusermount -u /mountpoint ./myfs /mountpoint

"Permission denied"

# Check /etc/fuse.conf has: user_allow_other # And mount with: -o allow_other

When to Use FUSE (and When Not To)

Use FUSE For
  • Prototyping new filesystem ideas
  • Network and cloud storage (SSHFS, rclone)
  • Encryption layers (EncFS, gocryptfs)
  • Format conversion (NTFS-3G)
  • Archive mounting (archivemount)
  • Database-as-filesystem experiments
Avoid FUSE For
  • Boot and root filesystems
  • High-performance databases
  • Real-time applications
  • Heavy concurrent workloads
  • Systems requiring kernel-level reliability

The Future of FUSE

FUSE 3.x

The latest FUSE version brings:

  • Better performance through improved caching
  • Enhanced security model
  • Splice support for zero-copy I/O

virtiofs

For virtual machines, virtiofs combines FUSE with virtio for near-native performance:

# Inside a VM, mount host directory mount -t virtiofs myfs /mnt/host

io_uring Integration

Future versions may use io_uring for:

  • Reduced context switches via batching
  • Better async I/O performance

FUSE made filesystem development practical outside the kernel. Before FUSE, a new filesystem meant kernel hacking; with FUSE, a working prototype is an afternoon's work. SSHFS, NTFS-3G, rclone, and many archive- and encryption-mounting tools all rely on this interface.

Further Reading

If you found this explanation helpful, consider sharing it with others.

Mastodon