File: remap-rootfs.go

package info (click to toggle)
runc 1.3.2%2Bds1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,684 kB
  • sloc: sh: 2,267; ansic: 1,125; makefile: 200
file content (143 lines) | stat: -rw-r--r-- 3,806 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
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 = `tests/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: uint64(st.Dev), Ino: st.Ino} // on mips64le, "stat.Dev" is a uint32
}

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)
	}
}