# Mender, CM4, \`dm-verity\`

**URL:** <https://hub.mender.io/t/mender-cm4-dm-verity/8260>\
**Category:** Board Integrations\
**Tags:** yocto, secure-booting, raspberry-pi-cm4\
**Created:** [April 24, 2026, 11:10pm UTC](https://hub.mender.io/t/mender-cm4-dm-verity/8260 "2026-04-24T23:10:45Z")\
**Posts on this page:** 1\
**Showing post:** 7

<div class="post-metadata">

**Author:** ![lukehatpadl](https://yyz2.discourse-cdn.com/flex036/user_avatar/hub.mender.io/lukehatpadl/32/2454_2.png) [@lukehatpadl](https://hub.mender.io/u/lukehatpadl)\
**Post date:** [May 18, 2026, 1:35am UTC](https://hub.mender.io/t/mender-cm4-dm-verity/8260/7 "2026-05-18T01:35:13Z")

</div>

# Mender + dm-verity + U-Boot signed boot on Raspberry Pi CM4

Here’s a Claude-generated, human-edited writeup of the secure boot implementation I (the human) had Claude implement recently for our CM4-based embedded system. There’s nothing particularly novel here but I couldn’t find anything that put all the pieces in one place with the requirements we had (specifically, using Mender with U-Boot and dm-verity on a CM4, and not requiring `initramfs` or a separate boot partition per slot).

## Survey of existing approaches

- **[meta-raspberrypi-secure](https://github.com/embetrix/meta-raspberrypi-secure)** (Embetrix) — covers the RPi-side signed boot: `boot.img` packaging, `rpi-eeprom-digest`, OTP fuse workflow, EEPROM lockdown. Excellent reference for the VC ROM half of the chain. Does not integrate with Mender (it assumes a stock rootfs flow) and does not wire into U-Boot’s FIT signature pipeline.

- **[meta-mender](https://github.com/mendersoftware/meta-mender)** + meta-mender-community — A/B slot management, `mender-artifact`, bootcount-rollback, U-Boot env integration. Does not assume an integrity layer over the rootfs; the stock `rootfs-image` Update Module writes a raw blob to the inactive slot with no expectation that what’s at rest in those blocks has to match a hash the bootloader will later verify.

- **[ejaaskel.dev’s dm-verity walkthrough](https://ejaaskel.dev/yocto-hardening-dm-verity/)** — a clean guide to getting a verity-protected rootfs booting under Yocto via initramfs. A great baseline for understanding the build-side machinery (`image_types_verity`, hash-tree generation, kernel cmdline). Doesn’t cover Mender A/B (the roothash is single-slot), doesn’t address what to do about per-slot bootargs, and uses an initramfs rather than `CONFIG_DM_INIT`.

The combined problem none of the above solves:

> Two A/B rootfs slots, **each with its own dm-verity roothash** , behind  
> a fully signed U-Boot chain, updatable as a single atomic OTA, on a  
> CM4 with `SIGNED_BOOT=1` and OTP fused — _and_ no initramfs.

## The core problem: where does the per-slot roothash live?

A dm-verity rootfs commits to a roothash that the kernel must be told about at mount time. With A/B slots, the two slots have _different_ roothashes (they’re different filesystem images). So an OTA that updates the rootfs has to also update _the authoritative record of the roothash that the bootloader will pass to the new kernel_. Otherwise the kernel `dm-init`s the wrong table and the rootfs fails to mount.

Three families of places to put the per-slot roothash:

1. **In the kernel cmdline that the bootloader composes.** Requires bootloader to know which slot is active and pick the right hash. Stock `boot.cmd.in` doesn’t do this.

2. **In U-Boot env.** Tempting — Mender already manages `mender_boot_part` there — but env is writable from user space (`fw_setenv`).

3. In a per-slot file on the boot partition, covered by a U-Boot FIT signature.

We chose 3, above:

- One **signed FIT per slot** on the shared FAT boot partition: `boot-2.fit`, `boot-3.fit` (named after the Mender partition number).
- Each FIT contains three subimages: `kernel`, `fdt` (stub), `script`.
- The `script` is a Hush `setenv bootargs '...'` carrying that slot’s dm-verity cmdline with the slot’s roothash baked in.
- **One signature** covers all three subimages. U-Boot’s `source` command verifies the signature and runs the script (sets env bootargs). `bootm` against the same FIT address verifies the same signature and boots the kernel.
- The OTA payload is the rootfs **plus** the new slot’s FIT. A custom Mender Update Module installs both atomically (well, as atomically as is possible, given power could be interrupted when installing a single image I don’t see this as too different).

```auto
VideoCore GPU ROM (hardware root of trust)
  → boot.img (RSA-2048, VC-verified post-OTP)
    → U-Boot kernel8.img (inside boot.img)
      → boot.scr (FIT-format, ECDSA P-256 signed)
        → boot-<N>.fit (ECDSA P-256 signed; per-slot)
              kernel : Linux Image (gzip-compressed)
              fdt : stub DTB (FIT-format requirement)
              script : setenv bootargs with this slot's
                        dm-verity roothash + cmdline
          → kernel + DTB → CONFIG_DM_INIT → /dev/dm-0 (verity rootfs)

```

There are thus two distinct signing keys:

- `boot_img` — RSA-2048. Algorithm fixed by Broadcom silicon (OTP holds SHA-256 of an RSA-2048 pubkey). Signs `boot.img` using `rpi-eeprom-digest`.
- `fit_sign` — ECDSA P-256, covering all internal FITs (`boot.scr`, `boot-<N>.fit`, the build-time linux fitImage). This public key is injected into U-Boot’s control DTB by the `kernel-fit-image`’s pass.

## Rootfs filesystem choice: erofs + dm-verity

We use `erofs` (with `-zlz4`) for the rootfs, not ext4 (I learned about that [here](https://hub.mender.io/t/read-only-filesystems-with-yocto/8266/2)). dm-verity is block-level and fs-agnostic, so it works with either.

One wic-side gotcha: erofs (like squashfs) cannot be “formatted” empty, so meta-mender’s default of leaving the inactive slot empty trips with _“It’s not possible to create empty erofs partition”_. The fix is as follows:

```bitbake
# machine conf
ARTIFACTIMG_FSTYPE = "erofs"
EXTRA_IMAGECMD:erofs = "-zlz4"

# Pre-populate the inactive slot at factory-flash with the same rootfs
# image. Costs ~rootfs-size additional bytes in the .sdimg (not in OTAs).
MENDER_FEATURES_ENABLE:append = " mender-prepopulate-inactive-partition"

# /data must stay writable; pin it to ext4 regardless of rootfs fstype.
MENDER_DATA_PART_FSTYPE = "ext4"

# Mount the U-Boot FAT read-only by default. The Update Module brackets
# its boot-<N>.fit write with remount,rw + remount,ro.
MENDER_BOOT_PART_FSTAB_OPTS = "ro"

```

## Build-side classes

### Kernel config fragment

No initramfs — `CONFIG_DM_INIT` lets the kernel parse a `dm-mod.create=` cmdline argument and set up `/dev/dm-0` before mounting `/`:

```kconfig
# dm-verity built in so the kernel can mount the rootfs through a
# dm-mod.create=... cmdline arg with no initramfs involved.
CONFIG_BLK_DEV_DM=y
CONFIG_DM_INIT=y
CONFIG_DM_VERITY=y

# erofs + LZ4 for the dm-verity-protected rootfs.
CONFIG_EROFS_FS=y
CONFIG_EROFS_FS_ZIP=y
CONFIG_EROFS_FS_ZIP_LZ4=y

```

### `verity-rootfs.bbclass`

Pairs with meta-oe’s `image_types_verity` (which produces `.${ARTIFACTIMG_FSTYPE}.verity` + a `.verity-params` shell fragment) and overwrites the plain rootfs blob in `IMGDEPLOYDIR` with the verity variant so Mender’s sdimg/`.mender`/bootstrap-artifact consumers pick it up unchanged.

```bitbake
inherit image_types_verity

# Read the same rootfs filename the rest of the pipeline produces.
# Without this, image_types_verity defaults to ext4 even when
# ARTIFACTIMG_FSTYPE is something else (e.g. erofs).
VERITY_IMAGE_FSTYPE = "${ARTIFACTIMG_FSTYPE}"

# Hash tree overhead reservation: ~0.8% of data at SHA-256/4 KiB.
# 32 MiB covers rootfses up to ~4 GiB with generous headroom.
VERITY_HASH_TREE_OVERHEAD_KB ?= "32768"

# Mender computes IMAGE_ROOTFS_SIZE from partition geometry; shrink it
# by the hash-tree overhead so rootfs + hash_tree fits the partition.
python __anonymous() {
    if 'verity' not in (d.getVar('IMAGE_FSTYPES') or '').split():
        return
    calc_kb = d.getVar('MENDER_CALC_ROOTFS_SIZE')
    if not calc_kb:
        return
    overhead_kb = int(d.getVar('VERITY_HASH_TREE_OVERHEAD_KB') or 0)
    extra_kb = int(eval(d.getVar('IMAGE_ROOTFS_EXTRA_SPACE') or '0'))
    new_size = int(calc_kb) - extra_kb - overhead_kb
    d.setVar('IMAGE_ROOTFS_SIZE', str(new_size))
}

# Force consumers to wait for the verity overwrite.
IMAGE_TYPEDEP:sdimg:append = "${@bb.utils.contains('IMAGE_FSTYPES', 'verity', ' verity', '', d)}"
IMAGE_TYPEDEP:mender:append = "${@bb.utils.contains('IMAGE_FSTYPES', 'verity', ' verity', '', d)}"
IMAGE_TYPEDEP:bootstrap-artifact:append = "${@bb.utils.contains('IMAGE_FSTYPES', 'verity', ' verity', '', d)}"

# Replace the plain artifact with the .verity variant as a post-step
# of do_image_verity (we can't modify do_image_verity itself).
python verity_rootfs_replace_rootfs() {
    import os, shutil
    imgdeploydir = d.getVar('IMGDEPLOYDIR')
    image_name = d.getVar('IMAGE_NAME')
    image_link = d.getVar('IMAGE_LINK_NAME')
    fstype = d.getVar('ARTIFACTIMG_FSTYPE')
    verity = os.path.join(imgdeploydir, image_name + '.' + fstype + '.verity')
    rootfs = os.path.join(imgdeploydir, image_name + '.' + fstype)
    link = os.path.join(imgdeploydir, image_link + '.' + fstype)
    if not os.path.exists(verity):
        bb.fatal("verity-rootfs: %s not produced by do_image_verity" % verity)
    shutil.copyfile(verity, rootfs)
    if os.path.islink(link) or os.path.exists(link):
        os.remove(link)
    os.symlink(image_name + '.' + fstype, link)
}
do_image_verity[postfuncs] += "verity_rootfs_replace_rootfs"

```

### The per-slot signed boot FIT

The build assembles a FIT containing { kernel, stub DTB, bootargs script } and signs it with `UBOOT_SIGN_KEYNAME` (ECDSA P-256). The bootargs script reads the verity parameters that `image_types_verity` emitted alongside the rootfs:

```bash
# Read verity params (data sectors, blocks, algo, roothash, salt, ...)
VERITY_PARAMS_FILE="${IMGDEPLOYDIR}/${IMAGE_LINK_NAME}.${ARTIFACTIMG_FSTYPE}.verity-params"
. "${VERITY_PARAMS_FILE}"

# Pre-gzip the kernel for FIT compression="gzip".
gzip -9 -n -c "${KERNEL_IMG}" > "${KERNEL_GZ}"

# Hush single-quoted literal. ${mender_kernel_root} stays as text;
# CONFIG_BOOTARGS_SUBST resolves it at bootm time. The single quotes
# prevent Hush from re-parsing the embedded dm-mod.create="..." double
# quotes at `source` time (which would otherwise lose them and break
# the kernel cmdline argument).
cat > "${BOOT_SCRIPT}" <<BOOTARGS_EOF
setenv bootargs '$CMDLINE_BASE dm-mod.waitfor=\${mender_kernel_root} dm-mod.create="vroot,,,ro,0 ${VERITY_DATA_SECTORS} verity 1 \${mender_kernel_root} \${mender_kernel_root} ${VERITY_DATA_BLOCK_SIZE} ${VERITY_HASH_BLOCK_SIZE} ${VERITY_DATA_BLOCKS} ${VERITY_DATA_BLOCKS} ${VERITY_HASH_ALGORITHM} ${VERITY_ROOT_HASH} ${VERITY_SALT} 1 ignore_zero_blocks" root=/dev/dm-0'
BOOTARGS_EOF

```

The ITS — one config carrying three subimages under one signature:

```dts
/dts-v1/;
/ {
    description = "Per-slot boot FIT (kernel + dtb stub + bootargs script)";
    #address-cells = <1>;
    images {
        kernel {
            data = /incbin/("${KERNEL_GZ}");
            type = "kernel";
            arch = "arm64";
            os = "linux";
            compression = "gzip";
            load = <0x20008000>;
            entry = <0x20008000>;
            hash-1 { algo = "sha256"; };
        };
        fdt {
            description = "Stub DTB (FIT-internal; runtime uses VC-merged DTB at fdt_addr)";
            data = /incbin/("${KERNEL_DTB}");
            type = "flat_dt";
            arch = "arm64";
            compression = "none";
            hash-1 { algo = "sha256"; };
        };
        script {
            description = "dm-verity bootargs setenv";
            data = /incbin/("${BOOT_SCRIPT}");
            type = "script";
            arch = "arm64";
            compression = "none";
            hash-1 { algo = "sha256"; };
        };
    };
    configurations {
        default = "conf-1";
        conf-1 {
            kernel = "kernel";
            fdt = "fdt";
            script = "script";
            hash-1 { algo = "sha256"; };
            signature-1 {
                algo = "sha256,ecdsa256";
                key-name-hint = "${UBOOT_SIGN_KEYNAME}";
                sign-images = "kernel", "fdt", "script";
            };
        };
    };
};

```

Note the device tree is present just so it builds and is a valid fitImage; the kernel really needs the merged device tree from the VC ROM (I tried).

A build-time check verifies the signature at build time:

```bash
mkimage -f "${BOOT_ITS}" -k "${UBOOT_SIGN_KEYDIR}" -r "${BOOT_FIT}"

# fit_check_sign runs the same verifier path U-Boot's bootm/source use.
fit_check_sign \
    -k "${DEPLOY_DIR_IMAGE}/u-boot.dtb" \
    -f "${BOOT_FIT}" > check.log 2>&1 \
    || (cat check.log >&2; bbfatal "boot.fit signature not verifiable against u-boot.dtb")

grep -q 'sha256,ecdsa256' check.log \
    || bbfatal "FIT ECDSA signature present but not enforceable. Check:
  - UBOOT_SIGN_ENABLE = \"1\"
  - u-boot.dtb has /signature/key-<KEY> with required=\"conf\"
  - mkimage produced a signature-1 node with algo=\"sha256,ecdsa256\""

```

### Signing `boot.scr` itself

Stock `mkimage -f auto -T script` emits a FIT without a signature node, so to close the loop we hand-write the ITS in the bbappend:

```bitbake
DEPENDS += "u-boot-tools-native"

do_compile() {
    cat > "${WORKDIR}/boot.its" <<BOOT_ITS_EOF
/dts-v1/;
/ {
    description = "boot script";
    #address-cells = <1>;
    images {
        bootscript {
            data = /incbin/("${WORKDIR}/boot.cmd");
            type = "script";
            arch = "${UBOOT_ARCH}";
            compression = "none";
            hash-1 { algo = "sha256"; };
        };
    };
    configurations {
        default = "conf-1";
        conf-1 {
            script = "bootscript";
            hash-1 { algo = "sha256"; };
            signature-1 {
                algo = "sha256,ecdsa256";
                key-name-hint = "${UBOOT_SIGN_KEYNAME}";
                sign-images = "script";
            };
        };
    };
};
BOOT_ITS_EOF

    mkimage -f "${WORKDIR}/boot.its" \
        -k "${UBOOT_SIGN_KEYDIR}" -r boot.scr
}

```

### `boot.cmd` — what U-Boot actually runs

```auto
run mender_setup

mmc dev 0
load mmc 0:1 ${kernel_addr_r} boot-${mender_boot_part}.fit \
    || run mender_try_to_recover
source ${kernel_addr_r} || run mender_try_to_recover

# VC firmware parks the overlay-merged DTB at a high ARM-visible RAM
# address; U-Boot 2026.01's lmb_reserve rejects it. Relocate first.
fdt move ${fdt_addr} ${fdt_addr_r}

# Third arg is the firmware-merged DTB, not the FIT-internal stub.
bootm ${kernel_addr_r} - ${fdt_addr_r}
run mender_try_to_recover

```

`source`, `bootm`, or `load` failing all route through `mender_try_to_recover`, which bumps `bootcount` and lets the standard Mender altbootcmd path roll back to the other slot. A bad new `boot-<N>.fit` cannot brick a factory-flashed unit.

## U-Boot configuration

### Functional knobs (apply to every build)

```kconfig
# CONFIG_FIT_SIGNATURE depends on CONFIG_FIT. Without the parent, Kconfig
# silently drops the child and bootm rejects FIT blobs at runtime.
CONFIG_FIT=y
CONFIG_FIT_SIGNATURE=y
CONFIG_FIT_SIGNATURE_MAX_SIZE=0x4000000

# ECDSA P-256 verification. CONFIG_ECDSA is the *parent* (depends on
# CONFIG_DM); CONFIG_ECDSA_VERIFY is the FIT-time verifier. Without
# the parent, Kconfig silently drops the child and bootm rejects
# signed FITs even though mkimage + host-side fit_check_sign happily
# produce them.
CONFIG_ECDSA=y
CONFIG_ECDSA_VERIFY=y

# bootm_process_cmdline_env runs cli_simple_process_macros to resolve
# ${var} in env "bootargs" at bootm time -- in-place, no Hush re-parse.
# Required so the dm-mod.create="..." double quotes survive to the
# kernel. Mender's expand_bootargs trick (Hush `run` on a stored setenv
# string) eats them.
CONFIG_BOOTARGS_SUBST=y

```

### Hardening knobs

Largely derived from [meta-raspberrypi-secure’s `security-harden.cfg`](https://github.com/embetrix/meta-raspberrypi-secure/blob/scarthgap/recipes-bsp/u-boot/files/security-harden.cfg) and [redpesk’s trusted-boot reference](https://docs.redpesk.bzh/docs/en/master/redpesk-os/trusted-boot/trusted-boot-uboot.html), with Mender-specific environment flags added:

```kconfig
# -2 = no autoboot prompt at all and no abort check.
CONFIG_BOOTDELAY=-2
# Disable EFI / extlinux bootmeths. Our bootflow is boot.scr only;
# leaving these on means U-Boot iterates them first, spamming
# "Cannot persist EFI variables" / "Boot failed (err=-14)", and they
# also widen the parallel-bootflow attack surface.
CONFIG_BOOTMETH_EFILOADER=n
CONFIG_BOOTMETH_EFI_BOOTMGR=n
CONFIG_BOOTMETH_EXTLINUX=n
CONFIG_BOOTMETH_EXTLINUX_PXE=n
CONFIG_ENV_FLAGS_LIST_STATIC="bootcmd:sr,altbootcmd:sr,preboot:sr,bootargs:sr,boot_targets:sr,bootdelay:dr,bootlimit:dr,mender_setup:sr,mender_altbootcmd:sr,mender_try_to_recover:sr,mender_pre_setup_commands:sr,mender_post_setup_commands:sr"
CONFIG_BOOTM_EFI=n
CONFIG_BOOTM_NETBSD=n
CONFIG_BOOTM_OPENRTOS=n
CONFIG_BOOTM_OSE=n
CONFIG_BOOTM_PLAN9=n
CONFIG_BOOTM_RTEMS=n
CONFIG_BOOTM_VXWORKS=n
CONFIG_CMD_BOOTI=n
CONFIG_CMD_BOOTD=n
CONFIG_CMD_BOOTZ=n
CONFIG_CMD_ABOOTIMG=n
CONFIG_CMD_ADTIMG=n
CONFIG_CMD_BOOTEFI=n
CONFIG_CMD_BOOTEFI_BINARY=n
CONFIG_CMD_BOOTEFI_BOOTMGR=n
CONFIG_CMD_ELF=n
CONFIG_CMD_GO=n
CONFIG_CMD_NET=n
CONFIG_CMD_UBI=n
CONFIG_CMD_UBIFS=n
CONFIG_CMD_USB=n
CONFIG_CMD_SF=n
CONFIG_CMD_I2C=n
CONFIG_CMD_SPI=n
CONFIG_CMD_PCI=n
CONFIG_CMD_DIAG=n
CONFIG_CMD_MEMORY=n
CONFIG_CMD_IMI=n
CONFIG_CMD_SMC=n
CONFIG_CMD_HVC=n
CONFIG_CMD_EXT4=n
CONFIG_CMD_EEPROM=n
CONFIG_VIDEO=n

```

One non-obvious bit specific to RPi: meta-raspberrypi ships a `maxsize.cfg` that pins `CONFIG_SYS_BOOTM_LEN=0x1000000` (16 MiB). For a ~25 MiB gzip-compressed kernel that’s too tight (“inflate() returned -5”). Drop the fragment:

```bitbake
SRC_URI:remove = " file://maxsize.cfg"

```

so the upstream ARM64 default (128 MiB) wins.

## OTA: the custom `rootfs-verity` Mender Update Module

The build-time piece overrides `IMAGE_CMD:mender` to emit a `module-image` artifact (type `rootfs-verity`) carrying two payload files:

```bitbake
# signed-bootimg produces .boot.fit as a side effect; wait for it.
IMAGE_TYPEDEP:mender:append = " signed-bootimg"

IMAGE_CMD:mender() {
    ROOTFS_SRC="${IMGDEPLOYDIR}/${IMAGE_LINK_NAME}.${ARTIFACTIMG_FSTYPE}.verity"
    BOOT_FIT_SRC="${IMGDEPLOYDIR}/${IMAGE_LINK_NAME}.boot.fit"

    # Pack payload files under known stable names. The rootfs basename
    # is derived from ARTIFACTIMG_FSTYPE so the on-target Update Module
    # can glob *.verity and stay fstype-agnostic.
    STAGE="${WORKDIR}/mender-rootfs-verity.stage"
    rm -rf "${STAGE}" && mkdir -p "${STAGE}"
    cp "${ROOTFS_SRC}" "${STAGE}/rootfs.${ARTIFACTIMG_FSTYPE}.verity"
    cp "${BOOT_FIT_SRC}" "${STAGE}/boot.fit"

    extra_args=
    for dev in ${MENDER_DEVICE_TYPES_COMPATIBLE}; do
        extra_args="$extra_args -t $dev"
    done
    [-n "${MENDER_ARTIFACT_SIGNING_KEY}"] \
        && extra_args="$extra_args -k ${MENDER_ARTIFACT_SIGNING_KEY}"

    mender-artifact write module-image \
        -n ${MENDER_ARTIFACT_NAME} \
        -T rootfs-verity \
        $extra_args \
        -f "${STAGE}/rootfs.${ARTIFACTIMG_FSTYPE}.verity" \
        -f "${STAGE}/boot.fit" \
        ${MENDER_ARTIFACT_EXTRA_ARGS} \
        -o ${IMGDEPLOYDIR}/${IMAGE_NAME}${IMAGE_NAME_SUFFIX}.mender
}
IMAGE_CMD:mender[vardepsexclude] += "IMAGE_ID"

```

The target-side Update Module is a shell script under `/usr/share/mender/modules/v3/rootfs-verity`. It’s a fork of stock `rootfs-image` with three intentional changes:

1. **Two payload files instead of one.** The rootfs streams to the inactive block device; the boot FIT goes to `/uboot/boot-<passive-slot>.fit`.

2. **`check_device_matches_root` understands `/dev/dm-*`.** The stock major/minor and `findfs` fallbacks both miss when `/` is on /dev/dm-0`because they compare against the mapper device rather than the underlying block device it wraps. Walk`/sys/block/dm-N/slaves/` first:

```sh
check_device_matches_root() {
    case "$1" in
        /dev/ubi*) ... ;;
        *)
            # Stock: match major/minor of $1 against /.
            if ["$(stat -L -c %02t%02T "$1")" = "$(stat -L -c %04D /)"]; then
                return 0
            fi

            # Added: if / is /dev/dm-*, the major/minor check above
            # compares against the mapper device, not the block device
            # it wraps. Walk the dm-N slaves.
            ROOT_MOUNTPOINT_DEV="$(mount | awk '$3 == "/" { print $1; exit }')"
            case "$ROOT_MOUNTPOINT_DEV" in
                /dev/dm-*)
                    for slave in "/sys/block/$(basename "$ROOT_MOUNTPOINT_DEV")/slaves/"*; do
                        [-e "$slave"] && ["$1" = "/dev/$(basename "$slave")"] && return 0
                    done
                    ;;
            esac

            # Stock fallback.
            ROOT_DEVICE="$(findfs "$(grep -o '\(^\| \)root=[^]*' /proc/cmdline | cut -d= -f2-)")"
            ;;
    esac
    ["$1" = "$ROOT_DEVICE"] && return 0
    echo "Mounted root ($ROOT_DEVICE) does not match boot loader environment ($1)!" 1>&2
    return 1
}

```

1. **`DownloadWithFileSizes`** rather than `Download`, so the rootfs streams straight to the inactive block device (via `mender-flash` or `cat > $passive`) without staging the full image. The small `boot.fit` is buffered to the  
module’s `$FILES` dir and installed via plain `cp` to the already-mounted `/uboot`:

```sh
DownloadWithFileSizes)
    check_requirements

    boot_mnt="$(boot_fat_mountpoint)"
    [-n "$boot_mnt"] || { echo "$BOOT_FAT not mounted" 1>&2; exit 1; }

    while true; do
        line="$(cat stream-next)"
        [-z "$line"] && break
        file="$(echo "$line" | cut -d' ' -f1)"
        size="$(echo "$line" | cut -d' ' -f2)"
        base="$(basename "$file")"

        case "$base" in
            # Fstype-agnostic glob: matches rootfs.ext4.verity,
            # rootfs.erofs.verity, etc.
            *.verity)
                if ["$MENDER_FLASH_AVAILABLE" = 1]; then
                    mender-flash --input-size "$size" --input "$file" --output "$passive"
                else
                    cat "$file" > "$passive"; sync
                fi
                ;;
            boot.fit)
                # /uboot is normally ro; remount rw for the write, then
                # back to ro. Shrinks the window where compromised
                # userspace can scribble on the boot partition.
                mount -o remount,rw "$boot_mnt"
                cat "$file" > "$boot_mnt/boot-$passive_num.fit"
                sync
                mount -o remount,ro "$boot_mnt"
                ;;
            *)
                echo "unexpected payload file '$base'" 1>&2; exit 1
                ;;
        esac
    done
    ;;

```

The recipe is one-line install:

```bitbake
SUMMARY = "Mender Update Module: rootfs + signed boot FIT installer"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

SRC_URI = "file://rootfs-verity"
S = "${UNPACKDIR}"

MENDER_MODULE_DIR = "${datadir}/mender/modules/v3"

do_install() {
    install -d ${D}${MENDER_MODULE_DIR}
    install -m 0755 ${S}/rootfs-verity ${D}${MENDER_MODULE_DIR}/rootfs-verity
}

FILES:${PN} = "${MENDER_MODULE_DIR}/rootfs-verity"
RDEPENDS:${PN} = "libubootenv-bin util-linux-findfs jq"

```

## Boot sequence

1. VC ROM verifies `boot.img` against OTP-fused pubkey hash.
2. U-Boot runs the signed `boot.scr`. `source` verifies the FIT signature against `u-boot.dtb`’s embedded pubkey.
3. `mender_setup` sets `mender_boot_part` (= 2 or 3).
4. `load mmc 0:1 ${kernel_addr_r} boot-${mender_boot_part}.fit`.
5. `source ${kernel_addr_r}` — U-Boot verifies the FIT signature; the embedded Hush script runs and `setenv bootargs '...'` with the slot’s dm-verity cmdline. `${mender_kernel_root}` is left as a literal placeholder (inside single quotes so Hush doesn’t expand at source-time).
6. `fdt move ${fdt_addr} ${fdt_addr_r}` (lmb\_reserve workaround).
7. `bootm ${kernel_addr_r} - ${fdt_addr_r}` — verifies the **same** FIT signature (kernel subimage this time), extracts and decompresses the kernel, hands off.
8. `bootm_process_cmdline_env` runs `cli_simple_process_macros` (via `CONFIG_BOOTARGS_SUBST`), resolving `${mender_kernel_root}` in env bootargs in-place — preserving the embedded `dm-mod.create="..."` double quotes.
9. Kernel boots. `CONFIG_DM_INIT` parses `dm-mod.create=`, waits for `/dev/mmcblk0pN` via `dm-mod.waitfor=`, creates `/dev/dm-0`.

## What this design buys you (and what it doesn’t)

Things this chain prevents, on a fused unit:

| Vector | Outcome |
| --- | --- |
| Modified `boot.img` (config.txt / cmdline / U-Boot / base DTB) | VC ROM rejects RSA-2048 signature, board halts |
| Modified `boot.scr` on FAT | U-Boot `source` verifies ECDSA against control-DTB pubkey, refuses to run |
| Modified `boot-<N>.fit` on FAT | `source` + `bootm` both verify the FIT config signature; mismatch routes to `mender_try_to_recover` |
| Modified rootfs blocks on `mmcblk0pX` | dm-verity hash tree authenticates each block; mismatched block returns EIO |
| Tampered Mender OTA artifact | Mender verifies signing key before applying |
| Userspace `fw_setenv bootcmd=...` (or other bootflow env vars) | `:sr` env flags reject the write |
| A/B swap to load an older signed rootfs | Mender state machine guards `mender_boot_part` transitions; rollback only honours the active slot |
| Half-written rootfs after power loss during OTA | dm-verity blocks read of corrupt sectors; bootcount triggers rollback to the still-good slot |

---

_[View the full topic](https://hub.mender.io/t/mender-cm4-dm-verity/8260)._
