Skip to main content

TECH VEDA

Embedded Linux on Edge-AI 23rd Sept 2026 enrollingLinux kernel & Device drivers starts on 24th Oct 2026 enrollingCorporate on-site training - Submit proposal Pick your modulessignup for free monthly live Masterclass Register
Deep Dives

dm-verity: How a Read-Only Rootfs Stays Verified

dm-verity gives an embedded device a tamper-evident read-only rootfs. Learn how per-block hash-tree checking works and why the root hash must be anchored.

dm-verity: How a Read-Only Rootfs Stays Verified

dm-verity is a device-mapper target that gives a block device transparent, read-only integrity checking. It builds a Merkle tree of hashes over the data, and every block read is verified against that tree up to a single root hash before the data reaches user space. The security of the whole scheme depends on one thing: the root hash must be delivered to the kernel by a source you already trust, such as a signed kernel command line or an in-kernel signature. Without that anchor, dm-verity checks data against a root hash an attacker could have supplied.

On an embedded device that ships a fixed root filesystem, you want a guarantee that the code running on the board is the code you built, not something an attacker rewrote in the field. dm-verity provides that guarantee for a read-only rootfs. It sits in the block layer, below the filesystem, and verifies each data block against a cryptographic hash tree as it is read. This post explains how that verification works, walks through building a verified rootfs with veritysetup, and spends most of its time on the step that decides whether the whole thing is real security or a checkbox: anchoring the root hash in a chain of trust.

What dm-verity actually guarantees

dm-verity is a target in the device-mapper framework, the same subsystem behind dm-crypt and LVM. You give it a data device and a hash device, and it presents a virtual block device that returns data only if the data matches the expected hashes. It is read-only by design. It does not encrypt anything and it does not protect data written at runtime. What it detects is any change to the on-disk contents of the protected device after the hash tree was built.

The kernel documentation states the trust boundary plainly: the root hash “should be trusted as there is no other authenticity beyond this point.” Everything dm-verity does reduces to comparing computed hashes against that one value. If the data on disk is altered by a single byte, the hash of that block no longer matches, the mismatch propagates up the tree, and the read fails. If the root hash you handed the kernel is genuine, tampering is caught. If it is not, dm-verity faithfully verifies the device against the attacker’s tree.

The hash tree and on-demand verification

dm-verity does not hash the whole partition on every boot. That would be far too slow for a multi-gigabyte rootfs. Instead it uses a Merkle tree, and it verifies lazily.

The data device is divided into fixed-size data blocks, usually 4096 bytes. Each data block is hashed, and those hashes are the leaf nodes of the tree. A group of neighbouring leaf hashes that fills one hash block is itself hashed to form a node one level up. That process repeats until a single block remains: the root. The hash of that root block, combined with a salt, is the root hash.

Verification happens on demand. When the filesystem asks for a data block, dm-verity reads it, computes its hash, and checks it against the corresponding leaf. To trust the leaf it checks the leaf’s parent, and so on up to the root. In practice the upper levels of the tree stay in the page cache after the first few reads, so a typical verification touches only the data block and a couple of cached hash blocks. The cost is a hash computation on the first read of each block into the page cache, which is why the documentation describes it as lightweight. A block that is never read is never verified, and a block already in cache is not re-read from disk.

The tree, for a device of 32768 data blocks with SHA-256 and 4096-byte blocks, looks like this in the kernel documentation:

                        [   root    ]
                       /    . . .    \
            [entry_0]                 [entry_1]
           /  . . .  \                 . . .   \
[entry_0_0]  . . . [entry_0_127]   . . .  [entry_1_127]
   / ... \           /  . . . \            /          \
blk_0 .. blk_127  blk_16256 .. blk_16383  blk_32640 .. blk_32767

The step tutorials skip: anchoring the root hash

Most dm-verity walkthroughs stop after printing a root hash and mounting the device. That demonstrates the mechanism but misses the security argument. dm-verity proves that the data matches the root hash. It says nothing about whether the root hash is the one you shipped. The kernel documentation is explicit that dm-verity “is meant to be set up as part of a verified boot path” and that “it is expected that the caller has been authenticated in some way.” Anchoring the root hash is that authentication, and it is your responsibility, not dm-verity’s.

