Skip to content

Commit 06f789c

Browse files
committed
Disable rootless mode except RootlessCgMgr when executed as the root in userns
This PR decomposes `libcontainer/configs.Config.Rootless bool` into `RootlessEUID bool` and `RootlessCgroups bool`, so as to make "runc-in-userns" to be more compatible with "rootful" runc. `RootlessEUID` denotes that runc is being executed as a non-root user (euid != 0) in the current user namespace. `RootlessEUID` is almost identical to the former `Rootless` except cgroups stuff. `RootlessCgroups` denotes that runc is unlikely to have the full access to cgroups. `RootlessCgroups` is set to false if runc is executed as the root (euid == 0) in the initial namespace. Otherwise `RootlessCgroups` is set to true. (Hint: if `RootlessEUID` is true, `RootlessCgroups` becomes true as well) When runc is executed as the root (euid == 0) in an user namespace (e.g. by Docker-in-LXD, Podman, Usernetes), `RootlessEUID` is set to false but `RootlessCgroups` is set to true. So, "runc-in-userns" behaves almost same as "rootful" runc except that cgroups errors are ignored. This PR does not have any impact on CLI flags and `state.json`. Note about CLI: * Now `runc --rootless=(auto|true|false)` CLI flag is only used for setting `RootlessCgroups`. * Now `runc spec --rootless` is only required when `RootlessEUID` is set to true. For runc-in-userns, `runc spec` without `--rootless` should work, when sufficient numbers of UID/GID are mapped. Note about `$XDG_RUNTIME_DIR` (e.g. `/run/user/1000`): * `$XDG_RUNTIME_DIR` is ignored if runc is being executed as the root (euid == 0) in the initial namespace, for backward compatibility. (`/run/runc` is used) * If runc is executed as the root (euid == 0) in an user namespace, `$XDG_RUNTIME_DIR` is honored if `$USER != "" && $USER != "root"`. This allows unprivileged users to allow execute runc as the root in userns, without mounting writable `/run/runc`. Note about `state.json`: * `rootless` is set to true when `RootlessEUID == true && RootlessCgroups == true`. Signed-off-by: Akihiro Suda <[email protected]>
1 parent 70ca035 commit 06f789c

22 files changed

+231
-198
lines changed

checkpoint.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ package main
44

