Files
Shailend ChandandgVisor bot 4a2431a813 cgroup2fs: charge existing tasks on pids controller enablement
Enabling +pids in cgroup.subtree_control creates child pids controllers
with committed = 0, ignoring tasks already present in those cgroups.
pids.current then underreports, and migrating such a task away drives
it negative, since attach() applies deltas unguarded.

Charge a newly instantiated pids controller for the tasks already in
its subtree, both when a parent enables it (rebuildCtrlsLocked) and
when it is returned to the root (returnController). Adds a regression
test.

PiperOrigin-RevId: 974763124
2026-09-01 16:17:01 -07:00

306 lines
6.8 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/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.charge(change)
}
}
}
// charge adjusts the committed pid count by n and updates peak if increased.
func (p *pids) charge(n int64) {
p.mu.Lock()
defer p.mu.Unlock()
p.committed += n
if p.committed > p.peak {
p.peak = p.committed
}
}
// 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()
}