Skip to content
Draft
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
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,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
146 changes: 146 additions & 0 deletions core/mount/manager/dmverity_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
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 manager

import (
"context"
"fmt"
"os"
"strings"
"time"

"github.com/containerd/errdefs"
"github.com/containerd/log"

"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/containerd/v2/internal/dmverity"
)

const (
// prefixDmverity is the option prefix for dm-verity specific options
prefixDmverity = "X-containerd.dmverity."
)

// parseDmverityMountOptions extracts dm-verity parameters from mount options
// Returns the root hash, device name, hash offset, and filtered regular options
func parseDmverityMountOptions(mountOptions []string) (string, string, uint64, []string, error) {
var rootHash string
var deviceName string
var hashOffset uint64
var regularOptions []string

for _, o := range mountOptions {
if dmverityOption, isDmverity := strings.CutPrefix(o, prefixDmverity); isDmverity {
key, value, ok := strings.Cut(dmverityOption, "=")
if !ok {
// Ignore unknown boolean flags for forward compatibility
continue
}
switch key {
case "device-name":
deviceName = value
case "roothash":
rootHash = value
case "hash-offset":
var offset uint64
if _, err := fmt.Sscanf(value, "%d", &offset); err != nil {
return "", "", 0, nil, fmt.Errorf("invalid hash-offset value %q: %w", value, errdefs.ErrInvalidArgument)
}
hashOffset = offset
default:
return "", "", 0, nil, fmt.Errorf("unknown dmverity option %q: %w", key, errdefs.ErrInvalidArgument)
}
} else {
regularOptions = append(regularOptions, o)
}
}

return rootHash, deviceName, hashOffset, regularOptions, nil
}

// dmverityTransformer is a mount transformer that sets up dm-verity devices
// for integrity verification. It reads dm-verity options from mount options
// and creates a read-only device-mapper target.
type dmverityTransformer struct{}

func (dmverityTransformer) Transform(ctx context.Context, m mount.Mount, a []mount.ActiveMount) (mount.Mount, error) {
log.G(ctx).Debugf("transforming dmverity mount: %+v", m)

supported, err := dmverity.IsSupported()
if err != nil {
return mount.Mount{}, fmt.Errorf("dm-verity support check failed: %w", err)
}
if !supported {
return mount.Mount{}, fmt.Errorf("dm-verity is not supported on this system: veritysetup not available or dm_verity module not loaded: %w", errdefs.ErrNotImplemented)
}

// Parse dm-verity options from mount options
rootHash, deviceName, hashOffset, regularOptions, err := parseDmverityMountOptions(m.Options)
if err != nil {
return mount.Mount{}, err
}

// Generate device name if not specified
if deviceName == "" {
deviceName = fmt.Sprintf("dmverity-%d", time.Now().UnixNano())
}

// Check if device already exists (for layer reuse)
devicePath := dmverity.DevicePath(deviceName)
if _, err := os.Stat(devicePath); err == nil {
log.G(ctx).WithField("device", devicePath).Debug("dm-verity device already exists, reusing")
m.Source = devicePath
m.Options = regularOptions
return m, nil
}

// Create dm-verity device
log.G(ctx).WithFields(log.Fields{
"source": m.Source,
"device-name": deviceName,
"hash-offset": hashOffset,
}).Debug("opening dm-verity device")

devicePath, err = dmverity.Open(m.Source, deviceName, m.Source, rootHash, hashOffset, nil)
if err != nil {
return mount.Mount{}, fmt.Errorf("failed to open dm-verity device: %w", err)
}

// Wait for device to appear
for i := 0; i < 100; i++ {
if _, err := os.Stat(devicePath); err == nil {
break
}
time.Sleep(10 * time.Millisecond)
}

// Verify device exists
if _, err := os.Stat(devicePath); err != nil {
// Try to close the device we just created
if closeErr := dmverity.Close(deviceName); closeErr != nil {
log.G(ctx).WithError(closeErr).Warn("failed to cleanup dm-verity device after creation failure")
}
return mount.Mount{}, fmt.Errorf("dm-verity device %q not found after creation: %w", devicePath, err)
}

log.G(ctx).WithField("device", devicePath).Info("dm-verity device created successfully")

// Return updated mount pointing to dm-verity device
m.Source = devicePath
m.Options = regularOptions
return m, nil
}
145 changes: 145 additions & 0 deletions core/mount/manager/dmverity_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
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 manager

import (
"context"
"strings"
"testing"

"github.com/containerd/log/logtest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/containerd/containerd/v2/core/mount"
)

