From 9874a4d190e4cadf31582bddbd9170c775031c7e Mon Sep 17 00:00:00 2001 From: Manish Mishra Date: Thu, 27 Nov 2025 18:43:12 +0000 Subject: [PATCH] runsc: Add EROFS mount support in bundle config.json This change enables EROFS filesystems to be specified as regular mounts in the OCI bundle config.json, not just as rootfs or debug mounts. EROFS mounts are now tracked in goferMountConfs alongside lisafs mounts. The implementation adds: - IsErofsMount() helper in specutils to identify EROFS mounts - Updated mount index tracking across container.go, gofer.go, and vfs.go - Support for opening EROFS image files and passing FDs to the sandbox - EROFS case in getMountNameAndOptions() for proper mount setup Key implementation details: - EROFS mounts are included in goferMountConfs but skip gofer-specific processing (e.g., lisafs serving, filestore creation) - Mount type determination happens before mount hint logic - Proper index tracking ensures EROFS mounts increment indices but skip lisafs-only operations Fixes #12307 --- runsc/boot/vfs.go | 16 +++++++++++- runsc/cmd/gofer.go | 16 ++++++++++++ runsc/container/container.go | 50 +++++++++++++++++++++++++++++++----- runsc/specutils/BUILD | 1 + runsc/specutils/specutils.go | 6 +++++ 5 files changed, 82 insertions(+), 7 deletions(-) diff --git a/runsc/boot/vfs.go b/runsc/boot/vfs.go index 237dfa9b3b..54c285d45d 100644 --- a/runsc/boot/vfs.go +++ b/runsc/boot/vfs.go @@ -812,10 +812,12 @@ func (c *containerMounter) prepareMounts() ([]mountInfo, error) { hint: c.hints.FindMount(c.mounts[i].Source), } specutils.MaybeConvertToBindMount(info.mount) - if specutils.IsGoferMount(*info.mount) { + if specutils.IsGoferMount(*info.mount) || specutils.IsErofsMount(*info.mount) { info.goferMountConf = c.goferMountConfs[goferMntIdx] if info.goferMountConf.ShouldUseLisafs() { info.goferFD = c.goferFDs.removeAsFD() + } else if info.goferMountConf.ShouldUseErofs() { + info.goferFD = c.goferFDs.removeAsFD() } if info.goferMountConf.IsFilestorePresent() { info.filestoreFD = c.goferFilestoreFDs.removeAsFD() @@ -988,6 +990,18 @@ func getMountNameAndOptions(spec *specs.Spec, conf *config.Config, m *mountInfo, return "", nil, err } + case erofs.Name: + if m.goferFD == nil { + return "", nil, fmt.Errorf("EROFS mount requires an image file FD") + } + data = []string{fmt.Sprintf("ifd=%d", m.goferFD.Release())} + internalData = erofs.InternalFilesystemOptions{ + UniqueID: vfs.RestoreID{ + ContainerName: containerName, + Path: m.mount.Destination, + }, + } + default: log.Warningf("ignoring unknown filesystem type %q", m.mount.Type) return "", nil, nil diff --git a/runsc/cmd/gofer.go b/runsc/cmd/gofer.go index 629f38ca5e..cedcb0973e 100644 --- a/runsc/cmd/gofer.go +++ b/runsc/cmd/gofer.go @@ -342,6 +342,11 @@ func (g *Gofer) serve(spec *specs.Spec, conf *config.Config, root string, ruid i mountIdx := 1 // first one is the root for _, m := range spec.Mounts { + // EROFS mounts are in goferMountConfs but gofer doesn't serve them + if specutils.IsErofsMount(m) { + mountIdx++ + continue + } if !specutils.IsGoferMount(m) { continue } @@ -528,6 +533,11 @@ func (g *Gofer) setupRootFS(spec *specs.Spec, conf *config.Config, goferToHostRP func (g *Gofer) setupMounts(conf *config.Config, mounts []specs.Mount, root, procPath string, goferToHostRPC *urpc.Client) (retErr error) { mountIdx := 1 // First index is for rootfs. for _, m := range mounts { + // EROFS mounts are in goferMountConfs but gofer doesn't set them up + if specutils.IsErofsMount(m) { + mountIdx++ + continue + } if !specutils.IsGoferMount(m) { continue } @@ -690,6 +700,12 @@ func (g *Gofer) resolveMounts(conf *config.Config, mounts []specs.Mount, root st mountIdx := 1 // First index is for rootfs. cleanMounts := make([]specs.Mount, 0, len(mounts)) for _, m := range mounts { + // EROFS mounts are in goferMountConfs but gofer doesn't resolve them + if specutils.IsErofsMount(m) { + cleanMounts = append(cleanMounts, m) + mountIdx++ + continue + } if !specutils.IsGoferMount(m) { cleanMounts = append(cleanMounts, m) continue diff --git a/runsc/container/container.go b/runsc/container/container.go index c53911d2e8..f41bba2ffb 100644 --- a/runsc/container/container.go +++ b/runsc/container/container.go @@ -911,6 +911,11 @@ func (c *Container) forEachSelfMount(fn func(mountSrc string)) { } goferMntIdx := 1 // First index is for rootfs. for i := range c.Spec.Mounts { + // EROFS mounts are in goferMountConfs but don't have self-backed filestores + if specutils.IsErofsMount(c.Spec.Mounts[i]) { + goferMntIdx++ + continue + } if !specutils.IsGoferMount(c.Spec.Mounts[i]) { continue } @@ -985,12 +990,17 @@ func (c *Container) initGoferConfs(ovlConf config.Overlay2, mountHints *boot.Pod // Handle bind mounts. for i := range c.Spec.Mounts { - if !specutils.IsGoferMount(c.Spec.Mounts[i]) { + if !specutils.IsGoferMount(c.Spec.Mounts[i]) && !specutils.IsErofsMount(c.Spec.Mounts[i]) { continue } + // Determine mount type: Bind for gofer mounts, erofs.Name for EROFS mounts + mountType := boot.Bind + if specutils.IsErofsMount(c.Spec.Mounts[i]) { + mountType = erofs.Name + } + overlayMedium := ovlConf.SubMountOverlayMedium() overlaySize := ovlConf.SubMountOverlaySize() - mountType = boot.Bind if specutils.IsReadonlyMount(c.Spec.Mounts[i].Options) { overlayMedium = config.NoOverlay } @@ -1037,6 +1047,11 @@ func (c *Container) createGoferFilestores(ovlConf config.Overlay2, mountHints *b // Then handle all the bind mounts. mountIdx := 1 // first one is the root for _, m := range c.Spec.Mounts { + // EROFS mounts are in goferMountConfs but don't need filestore processing + if specutils.IsErofsMount(m) { + mountIdx++ + continue + } if !specutils.IsGoferMount(m) { continue } @@ -1377,7 +1392,19 @@ func (c *Container) createGoferProcess(conf *config.Config, mountHints *boot.Pod } sandEnds := make([]*os.File, 0, ioFileCount) + // Track which spec.Mount corresponds to which goferMountConf + mountIdx := 0 for i, cfg := range c.GoferMountConfs { + // Align spec mount with gofer mount conf by skipping non-gofer/non-erofs mounts + // Skip alignment for root mount (i == 0) because it is not present in spec.Mounts + if i > 0 { + for mountIdx < len(c.Spec.Mounts) && + !specutils.IsGoferMount(c.Spec.Mounts[mountIdx]) && + !specutils.IsErofsMount(c.Spec.Mounts[mountIdx]) { + mountIdx++ + } + } + switch { case cfg.ShouldUseLisafs(): fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) @@ -1390,15 +1417,26 @@ func (c *Container) createGoferProcess(conf *config.Config, mountHints *boot.Pod donations.DonateAndClose("io-fds", goferEnd) case cfg.ShouldUseErofs(): - if i > 0 { - return nil, nil, nil, nil, fmt.Errorf("EROFS lower layer is only supported for root mount") + // Get the source for the EROFS image + var mountSrc string + if i == 0 { + // Root mount + mountSrc = rootfsHint.Mount.Source + } else { + // Non-root mount: use the aligned mountIdx + mountSrc = c.Spec.Mounts[mountIdx].Source } - f, err := os.Open(rootfsHint.Mount.Source) + f, err := os.Open(mountSrc) if err != nil { - return nil, nil, nil, nil, fmt.Errorf("opening rootfs image %q: %v", rootfsHint.Mount.Source, err) + return nil, nil, nil, nil, fmt.Errorf("opening EROFS image %q: %v", mountSrc, err) } sandEnds = append(sandEnds, f) } + + // Move to next mount in spec (for non-root mounts) + if i > 0 { + mountIdx++ + } } var devSandEnd *os.File if shouldCreateDeviceGofer(c.Spec, conf) { diff --git a/runsc/specutils/BUILD b/runsc/specutils/BUILD index 9f0d3e52a6..fbe2ed1761 100644 --- a/runsc/specutils/BUILD +++ b/runsc/specutils/BUILD @@ -20,6 +20,7 @@ go_library( "//pkg/abi/linux", "//pkg/log", "//pkg/sentry/devices/nvproxy/nvconf", + "//pkg/sentry/fsimpl/erofs", "//pkg/sentry/kernel/auth", "//runsc/config", "//runsc/flag", diff --git a/runsc/specutils/specutils.go b/runsc/specutils/specutils.go index 499fd2ac12..a573e7c249 100644 --- a/runsc/specutils/specutils.go +++ b/runsc/specutils/specutils.go @@ -34,6 +34,7 @@ import ( "golang.org/x/sys/unix" "gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/log" + "gvisor.dev/gvisor/pkg/sentry/fsimpl/erofs" "gvisor.dev/gvisor/pkg/sentry/kernel/auth" "gvisor.dev/gvisor/runsc/config" "gvisor.dev/gvisor/runsc/flag" @@ -502,6 +503,11 @@ func IsGoferMount(m specs.Mount) bool { return m.Type == "bind" && m.Source != "" } +// IsErofsMount returns true if the given mount can be mounted as EROFS. +func IsErofsMount(m specs.Mount) bool { + return m.Type == erofs.Name +} + // MaybeConvertToBindMount converts mount type to "bind" in case any of the // mount options are either "bind" or "rbind" as required by the OCI spec. //