You can find the other Yocto hardening posts from here!
In the last part of the Yocto Hardening, we talked about read-only file systems. The methods presented there show good ways to protect the integrity of a running system. But what if we want to protect the system from corruptions? Or from unauthorised modifications when the device is turned off? In embedded systems, it is often at least theoretically possible to remove the storage media, modify it offline, and then attempt to run the modified system. Or just modify the root file system in the boot loader. To prevent this, we need to have actual integrity checking in the system, instead of just relying on the read-only assumptions.
dm-verity
dm-verity is a Linux feature that provides integrity checking for read-only block devices. The first part of the name, dm, comes from the term “device mapper”. Device mapper is a Linux framework that allows creating virtual block devices on top of physical block devices. When the accesses to physical devices go through these dm-devices, the device mapper devices can implement custom functionality like integrity checking or encryption on top of the underlying device.
In dm-verity, the underlying file system is divided into blocks, each block is hashed (typically with SHA256), and a Merkle hash tree is generated from the block hashes. When a block from the file system is read, dm-verity calculates the hash of the accessed block and compares it against the hash tree. If someone or something has managed to modify the contents of the block device, a warning, reboot or kernel panic is raised.

The hash tree can be stored either after the file system in the slack space of the same partition, or in a separate partition. Storing it in an offset after the file system is simple, but because it is stored in an unmarked space, there is a risk that something at some point may accidentally overwrite it (for example, scripts that increase the size of the file system to match the partition size). You’ll also need to calculate the file system and partition sizes carefully so that there is enough extra space (usually around 8%-10%).
On the other hand, setting up the extra partition is a bit more complex to set up and maintain as it requires changes to the partition table. However, it is clearer as there is no mysterious unallocated yet vital space on the disk. This makes it harder to mess things up. Also, you don’t need to worry about calculating the hash tree offset.
Hash Tree and Root Hash
In the Merkle hash tree, changes to any node of the tree change the hash of the parent nodes. This, in practice, means that any change in the file system changes the root hash of the hash tree. Because all the changes propagate through the tree, we don’t need to protect the integrity of the entire hash tree: protecting the integrity and the authenticity of just the root hash is enough.
This root hash is required to open the protected device during the system initialization, either in kernel during the early mapped device creation, or in the initramfs, or even in the user-space. There are numerous places where we can store and retrieve the root hash from:
- Store the root hash in the initramfs. This is unsafe if the integrity of the initramfs isn’t verified, but if it is for example bundled into a signed kernel, then this method can be quite safe and simple.
- External partition. This partition should also be integrity-checked to keep the root hash secure, and the root hash itself should be signed.
- FIT image. This is a handy place to store everything that’s needed, and the information can be signed to ensure the authenticity as well.
- TPM, from where it can be unsealed if the device is in an acceptable state. Complex, but secure approach, as tampering it becomes a lot harder.
- Hardcoded into the kernel boot parameters. This is complex, as it creates a dependency between the bootloader, the root file system, and the kernel.
So, quite a few options then. If the root hash is stored somewhere like a signed FIT image or a signed initramfs, the authenticity of the root hash should come from the pre-existing signatures. However, if the root hash is stored for example in unsigned bootloader arguments or read from an external partition, you should look into root hash signing and signature verification. I attached a resource about this to the end of this text.
As one can guess, dm-verity requires the underlying file system to be read-only, as being read-write defeats the whole point of integrity checking. Therefore, you’ll need the learnings from the previous read-only rootfs blog post.
Getting Practical with dm-verity
That’s all for the theory part, how to actually enable dm-verity then? It is usually quite straightforward. First, you’ll prepare the disk (or disk image), and then open it before mounting it. With the verity tooling, this can be as simple as running two commands.
Preparing the Disk
To set up dm-verity for a partition, veritysetup command could be used. An alternative is using lower-level utility dmsetup directly, but it is a bit less user-friendly. Initialising the partition with veritysetup can be done with format subcommand, and the command looks as follows:
veritysetup format \
<DATA_DEVICE> \
<HASH_DEVICE>The device can be a block device, or it can also be a disk file, like <IMAGE_NAME>.ext4 or <IMAGE_NAME>.erofs. This command creates the hash tree to the destination defined in the second parameter. If you want to append the hash tree to a disk image, add the --hash-offset parameter. For example, with core-image-base image file from a Yocto build, the command could look as follows:
stat -c%s core-image-base-raspberrypi4-64.rootfs.ext3.inplace
veritysetup format \
--hash-offset=<STAT_COMMAND_OUTPUT> \
core-image-base-raspberrypi4-64.rootfs.ext3.inplace \
core-image-base-raspberrypi4-64.rootfs.ext3.inplaceThe output of the command might look like the following:
VERITY header information for core-image-base-raspberrypi4-64.rootfs.ext3.inplace
UUID: a69d12d3-4a32-4355-8f8d-3cf6d0d498af
Hash type: 1
Data blocks: 53248
Data block size: 4096
Hash block size: 4096
Hash algorithm: sha256
Salt: 5c8e96a895c8e22b40af9b06575b7dfbbc6095a03faaed1d03a3b30c41fb2ace
Root hash: fcc16a56e6915ff3d20adc9e7e23b55bd5a74b4d81b9a6ac9ae30dbf52f8442fFrom the output you can see the important root hash value that we need to secure and store for opening the device. Speaking of which.
Opening the Disk
To open the device, there are (at least) two options. We can either use dm-mod.create kernel parameter to open the device in kernel, or we can use veritysetup open in an initramfs. Let’s go through these:
Kernel Command-Line Parameter
When opening the disk directly with the kernel’s early device mapping parameter dm-mod.create, we need to add something like this to the kernel command-line:
dm-mod.create="<name>,<uuid>,<minor>,<flags>,<table>"Most of the fields are quite self-explanatory, and if they’re not, there will be an example that will hopefully explain things. The biggest mystery is table variable, which depends on the type of device mapper device you’re setting up. For dm-verity, it looks as follows:
<start> <length> verity <version> <data_dev> <hash_dev> <data_block_size> <hash_block_size> <num_data_blocks> <hash_start_block> <algorithm> <root_hash> <salt>This is… quite a number of fields to fill out. Still, with this command line parameter, you can avoid the need for an initramfs, assuming you can provide this information to the kernel parameters when it boots. Here’s one example of what the boot parameter may look like:
dm-mod.create="testdevice,,,ro,0 204800 verity 1 /dev/mmcblk0p2 /dev/mmcblk0p3 4096 4096 25600 0 sha256 9bcd1234af56e789b012c345d678e901f234a567b890c123d456e789f012a345 a3f1b2c4d5e6f7a8"
# Where:
# testdevice: device name
# : UUID empty
# : Device minor empty (kernel assigns it)
# ro: flags
# 0: start sector
# 204800: data length in 512 byte sectors
# verity: dm-mod name
# 1: verity version
# /dev/mmcblk0p2: data device
# /dev/mmcblk0p3: hash device
# 4096: data block size in bytes
# 4096: hash block size in bytes
# 25600: data block amount
# 0: hash device start sector
# sha256: hashing algorithm
# 9bcd1...: root hash
# a3f1b...: saltThis parameter assumes you have the hash tree in a separate partition. If that’s not the case, you would adjust the data and hash devices to be the same device, and the hash device start sector to continue right after the data sectors.
veritysetup in Initramfs
If all that seems like a lot of hassle, you can use veritysetup open in an initramfs instead. That is quite a lot simpler, at least if you are using an initramfs anyway and don’t have to start shoehorning it in. To do that, at some point in your initramfs scripts, you’d need to run the following command:
veritysetup open \
<DATA_DEVICE> \
<DEVICE_NAME> \
<HASH_DEVICE> \
<ROOT_HASH>
# There also exists obsolete syntax, so
# don't be confused if you see this instead:
# veritysetup create...Notice the distinct lack of bazillion parameters? This again applies to the situations where you have the hashes on a separate device. If that’s not the case, you should add the --hash-offset parameter with the correct offset.
After you have opened the device mapper device, you should still mount it with the regular mount command:
mount -o ro /dev/mapper/<DEVICE_NAME> /<MOUNT_POINT>
dm-verity in Yocto
Now that we know what should be done, let’s move on to implementing it on the Yocto side. Our old favourite, meta-security, has a mechanism for enabling dm-verity for the root file system. The provided feature calculates the rootfs hash tree during the build and uses initramfs to open the protected disk. The hash tree can either be appended to the end of the file system (default) or to a separate device. The root hash is stored in an environment file in initramfs, so the integrity of the initramfs has to be ensured when using this method.
Note: After writing about 99% of this text, I realised that meta-oe also has a class for generating verity images. When considering adding verity on your system, you may also want to check out that alternative. I didn’t try it out, but it seems to be generating a .verity suffixed image with appended hash-tree and an environment file for opening it, so it seems to be quite similar, but it’s still worth knowing about.
The feature requires some configuration and small Wic modifications to get running. This example builds a bit on the examples presented in the read-only file system blog post, where we enabled erofs on a Raspberry Pi, so I’m assuming you’re building erofs image already (this was enabled with IMAGE_FSTYPES:append = " erofs"). To enable dm-verity, first you’ll define the following in your build configuration:
# This mechanism stores the root hash in
# initramfs, and initramfs prepares verity,
# so we need an initramfs image
INITRAMFS_IMAGE = "dm-verity-image-initramfs"
# The name of the image you want to
# integrity check with dm-verity
DM_VERITY_IMAGE = "core-image-base"
# Type of the image, important for finding
# the artifacts during hash tree calculation
DM_VERITY_IMAGE_TYPE = "erofs"
# This actually enables the verity tasks for
# the image
IMAGE_CLASSES:append = " dm-verity-img "
# Not mandatory, but you may want to use this for
# the simplicity
INITRAMFS_IMAGE_BUNDLE = "1"
# Required for the variables in Wic file to
# expand correctly
WICVARS:append = " DM_VERITY_IMAGE DM_VERITY_IMAGE_TYPE IMAGE_NAME_SUFFIX"
# If using kernel recipe other than linux-yocto,
# add this as well
KERNEL_FEATURES:append = " features/device-mapper/dm-verity.scc"The magic here is the dm-verity-img that handles most of the image generation work, and dm-verity-image-initramfs that opens the device during the boot. If you’d like to use a separate device for storing the hash tree, you’d set DM_VERITY_SEPARATE_HASH to 1. This should take care of the Wic configuration as well, as it generates a wks.in file with the root and hash partitions. In the case of the appended hash tree, dm-verity-img takes the regular file system image, calculates the hash tree, and outputs a single complete file system image with .verity suffix.
In this demo, I’ll stick to using the default appended hash tree. Since we’re using the Raspberry Pi from the previous read-only rootfs example, we’ll need to modify the Wic file to use the generated verity image instead of the “regular” image:
# Replace this:
# part / --source rootfs --ondisk mmcblk0 --fstype=ext4 --label root --align 4096
# with this:
part / --source rawcopy --ondisk mmcblk0 --sourceparams="file=${IMGDEPLOYDIR}/${DM_VERITY_IMAGE}-${MACHINE}${IMAGE_NAME_SUFFIX}.${DM_VERITY_IMAGE_TYPE}.verity --align 4096And for this simplest case of appending the hash tree and storing the root hash in the initramfs, that’s all that is required! You should now be able to boot your device and see a verity-related message in the log:
device-mapper: verity: sha256 using implementation "sha256-generic"If you don’t see the log message, ensure that the correct kernel image is installed. For some reason, when I try enabling initramfs, the wrong kernel without initramfs always gets picked, so you may manually have to copy it in place.
If you want to try out corrupting the device, you could try something like this. Simply modifying the files doesn’t work because the underlying file system erofs does not support writing the files:
dd if=/dev/zero of=/dev/<ROOTFS_DEVICE> bs=4096 seek=128 count=32 conv=notruncNow, when you boot, you should see something new in the logs:
device-mapper: verity: sha256 using implementation "sha256-generic"
device-mapper: verity: 179:2: data block 512 is corrupted
device-mapper: verity: 179:2: data block 512 is corrupted
Buffer I/O error on dev dm-0, logical block 128, async page read
Verity device detected corruption after activation.
device-mapper: verity: 179:2: data block 512 is corrupted
device-mapper: verity: 179:2: data block 512 is corruptedThe default behaviour is to fail the I/O operation, which naturally results in the errors getting printed in the dmesg. The behaviour can be controlled by the veritysetup open command parameters, so you’ll need to modify the initramfs script to change it.
Closing Words
As you can see, dm-verity is indeed a really useful feature for ensuring the integrity of your read-only file systems. It can also be configured to work in a number of different ways, so you’ll have to do some thinking trying to come up with the best configuration for your situation. The important things to consider are at least the threats your device will be facing and how you plan to handle the firmware upgrades. The answers to these should give some guidance on where you need to store the root hash and how to handle the changes in the root hash during firmware upgrades. But that’s all for now, see you in the next text!
As a reminder, if you’re interested, you can find the previous Yocto hardening posts from here!
Recommended Reading
- The Linux Kernel documentation – dm-verity: As always, the kernel documentation for the feature is recommended reading.
- The Linux Kernel documentation – Early creation of mapped devices: Kernel documentation on how to create the mapped devices during the early boot using boot parameters. Good read for digging a bit deeper into the boot parameters.
- Lynx – DM-Verity Without an Initramfs: Comprehensive walkthrough of one possible implementation of dm-verity that doesn’t require an initramfs. It also contains information about the root hash signatures and verification.
- Rugix – Setting Up A/B OTA System Updates in Yocto for NXP i.MX with Verified Boot: This post touches on plenty of topics other than just dm-verity, but it shows well how to integrate dm-verity into a complete secure boot chain with A/B updates.
- Android Open Source Project – Implement dm-verity: Good summary of how the dm-verity works. Not necessarily that much information that isn’t covered in the other texts, but still a good post to skim through
- man7 – veritysetup: Documentation for the veritysetup command, if you want to for example check what different options there are for integrity failures.
