Skip to content

Commit c045886

Browse files
committed
tests: remap rootfs for userns tests
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 <[email protected]>
1 parent 6fa8d06 commit c045886

File tree

8 files changed

+159
-8
lines changed

8 files changed

+159
-8
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ vendor/pkg
77
/contrib/cmd/fs-idmap/fs-idmap
88
/contrib/cmd/memfd-bind/memfd-bind
99
/contrib/cmd/pidfd-kill/pidfd-kill
10+
/contrib/cmd/remap-rootfs/remap-rootfs
1011
man/man8
1112
release
1213
Vagrantfile

Makefile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,10 @@ runc-bin: runc-dmz
7171
$(GO_BUILD) -o runc .
7272

7373
.PHONY: all
74-
all: runc recvtty sd-helper seccompagent fs-idmap memfd-bind pidfd-kill
74+
all: runc recvtty sd-helper seccompagent fs-idmap memfd-bind pidfd-kill remap-rootfs
7575

76-
.PHONY: recvtty sd-helper seccompagent fs-idmap memfd-bind pidfd-kill
77-
recvtty sd-helper seccompagent fs-idmap memfd-bind pidfd-kill:
76+
.PHONY: recvtty sd-helper seccompagent fs-idmap memfd-bind pidfd-kill remap-rootfs
77+
recvtty sd-helper seccompagent fs-idmap memfd-bind pidfd-kill remap-rootfs:
7878
$(GO_BUILD) -o contrib/cmd/$@/$@ ./contrib/cmd/$@
7979

8080
.PHONY: static
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"syscall"
10+
11+
"github.com/urfave/cli"
12+
13+
"github.com/opencontainers/runtime-spec/specs-go"
14+
)
15+
16+
const usage = `contrib/cmd/remap-rootfs
17+
18+
remap-rootfs is a helper tool to remap the root filesystem of a Open Container
19+
Initiative bundle using user namespaces such that the file owners are remapped
20+
from "host" mappings to the user namespace's mappings.
21+
22+
Effectively, this is a slightly more complicated 'chown -R', and is primarily
23+
used within runc's integration tests to remap the test filesystem to match the
24+
test user namespace. Note that calling remap-rootfs multiple times, or changing
25+
the mapping and then calling remap-rootfs will likely produce incorrect results
26+
because we do not "un-map" any pre-applied mappings from previous remap-rootfs
27+
calls.
28+
29+
Note that the bundle is assumed to be produced by a trusted source, and thus
30+
malicious configuration files will likely not be handled safely.
31+
32+
To use remap-rootfs, simply pass it the path to an OCI bundle (a directory
33+
containing a config.json):
34+
35+
$ sudo remap-rootfs ./bundle
36+
`
37+
38+
func toHostID(mappings []specs.LinuxIDMapping, id uint32) (int, bool) {
39+
for _, m := range mappings {
40+
if m.ContainerID <= id && id < m.ContainerID+m.Size {
41+
return int(m.HostID + id), true
42+
}
43+
}
44+
return -1, false
45+
}
46+
47+
type inodeID struct {
48+
Dev, Ino uint64
49+
}
50+
51+
func toInodeID(st *syscall.Stat_t) inodeID {
52+
return inodeID{Dev: st.Dev, Ino: st.Ino}
53+
}
54+
55+
func remapRootfs(root string, uidMap, gidMap []specs.LinuxIDMapping) error {
56+
seenInodes := make(map[inodeID]struct{})
57+
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
58+
if err != nil {
59+
return err
60+
}
61+
62+
mode := info.Mode()
63+
st := info.Sys().(*syscall.Stat_t)
64+
65+
// Skip symlinks.
66+
if mode.Type() == os.ModeSymlink {
67+
return nil
68+
}
69+
// Skip hard-links to files we've already remapped.
70+
id := toInodeID(st)
71+
if _, seen := seenInodes[id]; seen {
72+
return nil
73+
}
74+
seenInodes[id] = struct{}{}
75+
76+
// Calculate the new uid:gid.
77+
uid := st.Uid
78+
newUID, ok1 := toHostID(uidMap, uid)
79+
gid := st.Gid
80+
newGID, ok2 := toHostID(gidMap, gid)
81+
82+
// Skip files that cannot be mapped.
83+
if !ok1 || !ok2 {
84+
niceName := path
85+
if relName, err := filepath.Rel(root, path); err == nil {
86+
niceName = "/" + relName
87+
}
88+
fmt.Printf("skipping file %s: cannot remap user %d:%d -> %d:%d\n", niceName, uid, gid, newUID, newGID)
89+
return nil
90+
}
91+
if err := os.Lchown(path, newUID, newGID); err != nil {
92+
return err
93+
}
94+
// Re-apply any setid bits that would be cleared due to chown(2).
95+
return os.Chmod(path, mode)
96+
})
97+
}
98+
99+
func main() {
100+
app := cli.NewApp()
101+
app.Name = "remap-rootfs"
102+
app.Usage = usage
103+
104+
app.Action = func(ctx *cli.Context) error {
105+
args := ctx.Args()
106+
if len(args) != 1 {
107+
return errors.New("exactly one bundle argument must be provided")
108+
}
109+
bundle := args[0]
110+
111+
configFile, err := os.Open(filepath.Join(bundle, "config.json"))
112+
if err != nil {
113+
return err
114+
}
115+
defer configFile.Close()
116+
117+
var spec specs.Spec
118+
if err := json.NewDecoder(configFile).Decode(&spec); err != nil {
119+
return fmt.Errorf("parsing config.json: %w", err)
120+
}
121+
122+
if spec.Root == nil {
123+
return errors.New("invalid config.json: root section is null")
124+
}
125+
rootfs := filepath.Join(bundle, spec.Root.Path)
126+
127+
if spec.Linux == nil {
128+
return errors.New("invalid config.json: linux section is null")
129+
}
130+
uidMap := spec.Linux.UIDMappings
131+
gidMap := spec.Linux.GIDMappings
132+
if len(uidMap) == 0 && len(gidMap) == 0 {
133+
fmt.Println("skipping remapping -- no userns mappings specified")
134+
return nil
135+
}
136+
137+
return remapRootfs(rootfs, uidMap, gidMap)
138+
}
139+
if err := app.Run(os.Args); err != nil {
140+
fmt.Fprintln(os.Stderr, "error:", err)
141+
os.Exit(1)
142+
}
143+
}

