mirror of
https://github.com/containerd/containerd.git
synced 2026-08-09 17:39:22 +00:00
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>
280 lines
8.1 KiB
Go
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
|
|
},
|
|
})
|
|
}
|