Skip to content

Commit e618fb9

Browse files
committed
WIP: support rootless containers
Fixes: google#311
1 parent 84e56e8 commit e618fb9

File tree

5 files changed

+155
-8
lines changed

5 files changed

+155
-8
lines changed

runsc/cmd/do.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ type Do struct {
4848
ip string
4949
quiet bool
5050
overlay bool
51+
uidMap idMapSlice
52+
gidMap idMapSlice
5153
}
5254

5355
// Name implements subcommands.Command.Name.
@@ -72,13 +74,53 @@ used for testing only.
7274
`
7375
}
7476

77+
type idMapSlice []specs.LinuxIDMapping
78+
79+
// String implements flag.Value.String.
80+
func (is *idMapSlice) String() string {
81+
return fmt.Sprintf("%#v", is)
82+
}
83+
84+
// Get implements flag.Value.Get.
85+
func (is *idMapSlice) Get() interface{} {
86+
return is
87+
}
88+
89+
// Set implements flag.Value.Set.
90+
func (is *idMapSlice) Set(s string) error {
91+
fs := strings.Fields(s)
92+
if len(fs) != 3 {
93+
return fmt.Errorf("invalid mapping: %s", s)
94+
}
95+
var cid, hid, size int
96+
var err error
97+
if cid, err = strconv.Atoi(fs[0]); err != nil {
98+
return fmt.Errorf("invalid mapping: %s", s)
99+
}
100+
if hid, err = strconv.Atoi(fs[1]); err != nil {
101+
return fmt.Errorf("invalid mapping: %s", s)
102+
}
103+
if size, err = strconv.Atoi(fs[2]); err != nil {
104+
return fmt.Errorf("invalid mapping: %s", s)
105+
}
106+
m := specs.LinuxIDMapping{
107+
ContainerID: uint32(cid),
108+
HostID: uint32(hid),
109+
Size: uint32(size),
110+
}
111+
*is = append(*is, m)
112+
return nil
113+
}
114+
75115
// SetFlags implements subcommands.Command.SetFlags.
76116
func (c *Do) SetFlags(f *flag.FlagSet) {
77117
f.StringVar(&c.root, "root", "/", `path to the root directory, defaults to "/"`)
78118
f.StringVar(&c.cwd, "cwd", ".", "path to the current directory, defaults to the current directory")
79119
f.StringVar(&c.ip, "ip", "192.168.10.2", "IPv4 address for the sandbox")
80120
f.BoolVar(&c.quiet, "quiet", false, "suppress runsc messages to stdout. Application output is still sent to stdout and stderr")
81121
f.BoolVar(&c.overlay, "force-overlay", true, "use an overlay. WARNING: disabling gives the command write access to the host")
122+
f.Var(&c.uidMap, "uid-map", "Add a user id mapping [ContainerID, HostID, Size]")
123+
f.Var(&c.gidMap, "gid-map", "Add a group id mapping [ContainerID, HostID, Size]")
82124
}
83125

84126
// Execute implements subcommands.Command.Execute.
@@ -129,6 +171,12 @@ func (c *Do) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) su
129171

130172
cid := fmt.Sprintf("runsc-%06d", rand.Int31n(1000000))
131173

174+
if c.uidMap != nil {
175+
addNamespace(spec, specs.LinuxNamespace{Type: specs.UserNamespace})
176+
spec.Linux.UIDMappings = c.uidMap
177+
spec.Linux.GIDMappings = c.gidMap
178+
}
179+
132180
if conf.Network == config.NetworkNone {
133181
addNamespace(spec, specs.LinuxNamespace{Type: specs.NetworkNamespace})
134182
} else if conf.Rootless {

runsc/cmd/gofer.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ import (
1818
"context"
1919
"encoding/json"
2020
"fmt"
21+
"io"
2122
"os"
2223
"path/filepath"
2324
"runtime/debug"
25+
"runtime"
2426
"strings"
2527

2628
"github.com/google/subcommands"
@@ -65,6 +67,7 @@ type Gofer struct {
6567

6668
specFD int
6769
mountsFD int
70+
syncFD int
6871
}
6972

7073
// Name implements subcommands.Command.
@@ -92,6 +95,7 @@ func (g *Gofer) SetFlags(f *flag.FlagSet) {
9295
f.Var(&g.ioFDs, "io-fds", "list of FDs to connect gofer servers. They must follow this order: root first, then mounts as defined in the spec")
9396
f.IntVar(&g.specFD, "spec-fd", -1, "required fd with the container spec")
9497
f.IntVar(&g.mountsFD, "mounts-fd", -1, "mountsFD is the file descriptor to write list of mounts after they have been resolved (direct paths, no symlinks).")
98+
f.IntVar(&g.syncFD, "sync-fd", -1, "")
9599
}
96100

97101
// Execute implements subcommands.Command.
@@ -113,6 +117,26 @@ func (g *Gofer) Execute(_ context.Context, f *flag.FlagSet, args ...interface{})
113117
util.Fatalf("reading spec: %v", err)
114118
}
115119

120+
if g.syncFD >= 0 {
121+
f := os.NewFile(uintptr(g.syncFD), "sync FD")
122+
defer f.Close()
123+
var b [1]byte
124+
if n, err := f.Read(b[:]); n != 0 || err != io.EOF {
125+
util.Fatalf("failed to sync: %v: %v", n, err)
126+
}
127+
128+
f.Close()
129+
// SETUID changes UID on the current system thread, so we have
130+
// to re-execute current binary.
131+
runtime.LockOSThread()
132+
if _, _, errno := unix.RawSyscall(unix.SYS_SETUID, 0, 0, 0); errno != 0 {
133+
util.Fatalf("failed to set UID: %v", errno)
134+
}
135+
if _, _, errno := unix.RawSyscall(unix.SYS_SETGID, 0, 0, 0); errno != 0 {
136+
util.Fatalf("failed to set GID: %v", errno)
137+
}
138+
}
139+
116140
if g.setUpRoot {
117141
if err := setupRootFS(spec, conf); err != nil {
118142
util.Fatalf("Error setting up root FS: %v", err)
@@ -122,7 +146,7 @@ func (g *Gofer) Execute(_ context.Context, f *flag.FlagSet, args ...interface{})
122146
// Disable caps when calling myself again.
123147
// Note: minimal argument handling for the default case to keep it simple.
124148
args := os.Args
125-
args = append(args, "--apply-caps=false", "--setup-root=false")
149+
args = append(args, "--apply-caps=false", "--setup-root=false", "--sync-fd=-1")
126150
util.Fatalf("setCapsAndCallSelf(%v, %v): %v", args, goferCaps, setCapsAndCallSelf(args, goferCaps))
127151
panic("unreachable")
128152
}

runsc/container/container.go

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -971,15 +971,49 @@ func (c *Container) createGoferProcess(spec *specs.Spec, conf *config.Config, bu
971971
{Type: specs.UTSNamespace},
972972
}
973973

974+
rootless := unix.Getuid() != 0
975+
var syncFile *os.File
974976
// Setup any uid/gid mappings, and create or join the configured user
975977
// namespace so the gofer's view of the filesystem aligns with the
976978
// users in the sandbox.
977-
userNS := specutils.FilterNS([]specs.LinuxNamespaceType{specs.UserNamespace}, spec)
978-
nss = append(nss, userNS...)
979-
specutils.SetUIDGIDMappings(cmd, spec)
980-
if len(userNS) != 0 {
981-
// We need to set UID and GID to have capabilities in a new user namespace.
982-
cmd.SysProcAttr.Credential = &syscall.Credential{Uid: 0, Gid: 0}
979+
if !rootless {
980+
userNS := specutils.FilterNS([]specs.LinuxNamespaceType{specs.UserNamespace}, spec)
981+
nss = append(nss, userNS...)
982+
specutils.SetUIDGIDMappings(cmd, spec)
983+
if len(userNS) != 0 {
984+
// We need to set UID and GID to have capabilities in a new user namespace.
985+
cmd.SysProcAttr.Credential = &syscall.Credential{Uid: 0, Gid: 0}
986+
}
987+
} else {
988+
userNS := specutils.FilterNS([]specs.LinuxNamespaceType{specs.UserNamespace}, spec)
989+
if len(userNS) == 0 {
990+
return nil, nil, fmt.Errorf("unable to run a rootless container without userns")
991+
}
992+
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
993+
if err != nil {
994+
return nil, nil, err
995+
}
996+
syncFile = os.NewFile(uintptr(fds[0]), "sync FD")
997+
defer syncFile.Close()
998+
999+
f := os.NewFile(uintptr(fds[1]), "sync other FD")
1000+
donations.DonateAndClose("sync-fd", f)
1001+
if cmd.SysProcAttr == nil {
1002+
cmd.SysProcAttr = &unix.SysProcAttr{}
1003+
}
1004+
cmd.SysProcAttr.AmbientCaps = []uintptr{
1005+
unix.CAP_CHOWN,
1006+
unix.CAP_DAC_OVERRIDE,
1007+
unix.CAP_DAC_READ_SEARCH,
1008+
unix.CAP_FOWNER,
1009+
unix.CAP_FSETID,
1010+
unix.CAP_SYS_CHROOT,
1011+
unix.CAP_SETUID,
1012+
unix.CAP_SETGID,
1013+
unix.CAP_SYS_ADMIN,
1014+
unix.CAP_SETPCAP,
1015+
}
1016+
nss = append(nss, specs.LinuxNamespace{Type: specs.UserNamespace})
9831017
}
9841018

9851019
donations.Transfer(cmd, nextFD)
@@ -990,6 +1024,43 @@ func (c *Container) createGoferProcess(spec *specs.Spec, conf *config.Config, bu
9901024
if err := specutils.StartInNS(cmd, nss); err != nil {
9911025
return nil, nil, fmt.Errorf("gofer: %v", err)
9921026
}
1027+
1028+
if rootless {
1029+
log.Debugf("Setting user mappings")
1030+
args := []string{strconv.Itoa(cmd.Process.Pid)}
1031+
for _, idMap := range spec.Linux.UIDMappings {
1032+
log.Infof("Mapping host uid %d to container uid %d (size=%d)",
1033+
idMap.HostID, idMap.ContainerID, idMap.Size)
1034+
args = append(args,
1035+
strconv.Itoa(int(idMap.ContainerID)),
1036+
strconv.Itoa(int(idMap.HostID)),
1037+
strconv.Itoa(int(idMap.Size)),
1038+
)
1039+
}
1040+
1041+
out, err := exec.Command("newuidmap", args...).CombinedOutput()
1042+
log.Debugf("newuidmap: %#v\n%s", args, out)
1043+
if err != nil {
1044+
return nil, nil, fmt.Errorf("newuidmap failed: %w", err)
1045+
}
1046+
1047+
args = []string{strconv.Itoa(cmd.Process.Pid)}
1048+
for _, idMap := range spec.Linux.GIDMappings {
1049+
log.Infof("Mapping host uid %d to container uid %d (size=%d)",
1050+
idMap.HostID, idMap.ContainerID, idMap.Size)
1051+
args = append(args,
1052+
strconv.Itoa(int(idMap.ContainerID)),
1053+
strconv.Itoa(int(idMap.HostID)),
1054+
strconv.Itoa(int(idMap.Size)),
1055+
)
1056+
}
1057+
log.Debugf("newgidmap: %#v\n%s", args, out)
1058+
out, err = exec.Command("newgidmap", args...).CombinedOutput()
1059+
if err != nil {
1060+
return nil, nil, fmt.Errorf("newgidmap failed: %w", err)
1061+
}
1062+
}
1063+
9931064
log.Infof("Gofer started, PID: %d", cmd.Process.Pid)
9941065
c.GoferPid = cmd.Process.Pid
9951066
c.goferIsChild = true

runsc/sandbox/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ go_library(
2929
"//pkg/tcpip/stack",
3030
"//pkg/unet",
3131
"//pkg/urpc",
32+
"//pkg/abi/linux",
3233
"//runsc/boot",
3334
"//runsc/boot/procfs",
3435
"//runsc/cgroup",

runsc/sandbox/sandbox.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,9 @@ func (s *Sandbox) createSandboxProcess(conf *config.Config, args *Args, startSyn
536536
donations := donation.Agency{}
537537
defer donations.Close()
538538

539+
if unix.Getuid() != 0 {
540+
conf.Rootless = true
541+
}
539542
//
540543
// These flags must come BEFORE the "boot" command in cmd.Args.
541544
//
@@ -722,7 +725,7 @@ func (s *Sandbox) createSandboxProcess(conf *config.Config, args *Args, startSyn
722725
if conf.TestOnlyAllowRunAsCurrentUserWithoutChroot {
723726
log.Warningf("Running sandbox in test mode as current user (uid=%d gid=%d). This is only safe in tests!", os.Getuid(), os.Getgid())
724727
log.Warningf("Running sandbox in test mode without chroot. This is only safe in tests!")
725-
} else if specutils.HasCapabilities(capability.CAP_SETUID, capability.CAP_SETGID) {
728+
} else if conf.Rootless || specutils.HasCapabilities(capability.CAP_SETUID, capability.CAP_SETGID) {
726729
log.Infof("Sandbox will be started in new user namespace")
727730
nss = append(nss, specs.LinuxNamespace{Type: specs.UserNamespace})
728731
cmd.Args = append(cmd.Args, "--setup-root")

0 commit comments

Comments
 (0)