tests/integration/helpers.bash

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ SD_HELPER="${INTEGRATION_ROOT}/../../contrib/cmd/sd-helper/sd-helper"
1818
SECCOMP_AGENT="${INTEGRATION_ROOT}/../../contrib/cmd/seccompagent/seccompagent"
1919
FS_IDMAP="${INTEGRATION_ROOT}/../../contrib/cmd/fs-idmap/fs-idmap"
2020
PIDFD_KILL="${INTEGRATION_ROOT}/../../contrib/cmd/pidfd-kill/pidfd-kill"
21+
REMAP_ROOTFS="${INTEGRATION_ROOT}/../../contrib/cmd/remap-rootfs/remap-rootfs"
2122

2223
# Some variables may not always be set. Set those to empty value,
2324
# if unset, to avoid "unbound variable" error.
@@ -657,6 +658,12 @@ function teardown_bundle() {
657658
remove_parent
658659
}
659660

661+
function remap_rootfs() {
662+
[ ! -v ROOT ] && return 0 # nothing to remap
663+
664+
"$REMAP_ROOTFS" "$ROOT/bundle"
665+
}
666+
660667
function is_kernel_gte() {
661668
local major_required minor_required
662669
major_required=$(echo "$1" | cut -d. -f1)

tests/integration/idmap.bats

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ function setup() {
1616
# Use other owner for source-2
1717
chown 1:1 source-2/foo.txt
1818

19-
mkdir -p rootfs/{proc,sys,tmp}
2019
mkdir -p rootfs/tmp/mount-{1,2}
2120
mkdir -p rootfs/mnt/bind-mount-{1,2}
2221

@@ -43,6 +42,8 @@ function setup() {
4342
]
4443
}
4544
] '
45+
46+
remap_rootfs
4647
}
4748

4849
function teardown() {

tests/integration/run.bats

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ function teardown() {
171171
update_config '.linux.namespaces += [{"type": "user"}]
172172
| .linux.uidMappings += [{"containerID": 0, "hostID": 100000, "size": 100}]
173173
| .linux.gidMappings += [{"containerID": 0, "hostID": 200000, "size": 200}]'
174-
mkdir -p rootfs/{proc,sys,tmp}
174+
remap_rootfs
175175
fi
176176
update_config '.linux.namespaces += [{"type": "time"}]
177177
| .linux.timeOffsets = {

tests/integration/timens.bats

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ load helpers
44

55
function setup() {
66
setup_busybox
7-
8-
mkdir -p rootfs/{proc,sys,tmp}
97
}
108

119
function teardown() {
@@ -63,6 +61,7 @@ function teardown() {
6361
update_config ' .linux.namespaces += [{"type": "user"}]
6462
| .linux.uidMappings += [{"hostID": 100000, "containerID": 0, "size": 65534}]
6563
| .linux.gidMappings += [{"hostID": 200000, "containerID": 0, "size": 65534}] '
64+
remap_rootfs
6665

6766
update_config '.process.args = ["cat", "/proc/self/timens_offsets"]'
6867
update_config '.linux.namespaces += [{"type": "time"}]

tests/integration/userns.bats

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ function setup() {
1212
# Permissions only to the owner, it is inaccessible to group/others
1313
chmod 700 source-inaccessible-{1,2}
1414

15-
mkdir -p rootfs/{proc,sys,tmp}
1615
mkdir -p rootfs/tmp/mount-{1,2}
1716

1817
to_umount_list="$(mktemp "$BATS_RUN_TMPDIR/userns-mounts.XXXXXX")"
1918
if [ $EUID -eq 0 ]; then
2019
update_config ' .linux.namespaces += [{"type": "user"}]
2120
| .linux.uidMappings += [{"hostID": 100000, "containerID": 0, "size": 65534}]
2221
| .linux.gidMappings += [{"hostID": 200000, "containerID": 0, "size": 65534}] '
22+
remap_rootfs
2323
fi
2424
}
2525

0 commit comments

Comments
 (0)