Loading learning content...
Traditionally, implementing a file system required kernel programming—a domain of C code, kernel APIs, and the risk of crashing the entire system with a single bug. File system development was limited to kernel experts, and prototyping new ideas meant long compile-debug-reboot cycles.
FUSE changed everything. FUSE—Filesystem in Userspace—provides a bridge between the kernel's VFS layer and ordinary userspace programs. Your file system can be written in Python, Go, Rust, JavaScript, or any language. A bug crashes only your FUSE process, not the kernel. Development happens in minutes rather than hours.
The result is an explosion of creative file systems:
FUSE democratized file system development, enabling solutions that would never have been written as kernel modules.
By the end of this page, you will understand FUSE architecture from kernel module to libfuse to user implementation. You'll learn the performance tradeoffs, security considerations, and how to evaluate when FUSE is appropriate versus kernel file systems.
FUSE consists of three main components that work together to route file system operations from kernel to userspace and back:
fuse.ko) — Intercepts VFS operations and routes them to userspaceThe request flow:
Key architectural points:
/dev/fuse; libfuse may use multiple threads1234567891011121314151617181920
# Check if FUSE kernel module is loaded$ lsmod | grep fusefuse 147456 5 # The /dev/fuse device node$ ls -la /dev/fusecrw-rw-rw- 1 root root 10, 229 Jan 16 10:00 /dev/fuse # View mounted FUSE filesystems$ mount | grep fusefusectl on /sys/fs/fuse/connections type fusectl (rw,nosuid,nodev,noexec,relatime)sshfs#user@host:/path on /mnt/remote type fuse.sshfs (rw,nosuid,nodev,...) # Each FUSE mount has a connection entry$ ls /sys/fs/fuse/connections/42 43 45 # Connection details$ cat /sys/fs/fuse/connections/42/waiting0 # Number of requests waiting for userspaceFUSE filesystems can be mounted by unprivileged users using fusermount (a setuid helper) or via mount.fuse. This is a key feature—no root access required to mount your network drives or encrypted folders.
Communication between the kernel and userspace happens through a well-defined protocol over /dev/fuse. Understanding this protocol reveals how FUSE operations map to your file system callbacks.
| Operation | VFS Trigger | Description | Must Implement? |
|---|---|---|---|
FUSE_LOOKUP | path_lookup | Resolve path component to inode | Yes |
FUSE_GETATTR | stat, fstat | Get file/directory attributes | Yes |
FUSE_READDIR | readdir | List directory contents | Yes (for directories) |
FUSE_OPEN | open | Open file, return handle | Yes |
FUSE_READ | read | Read file content | Yes |
FUSE_WRITE | write | Write file content | If writable |
FUSE_CREATE | creat, open(O_CREAT) | Create and open file | If writable |
FUSE_MKDIR | mkdir | Create directory | If writable |
FUSE_UNLINK | unlink | Delete file | If writable |
FUSE_RELEASE | close | Close file handle | Usually |
FUSE_FLUSH | close, fsync | Flush buffers before close | Optional |
FUSE_SETATTR | chmod, chown, utimes | Modify attributes | If writable |
Protocol structure:
Each request contains a header followed by operation-specific data:
struct fuse_in_header {
uint32_t len; // Total message length
uint32_t opcode; // FUSE_READ, FUSE_WRITE, etc.
uint64_t unique; // Request ID for matching replies
uint64_t nodeid; // Inode number
uint32_t uid; // Caller's UID
uint32_t gid; // Caller's GID
uint32_t pid; // Caller's PID
uint32_t padding;
};
Replies include a header and operation-specific results:
struct fuse_out_header {
uint32_t len; // Total message length
int32_t error; // 0 for success, negative errno for error
uint64_t unique; // Matches request's unique ID
};
libfuse provides two APIs: the high-level API operates on paths (easier, recommended for most users) while the low-level API operates on inodes with explicit reply functions (more control, required for advanced features). Most FUSE filesystems use the high-level API.
Let's walk through implementing a simple FUSE filesystem. This example creates a read-only filesystem with a single file—enough to demonstrate the core concepts.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
/* * simplefs.c - A minimal FUSE filesystem * Compile: gcc -Wall simplefs.c `pkg-config fuse3 --cflags --libs` -o simplefs * Usage: ./simplefs /mnt/point */ #define FUSE_USE_VERSION 31 #include <fuse.h>#include <string.h>#include <errno.h> /* Our single file's content */static const char *hello_str = "Hello from FUSE!\n";static const char *hello_path = "/hello.txt"; /* * getattr - Return file/directory attributes * Called for stat(), lstat(), etc. */static int simple_getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi){ memset(stbuf, 0, sizeof(struct stat)); if (strcmp(path, "/") == 0) { /* Root directory */ stbuf->st_mode = S_IFDIR | 0755; stbuf->st_nlink = 2; return 0; } else if (strcmp(path, hello_path) == 0) { /* Our file */ stbuf->st_mode = S_IFREG | 0444; stbuf->st_nlink = 1; stbuf->st_size = strlen(hello_str); return 0; } return -ENOENT; /* Path not found */} /* * readdir - List directory contents * Called for ls, etc. */static int simple_readdir(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags){ if (strcmp(path, "/") != 0) return -ENOENT; /* Standard directory entries */ filler(buf, ".", NULL, 0, 0); filler(buf, "..", NULL, 0, 0); /* Our file (without leading /) */ filler(buf, "hello.txt", NULL, 0, 0); return 0;} /* * open - Open a file * Validate access permissions */static int simple_open(const char *path, struct fuse_file_info *fi){ if (strcmp(path, hello_path) != 0) return -ENOENT; /* Read-only filesystem */ if ((fi->flags & O_ACCMODE) != O_RDONLY) return -EACCES; return 0;} /* * read - Read file content */static int simple_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi){ size_t len; if (strcmp(path, hello_path) != 0) return -ENOENT; len = strlen(hello_str); if (offset >= len) return 0; /* EOF */ if (offset + size > len) size = len - offset; memcpy(buf, hello_str + offset, size); return size;} /* Operations table - which functions we implement */static const struct fuse_operations simple_ops = { .getattr = simple_getattr, .readdir = simple_readdir, .open = simple_open, .read = simple_read,}; int main(int argc, char *argv[]){ return fuse_main(argc, argv, &simple_ops, NULL);}12345678910111213141516171819202122232425
# Build the filesystem$ gcc -Wall simplefs.c `pkg-config fuse3 --cflags --libs` -o simplefs # Mount it$ mkdir /tmp/mnt$ ./simplefs /tmp/mnt # Use it like any filesystem!$ ls /tmp/mnthello.txt $ cat /tmp/mnt/hello.txtHello from FUSE! $ ls -la /tmp/mnt/total 0drwxr-xr-x 2 user user 0 Jan 1 1970 .drwxrwxrwt 5 root root 1024 Jan 16 10:00 ..-r--r--r-- 1 user user 17 Jan 1 1970 hello.txt # Unmount$ fusermount -u /tmp/mnt # Debug mode (foreground, verbose)$ ./simplefs -f -d /tmp/mntPython example with fusepy:
FUSE filesystems don't have to be in C. Here's the equivalent in Python:
123456789101112131415161718192021222324252627282930313233343536373839404142
#!/usr/bin/env python3"""simple_fuse.py - A minimal FUSE filesystem in PythonInstall: pip install fusepyUsage: python simple_fuse.py /mnt/point""" from fuse import FUSE, FuseOSError, Operationsfrom errno import ENOENTfrom stat import S_IFDIR, S_IFREGimport os class SimpleFS(Operations): def __init__(self): self.content = b"Hello from FUSE!\n" def getattr(self, path, fh=None): if path == '/': return {'st_mode': S_IFDIR | 0o755, 'st_nlink': 2} elif path == '/hello.txt': return { 'st_mode': S_IFREG | 0o444, 'st_nlink': 1, 'st_size': len(self.content) } else: raise FuseOSError(ENOENT) def readdir(self, path, fh): return ['.', '..', 'hello.txt'] def open(self, path, flags): if path != '/hello.txt': raise FuseOSError(ENOENT) return 0 def read(self, path, size, offset, fh): return self.content[offset:offset + size] if __name__ == '__main__': import sys FUSE(SimpleFS(), sys.argv[1], foreground=True)FUSE has enabled hundreds of creative file system implementations. Here are the most notable categories and examples:
| Filesystem | Description | Use Case |
|---|---|---|
| sshfs | Mount remote directories over SSH | Access remote files as local; no special server setup |
| s3fs-fuse | Mount Amazon S3 buckets | Access S3 as local filesystem; suitable for read-heavy workloads |
| rclone mount | Mount 40+ cloud providers | Google Drive, Dropbox, OneDrive, etc. as local folders |
| davfs2 | Mount WebDAV shares | NextCloud, ownCloud, Box.com access |
| gcsfs | Mount Google Cloud Storage | GCS as local filesystem |
# Mount remote directory over SSH$ sshfs user@host:/remote/path /local/mount # With options for better performance$ sshfs user@host:/path /mnt \ -o cache=yes \ -o kernel_cache \ -o compression=yes \ -o ServerAliveInterval=15 # Unmount$ fusermount -u /local/mountFUSE's flexibility comes at a performance cost. Understanding these tradeoffs helps you decide when FUSE is appropriate and how to optimize FUSE-based solutions.
-o max_threads=N for parallel requestskernel_cache, auto_cache for reads-o writeback_cache for write aggregation-o max_read=N for fewer, larger requests12345678910111213141516171819202122232425262728293031323334353637383940
# ═══════════════════════════════════════════════════════════════# Performance-optimized FUSE mount options# ═══════════════════════════════════════════════════════════════ # For read-heavy workloads (sshfs, s3fs)$ sshfs user@host:/path /mnt \ -o kernel_cache \ # Cache file data in kernel -o auto_cache \ # Invalidate cache intelligently -o cache=yes \ # Enable all caching -o cache_timeout=300 \ # 5 minute attribute cache -o max_readahead=131072 \ # 128K readahead -o compression=yes # Reduce network transfer # For write-heavy workloads$ fuse_fs /mnt \ -o writeback_cache \ # Aggregate writes -o max_write=131072 \ # 128K writes -o max_background=32 # More background writeback # Multithreaded for concurrent access$ fuse_fs /mnt -o max_threads=16 # ═══════════════════════════════════════════════════════════════# Benchmarking FUSE overhead# ═══════════════════════════════════════════════════════════════ # Baseline: native filesystem$ fio --name=test --filename=/native/testfile --size=1G --rw=randread --bs=4k # FUSE filesystem (same underlying storage)$ fio --name=test --filename=/fuse_mount/testfile --size=1G --rw=randread --bs=4k # Typical results:# Native: 200k IOPS, 50µs latency# FUSE: 50k IOPS, 200µs latency# Overhead: ~4x for small random operations # For sequential large I/O, overhead is much smaller:# Native: 3.5 GB/s# FUSE: 3.0 GB/s (14% overhead)| Factor | Prefer FUSE | Prefer Kernel FS |
|---|---|---|
| Development speed | Rapid prototyping, iterating | Mature, stable implementation |
| Safety | Bugs crash only daemon | Must not crash kernel |
| Performance needs | Network I/O dominates | Low-latency local storage |
| Complexity | Simple logic, any language | Complex, must be C/Rust |
| Deployment | No kernel modules needed | Needs kernel/module updates |
| Use case | Cloud storage, encryption overlays | Local block devices, high IOPS |
For network-based FUSE filesystems (sshfs, s3fs, rclone), the network latency typically dwarfs FUSE overhead. A 10ms network round-trip makes FUSE's 100µs overhead negligible. FUSE is often the perfect solution for these cases.
FUSE introduces unique security considerations since it allows unprivileged users to provide filesystem operations that affect how files appear to the system.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
# ═══════════════════════════════════════════════════════════════# Default behavior: only mounting user can access# ═══════════════════════════════════════════════════════════════ $ sshfs user@host:/path /mnt/remote # Mount as user$ ls /mnt/remote # Works$ sudo ls /mnt/remote # Permission denied! # ═══════════════════════════════════════════════════════════════# Enabling access for other users# ═══════════════════════════════════════════════════════════════ # 1. Edit /etc/fuse.conf (as root)$ cat /etc/fuse.confuser_allow_other # Uncomment this line # 2. Mount with allow_other$ sshfs -o allow_other user@host:/path /mnt/remote$ sudo ls /mnt/remote # Now works! # ═══════════════════════════════════════════════════════════════# Security restrictions# ═══════════════════════════════════════════════════════════════ # FUSE mounts are nosuid by default$ mount | grep fusesshfs#user@host:/path on /mnt/remote type fuse.sshfs (rw,nosuid,nodev,...) # Even if filesystem returns setuid bit, it's ignored$ ls -la /mnt/remote/setuid_binary-rwsr-xr-x 1 root root 12345 ... setuid_binary# The 's' is displayed, but execution won't gain privileges # ═══════════════════════════════════════════════════════════════# Preventing malicious FUSE filesystems# ═══════════════════════════════════════════════════════════════ # A malicious FUSE FS could:# - Return different content each read (confuse security tools)# - Lie about file permissions/ownership# - Hang on read (denial of service)# - Return very slow (resource exhaustion) # Mitigations:# - nosuid/nodev defaults# - User isolation (no allow_other by default)# - Timeout handling in VFS# - Never run untrusted FUSE daemonsEnabling allow_other means root (and other users) can access files through your FUSE daemon. If your daemon has bugs or returns incorrect permissions, this could expose data or enable attacks. Only use allow_other when necessary, and only with trusted FUSE implementations.
Modern FUSE (especially FUSE 3.x) includes advanced features for improved performance and functionality:
| Feature | Description | Benefit |
|---|---|---|
| Writeback cache | Kernel aggregates writes before sending | Dramatically better write performance |
| Readdirplus | Readdir returns stat info too | Fewer round-trips for ls -la |
| Parallel readdir | Multiple readdir requests concurrently | Faster large directory listing |
| Splice support | Zero-copy data transfer | Reduced CPU usage for large transfers |
| FOPEN_KEEP_CACHE | Preserve cache across opens | Better cache utilization |
| Passthrough I/O | Direct I/O to underlying file (experimental) | Near-native performance for overlays |
| Notifications | Kernel can notify daemon of invalidation | Cache coherence with external changes |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
/* Enabling advanced features in FUSE daemon */ /* In init() callback, specify capabilities */static void *my_init(struct fuse_conn_info *conn, struct fuse_config *cfg){ /* Enable writeback caching */ if (conn->capable & FUSE_CAP_WRITEBACK_CACHE) conn->want |= FUSE_CAP_WRITEBACK_CACHE; /* Enable readdirplus */ if (conn->capable & FUSE_CAP_READDIRPLUS) conn->want |= FUSE_CAP_READDIRPLUS; /* Enable parallel readdir */ if (conn->capable & FUSE_CAP_PARALLEL_DIROPS) conn->want |= FUSE_CAP_PARALLEL_DIROPS; /* Keep page cache across opens */ cfg->kernel_cache = 1; cfg->auto_cache = 1; /* Larger read/write buffers */ conn->max_read = 131072; /* 128KB */ conn->max_write = 131072; return NULL;} /* Readdirplus: return stat info with each entry */static int my_readdirplus(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags){ struct stat stbuf; /* Fill stat info for each entry */ memset(&stbuf, 0, sizeof(stbuf)); stbuf.st_mode = S_IFREG | 0644; stbuf.st_size = 1234; stbuf.st_mtime = time(NULL); /* Pass stat info to filler - kernel won't need extra getattr calls */ filler(buf, "filename.txt", &stbuf, 0, FUSE_FILL_DIR_PLUS); return 0;}virtiofs uses the FUSE protocol over a virtio transport to share host directories with VMs. This provides near-native performance because the FUSE operations go directly to the host without network serialization. QEMU/KVM and libvirt support virtiofs for fast shared filesystem access.
Debugging FUSE filesystems is much easier than kernel filesystems—you can use standard debugging tools, add print statements, and attach debuggers without rebooting.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
# ═══════════════════════════════════════════════════════════════# Debug mode: foreground with verbose output# ═══════════════════════════════════════════════════════════════ $ ./my_fuse_fs -f -d /mnt/point# -f = foreground (don't daemonize)# -d = debug (print every FUSE operation) # Output shows every operation:unique: 1, opcode: LOOKUP (1), nodeid: 1, insize: 48, pid: 12345LOOKUP /hello.txt getattr /hello.txt unique: 1, success, outsize: 144 unique: 2, opcode: OPEN (14), nodeid: 2, insize: 48, pid: 12345OPEN /hello.txt unique: 2, success, outsize: 32 # ═══════════════════════════════════════════════════════════════# Attach gdb to running FUSE process# ═══════════════════════════════════════════════════════════════ $ ps aux | grep my_fuse_fsuser 12345 0.0 0.1 12345 6789 ? Sl 10:00 0:00 ./my_fuse_fs /mnt $ sudo gdb -p 12345 # ═══════════════════════════════════════════════════════════════# Trace FUSE operations with strace# ═══════════════════════════════════════════════════════════════ # Watch the application side$ strace cat /mnt/fuse_fs/file.txtopen("/mnt/fuse_fs/file.txt", O_RDONLY) = 3read(3, "Hello from FUSE!\n", 4096) = 17... # Watch the FUSE daemon side$ strace -p $(pgrep my_fuse_fs)read(3, "\x30\x00\x00\x00\x01\x00\x00\x00...", 135168) = 48 # Read requestwrite(3, "\x90\x00\x00\x00\x00\x00\x00\x00...", 144) = 144 # Send reply # ═══════════════════════════════════════════════════════════════# Check FUSE connection status# ═══════════════════════════════════════════════════════════════ $ cat /sys/fs/fuse/connections/*/waiting0 # Requests waiting for daemon $ cat /sys/fs/fuse/connections/*/congestion_threshold0 # Congestion control # If 'waiting' is high, daemon is too slow # ═══════════════════════════════════════════════════════════════# Force unmount stuck FUSE filesystem# ═══════════════════════════════════════════════════════════════ # Normal unmount$ fusermount -u /mnt/point # Force unmount (if daemon is hung)$ fusermount -zu /mnt/point # Lazy unmount # Kill daemon directly$ pkill -9 my_fuse_fs$ fusermount -u /mnt/pointFUSE democratized filesystem development, enabling solutions from encrypted overlays to cloud storage mounts that would never have been practical as kernel modules.
Module complete:
This concludes our exploration of special file systems. You've learned about:
These special file systems form the infrastructure that makes modern Linux work—from system monitoring to device management to application-specific virtual file systems.
You now have comprehensive knowledge of special file systems in Linux. This understanding is essential for system administration, performance optimization, security hardening, and building applications that leverage these powerful abstractions.