// TestDmverityTransformer tests the core transformer functionality
func TestDmverityTransformer(t *testing.T) {
ctx := logtest.WithT(context.Background(), t)
tr := dmverityTransformer{}

// Test parsing valid options with roothash, device-name, and hash-offset
t.Run("parses valid dmverity options", func(t *testing.T) {
rootHash, deviceName, hashOffset, regularOpts, err := parseDmverityMountOptions([]string{
"ro",
"X-containerd.dmverity.roothash=abc123def456789012345678901234567890123456789012345678901234",
"X-containerd.dmverity.device-name=test-device",
"X-containerd.dmverity.hash-offset=12288",
})
require.NoError(t, err)
assert.Equal(t, "abc123def456789012345678901234567890123456789012345678901234", rootHash)
assert.Equal(t, "test-device", deviceName)
assert.Equal(t, uint64(12288), hashOffset)
assert.Equal(t, []string{"ro"}, regularOpts)
})

// Test unknown dmverity option with = sign
t.Run("rejects unknown dmverity key-value options", func(t *testing.T) {
_, _, _, _, err := parseDmverityMountOptions([]string{
"ro",
"X-containerd.dmverity.unknown=value",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "unknown dmverity option")
})

// Test unknown boolean flag (should be ignored for forward compatibility)
t.Run("ignores unknown dmverity boolean flags", func(t *testing.T) {
rootHash, deviceName, hashOffset, regularOpts, err := parseDmverityMountOptions([]string{
"ro",
"X-containerd.dmverity.roothash=abc123def456789012345678901234567890123456789012345678901234",
"X-containerd.dmverity.unknown-flag",
})
require.NoError(t, err)
assert.Equal(t, "abc123def456789012345678901234567890123456789012345678901234", rootHash)
assert.Equal(t, "", deviceName)
assert.Equal(t, uint64(0), hashOffset)
assert.Equal(t, []string{"ro"}, regularOpts)
})

// Test invalid hash-offset value
t.Run("rejects invalid hash-offset", func(t *testing.T) {
_, _, _, _, err := parseDmverityMountOptions([]string{
"ro",
"X-containerd.dmverity.hash-offset=invalid",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid hash-offset value")
})

// Test regular options are preserved
t.Run("preserves regular mount options", func(t *testing.T) {
rootHash, deviceName, hashOffset, regularOpts, err := parseDmverityMountOptions([]string{
"ro",
"nodev",
"X-containerd.dmverity.roothash=abc123",
"X-containerd.dmverity.device-name=test",
"X-containerd.dmverity.hash-offset=16384",
"nosuid",
})
require.NoError(t, err)
assert.Equal(t, "abc123", rootHash)
assert.Equal(t, "test", deviceName)
assert.Equal(t, uint64(16384), hashOffset)
assert.Equal(t, []string{"ro", "nodev", "nosuid"}, regularOpts)
})

// Test dmverity options are filtered out from regular options
t.Run("filters dmverity options from mount options", func(t *testing.T) {
_, _, _, regularOpts, err := parseDmverityMountOptions([]string{
"ro",
"X-containerd.dmverity.roothash=abc123",
"noatime",
"X-containerd.dmverity.device-name=test",
"nodev",
})
require.NoError(t, err)
assert.Equal(t, []string{"ro", "noatime", "nodev"}, regularOpts)
// Ensure no dmverity options are in regular options
for _, opt := range regularOpts {
assert.False(t, strings.HasPrefix(opt, "X-containerd.dmverity."))
}
})

// Test empty options
t.Run("handles empty options", func(t *testing.T) {
rootHash, deviceName, hashOffset, regularOpts, err := parseDmverityMountOptions([]string{})
require.NoError(t, err)
assert.Equal(t, "", rootHash)
assert.Equal(t, "", deviceName)
assert.Equal(t, uint64(0), hashOffset)
assert.Empty(t, regularOpts)
})

// Test Transform will fail without actual device (expected)
t.Run("Transform fails with non-existent source device", func(t *testing.T) {
m := mount.Mount{
Source: "/path/to/nonexistent.erofs",
Type: "erofs",
Options: []string{
"ro",
"X-containerd.dmverity.roothash=abc123def456789012345678901234567890123456789012345678901234",
"X-containerd.dmverity.device-name=test-device",
},
}
_, err := tr.Transform(ctx, m, nil)
require.Error(t, err)
// Should fail because device doesn't exist, not because of parsing
assert.NotContains(t, err.Error(), "unknown dmverity option")
})
}
33 changes: 33 additions & 0 deletions core/mount/manager/dmverity_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//go:build !linux

/*
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 manager

import (
"context"
"fmt"

"github.com/containerd/containerd/v2/core/mount"
)

// dmverityTransformer is a no-op transformer for non-Linux platforms
type dmverityTransformer struct{}

func (dmverityTransformer) Transform(ctx context.Context, m mount.Mount, a []mount.ActiveMount) (mount.Mount, error) {
return mount.Mount{}, fmt.Errorf("dm-verity is not supported on this platform")
}
1 change: 1 addition & 0 deletions core/mount/manager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ func (mm *mountManager) Activate(ctx context.Context, name string, mounts []moun
"mkdir": &mkdir{
rootMap: mm.rootMap,
},
"dmverity": dmverityTransformer{},
}

start := time.Now()
Expand Down
Loading
Loading