mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
When --mount-cgroup-v2 is enabled, a container whose spec includes a cgroup mount is now given its own cgroup "/<cid>" in the cgroup2 hierarchy, with its init process placed there. Cgroup namespaces from the spec are now honored, mirroring PID namespace policy: no entry means the root namespace, an entry with a path joins the container that declared the same path, and an entry without a path gets a new namespace rooted at the container's cgroup. Since a fresh cgroup2 mount is rooted at the mounting namespace's root, a private-namespace container's /sys/fs/cgroup shows only its own subtree, as on Linux, with no need for bind mounts. At boot the sentry now enables nsdelegate and delegates controllers to per-container cgroups via the root cgroup.subtree_control. Exec sessions join the container's cgroup and cgroup namespace, like runc exec. Per-container "runsc events" and read-control queries now resolve for any container with a cgroup mount. Note two differences from the existing v1 support. In v1 we bind mount: the per-container dir "/$cid" of each controller hierarchy appears as /sys/fs/cgroup/<controller>. With v2 no bind mounts are needed: a fresh cgroup2 mount is rooted at the mounting namespace's root, so honoring the spec's cgroupns request yields the scoped view naturally. (Binding a subtree a la v1 was not an option anyway: systemd explicitly rejects a sub-hierarchy mounted without a matching cgroup namespace.) Second, on container destroy we now make a best-effort, recursive attempt to remove the container's cgroup subtree; v1 leaves its per-container dirs behind. The root container is not special-cased. As with v1, where even the root container gets container-id-stamped cgroup dirs and the stats queries they enable, it gets its own "/$root_cid", and its cgroup namespace comes from the spec like everyone else's. Docker and Kubernetes both request a private cgroup namespace for every container on cgroup2 hosts, so in practice the root container sees its cgroup as a pleasing "/" rather than "/$root_cid"; a spec with no cgroupns entry (e.g. docker run --cgroupns=host) instead sees the full sandbox hierarchy, as on Linux. One consequence of the private default: in a multi-container sandbox the root container cannot see the other containers' cgroups (namespace roots are not hierarchical). I don't expect that to be problematic, and a spec without a cgroupns entry regains the full view if ever needed. PiperOrigin-RevId: 960153510
344 lines
10 KiB
Go
344 lines
10 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 kernel
|
|
|
|
import (
|
|
"math"
|
|
|
|
"gvisor.dev/gvisor/pkg/context"
|
|
"gvisor.dev/gvisor/pkg/ebpf"
|
|
"gvisor.dev/gvisor/pkg/errors/linuxerr"
|
|
"gvisor.dev/gvisor/pkg/sentry/fsimpl/kernfs"
|
|
"gvisor.dev/gvisor/pkg/sentry/fsimpl/nsfs"
|
|
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
|
|
"gvisor.dev/gvisor/pkg/sync"
|
|
)
|
|
|
|
// 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
|
|
)
|
|
|
|
// Cgroup2BPFProgram represents a single cgroup2 eBPF program.
|
|
//
|
|
// +stateify savable
|
|
type Cgroup2BPFProgram struct {
|
|
// Prog represents the program attached at this slot.
|
|
Prog *ebpf.Program
|
|
|
|
// Flags is the full set of per-program flags.
|
|
Flags uint32
|
|
}
|
|
|
|
// Cgroup2BPFAttachmentSlot stores a single slot where programs can be attached.
|
|
// There is one slot per attachment type.
|
|
//
|
|
// +stateify savable
|
|
type Cgroup2BPFAttachmentSlot struct {
|
|
// Progs represents the programs attached at this slot.
|
|
//
|
|
// Linux uses a linked list here, but since BPF_CGROUP_MAX_PROGS=64,
|
|
// the removal cost shouldn't be too bad.
|
|
Progs []*Cgroup2BPFProgram
|
|
|
|
// Flags stores the slot-wide attachment flags for this attachment slot.
|
|
Flags uint8
|
|
|
|
// Revision stores the current revision for this attachment slot.
|
|
//
|
|
// To match Linux, the revision reported to userspace is 1 larger than
|
|
// the revision stored here.
|
|
Revision uint64
|
|
}
|
|
|
|
// Cgroup2BPF stores eBPF programs associated with a cgroup2 node.
|
|
//
|
|
// +stateify savable
|
|
type Cgroup2BPF struct {
|
|
// Slots stores the programs slot for each cgroup attachment type.
|
|
Slots [ebpf.MAX_CGROUP_BPF_ATTACH_TYPE]Cgroup2BPFAttachmentSlot
|
|
}
|
|
|
|
// 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
|
|
|
|
// ReadControl implements background accessible reading for cgroup v2 control files.
|
|
ReadControl(ctx context.Context, name string) (string, error)
|
|
|
|
// WriteControl implements background accessible writing for cgroup v2 control files.
|
|
WriteControl(ctx context.Context, name string, val string) error
|
|
|
|
// PathFrom returns the path of the cgroup relative to nsRoot, following
|
|
// Linux's cgroup_path_ns() semantics: the result always starts with '/',
|
|
// and contains leading "/.." components if the cgroup is not a descendant
|
|
// of nsRoot. Used by procfs for cgroup namespace path virtualization.
|
|
PathFrom(nsRoot Cgroup2) 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. ns is
|
|
// the forking task's cgroup namespace.
|
|
CanCloneInto(ctx context.Context, creds *auth.Credentials, ns *CgroupNamespace) error
|
|
|
|
// KillSeq returns the kill sequence number of the cgroup.
|
|
// It helps prevent fork()s racing with cgroup.kill.
|
|
KillSeq() uint64
|
|
|
|
// Deleted returns true if the cgroup has been deleted.
|
|
Deleted() bool
|
|
|
|
// IfBPF runs f if the cgroup has eBPF filters present.
|
|
//
|
|
// f may access the eBPF structure as read-only.
|
|
IfBPF(f func(*Cgroup2BPF))
|
|
|
|
// WriteBPF initializes the cgroup's eBPF filter structure if it
|
|
// is not present, then runs f.
|
|
//
|
|
// f may access the eBPF structure for reading or writing, or its
|
|
// ancestors' eBPF structures for reading.
|
|
//
|
|
// If an eBPF structure was not present, and f returns an error,
|
|
// the initialization of the eBPF structure is not applied.
|
|
//
|
|
// WriteBPF returns the error returned by f.
|
|
WriteBPF(f func(*Cgroup2BPF, []*Cgroup2BPF) error) error
|
|
}
|
|
|
|
// 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
|
|
|
|
// FindCgroup returns the cgroup v2 node at the specified root-relative path.
|
|
FindCgroup(ctx context.Context, path string) (Cgroup2, error)
|
|
|
|
// 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. The
|
|
// cgroup path is expressed relative to the root of readerNS, the cgroup
|
|
// namespace of the task reading the entry. If readerNS is nil (e.g. for
|
|
// background contexts), the absolute path is used.
|
|
func (t *Task) GetCgroup2Entry(readerNS *CgroupNamespace) *TaskCgroupEntry {
|
|
var path string
|
|
if c := t.Cgroup2(); c != nil {
|
|
if readerNS != nil {
|
|
path = c.PathFrom(readerNS.Root())
|
|
} else {
|
|
path = c.Path()
|
|
}
|
|
if c.Deleted() {
|
|
path += " (deleted)"
|
|
}
|
|
}
|
|
if path == "" {
|
|
path = "/"
|
|
}
|
|
return &TaskCgroupEntry{
|
|
HierarchyID: 0,
|
|
Path: path,
|
|
}
|
|
}
|
|
|
|
// CgroupNamespace represents a cgroup namespace. A cgroup namespace
|
|
// virtualizes the view of a task's cgroups: paths in /proc/<pid>/cgroup are
|
|
// shown relative to the namespace root, and cgroup2 mounts created from
|
|
// within the namespace are rooted at the namespace root.
|
|
//
|
|
// +stateify savable
|
|
type CgroupNamespace struct {
|
|
// root is the cgroup2 node this namespace is rooted at. It was the
|
|
// creating task's cgroup at the time the namespace was created, and does
|
|
// not change even if that task subsequently migrates. Immutable.
|
|
root Cgroup2
|
|
|
|
// userns is the user namespace that owns this cgroup namespace. Immutable.
|
|
userns *auth.UserNamespace
|
|
|
|
// mu protects inode.
|
|
mu sync.Mutex `state:"nosave"`
|
|
|
|
// inode is the nsfs inode backing /proc/<pid>/ns/cgroup for this
|
|
// namespace. It also holds this namespace's reference count.
|
|
// +checklocks:mu
|
|
inode *nsfs.Inode
|
|
}
|
|
|
|
// newCgroupNamespace creates a new cgroup namespace rooted at root and owned
|
|
// by userns.
|
|
func newCgroupNamespace(root Cgroup2, userns *auth.UserNamespace) *CgroupNamespace {
|
|
return &CgroupNamespace{
|
|
root: root,
|
|
userns: userns,
|
|
}
|
|
}
|
|
|
|
// NewCgroupNamespace creates a new cgroup namespace rooted at root and owned
|
|
// by userns, along with its backing nsfs inode. The returned namespace holds
|
|
// one reference, owned by the caller.
|
|
func (k *Kernel) NewCgroupNamespace(ctx context.Context, root Cgroup2, userns *auth.UserNamespace) *CgroupNamespace {
|
|
ns := newCgroupNamespace(root, userns)
|
|
ns.SetInode(nsfs.NewInode(ctx, k.nsfsMount, ns))
|
|
return ns
|
|
}
|
|
|
|
// Root returns the cgroup2 node this namespace is rooted at.
|
|
func (ns *CgroupNamespace) Root() Cgroup2 {
|
|
return ns.root
|
|
}
|
|
|
|
// UserNamespace returns the user namespace that owns this cgroup namespace.
|
|
func (ns *CgroupNamespace) UserNamespace() *auth.UserNamespace {
|
|
return ns.userns
|
|
}
|
|
|
|
// Type implements vfs.Namespace.Type.
|
|
func (ns *CgroupNamespace) Type() string {
|
|
return "cgroup"
|
|
}
|
|
|
|
// Destroy implements vfs.Namespace.Destroy.
|
|
func (ns *CgroupNamespace) Destroy(ctx context.Context) {}
|
|
|
|
// SetInode sets the nsfs inode of the cgroup namespace.
|
|
func (ns *CgroupNamespace) SetInode(inode *nsfs.Inode) {
|
|
ns.mu.Lock()
|
|
defer ns.mu.Unlock()
|
|
ns.inode = inode
|
|
}
|
|
|
|
// GetInode returns the nsfs inode associated with the cgroup namespace.
|
|
func (ns *CgroupNamespace) GetInode() *nsfs.Inode {
|
|
ns.mu.Lock()
|
|
defer ns.mu.Unlock()
|
|
return ns.inode
|
|
}
|
|
|
|
// IncRef increments the namespace's reference count.
|
|
func (ns *CgroupNamespace) IncRef() {
|
|
ns.mu.Lock()
|
|
defer ns.mu.Unlock()
|
|
ns.inode.IncRef()
|
|
}
|
|
|
|
// DecRef decrements the namespace's reference count.
|
|
func (ns *CgroupNamespace) DecRef(ctx context.Context) {
|
|
ns.mu.Lock()
|
|
defer ns.mu.Unlock()
|
|
ns.inode.DecRef(ctx)
|
|
}
|
|
|
|
// CgroupNamespace returns the task's cgroup namespace.
|
|
func (t *Task) CgroupNamespace() *CgroupNamespace {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return t.cgroupns
|
|
}
|
|
|
|
// GetCgroupNamespace takes a reference on the task's cgroup namespace and
|
|
// returns it. It returns nil if the task has exited.
|
|
func (t *Task) GetCgroupNamespace() *CgroupNamespace {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
if t.cgroupns != nil {
|
|
t.cgroupns.IncRef()
|
|
}
|
|
return t.cgroupns
|
|
}
|