Why File Systems Need a Smaller Blast Radius

To understand what kSFS must isolate, start with the failure path of an ordinary Linux file system. This chapter examines why file-system code is unusually difficult to compartmentalize in a monolithic kernel, then compares the partial remedies represented by user-space FUSE, eBPF, and memory-safe languages. The aim is to separate four requirements that are often blurred together: preventing memory damage, limiting authority, keeping the kernel live, and defining an interface that can be checked.

When a file-system bug becomes a kernel problem

A file system is not merely a format reader attached to a disk. In Linux, it participates in the virtual file system layer, interacts with the page cache, reaches storage through block devices, maintains metadata and locks, and exposes semantics to every application that opens, reads, writes, renames, or deletes a file. That participation is what makes a file system useful. It is also what makes a defect in the implementation unusually consequential.

In a monolithic kernel, this code runs with kernel privilege. The kSFS paper describes the consequence in terms of the trusted computing base: if a file-system implementation contains a memory-safety or concurrency bug, the bug is not naturally confined to that implementation. It can crash the kernel, corrupt shared state, disclose information, or become a path to code execution. The exact exploitability depends on the defect and the surrounding conditions, but the default failure domain is broad because the execution environment is shared.

The paper uses vulnerability data to motivate the problem. Its cited analysis reports 72 CVEs for ext4 since it was mainlined, while its table lists substantial counts for several other Linux file systems. Those numbers are reported results from the paper's analysis, not a current or exhaustive vulnerability census. Their significance is comparative: even mature filesystem implementations are substantial bodies of privileged code, and the cost of a mistake is not limited to a failed user-space process.

The architectural issue is deeper than line count. A small implementation can still be dangerous if it receives pointers to shared kernel structures and can invoke a wide set of kernel functions. Conversely, a large implementation can be made less dangerous if it interacts through a narrow contract and has no authority beyond the resources that contract exposes. Counting bugs helps establish the stakes; it does not tell us where to draw the boundary.

Linux's ordinary in-kernel interface was built for cooperation. The virtual file system expects a filesystem implementation to follow established semantics, populate structures correctly, observe locking rules, and return promptly. The components around it are optimized for a trusted participant. That makes normal operation efficient, but it makes hostile validation difficult. To check every field of every shared structure and every pointer crossing every subsystem would require reconstructing much of the semantics that the interface currently assumes.

The result is a chain of possible consequences:

  1. A filesystem defect begins in code that handles a request or interprets on-disk data.
  2. The defect operates through a trusted interface with access to shared kernel state.
  3. The affected state is consumed by VFS, the page cache, storage code, or another process.
  4. The original error becomes a kernel crash, a stall, an integrity failure, an information leak, or an exploit opportunity.

This is not a claim that every defect follows every step. It is a description of why the default blast radius is too large. A sandbox must interrupt the chain before untrusted implementation behavior becomes arbitrary behavior in the trusted kernel.

The state relationships make the design problem harder. A page-cache operation may trigger file-system code, which may need metadata locks, which may request data from a block device. A request that appears to be a simple read can therefore cross several ownership boundaries. If the filesystem code can call back into the kernel while holding a resource, or if a response causes a page fault that re-enters the same path, the failure may be a deadlock rather than a memory violation. A boundary designed only to stop out-of-bounds writes would miss that class of problem.

Imagine a read that begins as an application request but ends up waiting for storage while filesystem metadata is locked. The file-system code owns the interpretation of the on-disk format, yet the kernel owns the scheduling, memory, and I/O machinery that make the request possible. If the implementation can freely retain a kernel pointer or invoke an unexpected callback, the kernel must assume that its state may be observed or changed at any point in the sequence. That is why “put the code in a sandbox” is incomplete as a design instruction. The host must also decide which arguments cross the boundary, when a request is considered valid, which operations may block, and how ownership is restored after an error.

This is the difference between isolation as a property of memory and isolation as a property of an interface. Memory protection can stop a write to an address outside the module's region. It cannot, by itself, tell whether a request refers to an authorized disk, whether a metadata transition is legal, or whether a callback can re-enter a locked path. Those questions belong to the contract between the implementation and the kernel.

The kSFS paper therefore treats isolation as a compound requirement. The implementation should be restricted to its own memory and control-flow domain, should not crash or stall the kernel, and should reach only resources authorized for its instance. Correctness of the file system itself—such as preserving data consistency—is a different question. The diagnosis must keep those properties separate or it will promise more than any mechanism can deliver.

flowchart LR A["File-system implementation"] -->|trusted calls and shared state| B["Linux filesystem interface"] B --> C["VFS, page cache, and block-device relationships"] C --> D["Crash, corruption, leak, exploit, or stall"] E["FUSE process boundary"] -.->|moves execution away| B F["Memory-safe or verified code"] -.->|reduces selected bug classes| B G["Validated interface and resource policy"] -.->|narrows authority| B H["Liveness control"] -.->|limits indefinite execution| D

The point of this picture is that no single remedy blocks every arrow. A process boundary changes where implementation faults occur, but it does not remove request latency. Memory-safe code reduces some classes of memory error, but it does not automatically control liveness or authority. A validated interface narrows what the implementation can ask for, but it must be paired with an execution mechanism that can survive faults. kSFS is motivated by the need to combine these interventions.

Why no single hardening remedy closes the boundary

The first obvious remedy is to move file-system implementations out of the kernel. FUSE does exactly that. Its daemon runs as an ordinary user-space process while the kernel exposes the filesystem through the normal interface. The Linux FUSE documentation describes security rules for non-privileged mounts: device files are not allowed, set-user-ID and set-group-ID bits are ignored, mount points are checked, and access by other processes is restricted unless policy explicitly relaxes it. These rules prevent a mount owner from turning a filesystem image into a route to broader privilege or interference.

