diff --git a/docs/snapshotters/erofs.md b/docs/snapshotters/erofs.md index d6871bc13c..427c3501b7 100644 --- a/docs/snapshotters/erofs.md +++ b/docs/snapshotters/erofs.md @@ -82,6 +82,13 @@ The following configuration can be used in your containerd `config.toml`. Don't forget to restart containerd after changing the configuration. ``` + [plugins."io.containerd.snapshotter.v1.erofs"] + # Enable fsverity support for EROFS layers, default is false + enable_fsverity = true + + # Optional: Additional mount options for overlayfs + ovl_mount_options = [] + [plugins."io.containerd.service.v1.diff-service"] default = ["erofs","walking"] ``` @@ -135,6 +142,13 @@ In other words, the EROFS differ can only be used with the EROFS snapshotter; otherwise, it will skip to the next differ. The EROFS snapshotter can work with or without the EROFS differ. +## Data Integrity with fsverity + +The EROFS snapshotter supports fsverity for data integrity verification of EROFS layers. +When enabled via `enable_fsverity = true`, the snapshotter will: +- Enable fsverity on EROFS layers during commit +- Verify fsverity status before mounting layers +- Skip fsverity if the filesystem or kernel does not support it ## TODO diff --git a/plugins/snapshots/erofs/erofs_linux.go b/plugins/snapshots/erofs/erofs_linux.go index 224418e595..67645f69ca 100644 --- a/plugins/snapshots/erofs/erofs_linux.go +++ b/plugins/snapshots/erofs/erofs_linux.go @@ -28,6 +28,7 @@ import ( "github.com/containerd/containerd/v2/core/mount" "github.com/containerd/containerd/v2/core/snapshots" "github.com/containerd/containerd/v2/core/snapshots/storage" + "github.com/containerd/containerd/v2/internal/fsverity" "github.com/containerd/containerd/v2/plugins/snapshots/erofs/erofsutils" "github.com/containerd/continuity/fs" "github.com/containerd/log" @@ -39,6 +40,8 @@ import ( type SnapshotterConfig struct { // ovlOptions are the base options added to the overlayfs mount (defaults to [""]) ovlOptions []string + // enableFsverity enables fsverity for EROFS layers + enableFsverity bool } // Opt is an option to configure the erofs snapshotter @@ -51,6 +54,13 @@ func WithOvlOptions(options []string) Opt { } } +// WithFsverity enables fsverity for EROFS layers +func WithFsverity() Opt { + return func(config *SnapshotterConfig) { + config.enableFsverity = true + } +} + type MetaStore interface { TransactionContext(ctx context.Context, writable bool) (context.Context, storage.Transactor, error) WithTransaction(ctx context.Context, writable bool, fn storage.TransactionCallback) error @@ -58,9 +68,10 @@ type MetaStore interface { } type snapshotter struct { - root string - ms *storage.MetaStore - ovlOptions []string + root string + ms *storage.MetaStore + ovlOptions []string + enableFsverity bool } // check if EROFS kernel filesystem is registered or not @@ -108,6 +119,17 @@ func NewSnapshotter(root string, opts ...Opt) (snapshots.Snapshotter, error) { return nil, fmt.Errorf("EROFS unsupported, please `modprobe erofs`: %w", plugin.ErrSkipPlugin) } + // Check fsverity support if enabled + if config.enableFsverity { + supported, err := fsverity.IsSupported(root) + if err != nil { + return nil, fmt.Errorf("failed to check fsverity support on %q: %w", root, err) + } + if !supported { + return nil, fmt.Errorf("fsverity is not supported on the filesystem of %q", root) + } + } + ms, err := storage.NewMetaStore(filepath.Join(root, "metadata.db")) if err != nil { return nil, err @@ -118,9 +140,10 @@ func NewSnapshotter(root string, opts ...Opt) (snapshots.Snapshotter, error) { } return &snapshotter{ - root: root, - ms: ms, - ovlOptions: config.ovlOptions, + root: root, + ms: ms, + ovlOptions: config.ovlOptions, + enableFsverity: config.enableFsverity, }, nil } @@ -182,12 +205,16 @@ func (s *snapshotter) mounts(snap storage.Snapshot, info snapshots.Info) ([]moun var options []string if len(snap.ParentIDs) == 0 { - // If the EROFS layer blob is valid, only snapshots.KindView is allowed. m, _, err := s.lowerPath(snap.ID) if err == nil { if snap.Kind != snapshots.KindView { return nil, fmt.Errorf("only works for snapshots.KindView on a committed snapshot: %w", err) } + if s.enableFsverity { + if err := s.verifyFsverity(m.Source); err != nil { + return nil, err + } + } // We have to force a loop device here since mount[] is static. m.Options = append(m.Options, "loop") return []mount.Mount{m}, nil @@ -369,6 +396,14 @@ func (s *snapshotter) Commit(ctx context.Context, name, key string, opts ...snap } } } + + // Enable fsverity on the EROFS layer if configured + if s.enableFsverity { + if err := fsverity.Enable(layerBlob); err != nil { + return fmt.Errorf("failed to enable fsverity: %w", err) + } + } + return nil }) @@ -536,3 +571,18 @@ func (s *snapshotter) Usage(ctx context.Context, key string) (_ snapshots.Usage, } return usage, nil } + +// Add a method to verify fsverity +func (s *snapshotter) verifyFsverity(path string) error { + if !s.enableFsverity { + return nil + } + enabled, err := fsverity.IsEnabled(path) + if err != nil { + return fmt.Errorf("failed to check fsverity status: %w", err) + } + if !enabled { + return fmt.Errorf("fsverity is not enabled on %s", path) + } + return nil +} diff --git a/plugins/snapshots/erofs/erofs_linux_test.go b/plugins/snapshots/erofs/erofs_linux_test.go index 5e66274b43..195cc41c93 100644 --- a/plugins/snapshots/erofs/erofs_linux_test.go +++ b/plugins/snapshots/erofs/erofs_linux_test.go @@ -18,11 +18,16 @@ package erofs import ( "context" + "os" "os/exec" + "path/filepath" "testing" + "github.com/containerd/containerd/v2/core/mount" "github.com/containerd/containerd/v2/core/snapshots" + "github.com/containerd/containerd/v2/core/snapshots/storage" "github.com/containerd/containerd/v2/core/snapshots/testsuite" + "github.com/containerd/containerd/v2/internal/fsverity" "github.com/containerd/containerd/v2/pkg/testutil" ) @@ -51,3 +56,78 @@ func TestErofs(t *testing.T) { testutil.RequiresRoot(t) testsuite.SnapshotterSuite(t, "erofs", newSnapshotter(t)) } + +func TestErofsFsverity(t *testing.T) { + testutil.RequiresRoot(t) + ctx := context.Background() + + root := t.TempDir() + + // Skip if fsverity is not supported + supported, err := fsverity.IsSupported(root) + if !supported || err != nil { + t.Skip("fsverity not supported, skipping test") + } + + // Create snapshotter with fsverity enabled + s, err := NewSnapshotter(root, WithFsverity()) + if err != nil { + t.Fatal(err) + } + defer s.Close() + + // Create a test snapshot + key := "test-snapshot" + mounts, err := s.Prepare(ctx, key, "") + if err != nil { + t.Fatal(err) + } + + target := filepath.Join(root, key) + if err := os.MkdirAll(target, 0755); err != nil { + t.Fatal(err) + } + if err := mount.All(mounts, target); err != nil { + t.Fatal(err) + } + defer testutil.Unmount(t, target) + + // Write test data + if err := os.WriteFile(filepath.Join(target, "foo"), []byte("test data"), 0777); err != nil { + t.Fatal(err) + } + + // Commit the snapshot + commitKey := "test-commit" + if err := s.Commit(ctx, commitKey, key); err != nil { + t.Fatal(err) + } + + snap := s.(*snapshotter) + + // Get the internal ID from the snapshotter + var id string + if err := snap.ms.WithTransaction(ctx, false, func(ctx context.Context) error { + id, _, _, err = storage.GetInfo(ctx, commitKey) + return err + }); err != nil { + t.Fatal(err) + } + + // Verify fsverity is enabled on the EROFS layer + + layerPath := snap.layerBlobPath(id) + + enabled, err := fsverity.IsEnabled(layerPath) + if err != nil { + t.Fatalf("Failed to check fsverity status: %v", err) + } + if !enabled { + t.Fatal("Expected fsverity to be enabled on committed layer") + } + + // Try to modify the layer file directly (should fail) + if err := os.WriteFile(layerPath, []byte("tampered data"), 0666); err == nil { + t.Fatal("Expected direct write to fsverity-enabled layer to fail") + } +} diff --git a/plugins/snapshots/erofs/plugin/plugin_linux.go b/plugins/snapshots/erofs/plugin/plugin_linux.go index 8263d5dc13..5fb5241380 100644 --- a/plugins/snapshots/erofs/plugin/plugin_linux.go +++ b/plugins/snapshots/erofs/plugin/plugin_linux.go @@ -33,6 +33,9 @@ type Config struct { // MountOptions are options used for the EROFS overlayfs mount OvlOptions []string `toml:"ovl_mount_options"` + + // EnableFsverity enables fsverity for EROFS layers + EnableFsverity bool `toml:"enable_fsverity"` } func init() { @@ -58,6 +61,10 @@ func init() { opts = append(opts, erofs.WithOvlOptions(config.OvlOptions)) } + if config.EnableFsverity { + opts = append(opts, erofs.WithFsverity()) + } + ic.Meta.Exports[plugins.SnapshotterRootDir] = root return erofs.NewSnapshotter(root, opts...) },