From 70d88bc449535c0b87174034ecf013d405cf39fa Mon Sep 17 00:00:00 2001 From: Tycho Andersen Date: Fri, 5 Sep 2025 11:13:09 -0600 Subject: [PATCH] libcontainer/validator: allow setting user.* sysctls inside userns These sysctls are all per-userns (termed `ucounts` in the kernel code) are settable with CAP_SYS_RESOURCE in the user namespace. Signed-off-by: Tycho Andersen --- libcontainer/configs/validate/validator.go | 24 +++++++++++++ .../configs/validate/validator_test.go | 34 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/libcontainer/configs/validate/validator.go b/libcontainer/configs/validate/validator.go index 1fe66e148..605286c18 100644 --- a/libcontainer/configs/validate/validator.go +++ b/libcontainer/configs/validate/validator.go @@ -274,6 +274,30 @@ func sysctl(config *configs.Config) error { return fmt.Errorf("sysctl %q is not allowed as it conflicts with the OCI %q field", s, "hostname") } } + + if strings.HasPrefix(s, "user.") { + + // while it is technically true that a non-userns + // container can write to /proc/sys/user on behalf of + // the init_user_ns, it was not previously supported, + // and doesn't guarantee that someone else spawns a + // different container and writes there, changing the + // values. in particular, setting something like + // max_user_namespaces to non-zero could be a vector to + // use 0-days where the admin had previously disabled + // them. + // + // additionally, this setting affects other host + // processes that are not container related. + // + // so let's refuse this unless we know for sure it + // won't touch anything else. + if !config.Namespaces.Contains(configs.NEWUSER) { + return fmt.Errorf("setting ucounts without a user namespace not allowed: %v", s) + } + continue + } + return fmt.Errorf("sysctl %q is not in a separate kernel namespace", s) } diff --git a/libcontainer/configs/validate/validator_test.go b/libcontainer/configs/validate/validator_test.go index 2dd47a198..160679efe 100644 --- a/libcontainer/configs/validate/validator_test.go +++ b/libcontainer/configs/validate/validator_test.go @@ -1019,6 +1019,40 @@ func TestValidateNetDevices(t *testing.T) { } } +func TestValidateUserSysctlWithUserNamespace(t *testing.T) { + if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) { + t.Skip("Test requires userns.") + } + config := &configs.Config{ + Rootfs: "/var", + Sysctl: map[string]string{"user.max_inotify_watches": "8192"}, + Namespaces: configs.Namespaces( + []configs.Namespace{ + {Type: configs.NEWUSER}, + }, + ), + UIDMappings: []configs.IDMap{{HostID: 0, ContainerID: 123, Size: 100}}, + GIDMappings: []configs.IDMap{{HostID: 0, ContainerID: 123, Size: 100}}, + } + + err := Validate(config) + if err != nil { + t.Errorf("Expected error to not occur with user.* sysctl and NEWUSER namespace: %+v", err) + } +} + +func TestValidateUserSysctlWithoutUserNamespace(t *testing.T) { + config := &configs.Config{ + Rootfs: "/var", + Sysctl: map[string]string{"user.max_inotify_watches": "8192"}, + } + + err := Validate(config) + if err == nil { + t.Error("Expected error to occur with user.* sysctl without NEWUSER namespace but it was nil") + } +} + func TestDevValidName(t *testing.T) { testCases := []struct { name string