On a real device the anchor comes from the boot chain. The root of trust is usually a key in one-time-programmable fuses or ROM that the SoC uses to verify the first-stage bootloader. That bootloader verifies the next stage, which verifies the kernel and its command line. Because the root hash travels on that command line, and the command line is covered by a signature the bootloader checked, the root hash arrives already trusted. There are two common ways to deliver it:

  • Signed kernel command line. The verity table, including the root hash, is passed with the kernel’s dm-mod.create= parameter (enabled by CONFIG_DM_INIT). The device is assembled before init runs. This works only if the command line itself is part of what the bootloader authenticated.
  • In-kernel root hash signature. With CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG, you attach a PKCS#7 signature over the root hash and pass its key description through the root_hash_sig_key_desc option. The kernel validates that signature against its built-in trusted keyring when the device is created. Here the root hash carries its own proof, checked against a key compiled into a kernel you already trust.

Either path closes the loop. The test to apply to any dm-verity setup is simple: trace where the root hash comes from, and stop only when you reach something a signature already covers. If that trace ends at a plain text file on the same partition, the setup provides integrity checking with no authenticity, and an attacker who can rewrite the rootfs can rewrite the root hash beside it.

Building a verified rootfs with veritysetup

The tool for the userspace side is veritysetup, shipped with cryptsetup. Start from a read-only filesystem image, for example a SquashFS rootfs, and generate its hash tree into a separate file:

raghu@techveda.org:~$ sudo veritysetup format rootfs.squashfs verity-hash.img
VERITY header information for verity-hash.img
UUID:            9f6a1c2e-1b7a-4b0e-9d2a-7c3b5e8f0a11
Hash type:       1
Data blocks:     262144
Data block size: 4096
Hash block size: 4096
Hash algorithm:  sha256
Salt:            1c9a...e4f0
Root hash:       4392712ba01368efdf14b05c76f9e4df0d53664630b5d48632ed17a137f39076

The format step is offline and runs on your build host, not the target. Record the root hash; it is the value you will anchor. To activate the device on a running system, hand the same root hash back to the kernel:

raghu@techveda.org:~$ sudo veritysetup open rootfs.squashfs vroot verity-hash.img \
  4392712ba01368efdf14b05c76f9e4df0d53664630b5d48632ed17a137f39076
raghu@techveda.org:~$ sudo mount -o ro /dev/mapper/vroot /mnt

You can confirm the mapping is healthy at any time. veritysetup reports the device as verified, or corrupted if any check has failed since activation; the underlying kernel status character, shown by dmsetup status, is V for valid or C for corruption:

raghu@techveda.org:~$ sudo veritysetup status vroot
/dev/mapper/vroot is active and is in use.
  type:    VERITY
  status:  verified
  hash type: 1
  data block: 4096
  hash block: 4096
  hash name: sha256
  data device: /dev/loop0
  hash device: /dev/loop1

To see the mechanism react, change one byte in the data image and read a block that depends on it. The read returns an I/O error rather than wrong data, and the kernel logs the failing block:

raghu@techveda.org:~$ sudo dd if=/dev/mapper/vroot of=/dev/null bs=4096 count=1 skip=5000
dd: error reading '/dev/mapper/vroot': Input/output error
raghu@techveda.org:~$ dmesg | tail -1
device-mapper: verity: 7:0: data block 5000 is corrupted

On the target the open step is normally not run by hand. The dm-mod.create= command line, or an initramfs that has itself been verified, assembles the device before the rootfs is mounted.

What happens when a block is corrupted

