Add initial cgroup2 support.

The cgroup v2 fs is initialized early in the boot process, so that every task
in the sandbox resides in at least the root v2 cgroup. A new runsc flag
--mount-cgroup-v2 has been added: this controls whether runsc mounts a
cgroupv2 filesystem at /sys/fs/cgroup automatically (currently it always
mounts a v1 hierarchy at this path). This flag also gates whether userspace
can mount a cgroup2 fs. When this flag is false, which is the default, all
tasks in the sandbox reside forever in the root cgroup.

Like in Linux, the v2 hierarchy is deemed the default owner of a controller,
that is, until the corresponding v1 hierarchy is mounted: then, the v1 tree
steals ownership of that controller and it remains this way till as long as
a v1 mount exists. In other words, hybrid mode is supported, even if such
workloads are rare these days.

The pids controller is the only controller implemented in full: it serves
as validation for the unified hierarchy. The memory controller can be used
to learn the usage stats of a subtree: but like the current v1 implementation,
and unlike Linux, it doesn't achieve this through charging up the hierarchy
during allocation, instead it queries pgalloc for every active controller's
id. And it does no enforcing. The cpu and cpuset controllers are mere stubs,
like in the current v1 implementation.

The following are supported:
* General matters of topology: cgroup.subtree_control, cgroup.controllers,
                               cgroup.procs etc
* CLONE_INTO_CGROUP
* cgroup.kill
* cgroup.events
* A working pids controller and its events files
* A minimal memory controller that just reports mem usage
* Stubbed cpu and cpuset controllers

The following are NOT supported:
* threaded cgroups
* cgroup namespaces
* BPF_PROG_TYPE_CGROUP_DEVICE
* cgroup.freeze

Updates #13007

