Skip to content

Commit d2f4969

Browse files
committed
runc: add support for rootless containers
This enables the support for the rootless container mode. There are many restrictions on what rootless containers can do, so many different runC commands have been disabled: * runc checkpoint * runc events * runc pause * runc ps * runc restore * runc resume * runc update The following commands work: * runc create * runc delete * runc exec * runc kill * runc list * runc run * runc spec * runc state In addition, any specification options that imply joining cgroups have also been disabled. This is due to support for unprivileged subtree management not being available from Linux upstream. Signed-off-by: Aleksa Sarai <[email protected]>
1 parent 6bd4bd9 commit d2f4969

21 files changed

+742
-193
lines changed

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
SOURCES := $(shell find . 2>&1 | grep -E '.*\.(c|h|go)$$')
66
PREFIX := $(DESTDIR)/usr/local
7-
BINDIR := $(PREFIX)/sbin
7+
BINDIR := $(PREFIX)/bin
88
GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null)
99
GIT_BRANCH_CLEAN := $(shell echo $(GIT_BRANCH) | sed -e "s/[^[:alnum:]]/-/g")
1010
RUNC_IMAGE := runc_dev$(if $(GIT_BRANCH_CLEAN),:$(GIT_BRANCH_CLEAN))

checkpoint.go

