Building a Linux filesystem module in almost pure Rust

pierrebarre1 pts0 comments

Building a Linux filesystem module in (almost) pure Rust — ZeroFS

← Back to blog<br>Building a Linux filesystem module in (almost) pure Rust

July 31, 2026·<br>Pierre Barre·<br>6 min read

ZeroFS now has a native kernel client: an out-of-tree Linux filesystem module written in Rust that is a port of the userspace client, with a small amount of C for the netfslib helpers the Rust bindings cannot reach yet. It registers zerofs with VFS, moves file data through netfslib, and speaks the private 9P2000.L.Z dialect itself over TCP or a Unix socket.

From userspace to the kernel

With zerofs mount, every operation the page cache cannot answer leaves the kernel through /dev/fuse, gets turned into a 9P2000.L.Z request by the bundled userspace client, and re-enters the kernel as a socket write to the server. The reply makes the same trip in reverse. That detour compounds, because a strongly consistent network filesystem is chatty: writing one file is a walk to the file, an open, a stat, the writes, perhaps an fsync, then a close, and most of those cannot be answered from cache without weakening consistency. Each one pays the round trip through userspace. The client is also an ordinary process, and the mount fails if it dies.

Linux v9fs removes the userspace hop but speaks standard 9P2000.L. It cannot use ZeroFS metadata operations, does not reconnect after a server restart or failover, and has no notion of a leader/standby pair.

Rust in the kernel

Linux 6.18 ships enough in-tree Rust for the everyday parts of a module: the module! entry point, allocation with explicit GFP flags, mutexes, spinlocks, condition variables whose waits report pending signals, errno-based errors, kernel strings, credentials, and iov iterators; code written against them reads like ordinary Rust.

There is no in-tree abstraction for VFS itself yet. The module declares its file_system_type, super_operations, inode_operations, and address_space_operations by filling the raw C structs from the generated bindings in vfs.rs, and each callback in those tables is an extern "C" function that rebuilds typed Rust state from raw kernel pointers before doing anything else. Sockets and threads likewise go through the raw bindings: transport.rs calls sock_create_kern, kernel_sendmsg, and kernel_recvmsg directly, and client/mod.rs starts the connection task as a kthread with kthread_create_on_node. kernel::error::code declares constants for most errnos but not ESTALE on every kernel, so zerofs_main.rs pulls it from the raw bindings with a two-line macro.

Nearly all of the module's unsafe sits in that glue; the protocol implementation has none at all, and the session logic almost none. The module implements most of what VFS asks of a filesystem: mounting, lookup, attribute reads and changes, file and directory creation, rename and unlink, hard and symbolic links, device nodes and fifos, persistent opens, atomic_open, directory iteration, buffered reads, dirty-folio writeback and writable mmap, direct I/O, fallocate, SEEK_DATA and SEEK_HOLE, POSIX record locks and flock, fsync, and statfs against the remote filesystem.

The C that remains

The module is not quite pure Rust due to the use of netfslib. The Rust-for-Linux bindings do not cover it yet, so the build runs bindgen over the target kernel's . Bindgen cannot generate calls to static inline functions, though, and much of netfslib's helper API is exactly that; those helpers are recompiled in two C files, netfs/compat_helpers.c and vfs/compat_helpers.c, fewer than 250 lines between them, which also alias the fields that newer headers rename or make private.

The same machinery sets the version floor. 6.18 is the first kernel whose in-tree Rust support covers what the module needs, and past it there is no version check anywhere in the build: bindgen and a set of struct-layout assertions run against the exact kernel tree being targeted, and a tree that cannot support the module fails to compile.

Buffered I/O through netfslib

Netfslib is the kernel's shared I/O layer for network filesystems. It handles readahead, dirty-folio writeback, writable mmap, and direct I/O, and asks the filesystem only to move bytes: the module implements netfslib's request operations and turns each subrequest into 9P2000.L.Z reads and writes. The mount negotiates a message size of up to 10 MiB, so a large readahead or writeback batch can travel as a single request. Caching policy is a mount option: consistency=relaxed, the default, allows one second of caching, while consistency=strict revalidates on the server and does unbuffered I/O.

One connection, many callers

Each mount holds one connection to the server. Requests carry 16-bit tags, and replies can come back in any order. A VFS caller serializes its request, writes it to the socket, and waits on its own tag; a single connection task reads the stream, reassembles complete replies, and wakes whichever caller was waiting on that tag.

Kernels without Rust

Linux has no stable...

module rust kernel linux filesystem netfslib

Related Articles