Files
containerd/plugins/mount/erofs/plugin_linux.go
Alex Lyn 31ca428580 snapshots/erofs: pass dm-verity metadata path via mount options
Update the EROFS snapshotter to pass the dm-verity metadata path
through the `X-containerd.dmverity` mount option. Previously, the
system relied on implicit mode detection; providing the explicit
path allows downstream components (such as Kata Containers mount
handlers) to reliably locate and read `.dmverity` files.

Correspondingly, update the EROFS mount handler to parse and utilize
this explicit metadata path for dm-verity device initialization.
This enables the runtime to retrieve root hashes and other necessary
metadata directly, ensuring robust layer integrity verification.

Signed-off-by: Alex Lyn <alex.lyn@antgroup.com>
2026-04-25 18:34:57 +08:00

280 lines
8.1 KiB
Go

/*
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 erofs
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/containerd/log"
"github.com/containerd/platforms"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/containerd/v2/internal/dmverity"
"github.com/containerd/containerd/v2/internal/fsmount"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/errdefs"
"golang.org/x/sys/unix"
)
var forceloop bool
type erofsMountHandler struct{}
// NewErofsMountHandler creates a new EROFS mount handler that supports dm-verity
func NewErofsMountHandler() mount.Handler {
return &erofsMountHandler{}
}
func (h *erofsMountHandler) Mount(ctx context.Context, m mount.Mount, mp string, _ []mount.ActiveMount) (_ mount.ActiveMount, retErr error) {
if m.Type != "erofs" {
return mount.ActiveMount{}, errdefs.ErrNotImplemented
}
var dmverityDevice string
defer func() {
if retErr == nil || dmverityDevice == "" {
return
}
dmverity.Close(dmverityDevice)
}()
// Parse dm-verity metadata path from mount options
metadataPath := ""
for _, opt := range m.Options {
if path, ok := strings.CutPrefix(opt, "X-containerd.dmverity="); ok {
metadataPath = path
break
}
}
// Set up dm-verity device if metadata path is provided
if metadataPath != "" {
log.G(ctx).WithFields(log.Fields{
"source": m.Source,
"metadata-path": metadataPath,
}).Debug("setting up dm-verity device from mount option")
metadata, err := dmverity.ReadMetadata(metadataPath)
if err != nil {
return mount.ActiveMount{}, fmt.Errorf("failed to read dm-verity metadata from %s: %w", metadataPath, err)
}
devicePath, cleanupName, err := setupDmVerityDevice(ctx, m.Source, metadata)
dmverityDevice = cleanupName
if err != nil {
return mount.ActiveMount{}, err
}
m.Source = devicePath
}
filteredOptions := make([]string, 0, len(m.Options))
for _, v := range m.Options {
// Skip loop option (handled by loop device setup) and dmverity options (already processed)
if v == "loop" || strings.HasPrefix(v, "X-containerd.dmverity=") {
continue
}
filteredOptions = append(filteredOptions, v)
}
m.Options = filteredOptions
if err := os.MkdirAll(mp, 0700); err != nil {
return mount.ActiveMount{}, err
}
err := error(unix.ENOTBLK)
if !forceloop {
// Try to use file-backed mount feature if available (Linux 6.12+) first
err = doMount(m, mp)
}
if errors.Is(err, unix.ENOTBLK) {
var loops []*os.File
// Never try to mount with raw files anymore if tried
forceloop = true
params := mount.LoopParams{
Readonly: true,
Autoclear: true,
}
// set up all loop devices
loop, err := mount.SetupLoop(m.Source, params)
if err != nil {
return mount.ActiveMount{}, err
}
m.Source = loop.Name()
loops = append(loops, loop)
defer func() {
for _, loop := range loops {
loop.Close()
}
}()
for i, v := range m.Options {
// Convert raw files in `device=` into loop devices too
if after, ok := strings.CutPrefix(v, "device="); ok {
loop, err := mount.SetupLoop(after, params)
if err != nil {
return mount.ActiveMount{}, err
}
m.Options[i] = "device=" + loop.Name()
loops = append(loops, loop)
}
}
err = doMount(m, mp)
if err != nil {
return mount.ActiveMount{}, err
}
} else if err != nil {
return mount.ActiveMount{}, err
}
t := time.Now()
return mount.ActiveMount{
Mount: m,
MountedAt: &t,
MountPoint: mp,
}, nil
}
// setupDmVerityDevice creates or reuses a dm-verity device for the given EROFS source.
// It returns the device path to mount from, and a cleanup name (non-empty only when
// a new device was created, so the caller can close it on error).
func setupDmVerityDevice(ctx context.Context, source string, metadata *dmverity.DmverityMetadata) (devicePath string, cleanupName string, err error) {
supported, err := dmverity.IsSupported()
if err != nil || !supported {
return "", "", fmt.Errorf("layer requires dm-verity but system doesn't support it (dm_verity module not loaded): %w", err)
}
// Extract snapshot ID from source path
// Path format: {root}/snapshots/{id}/layer.erofs
snapshotID := filepath.Base(filepath.Dir(source))
deviceName := fmt.Sprintf("containerd-erofs-%s", snapshotID)
devicePath = dmverity.DevicePath(deviceName)
log.G(ctx).WithFields(log.Fields{
"source": source,
"device-name": deviceName,
"hash-offset": metadata.HashOffset,
}).Debug("opening dm-verity device")
// Try to create dm-verity device first (avoids TOCTOU race)
_, err = dmverity.Open(source, deviceName, source, metadata.RootHash, metadata.HashOffset, nil)
if err == nil {
// New device created — wait for it to appear and return cleanupName
// so the caller can close it on error.
if waitErr := waitForDevice(devicePath); waitErr != nil {
return "", deviceName, waitErr
}
log.G(ctx).WithField("device", devicePath).Debug("dm-verity device created successfully")
return devicePath, deviceName, nil
}
// Open failed — check if the device already exists and can be reused.
if _, statErr := os.Stat(devicePath); statErr != nil {
return "", "", fmt.Errorf("failed to open dm-verity device: %w", err)
}
if verifyErr := dmverity.VerifyDevice(deviceName, metadata.RootHash); verifyErr != nil {
return "", "", fmt.Errorf("existing dm-verity device %q verification failed: %w", deviceName, verifyErr)
}
log.G(ctx).WithField("device", devicePath).Debug("dm-verity device already exists and verified, reusing")
return devicePath, "", nil
}
// waitForDevice polls for the device node to appear, returning an error if it
// does not appear within a reasonable timeout.
func waitForDevice(devicePath string) error {
for range 100 {
if _, err := os.Stat(devicePath); err == nil {
return nil
}
time.Sleep(10 * time.Millisecond)
}
if _, err := os.Stat(devicePath); err != nil {
return fmt.Errorf("dm-verity device %q not found after creation: %w", devicePath, err)
}
return nil
}
func doMount(m mount.Mount, target string) error {
if err := fsmount.Fsmount(m, target); err != nil {
// Fall back to traditional mount() if fsmount syscall not available (Linux < 5.2)
if errors.Is(err, unix.ENOSYS) {
log.L.WithError(err).Debug("fsmount not available, falling back to traditional mount")
return m.Mount(target)
}
return err
}
return nil
}
func (h *erofsMountHandler) Unmount(ctx context.Context, path string) error {
// Check what's currently mounted to determine if dm-verity device cleanup is needed
var deviceName string
mountInfo, err := mount.Lookup(path)
if err == nil {
source := mountInfo.Source
if strings.HasPrefix(source, "/dev/mapper/containerd-erofs-") {
deviceName = strings.TrimPrefix(source, "/dev/mapper/")
}
}
err = mount.Unmount(path, 0)
if deviceName != "" {
log.G(ctx).WithFields(log.Fields{
"mount-point": path,
"device": deviceName,
}).Debug("attempting to close dm-verity device")
if closeErr := dmverity.Close(deviceName); closeErr != nil {
log.G(ctx).WithError(closeErr).WithField("device", deviceName).Debug("unable to close dm-verity device")
} else {
log.G(ctx).WithField("device", deviceName).Debug("dm-verity device closed successfully")
}
}
return err
}
type Config struct{}
func init() {
registry.Register(&plugin.Registration{
Type: plugins.MountHandlerPlugin,
ID: "erofs",
Config: &Config{},
InitFn: func(ic *plugin.InitContext) (any, error) {
p := platforms.DefaultSpec()
p.OS = runtime.GOOS
ic.Meta.Platforms = append(ic.Meta.Platforms, p)
return NewErofsMountHandler(), nil
},
})
}