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