PiperOrigin-RevId: 943519748
This commit is contained in:
Shailend Chand
2026-07-06 15:17:54 -07:00
committed by gVisor bot
parent 8d5cd43fea
commit dde347d9fc
46 changed files with 5428 additions and 1034 deletions
+1
View File
@@ -27,6 +27,7 @@ import (
const (
ANON_INODE_FS_MAGIC = 0x09041934
CGROUP_SUPER_MAGIC = 0x27e0eb
CGROUP2_SUPER_MAGIC = 0x63677270
DEVPTS_SUPER_MAGIC = 0x00001cd1
EXT_SUPER_MAGIC = 0xef53
FUSE_SUPER_MAGIC = 0x65735546
+8 -2
View File
@@ -7,13 +7,19 @@ package(
go_library(
name = "bitmap",
srcs = ["bitmap.go"],
srcs = [
"bitmap.go",
"format.go",
],
visibility = ["//:sandbox"],
)
go_test(
name = "bitmap_test",
size = "small",
srcs = ["bitmap_test.go"],
srcs = [
"bitmap_test.go",
"format_test.go",
],
library = ":bitmap",
)
@@ -12,22 +12,20 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package cgroupfs
package bitmap
import (
"fmt"
"strconv"
"strings"
"gvisor.dev/gvisor/pkg/bitmap"
)
// formatBitmap produces a string representation of b, which lists the indices
// FormatList produces a string representation of b, which lists the indices
// of set bits in the bitmap. Indices are separated by commas and ranges of
// set bits are abbreviated. Example outputs: "0,2,4", "0,3-7,10", "0-10".
//
// Inverse of parseBitmap.
func formatBitmap(b *bitmap.Bitmap) string {
// Inverse of ParseList.
func FormatList(b *Bitmap) string {
ones := b.ToSlice()
if len(ones) == 0 {
return ""
@@ -104,7 +102,7 @@ func parseToken(token string) (start, end uint32, err error) {
}
}
// parseBitmap parses input as a bitmap. input should be a comma separated list
// ParseList parses input as a bitmap. input should be a comma separated list
// of indices, and ranges of set bits may be abbreviated. Examples: "0,2,4",
// "0,3-7,10", "0-10". Input after the first newline or null byte is discarded.
//
@@ -112,9 +110,9 @@ func parseToken(token string) (start, end uint32, err error) {
// when growing the bitmap during parsing. Ideally sizeHint should be at least
// as large as the bitmap represented by input, but this is not required.
//
// Inverse of formatBitmap.
func parseBitmap(input string, sizeHint uint32) (*bitmap.Bitmap, error) {
b := bitmap.New(sizeHint)
// Inverse of FormatList.
func ParseList(input string, sizeHint uint32) (*Bitmap, error) {
b := New(sizeHint)
if termIdx := strings.IndexAny(input, "\n\000"); termIdx != -1 {
input = input[:termIdx]
@@ -12,14 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package cgroupfs
package bitmap
import (
"fmt"
"slices"
"testing"
"gvisor.dev/gvisor/pkg/bitmap"
)
func TestFormat(t *testing.T) {
@@ -36,15 +34,15 @@ func TestFormat(t *testing.T) {
}
for i, tt := range tests {
t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) {
b := bitmap.New(64)
b := New(64)
for _, v := range tt.input {
b.Add(v)
}
s := formatBitmap(&b)
s := FormatList(&b)
if s != tt.output {
t.Errorf("Expected %q, got %q", tt.output, s)
}
b1, err := parseBitmap(s, 64)
b1, err := ParseList(s, 64)
if err != nil {
t.Fatalf("Failed to parse formatted bitmap: %v", err)
}
@@ -77,7 +75,7 @@ func TestParse(t *testing.T) {
}
for i, tt := range tests {
t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) {
b, err := parseBitmap(tt.input, 64)
b, err := ParseList(tt.input, 64)
if tt.shouldFail {
if err == nil {
t.Fatalf("Expected parsing of %q to fail, but it didn't", tt.input)
+65
View File
@@ -0,0 +1,65 @@
load("//pkg/sync/locking:locking.bzl", "declare_mutex", "declare_rwmutex")
load("//tools:defs.bzl", "go_library")
package(default_applicable_licenses = ["//:license"])
licenses(["notice"])
go_library(
name = "cgroup2fs",
srcs = [
"base_files.go",
"cgroup.go",
"controllers.go",
"cpu.go",
"cpuset.go",
"event.go",
"filesystem.go",
"memory.go",
"pids.go",
"pids_mutex.go",
"save_restore.go",
"tasks_mutex.go",
"tree_mutex.go",
],
visibility = ["//pkg/sentry:internal"],
deps = [
"//pkg/abi/linux",
"//pkg/atomicbitops",
"//pkg/bitmap",
"//pkg/context",
"//pkg/errors/linuxerr",
"//pkg/hostarch",
"//pkg/log",
"//pkg/sentry/fsimpl/kernfs",
"//pkg/sentry/kernel",
"//pkg/sentry/kernel/auth",
"//pkg/sentry/usage",
"//pkg/sentry/vfs",
"//pkg/sync",
"//pkg/sync/locking",
"//pkg/usermem",
"//pkg/waiter",
],
)
declare_rwmutex(
name = "tree_mutex",
out = "tree_mutex.go",
package = "cgroup2fs",
prefix = "tree",
)
declare_rwmutex(
name = "tasks_mutex",
out = "tasks_mutex.go",
package = "cgroup2fs",
prefix = "tasks",
)
declare_mutex(
name = "pids_mutex",
out = "pids_mutex.go",
package = "cgroup2fs",
prefix = "pids",
)
+471
View File
@@ -0,0 +1,471 @@
// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cgroup2fs
import (
"bytes"
"fmt"
"strconv"
"strings"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/errors/linuxerr"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/kernfs"
"gvisor.dev/gvisor/pkg/sentry/kernel"
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
"gvisor.dev/gvisor/pkg/sentry/vfs"
"gvisor.dev/gvisor/pkg/usermem"
)
// +stateify savable
type cgroupInterfaceFile struct {
kernfs.DynamicBytesFile
c *cgroup
}
// Valid implements kernfs.Inode.Valid.
func (f *cgroupInterfaceFile) Valid(ctx context.Context, parent *kernfs.Dentry, name string) bool {
return !f.c.deleted.Load()
}
// SetStat implements kernfs.Inode.SetStat.
func (f *cgroupInterfaceFile) SetStat(ctx context.Context, fs *vfs.Filesystem, creds *auth.Credentials, opts vfs.SetStatOptions) error {
return f.InodeAttrs.SetStat(ctx, fs, creds, opts)
}
// +stateify savable
type cgroupSourceReadOnly struct {
c *cgroup
ctrl controller
src vfs.DynamicBytesSource
}
func (s *cgroupSourceReadOnly) Generate(ctx context.Context, buf *bytes.Buffer) error {
if s.c.deleted.Load() {
return linuxerr.ENODEV
}
if s.ctrl != nil && !s.ctrl.isActive() {
return linuxerr.ENODEV
}
return s.src.Generate(ctx, buf)
}
// +stateify savable
type cgroupSourceWritable struct {
c *cgroup
ctrl controller
src vfs.WritableDynamicBytesSource
}
func (s *cgroupSourceWritable) Generate(ctx context.Context, buf *bytes.Buffer) error {
if s.c.deleted.Load() {
return linuxerr.ENODEV
}
if s.ctrl != nil && !s.ctrl.isActive() {
return linuxerr.ENODEV
}
return s.src.Generate(ctx, buf)
}
func (s *cgroupSourceWritable) Write(ctx context.Context, fd *vfs.FileDescription, src usermem.IOSequence, offset int64) (int64, error) {
if s.c.deleted.Load() {
return 0, linuxerr.ENODEV
}
if s.ctrl != nil && !s.ctrl.isActive() {
return 0, linuxerr.ENODEV
}
return s.src.Write(ctx, fd, src, offset)
}
func (fs *filesystem) newInode(ctx context.Context, uid auth.KUID, gid auth.KGID, c *cgroup, def interfaceFile) kernfs.Inode {
if def.isEvent {
eventFile := fs.newEventFile(ctx, uid, gid, c, def.source, def.ctrl)
if def.onEventCreated != nil {
def.onEventCreated(eventFile)
}
return eventFile
}
f := &cgroupInterfaceFile{c: c}
var src vfs.DynamicBytesSource
if ws, ok := def.source.(vfs.WritableDynamicBytesSource); ok {
src = &cgroupSourceWritable{c: c, ctrl: def.ctrl, src: ws}
} else {
src = &cgroupSourceReadOnly{c: c, ctrl: def.ctrl, src: def.source}
}
f.InitWithIDs(ctx, uid, gid, linux.UNNAMED_MAJOR, fs.devMinor, fs.NextIno(), src, def.perm)
return f
}
func (fs *filesystem) rootInodes(ctx context.Context, uid auth.KUID, gid auth.KGID, c *cgroup) map[string]kernfs.Inode {
contents := make(map[string]kernfs.Inode)
contents["cgroup.procs"] = fs.newInode(ctx, uid, gid, c, interfaceFile{name: "cgroup.procs", source: &cgroupProcs{c: c}, perm: 0644})
contents["cgroup.controllers"] = fs.newInode(ctx, uid, gid, c, interfaceFile{name: "cgroup.controllers", source: &cgroupControllers{c: c}, perm: 0444})
contents["cgroup.subtree_control"] = fs.newInode(ctx, uid, gid, c, interfaceFile{name: "cgroup.subtree_control", source: &cgroupSubtreeControl{c: c}, perm: 0644})
contents["cgroup.max.descendants"] = fs.newInode(ctx, uid, gid, c, interfaceFile{name: "cgroup.max.descendants", source: &cgroupMaxDescendants{c: c}, perm: 0644})
contents["cgroup.max.depth"] = fs.newInode(ctx, uid, gid, c, interfaceFile{name: "cgroup.max.depth", source: &cgroupMaxDepth{c: c}, perm: 0644})
contents["cgroup.stat"] = fs.newInode(ctx, uid, gid, c, interfaceFile{name: "cgroup.stat", source: &cgroupStat{c: c}, perm: 0444})
return contents
}
func (fs *filesystem) cgroupInodes(ctx context.Context, uid auth.KUID, gid auth.KGID, c *cgroup) map[string]kernfs.Inode {
contents := make(map[string]kernfs.Inode)
contents["cgroup.procs"] = fs.newInode(ctx, uid, gid, c, interfaceFile{name: "cgroup.procs", source: &cgroupProcs{c: c}, perm: 0644})
contents["cgroup.controllers"] = fs.newInode(ctx, uid, gid, c, interfaceFile{name: "cgroup.controllers", source: &cgroupControllers{c: c}, perm: 0444})
contents["cgroup.subtree_control"] = fs.newInode(ctx, uid, gid, c, interfaceFile{name: "cgroup.subtree_control", source: &cgroupSubtreeControl{c: c}, perm: 0644})
contents["cgroup.max.descendants"] = fs.newInode(ctx, uid, gid, c, interfaceFile{name: "cgroup.max.descendants", source: &cgroupMaxDescendants{c: c}, perm: 0644})
contents["cgroup.max.depth"] = fs.newInode(ctx, uid, gid, c, interfaceFile{name: "cgroup.max.depth", source: &cgroupMaxDepth{c: c}, perm: 0644})
contents["cgroup.stat"] = fs.newInode(ctx, uid, gid, c, interfaceFile{name: "cgroup.stat", source: &cgroupStat{c: c}, perm: 0444})
contents["cgroup.type"] = fs.newInode(ctx, uid, gid, c, interfaceFile{name: "cgroup.type", source: &cgroupType{c: c}, perm: 0444})
contents["cgroup.kill"] = fs.newInode(ctx, uid, gid, c, interfaceFile{name: "cgroup.kill", source: &cgroupKill{c: c}, perm: 0200})
contents["cgroup.events"] = fs.newInode(ctx, uid, gid, c, interfaceFile{
name: "cgroup.events",
source: &cgroupEvents{c: c},
perm: 0444,
isEvent: true,
onEventCreated: func(inode *eventFile) {
c.eventsFile = inode
},
})
return contents
}
// cgroupProcs implements vfs.DynamicBytesSource for "cgroup.procs".
// +stateify savable
type cgroupProcs struct {
c *cgroup
}
// Generate implements vfs.DynamicBytesSource.Generate.
func (cf *cgroupProcs) Generate(ctx context.Context, buf *bytes.Buffer) error {
if cf.c.deleted.Load() {
return linuxerr.ENODEV
}
t := kernel.TaskFromContext(ctx)
if t == nil {
return nil
}
pids := cf.c.getPIDs(t)
for _, pid := range pids {
fmt.Fprintf(buf, "%d\n", pid)
}
return nil
}
// Write implements vfs.WritableDynamicBytesSource.Write.
func (cf *cgroupProcs) Write(ctx context.Context, fd *vfs.FileDescription, src usermem.IOSequence, offset int64) (int64, error) {
if cf.c.deleted.Load() {
return 0, linuxerr.ENODEV
}
data := make([]byte, src.NumBytes())
if _, err := src.CopyIn(ctx, data); err != nil {
return 0, err
}
str := strings.TrimSpace(string(data))
pid, err := strconv.ParseInt(str, 10, 64)
if err != nil || pid < 0 {
return 0, linuxerr.EINVAL
}
if err := cf.c.attachProcess(ctx, fd.Credentials(), pid); err != nil {
return 0, err
}
return src.NumBytes(), nil
}
// cgroupControllers implements vfs.DynamicBytesSource for "cgroup.controllers".
// +stateify savable
type cgroupControllers struct {
c *cgroup
}
// Generate implements vfs.DynamicBytesSource.Generate.
func (cf *cgroupControllers) Generate(ctx context.Context, buf *bytes.Buffer) error {
if cf.c.deleted.Load() {
return linuxerr.ENODEV
}
var available []string
cf.c.fs.treeMu.RLock()
defer cf.c.fs.treeMu.RUnlock()
for ctl := firstController; ctl < numControllers; ctl++ {
if cf.c.isControllerAvailableLocked(ctl) {
available = append(available, ctrlTypeStr[ctl])
}
}
if len(available) > 0 {
buf.WriteString(strings.Join(available, " ") + "\n")
}
return nil
}
// Write implements vfs.WritableDynamicBytesSource.Write.
func (cf *cgroupControllers) Write(ctx context.Context, fd *vfs.FileDescription, src usermem.IOSequence, offset int64) (int64, error) {
return 0, linuxerr.EINVAL
}
// cgroupSubtreeControl implements vfs.DynamicBytesSource for "cgroup.subtree_control".
// It is used to enable or disable specific controllers for the cgroup's children.
// +stateify savable
type cgroupSubtreeControl struct {
c *cgroup
}
// Generate implements vfs.DynamicBytesSource.Generate.
func (cf *cgroupSubtreeControl) Generate(ctx context.Context, buf *bytes.Buffer) error {
var enabled []string
cf.c.fs.treeMu.RLock()
defer cf.c.fs.treeMu.RUnlock()
for ctl := firstController; ctl < numControllers; ctl++ {
if cf.c.subtreeCtrls[ctl] {
enabled = append(enabled, ctrlTypeStr[ctl])
}
}
if len(enabled) > 0 {
buf.WriteString(strings.Join(enabled, " ") + "\n")
}
return nil
}
// Write implements vfs.WritableDynamicBytesSource.Write.
func (cf *cgroupSubtreeControl) Write(ctx context.Context, fd *vfs.FileDescription, src usermem.IOSequence, offset int64) (int64, error) {
if cf.c.deleted.Load() {
return 0, linuxerr.ENODEV
}
data := make([]byte, src.NumBytes())
if _, err := src.CopyIn(ctx, data); err != nil {
return 0, err
}
str := strings.TrimSpace(string(data))
if str == "" {
return src.NumBytes(), nil
}
var enable []kernel.Cgroup2Ctrl
var disable []kernel.Cgroup2Ctrl
for _, ctrl := range strings.Split(str, " ") {
if ctrl == "" {
continue
}
if len(ctrl) < 2 {
return 0, linuxerr.EINVAL
}
op := ctrl[0]
name := ctrl[1:]
if op != '+' && op != '-' {
return 0, linuxerr.EINVAL
}
cType, ok := ctrlNames[name]
if !ok {
return 0, linuxerr.EINVAL
}
if op == '+' {
enable = append(enable, cType)
} else {
disable = append(disable, cType)
}
}
if err := cf.c.setSubtreeControl(ctx, enable, disable); err != nil {
return 0, err
}
return src.NumBytes(), nil
}
// cgroupStat implements vfs.DynamicBytesSource for "cgroup.stat".
// +stateify savable
type cgroupStat struct {
c *cgroup
}
func (cf *cgroupStat) Generate(ctx context.Context, buf *bytes.Buffer) error {
if cf.c.deleted.Load() {
return linuxerr.ENODEV
}
cf.c.fs.treeMu.RLock()
defer cf.c.fs.treeMu.RUnlock()
cf.c.fs.tasksMu.RLock()
defer cf.c.fs.tasksMu.RUnlock()
descendants := cf.c.nrDescendants.Load()
fmt.Fprintf(buf, "nr_descendants %d\nnr_dying_descendants 0\n", descendants)
return nil
}
// cgroupType implements vfs.DynamicBytesSource for "cgroup.type".
// It identifies the cgroup namespace type.
// +stateify savable
type cgroupType struct {
c *cgroup
}
// Generate implements vfs.DynamicBytesSource.Generate.
func (cf *cgroupType) Generate(ctx context.Context, buf *bytes.Buffer) error {
if cf.c.deleted.Load() {
return linuxerr.ENODEV
}
buf.WriteString("domain\n")
return nil
}
// +stateify savable
type cgroupMaxDescendants struct{ c *cgroup }
// Generate implements vfs.DynamicBytesSource.Generate.
func (cf *cgroupMaxDescendants) Generate(ctx context.Context, buf *bytes.Buffer) error {
if cf.c.deleted.Load() {
return linuxerr.ENODEV
}
val := cf.c.maxDescendants.Load()
if val == limitMax {
buf.WriteString("max\n")
} else {
fmt.Fprintf(buf, "%d\n", val)
}
return nil
}
// Write implements vfs.WritableDynamicBytesSource.Write.
func (cf *cgroupMaxDescendants) Write(ctx context.Context, fd *vfs.FileDescription, src usermem.IOSequence, offset int64) (int64, error) {
if cf.c.deleted.Load() {
return 0, linuxerr.ENODEV
}
data := make([]byte, src.NumBytes())
if _, err := src.CopyIn(ctx, data); err != nil {
return 0, err
}
str := strings.TrimSpace(string(data))
var val int64
if str == "max" {
val = limitMax
} else {
var err error
val, err = strconv.ParseInt(str, 10, 64)
if err != nil || val < 0 {
return 0, linuxerr.EINVAL
}
}
cf.c.fs.treeMu.Lock()
defer cf.c.fs.treeMu.Unlock()
cf.c.maxDescendants.Store(val)
return src.NumBytes(), nil
}
// cgroupMaxDepth implements vfs.DynamicBytesSource for "cgroup.max.depth".
// +stateify savable
type cgroupMaxDepth struct{ c *cgroup }
// Generate implements vfs.DynamicBytesSource.Generate.
func (cf *cgroupMaxDepth) Generate(ctx context.Context, buf *bytes.Buffer) error {
if cf.c.deleted.Load() {
return linuxerr.ENODEV
}
val := cf.c.maxDepth.Load()
if val == limitMax {
buf.WriteString("max\n")
} else {
fmt.Fprintf(buf, "%d\n", val)
}
return nil
}
// Write implements vfs.WritableDynamicBytesSource.Write.
func (cf *cgroupMaxDepth) Write(ctx context.Context, fd *vfs.FileDescription, src usermem.IOSequence, offset int64) (int64, error) {
if cf.c.deleted.Load() {
return 0, linuxerr.ENODEV
}
data := make([]byte, src.NumBytes())
if _, err := src.CopyIn(ctx, data); err != nil {
return 0, err
}
str := strings.TrimSpace(string(data))
var val int64
if str == "max" {
val = limitMax
} else {
var err error
val, err = strconv.ParseInt(str, 10, 64)
if err != nil || val < 0 {
return 0, linuxerr.EINVAL
}
}
cf.c.fs.treeMu.Lock()
defer cf.c.fs.treeMu.Unlock()
cf.c.maxDepth.Store(val)
return src.NumBytes(), nil
}
// cgroupKill implements vfs.DynamicBytesSource for "cgroup.kill".
// +stateify savable
type cgroupKill struct{ c *cgroup }
// Generate implements vfs.DynamicBytesSource.Generate.
func (cf *cgroupKill) Generate(ctx context.Context, buf *bytes.Buffer) error {
if cf.c.deleted.Load() {
return linuxerr.ENODEV
}
return nil
}
// Write implements vfs.WritableDynamicBytesSource.Write.
func (cf *cgroupKill) Write(ctx context.Context, fd *vfs.FileDescription, src usermem.IOSequence, offset int64) (int64, error) {
if cf.c.deleted.Load() {
return 0, linuxerr.ENODEV
}
data := make([]byte, src.NumBytes())
if _, err := src.CopyIn(ctx, data); err != nil {
return 0, err
}
str := strings.TrimSpace(string(data))
val, err := strconv.Atoi(str)
if err != nil {
return 0, linuxerr.EINVAL
}
if val != 1 {
return 0, linuxerr.ERANGE
}
if err := cf.c.kill(); err != nil {
return 0, err
}
return src.NumBytes(), nil
}
// cgroupEvents implements vfs.DynamicBytesSource for "cgroup.events".
// +stateify savable
type cgroupEvents struct {
c *cgroup
}
// Generate implements vfs.DynamicBytesSource.Generate.
func (cf *cgroupEvents) Generate(ctx context.Context, buf *bytes.Buffer) error {
if cf.c.deleted.Load() {
return linuxerr.ENODEV
}
populated := 0
if cf.c.populated() {
populated = 1
}
fmt.Fprintf(buf, "populated %d\nfrozen 0\n", populated)
return nil
}
+963
View File
@@ -0,0 +1,963 @@
// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cgroup2fs
// Lock order:
//
// kernfs.filesystem.mu
// cgroup2fs.filesystem.treeMu
// kernfs.OrderedChildren.mu
// kernel.TaskSet.mu
// cgroup2fs.filesystem.tasksMu
// kernel.SignalHandlers.mu
// kernel.Task.cgroup2Mu
//
// The treeMu is an analogue to the kernel's cgroup_mutex, whereas
// tasksMu is an analogue to the kernel's css_set_lock. The former
// governs matters of topology: the basic structure of the tree and
// the controllers enabled in each node. The latter governs membership:
// the tasks associated with each cgroup.
import (
"sort"
"strings"
"sync/atomic"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/atomicbitops"
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/errors/linuxerr"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/kernfs"
"gvisor.dev/gvisor/pkg/sentry/kernel"
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
"gvisor.dev/gvisor/pkg/sentry/vfs"
)
// limitMax is the value for max.descendants and max.depth that indicates no limit.
const limitMax = -1
// cgroup is a cgroup2 directory inode.
//
// +stateify savable
type cgroup struct {
kernfs.InodeAttrs
kernfs.InodeDirectoryNoNewChildren // But we override NewDir() and RmDir().
kernfs.InodeNoopRefCount
kernfs.InodeNotAnonymous
kernfs.InodeNotSymlink
kernfs.InodeWatches
kernfs.OrderedChildren
kernfs.InodeFSOwned
implStatFS
locks vfs.FileLocks
// Back pointer to the filesystem. Immutable.
fs *filesystem
// parent is the parent cgroup. Immutable.
parent *cgroup // nil for the root cgroup.
// Used to publish to cgroup.events. Immutable.
eventsFile *eventFile
// path relative to the root cgroup. Immutable.
path string
// Makes searching for common ancestors faster. Immutable.
level int
// Used to make operations on FDs pointing to deleted cgroups fail.
// +checklocks:fs.treeMu
// +checkatomic
deleted atomicbitops.Bool
// children contains all sub-cgroups in this directory.
// +checklocks:fs.treeMu
children map[*cgroup]struct{}
// subtreeCtrls tracks the controller types that this cgroup has enabled for its children.
// +checklocks:fs.treeMu
subtreeCtrls [kernel.Cgroup2NumControllers]bool
// Task membership.
// +checklocks:fs.tasksMu
tasks map[*kernel.Task]struct{}
// +checkatomic
tasksCount atomicbitops.Int64
// ctrls stores pointers to active controllers attached to this cgroup.
// +checklocks:fs.treeMu
ctrls ctrlSet
// closestCtrls stores an array of deepest ancestor controllers that are enabled.
// It is an atomic pointer so that when we do implement hotpath charging, it can be
// achieved locklessly.
// If ctrls[i] is non-nil, that is, there is a controller
// attached to this cgroup, then closestCtrls[i] == ctrls[i].
closestCtrls atomic.Pointer[ctrlSet] `state:".(*ctrlSet)"`
// The field below counts the number of immediate children considered populated.
// It helps answer quickly if the subtree beneath a cgroup is considered populated.
// +checklocks:fs.tasksMu
// +checkatomic
nrPopulatedChildren atomicbitops.Int64
// Backing field for cgroup.max.descendants.
// +checklocks:fs.treeMu
// +checkatomic
maxDescendants atomicbitops.Int64
// Backing field for cgroup.max.depth.
// +checklocks:fs.treeMu
// +checkatomic
maxDepth atomicbitops.Int64
// The total number of cgroups nested inside this cgroup at all depths.
// Backs the nr_descendants field in cgroup.stat.
// +checklocks:fs.treeMu
// +checkatomic
nrDescendants atomicbitops.Int64
// killSeq tracks cgroup.kill invocations.
// +checklocks:fs.tasksMu
killSeq uint64
}
// +checklocks:c.fs.treeMu
func (c *cgroup) initRoot(ctx context.Context) {
arr := new(ctrlSet)
for i := firstController; i < numControllers; i++ {
// Root cgroup must have all controllers active.
ctrl := c.newController(i)
c.ctrls[i] = ctrl
c.populateInterfaceFiles(ctx, ctrl)
arr[i] = c.ctrls[i]
}
c.closestCtrls.Store(arr)
}
// +checklocks:c.fs.treeMu
func (c *cgroup) init(ctx context.Context) {
var parentSet *ctrlSet
if c.parent != nil {
parentSet = c.parent.closestCtrls.Load()
if parentSet == nil {
panic("cgroup tree inconsistency: parent closestCtrls is nil")
}
} else {
panic("cgroup tree inconsistency: init called on root cgroup")
}
arr := new(ctrlSet)
for i := firstController; i < numControllers; i++ {
if c.parent.subtreeCtrls[i] { // +checklocksforce: c.fs.treeMu is locked
ctrl := c.newController(i)
c.ctrls[i] = ctrl
c.populateInterfaceFiles(ctx, ctrl)
}
if c.ctrls[i] != nil {
arr[i] = c.ctrls[i]
} else {
arr[i] = parentSet[i]
}
}
c.closestCtrls.Store(arr)
}
// Keep implements kernfs.Inode.Keep.
func (*cgroup) Keep() bool {
return true
}
// SetStat implements kernfs.Inode.SetStat.
func (c *cgroup) SetStat(ctx context.Context, fs *vfs.Filesystem, creds *auth.Credentials, opts vfs.SetStatOptions) error {
return c.InodeAttrs.SetStat(ctx, fs, creds, opts)
}
// Open implements kernfs.Inode.Open.
func (c *cgroup) Open(ctx context.Context, rp *vfs.ResolvingPath, kd *kernfs.Dentry, opts vfs.OpenOptions) (*vfs.FileDescription, error) {
opts.Flags &= linux.O_ACCMODE | linux.O_CREAT | linux.O_EXCL | linux.O_TRUNC |
linux.O_DIRECTORY | linux.O_NOFOLLOW | linux.O_NONBLOCK | linux.O_NOCTTY
fd, err := kernfs.NewGenericDirectoryFD(rp.Mount(), kd, rp.Credentials(), &c.OrderedChildren, &c.locks, &opts, kernfs.GenericDirectoryFDOptions{
SeekEnd: kernfs.SeekEndStaticEntries,
})
if err != nil {
return nil, err
}
return fd.VFSFileDescription(), nil
}
// Valid implements kernfs.Inode.Valid.
func (c *cgroup) Valid(ctx context.Context, parent *kernfs.Dentry, name string) bool {
return !c.deleted.Load()
}
// Lookup implements kernfs.Inode.Lookup.
func (c *cgroup) Lookup(ctx context.Context, name string) (kernfs.Inode, error) {
if c.deleted.Load() {
return nil, linuxerr.ENOENT
}
return c.OrderedChildren.Lookup(ctx, name)
}
// IterDirents implements kernfs.Inode.IterDirents.
func (c *cgroup) IterDirents(ctx context.Context, mnt *vfs.Mount, cb vfs.IterDirentsCallback, offset, relOffset int64) (int64, error) {
return c.OrderedChildren.IterDirents(ctx, mnt, cb, offset, relOffset)
}
// NewDir implements kernfs.Inode.NewDir.
func (c *cgroup) NewDir(ctx context.Context, name string, opts vfs.MkdirOptions) (kernfs.Inode, error) {
if strings.Contains(name, "\n") {
return nil, linuxerr.EINVAL
}
c.fs.treeMu.Lock()
defer c.fs.treeMu.Unlock()
if c.deleted.Load() {
return nil, linuxerr.ENOENT
}
if err := c.checkLimitsLocked(); err != nil {
return nil, err
}
mode := opts.Mode.Permissions() | linux.ModeDirectory
return c.OrderedChildren.Inserter(name, func() kernfs.Inode {
c.IncLinks(1)
childPath := c.path
if childPath != "/" {
childPath += "/"
}
childPath += name
return c.fs.newCgroupLocked(ctx, auth.CredentialsFromContext(ctx), mode, childPath, c) // +checklocksforce: c.fs.treeMu is locked
})
}
// Unlink implements kernfs.Inode.Unlink.
func (c *cgroup) Unlink(ctx context.Context, name string, child kernfs.Inode) error {
return linuxerr.EPERM
}
// HasChildren implements kernfs.Inode.HasChildren.
// We return false to bypass kernfs yielding ENOTEMPTY instead doing it in RmDir manually to yield EBUSY.
func (c *cgroup) HasChildren() bool {
return false
}
// RmDir implements kernfs.Inode.RmDir.
func (c *cgroup) RmDir(ctx context.Context, name string, child kernfs.Inode) error {
childCgroup, ok := child.(*cgroup)
if !ok {
return linuxerr.ENOTDIR
}
c.fs.treeMu.Lock()
defer c.fs.treeMu.Unlock()
if childCgroup.InodeAttrs.Links()-2 > 0 || childCgroup.populated() {
return linuxerr.EBUSY
}
err := c.OrderedChildren.RmDir(ctx, name, child)
if err != nil {
return err
}
c.InodeAttrs.DecLinks()
delete(c.children, childCgroup)
childCgroup.deleted.Store(true) // +checklocksforce: c.fs.treeMu is locked
// Walk up the ancestry to keep nrDescendants up to date.
for curr := c; curr != nil; curr = curr.parent {
curr.nrDescendants.Add(-1) // +checklocksforce: c.fs.treeMu is locked
}
return nil
}
// Rename implements kernfs.Inode.Rename.
func (c *cgroup) Rename(ctx context.Context, oldname, newname string, child, dstDir kernfs.Inode) error {
return linuxerr.EPERM
}
// populated returns true if this cgroup, or any of its descendants contain tasks.
// nrPopulatedChildren keeps this O(1).
func (c *cgroup) populated() bool {
if c.tasksCount.Load() > 0 {
return true
}
if c.nrPopulatedChildren.Load() > 0 {
return true
}
return false
}
// updatePopulated propagates task population changes up the ancestry.
// When an ancestor transitions between empty and populated, it updates
// its parent's populated child counters and triggers cgroup.events notifications.
// +checklocks:c.fs.tasksMu
func (c *cgroup) updatePopulated(ctx context.Context, populated bool) {
diff := int64(-1)
if populated {
diff = 1
}
child := (*cgroup)(nil)
curr := c
for curr != nil {
wasPopulated := curr.populated()
if child != nil {
curr.nrPopulatedChildren.Add(diff) // +checklocksforce: c.fs.tasksMu is locked
}
if child != nil && wasPopulated == curr.populated() {
break
}
if curr.eventsFile != nil {
curr.eventsFile.Notify(ctx)
}
child = curr
curr = curr.parent
}
}
// setControllersLocked modifies the set of enabled controllers for the children of the
// given cgroup and updates impacted descendants.
// +checklocks:c.fs.treeMu
func (c *cgroup) setControllersLocked(ctx context.Context, enable []kernel.Cgroup2Ctrl, disable []kernel.Cgroup2Ctrl) {
var cTypes []kernel.Cgroup2Ctrl
for _, cType := range enable {
if !c.subtreeCtrls[cType] {
c.subtreeCtrls[cType] = true
cTypes = append(cTypes, cType)
}
}
for _, cType := range disable {
if c.subtreeCtrls[cType] {
c.subtreeCtrls[cType] = false
cTypes = append(cTypes, cType)
}
}
if len(cTypes) > 0 {
for child := range c.children {
child.rebuildCtrlsLocked(ctx, cTypes) // +checklocksforce: c.fs.treeMu is locked
}
}
}
// rebuildCtrls reconciles the child's active controllers with the parent's subtree_control.
// +checklocks:c.fs.treeMu
func (c *cgroup) rebuildCtrlsLocked(ctx context.Context, cTypes []kernel.Cgroup2Ctrl) {
if c.parent == nil {
panic("cgroup tree inconsistency: rebuildCtrlsLocked called on root")
}
for _, cType := range cTypes {
if c.parent.subtreeCtrls[cType] && c.ctrls[cType] == nil { // +checklocksforce: c.fs.treeMu is locked
ctrl := c.newController(cType)
c.ctrls[cType] = ctrl
c.populateInterfaceFiles(ctx, ctrl)
} else if !c.parent.subtreeCtrls[cType] && c.ctrls[cType] != nil { // +checklocksforce: c.fs.treeMu is locked
ctrl := c.ctrls[cType]
c.ctrls[cType] = nil
c.removeInterfaceFiles(ctx, ctrl)
ctrl.detach()
}
}
if !c.updateClosestCtrlsLocked(cTypes) {
return
}
c.walkSubtreeLocked(func(child *cgroup) bool {
return child.updateClosestCtrlsLocked(cTypes) // +checklocksforce: c.fs.treeMu is locked
})
for _, cType := range cTypes {
if cType == kernel.Cgroup2Memory {
// Update the memory cgroup IDs of existing tasks in this subtree
// because the memory controller has been enabled/disabled.
c.fs.tasksMu.Lock()
c.updateTaskMemoryCgIDsLocked()
c.walkSubtreeLocked(func(child *cgroup) bool {
child.updateTaskMemoryCgIDsLocked() // +checklocksforce: child.fs.treeMu and child.fs.tasksMu are locked.
return true
})
c.fs.tasksMu.Unlock()
}
}
}
// updateClosestCtrls updates the cached nearest active controller for the given types.
// Returns true if the cache was actually modified.
// +checklocks:c.fs.treeMu
func (c *cgroup) updateClosestCtrlsLocked(cTypes []kernel.Cgroup2Ctrl) bool {
curSet := c.closestCtrls.Load()
if curSet == nil {
panic("cgroup tree inconsistency: closestCtrls is nil")
}
curSet = curSet.fork()
var parentSet *ctrlSet
if c.parent != nil {
parentSet = c.parent.closestCtrls.Load()
if parentSet == nil {
panic("cgroup tree inconsistency: parent closestCtrls is nil")
}
}
changed := false
for _, cType := range cTypes {
var ctrl controller
if c.ctrls[cType] != nil {
ctrl = c.ctrls[cType]
} else if c.parent != nil {
ctrl = parentSet[cType]
}
if curSet[cType] != ctrl {
curSet[cType] = ctrl
changed = true
}
}
if changed {
c.closestCtrls.Store(curSet)
}
return changed
}
// CanEnter checks if a task can enter the cgroup.
func (c *cgroup) CanEnter(ctx context.Context, t *kernel.Task) (func(), func(), error) {
curSet := c.closestCtrls.Load()
if curSet == nil {
return func() {}, func() {}, nil
}
for i, ctrl := range curSet {
if ctrl == nil {
continue
}
if !ctrl.canEnter(ctx, t) {
for _, prev := range curSet[:i] {
if prev != nil {
prev.cancelEnter(ctx, t)
}
}
return nil, nil, linuxerr.EAGAIN
}
}
// rollback and commit should be bound to the cached curSet, so that a concurrent
// update to c.closestCtrls is safe.
rollback := func() {
for _, ctrl := range curSet {
if ctrl != nil {
ctrl.cancelEnter(ctx, t)
}
}
}
commit := func() {
c.fs.tasksMu.Lock()
c.tasks[t] = struct{}{}
if c.tasksCount.Add(1) == 1 {
c.updatePopulated(ctx, true)
}
c.fs.tasksMu.Unlock()
for _, ctrl := range curSet {
if ctrl != nil {
ctrl.enter(ctx, t)
}
}
}
return rollback, commit, nil
}
// Exit is called when a task dies.
func (c *cgroup) Exit(ctx context.Context, t *kernel.Task) {
c.fs.tasksMu.Lock()
delete(c.tasks, t)
if c.tasksCount.Add(-1) == 0 {
c.updatePopulated(ctx, false)
}
c.fs.tasksMu.Unlock()
curSet := c.closestCtrls.Load()
if curSet == nil {
return
}
for _, ctrl := range curSet {
if ctrl == nil {
continue
}
ctrl.exit(ctx, t)
}
}
// CanCloneInto implements kernel.Cgroup2.CanCloneInto.
// It is used to check permissions for CLONE_CGROUP_INTO.
// +checklocksread:c.fs.treeMu
func (c *cgroup) CanCloneInto(ctx context.Context, creds *auth.Credentials) error {
if c.deleted.Load() {
return linuxerr.ENOENT
}
if c.parent != nil && c.hasControllersEnabledLocked() {
return linuxerr.EBUSY
}
inode, err := c.OrderedChildren.Lookup(ctx, "cgroup.procs")
if err != nil {
return err
}
if err := inode.CheckPermissions(ctx, creds, vfs.MayWrite); err != nil {
return err
}
t := kernel.TaskFromContext(ctx)
if t != nil {
if parentCg, ok := t.Cgroup2().(*cgroup); ok {
return c.checkMigrationPermsLocked(ctx, creds, parentCg)
}
}
return nil
}
func (c *cgroup) canAttach(ctx context.Context, actx *attachCtx) bool {
curSet := c.closestCtrls.Load()
if curSet == nil {
return true
}
for i, ctrl := range curSet {
if ctrl == nil {
continue
}
if !ctrl.canAttach(ctx, actx) {
for _, prev := range curSet[:i] {
if prev != nil {
prev.cancelAttach(ctx, actx)
}
}
return false
}
}
return true
}
// cancelAttach cancels an attach().
func (c *cgroup) cancelAttach(ctx context.Context, actx *attachCtx) {
curSet := c.closestCtrls.Load()
if curSet == nil {
return
}
for _, ctrl := range curSet {
if ctrl == nil {
continue
}
ctrl.cancelAttach(ctx, actx)
}
}
// attach() helps achieve administrative migration (cgroup.procs or cgroup.threads)
func (c *cgroup) attach(ctx context.Context, actx *attachCtx) {
c.fs.tasksMu.Lock()
defer c.fs.tasksMu.Unlock()
for t := range actx.tasks {
t.SetCgroup2(c)
oldNode := actx.oldNodes[t]
if oldNode == nil {
panic("cgroup tree inconsistency: task has no cgroup2 node")
}
delete(oldNode.tasks, t) // +checklocksforce: c.fs.tasksMu is locked
if oldNode.tasksCount.Add(-1) == 0 { // +checklocksforce: c.fs.tasksMu is locked
oldNode.updatePopulated(ctx, false) // +checklocksforce: c.fs.tasksMu is locked
}
c.tasks[t] = struct{}{}
if c.tasksCount.Add(1) == 1 {
c.updatePopulated(ctx, true)
}
}
curSet := c.closestCtrls.Load()
if curSet == nil {
return
}
for _, ctrl := range curSet {
if ctrl == nil {
continue
}
ctrl.attach(ctx, actx)
}
}
func (c *cgroup) populateInterfaceFiles(ctx context.Context, ctrl controller) {
if ctrl == nil {
return
}
uid := c.UID()
gid := c.GID()
for _, def := range ctrl.interfaceFiles() {
def.ctrl = ctrl
if c.parent == nil && !def.showAtRoot {
continue
}
inode := c.fs.newInode(ctx, uid, gid, c, def)
// Insert() can only return EEXIST. Short of cgroup2fs inconsistency, this should never happen.
if err := c.OrderedChildren.Insert(def.name, inode); err != nil {
ctx.Warningf("cgroup2fs: failed to insert interface file %q: %v", def.name, err)
}
}
}
func (c *cgroup) removeInterfaceFiles(ctx context.Context, ctrl controller) {
if ctrl == nil {
return
}
for _, name := range ctrl.interfaceFileNames() {
if inode, err := c.OrderedChildren.Lookup(ctx, name); err == nil {
c.OrderedChildren.Unlink(ctx, name, inode)
}
}
}
// Path returns the path of the cgroup.
func (c *cgroup) Path() string {
if c.deleted.Load() {
return c.path + " (deleted)"
}
return c.path
}
// KillSeq implements kernel.Cgroup2.KillSeq.
func (c *cgroup) KillSeq() uint64 {
c.fs.tasksMu.RLock()
defer c.fs.tasksMu.RUnlock()
return c.killSeq
}
// +checklocksread:c.fs.treeMu
func (c *cgroup) isControllerAvailableLocked(i kernel.Cgroup2Ctrl) bool {
if c.parent != nil {
return c.parent.subtreeCtrls[i] // +checklocksforce: c.fs.treeMu is locked
}
return c.ctrls[i] != nil
}
// walkSubtree walks the subtree rooted at c, calling f on each descendant.
// If f returns false, the subtree walk stops descending down that branch.
// +checklocksread:c.fs.treeMu
func (c *cgroup) walkSubtreeLocked(f func(n *cgroup) bool) {
for child := range c.children {
if f(child) {
child.walkSubtreeLocked(f) // +checklocksforce: c.fs.treeMu is locked
}
}
}
// +checklocksread:c.fs.treeMu
func (c *cgroup) checkMigrationPermsLocked(ctx context.Context, creds *auth.Credentials, oldNode *cgroup) error {
lca := lowestCommonAncestor(oldNode, c)
if lca == nil {
return nil
}
lcaProcs, err := lca.OrderedChildren.Lookup(ctx, "cgroup.procs")
if err != nil {
return err
}
return lcaProcs.CheckPermissions(ctx, creds, vfs.MayWrite)
}
func lowestCommonAncestor(a, b *cgroup) *cgroup {
da := a.level
db := b.level
for da > db {
a = a.parent
da--
}
for db > da {
b = b.parent
db--
}
for a != b && a != nil && b != nil {
a = a.parent
b = b.parent
}
return a
}
// checkLimitsLocked checks if we are hitting cgroup dir limits.
// +checklocksread:c.fs.treeMu
func (c *cgroup) checkLimitsLocked() error {
for curr := c; curr != nil; curr = curr.parent {
maxDepth := curr.maxDepth.Load()
maxDesc := curr.maxDescendants.Load()
if maxDepth != limitMax {
diff := int64(c.level - curr.level)
if diff >= maxDepth {
return linuxerr.EAGAIN
}
}
if maxDesc != limitMax {
if curr.nrDescendants.Load() >= maxDesc {
return linuxerr.EAGAIN
}
}
}
return nil
}
// +checklocksread:c.fs.treeMu
func (c *cgroup) hasControllersEnabledLocked() bool {
for _, enabled := range c.subtreeCtrls {
if enabled {
return true
}
}
return false
}
// attachProcess handles writes to cgroup.procs.
func (c *cgroup) attachProcess(ctx context.Context, creds *auth.Credentials, pid int64) error {
c.fs.treeMu.Lock()
defer c.fs.treeMu.Unlock()
if c.deleted.Load() {
return linuxerr.ENODEV
}
// No internal processes.
// cgroup-v2.rst: "Only cgroups which don't contain any processes can have
// controllers enabled in their "cgroup.subtree_control" files."
if c.parent != nil && c.hasControllersEnabledLocked() {
return linuxerr.EBUSY
}
t := kernel.TaskFromContext(ctx)
if t == nil {
return nil
}
var targetTask *kernel.Task
if pid != 0 {
targetTask = t.PIDNamespace().TaskWithID(kernel.ThreadID(pid))
} else {
targetTask = t
}
if targetTask == nil {
return linuxerr.ESRCH
}
oldNode := targetTask.Cgroup2().(*cgroup)
if err := c.checkMigrationPermsLocked(ctx, creds, oldNode); err != nil {
return err
}
// Locking the TaskSet mutex helps prevent the threadgroup
// membership from changing during the migration.
var errRet error
targetTask.ThreadGroup().WithTaskSetRLock(func() {
actx := &attachCtx{
tasks: make(map[*kernel.Task]struct{}),
oldNodes: make(map[*kernel.Task]*cgroup),
}
targetTask.ThreadGroup().ForEachTaskLocked(func(t *kernel.Task) bool {
if t.ExitState() < kernel.TaskExitInitiated {
if tn, _ := t.Cgroup2().(*cgroup); tn != c {
actx.tasks[t] = struct{}{}
actx.oldNodes[t] = tn
}
}
return true
})
if len(actx.tasks) > 0 {
if !c.canAttach(ctx, actx) {
errRet = linuxerr.EAGAIN
return
}
c.attach(ctx, actx)
}
})
if errRet != nil {
return errRet
}
return nil
}
// getPIDs handles reads from cgroup.procs.
func (c *cgroup) getPIDs(t *kernel.Task) []int {
currPidns := t.PIDNamespace()
var tasks []*kernel.Task
c.fs.tasksMu.RLock()
for task := range c.tasks {
tasks = append(tasks, task)
}
c.fs.tasksMu.RUnlock()
var pids []int
for tgid := range currPidns.IDsOfTasks(tasks, true /* threadGroupIDs */) {
pids = append(pids, int(tgid))
}
sort.Ints(pids)
return pids
}
// setSubtreeControl() handles writes to cgroup.subtree_control.
func (c *cgroup) setSubtreeControl(ctx context.Context, enable []kernel.Cgroup2Ctrl, disable []kernel.Cgroup2Ctrl) error {
c.fs.treeMu.Lock()
defer c.fs.treeMu.Unlock()
if c.deleted.Load() {
return linuxerr.ENODEV
}
for _, cType := range enable {
if !c.isControllerAvailableLocked(cType) {
return linuxerr.ENOENT
}
// No internal processes.
// cgroup-v2.rst: "Only cgroups which don't contain any processes can have
// controllers enabled in their "cgroup.subtree_control" files."
hasTasks := c.tasksCount.Load() > 0
if c.parent != nil && hasTasks { // Although the root cgroup is exempt.
return linuxerr.EBUSY
}
}
for _, cType := range disable {
// Cannot disable if a child cgroup has this controller enabled.
inUse := false
for child := range c.children {
if child.subtreeCtrls[cType] { // +checklocksforce: c.fs.treeMu is locked
inUse = true
break
}
}
if inUse {
return linuxerr.EBUSY
}
}
c.setControllersLocked(ctx, enable, disable)
return nil
}
// kill() handles writes to cgroup.kill.
func (c *cgroup) kill() error {
// cgroup-v2.rst says: "Killing a cgroup tree will deal with concurrent forks appropriately and
// is protected against migrations."
c.fs.treeMu.Lock()
defer c.fs.treeMu.Unlock()
if c.deleted.Load() {
return linuxerr.ENODEV
}
c.fs.tasksMu.Lock()
defer c.fs.tasksMu.Unlock()
// Mark the kill intention across the subtree.
c.killSeq++
c.walkSubtreeLocked(func(child *cgroup) bool {
child.killSeq++ // +checklocksforce: c.fs.tasksMu is locked
return true
})
// Collect tasks to kill. Racing fork()s will check the sequence number we just updated.
var toKill []*kernel.Task
for t := range c.tasks {
toKill = append(toKill, t)
}
c.walkSubtreeLocked(func(child *cgroup) bool {
for t := range child.tasks { // +checklocksforce: c.fs.tasksMu is locked
toKill = append(toKill, t)
}
return true
})
for _, t := range toKill {
t.SendSignal(kernel.SignalInfoPriv(linux.SIGKILL))
}
return nil
}
// stealController detaches a controller from the root cgroup V2 tree.
// +checklocks:c.fs.treeMu
func (c *cgroup) stealController(ctx context.Context, cType kernel.Cgroup2Ctrl) error {
if c.parent != nil {
panic("Unified cgroups inconsistency: stealController called on non-root")
}
if c.subtreeCtrls[cType] {
return linuxerr.EBUSY
}
if ctrl := c.ctrls[cType]; ctrl != nil {
c.removeInterfaceFiles(ctx, ctrl)
ctrl.detach()
}
c.ctrls[cType] = nil
cTypes := []kernel.Cgroup2Ctrl{cType}
c.updateClosestCtrlsLocked(cTypes)
for child := range c.children {
child.rebuildCtrlsLocked(ctx, cTypes) // +checklocksforce: c.fs.treeMu is locked
}
if cType == kernel.Cgroup2Memory {
// Update tasks residing in the root cgroup itself to charge to system ID 0
// since the root memory controller has been stolen.
c.fs.tasksMu.Lock()
c.updateTaskMemoryCgIDsLocked()
c.fs.tasksMu.Unlock()
}
return nil
}
// returnController attaches a controller back to the root cgroup V2 tree.
// +checklocks:c.fs.treeMu
func (c *cgroup) returnController(ctx context.Context, cType kernel.Cgroup2Ctrl) {
if c.parent != nil {
panic("Unified cgroups inconsistency: returnController called on non-root")
}
ctrl := c.newController(cType)
c.ctrls[cType] = ctrl
c.populateInterfaceFiles(ctx, ctrl)
cTypes := []kernel.Cgroup2Ctrl{cType}
c.updateClosestCtrlsLocked(cTypes)
for child := range c.children {
child.rebuildCtrlsLocked(ctx, cTypes) // +checklocksforce: c.fs.treeMu is locked
}
if cType == kernel.Cgroup2Memory {
// Update tasks residing in the root cgroup itself to charge to the root
// memory controller ID now that it has been returned.
c.fs.tasksMu.Lock()
c.updateTaskMemoryCgIDsLocked()
c.fs.tasksMu.Unlock()
}
}
// +checklocks:c.fs.treeMu
// +checklocks:c.fs.tasksMu
func (c *cgroup) updateTaskMemoryCgIDsLocked() {
var memCgID uint32
if mem := c.closestCtrls.Load()[kernel.Cgroup2Memory]; mem != nil {
memCgID = mem.(*memory).id
}
for t := range c.tasks {
t.SetMemCgID(memCgID)
}
}
+149
View File
@@ -0,0 +1,149 @@
// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cgroup2fs
import (
"math"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/sentry/kernel"
"gvisor.dev/gvisor/pkg/sentry/vfs"
)
// Aliased for brevity.
const (
firstController = kernel.Cgroup2Ctrl(0)
numControllers = kernel.Cgroup2NumControllers
)
var ctrlNames = map[string]kernel.Cgroup2Ctrl{
"cpu": kernel.Cgroup2CPU,
"memory": kernel.Cgroup2Memory,
"pids": kernel.Cgroup2PIDs,
"cpuset": kernel.Cgroup2CPUSet,
}
var ctrlTypeStr = map[kernel.Cgroup2Ctrl]string{
kernel.Cgroup2CPU: "cpu",
kernel.Cgroup2Memory: "memory",
kernel.Cgroup2PIDs: "pids",
kernel.Cgroup2CPUSet: "cpuset",
}
// ctrlSet is an array mapping ctrlTypes to their matching active controller instances.
type ctrlSet [kernel.Cgroup2NumControllers]controller
func (cs *ctrlSet) fork() *ctrlSet {
clone := *cs
return &clone
}
// interfaceFile defines a cgroup interface file to be instantiated.
type interfaceFile struct {
name string
source vfs.DynamicBytesSource
perm linux.FileMode
isEvent bool
onEventCreated func(inode *eventFile)
showAtRoot bool
ctrl controller
}
type attachCtx struct {
tasks map[*kernel.Task]struct{}
oldNodes map[*kernel.Task]*cgroup
}
// controller is an interface for a cgroup subsystem controller.
type controller interface {
// Task lifecycle operations.
// Invoked during CreateProcess, clone(), exit() etc.
canEnter(ctx context.Context, t *kernel.Task) bool
cancelEnter(ctx context.Context, t *kernel.Task)
enter(ctx context.Context, t *kernel.Task)
exit(ctx context.Context, t *kernel.Task)
// Task migration operations.
// Invoked by cgroup v2 admin operations.
canAttach(ctx context.Context, actx *attachCtx) bool
cancelAttach(ctx context.Context, actx *attachCtx)
attach(ctx context.Context, actx *attachCtx)
// interfaceFiles returns definitions for tailored interface files specific to this controller.
interfaceFiles() []interfaceFile
// interfaceFileNames returns the names of the interface files tied to this controller.
interfaceFileNames() []string
// detach marks this controller instance as detached/disabled.
detach()
// isActive returns true if the hosting cgroup hasn't detached this controller yet.
isActive() bool
}
// newController instantiates a new controller of the given type for the specified cgroup.
// +checklocksread:c.fs.treeMu
func (c *cgroup) newController(cType kernel.Cgroup2Ctrl) controller {
var parent controller
if c.parent != nil {
parent = c.parent.ctrls[cType] // +checklocksforce: c.fs.treeMu is locked
if parent == nil {
panic("unified cgroups inconsistency: parent controller is nil")
}
}
switch cType {
case kernel.Cgroup2CPU:
var cpuParent *cpu
if parent != nil {
cpuParent = parent.(*cpu)
}
cpuCtrl := &cpu{c: c, parent: cpuParent}
cpuCtrl.weight.Store(100)
cpuCtrl.maxUSec.Store(math.MaxInt64)
cpuCtrl.periodUSec.Store(100000)
return cpuCtrl
case kernel.Cgroup2Memory:
var memParent *memory
if parent != nil {
memParent = parent.(*memory)
}
memCtrl := &memory{c: c, parent: memParent, id: c.fs.nextMemoryID()}
memCtrl.maxBytes.Store(math.MaxInt64)
memCtrl.highBytes.Store(math.MaxInt64)
return memCtrl
case kernel.Cgroup2PIDs:
var pidsParent *pids
if parent != nil {
pidsParent = parent.(*pids)
}
return &pids{
c: c,
parent: pidsParent,
max: pidLimitUnlimited,
}
case kernel.Cgroup2CPUSet:
var cpusetParent *cpuset
if parent != nil {
cpusetParent = parent.(*cpuset)
}
return &cpuset{c: c, parent: cpusetParent}
default:
return nil
}
}
+198
View File
@@ -0,0 +1,198 @@
// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cgroup2fs
import (
"bytes"
"fmt"
"math"
"strconv"
"strings"
"gvisor.dev/gvisor/pkg/atomicbitops"
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/errors/linuxerr"
"gvisor.dev/gvisor/pkg/sentry/kernel"
"gvisor.dev/gvisor/pkg/sentry/vfs"
"gvisor.dev/gvisor/pkg/usermem"
)
// +stateify savable
type cpu struct {
c *cgroup
parent *cpu
detached atomicbitops.Bool
// weight is the CPU weight representing the cpu.weight value.
// +checkatomic
weight atomicbitops.Int64
// maxUSec is the CPU quota limit representing the cpu.max quota.
// +checkatomic
maxUSec atomicbitops.Int64
// periodUSec is the CPU period limit representing the cpu.max period.
// +checkatomic
periodUSec atomicbitops.Int64
}
// canEnter implements controller.canEnter.
func (c *cpu) canEnter(ctx context.Context, t *kernel.Task) bool { return true }
// cancelEnter implements controller.cancelEnter.
func (c *cpu) cancelEnter(ctx context.Context, t *kernel.Task) {}
// enter implements controller.enter.
func (c *cpu) enter(ctx context.Context, t *kernel.Task) {}
// exit implements controller.exit.
func (c *cpu) exit(ctx context.Context, t *kernel.Task) {}
// canAttach implements controller.canAttach.
func (c *cpu) canAttach(ctx context.Context, actx *attachCtx) bool { return true }
// cancelAttach implements controller.cancelAttach.
func (c *cpu) cancelAttach(ctx context.Context, actx *attachCtx) {}
// attach implements controller.attach.
func (c *cpu) attach(ctx context.Context, actx *attachCtx) {}
// interfaceFiles implements controller.interfaceFiles.
func (c *cpu) interfaceFiles() []interfaceFile {
return []interfaceFile{
{name: "cpu.stat", source: &cpuStat{c: c}, perm: 0444, showAtRoot: true},
{name: "cpu.max", source: &cpuMax{c: c}, perm: 0644},
{name: "cpu.weight", source: &cpuWeight{c: c}, perm: 0644},
}
}
// interfaceFileNames implements controller.interfaceFileNames.
func (c *cpu) interfaceFileNames() []string {
return []string{"cpu.stat", "cpu.max", "cpu.weight"}
}
// +stateify savable
type cpuStat struct {
c *cpu
}
// Generate implements vfs.DynamicBytesSource.Generate.
func (c *cpuStat) Generate(ctx context.Context, buf *bytes.Buffer) error {
buf.WriteString("usage_usec 0\nuser_usec 0\nsystem_usec 0\nnice_usec 0\nnr_periods 0\nnr_throttled 0\nthrottled_usec 0\nnr_bursts 0\nburst_usec 0\n")
return nil
}
// +stateify savable
type cpuMax struct {
c *cpu
}
// Generate implements vfs.DynamicBytesSource.Generate.
func (c *cpuMax) Generate(ctx context.Context, buf *bytes.Buffer) error {
quota := c.c.maxUSec.Load()
period := c.c.periodUSec.Load()
if quota == math.MaxInt64 {
fmt.Fprintf(buf, "max %d\n", period)
} else {
fmt.Fprintf(buf, "%d %d\n", quota, period)
}
return nil
}
// Write implements vfs.WritableDynamicBytesSource.Write.
//
// Note: Although cpu.max is writable and remembers the value, nothing is enforced.
func (c *cpuMax) Write(ctx context.Context, _ *vfs.FileDescription, src usermem.IOSequence, offset int64) (int64, error) {
if src.NumBytes() > 1024 {
return 0, linuxerr.EINVAL
}
buf := make([]byte, src.NumBytes())
if _, err := src.CopyIn(ctx, buf); err != nil {
return 0, err
}
str := strings.TrimSpace(string(buf))
fields := strings.Fields(str)
if len(fields) < 1 || len(fields) > 2 {
return 0, linuxerr.EINVAL
}
var quota int64
if fields[0] == "max" {
quota = math.MaxInt64
} else {
val, err := strconv.ParseInt(fields[0], 10, 64)
if err != nil || val <= 0 {
return 0, linuxerr.EINVAL
}
quota = val
}
var period int64
if len(fields) == 2 {
val, err := strconv.ParseInt(fields[1], 10, 64)
if err != nil || val <= 0 {
return 0, linuxerr.EINVAL
}
period = val
} else {
period = c.c.periodUSec.Load()
}
c.c.maxUSec.Store(quota)
c.c.periodUSec.Store(period)
return int64(len(buf)), nil
}
// +stateify savable
type cpuWeight struct {
c *cpu
}
// Generate implements vfs.DynamicBytesSource.Generate.
func (c *cpuWeight) Generate(ctx context.Context, buf *bytes.Buffer) error {
fmt.Fprintf(buf, "%d\n", c.c.weight.Load())
return nil
}
// Write implements vfs.WritableDynamicBytesSource.Write.
//
// Note: Although cpu.weight is writable and remembers the value, nothing is enforced.
func (c *cpuWeight) Write(ctx context.Context, _ *vfs.FileDescription, src usermem.IOSequence, offset int64) (int64, error) {
if src.NumBytes() > 1024 {
return 0, linuxerr.EINVAL
}
buf := make([]byte, src.NumBytes())
if _, err := src.CopyIn(ctx, buf); err != nil {
return 0, err
}
str := strings.TrimSpace(string(buf))
val, err := strconv.ParseInt(str, 10, 64)
if err != nil {
return 0, linuxerr.EINVAL
}
if val < 1 || val > 10000 {
return 0, linuxerr.ERANGE
}
c.c.weight.Store(val)
return int64(len(buf)), nil
}
// detach implements controller.detach.
func (c *cpu) detach() {
c.detached.Store(true)
}
// isActive implements controller.isActive.
func (c *cpu) isActive() bool {
return !c.detached.Load()
}
+182
View File
@@ -0,0 +1,182 @@
// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cgroup2fs
import (
"bytes"
"fmt"
"gvisor.dev/gvisor/pkg/atomicbitops"
"gvisor.dev/gvisor/pkg/bitmap"
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/errors/linuxerr"
"gvisor.dev/gvisor/pkg/hostarch"
"gvisor.dev/gvisor/pkg/sentry/kernel"
"gvisor.dev/gvisor/pkg/sentry/vfs"
"gvisor.dev/gvisor/pkg/sync"
"gvisor.dev/gvisor/pkg/usermem"
)
// +stateify savable
type cpuset struct {
c *cgroup
parent *cpuset
detached atomicbitops.Bool
mu sync.Mutex `state:"nosave"`
cpus *bitmap.Bitmap
mems *bitmap.Bitmap
}
// canEnter implements controller.canEnter.
func (cs *cpuset) canEnter(ctx context.Context, t *kernel.Task) bool { return true }
// cancelEnter implements controller.cancelEnter.
func (cs *cpuset) cancelEnter(ctx context.Context, t *kernel.Task) {}
// enter implements controller.enter.
func (cs *cpuset) enter(ctx context.Context, t *kernel.Task) {}
// exit implements controller.exit.
func (cs *cpuset) exit(ctx context.Context, t *kernel.Task) {}
// canAttach implements controller.canAttach.
func (cs *cpuset) canAttach(ctx context.Context, actx *attachCtx) bool { return true }
// cancelAttach implements controller.cancelAttach.
func (cs *cpuset) cancelAttach(ctx context.Context, actx *attachCtx) {}
// attach implements controller.attach.
func (cs *cpuset) attach(ctx context.Context, actx *attachCtx) {}
// interfaceFiles implements controller.interfaceFiles.
func (cs *cpuset) interfaceFiles() []interfaceFile {
return []interfaceFile{
{name: "cpuset.cpus", source: &cpusetCpus{cs: cs}, perm: 0644},
{name: "cpuset.mems", source: &cpusetMems{cs: cs}, perm: 0644},
}
}
// interfaceFileNames implements controller.interfaceFileNames.
func (cs *cpuset) interfaceFileNames() []string {
return []string{"cpuset.cpus", "cpuset.mems"}
}
// +stateify savable
type cpusetCpus struct {
cs *cpuset
}
// Generate implements vfs.DynamicBytesSource.Generate.
func (cc *cpusetCpus) Generate(ctx context.Context, buf *bytes.Buffer) error {
cc.cs.mu.Lock()
defer cc.cs.mu.Unlock()
if cc.cs.cpus != nil {
fmt.Fprintf(buf, "%s\n", bitmap.FormatList(cc.cs.cpus))
} else {
buf.WriteString("\n")
}
return nil
}
// Write implements vfs.WritableDynamicBytesSource.Write.
func (cc *cpusetCpus) Write(ctx context.Context, _ *vfs.FileDescription, src usermem.IOSequence, offset int64) (int64, error) {
if src.NumBytes() > hostarch.PageSize {
return 0, linuxerr.EINVAL
}
k := kernel.KernelFromContext(ctx)
if k == nil {
return 0, linuxerr.EINVAL
}
maxCpus := uint32(k.ApplicationCores())
buf := make([]byte, src.NumBytes())
n, err := src.CopyIn(ctx, buf)
if err != nil {
return 0, err
}
buf = buf[:n]
b, err := bitmap.ParseList(string(buf), maxCpus)
if err != nil {
return 0, linuxerr.EINVAL
}
if got, want := b.Maximum(), maxCpus; got > want {
return 0, linuxerr.EINVAL
}
cc.cs.mu.Lock()
defer cc.cs.mu.Unlock()
cc.cs.cpus = b
return int64(n), nil
}
// +stateify savable
type cpusetMems struct {
cs *cpuset
}
// Generate implements vfs.DynamicBytesSource.Generate.
func (cm *cpusetMems) Generate(ctx context.Context, buf *bytes.Buffer) error {
cm.cs.mu.Lock()
defer cm.cs.mu.Unlock()
if cm.cs.mems != nil {
fmt.Fprintf(buf, "%s\n", bitmap.FormatList(cm.cs.mems))
} else {
buf.WriteString("\n")
}
return nil
}
// Write implements vfs.WritableDynamicBytesSource.Write.
func (cm *cpusetMems) Write(ctx context.Context, _ *vfs.FileDescription, src usermem.IOSequence, offset int64) (int64, error) {
if src.NumBytes() > hostarch.PageSize {
return 0, linuxerr.EINVAL
}
maxMems := uint32(1)
buf := make([]byte, src.NumBytes())
n, err := src.CopyIn(ctx, buf)
if err != nil {
return 0, err
}
buf = buf[:n]
b, err := bitmap.ParseList(string(buf), maxMems)
if err != nil {
return 0, linuxerr.EINVAL
}
if got, want := b.Maximum(), maxMems; got > want {
return 0, linuxerr.EINVAL
}
cm.cs.mu.Lock()
defer cm.cs.mu.Unlock()
cm.cs.mems = b
return int64(n), nil
}
// detach implements controller.detach.
func (cs *cpuset) detach() {
cs.detached.Store(true)
}
// isActive implements controller.isActive.
func (cs *cpuset) isActive() bool {
return !cs.detached.Load()
}
+177
View File
@@ -0,0 +1,177 @@
// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cgroup2fs
import (
"bytes"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/atomicbitops"
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/errors/linuxerr"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/kernfs"
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
"gvisor.dev/gvisor/pkg/sentry/vfs"
"gvisor.dev/gvisor/pkg/usermem"
"gvisor.dev/gvisor/pkg/waiter"
)
// eventFile represents a generic cgroup event file that supports both poll()
// via a sequence counter and inotify.
//
// +stateify savable
type eventFile struct {
kernfs.DynamicBytesFile
c *cgroup
notifyQueue waiter.Queue
eventSeq atomicbitops.Uint64
}
// Valid implements kernfs.Inode.Valid.
func (f *eventFile) Valid(ctx context.Context, parent *kernfs.Dentry, name string) bool {
return !f.c.deleted.Load()
}
func (fs *filesystem) newEventFile(ctx context.Context, uid auth.KUID, gid auth.KGID, c *cgroup, data vfs.DynamicBytesSource, ctrl controller) *eventFile {
f := &eventFile{c: c}
f.eventSeq.Store(1)
var src vfs.DynamicBytesSource
if ws, ok := data.(vfs.WritableDynamicBytesSource); ok {
src = &cgroupSourceWritable{c: c, ctrl: ctrl, src: ws}
} else {
src = &cgroupSourceReadOnly{c: c, ctrl: ctrl, src: data}
}
f.DynamicBytesFile.InitWithIDs(ctx, uid, gid, linux.UNNAMED_MAJOR, fs.devMinor, fs.NextIno(), src, 0444)
return f
}
// SetStat implements kernfs.Inode.SetStat.
func (f *eventFile) SetStat(ctx context.Context, fs *vfs.Filesystem, creds *auth.Credentials, opts vfs.SetStatOptions) error {
return f.InodeAttrs.SetStat(ctx, fs, creds, opts)
}
// Open implements kernfs.Inode.Open overriding DynamicBytesFile open method.
func (f *eventFile) Open(ctx context.Context, rp *vfs.ResolvingPath, d *kernfs.Dentry, opts vfs.OpenOptions) (*vfs.FileDescription, error) {
data, err := f.Data(ctx)
if err != nil {
return nil, err
}
fd := &eventFD{ep: f, data: data}
if err := fd.Init(rp.Mount(), d, fd, opts.Flags, rp.Credentials()); err != nil {
return nil, err
}
return &fd.vfsfd, nil
}
// Notify signals waiters and inotify watches that an event has occurred.
func (f *eventFile) Notify(ctx context.Context) {
f.eventSeq.Add(1)
f.notifyQueue.Notify(waiter.EventMask(waiter.EventPri | waiter.ReadableEvents))
f.Watches().Notify(ctx, "", linux.IN_MODIFY, 0, vfs.InodeEvent, false)
}
// eventFD implements vfs.FileDescriptionImpl providing poll readiness.
//
// +stateify savable
type eventFD struct {
vfs.FileDescriptionDefaultImpl
vfs.DynamicBytesFileDescriptionImpl
vfs.NoLockFD
vfsfd vfs.FileDescription
ep *eventFile
lastEventSeq atomicbitops.Uint64
data vfs.DynamicBytesSource
}
// Init overrides DynamicBytesFD.Init to initialize vfsfd correctly.
func (fd *eventFD) Init(m *vfs.Mount, d *kernfs.Dentry, data vfs.DynamicBytesSource, flags uint32, creds *auth.Credentials) error {
if err := fd.vfsfd.Init(fd, flags, creds, m, d.VFSDentry(),
&vfs.FileDescriptionOptions{
DenySpliceIn: true,
},
); err != nil {
return err
}
fd.DynamicBytesFileDescriptionImpl.Init(&fd.vfsfd, data)
return nil
}
// Seek implements vfs.FileDescriptionImpl.Seek.
func (fd *eventFD) Seek(ctx context.Context, offset int64, whence int32) (int64, error) {
return fd.DynamicBytesFileDescriptionImpl.Seek(ctx, offset, whence)
}
// Generate implements vfs.DynamicBytesSource.Generate.
func (fd *eventFD) Generate(ctx context.Context, buf *bytes.Buffer) error {
fd.lastEventSeq.Store(fd.ep.eventSeq.Load())
return fd.data.Generate(ctx, buf)
}
// Read implements vfs.FileDescriptionImpl.Read.
func (fd *eventFD) Read(ctx context.Context, dst usermem.IOSequence, opts vfs.ReadOptions) (int64, error) {
return fd.DynamicBytesFileDescriptionImpl.Read(ctx, dst, opts)
}
// PRead implements vfs.FileDescriptionImpl.PRead.
func (fd *eventFD) PRead(ctx context.Context, dst usermem.IOSequence, offset int64, opts vfs.ReadOptions) (int64, error) {
return fd.DynamicBytesFileDescriptionImpl.PRead(ctx, dst, offset, opts)
}
// Write implements vfs.FileDescriptionImpl.Write.
func (fd *eventFD) Write(ctx context.Context, src usermem.IOSequence, opts vfs.WriteOptions) (int64, error) {
return 0, linuxerr.EBADF
}
// PWrite implements vfs.FileDescriptionImpl.PWrite.
func (fd *eventFD) PWrite(ctx context.Context, src usermem.IOSequence, offset int64, opts vfs.WriteOptions) (int64, error) {
return 0, linuxerr.EBADF
}
// Release implements vfs.FileDescriptionImpl.Release.
func (fd *eventFD) Release(context.Context) {}
// Stat implements vfs.FileDescriptionImpl.Stat.
func (fd *eventFD) Stat(ctx context.Context, opts vfs.StatOptions) (linux.Statx, error) {
fs := fd.vfsfd.VirtualDentry().Mount().Filesystem()
return fd.ep.Stat(ctx, fs, opts)
}
// SetStat implements vfs.FileDescriptionImpl.SetStat.
func (fd *eventFD) SetStat(context.Context, vfs.SetStatOptions) error {
return linuxerr.EPERM
}
// Readiness implements waiter.Waitable.Readiness.
func (fd *eventFD) Readiness(mask waiter.EventMask) waiter.EventMask {
ready := waiter.ReadableEvents
if fd.lastEventSeq.Load() < fd.ep.eventSeq.Load() {
ready |= waiter.EventPri | waiter.EventErr
}
return ready & mask
}
// EventRegister implements waiter.Waitable.EventRegister.
func (fd *eventFD) EventRegister(e *waiter.Entry) error {
fd.ep.notifyQueue.EventRegister(e)
return nil
}
// EventUnregister implements waiter.Waitable.EventUnregister.
func (fd *eventFD) EventUnregister(e *waiter.Entry) {
fd.ep.notifyQueue.EventUnregister(e)
}
+241
View File
@@ -0,0 +1,241 @@
// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package cgroup2fs provides the cgroupv2 filesystem implementation.
package cgroup2fs
import (
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/atomicbitops"
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/errors/linuxerr"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/kernfs"
"gvisor.dev/gvisor/pkg/sentry/kernel"
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
"gvisor.dev/gvisor/pkg/sentry/vfs"
)
// Name is the default filesystem name.
const Name = "cgroup2"
// FilesystemType implements vfs.FilesystemType for cgroup2 (cgroup v2).
//
// +stateify savable
type FilesystemType struct{}
// Name implements vfs.FilesystemType.Name.
func (FilesystemType) Name() string {
return Name
}
// Release implements vfs.FilesystemType.Release.
func (FilesystemType) Release(ctx context.Context) {}
// GetFilesystem implements vfs.FilesystemType.GetFilesystem.
func (ft FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.VirtualFilesystem, creds *auth.Credentials, source string, opts vfs.GetFilesystemOptions) (*vfs.Filesystem, *vfs.Dentry, error) {
mopts := vfs.GenericParseMountOptions(opts.Data)
for k := range mopts {
switch k {
case "nsdelegate":
// TODO (b/513700867): Silently ignore because we don't support cgroup namespaces anyway.
default:
ctx.Debugf("cgroup2fs.FilesystemType.GetFilesystem: unknown option: %s", k)
return nil, nil, linuxerr.EINVAL
}
}
fs := kernel.KernelFromContext(ctx).Cgroup2FS().(*filesystem)
fs.mounted.Store(1)
fs.root.IncRef()
vfsfs := fs.VFSFilesystem()
vfsfs.IncRef()
return vfsfs, fs.root.VFSDentry(), nil
}
// NewFilesystem creates and registers the cgroup2fs singleton. It should be called early
// during boot before the first task is created.
func NewFilesystem(ctx context.Context, k *kernel.Kernel, vfsObj *vfs.VirtualFilesystem) (*vfs.Filesystem, error) {
devMinor, err := vfsObj.GetAnonBlockDevMinor()
if err != nil {
return nil, err
}
fs := &filesystem{
rootCreds: auth.NewRootCredentials(k.RootUserNamespace()),
devMinor: devMinor,
}
fs.VFSFilesystem().Init(vfsObj, &FilesystemType{}, fs)
rootMode := linux.FileMode(0755) | linux.ModeDirectory
rootInode := fs.newRootInode(ctx, rootMode)
rootD := &kernfs.Dentry{}
rootD.InitRoot(&fs.Filesystem, rootInode)
fs.root = rootD
return fs.VFSFilesystem(), nil
}
// filesystem implements vfs.FilesystemImpl for cgroup2.
//
// +stateify savable
type filesystem struct {
kernfs.Filesystem
rootCreds *auth.Credentials
devMinor uint32
root *kernfs.Dentry
// mounted tracks whether the filesystem has been mounted/initialized.
mounted atomicbitops.Uint32
// nextMemCgroupID is used to allocate unique IDs to memory controllers.
nextMemCgroupID atomicbitops.Uint32
// treeMu protects controller hierarchy enablement traversals, and tracking of owned controllers.
treeMu treeRWMutex `state:"nosave"`
// tasksMu protects the task-to-cgroup mapping (the c.tasks map across all cgroups)
// and population bubbling.
tasksMu tasksRWMutex `state:"nosave"`
}
// EverMounted implements EverMounted.
func (fs *filesystem) EverMounted() bool {
return fs.mounted.Load() != 0
}
// Release implements vfs.FilesystemImpl.Release.
func (fs *filesystem) Release(ctx context.Context) {
if fs.root != nil {
fs.root.DecRef(ctx)
fs.root = nil
}
fs.Filesystem.VFSFilesystem().VirtualFilesystem().PutAnonBlockDevMinor(fs.devMinor)
fs.Filesystem.Release(ctx)
}
// MountOptions implements vfs.FilesystemImpl.MountOptions.
func (fs *filesystem) MountOptions() string {
return ""
}
// LockTree implements kernel.Cgroup2FS.LockTree.
func (fs *filesystem) LockTree() {
fs.treeMu.Lock()
}
// UnlockTree implements kernel.Cgroup2FS.UnlockTree.
func (fs *filesystem) UnlockTree() {
fs.treeMu.Unlock()
}
// RLockTree implements kernel.Cgroup2FS.RLockTree.
func (fs *filesystem) RLockTree() {
fs.treeMu.RLock()
}
// RUnlockTree implements kernel.Cgroup2FS.RUnlockTree.
func (fs *filesystem) RUnlockTree() {
fs.treeMu.RUnlock()
}
// StealControllerLocked implements kernel.Cgroup2FS.StealControllerLocked.
// +checklocks:fs.treeMu
func (fs *filesystem) StealControllerLocked(ctx context.Context, cType kernel.Cgroup2Ctrl) error {
rootCG := fs.root.Inode().(*cgroup)
return rootCG.stealController(ctx, cType) // +checklocksforce: fs.treeMu is locked
}
// ReturnControllerLocked implements kernel.Cgroup2FS.ReturnControllerLocked.
// +checklocks:fs.treeMu
func (fs *filesystem) ReturnControllerLocked(ctx context.Context, cType kernel.Cgroup2Ctrl) {
rootCG := fs.root.Inode().(*cgroup)
rootCG.returnController(ctx, cType) // +checklocksforce: fs.treeMu is locked
}
// RootCgroup implements kernel.Cgroup2FS.RootCgroup.
func (fs *filesystem) RootCgroup() kernel.Cgroup2 {
return fs.root.Inode().(*cgroup)
}
func (fs *filesystem) newRootInode(ctx context.Context, mode linux.FileMode) kernfs.Inode {
c := &cgroup{
fs: fs,
tasks: make(map[*kernel.Task]struct{}),
path: "/",
parent: nil,
children: make(map[*cgroup]struct{}),
}
c.InodeAttrs.Init(ctx, fs.rootCreds, linux.UNNAMED_MAJOR, fs.devMinor, fs.NextIno(), mode)
c.OrderedChildren.Init(kernfs.OrderedChildrenOptions{Writable: true})
c.IncLinks(c.OrderedChildren.Populate(fs.rootInodes(ctx, fs.rootCreds.EffectiveKUID, fs.rootCreds.EffectiveKGID, c)))
c.fs.treeMu.Lock()
defer c.fs.treeMu.Unlock()
c.initRoot(ctx)
c.maxDescendants.Store(limitMax)
c.maxDepth.Store(limitMax)
return c
}
// +checklocks:fs.treeMu
func (fs *filesystem) newCgroupLocked(ctx context.Context, creds *auth.Credentials, mode linux.FileMode, path string, parent *cgroup) kernfs.Inode {
c := &cgroup{
fs: fs,
tasks: make(map[*kernel.Task]struct{}),
path: path,
parent: parent,
children: make(map[*cgroup]struct{}),
level: parent.level + 1,
}
c.InodeAttrs.Init(ctx, creds, linux.UNNAMED_MAJOR, fs.devMinor, fs.NextIno(), mode)
c.OrderedChildren.Init(kernfs.OrderedChildrenOptions{Writable: true})
c.IncLinks(c.OrderedChildren.Populate(fs.cgroupInodes(ctx, creds.EffectiveKUID, creds.EffectiveKGID, c)))
c.init(ctx) // +checklocksforce: c.fs.treeMu is locked
c.maxDescendants.Store(limitMax)
c.maxDepth.Store(limitMax)
parent.children[c] = struct{}{} // +checklocksforce: c.fs.treeMu is locked
for curr := parent; curr != nil; curr = curr.parent {
curr.nrDescendants.Add(1) // +checklocksforce: c.fs.treeMu is locked
}
return c
}
// +stateify savable
type implStatFS struct{}
// StatFS implements kernfs.Inode.StatFS.
func (*implStatFS) StatFS(context.Context, *vfs.Filesystem) (linux.Statfs, error) {
return vfs.GenericStatFS(linux.CGROUP2_SUPER_MAGIC), nil
}
// StatFSAt implements vfs.FilesystemImpl.StatFSAt.
func (fs *filesystem) StatFSAt(ctx context.Context, rp *vfs.ResolvingPath) (linux.Statfs, error) {
return vfs.GenericStatFS(linux.CGROUP2_SUPER_MAGIC), nil
}
func (fs *filesystem) nextMemoryID() uint32 {
id := fs.nextMemCgroupID.Add(1)
if id == 0 {
id = fs.nextMemCgroupID.Add(1)
}
return id
}
+297
View File
@@ -0,0 +1,297 @@
// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cgroup2fs
import (
"bytes"
"fmt"
"math"
"strconv"
"strings"
"gvisor.dev/gvisor/pkg/atomicbitops"
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/errors/linuxerr"
"gvisor.dev/gvisor/pkg/hostarch"
"gvisor.dev/gvisor/pkg/sentry/kernel"
"gvisor.dev/gvisor/pkg/sentry/usage"
"gvisor.dev/gvisor/pkg/sentry/vfs"
"gvisor.dev/gvisor/pkg/usermem"
)
// +stateify savable
type memory struct {
c *cgroup
parent *memory
id uint32
detached atomicbitops.Bool
// maxBytes is the memory limit representing the memory.max limit.
// +checkatomic
maxBytes atomicbitops.Int64
// highBytes is the memory limit representing the memory.high limit.
// +checkatomic
highBytes atomicbitops.Int64
}
// canEnter implements controller.canEnter.
func (m *memory) canEnter(ctx context.Context, t *kernel.Task) bool { return true }
// cancelEnter implements controller.cancelEnter.
func (m *memory) cancelEnter(ctx context.Context, t *kernel.Task) {}
// enter implements controller.enter.
func (m *memory) enter(ctx context.Context, t *kernel.Task) {
t.SetMemCgID(m.id)
}
// exit implements controller.exit.
func (m *memory) exit(ctx context.Context, t *kernel.Task) {
t.SetMemCgID(0)
}
// canAttach implements controller.canAttach.
func (m *memory) canAttach(ctx context.Context, actx *attachCtx) bool { return true }
// cancelAttach implements controller.cancelAttach.
func (m *memory) cancelAttach(ctx context.Context, actx *attachCtx) {}
// attach implements controller.attach.
func (m *memory) attach(ctx context.Context, actx *attachCtx) {
for t := range actx.tasks {
t.SetMemCgID(m.id)
}
}
// interfaceFiles implements controller.interfaceFiles.
func (m *memory) interfaceFiles() []interfaceFile {
return []interfaceFile{
{name: "memory.events", source: &memoryEvents{m: m}, perm: 0444},
{name: "memory.current", source: &memoryCurrent{m: m}, perm: 0444},
{name: "memory.max", source: &memoryMax{m: m}, perm: 0644},
{name: "memory.high", source: &memoryHigh{m: m}, perm: 0644},
}
}
// interfaceFileNames implements controller.interfaceFileNames.
func (m *memory) interfaceFileNames() []string {
return []string{"memory.events", "memory.current", "memory.max", "memory.high"}
}
// +stateify savable
type memoryEvents struct {
m *memory
}
// Generate implements vfs.DynamicBytesSource.Generate.
func (me *memoryEvents) Generate(ctx context.Context, buf *bytes.Buffer) error {
buf.WriteString("low 0\nhigh 0\nmax 0\noom 0\noom_kill 0\noom_group_kill 0\nsock_throttled 0\n")
return nil
}
// +stateify savable
type memoryCurrent struct {
m *memory
}
// Collects all the memory cgroup ids under the given cgroup.
// +checklocksread:c.fs.treeMu
func (mc *memoryCurrent) collectMemCgIDs(c *cgroup, memCgIDs map[uint32]struct{}) {
// Add ourselves.
if mem := c.ctrls[kernel.Cgroup2Memory]; mem != nil {
memCgIDs[mem.(*memory).id] = struct{}{}
}
// Add our children.
for child := range c.children {
mc.collectMemCgIDs(child, memCgIDs) // +checklocksforce: c.fs.treeMu is locked
}
}
// Returns the memory usage for all cgroup ids in memCgIDs.
func getUsage(k *kernel.Kernel, memCgIDs map[uint32]struct{}) uint64 {
k.MemoryFile().UpdateUsage(memCgIDs)
var totalBytes uint64
for id := range memCgIDs {
_, bytes := usage.MemoryAccounting.CopyPerCg(id)
totalBytes += bytes
}
return totalBytes
}
// Generate implements vfs.DynamicBytesSource.Generate.
func (mc *memoryCurrent) Generate(ctx context.Context, buf *bytes.Buffer) error {
k := kernel.KernelFromContext(ctx)
memCgIDs := make(map[uint32]struct{})
mc.m.c.fs.treeMu.RLock()
mc.collectMemCgIDs(mc.m.c, memCgIDs)
mc.m.c.fs.treeMu.RUnlock()
totalBytes := getUsage(k, memCgIDs)
fmt.Fprintf(buf, "%d\n", totalBytes)
return nil
}
// +stateify savable
type memoryMax struct {
m *memory
}
// Generate implements vfs.DynamicBytesSource.Generate.
func (mm *memoryMax) Generate(ctx context.Context, buf *bytes.Buffer) error {
val := mm.m.maxBytes.Load()
if val == math.MaxInt64 {
buf.WriteString("max\n")
} else {
fmt.Fprintf(buf, "%d\n", val)
}
return nil
}
// Write implements vfs.WritableDynamicBytesSource.Write.
//
// Note: Although memory.max is writable and remembers the value, nothing is enforced.
func (mm *memoryMax) Write(ctx context.Context, _ *vfs.FileDescription, src usermem.IOSequence, offset int64) (int64, error) {
if src.NumBytes() > 1024 {
return 0, linuxerr.EINVAL
}
buf := make([]byte, src.NumBytes())
if _, err := src.CopyIn(ctx, buf); err != nil {
return 0, err
}
str := strings.TrimSpace(string(buf))
val, err := parseMemoryLimit(str)
if err != nil {
return 0, linuxerr.EINVAL
}
mm.m.maxBytes.Store(val)
return int64(len(buf)), nil
}
// +stateify savable
type memoryHigh struct {
m *memory
}
// Generate implements vfs.DynamicBytesSource.Generate.
func (mh *memoryHigh) Generate(ctx context.Context, buf *bytes.Buffer) error {
val := mh.m.highBytes.Load()
if val == math.MaxInt64 {
buf.WriteString("max\n")
} else {
fmt.Fprintf(buf, "%d\n", val)
}
return nil
}
// Write implements vfs.WritableDynamicBytesSource.Write.
//
// Note: Although memory.high is writable and remembers the value, nothing is enforced.
func (mh *memoryHigh) Write(ctx context.Context, _ *vfs.FileDescription, src usermem.IOSequence, offset int64) (int64, error) {
if src.NumBytes() > 1024 {
return 0, linuxerr.EINVAL
}
buf := make([]byte, src.NumBytes())
if _, err := src.CopyIn(ctx, buf); err != nil {
return 0, err
}
str := strings.TrimSpace(string(buf))
val, err := parseMemoryLimit(str)
if err != nil {
return 0, linuxerr.EINVAL
}
mh.m.highBytes.Store(val)
return int64(len(buf)), nil
}
// detach implements controller.detach.
func (m *memory) detach() {
m.detached.Store(true)
}
// isActive implements controller.isActive.
func (m *memory) isActive() bool {
return !m.detached.Load()
}
func parseMemoryLimit(str string) (int64, error) {
if str == "max" {
return math.MaxInt64, nil
}
if len(str) == 0 {
return 0, fmt.Errorf("empty memory limit")
}
idx := -1
for i := 0; i < len(str); i++ {
c := str[i]
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') {
idx = i
break
}
}
var numStr, suffix string
if idx == -1 {
numStr = str
} else {
numStr = str[:idx]
suffix = strings.ToLower(str[idx:])
}
val, err := strconv.ParseInt(numStr, 10, 64)
if err != nil {
return 0, err
}
if val < 0 {
return 0, fmt.Errorf("negative memory limit: %d", val)
}
result := val
if suffix != "" {
var shift uint
switch {
case strings.HasPrefix(suffix, "k"):
shift = 10
case strings.HasPrefix(suffix, "m"):
shift = 20
case strings.HasPrefix(suffix, "g"):
shift = 30
case strings.HasPrefix(suffix, "t"):
shift = 40
case strings.HasPrefix(suffix, "p"):
shift = 50
case strings.HasPrefix(suffix, "e"):
shift = 60
default:
return 0, fmt.Errorf("invalid suffix %q", suffix)
}
rem := suffix[1:]
if rem != "" && rem != "b" && rem != "ib" {
return 0, fmt.Errorf("invalid suffix configuration %q", suffix)
}
if val > (math.MaxInt64 >> shift) {
return 0, fmt.Errorf("value overflowed")
}
result = val << shift
}
if result != math.MaxInt64 {
result = result &^ int64(hostarch.PageSize-1)
}
return result, nil
}
+300
View File
@@ -0,0 +1,300 @@
// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cgroup2fs
import (
"bytes"
"fmt"
"strconv"
"strings"
"gvisor.dev/gvisor/pkg/atomicbitops"
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/errors/linuxerr"
"gvisor.dev/gvisor/pkg/sentry/kernel"
"gvisor.dev/gvisor/pkg/sentry/vfs"
"gvisor.dev/gvisor/pkg/usermem"
)
const (
pidMaxLimit = 4 * 1024 * 1024
pidLimitUnlimited = pidMaxLimit + 1
)
// +stateify savable
type pids struct {
c *cgroup
parent *pids
detached atomicbitops.Bool
// mu protects the fields below.
mu pidsMutex `state:"nosave"`
committed int64
max int64
peak int64
localMaxHits atomicbitops.Uint64
hierMaxHits atomicbitops.Uint64
eventsFile *eventFile
eventsLocalFile *eventFile
}
// canEnter implements controller.canEnter.
func (p *pids) canEnter(ctx context.Context, t *kernel.Task) bool {
var failedAt *pids
for curr := p; curr != nil; curr = curr.parent {
curr.mu.Lock()
curr.committed++
if curr.max != pidLimitUnlimited && curr.committed > curr.max {
failedAt = curr
curr.mu.Unlock()
break
}
if curr.committed > curr.peak {
curr.peak = curr.committed
}
curr.mu.Unlock()
}
if failedAt != nil {
for q := p; q != failedAt; q = q.parent {
q.mu.Lock()
q.committed--
q.mu.Unlock()
}
failedAt.mu.Lock()
failedAt.committed--
failedAt.mu.Unlock()
failedAt.localMaxHits.Add(1)
if failedAt.eventsLocalFile != nil {
failedAt.eventsLocalFile.Notify(ctx)
}
for curr := failedAt; curr != nil; curr = curr.parent {
curr.hierMaxHits.Add(1)
if curr.eventsFile != nil {
curr.eventsFile.Notify(ctx)
}
}
return false
}
return true
}
// cancelEnter implements controller.cancelEnter.
func (p *pids) cancelEnter(ctx context.Context, t *kernel.Task) {
for curr := p; curr != nil; curr = curr.parent {
curr.mu.Lock()
if curr.committed > 0 {
curr.committed--
}
curr.mu.Unlock()
}
}
// enter implements controller.enter.
func (p *pids) enter(ctx context.Context, t *kernel.Task) {
// Already eagerly charged inside canEnter
}
// exit implements controller.exit.
func (p *pids) exit(ctx context.Context, t *kernel.Task) {
for curr := p; curr != nil; curr = curr.parent {
curr.mu.Lock()
if curr.committed > 0 {
curr.committed--
}
curr.mu.Unlock()
}
}
// getPidsCtrl returns the pids controller for a cgroup.
func getPidsCtrl(cg *cgroup) *pids {
if cg == nil {
return nil
}
if curSet := cg.closestCtrls.Load(); curSet != nil {
if ctrl, ok := curSet[kernel.Cgroup2PIDs].(*pids); ok {
return ctrl
}
}
return nil
}
// canAttach implements controller.canAttach.
func (p *pids) canAttach(ctx context.Context, actx *attachCtx) bool {
return true
}
// cancelAttach implements controller.cancelAttach.
func (p *pids) cancelAttach(ctx context.Context, actx *attachCtx) {
}
// attach implements controller.attach.
func (p *pids) attach(ctx context.Context, actx *attachCtx) {
netChange := make(map[*pids]int64)
for t := range actx.tasks {
oldCg := actx.oldNodes[t]
oldPidsCtrl := getPidsCtrl(oldCg)
for curr := p; curr != nil; curr = curr.parent {
netChange[curr]++
}
for curr := oldPidsCtrl; curr != nil; curr = curr.parent {
netChange[curr]--
}
}
for curr, change := range netChange {
if change != 0 {
curr.mu.Lock()
curr.committed += change
if curr.committed > curr.peak {
curr.peak = curr.committed
}
curr.mu.Unlock()
}
}
}
// interfaceFiles implements controller.interfaceFiles.
func (p *pids) interfaceFiles() []interfaceFile {
return []interfaceFile{
{
name: "pids.events",
source: &pidsEvents{p: p},
perm: 0444,
isEvent: true,
onEventCreated: func(inode *eventFile) {
p.eventsFile = inode
},
},
{
name: "pids.events.local",
source: &pidsEventsLocal{p: p},
perm: 0444,
isEvent: true,
onEventCreated: func(inode *eventFile) {
p.eventsLocalFile = inode
},
},
{name: "pids.current", source: &pidsCurrent{p: p}, perm: 0444},
{name: "pids.max", source: &pidsMax{p: p}, perm: 0644},
{name: "pids.peak", source: &pidsPeak{p: p}, perm: 0444},
}
}
// interfaceFileNames implements controller.interfaceFileNames.
func (p *pids) interfaceFileNames() []string {
return []string{"pids.events", "pids.events.local", "pids.current", "pids.max", "pids.peak"}
}
// +stateify savable
type pidsEvents struct {
p *pids
}
func (pe *pidsEvents) Generate(ctx context.Context, buf *bytes.Buffer) error {
fmt.Fprintf(buf, "max %d\n", pe.p.hierMaxHits.Load())
return nil
}
// +stateify savable
type pidsEventsLocal struct {
p *pids
}
func (pel *pidsEventsLocal) Generate(ctx context.Context, buf *bytes.Buffer) error {
fmt.Fprintf(buf, "max %d\n", pel.p.localMaxHits.Load())
return nil
}
// +stateify savable
type pidsCurrent struct {
p *pids
}
func (pc *pidsCurrent) Generate(ctx context.Context, buf *bytes.Buffer) error {
pc.p.mu.Lock()
defer pc.p.mu.Unlock()
fmt.Fprintf(buf, "%d\n", pc.p.committed)
return nil
}
// +stateify savable
type pidsMax struct {
p *pids
}
func (pm *pidsMax) Generate(ctx context.Context, buf *bytes.Buffer) error {
pm.p.mu.Lock()
defer pm.p.mu.Unlock()
if pm.p.max == pidLimitUnlimited {
buf.WriteString("max\n")
} else {
fmt.Fprintf(buf, "%d\n", pm.p.max)
}
return nil
}
func (pm *pidsMax) Write(ctx context.Context, fd *vfs.FileDescription, src usermem.IOSequence, offset int64) (int64, error) {
data := make([]byte, src.NumBytes())
if _, err := src.CopyIn(ctx, data); err != nil {
return 0, err
}
str := strings.TrimSpace(string(data))
if str == "max" {
pm.p.mu.Lock()
pm.p.max = pidLimitUnlimited
pm.p.mu.Unlock()
return int64(len(data)), nil
}
val, err := strconv.ParseInt(str, 10, 64)
if err != nil || val < 0 {
return 0, linuxerr.EINVAL
}
pm.p.mu.Lock()
pm.p.max = val
pm.p.mu.Unlock()
return int64(len(data)), nil
}
// +stateify savable
type pidsPeak struct {
p *pids
}
func (pp *pidsPeak) Generate(ctx context.Context, buf *bytes.Buffer) error {
pp.p.mu.Lock()
defer pp.p.mu.Unlock()
fmt.Fprintf(buf, "%d\n", pp.p.peak)
return nil
}
// detach implements controller.detach.
func (p *pids) detach() {
p.detached.Store(true)
}
// isActive implements controller.isActive.
func (p *pids) isActive() bool {
return !p.detached.Load()
}
@@ -0,0 +1,29 @@
// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cgroup2fs
import (
"context"
)
// saveClosestCtrls is called by stateify.
func (c *cgroup) saveClosestCtrls() *ctrlSet {
return c.closestCtrls.Load()
}
// loadClosestCtrls is called by stateify.
func (c *cgroup) loadClosestCtrls(ctx context.Context, set *ctrlSet) {
c.closestCtrls.Store(set)
}
+1 -10
View File
@@ -1,5 +1,5 @@
load("//pkg/sync/locking:locking.bzl", "declare_mutex", "declare_rwmutex")
load("//tools:defs.bzl", "go_library", "go_test")
load("//tools:defs.bzl", "go_library")
load("//tools/go_generics:defs.bzl", "go_template_instance")
package(default_applicable_licenses = ["//:license"])
@@ -35,7 +35,6 @@ go_library(
name = "cgroupfs",
srcs = [
"base.go",
"bitmap.go",
"cgroupfs.go",
"cpu.go",
"cpuacct.go",
@@ -72,11 +71,3 @@ go_library(
"//pkg/usermem",
],
)
go_test(
name = "cgroupfs_test",
size = "small",
srcs = ["bitmap_test.go"],
library = ":cgroupfs",
deps = ["//pkg/bitmap"],
)
+3 -3
View File
@@ -305,7 +305,7 @@ func (fsType FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt
//
// Note: we're guaranteed to have at least one requested controller, since
// no explicit controller name implies all controllers.
vfsfs, err := r.FindHierarchy(name, wantControllers)
vfsfs, err := r.FindHierarchy(ctx, name, wantControllers)
if err != nil {
return nil, nil, err
}
@@ -390,7 +390,7 @@ func (fsType FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt
// Register controllers. The registry may be modified concurrently, so if we
// get an error, we raced with someone else who registered the same
// controllers first.
if err := r.Register(name, fs.kcontrollers, fs); err != nil {
if err := r.Register(ctx, name, fs.kcontrollers, fs); err != nil {
ctx.Infof("cgroupfs.FilesystemType.GetFilesystem: failed to register new hierarchy with controllers %v: %v", wantControllers, err)
rootD.DecRef(ctx)
fs.VFSFilesystem().DecRef(ctx)
@@ -470,7 +470,7 @@ func (fs *filesystem) Release(ctx context.Context) {
if fs.hierarchyID != kernel.InvalidCgroupHierarchyID {
k.ReleaseCgroupHierarchy(fs.hierarchyID)
r.Unregister(fs.hierarchyID)
r.Unregister(ctx, fs.hierarchyID)
}
if fs.root != fs.effectiveRoot {
+4 -4
View File
@@ -95,7 +95,7 @@ type cpusData struct {
func (d *cpusData) Generate(ctx context.Context, buf *bytes.Buffer) error {
d.c.mu.Lock()
defer d.c.mu.Unlock()
fmt.Fprintf(buf, "%s\n", formatBitmap(d.c.cpus))
fmt.Fprintf(buf, "%s\n", bitmap.FormatList(d.c.cpus))
return nil
}
@@ -117,7 +117,7 @@ func (d *cpusData) WriteBackground(ctx context.Context, src usermem.IOSequence)
}
buf = buf[:n]
b, err := parseBitmap(string(buf), d.c.maxCpus)
b, err := bitmap.ParseList(string(buf), d.c.maxCpus)
if err != nil {
log.Warningf("cgroupfs cpuset controller: Failed to parse bitmap: %v", err)
return 0, linuxerr.EINVAL
@@ -143,7 +143,7 @@ type memsData struct {
func (d *memsData) Generate(ctx context.Context, buf *bytes.Buffer) error {
d.c.mu.Lock()
defer d.c.mu.Unlock()
fmt.Fprintf(buf, "%s\n", formatBitmap(d.c.mems))
fmt.Fprintf(buf, "%s\n", bitmap.FormatList(d.c.mems))
return nil
}
@@ -165,7 +165,7 @@ func (d *memsData) WriteBackground(ctx context.Context, src usermem.IOSequence)
}
buf = buf[:n]
b, err := parseBitmap(string(buf), d.c.maxMems)
b, err := bitmap.ParseList(string(buf), d.c.maxMems)
if err != nil {
log.Warningf("cgroupfs cpuset controller: Failed to parse bitmap: %v", err)
return 0, linuxerr.EINVAL
@@ -73,6 +73,15 @@ func (f *DynamicBytesFile) Init(ctx context.Context, creds *auth.Credentials, de
f.data = data
}
// InitWithIDs initializes a dynamic bytes file with the given owner UIDs and GIDs.
func (f *DynamicBytesFile) InitWithIDs(ctx context.Context, uid auth.KUID, gid auth.KGID, devMajor, devMinor uint32, ino uint64, data vfs.DynamicBytesSource, perm linux.FileMode) {
if perm&^linux.PermissionsMask != 0 {
panic(fmt.Sprintf("Only permission mask must be set: %x", perm&linux.PermissionsMask))
}
f.InodeAttrs.InitWithIDs(ctx, uid, gid, devMajor, devMinor, ino, linux.ModeRegular|perm)
f.data = data
}
// Open implements Inode.Open.
func (f *DynamicBytesFile) Open(ctx context.Context, rp *vfs.ResolvingPath, d *Dentry, opts vfs.OpenOptions) (*vfs.FileDescription, error) {
data, err := f.Data(ctx)
+8 -1
View File
@@ -1598,6 +1598,8 @@ var _ dynamicInode = (*taskCgroupData)(nil)
// Generate implements vfs.DynamicBytesSource.Generate.
func (d *taskCgroupData) Generate(ctx context.Context, buf *bytes.Buffer) error {
v2Mounted := kernel.KernelFromContext(ctx).Cgroup2FS().EverMounted()
// When a task is existing on Linux, a task's cgroup set is cleared and
// reset to the initial cgroup set, which is essentially the set of root
// cgroups. Because of this, the /proc/<pid>/cgroup file is always readable
@@ -1607,11 +1609,16 @@ func (d *taskCgroupData) Generate(ctx context.Context, buf *bytes.Buffer) error
// doesn't move them into an initial cgroup set, so partway through task
// exit this file show a task is in no cgroups, which is incorrect. Instead,
// once a task has left its cgroups, we return an error.
if d.task.ExitState() >= kernel.TaskExitInitiated {
if d.task.ExitState() >= kernel.TaskExitInitiated && !v2Mounted {
return linuxerr.ESRCH
}
d.task.GenerateProcTaskCgroup(buf)
if v2Mounted {
if cg2Entry := d.task.GetCgroup2Entry(); cg2Entry != nil {
fmt.Fprintf(buf, "0::%s\n", cg2Entry.Path)
}
}
return nil
}
+1
View File
@@ -19,6 +19,7 @@ go_library(
"//pkg/fspath",
"//pkg/hostarch",
"//pkg/memutil",
"//pkg/sentry/fsimpl/cgroup2fs",
"//pkg/sentry/fsimpl/tmpfs",
"//pkg/sentry/kernel",
"//pkg/sentry/kernel/auth",
+4
View File
@@ -42,6 +42,9 @@ import (
// Platforms are pluggable.
_ "gvisor.dev/gvisor/pkg/sentry/platform/kvm"
_ "gvisor.dev/gvisor/pkg/sentry/platform/ptrace"
// Cgroup2fs is required by Kernel.Init.
cgroup2fs "gvisor.dev/gvisor/pkg/sentry/fsimpl/cgroup2fs"
)
var (
@@ -106,6 +109,7 @@ func Boot() (*kernel.Kernel, error) {
RootUTSNamespace: kernel.NewUTSNamespace("hostname", "domain", creds.UserNamespace),
RootIPCNamespace: kernel.NewIPCNamespace(creds.UserNamespace),
RootPIDNamespace: kernel.NewRootPIDNamespace(creds.UserNamespace),
Cgroup2FSInit: cgroup2fs.NewFilesystem,
}); err != nil {
return nil, fmt.Errorf("initializing kernel: %v", err)
}
+9
View File
@@ -87,6 +87,13 @@ declare_mutex(
prefix = "cgroup",
)
declare_mutex(
name = "cgroup_v2_mutex",
out = "cgroup_v2_mutex.go",
package = "kernel",
prefix = "cgroup2",
)
declare_mutex(
name = "fd_table_mutex",
out = "fd_table_mutex.go",
@@ -251,8 +258,10 @@ go_library(
"atomicptr_bucket_unsafe.go",
"atomicptr_descriptor_unsafe.go",
"cgroup.go",
"cgroup2.go",
"cgroup_mounts_mutex.go",
"cgroup_mutex.go",
"cgroup_v2_mutex.go",
"context.go",
"fd_table.go",
"fd_table_mutex.go",
+78 -10
View File
@@ -275,6 +275,9 @@ type cgroupFS interface {
//
// +stateify savable
type CgroupRegistry struct {
// v2fs is the cgroup v2 filesystem singleton.
v2fs *vfs.Filesystem
// lastHierarchyID is the id of the last allocated cgroup hierarchy. Valid
// ids are from 1 to math.MaxUint32.
//
@@ -312,6 +315,43 @@ type CgroupRegistry struct {
cgroups map[uint32]CgroupImpl
}
// IsControllerBound returns true if the controller type is bound to a v1 hierarchy.
func (r *CgroupRegistry) IsControllerBound(ctype CgroupControllerType) bool {
r.mu.Lock()
defer r.mu.Unlock()
_, ok := r.controllers[ctype]
return ok
}
// IsCgroup2CtrlBound returns true if the cgroup v2 controller is bound to a v1 hierarchy.
func (r *CgroupRegistry) IsCgroup2CtrlBound(cType Cgroup2Ctrl) bool {
var v1Type CgroupControllerType
switch cType {
case Cgroup2CPU:
v1Type = CgroupControllerCPU
case Cgroup2Memory:
v1Type = CgroupControllerMemory
case Cgroup2PIDs:
v1Type = CgroupControllerPIDs
default:
return false
}
return r.IsControllerBound(v1Type)
}
func toCgroup2Ctrl(cType CgroupControllerType) (Cgroup2Ctrl, bool) {
switch cType {
case CgroupControllerCPU:
return Cgroup2CPU, true
case CgroupControllerMemory:
return Cgroup2Memory, true
case CgroupControllerPIDs:
return Cgroup2PIDs, true
default:
return 0, false
}
}
func newCgroupRegistry() *CgroupRegistry {
return &CgroupRegistry{
controllers: make(map[CgroupControllerType]CgroupController),
@@ -333,7 +373,10 @@ func (r *CgroupRegistry) nextHierarchyID() (uint32, error) {
// controllers named in ctypes, and optionally the name specified in name if it
// isn't empty. If no such FS is found, FindHierarchy return nil. FindHierarchy
// takes a reference on the returned FS, which is transferred to the caller.
func (r *CgroupRegistry) FindHierarchy(name string, ctypes []CgroupControllerType) (*vfs.Filesystem, error) {
func (r *CgroupRegistry) FindHierarchy(ctx context.Context, name string, ctypes []CgroupControllerType) (*vfs.Filesystem, error) {
c2fs := r.v2fs.Impl().(Cgroup2FS)
c2fs.LockTree()
defer c2fs.UnlockTree()
r.mu.Lock()
defer r.mu.Unlock()
@@ -348,7 +391,7 @@ func (r *CgroupRegistry) FindHierarchy(name string, ctypes []CgroupControllerTyp
if h.match(ctypes) {
if !h.fs.TryIncRef() {
// May be racing with filesystem destruction, see below.
r.unregisterLocked(h.id)
r.unregisterLocked(ctx, h.id)
return nil, nil
}
return h.fs, nil
@@ -377,7 +420,7 @@ func (r *CgroupRegistry) FindHierarchy(name string, ctypes []CgroupControllerTyp
// uniqueness of controllers enforced by Register, drop the
// dying hierarchy now. The eventual unregister by the FS
// teardown will become a no-op.
r.unregisterLocked(h.id)
r.unregisterLocked(ctx, h.id)
return nil, nil
}
return h.fs, nil
@@ -397,7 +440,7 @@ func (r *CgroupRegistry) FindCgroup(ctx context.Context, ctype CgroupControllerT
return Cgroup{}, fmt.Errorf("path must be absolute")
}
k := KernelFromContext(ctx)
vfsfs, err := r.FindHierarchy("", []CgroupControllerType{ctype})
vfsfs, err := r.FindHierarchy(ctx, "", []CgroupControllerType{ctype})
if err != nil {
return Cgroup{}, err
}
@@ -420,7 +463,10 @@ func (r *CgroupRegistry) FindCgroup(ctx context.Context, ctype CgroupControllerT
// hierarchy. If any controller is already registered, the function returns an
// error without modifying the registry. Register sets the hierarchy ID for the
// filesystem on success.
func (r *CgroupRegistry) Register(name string, cs []CgroupController, fs cgroupFS) error {
func (r *CgroupRegistry) Register(ctx context.Context, name string, cs []CgroupController, fs cgroupFS) error {
c2fs := r.v2fs.Impl().(Cgroup2FS)
c2fs.LockTree()
defer c2fs.UnlockTree()
r.mu.Lock()
defer r.mu.Unlock()
@@ -429,7 +475,8 @@ func (r *CgroupRegistry) Register(name string, cs []CgroupController, fs cgroupF
}
for _, c := range cs {
if _, ok := r.controllers[c.Type()]; ok {
n := c.Type()
if _, ok := r.controllers[n]; ok {
return fmt.Errorf("controllers may only be mounted on a single hierarchy")
}
}
@@ -443,6 +490,20 @@ func (r *CgroupRegistry) Register(name string, cs []CgroupController, fs cgroupF
return err
}
for i, c := range cs {
if c2Type, ok := toCgroup2Ctrl(c.Type()); ok {
if err := c2fs.StealControllerLocked(ctx, c2Type); err != nil {
// Rollback steals for earlier controllers
for _, rollbackC := range cs[:i] {
if rollbackC2Type, ok := toCgroup2Ctrl(rollbackC.Type()); ok {
c2fs.ReturnControllerLocked(ctx, rollbackC2Type)
}
}
return err
}
}
}
// Must not fail below here, once we publish the hierarchy ID.
fs.InitializeHierarchyID(hid)
@@ -467,18 +528,25 @@ func (r *CgroupRegistry) Register(name string, cs []CgroupController, fs cgroupF
// Unregister removes a previously registered hierarchy from the registry. If no
// such hierarchy is registered, Unregister is a no-op.
func (r *CgroupRegistry) Unregister(hid uint32) {
func (r *CgroupRegistry) Unregister(ctx context.Context, hid uint32) {
c2fs := r.v2fs.Impl().(Cgroup2FS)
c2fs.LockTree()
defer c2fs.UnlockTree()
r.mu.Lock()
r.unregisterLocked(hid)
r.unregisterLocked(ctx, hid)
r.mu.Unlock()
}
// Precondition: Caller must hold r.mu.
// Precondition: Caller must hold r.mu and must have called c2fs.LockTree().
// +checklocks:r.mu
func (r *CgroupRegistry) unregisterLocked(hid uint32) {
func (r *CgroupRegistry) unregisterLocked(ctx context.Context, hid uint32) {
if h, ok := r.hierarchies[hid]; ok {
for name := range h.controllers {
delete(r.controllers, name)
c2fs := r.v2fs.Impl().(Cgroup2FS)
if c2Type, isV2Mapped := toCgroup2Ctrl(name); isV2Mapped {
c2fs.ReturnControllerLocked(ctx, c2Type)
}
}
delete(r.hierarchies, hid)
}
+149
View File
@@ -0,0 +1,149 @@
// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package kernel
import (
"math"
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/errors/linuxerr"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/kernfs"
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
)
// Cgroup2Ctrl represents a supported cgroup v2 controller.
type Cgroup2Ctrl int
const (
// Cgroup2CPU represents the CPU controller.
Cgroup2CPU Cgroup2Ctrl = iota
// Cgroup2Memory represents the memory controller.
Cgroup2Memory
// Cgroup2PIDs represents the pids controller.
Cgroup2PIDs
// Cgroup2CPUSet represents the cpuset controller.
Cgroup2CPUSet
// Cgroup2NumControllers is the total number of cgroup v2 controllers currently supported.
Cgroup2NumControllers
)
// Cgroup2 is an interface representing a cgroup v2 node.
type Cgroup2 interface {
// Path returns the root-relative path of the cgroup.
// Used by procfs.
Path() string
// The following are used by clone() and CreateProcess().
// CanEnter checks if a task can enter the cgroup.
CanEnter(ctx context.Context, t *Task) (func(), func(), error)
// Exit removes a task from a cgroup.
// Called by exiting tasks.
Exit(ctx context.Context, t *Task)
// Called by clone() to check permissions for CLONE_CGROUP_INTO.
CanCloneInto(ctx context.Context, creds *auth.Credentials) error
// KillSeq returns the kill sequence number of the cgroup.
// It helps prevent fork()s racing with cgroup.kill.
KillSeq() uint64
}
// Cgroup2FS is the public interface to cgroup2fs.
type Cgroup2FS interface {
// EverMounted returns true if the filesystem has ever been mounted.
EverMounted() bool
// RootCgroup returns the root cgroup v2 node.
RootCgroup() Cgroup2
// LockTree locks the cgroup2fs tree for writing.
LockTree()
// UnlockTree is the inverse of LockTree.
UnlockTree()
// RLockTree locks the cgroup2fs tree for reading.
RLockTree()
// RUnlockTree is the inverse of RLockTree.
RUnlockTree()
// StealControllerLocked transfers ownership of the controller
// away from the v2 hierarchy if a v1 hierarchy mounts it.
StealControllerLocked(ctx context.Context, cType Cgroup2Ctrl) error
// ReturnControllerLocked returns ownership of the controller
// to the v2 hierarchy when a v1 hierarchy unmounts it.
ReturnControllerLocked(ctx context.Context, cType Cgroup2Ctrl)
}
// Cgroup2FS returns the cgroup v2 filesystem singleton.
func (k *Kernel) Cgroup2FS() Cgroup2FS {
return k.cgroupRegistry.v2fs.Impl().(Cgroup2FS)
}
// SetCgroup2 sets the cgroup v2 node for the task t.
func (t *Task) SetCgroup2(node Cgroup2) {
t.cgroup2Mu.Lock()
defer t.cgroup2Mu.Unlock()
t.cgroup2 = node
}
// Cgroup2 returns t's cgroup v2 node.
func (t *Task) Cgroup2() Cgroup2 {
t.cgroup2Mu.Lock()
defer t.cgroup2Mu.Unlock()
return t.cgroup2
}
// getCgroup2NodeFromFD returns the cgroup v2 node associated with the cgroupFD.
// If the cgroupFD is not valid, returns an error.
func (t *Task) getCgroup2NodeFromFD(cgroupFD uint64) (Cgroup2, error) {
if cgroupFD > math.MaxInt32 {
return nil, linuxerr.EINVAL
}
cgroupFile := t.GetFile(int32(cgroupFD))
if cgroupFile == nil {
return nil, linuxerr.EBADF
}
defer cgroupFile.DecRef(t)
d, ok := cgroupFile.VirtualDentry().Dentry().Impl().(*kernfs.Dentry)
if !ok {
return nil, linuxerr.EBADF
}
if cgroupFile.VirtualDentry().Dentry().IsDead() {
return nil, linuxerr.ENOENT
}
c, ok := d.Inode().(Cgroup2)
if !ok {
return nil, linuxerr.EBADF
}
return c, nil
}
// GetCgroup2Entry returns the cgroup v2 entry if cgroup v2 is mounted.
func (t *Task) GetCgroup2Entry() *TaskCgroupEntry {
var path string
if c := t.Cgroup2(); c != nil {
path = c.Path()
}
if path == "" {
path = "/"
}
return &TaskCgroupEntry{
HierarchyID: 0,
Controllers: "",
Path: path,
}
}
+17
View File
@@ -500,6 +500,9 @@ type InitKernelArgs struct {
// used by processes. If it is zero, the limit will be set to
// unlimited.
MaxFDLimit int32
// Cgroup2FSInit initializes the cgroup2fs filesystem singleton.
Cgroup2FSInit func(ctx context.Context, k *Kernel, vfsObj *vfs.VirtualFilesystem) (*vfs.Filesystem, error)
}
// Init initialize the Kernel with no tasks.
@@ -641,6 +644,16 @@ func (k *Kernel) Init(args InitKernelArgs) error {
k.sockets = make(map[*vfs.FileDescription]*SocketRecord)
k.cgroupRegistry = newCgroupRegistry()
if args.Cgroup2FSInit == nil {
return fmt.Errorf("cgroup2fs initializer is required")
}
cfs, err := args.Cgroup2FSInit(ctx, k, &k.vfs)
if err != nil {
return fmt.Errorf("failed to initialize cgroup2fs: %v", err)
}
k.cgroupRegistry.v2fs = cfs
k.MaxKeySetSize = atomicbitops.FromInt32(auth.MaxSetSize)
return nil
}
@@ -2210,6 +2223,10 @@ func (k *Kernel) Release() {
k.shmMount.DecRef(ctx)
k.socketMount.DecRef(ctx)
k.vfs.Release(ctx)
if k.cgroupRegistry != nil {
k.cgroupRegistry.v2fs.DecRef(ctx)
k.cgroupRegistry.v2fs = nil
}
k.timekeeper.Destroy()
k.vdso.Release(ctx)
k.RootNetworkNamespace().DecRef(ctx)
+8
View File
@@ -634,6 +634,14 @@ type Task struct {
// +checklocks:mu
cgroups map[Cgroup]struct{}
// cgroup2 is the cgroup v2 node this task belongs to.
// Protected by cgroup2Mu.
// +checklocks:cgroup2Mu
cgroup2 Cgroup2
// cgroup2Mu protects the cgroup2 field.
cgroup2Mu cgroup2Mutex `state:"nosave"`
// memCgID is the memory cgroup id.
memCgID atomicbitops.Uint32
+2 -2
View File
@@ -24,7 +24,7 @@ import (
"gvisor.dev/gvisor/pkg/log"
)
// EnterInitialCgroups moves t into an initial set of cgroups.
// EnterInitialV1Cgroups moves t into an initial set of cgroups.
// If initCgroups is not nil, the new task will be placed in the specified cgroups.
// Otherwise, if parent is not nil, the new task will be placed in the parent's cgroups.
// If neither is specified, the new task will be in the root cgroups.
@@ -32,7 +32,7 @@ import (
// This is analogous to Linux's kernel/cgroup/cgroup.c:cgroup_css_set_fork().
//
// Precondition: t isn't in any cgroups yet, t.cgroups is empty.
func (t *Task) EnterInitialCgroups(parent *Task, initCgroups map[Cgroup]struct{}) {
func (t *Task) EnterInitialV1Cgroups(parent *Task, initCgroups map[Cgroup]struct{}) {
var inherit map[Cgroup]struct{}
if initCgroups != nil {
inherit = initCgroups
+3 -3
View File
@@ -31,15 +31,13 @@ import (
)
// SupportedCloneFlags is the bitwise OR of all the supported flags for clone.
// TODO(b/290826530): Implement CLONE_INTO_CGROUP when cgroups v2 is
// implemented.
const SupportedCloneFlags = linux.CLONE_VM | linux.CLONE_FS | linux.CLONE_FILES | linux.CLONE_SYSVSEM |
linux.CLONE_THREAD | linux.CLONE_SIGHAND | linux.CLONE_NEWPID |
linux.CLONE_CHILD_CLEARTID | linux.CLONE_CHILD_SETTID | linux.CLONE_PARENT |
linux.CLONE_PARENT_SETTID | linux.CLONE_SETTLS | linux.CLONE_NEWUSER | linux.CLONE_NEWUTS |
linux.CLONE_NEWIPC | linux.CLONE_NEWNET | linux.CLONE_PTRACE | linux.CLONE_UNTRACED |
linux.CLONE_IO | linux.CLONE_VFORK | linux.CLONE_DETACHED | linux.CLONE_NEWNS |
linux.CLONE_PIDFD | linux.CLONE_CLEAR_SIGHAND
linux.CLONE_PIDFD | linux.CLONE_CLEAR_SIGHAND | linux.CLONE_INTO_CGROUP
func failCloneAfterTaskCreation(nt *Task) {
// nt has been visible to the rest of the system since NewTask, so
@@ -318,6 +316,8 @@ func (t *Task) Clone(args *linux.CloneArgs) (ThreadID, *SyscallControl, error) {
SessionKeyring: sessionKeyring,
Personality: t.personality.Load(),
Origin: t.Origin,
cgroupFD: args.Cgroup,
cloneIntoCgroup: args.Flags&linux.CLONE_INTO_CGROUP != 0,
}
if args.Flags&(linux.CLONE_THREAD|linux.CLONE_PARENT) == 0 {
cfg.Parent = t
+1
View File
@@ -303,6 +303,7 @@ func (*runExitMain) execute(t *Task) taskRunState {
// Detach task from all cgroups. This must happen before potentially the
// last ref to the cgroupfs mount is dropped below.
t.LeaveCgroups()
t.Cgroup2().Exit(t, t)
t.mu.Lock()
mntns := t.mountNamespace
+102 -15
View File
@@ -129,6 +129,12 @@ type TaskConfig struct {
// Used by clone3 to support CLONE_PIDFD.
pidFDMode pidFDMode
pidFDAddr hostarch.Addr
// cgroupFD is the CLONE_INTO_CGROUP file descriptor. Valid only if
// cloneIntoCgroup is true.
cgroupFD uint64
// Set only if CLONE_INTO_CGROUP is passed to clone.
cloneIntoCgroup bool
}
// NewTask creates a new task defined by cfg.
@@ -177,6 +183,38 @@ func (ts *TaskSet) newTask(ctx context.Context, cfg *TaskConfig) (*Task, int32,
srcT := TaskFromContext(ctx)
tg := cfg.ThreadGroup
image := cfg.TaskImage
var cu cleanup.Cleanup
defer cu.Clean()
var cgroup2 Cgroup2 // The destination cgroup2 node.
var cachedKillSeq uint64 // To avoid racing with cgroup.kill.
if cfg.cloneIntoCgroup {
c, err := srcT.getCgroup2NodeFromFD(cfg.cgroupFD)
if err != nil {
return nil, -1, err
}
// We must lock the cgroup2 tree down to avoid racing with another
// thread that might destroy the destination cgroup. Note that we
// only lock this after we have extracted the destination cgroup to
// respect the prevailing lock order between the kernfs's filesystem
// mutex and the cgroup2 tree mutex.
srcT.k.Cgroup2FS().RLockTree()
cu.Add(func() {
// Note how the unlock is not deferred but added to cu.
// This means in the happy path, when cu is Release'd, we
// must unlock further down in this function, just after
// committing the entry of the new task into the cgroup.
srcT.k.Cgroup2FS().RUnlockTree()
})
if err := c.CanCloneInto(ctx, srcT.Credentials()); err != nil {
return nil, -1, err
}
cgroup2 = c
cachedKillSeq = cgroup2.KillSeq()
}
t := &Task{
taskNode: taskNode{
tg: tg,
@@ -209,6 +247,7 @@ func (ts *TaskSet) newTask(ctx context.Context, cfg *TaskConfig) (*Task, int32,
Origin: cfg.Origin,
onDestroyAction: make(map[TaskDestroyAction]struct{}),
noNewPrivs: cfg.NoNewPrivs,
cgroup2: cgroup2,
}
t.netns = cfg.NetworkNamespace
t.creds.Store(cfg.Credentials)
@@ -217,11 +256,7 @@ func (ts *TaskSet) newTask(ctx context.Context, cfg *TaskConfig) (*Task, int32,
// We don't construct t.blockingTimer until Task.run(); see that function
// for justification.
var cu cleanup.Cleanup
defer cu.Clean()
pidFD := int32(-1)
if cfg.pidFDMode != dontMakePIDFD {
isThread := cfg.pidFDMode == makeThreadedPIDFD
t.pid = &pid{}
@@ -249,18 +284,16 @@ func (ts *TaskSet) newTask(ctx context.Context, cfg *TaskConfig) (*Task, int32,
}
}
var (
cg Cgroup
charged, committed bool
)
// Reserve cgroup PIDs controller charge. This is either committed when the
// new task enters the cgroup below, or rolled back on failure.
//
// We may also get here from a non-task context (for example, when
// creating the init task, or from the exec control command). In these cases
// we skip charging the pids controller, as non-userspace task creation
// bypasses pid limits.
var (
cg Cgroup
charged, committed bool
)
if srcT != nil {
var err error
if charged, cg, err = srcT.ChargeFor(t, CgroupControllerPIDs, CgroupResourcePID, 1); err != nil {
@@ -295,8 +328,44 @@ func (ts *TaskSet) newTask(ctx context.Context, cfg *TaskConfig) (*Task, int32,
// the system atomically.
ts.mu.Lock()
defer ts.mu.Unlock()
// For the standard, non-cloneIntoCgroup fork case, we have to do this
// after acquiring the TaskSet mutex to guard against a racing cgroup.procs
// write that might migrate the parent to a different cgroup. Note that
// task migration locks the taskset mutex for reading, and hence is mutually
// excluded from clone here.
if !cfg.cloneIntoCgroup {
if srcT != nil {
cgroup2 = srcT.Cgroup2()
} else {
// Direct exec into the sandbox.
cgroup2 = cfg.Kernel.Cgroup2FS().RootCgroup()
}
t.cgroup2 = cgroup2 // +checklocksignore: task not visible to anything yet
// This new task should not escape from a racing cgroup.kill.
// If here we cache the pre-kill seq no, we will either:
// - Get killed naturally when the kill finds us in the cgroup task list.
// - We kill ourselves when we find an augmented seq no later in this function.
// If instead we see the post-kill seq no, then we will find the parent
// to have been killed() below.
cachedKillSeq = cgroup2.KillSeq()
if srcT != nil && srcT.killed() {
return nil, -1, linuxerr.EINTR
}
}
abort, commitCgroupV2, err := cgroup2.CanEnter(ctx, t)
if err != nil {
return nil, -1, err
}
cu.Add(abort)
tg.signalHandlers.mu.Lock()
defer tg.signalHandlers.mu.Unlock()
// N.B. unlock is not deferred for the happy path because calling
// commitCgroupV2() with the signals mutex held would violate lock order.
cu.Add(func() { tg.signalHandlers.mu.Unlock() })
if tg.exiting || tg.execing != nil {
// If the caller is in the same thread group, then what we return
// doesn't matter too much since the caller will exit before it returns
@@ -310,11 +379,14 @@ func (ts *TaskSet) newTask(ctx context.Context, cfg *TaskConfig) (*Task, int32,
// explanatory.
return nil, -1, fmt.Errorf("task creation disabled after Kernel.WaitExited() may have returned")
}
if err := ts.assignTIDsLocked(t); err != nil {
return nil, -1, err
}
// Below this point, newTask is expected not to fail (there is no rollback
// of assignTIDsLocked or any of the following).
cu.Release()
ts.liveTasks++
@@ -334,7 +406,7 @@ func (ts *TaskSet) newTask(ctx context.Context, cfg *TaskConfig) (*Task, int32,
// specified cgroups. Otherwise, if srcT is not nil, the new task will
// be placed in the srcT's cgroups. If neither is specified, the new task
// will be in the root cgroups.
t.EnterInitialCgroups(srcT, cfg.InitialCgroups)
t.EnterInitialV1Cgroups(srcT, cfg.InitialCgroups)
committed = true
if isFirstTask = tg.leader == nil; isFirstTask {
@@ -364,8 +436,6 @@ func (ts *TaskSet) newTask(ctx context.Context, cfg *TaskConfig) (*Task, int32,
t.stopCount = atomicbitops.FromInt32(ts.stopCount)
t.mu.Lock()
defer t.mu.Unlock()
t.cpu = atomicbitops.FromInt32(assignCPU(t.allowedCPUMask, ts.Root.tids[t]))
t.startTime = t.k.RealtimeClock().Now()
@@ -373,8 +443,25 @@ func (ts *TaskSet) newTask(ctx context.Context, cfg *TaskConfig) (*Task, int32,
// As a final step, initialize the platform context. This may require
// other pieces to be initialized as the task is used the context.
t.p = cfg.Kernel.Platform.NewContext(t.AsyncContext())
t.mu.Unlock()
tg.signalHandlers.mu.Unlock()
if commitCgroupV2 != nil {
// We can call this only after releasing the signals mutex due to lock
// order reasons.
commitCgroupV2()
}
if cfg.cloneIntoCgroup {
// Unlock the cgroup v2 tree mutex now that the task is in the desired cgroup.
t.k.Cgroup2FS().RUnlockTree()
}
// We raced with a cgroup.kill write.
// Kills occurring after this check will find t in the cgroup's task
// list and will kill the task naturally.
if t.Cgroup2().KillSeq() != cachedKillSeq {
t.SendSignal(SignalInfoPriv(linux.SIGKILL))
}
cu.Release()
return t, pidFD, nil
}
+39
View File
@@ -322,6 +322,31 @@ func (ns *PIDNamespace) IDOfTask(t *Task) ThreadID {
return id
}
// IDsOfTasks returns the deduplicated TIDs assigned to the given tasks in PID namespace ns.
// If a task is not visible in that namespace or is exiting, its TID is not included in the
// returned map. If threadGroupIDs is true, the returned thread IDs correspond to the
// thread groups of the given tasks.
func (ns *PIDNamespace) IDsOfTasks(tasks []*Task, threadGroupIDs bool) map[ThreadID]struct{} {
ns.owner.mu.RLock()
defer ns.owner.mu.RUnlock()
idMap := make(map[ThreadID]struct{})
for _, task := range tasks {
if task.exitStateLocked() < TaskExitInitiated {
var id ThreadID
if threadGroupIDs {
id = ns.tgids[task.tg]
} else {
id = ns.tids[task]
}
if id != 0 {
idMap[id] = struct{}{}
}
}
}
return idMap
}
// IDOfThreadGroup returns the TID assigned to tg's leader in PID namespace ns.
// If the task is not visible in that namespace, IDOfThreadGroup returns 0.
func (ns *PIDNamespace) IDOfThreadGroup(tg *ThreadGroup) ThreadID {
@@ -541,6 +566,13 @@ func (tg *ThreadGroup) MemberIDs(pidns *PIDNamespace) []ThreadID {
func (tg *ThreadGroup) ForEachTask(f func(t *Task) bool) {
tg.pidns.owner.mu.RLock()
defer tg.pidns.owner.mu.RUnlock()
tg.ForEachTaskLocked(f)
}
// ForEachTaskLocked invokes f() on each task in tg.
//
// Preconditions: The TaskSet mutex must be locked (for reading or writing).
func (tg *ThreadGroup) ForEachTaskLocked(f func(t *Task) bool) {
for t := tg.tasks.Front(); t != nil; t = t.Next() {
if !f(t) {
break
@@ -548,6 +580,13 @@ func (tg *ThreadGroup) ForEachTask(f func(t *Task) bool) {
}
}
// WithTaskSetRLock executes the given function f while holding the TaskSet's read lock.
func (tg *ThreadGroup) WithTaskSetRLock(f func()) {
tg.pidns.owner.mu.RLock()
defer tg.pidns.owner.mu.RUnlock()
f()
}
// ID returns tg's leader's thread ID in its own PID namespace.
// If tg's leader is dead, ID returns 0.
func (tg *ThreadGroup) ID() ThreadID {
+10 -4
View File
@@ -34,9 +34,10 @@ import (
// configured by the user at /proc/sys/fs/mount-max, but the default is
// 100,000. We set the gVisor limit to 10,000.
const (
MountMax = 10000
nsfsName = "nsfs"
cgroupFsName = "cgroup"
MountMax = 10000
nsfsName = "nsfs"
cgroupFsName = "cgroup"
cgroup2FsName = "cgroup2"
)
// A Mount is a replacement of a Dentry (Mount.key.point) from one Filesystem
@@ -404,6 +405,11 @@ func (vfs *VirtualFilesystem) ConnectMountAt(ctx context.Context, creds *auth.Cr
vfs.delayDecRef(mp)
return linuxerr.EINVAL
}
if mp.dentry == mnt.root {
mp.dentry.mu.Unlock()
vfs.delayDecRef(mp)
return linuxerr.EBUSY
}
return vfs.attachTreeLocked(ctx, mnt, mp, false)
}
@@ -733,7 +739,7 @@ func (vfs *VirtualFilesystem) BindAt(ctx context.Context, creds *auth.Credential
defer cleanup.Clean()
// Namespace mounts can be binded to other mount points.
fsName := sourceVd.mount.Filesystem().FilesystemType().Name()
if !vfs.validInMountNS(ctx, sourceVd.mount) && fsName != nsfsName && fsName != cgroupFsName {
if !vfs.validInMountNS(ctx, sourceVd.mount) && fsName != nsfsName && fsName != cgroupFsName && fsName != cgroup2FsName {
return linuxerr.EINVAL
}
if !vfs.validInMountNS(ctx, mp.mount) {
+1
View File
@@ -69,6 +69,7 @@ go_library(
"//pkg/sentry/devices/tundev",
"//pkg/sentry/fdimport",
"//pkg/sentry/fscheckpoint",
"//pkg/sentry/fsimpl/cgroup2fs",
"//pkg/sentry/fsimpl/cgroupfs",
"//pkg/sentry/fsimpl/dev",
"//pkg/sentry/fsimpl/devpts",
+3 -1
View File
@@ -43,6 +43,7 @@ import (
"gvisor.dev/gvisor/pkg/sentry/devices/nvproxy"
"gvisor.dev/gvisor/pkg/sentry/devices/nvproxy/nvconf"
"gvisor.dev/gvisor/pkg/sentry/fdimport"
cgroup2fs "gvisor.dev/gvisor/pkg/sentry/fsimpl/cgroup2fs"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/host"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/tmpfs"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/user"
@@ -734,6 +735,7 @@ func New(args Args) (*Loader, error) {
RootIPCNamespace: kernel.NewIPCNamespace(creds.UserNamespace),
RootPIDNamespace: kernel.NewRootPIDNamespace(creds.UserNamespace),
MaxFDLimit: maxFDLimit,
Cgroup2FSInit: cgroup2fs.NewFilesystem,
}); err != nil {
return nil, fmt.Errorf("initializing kernel: %w", err)
}
@@ -1383,7 +1385,7 @@ func (l *Loader) createContainerProcess(info *containerInfo) (*kernel.ThreadGrou
}
l.startGoferMonitor(info)
if l.root.cid == l.sandboxID {
if l.root.cid == l.sandboxID && !l.root.conf.MountCgroupV2 {
// Mounts cgroups for all the controllers.
if err := l.mountCgroupMounts(info.conf, info.procArgs.Credentials); err != nil {
return nil, nil, err
+42 -11
View File
@@ -44,6 +44,7 @@ import (
"gvisor.dev/gvisor/pkg/sentry/devices/tpuproxy/vfio"
"gvisor.dev/gvisor/pkg/sentry/devices/ttydev"
"gvisor.dev/gvisor/pkg/sentry/devices/tundev"
cgroup2fs "gvisor.dev/gvisor/pkg/sentry/fsimpl/cgroup2fs"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/cgroupfs"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/dev"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/devpts"
@@ -124,6 +125,12 @@ func registerFilesystems(k *kernel.Kernel, info *containerInfo) error {
AllowUserMount: true,
AllowUserList: true,
})
if info.conf.MountCgroupV2 {
vfsObj.MustRegisterFilesystemType(cgroup2fs.Name, &cgroup2fs.FilesystemType{}, &vfs.RegisterFilesystemTypeOptions{
AllowUserMount: true,
AllowUserList: true,
})
}
vfsObj.MustRegisterFilesystemType(devpts.Name, &devpts.FilesystemType{}, &vfs.RegisterFilesystemTypeOptions{
AllowUserList: true,
AllowUserMount: true,
@@ -207,7 +214,7 @@ func setupContainerVFS(ctx context.Context, info *containerInfo, mntr *container
// If cgroups are mounted, then only check for the cgroup mounts per
// container. Otherwise the root cgroups will be enabled.
if mntr.cgroupsMounted {
if mntr.cgroupsMounted && !info.conf.MountCgroupV2 {
cgroupRegistry := mntr.l.k.CgroupRegistry()
for _, ctrl := range kernel.CgroupCtrls {
cg, err := cgroupRegistry.FindCgroup(ctx, ctrl, "/"+mntr.containerID)
@@ -260,11 +267,20 @@ func compileMounts(spec *specs.Spec, conf *config.Config, containerID string) []
// Mount all submounts from the spec.
for _, m := range spec.Mounts {
// Mount all the cgroup controllers when "/sys/fs/cgroup" mount
// is present. If any other cgroup controller mounts are there,
// it will be a no-op, drop them.
if m.Type == cgroupfs.Name && cgroupsMounted {
continue
if conf.MountCgroupV2 {
// Under this flag, we only want a single unified mount at
// /sys/fs/cgroup. Skip any legacy v1 controller sub-mounts
// (e.g., /sys/fs/cgroup/cpu) requested by the OCI spec.
if (m.Type == cgroupfs.Name || m.Type == cgroup2fs.Name) && strings.HasPrefix(filepath.Clean(m.Destination), "/sys/fs/cgroup/") {
continue
}
} else {
// Mount all the cgroup controllers when "/sys/fs/cgroup" mount
// is present. If any other cgroup controller mounts are there,
// it will be a no-op, drop them.
if m.Type == cgroupfs.Name && cgroupsMounted {
continue
}
}
switch filepath.Clean(m.Destination) {
@@ -790,10 +806,25 @@ func (c *containerMounter) mountSubmounts(ctx context.Context, spec *specs.Spec,
if err != nil {
return fmt.Errorf("mount shared mount %q to %q: %v", submount.hint.Name, submount.mount.Destination, err)
}
} else if submount.mount.Type == cgroupfs.Name {
// Mount all the cgroups controllers.
if err := c.mountCgroupSubmounts(ctx, spec, conf, mns, creds, submount); err != nil {
return fmt.Errorf("mount cgroup %q: %w", submount.mount.Destination, err)
} else if submount.mount.Type == cgroupfs.Name || submount.mount.Type == cgroup2fs.Name {
if conf.MountCgroupV2 {
// There is no "type: cgroup2" defined in the OCI spec.
// So, when the runsc flag MountCgroupV2 is set, we honor the
// OCI request for "type: cgroupfs" by a v2 mount.
if submount.mount.Type == cgroupfs.Name {
submount.mount.Type = cgroup2fs.Name
}
// TODO (b/524360347): Bind a per-container cgroup2 mount rooted at /sys/fs/cgroup/$cid,
// much like we do for cgroup v1 in mountCgroupSubmounts.
mnt, err = c.mountSubmount(ctx, spec, conf, mns, creds, submount)
if err != nil {
return fmt.Errorf("mount cgroup2 %q: %w", submount.mount.Destination, err)
}
} else {
// Mount all the cgroups controllers.
if err := c.mountCgroupSubmounts(ctx, spec, conf, mns, creds, submount); err != nil {
return fmt.Errorf("mount cgroup %q: %w", submount.mount.Destination, err)
}
}
} else {
mnt, err = c.mountSubmount(ctx, spec, conf, mns, creds, submount)
@@ -963,7 +994,7 @@ func getMountNameAndOptions(spec *specs.Spec, conf *config.Config, m *mountInfo,
// Find filesystem name and FS specific data field.
switch m.mount.Type {
case devpts.Name, dev.Name:
case devpts.Name, dev.Name, cgroup2fs.Name:
// Nothing to do.
case Nonefs:
+3
View File
@@ -304,6 +304,9 @@ type Config struct {
// Don't configure cgroups.
IgnoreCgroups bool `flag:"ignore-cgroups"`
// Mount cgroup v2 instead of cgroup v1 inside the sandbox.
MountCgroupV2 bool `flag:"mount-cgroup-v2"`
// Use systemd to configure cgroups.
SystemdCgroup bool `flag:"systemd-cgroup"`
+1
View File
@@ -143,6 +143,7 @@ func RegisterFlags(flagSet *flag.FlagSet) {
flagSet.String("override-procs", "", "comma-separated list of proc files to override with stubs (e.g. kallsyms)")
flagSet.Bool("ignore-cgroups", false, "don't configure cgroups.")
flagSet.Bool("mount-cgroup-v2", false, "EXPERIMENTAL. Mount cgroup v2 instead of cgroup v1 inside the sandbox. cgroup v2 support in gVisor is experimental and incomplete. Do not use for production workloads.")
flagSet.Int("fdlimit", -1, "Specifies a limit on the number of host file descriptors that can be open. Applies separately to the sentry and gofer. Note: each file in the sandbox holds more than one host FD open.")
flagSet.Int("dcache", -1, "Set the global dentry cache size. This acts as a coarse-grained control on the number of host FDs simultaneously open by the sentry. If negative, per-mount caches are used.")
flagSet.Bool("iouring", false, "TEST ONLY; Enables io_uring syscalls in the sentry. Support is experimental and very limited.")
+17
View File
@@ -83,6 +83,7 @@ def _syscall_test(
netstack_sr = False,
nftables = False,
kvm_use_cpu_nums = True,
mount_cgroup_v2 = False,
**kwargs):
# Prepend "runsc" to non-native platform names.
full_platform = platform if platform == "native" else "runsc_" + platform
@@ -183,6 +184,11 @@ def _syscall_test(
"--kvm-use-cpu-nums=" + str(kvm_use_cpu_nums),
]
if mount_cgroup_v2:
runner_args.append("--mount-cgroup-v2=true")
else:
runner_args.append("--mount-cgroup-v2=false")
# Trace points are platform agnostic, so enable them for ptrace only.
if platform == "ptrace":
runner_args.append("--trace")
@@ -228,6 +234,7 @@ def syscall_test_variants(
netstack_sr = False,
nftables = False,
kvm_use_cpu_nums = False,
mount_cgroup_v2 = False,
**kwargs):
"""Generates syscall tests for all variants.
@@ -285,6 +292,7 @@ def syscall_test_variants(
netstack_sr = netstack_sr,
nftables = nftables,
kvm_use_cpu_nums = kvm_use_cpu_nums,
mount_cgroup_v2 = mount_cgroup_v2,
**kwargs
)
@@ -311,6 +319,7 @@ def syscall_test_variants(
netstack_sr = netstack_sr,
nftables = nftables,
kvm_use_cpu_nums = kvm_use_cpu_nums,
mount_cgroup_v2 = mount_cgroup_v2,
**kwargs
)
@@ -338,6 +347,7 @@ def syscall_test_variants(
netstack_sr = netstack_sr,
nftables = nftables,
kvm_use_cpu_nums = kvm_use_cpu_nums,
mount_cgroup_v2 = mount_cgroup_v2,
**kwargs
)
if not use_tmpfs:
@@ -364,6 +374,7 @@ def syscall_test_variants(
netstack_sr = netstack_sr,
nftables = nftables,
kvm_use_cpu_nums = kvm_use_cpu_nums,
mount_cgroup_v2 = mount_cgroup_v2,
**kwargs
)
if add_fusefs:
@@ -388,6 +399,7 @@ def syscall_test_variants(
netstack_sr = netstack_sr,
nftables = nftables,
kvm_use_cpu_nums = kvm_use_cpu_nums,
mount_cgroup_v2 = mount_cgroup_v2,
**kwargs
)
@@ -416,6 +428,7 @@ def syscall_test(
nftables = False,
perf = False,
kvm_use_cpu_nums = False,
mount_cgroup_v2 = False,
**kwargs):
"""syscall_test is a macro that will create targets for all platforms.
@@ -474,6 +487,7 @@ def syscall_test(
container = container,
one_sandbox = one_sandbox,
kvm_use_cpu_nums = kvm_use_cpu_nums,
mount_cgroup_v2 = mount_cgroup_v2,
**kwargs
)
@@ -502,6 +516,7 @@ def syscall_test(
netstack_sr = False,
nftables = nftables,
kvm_use_cpu_nums = kvm_use_cpu_nums,
mount_cgroup_v2 = mount_cgroup_v2,
**kwargs
)
@@ -534,6 +549,7 @@ def syscall_test(
netstack_sr = False,
nftables = nftables,
kvm_use_cpu_nums = kvm_use_cpu_nums,
mount_cgroup_v2 = mount_cgroup_v2,
**kwargs
)
@@ -592,5 +608,6 @@ def syscall_test(
netstack_sr = False,
nftables = nftables,
kvm_use_cpu_nums = kvm_use_cpu_nums,
mount_cgroup_v2 = mount_cgroup_v2,
**kwargs
)
+6
View File
@@ -77,6 +77,7 @@ var (
netstackSR = flag.Bool("netstack-sr", false, "enables netstack s/r")
nftables = flag.Bool("nftables", false, "enables nftables")
kvmUseCPUNums = flag.Bool("kvm-use-cpu-nums", false, "use cpu numbers in kvm platform")
mountCgroupV2 = flag.Bool("mount-cgroup-v2", false, "mount cgroups v2")
)
const (
@@ -430,6 +431,11 @@ func runRunsc(tc *gtest.TestCase, spec *specs.Spec) error {
} else {
args = append(args, "-kvm-use-cpu-nums=false")
}
if *mountCgroupV2 {
args = append(args, "-mount-cgroup-v2=true")
} else {
args = append(args, "-mount-cgroup-v2=false")
}
testLogDir := ""
runscLogDir := ""
+6
View File
@@ -75,6 +75,12 @@ syscall_test(
test = "//test/syscalls/linux:cgroup_test",
)
syscall_test(
size = "large",
mount_cgroup_v2 = True,
test = "//test/syscalls/linux:cgroup2_test",
)
syscall_test(
add_fusefs = True,
add_overlay = True,
+29
View File
@@ -4948,6 +4948,35 @@ cc_binary(
],
)
cc_binary(
name = "cgroup2_test",
testonly = 1,
srcs = ["cgroup2.cc"],
linkstatic = 1,
malloc = "//test/util:errno_safe_allocator",
deps = select_gtest() + [
"//test/util:capability_util",
"//test/util:cgroup_util",
"//test/util:cleanup",
"//test/util:file_descriptor",
"//test/util:fs_util",
"//test/util:logging",
"//test/util:mount_util",
"//test/util:posix_error",
"//test/util:save_util",
"//test/util:temp_path",
"//test/util:test_main",
"//test/util:test_util",
"//test/util:thread_util",
"@com_google_absl//absl/container:flat_hash_map",
"@com_google_absl//absl/container:flat_hash_set",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/strings:str_format",
"@com_google_absl//absl/synchronization",
"@com_google_absl//absl/time",
],
)
cc_binary(
name = "deleted_test",
testonly = 1,
+7 -942
View File
@@ -82,11 +82,6 @@ bool CgroupsAvailable() {
TEST_CHECK_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN));
}
bool Cgroup2Available() {
return !IsRunningOnGvisor() &&
TEST_CHECK_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN));
}
// NoopThreads spawns a set of threads that do nothing until they're asked to
// exit. Useful for testing functionality that requires a process with multiple
// threads.
@@ -315,7 +310,7 @@ TEST(Cgroup, MigrateToSubcontainerThread) {
Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(c.CreateChild("child1"));
// Ensure there are some threads for this process.
NoopThreads threads(10);
NoopThreads threads(1);
// Initially, test process should be in the root cgroup c.
EXPECT_NO_ERRNO(c.ContainsCallingThread());
@@ -1110,946 +1105,16 @@ TEST(DevicesCgroup, IgnorePartialMatchRule) {
IsPosixErrorOkAndHolds("c 7:* rw\n"));
}
class Cgroup2Test : public ::testing::Test {
protected:
void SetUp() override {
if (!Cgroup2Available()) {
GTEST_SKIP() << "Cgroup v2 not available or ignored on gVisor";
}
}
void CleanCgroupDirs(absl::string_view dir) {
auto children = ListDir(dir, /*skipdots=*/true);
if (children.ok()) {
for (const auto& child : children.ValueOrDie()) {
std::string full_path = JoinPath(dir, child);
auto is_dir = IsDirectory(full_path);
if (is_dir.ok() && is_dir.ValueOrDie()) {
CleanCgroupDirs(full_path);
}
}
}
absl::Time deadline = absl::Now() + absl::Seconds(5);
PosixError err;
while (true) {
err = Rmdir(dir);
if (err.ok()) {
break;
}
if (absl::Now() >= deadline) {
ASSERT_NO_ERRNO(err);
break;
}
absl::SleepFor(absl::Milliseconds(10));
}
}
void TearDown() override {
if (root_) {
root_->Enter(getpid()).IgnoreError();
}
if (c_) {
CleanCgroupDirs(c_->Path());
}
}
void Init() {
if (m_) return;
m_.emplace(ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()));
root_.emplace(ASSERT_NO_ERRNO_AND_VALUE(m_->MountCgroup2fs()));
auto controllers = root_->ReadControlFile("cgroup.controllers");
if (controllers.ok()) {
std::vector<std::string> list = absl::StrSplit(
controllers.ValueOrDie(), absl::ByAnyChar(" \n"), absl::SkipEmpty());
for (const std::string& ctrl : list) {
ASSERT_NO_ERRNO(
root_->WriteControlFile("cgroup.subtree_control", "+" + ctrl));
}
}
c_.emplace(ASSERT_NO_ERRNO_AND_VALUE(root_->CreateChild("test")));
}
const Cgroup& c() {
Init();
return *c_;
}
const Cgroup& root() {
Init();
return *root_;
}
void ExpectInotifyEventOrTimeout(int fd) {
struct pollfd pfd = {fd, POLLIN, 0};
ASSERT_GT(poll(&pfd, 1, 5000), 0);
char buf[4096];
EXPECT_GT(read(fd, buf, sizeof(buf)), 0);
}
void ExpectDefaultControlFiles(const Cgroup& cg, bool is_root = false) {
EXPECT_THAT(Exists(cg.Relpath("cgroup.procs")),
IsPosixErrorOkAndHolds(true));
EXPECT_THAT(Exists(cg.Relpath("cgroup.controllers")),
IsPosixErrorOkAndHolds(true));
EXPECT_THAT(Exists(cg.Relpath("cgroup.subtree_control")),
IsPosixErrorOkAndHolds(true));
EXPECT_THAT(Exists(cg.Relpath("cgroup.stat")),
IsPosixErrorOkAndHolds(true));
if (!is_root) {
EXPECT_THAT(Exists(cg.Relpath("cgroup.events")),
IsPosixErrorOkAndHolds(true));
EXPECT_THAT(Exists(cg.Relpath("cgroup.type")),
IsPosixErrorOkAndHolds(true));
EXPECT_THAT(Exists(cg.Relpath("cgroup.kill")),
IsPosixErrorOkAndHolds(true));
}
}
private:
std::optional<Mounter> m_;
std::optional<Cgroup> root_;
std::optional<Cgroup> c_;
};
TEST_F(Cgroup2Test, RootControlFilesPopulated) {
ExpectDefaultControlFiles(c(), /*is_root=*/true);
}
TEST_F(Cgroup2Test, Create) {
Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(c().CreateChild("child1"));
EXPECT_THAT(Exists(child.Path()), IsPosixErrorOkAndHolds(true));
ExpectDefaultControlFiles(child, /*is_root=*/false);
// Defaults.
EXPECT_THAT(child.ReadControlFile("cgroup.type"),
IsPosixErrorOkAndHolds(HasSubstr("domain")));
EXPECT_THAT(child.ReadControlFile("cgroup.subtree_control"),
IsPosixErrorOkAndHolds(Eq("")));
EXPECT_THAT(child.ReadControlFile("cgroup.stat"),
IsPosixErrorOkAndHolds(HasSubstr("nr_descendants 0")));
// No tasks.
auto procs = ASSERT_NO_ERRNO_AND_VALUE(child.Procs());
EXPECT_TRUE(procs.empty());
EXPECT_THAT(child.ReadControlFile("cgroup.events"),
IsPosixErrorOkAndHolds(HasSubstr("populated 0")));
}
TEST_F(Cgroup2Test, MkdirWithPermissions) {
std::string child_path = JoinPath(c().Path(), "child");
ASSERT_NO_ERRNO(Mkdir(child_path, 0444));
const struct stat s1 = ASSERT_NO_ERRNO_AND_VALUE(Stat(child_path));
EXPECT_THAT(s1.st_mode, PermissionIs(0444));
EXPECT_TRUE(S_ISDIR(s1.st_mode));
}
TEST_F(Cgroup2Test, CannotRenameControlFile) {
EXPECT_THAT(
rename(c().Relpath("cgroup.procs").c_str(), c().Relpath("foo").c_str()),
SyscallFailsWithErrno(EPERM));
}
TEST_F(Cgroup2Test, CannotMountOverBusy) {
EXPECT_THAT(Mount("none", root().Path(), "cgroup2", 0, "", 0),
PosixErrorIs(EBUSY));
}
TEST_F(Cgroup2Test, DestroyConstraints) {
Cgroup parent = ASSERT_NO_ERRNO_AND_VALUE(c().CreateChild("parent"));
// Cannot destroy a cgroup node that has a live sub-directory.
Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(parent.CreateChild("child"));
EXPECT_THAT(parent.Delete(), PosixErrorIs(EBUSY));
// Cannot destroy a cgroup node with a live attached process.
auto clean = Cleanup([&] { ASSERT_NO_ERRNO(root().Enter(getpid())); });
ASSERT_NO_ERRNO(child.Enter(getpid()));
EXPECT_THAT(child.Delete(), PosixErrorIs(EBUSY));
// Destroy successfully once leaf node is empty.
ASSERT_NO_ERRNO(root().Enter(getpid()));
clean.Release();
EXPECT_NO_ERRNO(child.Delete());
EXPECT_NO_ERRNO(parent.Delete());
}
TEST_F(Cgroup2Test, CannotEnableControllerDisabledInParent) {
std::string controllers =
ASSERT_NO_ERRNO_AND_VALUE(c().ReadControlFile("cgroup.controllers"));
SKIP_IF(!absl::StrContains(controllers, "pids"));
ASSERT_NO_ERRNO(c().WriteControlFile("cgroup.subtree_control", "+pids"));
Cgroup parent = ASSERT_NO_ERRNO_AND_VALUE(c().CreateChild("parent"));
EXPECT_THAT(parent.ReadControlFile("cgroup.subtree_control"),
IsPosixErrorOkAndHolds(Not(HasSubstr("pids"))));
Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(parent.CreateChild("child"));
EXPECT_THAT(child.WriteControlFile("cgroup.subtree_control", "+pids"),
PosixErrorIs(ENOENT));
}
TEST_F(Cgroup2Test, CannotDisableControllerEnabledInChild) {
std::string controllers =
ASSERT_NO_ERRNO_AND_VALUE(c().ReadControlFile("cgroup.controllers"));
SKIP_IF(!absl::StrContains(controllers, "pids"));
Cgroup parent = ASSERT_NO_ERRNO_AND_VALUE(c().CreateChild("parent"));
Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(parent.CreateChild("child"));
ASSERT_NO_ERRNO(c().WriteControlFile("cgroup.subtree_control", "+pids"));
ASSERT_NO_ERRNO(parent.WriteControlFile("cgroup.subtree_control", "+pids"));
ASSERT_NO_ERRNO(child.WriteControlFile("cgroup.subtree_control", "+pids"));
EXPECT_THAT(parent.WriteControlFile("cgroup.subtree_control", "-pids"),
PosixErrorIs(EBUSY));
ASSERT_NO_ERRNO(child.WriteControlFile("cgroup.subtree_control", "-pids"));
EXPECT_NO_ERRNO(parent.WriteControlFile("cgroup.subtree_control", "-pids"));
}
TEST_F(Cgroup2Test, SubtreeControlPids) {
std::string controllers =
ASSERT_NO_ERRNO_AND_VALUE(c().ReadControlFile("cgroup.controllers"));
SKIP_IF(!absl::StrContains(controllers, "pids"));
Cgroup parent = ASSERT_NO_ERRNO_AND_VALUE(c().CreateChild("parent"));
Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(parent.CreateChild("child"));
// Once we enabled "pids" in the parent...
ASSERT_NO_ERRNO(c().WriteControlFile("cgroup.subtree_control", "+pids"));
auto clean = Cleanup([&] {
c().WriteControlFile("cgroup.subtree_control", "-pids").IgnoreError();
});
// ...it becomes available in the child.
ASSERT_NO_ERRNO(parent.WriteControlFile("cgroup.subtree_control", "+pids"));
ExpectDefaultControlFiles(child);
EXPECT_THAT(Exists(child.Relpath("pids.max")), IsPosixErrorOkAndHolds(true));
EXPECT_THAT(child.ReadControlFile("cgroup.controllers"),
IsPosixErrorOkAndHolds(::testing::HasSubstr("pids")));
// Invalid writes to the subtree_control file must fail.
EXPECT_THAT(parent.WriteControlFile("cgroup.subtree_control", "+garbage"),
PosixErrorIs(EINVAL));
EXPECT_THAT(parent.WriteControlFile("cgroup.subtree_control", "pids"),
PosixErrorIs(EINVAL));
// Attempting to write to the read-only cgroup.controllers list must fail.
EXPECT_THAT(parent.WriteControlFile("cgroup.controllers", "+pids"),
PosixErrorIs(EINVAL));
// Disabling a controller in the parent makes it unavailable in the child.
ASSERT_NO_ERRNO(parent.WriteControlFile("cgroup.subtree_control", "-pids"));
EXPECT_THAT(Exists(child.Relpath("pids.max")), IsPosixErrorOkAndHolds(false));
EXPECT_THAT(child.ReadControlFile("cgroup.controllers"),
IsPosixErrorOkAndHolds(Not(HasSubstr("pids"))));
}
TEST_F(Cgroup2Test, PidsEnforcement) {
std::string controllers =
ASSERT_NO_ERRNO_AND_VALUE(c().ReadControlFile("cgroup.controllers"));
SKIP_IF(!absl::StrContains(controllers, "pids"));
ASSERT_NO_ERRNO(c().WriteControlFile("cgroup.subtree_control", "+pids"));
Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(c().CreateChild("child"));
ExpectDefaultControlFiles(child);
ASSERT_NO_ERRNO(child.WriteControlFile("pids.max", "1"));
int fds[2];
ASSERT_THAT(pipe(fds), SyscallSucceeds());
FileDescriptor rfd(fds[0]);
FileDescriptor wfd(fds[1]);
constexpr int kCantFork = 2;
pid_t pid = fork();
if (pid == 0) {
wfd.CloseSignalSafe();
char token;
if (read(rfd.get(), &token, 1) <= 0) {
_exit(1);
}
pid_t grand_pid = fork();
if (grand_pid < 0) {
_exit(kCantFork);
} else if (grand_pid == 0) {
_exit(0);
}
_exit(3);
}
rfd.reset();
ASSERT_GT(pid, 0);
ASSERT_NO_ERRNO(child.Enter(pid));
ASSERT_THAT(write(wfd.get(), "x", 1), SyscallSucceeds());
wfd.reset();
int status;
ASSERT_EQ(waitpid(pid, &status, 0), pid);
EXPECT_TRUE(WIFEXITED(status));
EXPECT_EQ(WEXITSTATUS(status), kCantFork);
}
TEST_F(Cgroup2Test, NoInternalProcesses) {
std::string controllers =
ASSERT_NO_ERRNO_AND_VALUE(c().ReadControlFile("cgroup.controllers"));
SKIP_IF(!absl::StrContains(controllers, "memory"));
ASSERT_NO_ERRNO(c().WriteControlFile("cgroup.subtree_control", "+memory"));
auto clean_mem = Cleanup([&] {
c().WriteControlFile("cgroup.subtree_control", "-memory").IgnoreError();
});
Cgroup parent = ASSERT_NO_ERRNO_AND_VALUE(c().CreateChild("parent"));
Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(parent.CreateChild("child"));
// A cgroup may contain both member child cgroups and member processes
// as long as no controllers are enabled in its subtree_control.
ASSERT_THAT(parent.ReadControlFile("cgroup.subtree_control"),
IsPosixErrorOkAndHolds(Eq("")));
auto clean = Cleanup([&] { ASSERT_NO_ERRNO(root().Enter(getpid())); });
ASSERT_NO_ERRNO(parent.Enter(getpid()));
// Attempting to subsequently enable any resource controllers while
// member processes exist should fail.
EXPECT_THAT(parent.WriteControlFile("cgroup.subtree_control", "+memory"),
PosixErrorIs(EBUSY));
// Moving the process down into the sub-cgroup renders the parent empty,
// allowing us to enable controllers in the parent.
ASSERT_NO_ERRNO(child.Enter(getpid()));
EXPECT_TRUE(
parent.WriteControlFile("cgroup.subtree_control", "+memory").ok());
// Finally, moving the process back up into the parent is now
// strictly denied by the rule since subtree_control is active/non-empty.
EXPECT_THAT(parent.Enter(getpid()), PosixErrorIs(EBUSY));
}
TEST_F(Cgroup2Test, PIDZeroMovesSelf) {
Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(c().CreateChild("child"));
ExpectDefaultControlFiles(child);
auto clean = Cleanup([&] { ASSERT_NO_ERRNO(root().Enter(getpid())); });
ASSERT_NO_ERRNO(child.WriteIntegerControlFile("cgroup.procs", 0));
auto procs = ASSERT_NO_ERRNO_AND_VALUE(child.Procs());
EXPECT_TRUE(procs.contains(getpid()));
TEST(Cgroup, ProcSelfCgroupNoV2LineIfUnmounted) {
SKIP_IF(!CgroupsAvailable());
std::string content;
ASSERT_NO_ERRNO(GetContents("/proc/self/cgroup", &content));
EXPECT_THAT(content, HasSubstr("0::/test/child\n"));
}
TEST_F(Cgroup2Test, TaskMigration) {
Cgroup src = ASSERT_NO_ERRNO_AND_VALUE(c().CreateChild("src"));
ExpectDefaultControlFiles(src);
Cgroup dst = ASSERT_NO_ERRNO_AND_VALUE(c().CreateChild("dst"));
ExpectDefaultControlFiles(dst);
auto clean = Cleanup([&] { ASSERT_NO_ERRNO(root().Enter(getpid())); });
ASSERT_NO_ERRNO(src.Enter(getpid()));
EXPECT_THAT(src.ReadControlFile("cgroup.events"),
IsPosixErrorOkAndHolds(HasSubstr("populated 1")));
EXPECT_THAT(dst.ReadControlFile("cgroup.events"),
IsPosixErrorOkAndHolds(HasSubstr("populated 0")));
EXPECT_NO_ERRNO(dst.Enter(getpid()));
EXPECT_THAT(src.ReadControlFile("cgroup.events"),
IsPosixErrorOkAndHolds(HasSubstr("populated 0")));
EXPECT_THAT(dst.ReadControlFile("cgroup.events"),
IsPosixErrorOkAndHolds(HasSubstr("populated 1")));
}
TEST_F(Cgroup2Test, InotifyEventsOnEntry) {
Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(c().CreateChild("child"));
FileDescriptor inotify_fd(inotify_init1(IN_NONBLOCK));
int wd = inotify_add_watch(inotify_fd.get(),
child.Relpath("cgroup.events").c_str(), IN_MODIFY);
ASSERT_GE(wd, 0);
// Enter the cgroup.
auto clean = Cleanup([&] { ASSERT_NO_ERRNO(root().Enter(getpid())); });
ASSERT_NO_ERRNO(child.Enter(getpid()));
// Entry is observed via inotify and the events file.
ExpectInotifyEventOrTimeout(inotify_fd.get());
EXPECT_THAT(child.ReadControlFile("cgroup.events"),
IsPosixErrorOkAndHolds(HasSubstr("populated 1")));
}
TEST_F(Cgroup2Test, InotifyEventsOnExit) {
Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(c().CreateChild("child"));
ExpectDefaultControlFiles(child);
FileDescriptor inotify_fd(inotify_init1(IN_NONBLOCK));
int wd = inotify_add_watch(inotify_fd.get(),
child.Relpath("cgroup.events").c_str(), IN_MODIFY);
ASSERT_GE(wd, 0);
int fds[2];
ASSERT_THAT(pipe(fds), SyscallSucceeds());
FileDescriptor rfd(fds[0]);
FileDescriptor wfd(fds[1]);
pid_t pid = fork();
if (pid == 0) {
wfd.CloseSignalSafe();
char token;
if (read(rfd.get(), &token, 1) <= 0) {
_exit(1);
}
_exit(0);
}
rfd.reset();
ASSERT_GT(pid, 0);
// Enter the cgroup and consume the entry event.
ASSERT_NO_ERRNO(child.Enter(pid));
ExpectInotifyEventOrTimeout(inotify_fd.get());
// Instruct task to exit, creating an unreaped zombie.
ASSERT_THAT(write(wfd.get(), "x", 1), SyscallSucceeds());
wfd.reset();
// Zombies are considered to have left the cgroup.
ExpectInotifyEventOrTimeout(inotify_fd.get());
EXPECT_THAT(child.ReadControlFile("cgroup.events"),
IsPosixErrorOkAndHolds(HasSubstr("populated 0")));
int status;
ASSERT_EQ(waitpid(pid, &status, 0), pid);
EXPECT_TRUE(WIFEXITED(status));
}
TEST_F(Cgroup2Test, ZombieCgroupMembership) {
Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(c().CreateChild("child"));
ExpectDefaultControlFiles(child);
int fds[2];
ASSERT_THAT(pipe(fds), SyscallSucceeds());
FileDescriptor rfd(fds[0]);
FileDescriptor wfd(fds[1]);
pid_t pid = fork();
if (pid == 0) {
wfd.CloseSignalSafe();
char token;
if (read(rfd.get(), &token, 1) <= 0) {
_exit(1);
}
_exit(0);
}
rfd.reset();
ASSERT_GT(pid, 0);
ASSERT_NO_ERRNO(child.Enter(pid));
// Zombify the child.
ASSERT_THAT(write(wfd.get(), "x", 1), SyscallSucceeds());
wfd.reset();
siginfo_t info = {};
ASSERT_THAT(waitid(P_PID, pid, &info, WEXITED | WNOWAIT), SyscallSucceeds());
// A zombie process does not appear in cgroup.procs...
auto procs = ASSERT_NO_ERRNO_AND_VALUE(child.Procs());
EXPECT_FALSE(procs.contains(pid));
// ...and thus cannot be moved to another cgroup: the write succeeds,
// but the process is not moved, as shown by the subsequent read.
EXPECT_NO_ERRNO(c().WriteIntegerControlFile("cgroup.procs", pid));
auto root_procs = ASSERT_NO_ERRNO_AND_VALUE(c().Procs());
EXPECT_FALSE(root_procs.contains(pid));
// Delete the child cgroup.
ASSERT_NO_ERRNO(child.Delete());
// To see "deleted".
std::string content;
ASSERT_NO_ERRNO(
GetContents(absl::StrFormat("/proc/%d/cgroup", pid), &content));
EXPECT_THAT(content, HasSubstr(" (deleted)\n"));
int status;
ASSERT_EQ(waitpid(pid, &status, 0), pid);
EXPECT_TRUE(WIFEXITED(status));
}
TEST_F(Cgroup2Test, Threaded) {
Cgroup parent = ASSERT_NO_ERRNO_AND_VALUE(c().CreateChild("parent"));
Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(parent.CreateChild("child"));
// "domain" is the default.
EXPECT_THAT(child.ReadControlFile("cgroup.type"),
IsPosixErrorOkAndHolds(HasSubstr("domain")));
// Make the child a threaded cgroup.
EXPECT_NO_ERRNO(child.WriteControlFile("cgroup.type", "threaded"));
EXPECT_THAT(child.ReadControlFile("cgroup.type"),
IsPosixErrorOkAndHolds(HasSubstr("threaded")));
// The parent automatically becomes "domain threaded".
EXPECT_THAT(parent.ReadControlFile("cgroup.type"),
IsPosixErrorOkAndHolds(HasSubstr("domain threaded")));
// Removing the threaded child clears the condition, reverting parent to
// normal domain.
ASSERT_NO_ERRNO(child.Delete());
EXPECT_THAT(parent.ReadControlFile("cgroup.type"),
IsPosixErrorOkAndHolds(HasSubstr("domain")));
}
TEST_F(Cgroup2Test, ThreadedDomainViaSubtreeControl) {
std::string controllers =
ASSERT_NO_ERRNO_AND_VALUE(c().ReadControlFile("cgroup.controllers"));
SKIP_IF(!absl::StrContains(controllers, "pids"));
auto clean = Cleanup([&] { ASSERT_NO_ERRNO(root().Enter(getpid())); });
ASSERT_NO_ERRNO(c().Enter(getpid()));
// Enabling threaded controllers in subtree_control while a process exists
// makes it domain threaded.
ASSERT_NO_ERRNO(c().WriteControlFile("cgroup.subtree_control", "+pids"));
EXPECT_THAT(c().ReadControlFile("cgroup.type"),
IsPosixErrorOkAndHolds(HasSubstr("domain threaded")));
}
TEST_F(Cgroup2Test, ThreadedSubtreeBecomesInvalidDomain) {
Cgroup parent = ASSERT_NO_ERRNO_AND_VALUE(c().CreateChild("parent"));
Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(parent.CreateChild("child"));
Cgroup grandchild =
ASSERT_NO_ERRNO_AND_VALUE(child.CreateChild("grandchild"));
ASSERT_NO_ERRNO(child.WriteControlFile("cgroup.type", "threaded"));
EXPECT_THAT(grandchild.ReadControlFile("cgroup.type"),
IsPosixErrorOkAndHolds(HasSubstr("domain invalid")));
// Cannot add processes to an invalid domain.
EXPECT_THAT(grandchild.Enter(getpid()), PosixErrorIs(EOPNOTSUPP));
}
TEST_F(Cgroup2Test, ThreadedNodeCanHaveInternalProcesses) {
std::string controllers =
ASSERT_NO_ERRNO_AND_VALUE(c().ReadControlFile("cgroup.controllers"));
SKIP_IF(!absl::StrContains(controllers, "pids"));
ASSERT_NO_ERRNO(c().WriteControlFile("cgroup.subtree_control", "+pids"));
auto clean_pids = Cleanup([&] {
c().WriteControlFile("cgroup.subtree_control", "-pids").IgnoreError();
});
Cgroup parent = ASSERT_NO_ERRNO_AND_VALUE(c().CreateChild("parent"));
Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(parent.CreateChild("child"));
ASSERT_NO_ERRNO(parent.WriteControlFile("cgroup.type", "threaded"));
ASSERT_NO_ERRNO(child.WriteControlFile("cgroup.type", "threaded"));
ASSERT_NO_ERRNO(parent.WriteControlFile("cgroup.subtree_control", "+pids"));
auto clean = Cleanup([&] { ASSERT_NO_ERRNO(root().Enter(getpid())); });
EXPECT_NO_ERRNO(parent.Enter(getpid()));
}
TEST_F(Cgroup2Test, ThreadedSubtreeMasking) {
std::string controllers =
ASSERT_NO_ERRNO_AND_VALUE(c().ReadControlFile("cgroup.controllers"));
SKIP_IF(!absl::StrContains(controllers, "memory") ||
!absl::StrContains(controllers, "pids"));
Cgroup parent = ASSERT_NO_ERRNO_AND_VALUE(c().CreateChild("parent"));
Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(parent.CreateChild("child"));
ASSERT_NO_ERRNO(c().WriteControlFile("cgroup.subtree_control", "+memory"));
ASSERT_NO_ERRNO(c().WriteControlFile("cgroup.subtree_control", "+pids"));
ASSERT_NO_ERRNO(parent.WriteControlFile("cgroup.subtree_control", "+memory"));
ASSERT_NO_ERRNO(parent.WriteControlFile("cgroup.subtree_control", "+pids"));
// Cannot make child threaded while parent has domain controllers enabled.
EXPECT_THAT(child.WriteControlFile("cgroup.type", "threaded"),
PosixErrorIs(EOPNOTSUPP));
// Disable the domain controllers.
ASSERT_NO_ERRNO(parent.WriteControlFile("cgroup.subtree_control", "-memory"));
// Make the child threaded and verify that the domain controllers are masked.
ASSERT_NO_ERRNO(child.WriteControlFile("cgroup.type", "threaded"));
EXPECT_THAT(child.ReadControlFile("cgroup.controllers"),
IsPosixErrorOkAndHolds(Not(HasSubstr("memory"))));
EXPECT_THAT(child.ReadControlFile("cgroup.controllers"),
IsPosixErrorOkAndHolds(HasSubstr("pids")));
// Cannot enable domain controllers in the child.
EXPECT_THAT(child.WriteControlFile("cgroup.subtree_control", "+memory"),
PosixErrorIs(ENOENT));
ASSERT_NO_ERRNO(child.WriteControlFile("cgroup.subtree_control", "+pids"));
}
TEST_F(Cgroup2Test, ThreadedCannotBeEnabledWithPopulatedDomainChildren) {
Cgroup parent = ASSERT_NO_ERRNO_AND_VALUE(c().CreateChild("parent"));
Cgroup child1 = ASSERT_NO_ERRNO_AND_VALUE(parent.CreateChild("child1"));
Cgroup child2 = ASSERT_NO_ERRNO_AND_VALUE(parent.CreateChild("child2"));
auto clean = Cleanup([&] { ASSERT_NO_ERRNO(root().Enter(getpid())); });
ASSERT_NO_ERRNO(child1.Enter(getpid()));
// Attempting to make child2 threaded should fail because parent has a
// populated domain child in child1.
EXPECT_THAT(child2.WriteControlFile("cgroup.type", "threaded"),
PosixErrorIs(EOPNOTSUPP));
}
TEST_F(Cgroup2Test, ThreadgroupMigration) {
Cgroup dst = ASSERT_NO_ERRNO_AND_VALUE(c().CreateChild("dst"));
constexpr int kNumThreads = 10;
std::vector<std::unique_ptr<ScopedThread>> threads;
std::vector<pid_t> tids;
absl::Mutex mu;
absl::Notification ready;
absl::Notification exit;
threads.reserve(kNumThreads);
for (int i = 0; i < kNumThreads; ++i) {
threads.push_back(std::make_unique<ScopedThread>([&]() {
pid_t tid = gettid();
{
absl::MutexLock lock(mu);
tids.push_back(tid);
if (tids.size() == kNumThreads) {
ready.Notify();
}
}
exit.WaitForNotification();
}));
}
ready.WaitForNotification();
// Move the thread group to dst.
ASSERT_NO_ERRNO(dst.WriteIntegerControlFile("cgroup.procs", getpid()));
auto clean_dst = Cleanup([&] {
ASSERT_NO_ERRNO(c().WriteIntegerControlFile("cgroup.procs", getpid()));
});
// The subthreads must be moved too.
auto thread_set = ASSERT_NO_ERRNO_AND_VALUE(dst.Threads());
EXPECT_THAT(thread_set, Contains(gettid()));
for (pid_t tid : tids) {
EXPECT_THAT(thread_set, Contains(tid));
}
exit.Notify();
}
TEST_F(Cgroup2Test, ThreadMigration) {
Cgroup dom = ASSERT_NO_ERRNO_AND_VALUE(c().CreateChild("dom"));
Cgroup src = ASSERT_NO_ERRNO_AND_VALUE(dom.CreateChild("src"));
Cgroup dst = ASSERT_NO_ERRNO_AND_VALUE(dom.CreateChild("dst"));
ASSERT_NO_ERRNO(src.WriteControlFile("cgroup.type", "threaded"));
ASSERT_NO_ERRNO(dst.WriteControlFile("cgroup.type", "threaded"));
// The main process must reside within the threaded subtree root domain
// ('dom') before individual sub-threads can be mapped across its threaded
// descendant directories.
ASSERT_NO_ERRNO(dom.WriteIntegerControlFile("cgroup.procs", getpid()));
auto clean_dom = Cleanup([&] {
ASSERT_NO_ERRNO(c().WriteIntegerControlFile("cgroup.procs", getpid()));
});
absl::Notification done;
bool failed = false;
ScopedThread t([&]() {
// Oscillate vigorously.
for (int i = 0; i < 1000; ++i) {
const Cgroup& target = (i % 2 == 0) ? dst : src;
if (!target.WriteIntegerControlFile("cgroup.threads", gettid()).ok()) {
failed = true;
break;
}
}
done.Notify();
});
done.WaitForNotification();
EXPECT_FALSE(failed);
auto dom_threads = ASSERT_NO_ERRNO_AND_VALUE(dom.Threads());
EXPECT_THAT(dom_threads, Contains(gettid()));
}
TEST_F(Cgroup2Test, ThreadBoundarySplittingDisallowed) {
Cgroup dom = ASSERT_NO_ERRNO_AND_VALUE(c().CreateChild("dom"));
Cgroup threaded = ASSERT_NO_ERRNO_AND_VALUE(dom.CreateChild("threaded"));
ASSERT_NO_ERRNO(threaded.WriteControlFile("cgroup.type", "threaded"));
absl::Notification ready;
absl::Notification exit;
int err = 0;
// The test process resides outside dom.
ScopedThread t([&]() {
// Attempting to map a single sub-thread directly into a threaded
// subtree while its sibling resides completely outside the subtree
// violates the thread-splitting rule.
auto status = threaded.WriteIntegerControlFile("cgroup.threads", gettid());
if (!status.ok()) {
err = status.errno_value();
}
ready.Notify();
exit.WaitForNotification();
});
ready.WaitForNotification();
EXPECT_EQ(err, EOPNOTSUPP);
exit.Notify();
}
TEST_F(Cgroup2Test, PermChecksUseOpenTimeCreds) {
Cgroup parent = ASSERT_NO_ERRNO_AND_VALUE(c().CreateChild("parent"));
Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(parent.CreateChild("child"));
auto clean = Cleanup([&] { ASSERT_NO_ERRNO(root().Enter(getpid())); });
ASSERT_NO_ERRNO(parent.Enter(getpid()));
// Grant unprivileged user read/write permissions to cgroup.procs.
constexpr uid_t kFeebleEUID = 65534;
ASSERT_THAT(chown(child.Relpath("cgroup.procs").c_str(), kFeebleEUID, -1),
SyscallSucceeds());
FileDescriptor child_procs_fd;
absl::Notification ready;
ScopedThread t([&]() {
ASSERT_THAT(syscall(SYS_setresuid, kFeebleEUID, kFeebleEUID, kFeebleEUID),
SyscallSucceeds());
child_procs_fd.reset(open(child.Relpath("cgroup.procs").c_str(), O_RDWR));
ready.Notify();
});
ready.WaitForNotification();
SKIP_IF(child_procs_fd.get() < 0);
// Open-time creds of the feeble subthread are not sufficient for access.
EXPECT_THAT(write(child_procs_fd.get(), "0", 1),
SyscallFailsWithErrno(EACCES));
// But if the feeble uid owns the common ancestor, then the write succeeds.
ASSERT_THAT(chown(parent.Relpath("cgroup.procs").c_str(), kFeebleEUID, -1),
SyscallSucceeds());
EXPECT_THAT(write(child_procs_fd.get(), "0", 1), SyscallSucceedsWithValue(1));
}
TEST_F(Cgroup2Test, KillTree) {
Cgroup parent = ASSERT_NO_ERRNO_AND_VALUE(c().CreateChild("parent"));
Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(parent.CreateChild("child"));
int fds[2];
ASSERT_THAT(pipe(fds), SyscallSucceeds());
FileDescriptor rfd(fds[0]);
FileDescriptor wfd(fds[1]);
pid_t pid1 = fork();
if (pid1 == 0) {
wfd.CloseSignalSafe();
char token;
if (read(rfd.get(), &token, 1) <= 0) {
_exit(1);
}
_exit(0);
}
ASSERT_GT(pid1, 0);
pid_t pid2 = fork();
if (pid2 == 0) {
wfd.CloseSignalSafe();
char token;
if (read(rfd.get(), &token, 1) <= 0) {
_exit(1);
}
_exit(0);
}
ASSERT_GT(pid2, 0);
rfd.reset();
ASSERT_NO_ERRNO(parent.Enter(pid1));
ASSERT_NO_ERRNO(child.Enter(pid2));
// Writing 1 to cgroup.kill kills all descendant tasks across the entire tree.
EXPECT_TRUE(parent.WriteControlFile("cgroup.kill", "1").ok());
wfd.reset();
int status;
ASSERT_EQ(waitpid(pid1, &status, 0), pid1);
EXPECT_TRUE(WIFSIGNALED(status));
EXPECT_EQ(WTERMSIG(status), SIGKILL);
ASSERT_EQ(waitpid(pid2, &status, 0), pid2);
EXPECT_TRUE(WIFSIGNALED(status));
EXPECT_EQ(WTERMSIG(status), SIGKILL);
}
TEST_F(Cgroup2Test, DescendantsStatAndLimit) {
// Verify defaults.
EXPECT_THAT(c().ReadControlFile("cgroup.stat"),
IsPosixErrorOkAndHolds(HasSubstr("nr_descendants 0")));
EXPECT_THAT(c().ReadControlFile("cgroup.max.descendants"),
IsPosixErrorOkAndHolds("max\n"));
EXPECT_THAT(c().ReadControlFile("cgroup.max.depth"),
IsPosixErrorOkAndHolds("max\n"));
// A single descendant.
Cgroup child = ASSERT_NO_ERRNO_AND_VALUE(c().CreateChild("child"));
EXPECT_THAT(c().ReadControlFile("cgroup.stat"),
IsPosixErrorOkAndHolds(HasSubstr("nr_descendants 1")));
// Set max descendants to 1 on the child...
ASSERT_NO_ERRNO(child.WriteControlFile("cgroup.max.descendants", "1"));
// ...thereby allowing the birth of the first grandchild.
Cgroup grandchild =
ASSERT_NO_ERRNO_AND_VALUE(child.CreateChild("grandchild"));
EXPECT_THAT(c().ReadControlFile("cgroup.stat"),
IsPosixErrorOkAndHolds(HasSubstr("nr_descendants 2")));
// ...but not the second.
auto second_grandchild = child.CreateChild("second_grandchild");
EXPECT_FALSE(second_grandchild.ok());
// Allow unlimited descendants on the child, but deny depth.
ASSERT_NO_ERRNO(child.WriteControlFile("cgroup.max.descendants", "max"));
ASSERT_NO_ERRNO(child.WriteControlFile("cgroup.max.depth", "1"));
auto great_grandchild = grandchild.CreateChild("great_grandchild");
EXPECT_FALSE(great_grandchild.ok());
}
struct CloneArgs {
int cgroup_fd;
int ready_fd;
};
int CloneSubthreadFunc(void* arg) {
auto args = static_cast<CloneArgs*>(arg);
pid_t tid = gettid();
char buf[32];
int len = snprintf(buf, sizeof(buf), "%d", tid);
if (write(args->cgroup_fd, buf, len) < 0) {
_exit(1);
}
char ready = 'x';
if (write(args->ready_fd, &ready, 1) < 0) {
_exit(1);
}
pause();
return 0;
}
// A slightly complex topology:
//
// [ dom ] (Type: "domain threaded")
// ├── Parent leader (getpid())
// ├── [ child1 ] (Type: "threaded")
// │ ├── Parent subthread (via ScopedThread)
// │ └── Child subthread (via raw clone)
// └── [ child2 ] (Type: "threaded")
// └── Child leader
TEST_F(Cgroup2Test, ThreadedDomainComplexTopology) {
Cgroup dom = ASSERT_NO_ERRNO_AND_VALUE(c().CreateChild("dom"));
Cgroup child1 = ASSERT_NO_ERRNO_AND_VALUE(dom.CreateChild("child1"));
Cgroup child2 = ASSERT_NO_ERRNO_AND_VALUE(dom.CreateChild("child2"));
ASSERT_NO_ERRNO(child1.WriteControlFile("cgroup.type", "threaded"));
ASSERT_NO_ERRNO(child2.WriteControlFile("cgroup.type", "threaded"));
auto clean = Cleanup([&] { ASSERT_NO_ERRNO(root().Enter(getpid())); });
ASSERT_NO_ERRNO(dom.Enter(getpid()));
absl::Notification start;
absl::Notification exit;
pid_t parent_subthread_tid = -1;
ScopedThread parent_subthread([&]() {
parent_subthread_tid = gettid();
ASSERT_NO_ERRNO(child1.WriteIntegerControlFile("cgroup.threads", gettid()));
start.Notify();
exit.WaitForNotification();
});
start.WaitForNotification();
// So we can write to it from the child subthread.
FileDescriptor child1_threads_fd = ASSERT_NO_ERRNO_AND_VALUE(
Open(child1.Relpath("cgroup.threads"), O_WRONLY));
int exit_fds[2];
ASSERT_THAT(pipe(exit_fds), SyscallSucceeds());
FileDescriptor exit_rfd(exit_fds[0]);
FileDescriptor exit_wfd(exit_fds[1]);
int start_fds[2];
ASSERT_THAT(pipe(start_fds), SyscallSucceeds());
FileDescriptor start_rfd(start_fds[0]);
FileDescriptor start_wfd(start_fds[1]);
pid_t p2 = fork();
if (p2 == 0) {
exit_wfd.CloseSignalSafe();
start_rfd.CloseSignalSafe();
// Move the child leader into child2.
ASSERT_NO_ERRNO(child2.WriteIntegerControlFile("cgroup.threads", gettid()));
int ready_fds[2];
if (pipe(ready_fds) < 0) {
_exit(1);
}
CloneArgs args = {
.cgroup_fd = child1_threads_fd.get(),
.ready_fd = ready_fds[1],
};
char stack[65536];
pid_t tid = clone(CloneSubthreadFunc, stack + sizeof(stack),
CLONE_THREAD | CLONE_SIGHAND | CLONE_VM, &args);
if (tid < 0) {
_exit(1);
}
close(ready_fds[1]);
// Wait till subthread has entered the child1 cgroup.
char ready_token;
if (read(ready_fds[0], &ready_token, 1) <= 0) {
_exit(1);
}
close(ready_fds[0]);
// Tell parent we're completely ready.
char sync = 'x';
if (write(start_wfd.get(), &sync, 1) < 0) {
_exit(1);
}
start_wfd.reset();
// Wait till parent signals to exit.
char token;
if (read(exit_rfd.get(), &token, 1) <= 0) {
_exit(1);
}
_exit(0);
}
exit_rfd.reset();
start_wfd.reset();
ASSERT_GT(p2, 0);
char sync_token;
ASSERT_THAT(read(start_rfd.get(), &sync_token, 1),
SyscallSucceedsWithValue(1));
start_rfd.reset();
// Sanity check for the topology.
EXPECT_THAT(child2.Threads(), IsPosixErrorOkAndHolds(Contains(p2)));
EXPECT_THAT(child1.Threads(),
IsPosixErrorOkAndHolds(Contains(parent_subthread_tid)));
// Reading cgroup.procs on dom returns both processes.
auto procs = ASSERT_NO_ERRNO_AND_VALUE(dom.Procs());
EXPECT_TRUE(procs.contains(getpid()));
EXPECT_TRUE(procs.contains(p2));
// Reading cgroup.procs from a threaded child inside the subtree must be
// disallowed.
EXPECT_THAT(child1.Procs(), PosixErrorIs(EOPNOTSUPP));
// Move the p2 process into child1.
EXPECT_NO_ERRNO(child1.WriteIntegerControlFile("cgroup.procs", p2));
EXPECT_THAT(child2.Threads(), IsPosixErrorOkAndHolds(::testing::IsEmpty()));
auto child1_threads = ASSERT_NO_ERRNO_AND_VALUE(child1.Threads());
// That should migrate both threads.
EXPECT_THAT(child1_threads, Contains(p2));
EXPECT_EQ(child1_threads.size(), 3);
// Signal child leader to exit and reap it.
ASSERT_THAT(write(exit_wfd.get(), "x", 1), SyscallSucceeds());
exit_wfd.reset();
int status;
ASSERT_EQ(waitpid(p2, &status, 0), p2);
EXPECT_TRUE(WIFEXITED(status));
exit.Notify(); // Join with subthread.
// The cgroup v2 entry "0::/..." should only appear if cgroup v2 has been
// mounted at least once. Since cgroup v2 has not been mounted in this test
// environment, the string "0::" should not appear in /proc/self/cgroup.
EXPECT_FALSE(absl::StrContains(content, "0::"));
}
} // namespace
File diff suppressed because it is too large Load Diff
+1 -7
View File
@@ -85,11 +85,6 @@ PosixErrorOr<absl::flat_hash_set<pid_t>> Cgroup::Tasks() const {
return ParsePIDList(buf);
}
PosixErrorOr<absl::flat_hash_set<pid_t>> Cgroup::Threads() const {
ASSIGN_OR_RETURN_ERRNO(std::string buf, ReadControlFile("cgroup.threads"));
return ParsePIDList(buf);
}
PosixError Cgroup::PollControlFileForChange(absl::string_view name,
absl::Duration timeout) const {
return PollControlFileForChangeAfter(name, timeout, []() {});
@@ -325,8 +320,7 @@ ProcPIDCgroupEntries(pid_t pid) {
// 1:memory:/
PIDCgroupEntry entry;
std::vector<std::string> fields =
absl::StrSplit(line, absl::ByChar(':'), absl::SkipEmpty());
std::vector<std::string> fields = absl::StrSplit(line, absl::ByChar(':'));
ASSIGN_OR_RETURN_ERRNO(entry.hierarchy, Atoi<uint32_t>(fields[0]));
entry.controllers = fields[1];
-2
View File
@@ -89,8 +89,6 @@ class Cgroup {
PosixErrorOr<absl::flat_hash_set<pid_t>> Tasks() const;
PosixErrorOr<absl::flat_hash_set<pid_t>> Threads() const;
// ContainsCallingProcess checks whether the calling process is part of the
// cgroup.
PosixError ContainsCallingProcess() const;