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

538 lines
16 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
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)
}
// Open implements kernfs.Inode.Open, overriding the DynamicBytesFile method
// to capture the opener's cgroup namespace.
func (f *cgroupInterfaceFile) 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
}
var ns *kernel.CgroupNamespace
if t := kernel.TaskFromContext(ctx); t != nil {
ns = t.GetCgroupNamespace() // ref taken
}
fd := &interfaceFD{ns: ns}
if err := fd.InitWithImpl(fd, rp.Mount(), d, data, f.Locks(), opts.Flags, rp.Credentials()); err != nil {
if ns != nil {
ns.DecRef(ctx)
}
return nil, err
}
return fd.VFSFileDescription(), nil
}
// interfaceFD implements vfs.FileDescriptionImpl for cgroup2 interface
// files. It is kernfs.DynamicBytesFD plus the opener's cgroup namespace.
//
// +stateify savable
type interfaceFD struct {
kernfs.DynamicBytesFD
// ns is the cgroup namespace of the task that opened this fd at open time.
ns *kernel.CgroupNamespace
}
// Release implements vfs.FileDescriptionImpl.Release.
func (fd *interfaceFD) Release(ctx context.Context) {
if fd.ns != nil {
fd.ns.DecRef(ctx)
}
fd.DynamicBytesFD.Release(ctx)
}
// +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 {
// The hosting cgroup.
c *cgroup
// The backing controller.
ctrl controller
// The backing writable source.
src vfs.WritableDynamicBytesSource
// Whether the source is delegatable to a child namespace.
nsDelegatable bool
}
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
}
if !s.nsDelegatable {
if err := s.c.checkNSDelegateWrite(ctx, fd); err != nil {
return 0, err
}
}
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, nsDelegatable: def.nsDelegatable}
} 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, nsDelegatable: true})
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, nsDelegatable: true})
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, nsDelegatable: true})
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, nsDelegatable: true})
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
}
var currPidns *kernel.PIDNamespace
if t := kernel.TaskFromContext(ctx); t != nil {
currPidns = t.PIDNamespace()
} else if k := kernel.KernelFromContext(ctx); k != nil {
currPidns = k.RootPIDNamespace()
}
pids := cf.c.getPIDsInNamespace(currPidns)
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
}
var nsRoot *cgroup
var creds *auth.Credentials
if fd != nil {
creds = fd.Credentials()
if ifd, ok := fd.Impl().(*interfaceFD); ok && ifd.ns != nil {
nsRoot = ifd.ns.Root().(*cgroup)
}
} else {
// If fd is nil, this write originates from outside the sandbox
// (e.g. via WriteControl or control RPCs) rather than through a
// sandboxed VFS file descriptor. Credentials come from ctx, and
// nsRoot remains nil so migration is unconfined by any cgroup
// namespace delegation boundary.
creds = auth.CredentialsFromContext(ctx)
}
if err := cf.c.attachProcess(ctx, creds, nsRoot, 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
}