55
import (
66
"fmt"
7+
"os"
78
"strconv"
89
"strings"
910

1011
"github.com/opencontainers/runc/libcontainer"
12+
"github.com/opencontainers/runc/libcontainer/system"
1113
"github.com/opencontainers/runtime-spec/specs-go"
14+
"github.com/sirupsen/logrus"
1215
"github.com/urfave/cli"
1316

1417
"golang.org/x/sys/unix"
@@ -44,12 +47,8 @@ checkpointed.`,
4447
return err
4548
}
4649
// XXX: Currently this is untested with rootless containers.
47-
rootless, err := isRootless(context)
48-
if err != nil {
49-
return err
50-
}
51-
if rootless {
52-
return fmt.Errorf("runc checkpoint requires root")
50+
if os.Geteuid() != 0 || system.RunningInUserNS() {
51+
logrus.Warn("runc checkpoint is untested with rootless containers")
5352
}
5453

5554
container, err := getContainer(context)

libcontainer/cgroups/fs/apply_raw.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ type subsystem interface {
6565
type Manager struct {
6666
mu sync.Mutex
6767
Cgroups *configs.Cgroup
68-
Rootless bool
68+
Rootless bool // ignore permission-related errors
6969
Paths map[string]string
7070
}
7171

@@ -174,7 +174,7 @@ func (m *Manager) Apply(pid int) (err error) {
174174
m.Paths[sys.Name()] = p
175175

176176
if err := sys.Apply(d); err != nil {
177-
// In the case of rootless, where an explicit cgroup path hasn't
177+
// In the case of rootless (including euid=0 in userns), where an explicit cgroup path hasn't
178178
// been set, we don't bail on error in case of permission problems.
179179
// Cases where limits have been set (and we couldn't create our own
180180
// cgroup) are handled by Set.
@@ -236,6 +236,12 @@ func (m *Manager) Set(container *configs.Config) error {
236236
for _, sys := range subsystems {
237237
path := paths[sys.Name()]
238238
if err := sys.Set(path, container.Cgroups); err != nil {
239+
if m.Rootless && sys.Name() == "devices" {
240+
continue
241+
}
242+
// When m.Rootless is true, errors from the device subsystem are ignored because it is really not expected to work.
243+
// However, errors from other subsystems are not ignored.
244+
// see @test "runc create (rootless + limits + no cgrouppath + no permission) fails with informative error"
239245
if path == "" {
240246
// We never created a path for this cgroup, so we cannot set
241247
// limits for it (though we have already tried at this point).

libcontainer/configs/config.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,12 +186,19 @@ type Config struct {
186186
// callers keyring in this case.
187187
NoNewKeyring bool `json:"no_new_keyring"`
188188

189-
// Rootless specifies whether the container is a rootless container.
190-
Rootless bool `json:"rootless"`
191-
192189
// IntelRdt specifies settings for Intel RDT/CAT group that the container is placed into
193190
// to limit the resources (e.g., L3 cache) the container has available
194191
IntelRdt *IntelRdt `json:"intel_rdt,omitempty"`
192+
193+
// RootlessEUID is set when the runc was launched with non-zero EUID.
194+
// Note that RootlessEUID is set to false when launched with EUID=0 in userns.
195+
// When RootlessEUID is set, runc creates a new userns for the container.
196+
// (config.json needs to contain userns settings)
197+
RootlessEUID bool `json:"rootless_euid,omitempty"`
198+
199+
// RootlessCgroups is set when unlikely to have the full access to cgroups.
200+
// When RootlessCgroups is set, cgroups errors are ignored.
201+
RootlessCgroups bool `json:"rootless_cgroups,omitempty"`
195202
}
196203

197204
type Hooks struct {

libcontainer/configs/validate/rootless.go

Lines changed: 13 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,18 @@ package validate
22

33
import (
44
"fmt"
5-
"os"
6-
"reflect"
75
"strings"
86

97
"github.com/opencontainers/runc/libcontainer/configs"
108
)
119

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 {
10+
// rootlessEUID makes sure that the config can be applied when runc
11+
// is being executed as a non-root user (euid != 0) in the current user namespace.
12+
func (v *ConfigValidator) rootlessEUID(config *configs.Config) error {
13+
if err := rootlessEUIDMappings(config); err != nil {
1914
return err
2015
}
21-
if err := rootlessMount(config); err != nil {
16+
if err := rootlessEUIDMount(config); err != nil {
2217
return err
2318
}
2419

@@ -38,46 +33,24 @@ func hasIDMapping(id int, mappings []configs.IDMap) bool {
3833
return false
3934
}
4035

41-
func rootlessMappings(config *configs.Config) error {
42-
if euid := geteuid(); euid != 0 {
43-
if !config.Namespaces.Contains(configs.NEWUSER) {
44-
return fmt.Errorf("rootless containers require user namespaces")
45-
}
46-
if len(config.UidMappings) == 0 {
47-
return fmt.Errorf("rootless containers requires at least one UID mapping")
48-
}
49-
if len(config.GidMappings) == 0 {
50-
return fmt.Errorf("rootless containers requires at least one GID mapping")
51-
}
36+
func rootlessEUIDMappings(config *configs.Config) error {
37+
if !config.Namespaces.Contains(configs.NEWUSER) {
38+
return fmt.Errorf("rootless container requires user namespaces")
5239
}
5340

54-
return nil
55-
}
56-
57-
// cgroup verifies that the user isn't trying to set any cgroup limits or paths.
58-
func rootlessCgroup(config *configs.Config) error {
59-
// Nothing set at all.
60-
if config.Cgroups == nil || config.Cgroups.Resources == nil {
61-
return nil
41+
if len(config.UidMappings) == 0 {
42+
return fmt.Errorf("rootless containers requires at least one UID mapping")
6243
}
63-
64-
// Used for comparing to the zero value.
65-
left := reflect.ValueOf(*config.Cgroups.Resources)
66-
right := reflect.Zero(left.Type())
67-
68-
// This is all we need to do, since specconv won't add cgroup options in
69-
// rootless mode.
70-
if !reflect.DeepEqual(left.Interface(), right.Interface()) {
71-
return fmt.Errorf("cannot specify resource limits in rootless container")
44+
if len(config.GidMappings) == 0 {
45+
return fmt.Errorf("rootless containers requires at least one GID mapping")
7246
}
73-
7447
return nil
7548
}
7649

7750
// mount verifies that the user isn't trying to set up any mounts they don't have
7851
// the rights to do. In addition, it makes sure that no mount has a `uid=` or
7952
// `gid=` option that doesn't resolve to root.
80-
func rootlessMount(config *configs.Config) error {
53+
func rootlessEUIDMount(config *configs.Config) error {
8154
// XXX: We could whitelist allowed devices at this point, but I'm not
8255
// convinced that's a good idea. The kernel is the best arbiter of
8356
// access control.

libcontainer/configs/validate/rootless_test.go

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,82 +6,78 @@ import (
66
"github.com/opencontainers/runc/libcontainer/configs"
77
)
88

9-
func init() {
10-
geteuid = func() int { return 1337 }
11-
getegid = func() int { return 7331 }
12-
}
13-
14-
func rootlessConfig() *configs.Config {
9+
func rootlessEUIDConfig() *configs.Config {
1510
return &configs.Config{
16-
Rootfs: "/var",
17-
Rootless: true,
11+
Rootfs: "/var",
12+
RootlessEUID: true,
13+
RootlessCgroups: true,
1814
Namespaces: configs.Namespaces(
1915
[]configs.Namespace{
2016
{Type: configs.NEWUSER},
2117
},
2218
),
2319
UidMappings: []configs.IDMap{
2420
{
25-
HostID: geteuid(),
21+
HostID: 1337,
2622
ContainerID: 0,
2723
Size: 1,
2824
},
2925
},
3026
GidMappings: []configs.IDMap{
3127
{
32-
HostID: getegid(),
28+
HostID: 7331,
3329
ContainerID: 0,
3430
Size: 1,
3531
},
3632
},
3733
}
3834
}
3935

40-
func TestValidateRootless(t *testing.T) {
36+
func TestValidateRootlessEUID(t *testing.T) {
4137
validator := New()
4238

43-
config := rootlessConfig()
39+
config := rootlessEUIDConfig()
4440
if err := validator.Validate(config); err != nil {
4541
t.Errorf("Expected error to not occur: %+v", err)
4642
}
4743
}
4844

49-
/* rootlessMappings() */
45+
/* rootlessEUIDMappings */
5046

51-
func TestValidateRootlessUserns(t *testing.T) {
47+
func TestValidateRootlessEUIDUserns(t *testing.T) {
5248
validator := New()
5349

54-
config := rootlessConfig()
50+
config := rootlessEUIDConfig()
5551
config.Namespaces = nil
5652
if err := validator.Validate(config); err == nil {
5753
t.Errorf("Expected error to occur if user namespaces not set")
5854
}
5955
}
6056

61-
func TestValidateRootlessMappingUid(t *testing.T) {
57+
func TestValidateRootlessEUIDMappingUid(t *testing.T) {
6258
validator := New()
6359

64-
config := rootlessConfig()
60+
config := rootlessEUIDConfig()
6561
config.UidMappings = nil
6662
if err := validator.Validate(config); err == nil {
6763
t.Errorf("Expected error to occur if no uid mappings provided")
6864
}
6965
}
7066

71-
func TestValidateRootlessMappingGid(t *testing.T) {
67+
func TestValidateNonZeroEUIDMappingGid(t *testing.T) {
7268
validator := New()
7369

74-
config := rootlessConfig()
70+
config := rootlessEUIDConfig()
7571
config.GidMappings = nil
7672
if err := validator.Validate(config); err == nil {
7773
t.Errorf("Expected error to occur if no gid mappings provided")
7874
}
7975
}
8076

81-
/* rootlessMount() */
77+
/* rootlessEUIDMount() */
8278

83-
func TestValidateRootlessMountUid(t *testing.T) {
84-
config := rootlessConfig()
79+
func TestValidateRootlessEUIDMountUid(t *testing.T) {
80+
config := rootlessEUIDConfig()
8581
validator := New()
8682

8783
config.Mounts = []*configs.Mount{
@@ -119,8 +115,8 @@ func TestValidateRootlessMountUid(t *testing.T) {
119115
}
120116
}
121117

122-
func TestValidateRootlessMountGid(t *testing.T) {
123-
config := rootlessConfig()
118+
func TestValidateRootlessEUIDMountGid(t *testing.T) {
119+
config := rootlessEUIDConfig()
124120
validator := New()
125121

126122
config.Mounts = []*configs.Mount{

libcontainer/configs/validate/validator.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ func (v *ConfigValidator) Validate(config *configs.Config) error {
4444
if err := v.intelrdt(config); err != nil {
4545
return err
4646
}
47-
if config.Rootless {
48-
if err := v.rootless(config); err != nil {
47+
if config.RootlessEUID {
48+
if err := v.rootlessEUID(config); err != nil {
4949
return err
5050
}
5151
}

0 commit comments

Comments
 (0)