<?xml-model href='http://www.tei-c.org/release/xml/tei/custom/schema/relaxng/tei_all.rng' schematypens='http://relaxng.org/ns/structure/1.0'?><TEI xmlns="http://www.tei-c.org/ns/1.0">
	<teiHeader>
		<fileDesc>
			<titleStmt><title level='a'>Metis: File System Model Checking via Versatile Input and State Exploration</title></titleStmt>
			<publicationStmt>
				<publisher>Usenix FAST 2024</publisher>
				<date>02/27/2024</date>
			</publicationStmt>
			<sourceDesc>
				<bibl> 
					<idno type="par_id">10542634</idno>
					<idno type="doi"></idno>
					
					<author>Yifei Liu</author><author>Manish Adkar</author><author>Gerard Holzmann</author><author>Geoff Kuenning</author><author>Pei Liu</author><author>Scott Smolka</author><author>Wei Su</author><author>Erez Zadok</author>
				</bibl>
			</sourceDesc>
		</fileDesc>
		<profileDesc>
			<abstract><ab><![CDATA[We present Metis, a model-checking framework designed for versatile, thorough, yet configurable file system testing in the form of input and state exploration. It uses a nondeterministic loop and a weighting scheme to decide which system calls and their arguments to execute. Metis features a new abstract state representation for file-system states in support of efficient and effective state exploration. While exploring states, it compares the behavior of a file system under test against a reference file system and reports any discrepancies; it also provides support to investigate and reproduce any that are found. We also developed RefFS, a small, fast file system that serves as a reference, with special features designed to accelerate model checking and enhance bug reproducibility. Experimental results show that Metis can flexibly generate test inputs; also the rate at which it explores file-system states scales nearly linearly across multiple nodes. RefFS explores states 3–28× faster than other, more mature file systems. Metis aided the development of RefFS, reporting 11 bugs that we subsequently fixed. Metis further identified 12 bugs from five other file systems, five of which were confirmed and with one fixed and integrated into Linux.]]></ab></abstract>
		</profileDesc>
	</teiHeader>
	<text><body xmlns="http://www.tei-c.org/ns/1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink">
