fs-verity on Linux: Per-File Integrity with Merkle Trees
A precise look at how fs-verity authenticates read-only files, verifies page-cache reads, exposes stable digests, and differs from dm-verity.
fs-verity lets a supporting Linux filesystem enforce transparent integrity checks for an individual read-only file. When verity is enabled, the kernel builds and stores a Merkle tree for the file, makes the file immutable through ordinary writes, and verifies data as it enters the page cache. Corrupted blocks fail reads instead of quietly reaching the application.
It is tempting to describe this as “signing a file,” but the base mechanism is narrower. fs-verity gives the kernel a stable digest and verified reads. A trusted component still needs to decide which digest or signer is acceptable.
The Merkle tree makes partial reads verifiable
A conventional hash requires reading the entire file before use. That is expensive when an application touches only a small portion of a large package. fs-verity divides content into blocks, hashes those blocks, groups the hashes into higher levels, and continues until one root summarizes the tree.
When a page is read, the kernel verifies the necessary path through that tree. The file digest includes more than the raw root hash, so consumers should obtain it through the documented FS_IOC_MEASURE_VERITY interface rather than inventing a parallel format.
Verification happens beneath ordinary read APIs. read(), pread(), and memory-mapped access all depend on page-cache data that the filesystem marks valid only after the integrity path succeeds. Hooking only a high-level read syscall would miss mapped I/O, which is why filesystem integration occurs around folio read and readahead paths.
Enablement is a one-way transition for the file
Userspace opens a completed file, invokes FS_IOC_ENABLE_VERITY, and waits while the filesystem constructs verity metadata. The call fails if the file is open for writing. Once successful, ordinary attempts to modify or truncate the file fail, and verity cannot simply be disabled in place.
That sequence has release-engineering consequences:
- Write and synchronize the final content.
- Enable verity.
- Measure the kernel-enforced digest.
- Compare it with trusted release metadata or verify its signature.
- Publish or activate the file only after the comparison succeeds.
Renaming the completed inode into its final location is safer than enabling verity on a public path while consumers may still be selecting the old version. The directory remains writable even though the verity inode is not; an attacker who can replace the directory entry may substitute a different file unless the consumer authenticates the measured digest.
Integrity and authenticity are separate policy layers
The base feature detects storage corruption relative to the tree stored with the inode. If an attacker can create a new verity file and convince an application to trust its digest, verified reads alone do not establish publisher identity.
Authentication can happen in userspace by comparing the measured digest with signed metadata. Linux also offers optional built-in signature verification using keys in the .fs-verity keyring, and Integrity Measurement Architecture can incorporate verity digests into measurement or appraisal policy. The kernel documentation cautions that the built-in option is not automatically the best architecture for every deployment.
Define the threat model before choosing one. Package managers, app stores, and content-addressed systems often already possess a signed metadata channel, making userspace verification natural. An embedded appliance may integrate verity into a boot trust chain instead.
Filesystem support is explicit
The kernel support layer is used by ext4, F2FS, and Btrfs. Support depends on kernel version, filesystem features, and user-space tools. For ext4, the filesystem needs the verity feature. Enabling that feature changes compatibility: old kernels may mount the filesystem read-only, and old filesystem-checking tools may not understand it.
Copies and backups need deliberate handling. A normal byte-for-byte file copy does not necessarily preserve the filesystem-specific verity metadata. Restore the content, re-enable verity, remeasure it, and compare the digest before treating the restored inode as protected.
fs-verity is not dm-verity
dm-verity authenticates a read-only block device and is suited to an immutable system image. fs-verity operates per file on a filesystem that can otherwise remain writable. It is useful when trusted files are installed or updated independently, such as application packages, language assets, firmware blobs, or large model files.
Neither feature controls who may execute a file, who may replace a pathname, or whether the application itself is trustworthy. Combine verity with signed release metadata, directory permissions, mount policy, and an activation design that fails closed when measurement or signature verification fails.
Design the corruption path before deployment
The kernel exposes a failed verification through the I/O interface the application used. A buffered read() of corrupted data fails with EIO; touching the same bad range through mmap() raises SIGBUS. An application that treats either result as a transient network error and retries forever can turn one damaged file into a service outage.
Define a terminal integrity state. Stop consuming the inode, record its measured digest, device and inode identity, offset if known, filesystem health, kernel version, and package provenance, then obtain a new copy through the trusted release channel. Do not copy bytes out of the failed file to “repair” it in place. A verity inode cannot be rewritten, and a newly created replacement must be enabled, measured, authenticated, and atomically activated again.
Metadata remains a separate surface. Owner, mode, timestamps, and extended attributes are not included in the fs-verity measurement, and a verity file can still be renamed, linked, or deleted. If those properties influence execution or authorization, protect and validate them independently. Likewise, direct I/O and DAX are unavailable because they would bypass the page-cache verification path; a performance design must accept buffered verified reads instead of silently falling back to an unverified mechanism.
Exercise the failure path on disposable storage before rollout. Corrupt a copied backing image below the filesystem, verify the expected EIO and SIGBUS behavior, confirm that monitoring identifies the right artifact, and prove that replacement restores service without weakening digest validation.
The operational check is simple but strict: after installation, remeasure the inode, compare it to the approved digest, test a normal read, and test that modification is rejected. A label in a database is not proof that the live file has verity enabled.
Related:
- Linux pidfds: Race-Free Process Handles Beyond Numeric PIDs
- Landlock on Linux: Unprivileged Filesystem Sandboxing with Backward-Compatible Rulesets
Sources: