Files
Shailend ChandandgVisor bot 46e35291a0 Support writing to cgroup v2 control files without an open file descriptor
Fix a panic when writing to cgroup v2 control files from outside the sandbox
(e.g., via Cgroups.WriteControlFiles / WriteControl), where writes occur
without an open in-sandbox VFS file descriptor (fd == nil). cgroup.procs now
safely falls back to context credentials with unconfined namespace migration,
and checkNSDelegateWrite skips namespace delegation checks. Add an integration
test for runsc cgroup v2 control file writes.

Also:
- Export cgroup2fs.InitHierarchy to initialize sandbox-wide cgroup2 state
  and root controller enablement.
- Plumb CgroupNamespace through sentry lifecycle and task context to derive
  initial container cgroup2 placements.
PiperOrigin-RevId: 978087817
2026-09-08 13:36:37 -07:00

1225 lines
33 KiB
Go

// 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. It also governs eBPF programs
// attached to cgroups. The latter governs membership: the tasks
// associated with each cgroup.
import (
"bytes"
"fmt"
"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"
"gvisor.dev/gvisor/pkg/sentry/vfs/memxattr"
"gvisor.dev/gvisor/pkg/usermem"
)
// 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
// xattrs stores extended attributes on this cgroup directory.
xattrs memxattr.SimpleExtendedAttributes
// bpf contains eBPF programs associated with the cgroup.
// +checklocks:fs.treeMu
bpf *kernel.Cgroup2BPF
}
// +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) {
// c's own tasksCount just crossed the 0<->1 boundary (see callers). If
// c still has populated children, its populated() state did not change:
// notifying watchers or adjusting ancestor counters here would corrupt
// the accounting.
if c.nrPopulatedChildren.Load() > 0 {
return
}
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)
if newPids, ok := ctrl.(*pids); ok {
c.chargePidsForExistingTasksLocked(newPids)
}
} 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()
// Reparent the cpu controller's tasks and accumulated usage onto
// the parent so disabling +cpu does not discard them.
if oldCPU, ok := ctrl.(*cpu); ok {
c.reparentCPUOnDisableLocked(oldCPU)
}
}
}
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()
}
}
}
// chargePidsForExistingTasksLocked charges a newly instantiated pids
// controller for the tasks already present in c's subtree. Those tasks were
// charged to the controller's ancestors when they entered their cgroups, but
// predate this controller, so nothing else accounts for them.
// +checklocksread:c.fs.treeMu
func (c *cgroup) chargePidsForExistingTasksLocked(p *pids) {
c.fs.tasksMu.RLock()
defer c.fs.tasksMu.RUnlock()
n := c.tasksCount.Load()
c.walkSubtreeLocked(func(child *cgroup) bool {
n += child.tasksCount.Load()
return true
})
if n == 0 {
return
}
p.charge(n)
}
// 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. ns is the forking
// task's cgroup namespace.
// +checklocksread:c.fs.treeMu
func (c *cgroup) CanCloneInto(ctx context.Context, creds *auth.Credentials, ns *kernel.CgroupNamespace) 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
}
defer inode.DecRef(ctx)
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 {
var nsRoot *cgroup
if ns != nil {
nsRoot = ns.Root().(*cgroup)
}
return c.checkMigrationPermsLocked(ctx, creds, parentCg, nsRoot)
}
}
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)
inode.DecRef(ctx)
}
}
}
// Path returns the path of the cgroup (`without " (deleted)"`).
func (c *cgroup) Path() string {
return c.path
}
// PathFrom implements kernel.Cgroup2.PathFrom.
//
// It mirrors Linux's cgroup_path_ns(): the returned path is relative
// to nsRoot, always starts with '/', and contains one leading "/.." component
// per level separating nsRoot from the lowest common ancestor of the two
// cgroups.
//
// It relies only on immutable fields (parent, level, path), so it
// needs no locks.
func (c *cgroup) PathFrom(nsRoot kernel.Cgroup2) string {
root, ok := nsRoot.(*cgroup)
if !ok || root.fs != c.fs || root.parent == nil {
// Namespace rooted at the real root (or a foreign node, which
// shouldn't happen): the path is absolute.
return c.Path()
}
lca := lowestCommonAncestor(c, root)
var b strings.Builder
for i := 0; i < root.level-lca.level; i++ {
b.WriteString("/..")
}
if c != lca {
if lca.parent == nil {
b.WriteString(c.path)
} else {
b.WriteString(c.path[len(lca.path):])
}
}
if b.Len() == 0 {
b.WriteString("/")
}
return b.String()
}
// Deleted implements kernel.Cgroup2.Deleted.
func (c *cgroup) Deleted() bool {
return c.deleted.Load()
}
// IfBPF implements kernel.Cgroup2.IfBPF.
func (c *cgroup) IfBPF(f func(*kernel.Cgroup2BPF)) {
c.fs.treeMu.RLock()
defer c.fs.treeMu.RUnlock()
if c.bpf != nil {
f(c.bpf)
}
}
// WriteBPF implements kernel.Cgroup2.WriteBPF.
func (c *cgroup) WriteBPF(f func(*kernel.Cgroup2BPF, []*kernel.Cgroup2BPF) error) error {
c.fs.treeMu.Lock()
defer c.fs.treeMu.Unlock()
bpf := c.bpf
if bpf == nil {
bpf = &kernel.Cgroup2BPF{}
}
// Fetch the Cgroup2BPF structures for each ancestor.
ancestors := c.getAncestorsBPF()
err := f(bpf, ancestors)
if err == nil {
c.bpf = bpf
}
return err
}
// getAncestorsBPF returns the eBPF programs of each ancestor of c,
// starting with its immediate parent.
//
// +checklocksread:c.fs.treeMu
func (c *cgroup) getAncestorsBPF() []*kernel.Cgroup2BPF {
parents := make([]*kernel.Cgroup2BPF, 0, c.level)
for cur := c.parent; cur != nil; cur = cur.parent {
parents = append(parents, cur.bpf) // +checklocksforce: c.fs.treeMu is locked
}
return parents
}
// 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
}
}
}
// checkMigrationPermsLocked checks whether the caller may migrate a process
// from oldNode to c. nsRoot is the root cgroup of the calling task's cgroup
// namespace.
// +checklocksread:c.fs.treeMu
func (c *cgroup) checkMigrationPermsLocked(ctx context.Context, creds *auth.Credentials, oldNode, nsRoot *cgroup) error {
lca := lowestCommonAncestor(oldNode, c)
if lca == nil {
return nil
}
lcaProcs, err := lca.OrderedChildren.Lookup(ctx, "cgroup.procs")
if err != nil {
return err
}
defer lcaProcs.DecRef(ctx)
if err := lcaProcs.CheckPermissions(ctx, creds, vfs.MayWrite); err != nil {
return err
}
// If cgroup namespaces are delegation boundaries, both the source and
// destination cgroups must be reachable from the migrating task's cgroup
// namespace.
if c.fs.nsDelegate.Load() && nsRoot != nil {
if !oldNode.isDescendantOf(nsRoot) || !c.isDescendantOf(nsRoot) {
return linuxerr.ENOENT
}
}
return nil
}
// isDescendantOf returns true if c is a descendant of (or the same as) a.
// It relies only on immutable fields and needs no locks.
func (c *cgroup) isDescendantOf(ancestor *cgroup) bool {
if c == nil || ancestor == nil {
return false
}
for c != nil && c.level > ancestor.level {
c = c.parent
}
return c == ancestor
}
// checkNSDelegateWrite enforces the "nsdelegate" mount option: cgroup
// namespace roots are delegation boundaries, so writes from inside a
// non-init namespace to non-delegatable interface files of the namespace
// root cgroup are rejected.
func (c *cgroup) checkNSDelegateWrite(ctx context.Context, fd *vfs.FileDescription) error {
// If fd is nil (e.g. writes from outside the sandbox via WriteControl),
// there is no cgroup namespace delegation boundary to enforce.
if !c.fs.nsDelegate.Load() || fd == nil {
return nil
}
ifd, ok := fd.Impl().(*interfaceFD)
if !ok || ifd.ns == nil {
return nil
}
ns := ifd.ns
if k := kernel.KernelFromContext(ctx); k == nil || ns == k.RootCgroupNamespace() {
return nil
}
if ns.Root().(*cgroup) == c {
return linuxerr.EPERM
}
return nil
}
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.
// nsRoot is the root cgroup of the cgroupns of the task at the time of the
// opening of the cgroup.procs fd.
func (c *cgroup) attachProcess(ctx context.Context, creds *auth.Credentials, nsRoot *cgroup, 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, nsRoot); 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 {
if t == nil {
return nil
}
return c.getPIDsInNamespace(t.PIDNamespace())
}
// getPIDsInNamespace returns task IDs in currPidns for all tasks in c.
func (c *cgroup) getPIDsInNamespace(currPidns *kernel.PIDNamespace) []int {
if currPidns == nil {
return nil
}
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)
if newPids, ok := ctrl.(*pids); ok {
c.chargePidsForExistingTasksLocked(newPids)
}
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)
}
}
// ReadControl implements kernel.Cgroup2.ReadControl.
// It allows reading from control files from outside the sandbox.
func (c *cgroup) ReadControl(ctx context.Context, name string) (string, error) {
cfi, err := c.Lookup(ctx, name)
if err != nil {
return "", fmt.Errorf("no such control file")
}
defer cfi.DecRef(ctx)
dbf, ok := cfi.(*cgroupInterfaceFile)
var data vfs.DynamicBytesSource
if ok {
data, err = dbf.Data(ctx)
if err != nil {
return "", err
}
} else if ef, ok := cfi.(*eventFile); ok {
data, err = ef.Data(ctx)
if err != nil {
return "", err
}
} else {
return "", fmt.Errorf("no such control file")
}
var buf bytes.Buffer
if err := data.Generate(ctx, &buf); err != nil {
return "", err
}
return buf.String(), nil
}
// WriteControl implements kernel.Cgroup2.WriteControl.
// It allows writing to control files from outside the sandbox.
func (c *cgroup) WriteControl(ctx context.Context, name string, val string) error {
cfi, err := c.Lookup(ctx, name)
if err != nil {
return fmt.Errorf("no such control file")
}
defer cfi.DecRef(ctx)
dbf, ok := cfi.(*cgroupInterfaceFile)
if !ok {
return fmt.Errorf("control file not writable")
}
data, err := dbf.Data(ctx)
if err != nil {
return err
}
wdata, ok := data.(vfs.WritableDynamicBytesSource)
if !ok {
return fmt.Errorf("control file not writable")
}
ioSeq := usermem.BytesIOSequence([]byte(val))
n, err := wdata.Write(ctx, nil, ioSeq, 0)
if err != nil {
return err
}
if n != int64(len(val)) {
return fmt.Errorf("short write")
}
return nil
}