Merge pull request #12433 from halaney/ahalaney/erofs-idmap-latest

Add erofs idmap support
This commit is contained in:
Akihiro Suda
2026-01-09 06:35:49 +00:00
committed by GitHub
10 changed files with 152 additions and 23 deletions

View File

@@ -129,13 +129,15 @@ func readonlyMounts(mounts []Mount) []Mount {
// readonlyOverlay takes mount options for overlay mounts and makes them readonly by
// removing workdir and upperdir (and appending the upperdir layer to lowerdir) - see:
// https://www.kernel.org/doc/html/latest/filesystems/overlayfs.html#multiple-lower-layers
// It also strips the uidmap/gidmap options to avoid needlessly doing an idmap of this
// temporary mount
func readonlyOverlay(opt []string) []string {
out := make([]string, 0, len(opt))
upper := ""
for _, o := range opt {
if strings.HasPrefix(o, "upperdir=") {
upper = strings.TrimPrefix(o, "upperdir=")
} else if !strings.HasPrefix(o, "workdir=") {
} else if !isSkippedReadonlyOption(o) {
out = append(out, o)
}
}
@@ -149,6 +151,15 @@ func readonlyOverlay(opt []string) []string {
return out
}
// isSkippedReadonlyOption takes an overlayfs option string and returns
// true if such an option should be skipped when converting the mount
// to a readonly mount
func isSkippedReadonlyOption(o string) bool {
return strings.HasPrefix(o, "workdir=") ||
strings.HasPrefix(o, "uidmap=") ||
strings.HasPrefix(o, "gidmap=")
}
// ToProto converts from [Mount] to the containerd
// APIs protobuf definition of a Mount.
func ToProto(mounts []Mount) []*types.Mount {

View File

@@ -90,16 +90,16 @@ func IDMapMountWithAttrs(source, target string, usernsFd int, attrSet uint64, at
attr.Attr_set = unix.MOUNT_ATTR_IDMAP | attrSet
attr.Attr_clr = attrClr
attr.Propagation = 0
attr.Propagation = unix.MS_PRIVATE
attr.Userns_fd = uint64(usernsFd)
dFd, err := unix.OpenTree(-int(unix.EBADF), source, uint(unix.OPEN_TREE_CLONE|unix.OPEN_TREE_CLOEXEC|unix.AT_EMPTY_PATH))
dFd, err := unix.OpenTree(-int(unix.EBADF), source, uint(unix.OPEN_TREE_CLONE|unix.OPEN_TREE_CLOEXEC|unix.AT_EMPTY_PATH|unix.AT_RECURSIVE))
if err != nil {
return fmt.Errorf("unable to open tree for %s: %w", target, err)
}
defer unix.Close(dFd)
if err = unix.MountSetattr(dFd, "", unix.AT_EMPTY_PATH, &attr); err != nil {
if err = unix.MountSetattr(dFd, "", unix.AT_EMPTY_PATH|unix.AT_RECURSIVE, &attr); err != nil {
return fmt.Errorf("unable to shift GID/UID or set mount attrs for %s: %w", target, err)
}

View File

@@ -265,11 +265,12 @@ func doPrepareIDMappedOverlay(tmpDir string, lowerDirs []string, usernsFd int) (
if err := IDMapMountWithAttrs(commonDir, tempRemountsLocation, usernsFd, unix.MOUNT_ATTR_RDONLY, 0); err != nil {
return nil, nil, err
}
cleanMount := func() {
// Use the Unmount helper that does retries because there can be easily an open fd
// to the idmapped directory and when containerd forks to create a userns fd (maybe
// for another container), it will make the mount busy for a few ms.
err := Unmount(tempRemountsLocation, 0)
err := UnmountRecursive(tempRemountsLocation, 0)
if err != nil {
log.L.WithError(err).Warnf("failed to unmount idmapped directory %s: %v", tempRemountsLocation, err)
}

View File

@@ -239,6 +239,5 @@ For the EROFS differ:
## TODO
- ID-mapped mount spport;
- DMVerity support.
- DMVerity support.

View File

@@ -34,6 +34,7 @@ import (
"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/internal/userns"
)
// SnapshotterConfig is used to configure the erofs snapshotter instance
@@ -48,6 +49,7 @@ type SnapshotterConfig struct {
defaultSize int64
// fsMergeThreshold (>0) enables fsmerge when the number of image layers exceeds this value
fsMergeThreshold uint
remapIDs bool
}
// Opt is an option to configure the erofs snapshotter
@@ -88,6 +90,13 @@ func WithFsMergeThreshold(v uint) Opt {
}
}
// WithRemapIDs enables kernel ID-mapped mounts for user namespace support
func WithRemapIDs() Opt {
return func(config *SnapshotterConfig) {
config.remapIDs = 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
@@ -103,6 +112,7 @@ type snapshotter struct {
defaultWritable int64
blockMode bool
fsMergeThreshold uint
remapIDs bool
}
// NewSnapshotter returns a Snapshotter which uses EROFS+OverlayFS. The layers
@@ -160,6 +170,7 @@ func NewSnapshotter(root string, opts ...Opt) (snapshots.Snapshotter, error) {
defaultWritable: config.defaultSize,
blockMode: config.defaultSize > 0,
fsMergeThreshold: config.fsMergeThreshold,
remapIDs: config.remapIDs,
}, nil
}
@@ -241,7 +252,7 @@ func (s *snapshotter) mountFsMeta(snap storage.Snapshot, id int) (mount.Mount, b
return m, true
}
func (s *snapshotter) mounts(snap storage.Snapshot, _ snapshots.Info) ([]mount.Mount, error) {
func (s *snapshotter) mounts(snap storage.Snapshot, info snapshots.Info) ([]mount.Mount, error) {
var options []string
if len(snap.ParentIDs) == 0 {
@@ -377,6 +388,16 @@ func (s *snapshotter) mounts(snap storage.Snapshot, _ snapshots.Info) ([]mount.M
} else {
options = append(options, fmt.Sprintf("lowerdir={{ overlay %d %d }}", first, len(mounts)-1))
}
if s.remapIDs {
if v, ok := info.Labels[snapshots.LabelSnapshotUIDMapping]; ok {
options = append(options, fmt.Sprintf("uidmap=%s", v))
}
if v, ok := info.Labels[snapshots.LabelSnapshotGIDMapping]; ok {
options = append(options, fmt.Sprintf("gidmap=%s", v))
}
}
options = append(options, s.ovlOptions...)
return append(mounts, mount.Mount{
@@ -426,9 +447,48 @@ func (s *snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, k
return fmt.Errorf("failed to get snapshot info: %w", err)
}
if len(snap.ParentIDs) > 0 {
if err := upperDirectoryPermission(filepath.Join(td, "fs"), s.upperPath(snap.ParentIDs[0])); err != nil {
return err
var (
mappedUID, mappedGID = -1, -1
uidmapLabel, gidmapLabel string
needsRemap = false
)
if v, ok := info.Labels[snapshots.LabelSnapshotUIDMapping]; ok {
uidmapLabel = v
needsRemap = true
}
if v, ok := info.Labels[snapshots.LabelSnapshotGIDMapping]; ok {
gidmapLabel = v
needsRemap = true
}
if needsRemap {
var idMap userns.IDMap
if err = idMap.Unmarshal(uidmapLabel, gidmapLabel); err != nil {
return fmt.Errorf("failed to unmarshal snapshot ID mapped labels: %w", err)
}
root, err := idMap.RootPair()
if err != nil {
return fmt.Errorf("failed to find root pair: %w", err)
}
mappedUID, mappedGID = int(root.Uid), int(root.Gid)
}
// Fall back to copying ownership from parent if no ID mapping labels
if mappedUID == -1 || mappedGID == -1 {
if len(snap.ParentIDs) > 0 {
uid, gid, err := getParentOwnership(s.upperPath(snap.ParentIDs[0]))
if err != nil {
return fmt.Errorf("failed to get parent ownership: %w", err)
}
mappedUID = uid
mappedGID = gid
}
}
// Apply the ownership if we have valid UID/GID
if mappedUID != -1 && mappedGID != -1 {
if err := os.Lchown(filepath.Join(td, "fs"), mappedUID, mappedGID); err != nil {
return fmt.Errorf("failed to chown: %w", err)
}
}

View File

@@ -122,16 +122,12 @@ func convertDirToErofs(ctx context.Context, layerBlob, upperDir string) error {
return nil
}
func upperDirectoryPermission(p, parent string) error {
st, err := os.Stat(parent)
func getParentOwnership(parentPath string) (uid, gid int, err error) {
st, err := os.Stat(parentPath)
if err != nil {
return fmt.Errorf("failed to stat parent: %w", err)
return -1, -1, fmt.Errorf("failed to stat parent: %w", err)
}
stat := st.Sys().(*syscall.Stat_t)
if err := os.Lchown(p, int(stat.Uid), int(stat.Gid)); err != nil {
return fmt.Errorf("failed to chown: %w", err)
}
return nil
return int(stat.Uid), int(stat.Gid), nil
}

View File

@@ -41,10 +41,10 @@ func cleanupUpper(upper string) error {
return nil
}
func upperDirectoryPermission(p, parent string) error {
return nil
}
func convertDirToErofs(ctx context.Context, layerBlob, upperDir string) error {
return errdefs.ErrNotImplemented
}
func getParentOwnership(parentPath string) (uid, gid int, err error) {
return -1, -1, nil
}

View File

@@ -29,6 +29,11 @@ import (
"github.com/docker/go-units"
)
const (
capaRemapIDs = "remap-ids"
capaOnlyRemapIDs = "only-remap-ids"
)
// Config represents configuration for the native plugin.
type Config struct {
// Root directory for the plugin
@@ -94,6 +99,13 @@ func init() {
opts = append(opts, erofs.WithFsMergeThreshold(config.MaxUnmergedLayers))
}
// Don't bother supporting overlay's slow_chown, only RemapIDs
ic.Meta.Capabilities = append(ic.Meta.Capabilities, capaOnlyRemapIDs)
if ok, err := supportsIDMappedMounts(); err == nil && ok {
opts = append(opts, erofs.WithRemapIDs())
ic.Meta.Capabilities = append(ic.Meta.Capabilities, capaRemapIDs)
}
ic.Meta.Exports[plugins.SnapshotterRootDir] = root
ic.Meta.Capabilities = append(ic.Meta.Capabilities, "rebase")
return erofs.NewSnapshotter(root, opts...)

View File

@@ -0,0 +1,27 @@
//go:build linux
/*
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 plugin
import (
overlayutils "github.com/containerd/containerd/v2/plugins/snapshots/overlay/overlayutils"
)
func supportsIDMappedMounts() (bool, error) {
return overlayutils.SupportsIDMappedMounts()
}

View File

@@ -0,0 +1,23 @@
//go:build !linux
/*
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 plugin
func supportsIDMappedMounts() (bool, error) {
return false, nil
}