By default a corrupted block makes that block unreadable: the kernel returns EIO to user space, as shown above. That is the safe behaviour for a security feature, but a device that returns errors from its rootfs may be unusable, so dm-verity offers policy options set at device creation:

  • restart_on_corruption and panic_on_corruption turn a detected corruption into a reboot or a kernel panic, which suits a device that should refuse to run tampered code rather than limp along.
  • restart_on_error and panic_on_error do the same for I/O errors rather than hash mismatches.
  • ignore_corruption logs the bad block but lets the read proceed. This is a diagnostic aid, not a production setting.
  • ignore_zero_blocks skips verification of blocks expected to be all zero, and check_at_most_once verifies each data block only on its first read to save CPU and memory, at the cost of catching only offline tampering, not runtime changes.

Field devices using flash have a second problem: bit rot. A worn NAND block can flip bits with no attacker involved, and to plain dm-verity that is indistinguishable from tampering, so the block becomes unreadable. Forward error correction addresses this. With use_fec_from_device and a small amount of Reed-Solomon parity data (an RS(255, 253) code with fec_roots of 2 adds roughly 0.8% overhead), dm-verity can reconstruct a damaged block, then verify the reconstructed block against its hash before using it. Because the recovered block is still checked against the tree, FEC recovers from decay without weakening the integrity guarantee.

Limits and where dm-verity fits

dm-verity solves exactly one problem: it makes offline modification of a read-only device detectable. It is worth being clear about what it does not do. It does not provide confidentiality, so if the rootfs contents are sensitive you layer dm-crypt underneath for encryption and dm-verity on top for integrity. It does not protect writable data, so application state, logs, and configuration on a read-write partition need a different mechanism. And with check_at_most_once it does not catch a block that is altered on the data device after its first read. For its intended role, protecting the immutable system image on a device that boots through a verified chain, it is the standard building block, used in Android Verified Boot and Chrome OS for exactly this purpose.

Setting up that verified boot chain end to end, from the SoC root of trust to a dm-verity rootfs, is a core part of embedded board bring-up, and something we work through on real hardware in TECH VEDA’s Embedded Linux BSP Development training.

Key takeaways

  • dm-verity is a read-only device-mapper target that verifies each data block against a Merkle hash tree rooted in a single root hash.
  • Verification is on demand: a block is hashed on its first read into the page cache, and upper tree levels stay cached, so the cost is low.
  • The security depends entirely on anchoring the root hash. Deliver it through a signed kernel command line (dm-mod.create=) or an in-kernel PKCS#7 signature (root_hash_sig_key_desc), never a plain file beside the data.
  • Choose a corruption policy deliberately: EIO by default, or restart_on_corruption / panic_on_corruption for a device that must refuse tampered code.
  • Use forward error correction on flash to survive bit rot, and pair with dm-crypt when you also need confidentiality.
Was this worth your time?

Frequently asked questions

Does dm-verity encrypt the root filesystem?
No. dm-verity provides integrity checking only; it detects modification but does not hide the contents. For confidentiality you combine it with dm-crypt, typically dm-crypt for encryption underneath and dm-verity for integrity on top.

Why is anchoring the root hash so important?
dm-verity only proves that the data matches the root hash it was given. If an attacker can rewrite the read-only partition, they can also replace a root hash stored beside it. The root hash must arrive from a source the boot chain already authenticated, such as a signed kernel command line or an in-kernel PKCS#7 signature checked against a trusted keyring.

What happens when dm-verity finds a corrupted block?
By default the kernel returns an I/O error (EIO) for that block and logs the failing block number, so tampered data never reaches user space. You can instead configure restart_on_corruption or panic_on_corruption to reboot or panic the device.

Can dm-verity handle flash bit rot rather than tampering?
Yes, with forward error correction enabled through use_fec_from_device. dm-verity uses Reed-Solomon parity to reconstruct a damaged block and then verifies the reconstructed block against the hash tree, so it recovers from decay without weakening the integrity guarantee.

Further reading

RB
Raghu Bharadwaj

Founder, TECH VEDA — 20+ years teaching the Linux kernel, device drivers and embedded systems.

Follow on LinkedIn

Get new posts by email

Kernel, embedded Linux and AI-era engineering — a few sharp reads a month. No spam.

We email occasionally and never share your address.