Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
107a5f6
internal:dmverity: Implement dmverity functionality
ChengyuZhu6 May 19, 2025
dcefa2e
Improve error handling
aadhar-agarwal Oct 9, 2025
091104c
Improve IsSupported fn error handling
aadhar-agarwal Oct 10, 2025
50f861c
Validate dm verity options and root hash
aadhar-agarwal Oct 10, 2025
c8310fc
Ensure order of dm verity options is consistent
aadhar-agarwal Oct 10, 2025
dba744d
Remove extra comment
aadhar-agarwal Oct 10, 2025
2011872
add root hash file option
aadhar-agarwal Oct 10, 2025
1ce79e0
Improve testing
aadhar-agarwal Oct 10, 2025
bf1a083
Each command has specific options
aadhar-agarwal Oct 11, 2025
99a8f9c
Unify the options usage and prevent localization issues
aadhar-agarwal Oct 16, 2025
be93f84
Execute command with context to prevent goroutine leak, better handle…
aadhar-agarwal Oct 16, 2025
6116d15
Load dm_verity module
aadhar-agarwal Oct 16, 2025
c004710
Verify veritysetup version
aadhar-agarwal Oct 16, 2025
26d9cea
We only use the roothash from the veritysetup format command
aadhar-agarwal Oct 20, 2025
0f0041d
Open can also use root hash file
aadhar-agarwal Oct 20, 2025
8d0f6f8
Initial commit to integrate dm-verity support into the erofs snapshotter
aadhar-agarwal Oct 15, 2025
88c365f
Initial commit to address comments
aadhar-agarwal Oct 15, 2025
7a6f889
Do not allow fsverity and dmverity to be enabled at the same time
aadhar-agarwal Oct 16, 2025
b69f2b3
Improve function name
aadhar-agarwal Oct 16, 2025
4525e09
Keep setImmutable and maintain order
aadhar-agarwal Oct 17, 2025
f1c80b4
Do not allow immutable and dmverity to be enabled at the same time
aadhar-agarwal Oct 17, 2025
a68615e
Update calls to getDmverityDevicePath
aadhar-agarwal Oct 17, 2025
d3ac12d
Use root hash file
aadhar-agarwal Oct 20, 2025
965f7e7
Do not need .dmverity as well
aadhar-agarwal Oct 20, 2025
e0c627d
Add comment for file truncating and simplify Close function
aadhar-agarwal Oct 20, 2025
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
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,14 @@ jobs:
run: |
sudo modprobe erofs

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

- name: Verify veritysetup version
run: |
veritysetup --version

- name: Install containerd
env:
CGO_ENABLED: 1
Expand Down
174 changes: 174 additions & 0 deletions internal/dmverity/dmverity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
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
package dmverity

import (
"bufio"
"bytes"
"encoding/hex"
"fmt"
"os"
"strings"

"github.com/containerd/log"
)

// VeritySetupCommand represents the type of veritysetup command to execute
type VeritySetupCommand string

const (
// FormatCommand corresponds to "veritysetup format"
FormatCommand VeritySetupCommand = "format"
// OpenCommand corresponds to "veritysetup open"
OpenCommand VeritySetupCommand = "open"
// CloseCommand corresponds to "veritysetup close"
CloseCommand VeritySetupCommand = "close"
)

// DmverityOptions contains configuration options for dm-verity operations
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
// Superblock usage flag (false meaning --no-superblock)
UseSuperblock bool
// Debug flag
Debug bool
// UUID for device to use
UUID string
// RootHashFile specifies a file path where the root hash should be saved
RootHashFile string
}

// DefaultDmverityOptions returns a DmverityOptions struct with default values
func DefaultDmverityOptions() DmverityOptions {
return DmverityOptions{
Salt: "0000000000000000000000000000000000000000000000000000000000000000",
HashAlgorithm: "sha256",
DataBlockSize: 4096,
HashBlockSize: 4096,
HashType: 1,
UseSuperblock: true,
}
}

// ValidateOptions validates dm-verity options to ensure they are valid
// before being passed to veritysetup commands
func ValidateOptions(opts *DmverityOptions) error {
if opts == nil {
return fmt.Errorf("options cannot be nil")
}

// Validate block sizes are power of 2 (kernel requirement)
if opts.DataBlockSize > 0 {
if opts.DataBlockSize&(opts.DataBlockSize-1) != 0 {
return fmt.Errorf("data block size %d must be a power of 2", opts.DataBlockSize)
}
}

if opts.HashBlockSize > 0 {
if opts.HashBlockSize&(opts.HashBlockSize-1) != 0 {
return fmt.Errorf("hash block size %d must be a power of 2", opts.HashBlockSize)
}
}

// Validate salt format (must be hex string)
if opts.Salt != "" {
if _, err := hex.DecodeString(opts.Salt); err != nil {
return fmt.Errorf("salt must be a valid hex string: %w", err)
}
}

return nil
}

