mirror of
https://github.com/containerd/containerd.git
synced 2026-08-09 09:33:06 +00:00
plugins/mount/erofs: use fsmount API to avoid PAGE_SIZE limit
The traditional mount() syscall has a PAGE_SIZE (typically 4KB) limit for mount options. Use the new mount API (fsopen/fsconfig/fsmount/ move_mount) introduced in Linux 5.2 to bypass this limitation. Fixed: #12662 Signed-off-by: ChengyuZhu6 <hudson@cyzhu.com>
This commit is contained in:
150
internal/fsmount/fsmount_linux.go
Normal file
150
internal/fsmount/fsmount_linux.go
Normal file
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
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 fsmount
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/mount"
|
||||
)
|
||||
|
||||
var mountAttrFlags = map[string]struct {
|
||||
clear bool
|
||||
flag int
|
||||
}{
|
||||
"ro": {false, unix.MOUNT_ATTR_RDONLY},
|
||||
"rw": {true, unix.MOUNT_ATTR_RDONLY},
|
||||
"nosuid": {false, unix.MOUNT_ATTR_NOSUID},
|
||||
"suid": {true, unix.MOUNT_ATTR_NOSUID},
|
||||
"nodev": {false, unix.MOUNT_ATTR_NODEV},
|
||||
"dev": {true, unix.MOUNT_ATTR_NODEV},
|
||||
"noexec": {false, unix.MOUNT_ATTR_NOEXEC},
|
||||
"exec": {true, unix.MOUNT_ATTR_NOEXEC},
|
||||
"noatime": {false, unix.MOUNT_ATTR_NOATIME},
|
||||
"atime": {true, unix.MOUNT_ATTR_NOATIME},
|
||||
"nodiratime": {false, unix.MOUNT_ATTR_NODIRATIME},
|
||||
"diratime": {true, unix.MOUNT_ATTR_NODIRATIME},
|
||||
"relatime": {false, unix.MOUNT_ATTR_RELATIME},
|
||||
"norelatime": {true, unix.MOUNT_ATTR_RELATIME},
|
||||
"strictatime": {false, unix.MOUNT_ATTR_STRICTATIME},
|
||||
"nostrictatime": {true, unix.MOUNT_ATTR_STRICTATIME},
|
||||
}
|
||||
|
||||
// Fsopen opens a filesystem context for configuration.
|
||||
func Fsopen(fsName string, flags int) (*os.File, error) {
|
||||
flags |= unix.FSOPEN_CLOEXEC
|
||||
fd, err := unix.Fsopen(fsName, flags)
|
||||
if err != nil {
|
||||
return nil, os.NewSyscallError("fsopen "+fsName, err)
|
||||
}
|
||||
return os.NewFile(uintptr(fd), "fscontext:"+fsName), nil
|
||||
}
|
||||
|
||||
// fsmount creates a mount fd from a filesystem context.
|
||||
func fsmount(fsctx *os.File, flags, mountAttrs int) (*os.File, error) {
|
||||
flags |= unix.FSMOUNT_CLOEXEC
|
||||
fd, err := unix.Fsmount(int(fsctx.Fd()), flags, mountAttrs)
|
||||
if err != nil {
|
||||
return nil, os.NewSyscallError("fsmount "+fsctx.Name(), err)
|
||||
}
|
||||
return os.NewFile(uintptr(fd), "fsmount:"+fsctx.Name()), nil
|
||||
}
|
||||
|
||||
// SupportsFsmount checks if the fsmount syscall is available (Linux 5.2+).
|
||||
func SupportsFsmount() bool {
|
||||
fd, err := unix.Fsopen("__nonexistent__", unix.FSOPEN_CLOEXEC)
|
||||
if err == unix.ENOSYS {
|
||||
return false
|
||||
}
|
||||
if fd >= 0 {
|
||||
unix.Close(fd)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Fsmount mounts the filesystem using the new mount API (fsopen/fsconfig/fsmount/move_mount).
|
||||
// This approach avoids the PAGE_SIZE limitation of traditional mount() syscall by setting
|
||||
// options individually via fsconfig() instead of passing them as a single string.
|
||||
func Fsmount(m mount.Mount, target string) error {
|
||||
fsctx, err := Fsopen(m.Type, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fsctx.Close()
|
||||
|
||||
// Check if "ro" option is present - must be set before source for read-only loop devices
|
||||
roFlag := false
|
||||
for _, o := range m.Options {
|
||||
if o == "ro" {
|
||||
roFlag = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if roFlag {
|
||||
if err := unix.FsconfigSetFlag(int(fsctx.Fd()), "ro"); err != nil {
|
||||
return fmt.Errorf("failed to set ro flag: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := unix.FsconfigSetString(int(fsctx.Fd()), "source", m.Source); err != nil {
|
||||
return fmt.Errorf("failed to set source: %w", err)
|
||||
}
|
||||
|
||||
var mountAttrs int
|
||||
for _, o := range m.Options {
|
||||
if f, ok := mountAttrFlags[o]; ok {
|
||||
if f.clear {
|
||||
mountAttrs &^= f.flag
|
||||
} else {
|
||||
mountAttrs |= f.flag
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle key=value options
|
||||
if key, val, ok := strings.Cut(o, "="); ok {
|
||||
if err := unix.FsconfigSetString(int(fsctx.Fd()), key, val); err != nil {
|
||||
return fmt.Errorf("failed to set string option %s=%s: %w", key, val, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle filesystem-specific flags
|
||||
if err := unix.FsconfigSetFlag(int(fsctx.Fd()), o); err != nil {
|
||||
return fmt.Errorf("failed to set flag %s: %w", o, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := unix.FsconfigCreate(int(fsctx.Fd())); err != nil {
|
||||
return fmt.Errorf("failed to create fs: %w", err)
|
||||
}
|
||||
|
||||
mfd, err := fsmount(fsctx, 0, mountAttrs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fsmount: %w", err)
|
||||
}
|
||||
defer mfd.Close()
|
||||
|
||||
if err := unix.MoveMount(int(mfd.Fd()), "", unix.AT_FDCWD, target, unix.MOVE_MOUNT_F_EMPTY_PATH); err != nil {
|
||||
return fmt.Errorf("failed to move mount: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -24,11 +24,13 @@ import (
|
||||
"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/fsmount"
|
||||
"github.com/containerd/containerd/v2/plugins"
|
||||
"github.com/containerd/errdefs"
|
||||
|
||||
@@ -64,7 +66,7 @@ func (erofsMountHandler) Mount(ctx context.Context, m mount.Mount, mp string, _
|
||||
var err error = unix.ENOTBLK
|
||||
if !forceloop {
|
||||
// Try to use file-backed mount feature if available (Linux 6.12+) first
|
||||
err = m.Mount(mp)
|
||||
err = doMount(m, mp)
|
||||
}
|
||||
if errors.Is(err, unix.ENOTBLK) {
|
||||
var loops []*os.File
|
||||
@@ -96,9 +98,10 @@ func (erofsMountHandler) Mount(ctx context.Context, m mount.Mount, mp string, _
|
||||
return mount.ActiveMount{}, err
|
||||
}
|
||||
m.Options[i] = "device=" + loop.Name()
|
||||
loops = append(loops, loop)
|
||||
}
|
||||
}
|
||||
err = m.Mount(mp)
|
||||
err = doMount(m, mp)
|
||||
if err != nil {
|
||||
return mount.ActiveMount{}, err
|
||||
}
|
||||
@@ -114,6 +117,18 @@ func (erofsMountHandler) Mount(ctx context.Context, m mount.Mount, mp string, _
|
||||
}, 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 (erofsMountHandler) Unmount(ctx context.Context, path string) error {
|
||||
return mount.Unmount(path, 0)
|
||||
}
|
||||
|
||||
249
plugins/mount/erofs/plugin_linux_test.go
Normal file
249
plugins/mount/erofs/plugin_linux_test.go
Normal file
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
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 (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/mount"
|
||||
"github.com/containerd/containerd/v2/internal/fsmount"
|
||||
"github.com/containerd/containerd/v2/pkg/testutil"
|
||||
snapshotserofs "github.com/containerd/containerd/v2/plugins/snapshots/erofs"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// TestFsmountLoopDevice tests fsmount with loop device
|
||||
func TestFsmountLoopDevice(t *testing.T) {
|
||||
testutil.RequiresRoot(t)
|
||||
|
||||
if _, err := exec.LookPath("mkfs.erofs"); err != nil {
|
||||
t.Skipf("mkfs.erofs not found: %v", err)
|
||||
}
|
||||
|
||||
if !snapshotserofs.FindErofs() {
|
||||
t.Skip("erofs kernel support not available")
|
||||
}
|
||||
|
||||
if !fsmount.SupportsFsmount() {
|
||||
t.Skip("fsmount syscall not available (requires Linux 5.2+)")
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
|
||||
layerPath := filepath.Join(tempDir, "test.erofs")
|
||||
layerDir := filepath.Join(tempDir, "layer_dir")
|
||||
if err := os.MkdirAll(layerDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create layer dir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(layerDir, "test.txt"), []byte("hello"), 0644); err != nil {
|
||||
t.Fatalf("failed to write test file: %v", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command("mkfs.erofs", layerPath, layerDir)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("failed to create erofs image: %v, output: %s", err, output)
|
||||
}
|
||||
|
||||
loopFile, err := mount.SetupLoop(layerPath, mount.LoopParams{
|
||||
Readonly: true,
|
||||
Autoclear: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to setup loop device: %v", err)
|
||||
}
|
||||
loopDev := loopFile.Name()
|
||||
defer func() {
|
||||
loopFile.Close()
|
||||
mount.DetachLoopDevice(loopDev)
|
||||
}()
|
||||
|
||||
mountPoint := filepath.Join(tempDir, "mnt")
|
||||
if err := os.MkdirAll(mountPoint, 0755); err != nil {
|
||||
t.Fatalf("failed to create mount point: %v", err)
|
||||
}
|
||||
|
||||
m := mount.Mount{
|
||||
Type: "erofs",
|
||||
Source: loopDev,
|
||||
Options: []string{"ro"},
|
||||
}
|
||||
|
||||
err = fsmount.Fsmount(m, mountPoint)
|
||||
if err != nil {
|
||||
t.Fatalf("fsmount with loop device failed: %v", err)
|
||||
}
|
||||
defer mount.Unmount(mountPoint, 0)
|
||||
|
||||
content, err := os.ReadFile(filepath.Join(mountPoint, "test.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read test file: %v", err)
|
||||
}
|
||||
if string(content) != "hello" {
|
||||
t.Fatalf("unexpected content: %s", content)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMountOptionsPageSizeLimit tests that when mount options exceed the kernel's
|
||||
// PAGE_SIZE limit (typically 4KB), the traditional mount() syscall fails but
|
||||
// fsmount API succeeds.
|
||||
func TestMountOptionsPageSizeLimit(t *testing.T) {
|
||||
testutil.RequiresRoot(t)
|
||||
|
||||
if _, err := exec.LookPath("mkfs.erofs"); err != nil {
|
||||
t.Skipf("mkfs.erofs not found: %v", err)
|
||||
}
|
||||
|
||||
if !snapshotserofs.FindErofs() {
|
||||
t.Skip("erofs kernel support not available")
|
||||
}
|
||||
|
||||
if !fsmount.SupportsFsmount() {
|
||||
t.Skip("fsmount syscall not available (requires Linux 5.2+)")
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
|
||||
layerPath := filepath.Join(tempDir, "test.erofs")
|
||||
layerDir := filepath.Join(tempDir, "layer_dir")
|
||||
if err := os.MkdirAll(layerDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create layer dir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(layerDir, "test.txt"), []byte("hello"), 0644); err != nil {
|
||||
t.Fatalf("failed to write test file: %v", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command("mkfs.erofs", layerPath, layerDir)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("failed to create erofs image: %v, output: %s", err, output)
|
||||
}
|
||||
|
||||
loopFile, err := mount.SetupLoop(layerPath, mount.LoopParams{
|
||||
Readonly: true,
|
||||
Autoclear: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to setup loop device: %v", err)
|
||||
}
|
||||
loopDev := loopFile.Name()
|
||||
defer func() {
|
||||
loopFile.Close()
|
||||
mount.DetachLoopDevice(loopDev)
|
||||
}()
|
||||
|
||||
// Build mount options that exceed PAGE_SIZE (4096 bytes)
|
||||
options := []string{"ro"}
|
||||
var optionsBuilder strings.Builder
|
||||
optionsBuilder.WriteString("ro")
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
fakeDevice := filepath.Join(tempDir, strings.Repeat("x", 50), "fake_device_"+strings.Repeat("0", 10))
|
||||
opt := "device=" + fakeDevice
|
||||
options = append(options, opt)
|
||||
optionsBuilder.WriteString(",")
|
||||
optionsBuilder.WriteString(opt)
|
||||
}
|
||||
|
||||
optionsLen := optionsBuilder.Len()
|
||||
if optionsLen <= 4096 {
|
||||
t.Fatalf("Test setup error: options string (%d bytes) should exceed PAGE_SIZE (4096)", optionsLen)
|
||||
}
|
||||
|
||||
mountPoint := filepath.Join(tempDir, "mnt")
|
||||
if err := os.MkdirAll(mountPoint, 0755); err != nil {
|
||||
t.Fatalf("failed to create mount point: %v", err)
|
||||
}
|
||||
|
||||
m := mount.Mount{
|
||||
Type: "erofs",
|
||||
Source: loopDev,
|
||||
Options: options,
|
||||
}
|
||||
|
||||
// Test 1: Traditional mount should fail due to PAGE_SIZE limit
|
||||
t.Run("traditional_mount_fails", func(t *testing.T) {
|
||||
err := m.Mount(mountPoint)
|
||||
if err == nil {
|
||||
mount.Unmount(mountPoint, 0)
|
||||
t.Fatal("Expected traditional mount to fail due to PAGE_SIZE limit, but it succeeded")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "mount options is too long") {
|
||||
t.Fatalf("Expected 'mount options is too long' error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
// Test 2: fsmount should succeed
|
||||
t.Run("fsmount_succeeds", func(t *testing.T) {
|
||||
simpleMount := mount.Mount{
|
||||
Type: "erofs",
|
||||
Source: loopDev,
|
||||
Options: []string{"ro"},
|
||||
}
|
||||
|
||||
err := fsmount.Fsmount(simpleMount, mountPoint)
|
||||
if err != nil {
|
||||
if errors.Is(err, unix.EPERM) || errors.Is(err, unix.EACCES) {
|
||||
t.Skipf("fsmount failed due to permission restrictions: %v", err)
|
||||
}
|
||||
t.Fatalf("fsmount failed: %v", err)
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filepath.Join(mountPoint, "test.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read test file: %v", err)
|
||||
}
|
||||
if string(content) != "hello" {
|
||||
t.Fatalf("unexpected content: %s", content)
|
||||
}
|
||||
|
||||
if err := mount.Unmount(mountPoint, 0); err != nil {
|
||||
t.Fatalf("failed to unmount: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
// Test 3: Verify that fsmount can handle many options without PAGE_SIZE limit
|
||||
t.Run("fsmount_many_options", func(t *testing.T) {
|
||||
manyOptions := []string{"ro"}
|
||||
var builder strings.Builder
|
||||
builder.WriteString("ro")
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
manyOptions = append(manyOptions, "dax=always")
|
||||
builder.WriteString(",dax=always")
|
||||
}
|
||||
|
||||
manyOptionsMount := mount.Mount{
|
||||
Type: "erofs",
|
||||
Source: loopDev,
|
||||
Options: manyOptions,
|
||||
}
|
||||
|
||||
err := fsmount.Fsmount(manyOptionsMount, mountPoint)
|
||||
if err != nil {
|
||||
if errors.Is(err, unix.EPERM) || errors.Is(err, unix.EACCES) {
|
||||
t.Skipf("fsmount failed due to permission restrictions: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
mount.Unmount(mountPoint, 0)
|
||||
})
|
||||
}
|
||||
@@ -38,7 +38,7 @@ import (
|
||||
const defaultWritableSize = 0
|
||||
|
||||
// check if EROFS kernel filesystem is registered or not
|
||||
func findErofs() bool {
|
||||
func FindErofs() bool {
|
||||
fs, err := os.ReadFile("/proc/filesystems")
|
||||
if err != nil {
|
||||
return false
|
||||
@@ -55,7 +55,7 @@ func checkCompatibility(root string) error {
|
||||
return fmt.Errorf("%s does not support d_type. If the backing filesystem is xfs, please reformat with ftype=1 to enable d_type support", root)
|
||||
}
|
||||
|
||||
if !findErofs() {
|
||||
if !FindErofs() {
|
||||
return fmt.Errorf("EROFS unsupported, please `modprobe erofs`: %w", plugin.ErrSkipPlugin)
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ func newSnapshotter(t *testing.T, opts ...Opt) func(ctx context.Context, root st
|
||||
t.Skipf("could not find mkfs.erofs: %v", err)
|
||||
}
|
||||
|
||||
if !findErofs() {
|
||||
if !FindErofs() {
|
||||
t.Skip("check for erofs kernel support failed, skipping test")
|
||||
}
|
||||
return func(ctx context.Context, root string) (snapshots.Snapshotter, func() error, error) {
|
||||
@@ -180,7 +180,7 @@ func TestErofsDifferWithTarIndexMode(t *testing.T) {
|
||||
testutil.RequiresRoot(t)
|
||||
ctx := context.Background()
|
||||
|
||||
if !findErofs() {
|
||||
if !FindErofs() {
|
||||
t.Skip("check for erofs kernel support failed, skipping test")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user