diff --git a/.gitignore b/.gitignore index 52dac0bf421..1fcbcfbd379 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ vendor/pkg /contrib/cmd/fs-idmap/fs-idmap /contrib/cmd/memfd-bind/memfd-bind /contrib/cmd/pidfd-kill/pidfd-kill +/contrib/cmd/remap-rootfs/remap-rootfs man/man8 release Vagrantfile diff --git a/Makefile b/Makefile index df935e41929..caa4441789c 100644 --- a/Makefile +++ b/Makefile @@ -71,10 +71,10 @@ runc-bin: runc-dmz $(GO_BUILD) -o runc . .PHONY: all -all: runc recvtty sd-helper seccompagent fs-idmap memfd-bind pidfd-kill +all: runc recvtty sd-helper seccompagent fs-idmap memfd-bind pidfd-kill remap-rootfs -.PHONY: recvtty sd-helper seccompagent fs-idmap memfd-bind pidfd-kill -recvtty sd-helper seccompagent fs-idmap memfd-bind pidfd-kill: +.PHONY: recvtty sd-helper seccompagent fs-idmap memfd-bind pidfd-kill remap-rootfs +recvtty sd-helper seccompagent fs-idmap memfd-bind pidfd-kill remap-rootfs: $(GO_BUILD) -o contrib/cmd/$@/$@ ./contrib/cmd/$@ .PHONY: static diff --git a/contrib/cmd/remap-rootfs/remap-rootfs.go b/contrib/cmd/remap-rootfs/remap-rootfs.go new file mode 100644 index 00000000000..b9739ba89c7 --- /dev/null +++ b/contrib/cmd/remap-rootfs/remap-rootfs.go @@ -0,0 +1,143 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "syscall" + + "github.com/urfave/cli" + + "github.com/opencontainers/runtime-spec/specs-go" +) + +const usage = `contrib/cmd/remap-rootfs + +remap-rootfs is a helper tool to remap the root filesystem of a Open Container +Initiative bundle using user namespaces such that the file owners are remapped +from "host" mappings to the user namespace's mappings. + +Effectively, this is a slightly more complicated 'chown -R', and is primarily +used within runc's integration tests to remap the test filesystem to match the +test user namespace. Note that calling remap-rootfs multiple times, or changing +the mapping and then calling remap-rootfs will likely produce incorrect results +because we do not "un-map" any pre-applied mappings from previous remap-rootfs +calls. + +Note that the bundle is assumed to be produced by a trusted source, and thus +malicious configuration files will likely not be handled safely. + +To use remap-rootfs, simply pass it the path to an OCI bundle (a directory +containing a config.json): + + $ sudo remap-rootfs ./bundle +` + +func toHostID(mappings []specs.LinuxIDMapping, id uint32) (int, bool) { + for _, m := range mappings { + if m.ContainerID <= id && id < m.ContainerID+m.Size { + return int(m.HostID + id), true + } + } + return -1, false +} + +type inodeID struct { + Dev, Ino uint64 +} + +func toInodeID(st *syscall.Stat_t) inodeID { + return inodeID{Dev: st.Dev, Ino: st.Ino} +} + +func remapRootfs(root string, uidMap, gidMap []specs.LinuxIDMapping) error { + seenInodes := make(map[inodeID]struct{}) + return filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + mode := info.Mode() + st := info.Sys().(*syscall.Stat_t) + + // Skip symlinks. + if mode.Type() == os.ModeSymlink { + return nil + } + // Skip hard-links to files we've already remapped. + id := toInodeID(st) + if _, seen := seenInodes[id]; seen { + return nil + } + seenInodes[id] = struct{}{} + + // Calculate the new uid:gid. + uid := st.Uid + newUID, ok1 := toHostID(uidMap, uid) + gid := st.Gid + newGID, ok2 := toHostID(gidMap, gid) + + // Skip files that cannot be mapped. + if !ok1 || !ok2 { + niceName := path + if relName, err := filepath.Rel(root, path); err == nil { + niceName = "/" + relName + } + fmt.Printf("skipping file %s: cannot remap user %d:%d -> %d:%d\n", niceName, uid, gid, newUID, newGID) + return nil + } + if err := os.Lchown(path, newUID, newGID); err != nil { + return err + } + // Re-apply any setid bits that would be cleared due to chown(2). + return os.Chmod(path, mode) + }) +} + +func main() { + app := cli.NewApp() + app.Name = "remap-rootfs" + app.Usage = usage + + app.Action = func(ctx *cli.Context) error { + args := ctx.Args() + if len(args) != 1 { + return errors.New("exactly one bundle argument must be provided") + } + bundle := args[0] + + configFile, err := os.Open(filepath.Join(bundle, "config.json")) + if err != nil { + return err + } + defer configFile.Close() + + var spec specs.Spec + if err := json.NewDecoder(configFile).Decode(&spec); err != nil { + return fmt.Errorf("parsing config.json: %w", err) + } + + if spec.Root == nil { + return errors.New("invalid config.json: root section is null") + } + rootfs := filepath.Join(bundle, spec.Root.Path) + + if spec.Linux == nil { + return errors.New("invalid config.json: linux section is null") + } + uidMap := spec.Linux.UIDMappings + gidMap := spec.Linux.GIDMappings + if len(uidMap) == 0 && len(gidMap) == 0 { + fmt.Println("skipping remapping -- no userns mappings specified") + return nil + } + + return remapRootfs(rootfs, uidMap, gidMap) + } + if err := app.Run(os.Args); err != nil { + fmt.Fprintln(os.Stderr, "error:", err) + os.Exit(1) + } +} diff --git a/tests/integration/helpers.bash b/tests/integration/helpers.bash index 44af05d96cb..42f862511c3 100755 --- a/tests/integration/helpers.bash +++ b/tests/integration/helpers.bash @@ -18,6 +18,7 @@ SD_HELPER="${INTEGRATION_ROOT}/../../contrib/cmd/sd-helper/sd-helper" SECCOMP_AGENT="${INTEGRATION_ROOT}/../../contrib/cmd/seccompagent/seccompagent" FS_IDMAP="${INTEGRATION_ROOT}/../../contrib/cmd/fs-idmap/fs-idmap" PIDFD_KILL="${INTEGRATION_ROOT}/../../contrib/cmd/pidfd-kill/pidfd-kill" +REMAP_ROOTFS="${INTEGRATION_ROOT}/../../contrib/cmd/remap-rootfs/remap-rootfs" # Some variables may not always be set. Set those to empty value, # if unset, to avoid "unbound variable" error. @@ -657,6 +658,12 @@ function teardown_bundle() { remove_parent } +function remap_rootfs() { + [ ! -v ROOT ] && return 0 # nothing to remap + + "$REMAP_ROOTFS" "$ROOT/bundle" +} + function is_kernel_gte() { local major_required minor_required major_required=$(echo "$1" | cut -d. -f1) diff --git a/tests/integration/idmap.bats b/tests/integration/idmap.bats index 5bd554ca322..53784b55672 100644 --- a/tests/integration/idmap.bats +++ b/tests/integration/idmap.bats @@ -16,7 +16,6 @@ function setup() { # Use other owner for source-2 chown 1:1 source-2/foo.txt - mkdir -p rootfs/{proc,sys,tmp} mkdir -p rootfs/tmp/mount-{1,2} mkdir -p rootfs/mnt/bind-mount-{1,2} @@ -43,6 +42,8 @@ function setup() { ] } ] ' + + remap_rootfs } function teardown() { diff --git a/tests/integration/run.bats b/tests/integration/run.bats index 705868eb071..f6bd3c86500 100644 --- a/tests/integration/run.bats +++ b/tests/integration/run.bats @@ -171,7 +171,7 @@ function teardown() { update_config '.linux.namespaces += [{"type": "user"}] | .linux.uidMappings += [{"containerID": 0, "hostID": 100000, "size": 100}] | .linux.gidMappings += [{"containerID": 0, "hostID": 200000, "size": 200}]' - mkdir -p rootfs/{proc,sys,tmp} + remap_rootfs fi update_config '.linux.namespaces += [{"type": "time"}] | .linux.timeOffsets = { diff --git a/tests/integration/timens.bats b/tests/integration/timens.bats index 6b55a73e646..fb33e2b2783 100644 --- a/tests/integration/timens.bats +++ b/tests/integration/timens.bats @@ -4,8 +4,6 @@ load helpers function setup() { setup_busybox - - mkdir -p rootfs/{proc,sys,tmp} } function teardown() { @@ -63,6 +61,7 @@ function teardown() { update_config ' .linux.namespaces += [{"type": "user"}] | .linux.uidMappings += [{"hostID": 100000, "containerID": 0, "size": 65534}] | .linux.gidMappings += [{"hostID": 200000, "containerID": 0, "size": 65534}] ' + remap_rootfs update_config '.process.args = ["cat", "/proc/self/timens_offsets"]' update_config '.linux.namespaces += [{"type": "time"}] diff --git a/tests/integration/userns.bats b/tests/integration/userns.bats index 947681c9b7c..2094cbf1448 100644 --- a/tests/integration/userns.bats +++ b/tests/integration/userns.bats @@ -12,7 +12,6 @@ function setup() { # Permissions only to the owner, it is inaccessible to group/others chmod 700 source-inaccessible-{1,2} - mkdir -p rootfs/{proc,sys,tmp} mkdir -p rootfs/tmp/mount-{1,2} to_umount_list="$(mktemp "$BATS_RUN_TMPDIR/userns-mounts.XXXXXX")" @@ -20,6 +19,7 @@ function setup() { update_config ' .linux.namespaces += [{"type": "user"}] | .linux.uidMappings += [{"hostID": 100000, "containerID": 0, "size": 65534}] | .linux.gidMappings += [{"hostID": 200000, "containerID": 0, "size": 65534}] ' + remap_rootfs fi }