github.com/opencontainers/runc@v1.2.0-rc.1.0.20240520010911-492dc558cdd6/contrib/cmd/remap-rootfs/remap-rootfs.go (about)

     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  }