// ValidateRootHash validates that a root hash string is in valid hexadecimal format
func ValidateRootHash(rootHash string) error {
if rootHash == "" {
return fmt.Errorf("root hash cannot be empty")
}

// Validate root hash (must be hex string)
if _, err := hex.DecodeString(rootHash); err != nil {
return fmt.Errorf("root hash must be a valid hex string: %w", err)
}

return nil
}

// ExtractRootHash extracts the root hash from veritysetup format command output.
// It first attempts to read from the root hash file (if specified in opts.RootHashFile),
// then falls back to parsing the stdout output.
//
// Note: This function expects English output when parsing stdout. The calling code
// ensures veritysetup runs with LC_ALL=C and LANG=C to prevent localization issues.
func ExtractRootHash(output string, opts *DmverityOptions) (string, error) {
log.L.Debugf("veritysetup format output:\n%s", output)

var rootHash string

// Try to read from root hash file first (if specified)
if opts != nil && opts.RootHashFile != "" {
hashBytes, err := os.ReadFile(opts.RootHashFile)
if err != nil {
return "", fmt.Errorf("failed to read root hash from file %q: %w", opts.RootHashFile, err)
}
// Trim any whitespace/newlines
rootHash = string(bytes.TrimSpace(hashBytes))
} else {
// Parse stdout output to find the root hash
if output == "" {
return "", fmt.Errorf("output is empty")
}

scanner := bufio.NewScanner(strings.NewReader(output))
for scanner.Scan() {
line := scanner.Text()
// Look for the "Root hash:" line
if strings.HasPrefix(line, "Root hash:") {
parts := strings.Split(line, ":")
if len(parts) == 2 {
rootHash = strings.TrimSpace(parts[1])
break
}
}
}

if err := scanner.Err(); err != nil {
return "", fmt.Errorf("error scanning output: %w", err)
}
}

// Validate root hash
if err := ValidateRootHash(rootHash); err != nil {
return "", fmt.Errorf("root hash is invalid: %w", err)
}

return rootHash, nil
}
192 changes: 192 additions & 0 deletions internal/dmverity/dmverity_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/*
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

import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"time"
)

const (
// veritysetupTimeout is the maximum time allowed for a veritysetup command to complete
// Format operations can take time for large devices, but should complete within 5 minutes
veritysetupTimeout = 5 * time.Minute
)

func IsSupported() (bool, error) {
moduleData, err := os.ReadFile("/proc/modules")
if err != nil {
return false, fmt.Errorf("failed to read /proc/modules: %w", err)
}
if !bytes.Contains(moduleData, []byte("dm_verity")) {
return false, fmt.Errorf("dm_verity module not loaded")
}

veritysetupPath, err := exec.LookPath("veritysetup")
if err != nil {
return false, fmt.Errorf("veritysetup not found in PATH: %w", err)
}

cmd := exec.Command(veritysetupPath, "--version")
if _, err := cmd.CombinedOutput(); err != nil {
return false, fmt.Errorf("veritysetup not functional: %w", err)
}

return true, nil
}

// runVeritySetup executes a veritysetup command with the given arguments and options
func actions(cmd VeritySetupCommand, args []string, opts *DmverityOptions) (string, error) {
cmdArgs := []string{string(cmd)}

if opts == nil {
defaultOpts := DefaultDmverityOptions()
opts = &defaultOpts
}

// Validate options before building command
if err := ValidateOptions(opts); err != nil {
return "", fmt.Errorf("invalid dm-verity options: %w", err)
}

// Apply options based on command type according to veritysetup man page
switch cmd {
case FormatCommand:
// FORMAT options: --hash, --no-superblock, --format, --data-block-size,
// --hash-block-size, --data-blocks, --hash-offset, --salt, --uuid, --root-hash-file
if opts.Salt != "" {
cmdArgs = append(cmdArgs, fmt.Sprintf("--salt=%s", opts.Salt))
}
if opts.HashAlgorithm != "" {
cmdArgs = append(cmdArgs, fmt.Sprintf("--hash=%s", opts.HashAlgorithm))
}
if opts.DataBlockSize > 0 {
cmdArgs = append(cmdArgs, fmt.Sprintf("--data-block-size=%d", opts.DataBlockSize))
}
if opts.HashBlockSize > 0 {
cmdArgs = append(cmdArgs, fmt.Sprintf("--hash-block-size=%d", opts.HashBlockSize))
}
if opts.DataBlocks > 0 {
cmdArgs = append(cmdArgs, fmt.Sprintf("--data-blocks=%d", opts.DataBlocks))
}
if opts.HashOffset > 0 {
cmdArgs = append(cmdArgs, fmt.Sprintf("--hash-offset=%d", opts.HashOffset))
}
if opts.HashType == 0 {
cmdArgs = append(cmdArgs, fmt.Sprintf("--format=%d", opts.HashType))
}
if !opts.UseSuperblock {
cmdArgs = append(cmdArgs, "--no-superblock")
}
if opts.UUID != "" {
cmdArgs = append(cmdArgs, fmt.Sprintf("--uuid=%s", opts.UUID))
}
if opts.RootHashFile != "" {
cmdArgs = append(cmdArgs, fmt.Sprintf("--root-hash-file=%s", opts.RootHashFile))
}

case OpenCommand:
// OPEN options: --hash-offset, --no-superblock, --root-hash-file
// (ignoring advanced options we don't have: --ignore-corruption, --restart-on-corruption,
// --panic-on-corruption, --ignore-zero-blocks, --check-at-most-once,
// --root-hash-signature, --use-tasklets, --shared)
if opts.HashOffset > 0 {
cmdArgs = append(cmdArgs, fmt.Sprintf("--hash-offset=%d", opts.HashOffset))
}
if !opts.UseSuperblock {
cmdArgs = append(cmdArgs, "--no-superblock")
}
if opts.RootHashFile != "" {
cmdArgs = append(cmdArgs, fmt.Sprintf("--root-hash-file=%s", opts.RootHashFile))
}

case CloseCommand:
// CLOSE has minimal options (--deferred, --cancel-deferred not implemented)
// No options from DmverityOptions apply to close
}

// Debug is not command-specific, can be used with any command
if opts.Debug {
cmdArgs = append(cmdArgs, "--debug")
}

cmdArgs = append(cmdArgs, args...)

ctx, cancel := context.WithTimeout(context.Background(), veritysetupTimeout)
defer cancel()

execCmd := exec.CommandContext(ctx, "veritysetup", cmdArgs...)
// Force C locale to ensure consistent, non-localized output that we can parse reliably
// This prevents localization issues where field names like "Root hash", "Salt", etc.
// might be translated to other languages, breaking our text parsing
execCmd.Env = append(os.Environ(), "LC_ALL=C", "LANG=C")
output, err := execCmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("veritysetup %s failed: %w, output: %s", cmd, err, string(output))
}

return string(output), nil
}

// Format creates a dm-verity hash for a data device and returns the root hash.
// If hashDevice is the same as dataDevice, the hash will be stored on the same device.
func Format(dataDevice, hashDevice string, opts *DmverityOptions) (string, error) {
args := []string{dataDevice, hashDevice}
output, err := actions(FormatCommand, args, opts)
if err != nil {
return "", fmt.Errorf("failed to format dm-verity device: %w, output: %s", err, output)
}

// Extract the root hash from the format output
// Pass opts so ExtractRootHash can read root hash from file if RootHashFile was specified
rootHash, err := ExtractRootHash(output, opts)
if err != nil {
return "", fmt.Errorf("failed to extract root hash: %w", err)
}

return rootHash, nil
}

// Open creates a read-only device-mapper target for transparent integrity verification
func Open(dataDevice string, name string, hashDevice string, rootHash string, opts *DmverityOptions) (string, error) {
var args []string
// If RootHashFile is provided, use the alternate open syntax without root hash as command arg
if opts != nil && opts.RootHashFile != "" {
args = []string{dataDevice, name, hashDevice}
} else {
args = []string{dataDevice, name, hashDevice, rootHash}
}
output, err := actions(OpenCommand, args, opts)
if err != nil {
return "", fmt.Errorf("failed to open dm-verity device: %w, output: %s", err, output)
}
return output, nil
}

// Close removes a dm-verity target and its underlying device from the device mapper table
func Close(name string) (string, error) {
args := []string{name}
output, err := actions(CloseCommand, args, nil)
if err != nil {
return "", fmt.Errorf("failed to close dm-verity device: %w, output: %s", err, output)
}
return output, nil
}
Loading