Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,10 @@ jobs:
run: |
sudo modprobe erofs

- name: Load dm-verity kernel module
run: |
sudo modprobe dm_verity

- name: Install containerd
env:
CGO_ENABLED: 1
Expand Down
78 changes: 71 additions & 7 deletions docs/snapshotters/erofs.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ quota. The `default_size` option can be used in the containerd configuration:

## Data Integrity

The EROFS snapshotter provides two methods to consolidate data integrity:
The EROFS snapshotter provides three methods to consolidate data integrity:

### Data Integrity with Immutable File Attribute

Expand Down Expand Up @@ -221,6 +221,65 @@ introduces additional runtime overhead since all container image reads from
the container will be slower because it needs to verify the Merkle hash tree
first.

### Data Integrity with dm-verity

The EROFS snapshotter supports device-mapper verity to provide block-level integrity
verification for each EROFS layer. This method creates a dm-verity device for each
layer and mounts it read-only. The dm-verity implementation uses the `go-dmverity`
Go library, eliminating the need for external `veritysetup` command-line tools.
This requires a Linux kernel with dm-verity support (CONFIG_DM_VERITY) and the
device-mapper kernel module loaded.

The differ must be configured to generate dm-verity metadata:

```toml
[plugins."io.containerd.differ.v1.erofs"]
enable_dmverity = true
```

When dm-verity is enabled, the EROFS differ formats each layer with dm-verity
by appending a Merkle hash tree to the EROFS blob and generating a root hash.
The hash tree is stored inline within the layer blob itself.
The root hash and hash offset are saved in a `.dmverity` metadata file alongside the
layer blob in JSON format. All other dm-verity parameters (block sizes, salt, etc.)
are stored in a superblock within the layer blob and are auto-detected when mounting.
Regular mode uses 4096-byte blocks (standard page size), while tar-index mode uses
512-byte blocks (dm-verity logical_block_size constraint).

The snapshotter can be configured to control dm-verity behavior using `dmverity_mode`:

```toml
[plugins."io.containerd.snapshotter.v1.erofs"]
dmverity_mode = "auto" # Options: "auto" (default), "on", "off"
```

The available modes are:

- `"auto"` (default): Uses dm-verity if `.dmverity` metadata exists for a layer,
otherwise mounts as regular EROFS. This allows mixing dm-verity and non-dm-verity
layers in the same system.

- `"on"`: Requires dm-verity for all layers. If a layer lacks `.dmverity` metadata,
mounting will fail with an error. Use this mode when you want to enforce integrity
verification for all layers.

> **Important**: If you enable `dmverity_mode = "on"` after layers have already been
> unpacked without dm-verity enabled in the differ, those existing layers will not
> have `.dmverity` metadata files. In this case, you must clean up the existing
> snapshots and re-pull the images with both `enable_dmverity = true` in the differ
> and `dmverity_mode = "on"` in the snapshotter configured. Alternatively, use
> `dmverity_mode = "auto"` to allow mixing dm-verity and non-dm-verity layers.

- `"off"`: Disables dm-verity completely, even if `.dmverity` metadata exists.
Layers are mounted as regular EROFS without integrity verification. Use this for
compatibility or when dm-verity overhead is unacceptable.

When mounting a layer with dm-verity enabled, the snapshotter reads the metadata
from the `.dmverity` file and creates a dm-verity device. The dm-verity library
automatically reads all parameters from the superblock, ensuring that any corruption
or tampering will be detected at read time. The dm-verity device is then mounted as
the backing layer in the OverlayFS stack

## How It Works

For each layer, the EROFS snapshotter prepares a directory containing the
Expand All @@ -247,9 +306,19 @@ In this case, the snapshot layer directory will look like this:
work
```

If dm-verity is enabled, a `.dmverity` metadata file will also be present:
```
.erofslayer
fs
layer.erofs
layer.erofs.dmverity
work
```

Then the EROFS snapshotter will check for the existence of `layer.erofs`: it
will mount the EROFS layer blob to `fs/` and return a valid overlayfs mount
with all parent layers.
with all parent layers. If dm-verity is enabled and the `.dmverity` file exists,
the snapshotter will create a dm-verity device and mount that instead.

If other differs (not the EROFS differ) are used, the EROFS snapshotter will
convert the flat directory into an EROFS layer blob on Commit instead.
Expand Down Expand Up @@ -279,8 +348,3 @@ For the EROFS differ:
[plugins."io.containerd.differ.v1.erofs"]
enable_tar_index = true
```

