merge branch 'pr-3057'

Fraser Tweedale (1):
  chown cgroup to process uid in container namespace

LGTMs: kolyshkin cyphar
Closes #3057
This commit is contained in:
Aleksa Sarai
2021-12-07 17:06:19 +11:00
4 changed files with 150 additions and 0 deletions
+39
View File
@@ -1,8 +1,10 @@
package systemd
import (
"bufio"
"fmt"
"math"
"os"
"path/filepath"
"strconv"
"strings"
@@ -288,9 +290,46 @@ func (m *unifiedManager) Apply(pid int) error {
if err := fs2.CreateCgroupPath(m.path, m.cgroups); err != nil {
return err
}
if c.OwnerUID != nil {
filesToChown, err := cgroupFilesToChown()
if err != nil {
return err
}
for _, v := range filesToChown {
err := os.Chown(m.path+"/"+v, *c.OwnerUID, -1)
if err != nil {
return err
}
}
}
return nil
}
// The kernel exposes a list of files that should be chowned to the delegate
// uid in /sys/kernel/cgroup/delegate. If the file is not present
// (Linux < 4.15), use the initial values mentioned in cgroups(7).
func cgroupFilesToChown() ([]string, error) {
filesToChown := []string{"."} // the directory itself must be chowned
const cgroupDelegateFile = "/sys/kernel/cgroup/delegate"
f, err := os.Open(cgroupDelegateFile)
if err == nil {
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
filesToChown = append(filesToChown, scanner.Text())
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error reading %s: %w", cgroupDelegateFile, err)
}
} else {
filesToChown = append(filesToChown, "cgroup.procs", "cgroup.subtree_control", "cgroup.threads")
}
return filesToChown, nil
}
func (m *unifiedManager) Destroy() error {
m.mu.Lock()
defer m.mu.Unlock()
+7
View File
@@ -41,6 +41,13 @@ type Cgroup struct {
// Rootless tells if rootless cgroups should be used.
Rootless bool
// The host UID that should own the cgroup, or nil to accept
// the default ownership. This should only be set when the
// cgroupfs is to be mounted read/write.
// Not all cgroup manager implementations support changing
// the ownership.
OwnerUID *int `json:"owner_uid,omitempty"`
}
type Resources struct {
+43
View File
@@ -399,6 +399,49 @@ func CreateLibcontainerConfig(opts *CreateOpts) (*configs.Config, error) {
}
}
}
// Set the host UID that should own the container's cgroup.
// This must be performed after setupUserNamespace, so that
// config.HostRootUID() returns the correct result.
//
// Only set it if the container will have its own cgroup
// namespace and the cgroupfs will be mounted read/write.
//
hasCgroupNS := config.Namespaces.Contains(configs.NEWCGROUP) && config.Namespaces.PathOf(configs.NEWCGROUP) == ""
hasRwCgroupfs := false
if hasCgroupNS {
for _, m := range config.Mounts {
if m.Source == "cgroup" && filepath.Clean(m.Destination) == "/sys/fs/cgroup" && (m.Flags&unix.MS_RDONLY) == 0 {
hasRwCgroupfs = true
break
}
}
}
processUid := 0
if spec.Process != nil {
// Chown the cgroup to the UID running the process,
// which is not necessarily UID 0 in the container
// namespace (e.g., an unprivileged UID in the host
// user namespace).
processUid = int(spec.Process.User.UID)
}
if hasCgroupNS && hasRwCgroupfs {
ownerUid, err := config.HostUID(processUid)
// There are two error cases; we can ignore both.
//
// 1. uidMappings is unset. Either there is no user
// namespace (fine), or it is an error (which is
// checked elsewhere).
//
// 2. The user is unmapped in the user namespace. This is an
// unusual configuration and might be an error. But it too
// will be checked elsewhere, so we can ignore it here.
//
if err == nil {
config.Cgroups.OwnerUID = &ownerUid
}
}
if spec.Process != nil {
config.OomScoreAdj = spec.Process.OOMScoreAdj
config.NoNewPrivileges = spec.Process.NoNewPrivileges
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env bats
load helpers
function teardown() {
teardown_bundle
}
function setup() {
requires root cgroups_v2 systemd
setup_busybox
# chown test temp dir to allow host user to read it
chown 100000 "$ROOT"
# chown rootfs to allow host user to mkdir mount points
chown 100000 "$ROOT"/bundle/rootfs
set_cgroups_path
# configure a user namespace
update_config ' .linux.namespaces += [{"type": "user"}]
| .linux.uidMappings += [{"hostID": 100000, "containerID": 0, "size": 65536}]
| .linux.gidMappings += [{"hostID": 100000, "containerID": 0, "size": 65536}]
'
}
@test "runc exec (cgroup v2, ro cgroupfs, new cgroupns) does not chown cgroup" {
runc run -d --console-socket "$CONSOLE_SOCKET" test_cgroup_chown
[ "$status" -eq 0 ]
runc exec test_cgroup_chown sh -c "stat -c %U /sys/fs/cgroup"
[ "$status" -eq 0 ]
[ "$output" = "nobody" ] # /sys/fs/cgroup owned by unmapped user
}
@test "runc exec (cgroup v2, rw cgroupfs, inh cgroupns) does not chown cgroup" {
set_cgroup_mount_writable
# inherit cgroup namespace (remove cgroup from namespaces list)
update_config '.linux.namespaces |= map(select(.type != "cgroup"))'
runc run -d --console-socket "$CONSOLE_SOCKET" test_cgroup_chown
[ "$status" -eq 0 ]
runc exec test_cgroup_chown sh -c "stat -c %U /sys/fs/cgroup"
[ "$status" -eq 0 ]
[ "$output" = "nobody" ] # /sys/fs/cgroup owned by unmapped user
}
@test "runc exec (cgroup v2, rw cgroupfs, new cgroupns) does chown cgroup" {
set_cgroup_mount_writable
runc run -d --console-socket "$CONSOLE_SOCKET" test_cgroup_chown
[ "$status" -eq 0 ]
runc exec test_cgroup_chown sh -c "stat -c %U /sys/fs/cgroup"
[ "$status" -eq 0 ]
[ "$output" = "root" ] # /sys/fs/cgroup owned by root (of user namespace)
}