Skip to content

Commit

Permalink
tests: remap rootfs for userns tests
Browse files Browse the repository at this point in the history
Previously, all of our userns tests worked around the remapping issue by
creating the paths that runc would attempt to create (like /proc).
However, this isn't really accurate to how real userns containers are
created, so it's much better to actually remap the rootfs.

Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
  • Loading branch information
cyphar committed Dec 5, 2023
1 parent 6fa8d06 commit c045886
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 8 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
143 changes: 143 additions & 0 deletions contrib/cmd/remap-rootfs/remap-rootfs.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
7 changes: 7 additions & 0 deletions tests/integration/helpers.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion tests/integration/idmap.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand All @@ -43,6 +42,8 @@ function setup() {
]
}
] '

remap_rootfs
}

function teardown() {
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/run.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
3 changes: 1 addition & 2 deletions tests/integration/timens.bats
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ load helpers

function setup() {
setup_busybox

mkdir -p rootfs/{proc,sys,tmp}
}

function teardown() {
Expand Down Expand Up @@ -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"}]
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/userns.bats
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ 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")"
if [ $EUID -eq 0 ]; then
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
}

Expand Down

0 comments on commit c045886

Please sign in to comment.