## TODO


- DMVerity support.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ require (
github.com/containerd/errdefs/pkg v0.3.0
github.com/containerd/fifo v1.1.0
github.com/containerd/go-cni v1.1.13
github.com/containerd/go-dmverity v0.0.0-20260106143538-e097b6cc4a33
github.com/containerd/go-runc v1.1.0
github.com/containerd/imgcrypt/v2 v2.0.2
github.com/containerd/log v0.1.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY
github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o=
github.com/containerd/go-cni v1.1.13 h1:eFSGOKlhoYNxpJ51KRIMHZNlg5UgocXEIEBGkY7Hnis=
github.com/containerd/go-cni v1.1.13/go.mod h1:nTieub0XDRmvCZ9VI/SBG6PyqT95N4FIhxsauF1vSBI=
github.com/containerd/go-dmverity v0.0.0-20260106143538-e097b6cc4a33 h1:uBWSkYhocTHv/UYSMPMScQl4nbSNtaCVA8EXJ/i+ZOw=
github.com/containerd/go-dmverity v0.0.0-20260106143538-e097b6cc4a33/go.mod h1:Su0PfhcZ1JsxHWSk8lTncpzC28QO2W6cvaWx7WkjQuo=
github.com/containerd/go-runc v1.1.0 h1:OX4f+/i2y5sUT7LhmcJH7GYrjjhHa1QI4e8yO0gGleA=
github.com/containerd/go-runc v1.1.0/go.mod h1:xJv2hFF7GvHtTJd9JqTS2UVxMkULUYw4JN5XAUZqH5U=
github.com/containerd/imgcrypt/v2 v2.0.2 h1:WOEaE33CaSxzuRF8YLfAjHWuu1Xh27aPPQtqtALqfuM=
Expand Down
89 changes: 89 additions & 0 deletions internal/dmverity/dmverity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
Copyright The containerd Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Package dmverity provides functions for working with dm-verity for integrity verification
// using the veritysetup-go library
package dmverity

import (
"encoding/json"
"fmt"
"os"
)

type DmverityOptions struct {
// Salt for hashing, represented as a hex string
Salt string
// Hash algorithm to use (default: sha256)
HashAlgorithm string
// Size of data blocks in bytes (default: 4096)
DataBlockSize uint32
// Size of hash blocks in bytes (default: 4096)
HashBlockSize uint32
// Number of data blocks
DataBlocks uint64
// Offset of hash area in bytes
HashOffset uint64
// Hash type (default: 1)
HashType uint32
// NoSuperblock disables superblock usage (matches library's NoSuperblock field)
NoSuperblock bool
// UUID for device to use
UUID string
}

func DefaultDmverityOptions() *DmverityOptions {
return &DmverityOptions{
Salt: "0000000000000000000000000000000000000000000000000000000000000000",
HashAlgorithm: "sha256",
DataBlockSize: 4096,
HashBlockSize: 4096,
HashType: 1,
NoSuperblock: false, // By default, use superblock
}
}

func MetadataPath(layerBlobPath string) string {
return layerBlobPath + ".dmverity"
}

func DevicePath(name string) string {
return fmt.Sprintf("/dev/mapper/%s", name)
}

type DmverityMetadata struct {
RootHash string `json:"roothash"`
HashOffset uint64 `json:"hashoffset"`
}

func ReadMetadata(layerBlobPath string) (*DmverityMetadata, error) {
metadataPath := MetadataPath(layerBlobPath)
data, err := os.ReadFile(metadataPath)
if err != nil {
return nil, fmt.Errorf("failed to read metadata file %q: %w", metadataPath, err)
}

var metadata DmverityMetadata
if err := json.Unmarshal(data, &metadata); err != nil {
return nil, fmt.Errorf("failed to parse metadata file %q: %w", metadataPath, err)
}

if metadata.RootHash == "" {
return nil, fmt.Errorf("missing root hash in metadata file %q", metadataPath)
}

return &metadata, nil
}
Loading
Loading