<div xmlns="http://www.tei-c.org/ns/1.0"><head n="1">Introduction</head><p>File system testing is an essential technique for finding bugs <ref type="bibr">[43]</ref> and enhancing overall system reliability <ref type="bibr">[27]</ref>, as file-system bugs can have severe consequences <ref type="bibr">[53,</ref><ref type="bibr">92]</ref>. Effective testing of file systems is challenging, however, due to their inherent complexity <ref type="bibr">[4]</ref>, including many corner cases <ref type="bibr">[87]</ref>, myriad functionalities <ref type="bibr">[8]</ref>, and consistency requirements (e.g., crash consistency <ref type="bibr">[64,</ref><ref type="bibr">72]</ref>). Developers have created various testing technologies <ref type="bibr">[59,</ref><ref type="bibr">71,</ref><ref type="bibr">86]</ref> for file systems, but new bugs (both in-kernel and non-kernel) continue to emerge on a regular basis <ref type="bibr">[42,</ref><ref type="bibr">43,</ref><ref type="bibr">85]</ref>.</p><p>To expose a file-system bug, a testing tool must execute a particular system call using specific inputs on a given file-system state <ref type="bibr">[52,</ref><ref type="bibr">53,</ref><ref type="bibr">87]</ref>. For example, identifying a well-known Ext4 bug <ref type="bibr">[48]</ref> requires a write operation on a file initialized with a 530-byte data segment. In this case, the write operation is an input, and the file with a specific size constitutes (part of) the file-system state. Recent work <ref type="bibr">[9,</ref><ref type="bibr">52]</ref> also underscored the importance of adequately covering both file-system inputs and states during testing. While existing testing technologies seek to cover a broad range of file systems' functionality, they often do not, however, integrate coverage of both file-system inputs and states <ref type="bibr">[12,</ref><ref type="bibr">43,</ref><ref type="bibr">59,</ref><ref type="bibr">85]</ref>. For example, handwritten regression tools like xfstests <ref type="bibr">[71]</ref> can achieve good test coverage of specific file-system features <ref type="bibr">[4,</ref><ref type="bibr">58]</ref>, but do not comprehensively cover syscall inputs; similarly, fuzzing techniques (e.g., Syzkaller <ref type="bibr">[25]</ref>) are designed to maximize code-not input-coverage <ref type="bibr">[40]</ref>.</p><p>Both the input and state spaces of file systems are too vast to be completely explored and tested <ref type="bibr">[10,</ref><ref type="bibr">21]</ref>, so it is better to leverage finite resources by focusing on the most pertinent inputs and states <ref type="bibr">[52,</ref><ref type="bibr">86,</ref><ref type="bibr">88]</ref>. For example, metadata-altering operations, such as link and rename, and states with a complex directory structure are more frequently utilized in POSIX-compliance testing <ref type="bibr">[67]</ref>. Existing testing technologies also lack the versatility to test specific inputs and states <ref type="bibr">[25,</ref><ref type="bibr">59,</ref><ref type="bibr">71]</ref>. Thus, new testing tools and techniques are needed <ref type="bibr">[52,</ref><ref type="bibr">53]</ref> to avoid under-testing (which could miss potential bugs) or over-testing (which wastes resources that may be better deployed elsewhere).</p><p>This paper presents Metis, a novel model-checking framework that enables thorough and versatile input and state space exploration of file systems. Metis runs two file systems concurrently: a file system under test and a reference file system to compare against <ref type="bibr">[26]</ref>. Metis issues file-system operations (i.e., system calls with arguments) as inputs to both file systems while simultaneously monitoring and exploring the state space via graph search (e.g., depth-first search <ref type="bibr">[31]</ref>).</p><p>To compare the relevant aspects of file-system states, we first abstract them and then compare the abstractions. The abstract states include file data, directory structure, and essential metadata; abstract states constitute the state space to be explored. Metis first nondeterministically selects an operation and then fills in syscall arguments through a user-specified weighting scheme. Next, it executes the same operation in both file systems and then compares both systems' abstract states. Any discrepancy is flagged as a potential bug. Metis evaluates the post-operation states to decide if a state has been previously explored; if so, it backtracks to a parent state and selects a new state to explore <ref type="bibr">[31]</ref>. Metis continuously tests new file-system states until no additional unexplored states remain, logging all operations and visited states for subsequent analysis. Metis's replayer can reproduce potential bugs with minimum time and effort.</p><p>Metis effectively addresses the common challenges of model checking <ref type="bibr">[16,</ref><ref type="bibr">31]</ref> file systems. It checks file-system implementations directly, eliminating the need to build a formal model <ref type="bibr">[61]</ref>. To manage large file-system input and state spaces, Metis enables parallel and distributed exploration <ref type="bibr">[33]</ref> across multiple cores and machines. Metis works with any kernel or user file system, and does not require any specific utilities nor any modification or instrumentation of the kernel or the file system. It detects bugs by identifying behavioral discrepancies between two file systems without the need for oracles or external checkers, thus simplifying the process of applying Metis to new file systems. With few constraints, Metis is well suited for testing file systems that are challenging for other testing approaches, e.g., file system fuzzing <ref type="bibr">[43]</ref>, that require kernel instrumentation and utilities. Nevertheless, the quality of the reference file system is pivotal for assessing the behavior of other file systems <ref type="bibr">[26]</ref>. We therefore developed RefFS as Metis's reference file system. RefFS is an in-memory user-space POSIX file system with new APIs for efficient state checkpointing and restoration <ref type="bibr">[73,</ref><ref type="bibr">86]</ref>. Prior to using RefFS as our reference file system, we used Ext4 as the reference to check RefFS itself; Metis identified 11 RefFS bugs that we fixed during that process. Subsequently, we deployed 18 distributed Metis instances to compare RefFS and Ext4 for one month, totaling 557 compute days across all instances and executing over 3 billion file-system operations without detecting any discrepancy. This ensured that RefFS is robust enough to serve as Metis's (fast) reference file system.</p><p>Our experiments show that Metis can configure inputs more flexibly and cover more diverse inputs compared to other file-system testing tools <ref type="bibr">[25,</ref><ref type="bibr">59,</ref><ref type="bibr">71]</ref>. Metis's exploration rate scales nearly linearly with the number of Metis instances, also known as verification tasks (VTs). Despite being a user-level file system, RefFS's states can be explored by Metis 3-28&#215; faster than other popular in-kernel file systems (e.g., Ext4, XFS, Btrfs). Using Metis and RefFS, we discovered 12 potential bugs across five file systems. Of these, 10 were confirmed as previously unknown bugs, five of which were confirmed by developers as real bugs. Moreover, one of those bugs-which the developers confirmed existed for 16 years-and the fix we provided, was recently integrated into mainline Linux.</p><p>In sum, this paper makes the following contributions:</p><p>1. We designed and implemented Metis, a model-checking framework for versatile and thorough file-system input and state-space exploration. 2. We designed and implemented an effective abstract state representation for file systems and a corresponding differential state checker. 3. We designed and implemented the RefFS reference file system with novel APIs that accelerate and simplify the model-checking process. 4. Using RefFS, we evaluated Metis's input and state coverage, scalability, and performance. Our results show that Metis, together with RefFS, not only facilitates file-system development but also effectively identifies bugs in existing file systems.</p></div>
<div xmlns="http://www.tei-c.org/ns/1.0"><head n="2">Background and Motivation</head><p>In this section, we first introduce the procedures and challenges for testing and model-checking file systems. We then discuss two vital dimensions for file system testing: input and state.</p><p>We demonstrate the challenges of achieving versatile and comprehensive coverage of both inputs and states.</p><p>File system testing and model checking. File systems can be tested statically or dynamically. Static analysis <ref type="bibr">[9,</ref><ref type="bibr">57]</ref> evaluates the file system's code without running it; while useful, it struggles with complex execution paths that may depend on runtime state. Our work therefore emphasizes dynamic testing-executing and checking file systems in real-time scenarios <ref type="bibr">[12,</ref><ref type="bibr">59,</ref><ref type="bibr">67]</ref>. Generally, dynamic testing involves (1) crafting test cases using system calls, (2) initializing the file system, (3) running the test cases, and (4) post-execution validation of file system properties. Hence, the quality of test cases directly affects the testing efficacy.</p><p>Model checking is a formal verification technique that seeks to determine whether a system satisfies certain properties <ref type="bibr">[16,</ref><ref type="bibr">77]</ref>. The model is typically a state machine, and the properties, usually expressed in temporal logic, are checked using state-space exploration <ref type="bibr">[15]</ref>; here, each state represents a snapshot of the system under investigation. To automate this process, model checkers (e.g., SPIN <ref type="bibr">[31]</ref>) are used to generate the state space, verify property adherence, and provide a counterexample when a property is violated.</p><p>Extracting a model from a system implementation can be challenging, especially for large systems like file systems <ref type="bibr">[86,</ref><ref type="bibr">87]</ref>. Thus, recent work on implementation-level model checking <ref type="bibr">[86,</ref><ref type="bibr">87]</ref> seeks to check the implementation directly (without a model). Such approaches <ref type="bibr">[86]</ref> require one to create new, specialized checkers to test new file systems, and these checkers are typically focused on a limited range of bugs, such as crash-consistency bugs <ref type="bibr">[86,</ref><ref type="bibr">87]</ref>. The ongoing challenge is to simplify implementation-level file-system model checking so that using it does not require extensive effort or significant expertise in model checking and file systems, while at the same time being able to identify a wide range of bugs.</p><p>Covering system calls and their inputs. We refer to the system calls (syscalls) and their arguments as inputs or test inputs because syscalls are commonly used by user-space applications-and thus testing tools-to interact with file systems <ref type="bibr">[22,</ref><ref type="bibr">81]</ref>. Thoroughly testing file system inputs is challenging. While file-system-related syscalls represent only a subset of all Linux syscalls <ref type="bibr">[7,</ref><ref type="bibr">74]</ref>, each syscall has multiple arguments, and the potential value range for these arguments is vast <ref type="bibr">[52,</ref><ref type="bibr">74]</ref>. For example, open returns a file descriptor, accepting user-defined arguments for flags and mode in addition to pathname. Both flags and mode are bitmaps with 23 and 17 bits, respectively, representing many possible combinations. The bits represented in flags alone have 2 23 possible values, leading to an aggregate input space of 2 40 . Similarly, write and lseek take 64-bit-long byte-count arguments that have a large input domain of 2 64 possible values. Nevertheless, it is vital to test as many representative syscall inputs as possible.</p><p>Fully testing all syscalls with every potential argument is impractical <ref type="bibr">[25,</ref><ref type="bibr">37]</ref>. Instead, a sensible approach <ref type="bibr">[45,</ref><ref type="bibr">52]</ref> is to segment a large input space into multiple, disjoint input partitions-called input space partitioning <ref type="bibr">[39,</ref><ref type="bibr">52,</ref><ref type="bibr">78]</ref>. How choose what to execute, including individual syscalls (e.g., unlink) as well as meta-operations comprising a (small) sequence of syscalls (e.g., the write file operation opens a file and writes to it at a specific offset). From a given file system state, multiple potential successor states may arise. Through its nondeterministic choices of operations, Metis can effectively explore many of these options, ensuring thorough state exploration. To bound the input space, each operation randomly picks a file or directory name from a predetermined set of pathnames. The Input Driver is flexible and can generate files or directories with arbitrarily deep directory structures, long pathnames, and other unexpected scenarios such as many files inside a single directory.</p><p>We focus on state-changing operations <ref type="bibr">[26]</ref> (i.e., not read-only ones) as the Input Driver seeks to maximize the exploration of file system states. Currently, the Input Driver supports five meta-operations (create file, write file, chown file, chgrp file, and fallocate file), and 10 individual syscalls (truncate, unlink, mkdir, rmdir, chmod, setxattr, removexattr, rename, link, and symlink).</p><p>Adding a new operation has minimal effort of about 10 LoC. Metis exercises read-only operations such as read, getxattr, and stat after each state-changing operation, when computing file system abstract states in the State Explorer ( &#167;3.2).</p><p>After selecting the operation, Metis chooses its arguments based on a series of user-specified weights that control how often various argument partitions ( &#167;2) are tested. In the Input Driver, weights represent the probabilities assigned to different input partitions, which control testing frequencies. The method of assigning weights varies based on the argument type <ref type="bibr">[7,</ref><ref type="bibr">52]</ref>. For bitmap arguments, each bit receives a probability of being set. The number of input partitions in a bitmap argument is equivalent to its individual bit count. Given the ubiquity of powers of 2 in file systems <ref type="bibr">[38]</ref>, numeric arguments like write size (requested byte count) have input partitions segmented by these numbers as boundary values, rounding down to the nearest boundary. For example, write sizes ranging from 1024 to 2047 bytes (2 10 to 2 11 -1) are grouped in the same partition. Assigning a weight (e.g., 15%) to this partition implies a 15% chance of selecting a write size between 1024 and 2047 bytes. The total weight of all write-size partitions equals 100%. We placed 0 bytes as a distinct partition (unusual but allowed under POSIX) because the smallest power of 2 is 1, which is greater than 0. Additionally, Metis can also be configured to test only boundary values (powers of 2) such as 4096 as well as near-boundary values (&#177;1 from the boundary, e.g., 4095/4097) that are useful for testing underflow and overflow conditions.</p><p>The choice of weights depends on the user's objectives. For example, while O SYNC is common in crash-consistency testing <ref type="bibr">[59]</ref>, it is used infrequently for POSIX compliance <ref type="bibr">[67]</ref>. Due to disk I/O's slow speed, many tests focus on small write sizes <ref type="bibr">[12]</ref>. However, testing larger sizes can uncover size-specific bugs <ref type="bibr">[67,</ref><ref type="bibr">76]</ref>. Our objective is to ensure that Metis remains versatile and to allow one to adjust the input weights in line with the test focus.</p></div>
<div xmlns="http://www.tei-c.org/ns/1.0"><head n="3.2">State Exploration and Tracking</head><p>State explorer. The objective of Metis's State Explorer is to use graph traversal to conduct thorough and effective "state graph exploration," where the nodes correspond to file-system states and the edges represent transitions caused by operations <ref type="bibr">[15]</ref>. Metis supports depth-first search (DFS) as the main search algorithm.</p><p>The State Explorer relies on the SPIN model checker <ref type="bibr">[31]</ref> to conduct the state-space exploration. SPIN supports the Promela model-description language, and allows embedding C code in Promela code. This capability allows us to seamlessly issue low-level file-system syscalls and invoke utilities. SPIN's role is to provide optimized state-exploration algorithms (e.g., DFS) and data structures to track and store the status of the state graph; thus, we do not have to implement these features in the State Explorer.</p><p>In model checking, there are two types of states: concrete and abstract. Concrete states contain all the information that describes the states of the file system being checked. Abstract states serve as signatures to identify different system states of interest during the exploration.</p><p>After each operation, the State Explorer calls the abstraction function to extract abstract states as hash values from both file systems. Every time an abstract state is created, SPIN checks whether it has already been visited by looking up the abstract state in SPIN's hash table and decides on the next action, either backtracking to a previous concrete state or continuing from the current one. Meanwhile, the State Explorer mmaps the full file-system image into memory to be tracked by SPIN as a concrete state. Concrete states are stored in SPIN's stack to allow the State Explorer to restore the full file-system state as required.</p><p>To improve the performance of state exploration, we use RAM disks as backend devices for on-disk file systems. In Metis, we create both file systems with the minimum device sizes to reduce the memory consumption of maintaining concrete states and to make it easier to trigger corner cases such as ENOSPC.</p><p>File system abstract states. A concrete state is a reflection or snapshot of the entire (and highly detailed) file-system image, which renders it inappropriate for distinguishing a previously visited state <ref type="bibr">[11]</ref>. This is because any small change to the filesystem image leads to a new concrete state, even though there may be no "logical" change in the file system. For example, Ext4 updates timestamps in the superblock during each mutating operation, even if no actual change to a user-visible file was made. This substantially expands the state space, with many states differing only by minor timestamp changes, and leads to wasted resources on logically identical states. Additionally, because file systems are designed with different physical on-disk layouts, we cannot use concrete states to compare their behaviors. Therefore, we need a different state representation that includes only the essential and comparable attributes common to both file systems.</p><p>To address this problem, we defined an abstraction function to calculate file-system abstract states to distinguish unique states, and to compare file system behaviors. all files and directories (e.g., mode, size, nlink, UID, and GID); we exclude any noisy attributes such as atime timestamps.</p><p>We then hash this information to compact the abstract state for a more effective comparison. Metis supports several hash functions to compute abstract states; we evaluated the speed and collision resistance of each hash function (results elided for brevity) and chose MD5 by default as it had the best tradeoff of those characteristics.</p><p>The abstraction function deterministically aggregates key file system data and metadata, enabling comparison across different file systems. Specifically, the abstraction function begins by enumerating all files and directories in the file system by traversing it from the mount point. Their pathnames are sorted into a consistent, comparable order. We then read each file's contents and call stat to extract its important metadata mentioned above, following the pathname order. Finally, we compute the (MD5) hash based on the files' content, directory structure, important metadata, and pathnames to acquire the abstract state. Using abstract states not only prevents visiting duplicate states but also significantly reduces the amount of memory needed to track previously-visited states, owing to our lightweight hash representation, which in turn boosts Metis's exploration speed.</p><p>Tracking full file system states. In addition to abstract states, another complexity in tracking file system states is saving and restoring the concrete states when Metis needs to backtrack to a previous state (i.e., when reaching an already visited state); this involves State Save/Restore (SS/R) operations for concrete states. Concrete states must contain all file system information including persistent (on-disk) and dynamic (in-memory) states. Metis can feasibly save and restore on-disk states by copying the on-disk device and subsequently copying it back. Kernel file systems (e.g., Ext4 <ref type="bibr">[55]</ref>) maintain states in kernel space, which is inaccessible to Metis, a user process. Similarly, user-space file systems built on libFUSE (e.g., fuse-ext2 <ref type="bibr">[2]</ref>) are separate processes with separate address spaces, so again Metis cannot directly track their internal state. Tracking only persistent on-disk state leads to cache incoherency, because cached in-kernel information is inconsistent with the on-disk content.</p><p>We tried and evaluated several approaches to tracking full file system states (performance results elided for brevity) including fsync syscall, sync mount option, process snapshotting <ref type="bibr">[17,</ref><ref type="bibr">84]</ref>, VM snapshotting <ref type="bibr">[44,</ref><ref type="bibr">46]</ref>, and LightVM <ref type="bibr">[54]</ref>. None of these approaches were effective due to their functional deficiencies or inefficient performance. For those reasons, we adopted the approach presented in <ref type="bibr">[73]</ref> to unmount and remount the file system between each operation in Metis. An unmount is the only way to fully guarantee that no state remains in kernel memory. Remounting guarantees loading the latest on-disk state, ensuring cache coherency between each state exploration. This unmount-remount method was a compromise that ensures data coherency yet provides reasonable performance ( &#167;5.2), especially coupled with our specialized RefFS ( &#167;4).</p></div>
<div xmlns="http://www.tei-c.org/ns/1.0"><head n="3.3">Differential State Checker</head><p>Metis checker goals and approaches. Using only the Input Driver and State Explorer would constrain the detection of bugs to those manifesting as visible symptoms <ref type="bibr">[12]</ref>, such as kernel crashes. We thus needed a dedicated checker to identify cases where file systems fail silently <ref type="bibr">[43]</ref> (e.g., data corruption). Moreover, existing checkers usually require considerable effort to be applied to newly developed or constantly-evolving file systems. For example, since many checkers are hand-written (e.g., xfstests), the testing of new file systems involves redesigning and refactoring test cases. Some checkers depend on an exact (e.g., POSIX) specification or an oracle for bug detection <ref type="bibr">[59,</ref><ref type="bibr">67]</ref>: they are difficult to adapt to continuously-evolving file systems.</p><p>File systems vary considerably in terms of their developmental stages <ref type="bibr">[53,</ref><ref type="bibr">90]</ref>: mature file systems are typically more stable than new, emerging, or less popular ones <ref type="bibr">[53]</ref>. Yet many still share common (POSIX) features and data-integrity requirements. Therefore, we rely on a differential testing approach <ref type="bibr">[56]</ref>, to check emerging file systems for silent bugs, eliminating the need for a detailed specification or an oracle.</p><p>We developed Metis's Differential State Checker to identify a broad range of file system bugs and facilitate file system development. Our checker can easily adapt to test new file systems; it requires no modification to the checker, only a replacement of the file system under test. Metis uses a well-tested, reliable file system as the reference file system and a less-tested, emerging one as the file system under test. After each file system operation, the Differential State Checker compares the resulting states of both file systems to detect any discrepancies. To prevent false positives, it only compares the common attributes of file systems, including their abstract states, return values, and error codes.</p><p>Eliminating false positives. As any discrepancy is reported as a potential bug, when developing Metis we found that it sometimes identified discrepancies that were not bugs (i.e., false positives). We implemented measures to avoid these false positives. Table <ref type="table">1</ref> summarizes several such cases including their problems, causes, and solutions.</p><p>All these discrepancies arose due to different file system designs and implementations. For instance, Ext4 has a special lost+found directory and computes directory sizes by a multiple of the block size. In contrast, other file systems report sizes by the number of active entries and do not have a lost+found directory. Despite the same device sizes for different file systems, the available space varies due to different utilized and reserved space (e.g., for metadata). To address this, we equalize free space among file systems by creating dummy files based on the differences in their available spaces.</p><p>While developing Metis, we analyzed every discrepancy we encountered and addressed all false positives. Whenever a false positive was identified, we updated the state abstraction function or file system initialization code to eliminate such instances, an infrequent process that was conducted manually. None of these solutions introduce false negatives, because they all deal with non-standardized behavior. For example, an application should not expect sorted output from getdents. Nevertheless, if a change introduces any misbehavior, Metis's Differential State Checker will report and handle it.</p></div>
<div xmlns="http://www.tei-c.org/ns/1.0"><head n="3.4">Logging and Bug Replay</head><p>When detecting a discrepancy, it is important to be able to analyze the operations executed by the file systems to identify and reproduce the potential bug. Thus, Metis's Event Logger records details of all file-system operations and outcomes, comprising every syscall and their arguments, return values, error codes, SS/R operations, and resultant abstract state. Additionally, the Event Logger logs file-system information such as the directory structure and important metadata to pinpoint the deviant behavior as soon as a discrepancy is detected. To reduce disk I/O, we store the runtime logs in an in-memory queue and periodically commit them to disk. Leveraging the Event Logger, we can reproduce the precise sequence of operations leading to a discrepancy found by Metis.</p><p>Metis can replay identified bugs by re-executing the operations from the start of Metis's run. This process can be time-consuming, however, if the discrepancy was detected after executing many operations and passing through numerous states <ref type="bibr">[3]</ref>. So we needed a way to reproduce a discrepancy quickly. Existing test-case minimization techniques <ref type="bibr">[43,</ref><ref type="bibr">91]</ref> remove one operation from a sequence until the remaining operations can reproduce the bug; but this trial-and-error process is slow due to the abundance of I/O operations.</p><p>To replay bugs efficiently, the Optimized Replayer reproduces them using only a few operations (recorded in logs) and one (concrete state) file system image. Using SPIN, we retain concrete states in a stack, thereby capturing all file-system images along the current exploration path and allowing for bug reproduction from any desired location in the stack. Recent findings <ref type="bibr">[43,</ref><ref type="bibr">59]</ref> indicate that most bugs can be reproduced on a newly created file system using a sequence of eight or fewer operations. Accordingly, Metis uses an in-memory circular buffer to retain pointers to a few of the most recent file-system images (defaults to 10, but configurable) for quick post-bug processing. In practice, we first attempt to reproduce the bug using the most recent image (immediately preceding the bug state) along with the latest operation. If unsuccessful, we turn to the previous image and the two last operations, and so on in a similar pattern. This eliminates the need for Metis to replay the entire operation sequence from the beginning.</p></div>
<div xmlns="http://www.tei-c.org/ns/1.0"><head n="3.5">Distributed State Exploration</head><p>Along with performing state abstraction and setting limits on the number of files and directories, we also restrict the search depth to control the exponential growth of the state space. We set the maximum search depth to 10,000 by default <ref type="bibr">[31]</ref>. If the search hits the 10,000 th level, Metis reverts to the prior state rather than exploring deeper. Thus, the state space becomes bounded, allowing Metis to perform an exhaustive search. Still, even with this depth restriction, the state space remains large because of the variety in test inputs and file system properties <ref type="bibr">[21]</ref>. Exploring this space using a single Metis process (called a verification task, or VT) requires significant time.</p><p>To parallelize the state-space exploration <ref type="bibr">[32]</ref> we use Swarm verification <ref type="bibr">[33]</ref>, which generates parallel VTs based on the number of CPU cores. Each VT examines a specific portion of the state space. To prevent different VTs from re-exploring the same states, and to avoid having to coordinate states across VTs, SPIN employs several diversification techniques <ref type="bibr">[33]</ref>, where every VT receives a unique combination of bit-state hash polynomials, number of hash functions, random-number seeds, search orders (e.g., forward or in reverse) and search algorithms (e.g., DFS), ensuring varied exploration paths.</p><p>We enabled these parallel and distributed exploration capabilities for Metis. The setup uses a configuration file to determine the machine and CPU core count; Metis then produces the exact VT count based on the configuration file. When Metis runs on distributed machines, each runs a handful of VTs, one per CPU core. Each VT is automatically configured with a distinct combination of diversification parameters, guiding them to explore different state space areas. Utilizing multiple Metis VTs across multiple cores and machines increases the overall speed of state exploration while testing more inputs. Every Metis VT operates independently, with its own device, mount point, and logs, without interference with other VTs. Given that VTs explore states autonomously without inter-VT communication, there is a risk of resource wastage if several VTs examine the same state <ref type="bibr">[33]</ref>. We deployed multiple VTs on several multi-core machines and evaluated Metis extensively under Swarm verification ( &#167;5.2).</p></div>
<div xmlns="http://www.tei-c.org/ns/1.0"><head n="3.6">Implementation Details</head><p>Metis uses SPIN to achieve basic model-checking functions. The Promela modeling language <ref type="bibr">[31]</ref> serves as the main interface with SPIN. We wrote 413 lines of Promela, consisting of do...od loops that repeatedly select one of a number of cases in a nondeterministic fashion. Each case issues file-system operations, performs differential checks, and records logs. The main part of Metis comprises 7,911 lines of C/C++ code that implement Metis's components and its communication with SPIN. We also created 1,230 lines of Python/Bash scripts to manage different Metis VTs and runtime setup, such as invoking ramfs), to track their own state changes. However, capturing and restoring their entire state proved extremely challenging because the state resides across many kernel-resident data structures <ref type="bibr">[5]</ref>. Consequently, we developed a new file system, called RefFS, specifically designed to function as the reference system.</p><p>RefFS architecture. RefFS is a RAM-based FUSE file system. Figure <ref type="figure">2</ref> shows the architecture of RefFS and its interplay with Metis and relevant kernel components. It incorporates all the standard POSIX operations supported by the Input Driver along with the essential data structures for files, directories, links, and metadata. We developed RefFS in user space to avoid complex kernel interactions and have full control over its internal states. Comprising 3,993 lines of C++ code, RefFS uses the libFUSE user-space library together with /dev/fuse to bridge user-space implementations and the lower-level fuse kernel module. Metis handles file system operations on RefFS in the same manner as other in-kernel file systems. Most importantly, RefFS also provides four novel snapshot APIs to manage the full RefFS file system state via ioctls: ioctl SAVE, ioctl RESTORE, ioctl PICKLE, and ioctl LOAD. These are described next.</p></div>
<div xmlns="http://www.tei-c.org/ns/1.0"><head n="4.1">RefFS Snapshot APIs</head><p>RefFS shows how file systems themselves can support SS/R operations in model checking through snapshot APIs. The essence of SS/R operations lies in their ability to save, retrieve, and restore the concrete state of the file system. Although RefFS is an in-memory file system lacking persistence, it possesses a concrete state (i.e., snapshot) that includes all information associated with the file system. Existing file systems like BtrFS <ref type="bibr">[68]</ref> and ZFS <ref type="bibr">[8]</ref>, which support snapshots, can only clone (some of) the persistent state but not their in-memory states. In contrast, RefFS can capture and restore the in-memory states through its own APIs. Since RefFS stores all its data in memory, it guarantees saving and restoring the entire file system state.</p><p>Snapshot pool. The snapshot pool is a hash table that organizes all of RefFS's snapshots; the key is the current position in the search tree. The value associated with each key is a snapshot structure that saves the full file system state including all data and metadata such as the superblock, inode table, file contents, directory structures, etc. The memory overhead of the snapshot pool is low because the size of the pool is smaller than Metis's maximum search depth. Because RefFS is a simple file system, the average memory footprint for each state is just 12.5KB.</p><p>Save/Restore APIs. The ioctl SAVE API causes RefFS to take a snapshot of the full RefFS state and add an entry to the snapshot pool. The ioctl RESTORE does the reverse, restoring an existing snapshot from the pool. When Metis calls ioctl SAVE with a 64-bit key, RefFS locks itself, copies all the data and metadata into the snapshot pool under that key, and then releases the lock. Similarly, ioctl RESTORE causes RefFS to query the snapshot pool for the given key. If it is found, RefFS locks the file system, restores its full state, notifies the kernel to invalidate caches, unlocks the file system, and then discards the snapshot.</p><p>Pickle/Load APIs. Unlike other file systems, RefFS maintains concrete states by itself in the snapshot pool, so Metis does not need to keep RefFS's concrete states in its stack. To ensure good performance, RefFS's snapshot pool resides in memory. However, this means that all snapshots are lost when RefFS is unmounted, which would make it challenging to analyze and debug RefFS from a desired state. Thus, committing these snapshots to disk before Metis terminates is important to ensure they are available for post-testing analysis and debugging. Given a hash key, the ioctl PICKLE API writes the corresponding RefFS state to a disk file. It can also archive the entire snapshot pool to disk. Likewise, the ioctl LOAD API retrieves a snapshot from disk, loading it back into RefFS to reinstate the file system state. Using the ioctl PICKLE and ioctl LOAD APIs, RefFS can flexibly serialize and revert to any file system state both during and after model checking, aiding bug detection and correction. Specifically, these APIs allow RefFS to gain the same benefits as Metis's post-bug replay and processing, enabling bug reproduction from any point in a Metis run.</p></div>
<div xmlns="http://www.tei-c.org/ns/1.0"><head n="5">Evaluation</head><p>We evaluated the efficacy and performance of Metis and RefFS, specifically: (1) Does Metis have the versatility to test different input partitions compared to other testing tools? (See &#167;5.1.) (2) What is Metis's performance? How does it scale with the number of VTs when using Swarm verification? (See &#167;5.2.) (3) What is RefFS's performance compared to other file systems? How reliable and stable is RefFS, as Metis's reference file system? (See &#167;5.3.) (4) With RefFS set as the reference file system, does Metis find bugs in existing Linux file systems? (See &#167;5.4.) Experimental setup. We evaluated Metis on three identical machines, trying various configurations, particularly with multiple distributed VTs. Each machine runs Ubuntu 22.04 with dual Intel Xeon X5650 CPUs and 128GB RAM. We also allocated a 128GB NVMe SSD for swap space. We evaluated Metis's performance using RAM disks, HDDs, and SSDs by comparing Ext4 with Ext2. The results showed that RAM disks were 20&#215; faster than HDD and 18&#215; than SSD. Also, Metis performs best when the file system device is as small as possible. Therefore, we used RAM disks as backend devices for on-disk file systems and minimum mountable device sizes for all file systems in all evaluations that follow.</p></div>
<div xmlns="http://www.tei-c.org/ns/1.0"><head n="5.1">Test Input Coverage</head><p>We assessed input coverage ( &#167;2) for Metis and other file system tests on two dimensions: completeness and versatility. Completeness considers whether a testing tool covers all input partitions ( &#167;2) in test cases. Versatility is the ability to tailor test cases for any desired input coverage. Metis outperforms existing checkers and a fuzzer <ref type="bibr">[25]</ref> on both dimensions. Comparison with existing testing tools. We selected three testing tools, each representing a unique technique: CrashMonkey <ref type="bibr">[59]</ref> for automatic test generation, xfstests <ref type="bibr">[71]</ref> for (hand-written) regression testing, and Syzkaller <ref type="bibr">[25]</ref> for fuzzing.</p><p>FS Testing Approach Input Effort to test Effort to add State Code Cover-Bug (Examples) Versatility new FS new ops Tracking age Tracking Detection Metis: this work &#334;&#334;&#334; &#335; &#335; &#10004; &#10008; Behavioral discrepancies Traditional Model Checking: CVFS [21], CREFS [88] &#334; &#335;&#335;&#335; &#335;&#335;&#335; &#10004; &#10008; User-specified assertions Implementation-level Model Checking: FiSC [87], eXplode [86] &#334; &#335;&#335; &#335;&#335; &#10004; &#10008; User-written checkers Fuzzing: Syzkaller [25], Hydra [43] &#334;&#334; &#335;&#335; &#335; &#10008; &#10004; External checkers Regression Testing: xfstests [71], LTP [58] &#334; &#335;&#335; &#335;&#335;&#335; &#10008; &#10008; Preset expected outcome Automatic Test Generation: CrashMonkey [59], Dogfood [12] &#334;&#334; &#335; &#335;&#335; &#10008; &#10008; External checkers or an oracle</p><p>Table 3: Comparison of representative file system testing tools. In column 2, the more &#334; symbols, the more relatively versatile the system is; conversely, in columns 3-4, more &#335; symbols denote more effort.</p><p>Of the remaining unconfirmed bugs, four were deterministic and three were nondeterministic. Deterministic bugs are those easily reproducible after Metis reported a discrepancy or the kernel returned errors (e.g., hang or BUG). We are currently pinpointing the faulty code for the deterministic bugs and preparing patches for submission to the Linux community. Metis also detected nondeterministic bugs that its replayer could not reproduce. For instance, after using unlink to delete file d-00/f-01, the size of another file f-02 in F2FS incorrectly changed to 0 instead of the correct value. Replaying the same syscall sequence did not reproduce this bug. To trigger it, we had to rerun Metis, but the time and number of operations needed varied across experiments.</p><p>Given the bug's nondeterminism, we suspect a race condition between F2FS and other kernel contexts. We verified that these unconfirmed bugs persist in the Linux kernel repository (v6.3, May 2023) without any fixes, thus classifying them as unknown bugs.</p><p>To detect them, all these potential bugs require specific operations on a particular file system state, underscoring the value of both input and state exploration. JFFS2 bug #5 is an example of the interplay between input and state. After 4.3 hours of comparing JFFS2 with RefFS, Metis reported a discrepancy due to differing file content. We observed the bug occurred when truncating a file to a smaller size, writing bytes to it at an offset larger than its size, and then unmounting the file system to clear all caches. Uncovering this multi-step, data-corruption bug required specific inputs (truncate, write) and then unmounting and remounting, because there was a cache incoherency between the JFFS2 in-memory and on-disk states. Ironically, the fact that Metis was "forced" to un/mount, is exactly why we found this bug, which was present in the 2.6.24 Linux kernel and remained hidden for 16 years. We fixed this long-standing bug, and our patch has since been integrated into the Linux mainline (all stable and development branches).</p></div>
<div xmlns="http://www.tei-c.org/ns/1.0"><head n="6">Related Work</head><p>File system testing and debugging. We divide existing file system testing and bug-finding approaches into five classes: Tra-ditional Model Checking, Implementation-level Model Checking, Fuzzing, Regression Testing, and Automatic Test Generation. Table <ref type="table">3</ref> summarizes these approaches across various dimensions.</p><p>Traditional model checking <ref type="bibr">[21,</ref><ref type="bibr">88]</ref> builds an abstract model based on the file system implementation and verifies it for property violations. Doing so demands significant effort to create and adapt the model for each file system, given the internal design variations among file systems <ref type="bibr">[53]</ref>.</p><p>Implementation-level model checking <ref type="bibr">[86,</ref><ref type="bibr">87]</ref> directly examines the file system implementation, eliminating the need for model creation. Due to file systems' complexity, however, this approach requires either intrusive changes to the OS kernel <ref type="bibr">[86,</ref><ref type="bibr">87]</ref> or manually crafting system-specific checkers <ref type="bibr">[86]</ref>. Additionally, existing work <ref type="bibr">[86,</ref><ref type="bibr">87]</ref> based on this approach generally only identifies crash-consistency bugs and is incapable of detecting silent semantic bugs. Unlike these methods, Metis checks file systems for behavioral discrepancies on an unmodified kernel. Thus, there is no need to manually create checkers when testing a new file system <ref type="bibr">[86]</ref>. Moreover, other model-checking approaches rely on fixed test inputs <ref type="bibr">[21,</ref><ref type="bibr">86]</ref> and lack the versatility to accommodate different input patterns. All model-checking approaches, including Metis, track file system states to guarantee thorough state exploration <ref type="bibr">[15]</ref>, a feature often lacking in other approaches.</p><p>Model checking and fuzzing are orthogonal approaches, each with its own advantages and disadvantages. File system fuzzing <ref type="bibr">[25,</ref><ref type="bibr">43,</ref><ref type="bibr">83,</ref><ref type="bibr">85]</ref> continually mutates syscall inputs from a corpus, prioritizing those that trigger new code coverage for further mutation and execution, but they cannot make state-coverage guarantees, risk repeatedly exploring the same system states, and require kernel instrumentation. Some fuzzing techniques <ref type="bibr">[43,</ref><ref type="bibr">85]</ref> also corrupt metadata to trigger crashes more easily and use library OS <ref type="bibr">[65]</ref> to achieve faster and more reproducible execution than VM-based fuzzers. However, such designs have their own drawbacks: they require file-systemspecific utilities to locate metadata blocks and cannot test out-of-tree file systems unsupported by library OS. Hybridra <ref type="bibr">[89]</ref> enhances existing file system fuzzing with concolic execution, but it remains fuzzing-based and has the same limitations of file system fuzzers, including the lack of state-coverage guarantees.</p><p>Fuzzing mainly supplies inputs to stress file systems and commonly finds bugs using external checkers, such as KASan <ref type="bibr">[24]</ref> (memory errors) and SibylFS <ref type="bibr">[67]</ref> (POSIX violations). Current fuzzers configure the tested syscalls but not their arguments <ref type="bibr">[25,</ref><ref type="bibr">70]</ref>, as testing is driven by code coverage. Compared to fuzzing, Metis employs a test strategy that explores both the input and state spaces, rather than solely maximizing code coverage.</p><p>Manually written regression-testing suites like xfstests <ref type="bibr">[71]</ref> and LTP <ref type="bibr">[58]</ref> check expected outputs and ensure that code updates do not [re]introduce bugs. Because they are hand-created, they are not easily extensible and do not attempt to automate or systematize their input or state exploration. Compared to their XFS-specific tests, xfstests' "generic" tests can be used with any file system. Nevertheless, from our past experience (including building RefFS), even when adopting the generic tests, some setup functions must be manually modified.</p><p>Automatic test generation <ref type="bibr">[12,</ref><ref type="bibr">47,</ref><ref type="bibr">59]</ref> creates rule-based syscall workloads (e.g., opening a file before writing) and employs external checkers (e.g., KASan <ref type="bibr">[24]</ref>) or an oracle <ref type="bibr">[59]</ref> to identify file system defects. This technique is easily adapted to new file systems and extensible with new operations, owing to the universality of syscalls. Nevertheless these implementations have lacked the versatility needed to explore diverse inputs and do not explore the state space like Metis. Furthermore, these testing methods typically identify only a limited range of bugs; for instance, CrashMonkey <ref type="bibr">[59]</ref> exclusively detects crashconsistency bugs. We do not include a comparative analysis of testing for other storage systems, such as NVM libraries <ref type="bibr">[19]</ref> and data structures <ref type="bibr">[20]</ref>, given their different testing targets and goals.</p><p>Ultimately, Metis is not designed to replace any existing technique; rather, we believe that it is an additional tool that offers a complementary combination of capabilities not found elsewhere. Verified file systems. For Metis, a reliable and ideally bug-free reference file system is critical. Verified file systems are built according to formally verified logic or specifications. For example, FSCQ <ref type="bibr">[14]</ref> uses an extended Hoare logic to define a crash-safe specification and avoid crash-consistency bugs. Yggdrasil <ref type="bibr">[72]</ref> constructs file systems that incorporate automated verification for crash correctness. DFSCQ <ref type="bibr">[13]</ref> introduces a metadata-prefix specification to specify the properties of fsync and fdatasync for avoiding application-level bugs. SFSCQ <ref type="bibr">[34]</ref> offers a machine-checked security proof for confidentiality and uses data non-interference to capture discretionary access control to preclude confidentiality bugs. However, the specifications of verified file systems have only been used to verify particular properties (e.g., crash consistency <ref type="bibr">[13,</ref><ref type="bibr">14,</ref><ref type="bibr">72]</ref> or concurrency <ref type="bibr">[93]</ref>), so other unverified components can still contain bugs. Worse, even after rigorous verification, bugs can still hide due to erroneous specifications (e.g., a crash-consistency bug reported on FSCQ <ref type="bibr">[43]</ref>). None of these verified file systems include the extra APIs that RefFS provides, which are crucial for optimizing model-checking performance. While RefFS has not been formally verified, it re-lies on long-term Metis testing to attain high robustness. Thus, we chose it, rather than a verified file system, as the reference.</p></div>
<div xmlns="http://www.tei-c.org/ns/1.0"><head n="7">Conclusion</head><p>File system development is difficult due to code complexity, vast underlying state spaces, and slow execution times due to high I/O latencies. Many tools and techniques exist for testing file systems, but they cannot be easily updated to test specific conditions at a configurable level of thoroughness. Moreover, they tend to require code or kernel changes or cannot easily adapt to testing new file systems.</p><p>In this paper, we presented Metis, a versatile model-checking framework that can thoroughly explore file-system inputs and states. Metis abstracts file-system states into a representation that can be used to compare the file system under test against a reference one. We designed and built RefFS, a reference POSIX file system with novel features that accelerate the model-checking process. When used with Metis, RefFS is 3-28&#215; faster than other, more established, file systems. We extensively evaluated Metis's input and state coverage, scalability, and performance. Metis, helped by RefFS, can speed file-system development: we already found a dozen bugs across several file systems. Overall, we believe that Metis, with its unique features, serves as a valuable addition to file system developers' tool suite. Finally, Metis's framework is versatile enough to be adapted to other systems (e.g., databases). Future work. Our near-term plans include expanded state exploration using Swarm verification, investigating any bugs we discover, and then fixing and reporting them. We are also beginning to test network and distributed/parallel file systems <ref type="bibr">[29]</ref>.</p><p>In the long run, we plan the following: (i) Metis can trigger nondeterministic bugs, such as race conditions. Therefore, we need to integrate techniques to more deterministically explore and reproduce such bugs <ref type="bibr">[23]</ref>. Also, we plan to explore kernel thread interleaving states to find more concurrency bugs <ref type="bibr">[83]</ref>. (ii) We intend to enhance Metis by emulating crash states to identify crash-consistency bugs in kernel file systems <ref type="bibr">[47,</ref><ref type="bibr">59]</ref>. (iii) We aim to add support for testing controlled file-system corruptions <ref type="bibr">[29,</ref><ref type="bibr">85]</ref>. For example, if both RefFS and the test file system can be corrupted in a logically identical fashion, Metis can investigate more error paths (e.g., those leading to EIO).</p></div>
<div xmlns="http://www.tei-c.org/ns/1.0"><head>Requirements for running Metis with Swarm verification</head><p>When using multiple parallel Verification Tasks (VTs) in Metis, the required computational resources amount to the demand of a single VT, multiplied by the total number of VTs. Specifically, the number of CPU cores should equal or exceed the number of VTs operating on a machine. Similarly, memory and disk resources should linearly scale with the number of VTs. The number of VTs can be configured in the fs-state/swarm.lib file within the Metis repository.</p><p>When VTs in Metis are distributed over multiple machines, each machine must be equipped with resources proportional to the number of VTs it runs. Moreover, in this distributed setting, one machine should be designated as the primary, with the remaining machines serving as workers. The primary machine should be set up for password-less SSH key-based access to the workers. We recommend that the hostnames of the workers are accurately entered in the swarm.lib configuration file on the primary machine.</p></div></body>
		</text>
</TEI>