+5
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ checkpointed.`,
3939
if err := checkArgs(context, 1, exactArgs); err != nil {
4040
return err
4141
}
42+
// XXX: Currently this is untested with rootless containers.
43+
if isRootless() {
44+
return fmt.Errorf("runc checkpoint requires root")
45+
}
46+
4247
container, err := getContainer(context)
4348
if err != nil {
4449
return err

exec.go

-3
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,6 @@ following will output a list of processes running in the container:
9090
if err := checkArgs(context, 1, minArgs); err != nil {
9191
return err
9292
}
93-
if os.Geteuid() != 0 {
94-
return fmt.Errorf("runc should be run as root")
95-
}
9693
if err := revisePidFile(context); err != nil {
9794
return err
9895
}

libcontainer/configs/config.go

+3
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,9 @@ type Config struct {
183183
// NoNewKeyring will not allocated a new session keyring for the container. It will use the
184184
// callers keyring in this case.
185185
NoNewKeyring bool `json:"no_new_keyring"`
186+
187+
// Rootless specifies whether the container is a rootless container.
188+
Rootless bool `json:"rootless"`
186189
}
187190

188191
type Hooks struct {
+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package validate
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"reflect"
7+
"strings"
8+
9+
"github.com/opencontainers/runc/libcontainer/configs"
10+
)
11+
12+
var (
13+
geteuid = os.Geteuid
14+
getegid = os.Getegid
15+
)
16+
17+
func (v *ConfigValidator) rootless(config *configs.Config) error {
18+
if err := rootlessMappings(config); err != nil {
19+
return err
20+
}
21+
if err := rootlessMount(config); err != nil {
22+
return err
23+
}
24+
// Currently, cgroups cannot effectively be used in rootless containers.
25+
// The new cgroup namespace doesn't really help us either because it doesn't
26+
// have nice interactions with the user namespace (we're working with upstream
27+
// to fix this).
28+
if err := rootlessCgroup(config); err != nil {
29+
return err
30+
}
31+
32+
// XXX: We currently can't verify the user config at all, because
33+
// configs.Config doesn't store the user-related configs. So this
34+
// has to be verified by setupUser() in init_linux.go.
35+
36+
return nil
37+
}
38+
39+
func rootlessMappings(config *configs.Config) error {
40+
rootuid, err := config.HostUID()
41+
if err != nil {
42+
return fmt.Errorf("failed to get root uid from uidMappings: %v", err)
43+
}
44+
if euid := geteuid(); euid != 0 {
45+
if !config.Namespaces.Contains(configs.NEWUSER) {
46+
return fmt.Errorf("rootless containers require user namespaces")
47+
}
48+
if rootuid != euid {
49+
return fmt.Errorf("rootless containers cannot map container root to a different host user")
50+
}
51+
}
52+
53+
rootgid, err := config.HostGID()
54+
if err != nil {
55+
return fmt.Errorf("failed to get root gid from gidMappings: %v", err)
56+
}
57+
58+
// Similar to the above test, we need to make sure that we aren't trying to
59+
// map to a group ID that we don't have the right to be.
60+
if rootgid != getegid() {
61+
return fmt.Errorf("rootless containers cannot map container root to a different host group")
62+
}
63+
64+
// We can only map one user and group inside a container (our own).
65+
if len(config.UidMappings) != 1 || config.UidMappings[0].Size != 1 {
66+
return fmt.Errorf("rootless containers cannot map more than one user")
67+
}
68+
if len(config.GidMappings) != 1 || config.GidMappings[0].Size != 1 {
69+
return fmt.Errorf("rootless containers cannot map more than one group")
70+
}
71+
72+
return nil
73+
}
74+
75+
// cgroup verifies that the user isn't trying to set any cgroup limits or paths.
76+
func rootlessCgroup(config *configs.Config) error {
77+
// Nothing set at all.
78+
if config.Cgroups == nil || config.Cgroups.Resources == nil {
79+
return nil
80+
}
81+
82+
// Used for comparing to the zero value.
83+
left := reflect.ValueOf(*config.Cgroups.Resources)
84+
right := reflect.Zero(left.Type())
85+
86+
// This is all we need to do, since specconv won't add cgroup options in
87+
// rootless mode.
88+
if !reflect.DeepEqual(left.Interface(), right.Interface()) {
89+
return fmt.Errorf("cannot specify resource limits in rootless container")
90+
}
91+
92+
return nil
93+
}
94+
95+
// mount verifies that the user isn't trying to set up any mounts they don't have
96+
// the rights to do. In addition, it makes sure that no mount has a `uid=` or
97+
// `gid=` option that doesn't resolve to root.
98+
func rootlessMount(config *configs.Config) error {
99+
// XXX: We could whitelist allowed devices at this point, but I'm not
100+
// convinced that's a good idea. The kernel is the best arbiter of
101+
// access control.
102+
103+
for _, mount := range config.Mounts {
104+
// Check that the options list doesn't contain any uid= or gid= entries
105+
// that don't resolve to root.
106+
for _, opt := range strings.Split(mount.Data, ",") {
107+
if strings.HasPrefix(opt, "uid=") && opt != "uid=0" {
108+
return fmt.Errorf("cannot specify uid= mount options in rootless containers where argument isn't 0")
109+
}
110+
if strings.HasPrefix(opt, "gid=") && opt != "gid=0" {
111+
return fmt.Errorf("cannot specify gid= mount options in rootless containers where argument isn't 0")
112+
}
113+
}
114+
}
115+
116+
return nil
117+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package validate
2+
3+
import (
4+
"testing"
5+
6+
"github.com/opencontainers/runc/libcontainer/configs"
7+
)
8+
9+
func init() {
10+
geteuid = func() int { return 1337 }
11+
getegid = func() int { return 7331 }
12+
}
13+
14+
func rootlessConfig() *configs.Config {
15+
return &configs.Config{
16+
Rootfs: "/var",
17+
Rootless: true,
18+
Namespaces: configs.Namespaces(
19+
[]configs.Namespace{
20+
{Type: configs.NEWUSER},
21+
},
22+
),
23+
UidMappings: []configs.IDMap{
24+
{
25+
HostID: geteuid(),
26+
ContainerID: 0,
27+
Size: 1,
28+
},
29+
},
30+
GidMappings: []configs.IDMap{
31+
{
32+
HostID: getegid(),
33+
ContainerID: 0,
34+
Size: 1,
35+
},
36+
},
37+
}
38+
}
39+
40+
func TestValidateRootless(t *testing.T) {
41+
validator := New()
42+
43+
config := rootlessConfig()
44+
if err := validator.Validate(config); err != nil {
45+
t.Errorf("Expected error to not occur: %+v", err)
46+
}
47+
}
48+
49+
/* rootlessMappings() */
50+
51+
func TestValidateRootlessUserns(t *testing.T) {
52+
validator := New()
53+
54+
config := rootlessConfig()
55+
config.Namespaces = nil
56+
if err := validator.Validate(config); err == nil {
57+
t.Errorf("Expected error to occur if user namespaces not set")
58+
}
59+
}
60+
61+
func TestValidateRootlessMappingUid(t *testing.T) {
62+
validator := New()
63+
64+
config := rootlessConfig()
65+
config.UidMappings = nil
66+
if err := validator.Validate(config); err == nil {
67+
t.Errorf("Expected error to occur if no uid mappings provided")
68+
}
69+
70+
config = rootlessConfig()
71+
config.UidMappings[0].HostID = geteuid() + 1
72+
if err := validator.Validate(config); err == nil {
73+
t.Errorf("Expected error to occur if geteuid() != mapped uid")
74+
}
75+
76+
config = rootlessConfig()
77+
config.UidMappings[0].Size = 1024
78+
if err := validator.Validate(config); err == nil {
79+
t.Errorf("Expected error to occur if more than one uid mapped")
80+
}
81+
82+
config = rootlessConfig()
83+
config.UidMappings = append(config.UidMappings, configs.IDMap{
84+
HostID: geteuid() + 1,
85+
ContainerID: 0,
86+
Size: 1,
87+
})
88+
if err := validator.Validate(config); err == nil {
89+
t.Errorf("Expected error to occur if more than one uid extent mapped")
90+
}
91+
}
92+
93+
func TestValidateRootlessMappingGid(t *testing.T) {
94+
validator := New()
95+
96+
config := rootlessConfig()
97+
config.GidMappings = nil
98+
if err := validator.Validate(config); err == nil {
99+
t.Errorf("Expected error to occur if no gid mappings provided")
100+
}
101+
102+
config = rootlessConfig()
103+
config.GidMappings[0].HostID = getegid() + 1
104+
if err := validator.Validate(config); err == nil {
105+
t.Errorf("Expected error to occur if getegid() != mapped gid")
106+
}
107+
108+
config = rootlessConfig()
109+
config.GidMappings[0].Size = 1024
110+
if err := validator.Validate(config); err == nil {
111+
t.Errorf("Expected error to occur if more than one gid mapped")
112+
}
113+
114+
config = rootlessConfig()
115+
config.GidMappings = append(config.GidMappings, configs.IDMap{
116+
HostID: getegid() + 1,
117+
ContainerID: 0,
118+
Size: 1,
119+
})
120+
if err := validator.Validate(config); err == nil {
121+
t.Errorf("Expected error to occur if more than one gid extent mapped")
122+
}
123+
}
124+
125+
/* rootlessMount() */
126+
127+
func TestValidateRootlessMountUid(t *testing.T) {
128+
config := rootlessConfig()
129+
validator := New()
130+
131+
config.Mounts = []*configs.Mount{
132+
{
133+
Source: "devpts",
134+
Destination: "/dev/pts",
135+
Device: "devpts",
136+
},
137+
}
138+
139+
if err := validator.Validate(config); err != nil {
140+
t.Errorf("Expected error to not occur when uid= not set in mount options: %+v", err)
141+
}
142+
143+
config.Mounts[0].Data = "uid=5"
144+
if err := validator.Validate(config); err == nil {
145+
t.Errorf("Expected error to occur when setting uid=5 in mount options")
146+
}
147+
148+
config.Mounts[0].Data = "uid=0"
149+
if err := validator.Validate(config); err != nil {
150+
t.Errorf("Expected error to not occur when setting uid=0 in mount options: %+v", err)
151+
}
152+
}
153+
154+
func TestValidateRootlessMountGid(t *testing.T) {
155+
config := rootlessConfig()
156+
validator := New()
157+
158+
config.Mounts = []*configs.Mount{
159+
{
160+
Source: "devpts",
161+
Destination: "/dev/pts",
162+
Device: "devpts",
163+
},
164+
}
165+
166+
if err := validator.Validate(config); err != nil {
167+
t.Errorf("Expected error to not occur when gid= not set in mount options: %+v", err)
168+
}
169+
170+
config.Mounts[0].Data = "gid=5"
171+
if err := validator.Validate(config); err == nil {
172+
t.Errorf("Expected error to occur when setting gid=5 in mount options")
173+
}
174+
175+
config.Mounts[0].Data = "gid=0"
176+
if err := validator.Validate(config); err != nil {
177+
t.Errorf("Expected error to not occur when setting gid=0 in mount options: %+v", err)
178+
}
179+
}
180+
181+
/* rootlessCgroup() */
182+
183+
func TestValidateRootlessCgroup(t *testing.T) {
184+
validator := New()
185+
186+
config := rootlessConfig()
187+
config.Cgroups = &configs.Cgroup{
188+
Resources: &configs.Resources{
189+
PidsLimit: 1337,
190+
},
191+
}
192+
if err := validator.Validate(config); err == nil {
193+
t.Errorf("Expected error to occur if cgroup limits set")
194+
}
195+
}

libcontainer/configs/validate/validator.go

+5
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ func (v *ConfigValidator) Validate(config *configs.Config) error {
4040
if err := v.sysctl(config); err != nil {
4141
return err
4242
}
43+
if config.Rootless {
44+
if err := v.rootless(config); err != nil {
45+
return err
46+
}
47+
}
4348
return nil
4449
}
4550

0 commit comments

Comments
 (0)