That is a strong improvement in failure containment. A buffer overflow in the daemon does not automatically overwrite the kernel's memory. A daemon can be killed, and the FUSE connection can be aborted when it is not responding. The kernel retains a degree of control over the interaction instead of trusting the filesystem implementation to manipulate shared structures directly.

The cost is that the useful work is now separated from the kernel by a communication path. A typical operation becomes a request that the kernel queues, a user-space read, filesystem processing, a user-space write for the response, and a wake-up of the waiting kernel path. The FUSE documentation also discusses deadlocks caused by reentrant operations and the possibility that a broken or malicious daemon can delay other work. Isolation is real, but it is not free and it does not make liveness disappear.

FUSE's security controls make the contrast with the ordinary in-kernel interface concrete. A non-privileged mount cannot simply create a device file and use it to reach an underlying device, and it cannot use set-user-ID behavior to turn a mounted image into a privilege-escalation step. The mount-point and process-access checks address a different risk: a filesystem daemon should not be able to observe or manipulate operations performed by processes that the mounting user could not otherwise control. These are policy checks around the request path, not properties of the filesystem format itself.

The same documentation warns that administrators can relax some restrictions when they trust their users or have other containment measures. That detail matters because it shows that a boundary is partly a policy choice. The security result depends on which actors may mount, which processes may enter the mount, and whether a stuck daemon can be aborted. kSFS inherits the lesson even though its implementation runs in the kernel: authority and recovery must be explicit rather than assumed from placement.

The kSFS paper reports why that cost can matter in applications. For RocksDB random reads, the user-space NTFS implementation using FUSE delivered only a small fraction of the throughput of the Linux implementation in the authors' evaluation. The exact ratio belongs to their testbed and workload, but the mechanism is general: repeated transitions, serialization, and data copying become especially expensive when an application performs many small or concurrent operations. Moving code out of the kernel contains faults by making the boundary physically expensive to cross.

The second remedy is to run restricted code in the kernel. eBPF demonstrates the appeal of this approach: the kernel can verify a program before it runs, then execute it without paying the full cost of a user-space round trip. But the same restrictions that make a program amenable to verification limit its programming model. The paper presents eBPF as too constrained for large, stateful, general-purpose file systems. Those constraints fit small, well-defined extensions more naturally than an implementation with large data structures, complex control flow, and state that must persist across operations.

This is not a criticism of eBPF's design goal. Bounded execution is valuable when the task can be expressed within the bound. It is a mismatch when the task's normal behavior is to walk metadata structures, wait for storage, retry an operation, or maintain substantial state. Turning a file system into a sequence of narrowly bounded event handlers would not merely add a verifier; it would require rewriting the program around a different execution model. The paper treats that rewriting and runtime overhead as a practical barrier to using eBPF as a generic file-system sandbox.

The third remedy is to use a memory-safe language. Rust and related approaches can eliminate or reduce important classes of memory bugs, and the paper discusses Bento as a framework for Linux file systems written in Rust. But memory safety is not identical to fault isolation. A memory-safe implementation can still fail to guarantee kernel liveness, fault recovery, or filesystem-level semantics. The paper's comparison therefore treats language safety as valuable but incomplete: it narrows some failure classes without supplying the whole boundary kSFS needs.

There is also a migration problem. Existing Linux file systems and widely deployed user-space implementations are often written in C. Rewriting them in a memory-safe language requires more than wrapping a library; it changes the implementation, its maintenance path, and the opportunity to reuse tested code. The fact that a language can express the needed semantics does not make a port cheap or risk-free. For kSFS, compatibility is part of the security argument because a boundary that cannot accommodate real implementations will encourage developers to bypass it.

The remedies also differ in where they place responsibility. FUSE asks the kernel to police a communication endpoint and lets the operating system terminate the daemon. A restricted in-kernel extension asks a verifier to reject programs before execution, but the program must fit the verifier's model. A memory-safe language moves some responsibility into the language and compiler, while leaving the runtime and kernel integration to solve liveness and authority. None of these arrangements is automatically superior; each makes a different part of the trust boundary explicit and leaves a different part for the system designer to manage.

That distinction matters for adoption. A team choosing a boundary is not only choosing how to stop a buffer error. It is choosing who will maintain the verifier or runtime, how existing stateful code will be adapted, which failures can be recovered, and what users are allowed to mount or access. The safest design on paper can be the weaker design in practice if its cost forces production users back toward an unmediated in-kernel interface.

These comparisons produce a set of requirements rather than a winner's podium. A useful in-kernel boundary needs an interface narrow enough to validate, an execution model expressive enough for a stateful file system, memory and control-flow isolation, a way to stop or recover from unresponsive code, and a resource policy that does not expose every disk or kernel object. It should also preserve a practical path for existing implementations. FUSE supplies a promising interaction contract; WebAssembly supplies a candidate sandbox; kSFS adds the kernel-specific machinery that joins them.

The requirements also explain why “make the code safer” is not a sufficient diagnosis. Memory safety addresses how a program accesses memory. Interface validation addresses what requests the program is allowed to make. Liveness control addresses whether it returns. Resource capabilities address which external objects it can reach. Compatibility addresses whether the boundary can be adopted without discarding mature code. A design that improves one dimension can leave the others untouched.

That is the problem kSFS takes forward. The question is no longer whether file systems should be isolated in the abstract. It is whether Linux can reuse the part of FUSE that makes distrust manageable, move execution back into the kernel to reduce crossing costs, and add enough WebAssembly and kernel support to prevent the resulting code from regaining the original blast radius. The next step is to examine FUSE itself as the boundary Linux already knows how to enforce.

References