diff --git a/pkg/sentry/control/BUILD b/pkg/sentry/control/BUILD index d42c2a84b..e7abdb2f1 100644 --- a/pkg/sentry/control/BUILD +++ b/pkg/sentry/control/BUILD @@ -11,6 +11,7 @@ proto_library( go_library( name = "control", srcs = [ + "cgroups.go", "control.go", "events.go", "fs.go", diff --git a/pkg/sentry/control/cgroups.go b/pkg/sentry/control/cgroups.go new file mode 100644 index 000000000..6445dda99 --- /dev/null +++ b/pkg/sentry/control/cgroups.go @@ -0,0 +1,159 @@ +// Copyright 2022 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 control + +import ( + "fmt" + "strings" + + "gvisor.dev/gvisor/pkg/context" + "gvisor.dev/gvisor/pkg/sentry/kernel" +) + +// Cgroups contains the state for cgroupfs related control commands. +type Cgroups struct { + Kernel *kernel.Kernel +} + +func (c *Cgroups) findCgroup(ctx context.Context, file CgroupControlFile) (kernel.Cgroup, error) { + ctl, err := file.controller() + if err != nil { + return kernel.Cgroup{}, err + } + return c.Kernel.CgroupRegistry().FindCgroup(ctx, ctl, file.Path) +} + +// CgroupControlFile identifies a specific control file within a +// specific cgroup, for the hierarchy with a given controller. +type CgroupControlFile struct { + Controller string `json:"controller"` + Path string `json:"path"` + Name string `json:"name"` +} + +func (f *CgroupControlFile) controller() (kernel.CgroupControllerType, error) { + return kernel.ParseCgroupController(f.Controller) +} + +// CgroupsResult represents the result of a cgroup operation. +type CgroupsResult struct { + Data string `json:"value"` + IsError bool `json:"is_error"` +} + +// AsError interprets the result as an error. +func (r *CgroupsResult) AsError() error { + if r.IsError { + return fmt.Errorf(r.Data) + } + return nil +} + +// Unpack splits CgroupsResult into a (value, error) tuple. +func (r *CgroupsResult) Unpack() (string, error) { + if r.IsError { + return "", fmt.Errorf(r.Data) + } + return r.Data, nil +} + +func newValue(val string) CgroupsResult { + return CgroupsResult{ + Data: strings.TrimSpace(val), + } +} + +func newError(err error) CgroupsResult { + return CgroupsResult{ + Data: err.Error(), + IsError: true, + } +} + +// CgroupsResults represents the list of results for a batch command. +type CgroupsResults struct { + Results []CgroupsResult `json:"results"` +} + +func (o *CgroupsResults) appendValue(val string) { + o.Results = append(o.Results, newValue(val)) +} + +func (o *CgroupsResults) appendError(err error) { + o.Results = append(o.Results, newError(err)) +} + +// CgroupsReadArg represents the arguments for a single read command. +type CgroupsReadArg struct { + File CgroupControlFile `json:"file"` +} + +// CgroupsReadArgs represents the list of arguments for a batched read command. +type CgroupsReadArgs struct { + Args []CgroupsReadArg `json:"args"` +} + +// ReadControlFiles is an RPC stub for batch-reading cgroupfs control files. +func (c *Cgroups) ReadControlFiles(args *CgroupsReadArgs, out *CgroupsResults) error { + ctx := c.Kernel.SupervisorContext() + for _, arg := range args.Args { + cg, err := c.findCgroup(ctx, arg.File) + if err != nil { + out.appendError(err) + continue + } + + val, err := cg.ReadControl(ctx, arg.File.Name) + if err != nil { + out.appendError(err) + } else { + out.appendValue(val) + } + } + + return nil +} + +// CgroupsWriteArg represents the arguments for a single write command. +type CgroupsWriteArg struct { + File CgroupControlFile `json:"file"` + Value string `json:"value"` +} + +// CgroupsWriteArgs represents the lust of arguments for a batched write command. +type CgroupsWriteArgs struct { + Args []CgroupsWriteArg `json:"args"` +} + +// WriteControlFiles is an RPC stub for batch-writing cgroupfs control files. +func (c *Cgroups) WriteControlFiles(args *CgroupsWriteArgs, out *CgroupsResults) error { + ctx := c.Kernel.SupervisorContext() + + for _, arg := range args.Args { + cg, err := c.findCgroup(ctx, arg.File) + if err != nil { + out.appendError(err) + continue + } + + err = cg.WriteControl(ctx, arg.File.Name, arg.Value) + if err != nil { + out.appendError(err) + } else { + out.appendValue("") + } + } + return nil +} diff --git a/pkg/sentry/control/control.proto b/pkg/sentry/control/control.proto index 72dda3fbc..31009c38a 100644 --- a/pkg/sentry/control/control.proto +++ b/pkg/sentry/control/control.proto @@ -32,6 +32,7 @@ message ControlConfig { PROC = 7; STATE = 8; DEBUG = 9; + CGROUPS = 10; } // allowed_controls represents which endpoints may be registered to the diff --git a/pkg/sentry/fsimpl/cgroupfs/base.go b/pkg/sentry/fsimpl/cgroupfs/base.go index 753125400..30b4c5bfc 100644 --- a/pkg/sentry/fsimpl/cgroupfs/base.go +++ b/pkg/sentry/fsimpl/cgroupfs/base.go @@ -24,6 +24,7 @@ import ( "gvisor.dev/gvisor/pkg/abi/linux" "gvisor.dev/gvisor/pkg/context" "gvisor.dev/gvisor/pkg/errors/linuxerr" + "gvisor.dev/gvisor/pkg/hostarch" "gvisor.dev/gvisor/pkg/sentry/fsimpl/kernfs" "gvisor.dev/gvisor/pkg/sentry/kernel" "gvisor.dev/gvisor/pkg/sentry/kernel/auth" @@ -84,9 +85,9 @@ func (c *controllerCommon) Enabled() bool { return true } -// RootCgroup implements kernel.CgroupController.RootCgroup. -func (c *controllerCommon) RootCgroup() kernel.Cgroup { - return c.fs.rootCgroup() +// EffectiveRootCgroup implements kernel.CgroupController.EffectiveRootCgroup. +func (c *controllerCommon) EffectiveRootCgroup() kernel.Cgroup { + return c.fs.effectiveRootCgroup() } // controller is an interface for common functionality related to all cgroups. @@ -175,8 +176,8 @@ func (fs *filesystem) newCgroupInode(ctx context.Context, creds *auth.Credential c.dir.cgi = c contents := make(map[string]kernfs.Inode) - contents["cgroup.procs"] = fs.newControllerWritableFile(ctx, creds, &cgroupProcsData{c}) - contents["tasks"] = fs.newControllerWritableFile(ctx, creds, &tasksData{c}) + contents["cgroup.procs"] = fs.newControllerWritableFile(ctx, creds, &cgroupProcsData{c}, false) + contents["tasks"] = fs.newControllerWritableFile(ctx, creds, &tasksData{c}, false) if parent != nil { for ty, ctl := range parent.controllers { @@ -323,6 +324,64 @@ func (c *cgroupInode) Charge(t *kernel.Task, d *kernfs.Dentry, ctlType kernel.Cg return nil } +// ReadControl implements kernel.CgroupImpl.ReadControl. +func (c *cgroupInode) ReadControl(ctx context.Context, name string) (string, error) { + c.fs.tasksMu.RLock() + defer c.fs.tasksMu.RUnlock() + + cfi, err := c.Lookup(ctx, name) + if err != nil { + return "", fmt.Errorf("no such control file") + } + cbf, ok := cfi.(controllerFileImpl) + if !ok { + return "", fmt.Errorf("no such control file") + } + if !cbf.AllowBackgroundAccess() { + return "", fmt.Errorf("this control may not be accessed from a background context") + } + + var buf bytes.Buffer + err = cbf.Source().Data().Generate(ctx, &buf) + return buf.String(), err +} + +// WriteControl implements kernel.CgroupImpl.WriteControl. +func (c *cgroupInode) WriteControl(ctx context.Context, name string, value string) error { + c.fs.tasksMu.RLock() + defer c.fs.tasksMu.RUnlock() + + cfi, err := c.Lookup(ctx, name) + if err != nil { + return fmt.Errorf("no such control file") + } + // Do the more general cast first so we can give a meaningful error message when + // the control file exists, but isn't accessible (either due to being + // unwritable, or not being available from a background context). + cbf, ok := cfi.(controllerFileImpl) + if !ok { + return fmt.Errorf("no such control file") + } + if !cbf.AllowBackgroundAccess() { + return fmt.Errorf("this control may not be accessed from a background context") + } + wcbf, ok := cfi.(writableControllerFileImpl) + if !ok { + return fmt.Errorf("control file not writable") + } + + ioSeq := usermem.BytesIOSequence([]byte(value)) + n, err := wcbf.WriteBackground(ctx, ioSeq) + if err != nil { + return err + } + if n != int64(len(value)) { + return fmt.Errorf("short write") + } + + return nil +} + func sortTIDs(tids []kernel.ThreadID) { sort.Slice(tids, func(i, j int) bool { return tids[i] < tids[j] }) } @@ -434,9 +493,7 @@ func (d *tasksData) Write(ctx context.Context, fd *vfs.FileDescription, src user func parseInt64FromString(ctx context.Context, src usermem.IOSequence) (val, len int64, err error) { const maxInt64StrLen = 20 // i.e. len(fmt.Sprintf("%d", math.MinInt64)) == 20 - t := kernel.TaskFromContext(ctx) - - buf := t.CopyScratchBuffer(maxInt64StrLen) + buf := copyScratchBufferFromContext(ctx, maxInt64StrLen) n, err := src.CopyIn(ctx, buf) if err != nil { return 0, int64(n), err @@ -454,6 +511,18 @@ func parseInt64FromString(ctx context.Context, src usermem.IOSequence) (val, len return val, int64(n), nil } +// copyScratchBufferFromContext returns a scratch buffer of the given size. It +// tries to use the task's copy scratch buffer if we're on a task context, +// otherwise it allocates a new buffer. +func copyScratchBufferFromContext(ctx context.Context, size int) []byte { + t := kernel.TaskFromContext(ctx) + if t != nil { + return t.CopyScratchBuffer(hostarch.PageSize) + } + // Not on task context. + return make([]byte, hostarch.PageSize) +} + // controllerStateless partially implements controller. It stubs the migration // methods with noops for a stateless controller. type controllerStateless struct{} diff --git a/pkg/sentry/fsimpl/cgroupfs/cgroupfs.go b/pkg/sentry/fsimpl/cgroupfs/cgroupfs.go index 4cb27844d..a9e0791ec 100644 --- a/pkg/sentry/fsimpl/cgroupfs/cgroupfs.go +++ b/pkg/sentry/fsimpl/cgroupfs/cgroupfs.go @@ -180,6 +180,14 @@ func (fs *filesystem) InitializeHierarchyID(hid uint32) { fs.hierarchyID = hid } +// RootCgroup implements kernel.cgroupFS.RootCgroup. +func (fs *filesystem) RootCgroup() kernel.Cgroup { + return kernel.Cgroup{ + Dentry: fs.root, + CgroupImpl: fs.root.Inode().(kernel.CgroupImpl), + } +} + // Name implements vfs.FilesystemType.Name. func (FilesystemType) Name() string { return Name @@ -383,7 +391,7 @@ func (fsType FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt } // Move all existing tasks to the root of the new hierarchy. - k.PopulateNewCgroupHierarchy(fs.rootCgroup()) + k.PopulateNewCgroupHierarchy(fs.effectiveRootCgroup()) return fs.VFSFilesystem(), rootD.VFSDentry(), nil } @@ -441,7 +449,7 @@ func (fs *filesystem) prepareInitialCgroup(ctx context.Context, vfsObj *vfs.Virt return nil } -func (fs *filesystem) rootCgroup() kernel.Cgroup { +func (fs *filesystem) effectiveRootCgroup() kernel.Cgroup { return kernel.Cgroup{ Dentry: fs.effectiveRoot, CgroupImpl: fs.effectiveRoot.Inode().(kernel.CgroupImpl), @@ -633,12 +641,58 @@ func (d *dir) forEachChildDir(fn func(*dir)) { }) } +// controllerFileImpl represents common cgroupfs-specific operations for control +// files. +type controllerFileImpl interface { + // Source extracts the underlying DynamicBytesFile for a control file. + Source() *kernfs.DynamicBytesFile + // AllowBackgroundAccess indicates whether a control file can be accessed + // from a background (i.e. non-task) context. Some control files cannot be + // meaningfully accessed from a non-task context because accessing them + // either have side effects on the calling context (ex: task migration + // across cgroups), or they refer to data which must be interpreted within + // the calling context (ex: when referring to a pid, in which pid + // namespace?). + // + // Currently, all writable control files that allow access from a background + // process can handle a nil FD, since a background write doesn't explicitly + // open the control file. This is enforced through the + // writableControllerFileImpl. + AllowBackgroundAccess() bool +} + +// writableControllerFileImpl represents common cgroupfs-specific operations for +// a writable control file. +type writableControllerFileImpl interface { + controllerFileImpl + // WriteBackground writes data to a control file from a background + // context. This means the write isn't performed through and FD may be + // performed from a background context. + // + // Control files that support this should also return true for + // controllerFileImpl.AllowBackgroundAccess(). + WriteBackground(ctx context.Context, src usermem.IOSequence) (int64, error) +} + // controllerFile represents a generic control file that appears within a cgroup // directory. // // +stateify savable type controllerFile struct { kernfs.DynamicBytesFile + allowBackgroundAccess bool +} + +var _ controllerFileImpl = (*controllerFile)(nil) + +// Source implements controllerFileImpl.Source. +func (f *controllerFile) Source() *kernfs.DynamicBytesFile { + return &f.DynamicBytesFile +} + +// AllowBackgroundAccess implements controllerFileImpl.AllowBackgroundAccess. +func (f *controllerFile) AllowBackgroundAccess() bool { + return f.allowBackgroundAccess } // SetStat implements kernfs.Inode.SetStat. @@ -646,14 +700,18 @@ func (f *controllerFile) SetStat(ctx context.Context, fs *vfs.Filesystem, creds return f.InodeAttrs.SetStat(ctx, fs, creds, opts) } -func (fs *filesystem) newControllerFile(ctx context.Context, creds *auth.Credentials, data vfs.DynamicBytesSource) kernfs.Inode { - f := &controllerFile{} +func (fs *filesystem) newControllerFile(ctx context.Context, creds *auth.Credentials, data vfs.DynamicBytesSource, allowBackgroundAccess bool) kernfs.Inode { + f := &controllerFile{ + allowBackgroundAccess: allowBackgroundAccess, + } f.Init(ctx, creds, linux.UNNAMED_MAJOR, fs.devMinor, fs.NextIno(), data, readonlyFileMode) return f } -func (fs *filesystem) newControllerWritableFile(ctx context.Context, creds *auth.Credentials, data vfs.WritableDynamicBytesSource) kernfs.Inode { - f := &controllerFile{} +func (fs *filesystem) newControllerWritableFile(ctx context.Context, creds *auth.Credentials, data vfs.WritableDynamicBytesSource, allowBackgroundAccess bool) kernfs.Inode { + f := &controllerFile{ + allowBackgroundAccess: allowBackgroundAccess, + } f.Init(ctx, creds, linux.UNNAMED_MAJOR, fs.devMinor, fs.NextIno(), data, writableFileMode) return f } @@ -668,6 +726,18 @@ type staticControllerFile struct { vfs.StaticData } +var _ controllerFileImpl = (*staticControllerFile)(nil) + +// Source implements controllerFileImpl.Source. +func (f *staticControllerFile) Source() *kernfs.DynamicBytesFile { + return &f.DynamicBytesFile +} + +// AllowBackgroundAccess implements controllerFileImpl.AllowBackgroundAccess. +func (f *staticControllerFile) AllowBackgroundAccess() bool { + return true +} + // SetStat implements kernfs.Inode.SetStat. func (f *staticControllerFile) SetStat(ctx context.Context, fs *vfs.Filesystem, creds *auth.Credentials, opts vfs.SetStatOptions) error { return f.InodeAttrs.SetStat(ctx, fs, creds, opts) @@ -694,6 +764,8 @@ type stubControllerFile struct { data *atomicbitops.Int64 } +var _ controllerFileImpl = (*stubControllerFile)(nil) + // Generate implements vfs.DynamicBytesSource.Generate. func (f *stubControllerFile) Generate(ctx context.Context, buf *bytes.Buffer) error { fmt.Fprintf(buf, "%d\n", f.data.Load()) @@ -702,6 +774,11 @@ func (f *stubControllerFile) Generate(ctx context.Context, buf *bytes.Buffer) er // Write implements vfs.WritableDynamicBytesSource.Write. func (f *stubControllerFile) Write(ctx context.Context, _ *vfs.FileDescription, src usermem.IOSequence, offset int64) (int64, error) { + return f.WriteBackground(ctx, src) +} + +// WriteBackground implements writableControllerFileImpl.WriteBackground. +func (f *stubControllerFile) WriteBackground(ctx context.Context, src usermem.IOSequence) (int64, error) { val, n, err := parseInt64FromString(ctx, src) if err != nil { return 0, err @@ -712,8 +789,11 @@ func (f *stubControllerFile) Write(ctx context.Context, _ *vfs.FileDescription, // newStubControllerFile creates a new stub controller file that loads and // stores a control value from data. -func (fs *filesystem) newStubControllerFile(ctx context.Context, creds *auth.Credentials, data *atomicbitops.Int64) kernfs.Inode { +func (fs *filesystem) newStubControllerFile(ctx context.Context, creds *auth.Credentials, data *atomicbitops.Int64, allowBackgroundAccess bool) kernfs.Inode { f := &stubControllerFile{ + controllerFile: controllerFile{ + allowBackgroundAccess: allowBackgroundAccess, + }, data: data, } f.Init(ctx, creds, linux.UNNAMED_MAJOR, fs.devMinor, fs.NextIno(), f, writableFileMode) diff --git a/pkg/sentry/fsimpl/cgroupfs/cpu.go b/pkg/sentry/fsimpl/cgroupfs/cpu.go index ec27b92ac..3cdef3bac 100644 --- a/pkg/sentry/fsimpl/cgroupfs/cpu.go +++ b/pkg/sentry/fsimpl/cgroupfs/cpu.go @@ -76,7 +76,7 @@ func (c *cpuController) Clone() controller { // AddControlFiles implements controller.AddControlFiles. func (c *cpuController) AddControlFiles(ctx context.Context, creds *auth.Credentials, _ *cgroupInode, contents map[string]kernfs.Inode) { - contents["cpu.cfs_period_us"] = c.fs.newStubControllerFile(ctx, creds, &c.cfsPeriod) - contents["cpu.cfs_quota_us"] = c.fs.newStubControllerFile(ctx, creds, &c.cfsQuota) - contents["cpu.shares"] = c.fs.newStubControllerFile(ctx, creds, &c.shares) + contents["cpu.cfs_period_us"] = c.fs.newStubControllerFile(ctx, creds, &c.cfsPeriod, true) + contents["cpu.cfs_quota_us"] = c.fs.newStubControllerFile(ctx, creds, &c.cfsQuota, true) + contents["cpu.shares"] = c.fs.newStubControllerFile(ctx, creds, &c.shares, true) } diff --git a/pkg/sentry/fsimpl/cgroupfs/cpuacct.go b/pkg/sentry/fsimpl/cgroupfs/cpuacct.go index f36b68527..5d009f697 100644 --- a/pkg/sentry/fsimpl/cgroupfs/cpuacct.go +++ b/pkg/sentry/fsimpl/cgroupfs/cpuacct.go @@ -81,10 +81,10 @@ func (c *cpuacctController) Clone() controller { // AddControlFiles implements controller.AddControlFiles. func (c *cpuacctController) AddControlFiles(ctx context.Context, creds *auth.Credentials, cg *cgroupInode, contents map[string]kernfs.Inode) { cpuacctCG := &cpuacctCgroup{cg} - contents["cpuacct.stat"] = c.fs.newControllerFile(ctx, creds, &cpuacctStatData{cpuacctCG}) - contents["cpuacct.usage"] = c.fs.newControllerFile(ctx, creds, &cpuacctUsageData{cpuacctCG}) - contents["cpuacct.usage_user"] = c.fs.newControllerFile(ctx, creds, &cpuacctUsageUserData{cpuacctCG}) - contents["cpuacct.usage_sys"] = c.fs.newControllerFile(ctx, creds, &cpuacctUsageSysData{cpuacctCG}) + contents["cpuacct.stat"] = c.fs.newControllerFile(ctx, creds, &cpuacctStatData{cpuacctCG}, true) + contents["cpuacct.usage"] = c.fs.newControllerFile(ctx, creds, &cpuacctUsageData{cpuacctCG}, true) + contents["cpuacct.usage_user"] = c.fs.newControllerFile(ctx, creds, &cpuacctUsageUserData{cpuacctCG}, true) + contents["cpuacct.usage_sys"] = c.fs.newControllerFile(ctx, creds, &cpuacctUsageSysData{cpuacctCG}, true) } // Enter implements controller.Enter. diff --git a/pkg/sentry/fsimpl/cgroupfs/cpuset.go b/pkg/sentry/fsimpl/cgroupfs/cpuset.go index 86fa648f6..889aa1d8d 100644 --- a/pkg/sentry/fsimpl/cgroupfs/cpuset.go +++ b/pkg/sentry/fsimpl/cgroupfs/cpuset.go @@ -82,8 +82,8 @@ func (c *cpusetController) Clone() controller { // AddControlFiles implements controller.AddControlFiles. func (c *cpusetController) AddControlFiles(ctx context.Context, creds *auth.Credentials, _ *cgroupInode, contents map[string]kernfs.Inode) { - contents["cpuset.cpus"] = c.fs.newControllerWritableFile(ctx, creds, &cpusData{c: c}) - contents["cpuset.mems"] = c.fs.newControllerWritableFile(ctx, creds, &memsData{c: c}) + contents["cpuset.cpus"] = c.fs.newControllerWritableFile(ctx, creds, &cpusData{c: c}, true) + contents["cpuset.mems"] = c.fs.newControllerWritableFile(ctx, creds, &memsData{c: c}, true) } // +stateify savable @@ -101,12 +101,16 @@ func (d *cpusData) Generate(ctx context.Context, buf *bytes.Buffer) error { // Write implements vfs.WritableDynamicBytesSource.Write. func (d *cpusData) Write(ctx context.Context, _ *vfs.FileDescription, src usermem.IOSequence, offset int64) (int64, error) { + return d.WriteBackground(ctx, src) +} + +// WriteBackground implements writableControllerFileImpl.WriteBackground. +func (d *cpusData) WriteBackground(ctx context.Context, src usermem.IOSequence) (int64, error) { if src.NumBytes() > hostarch.PageSize { return 0, linuxerr.EINVAL } - t := kernel.TaskFromContext(ctx) - buf := t.CopyScratchBuffer(hostarch.PageSize) + buf := copyScratchBufferFromContext(ctx, hostarch.PageSize) n, err := src.CopyIn(ctx, buf) if err != nil { return 0, err @@ -145,12 +149,16 @@ func (d *memsData) Generate(ctx context.Context, buf *bytes.Buffer) error { // Write implements vfs.WritableDynamicBytesSource.Write. func (d *memsData) Write(ctx context.Context, _ *vfs.FileDescription, src usermem.IOSequence, offset int64) (int64, error) { + return d.WriteBackground(ctx, src) +} + +// WriteBackground implements writableControllerFileImpl.WriteBackground. +func (d *memsData) WriteBackground(ctx context.Context, src usermem.IOSequence) (int64, error) { if src.NumBytes() > hostarch.PageSize { return 0, linuxerr.EINVAL } - t := kernel.TaskFromContext(ctx) - buf := t.CopyScratchBuffer(hostarch.PageSize) + buf := copyScratchBufferFromContext(ctx, hostarch.PageSize) n, err := src.CopyIn(ctx, buf) if err != nil { return 0, err diff --git a/pkg/sentry/fsimpl/cgroupfs/job.go b/pkg/sentry/fsimpl/cgroupfs/job.go index 1de9a78f2..cae66bae4 100644 --- a/pkg/sentry/fsimpl/cgroupfs/job.go +++ b/pkg/sentry/fsimpl/cgroupfs/job.go @@ -49,5 +49,5 @@ func (c *jobController) Clone() controller { } func (c *jobController) AddControlFiles(ctx context.Context, creds *auth.Credentials, _ *cgroupInode, contents map[string]kernfs.Inode) { - contents["job.id"] = c.fs.newStubControllerFile(ctx, creds, &c.id) + contents["job.id"] = c.fs.newStubControllerFile(ctx, creds, &c.id, true) } diff --git a/pkg/sentry/fsimpl/cgroupfs/memory.go b/pkg/sentry/fsimpl/cgroupfs/memory.go index b2b140a9c..2b238837b 100644 --- a/pkg/sentry/fsimpl/cgroupfs/memory.go +++ b/pkg/sentry/fsimpl/cgroupfs/memory.go @@ -80,10 +80,10 @@ func (c *memoryController) Clone() controller { // AddControlFiles implements controller.AddControlFiles. func (c *memoryController) AddControlFiles(ctx context.Context, creds *auth.Credentials, _ *cgroupInode, contents map[string]kernfs.Inode) { - contents["memory.usage_in_bytes"] = c.fs.newControllerFile(ctx, creds, &memoryUsageInBytesData{}) - contents["memory.limit_in_bytes"] = c.fs.newStubControllerFile(ctx, creds, &c.limitBytes) - contents["memory.soft_limit_in_bytes"] = c.fs.newStubControllerFile(ctx, creds, &c.softLimitBytes) - contents["memory.move_charge_at_immigrate"] = c.fs.newStubControllerFile(ctx, creds, &c.moveChargeAtImmigrate) + contents["memory.usage_in_bytes"] = c.fs.newControllerFile(ctx, creds, &memoryUsageInBytesData{}, true) + contents["memory.limit_in_bytes"] = c.fs.newStubControllerFile(ctx, creds, &c.limitBytes, true) + contents["memory.soft_limit_in_bytes"] = c.fs.newStubControllerFile(ctx, creds, &c.softLimitBytes, true) + contents["memory.move_charge_at_immigrate"] = c.fs.newStubControllerFile(ctx, creds, &c.moveChargeAtImmigrate, true) contents["memory.pressure_level"] = c.fs.newStaticControllerFile(ctx, creds, linux.FileMode(0644), fmt.Sprintf("%d\n", c.pressureLevel)) } diff --git a/pkg/sentry/fsimpl/cgroupfs/pids.go b/pkg/sentry/fsimpl/cgroupfs/pids.go index 1a920a73c..5e1627e85 100644 --- a/pkg/sentry/fsimpl/cgroupfs/pids.go +++ b/pkg/sentry/fsimpl/cgroupfs/pids.go @@ -112,11 +112,11 @@ func (c *pidsController) Clone() controller { // AddControlFiles implements controller.AddControlFiles. func (c *pidsController) AddControlFiles(ctx context.Context, creds *auth.Credentials, _ *cgroupInode, contents map[string]kernfs.Inode) { - contents["pids.current"] = c.fs.newControllerFile(ctx, creds, &pidsCurrentData{c: c}) + contents["pids.current"] = c.fs.newControllerFile(ctx, creds, &pidsCurrentData{c: c}, true) if !c.isRoot { // "This is not available in the root cgroup for obvious reasons" -- // Linux, Documentation/cgroup-v1/pids.txt. - contents["pids.max"] = c.fs.newControllerWritableFile(ctx, creds, &pidsMaxData{c: c}) + contents["pids.max"] = c.fs.newControllerWritableFile(ctx, creds, &pidsMaxData{c: c}, true) } } @@ -268,8 +268,12 @@ func (d *pidsMaxData) Generate(ctx context.Context, buf *bytes.Buffer) error { // Write implements vfs.WritableDynamicBytesSource.Write. func (d *pidsMaxData) Write(ctx context.Context, _ *vfs.FileDescription, src usermem.IOSequence, offset int64) (int64, error) { - t := kernel.TaskFromContext(ctx) - buf := t.CopyScratchBuffer(hostarch.PageSize) + return d.WriteBackground(ctx, src) +} + +// WriteBackground implements writableControllerFileImpl.WriteBackground. +func (d *pidsMaxData) WriteBackground(ctx context.Context, src usermem.IOSequence) (int64, error) { + buf := copyScratchBufferFromContext(ctx, hostarch.PageSize) ncpy, err := src.CopyIn(ctx, buf) if err != nil { return 0, err diff --git a/pkg/sentry/fsimpl/kernfs/dynamic_bytes_file.go b/pkg/sentry/fsimpl/kernfs/dynamic_bytes_file.go index a60d589db..8fece48c4 100644 --- a/pkg/sentry/fsimpl/kernfs/dynamic_bytes_file.go +++ b/pkg/sentry/fsimpl/kernfs/dynamic_bytes_file.go @@ -44,7 +44,8 @@ type DynamicBytesFile struct { locks vfs.FileLocks // data can additionally implement vfs.WritableDynamicBytesSource to support - // writes. + // writes. This field cannot be changed to a different bytes source after + // Init. data vfs.DynamicBytesSource } @@ -80,6 +81,11 @@ func (f *DynamicBytesFile) Locks() *vfs.FileLocks { return &f.locks } +// Data returns the underlying data source. +func (f *DynamicBytesFile) Data() vfs.DynamicBytesSource { + return f.data +} + // DynamicBytesFD implements vfs.FileDescriptionImpl for an FD backed by a // DynamicBytesFile. // diff --git a/pkg/sentry/kernel/cgroup.go b/pkg/sentry/kernel/cgroup.go index 1a35c92d2..aefe0dae5 100644 --- a/pkg/sentry/kernel/cgroup.go +++ b/pkg/sentry/kernel/cgroup.go @@ -22,6 +22,7 @@ import ( "gvisor.dev/gvisor/pkg/atomicbitops" "gvisor.dev/gvisor/pkg/context" "gvisor.dev/gvisor/pkg/errors/linuxerr" + "gvisor.dev/gvisor/pkg/fspath" "gvisor.dev/gvisor/pkg/log" "gvisor.dev/gvisor/pkg/sentry/fsimpl/kernfs" "gvisor.dev/gvisor/pkg/sentry/vfs" @@ -43,6 +44,26 @@ const ( CgroupControllerPIDs = CgroupControllerType("pids") ) +// ParseCgroupController parses a string as a CgroupControllerType. +func ParseCgroupController(val string) (CgroupControllerType, error) { + switch val { + case "cpu": + return CgroupControllerCPU, nil + case "cpuacct": + return CgroupControllerCPUAcct, nil + case "cpuset": + return CgroupControllerCPUSet, nil + case "job": + return CgroupControllerJob, nil + case "memory": + return CgroupControllerMemory, nil + case "pids": + return CgroupControllerPIDs, nil + default: + return "", fmt.Errorf("no such cgroup controller") + } +} + // CgroupResourceType represents a resource type tracked by a particular // controller. type CgroupResourceType int @@ -69,9 +90,11 @@ type CgroupController interface { // attached to. Returned value is valid for the lifetime of the controller. HierarchyID() uint32 - // RootCgroup returns the root cgroup for this controller. Returned value is - // valid for the lifetime of the controller. - RootCgroup() Cgroup + // EffectiveRootCgroup returns the effective root cgroup for this + // controller. This is either the actual root of the underlying cgroupfs + // filesystem, or the override root configured at sandbox startup. Returned + // value is valid for the lifetime of the controller. + EffectiveRootCgroup() Cgroup // NumCgroups returns the number of cgroups managed by this controller. // Returned value is a snapshot in time. @@ -103,6 +126,18 @@ func (c *Cgroup) Path() string { return c.FSLocalPath() } +// Walk returns the cgroup at p, starting from c. +func (c *Cgroup) Walk(ctx context.Context, vfsObj *vfs.VirtualFilesystem, p fspath.Path) (Cgroup, error) { + d, err := c.Dentry.WalkDentryTree(ctx, vfsObj, p) + if err != nil { + return Cgroup{}, err + } + return Cgroup{ + Dentry: d, + CgroupImpl: d.Inode().(CgroupImpl), + }, nil +} + // CgroupMigrationContext represents an in-flight cgroup migration for // a single task. type CgroupMigrationContext struct { @@ -169,6 +204,14 @@ type CgroupImpl interface { // // See cgroupfs.controller.Charge. Charge(t *Task, d *kernfs.Dentry, ctl CgroupControllerType, res CgroupResourceType, value int64) error + + // ReadControlFromBackground allows a background context to read a cgroup's + // control values. + ReadControl(ctx context.Context, name string) (string, error) + + // WriteControl allows a background context to write a cgroup's control + // values. + WriteControl(ctx context.Context, name string, val string) error } // hierarchy represents a cgroupfs filesystem instance, with a unique set of @@ -210,6 +253,10 @@ type cgroupFS interface { // filesystem creation. May only be called before the filesystem is visible // to the vfs layer. InitializeHierarchyID(hid uint32) + + // RootCgroup returns the root cgroup of this instance. This returns the + // actual root, and ignores any overrides setting an effective root. + RootCgroup() Cgroup } // CgroupRegistry tracks the active set of cgroup controllers on the system. @@ -316,6 +363,34 @@ func (r *CgroupRegistry) FindHierarchy(name string, ctypes []CgroupControllerTyp return nil, nil } +// FindCgroup locates a cgroup with the given parameters. +// +// A cgroup is considered a match even if it contains other controllers on the +// same hierarchy. +func (r *CgroupRegistry) FindCgroup(ctx context.Context, ctype CgroupControllerType, path string) (Cgroup, error) { + p := fspath.Parse(path) + if !p.Absolute { + return Cgroup{}, fmt.Errorf("path must be absolute") + } + k := KernelFromContext(ctx) + vfsfs, err := r.FindHierarchy("", []CgroupControllerType{ctype}) + if err != nil { + return Cgroup{}, err + } + if vfsfs == nil { + return Cgroup{}, fmt.Errorf("controller not active") + } + + rootCG := vfsfs.Impl().(cgroupFS).RootCgroup() + + if !p.HasComponents() { + // Explicit root '/'. + return rootCG, nil + } + + return rootCG.Walk(ctx, k.VFS(), p) +} + // Register registers the provided set of controllers with the registry as a new // hierarchy. If any controller is already registered, the function returns an // error without modifying the registry. Register sets the hierarchy ID for the @@ -405,7 +480,7 @@ func (r *CgroupRegistry) computeInitialGroups(inherit map[Cgroup]struct{}) map[C // ... and add the root cgroups of all the missing controllers. for name, ctl := range r.controllers { if _, ok := ctlSet[name]; !ok { - cg := ctl.RootCgroup() + cg := ctl.EffectiveRootCgroup() // Multiple controllers may share the same hierarchy, so may have // the same root cgroup. Grab a single ref per hierarchy root. if _, ok := cgset[cg]; ok { diff --git a/runsc/boot/controller.go b/runsc/boot/controller.go index 3786aa7cf..11819403b 100644 --- a/runsc/boot/controller.go +++ b/runsc/boot/controller.go @@ -129,6 +129,12 @@ const ( UsageReduce = "Usage.Reduce" ) +// Commands for interacting with cgroupfs within the sandbox. +const ( + CgroupsReadControlFiles = "Cgroups.ReadControlFiles" + CgroupsWriteControlFiles = "Cgroups.WriteControlFiles" +) + // ControlSocketAddr generates an abstract unix socket name for the given ID. func ControlSocketAddr(id string) string { return fmt.Sprintf("\x00runsc-sandbox.%s", id) @@ -161,6 +167,7 @@ func newController(fd int, l *Loader) (*controller, error) { srv: srv, } ctrl.srv.Register(ctrl.manager) + ctrl.srv.Register(&control.Cgroups{Kernel: l.k}) ctrl.srv.Register(&control.Lifecycle{Kernel: l.k}) ctrl.srv.Register(&control.Logging{}) ctrl.srv.Register(&control.Proc{Kernel: l.k}) diff --git a/runsc/cli/main.go b/runsc/cli/main.go index 8f48d8ff6..af62a343e 100644 --- a/runsc/cli/main.go +++ b/runsc/cli/main.go @@ -95,6 +95,8 @@ func Main(version string) { subcommands.Register(new(cmd.Statefile), debugGroup) subcommands.Register(new(cmd.Symbolize), debugGroup) subcommands.Register(new(cmd.Usage), debugGroup) + subcommands.Register(new(cmd.ReadControl), debugGroup) + subcommands.Register(new(cmd.WriteControl), debugGroup) // Internal commands. const internalGroup = "internal use only" diff --git a/runsc/cmd/BUILD b/runsc/cmd/BUILD index 8a1ac21af..ddd637df0 100644 --- a/runsc/cmd/BUILD +++ b/runsc/cmd/BUILD @@ -27,6 +27,7 @@ go_library( "pause.go", "platforms.go", "ps.go", + "read_control.go", "restore.go", "resume.go", "run.go", @@ -38,6 +39,7 @@ go_library( "syscalls.go", "usage.go", "wait.go", + "write_control.go", ], visibility = [ "//runsc:__subpackages__", diff --git a/runsc/cmd/read_control.go b/runsc/cmd/read_control.go new file mode 100644 index 000000000..61cbe7abe --- /dev/null +++ b/runsc/cmd/read_control.go @@ -0,0 +1,85 @@ +// Copyright 2022 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 cmd + +import ( + "context" + "fmt" + + "github.com/google/subcommands" + "gvisor.dev/gvisor/pkg/sentry/control" + "gvisor.dev/gvisor/runsc/cmd/util" + "gvisor.dev/gvisor/runsc/config" + "gvisor.dev/gvisor/runsc/container" + "gvisor.dev/gvisor/runsc/flag" +) + +// ReadControl implements subcommands.Command for the "read-control" command. +type ReadControl struct{} + +// Name implements subcommands.Command.Name. +func (*ReadControl) Name() string { + return "read-control" +} + +// Synopsis implements subcommands.Command.Synopsis. +func (*ReadControl) Synopsis() string { + return "read a cgroups control value inside the container" +} + +// Usage implements subcommands.Command.Usage. +func (*ReadControl) Usage() string { + return `read-control + +Where "" is the name for the instance of the container, +"" is the name of an active cgroupv1 controller, is +the path to the cgroup to read and is the name of the +control file to read. + +EXAMPLE: + # runsc read-control cpuacct / cpuacct.usage +` +} + +// SetFlags implements subcommands.Command.SetFlags. +func (r *ReadControl) SetFlags(f *flag.FlagSet) {} + +// Execute implements subcommands.Command.Execute. +func (r *ReadControl) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus { + if f.NArg() < 4 { + f.Usage() + return subcommands.ExitUsageError + } + + id := f.Arg(0) + conf := args[0].(*config.Config) + + c, err := container.Load(conf.RootDir, container.FullID{ContainerID: id}, container.LoadOpts{}) + if err != nil { + util.Fatalf("loading sandbox: %v", err) + } + + out, err := c.Sandbox.CgroupsReadControlFile(control.CgroupControlFile{ + Controller: f.Arg(1), + Path: f.Arg(2), + Name: f.Arg(3), + }) + if err != nil { + fmt.Printf("ERROR: %s\n", err) + return subcommands.ExitFailure + } + fmt.Printf("%s\n", out) + return subcommands.ExitSuccess +} diff --git a/runsc/cmd/write_control.go b/runsc/cmd/write_control.go new file mode 100644 index 000000000..92ede65f0 --- /dev/null +++ b/runsc/cmd/write_control.go @@ -0,0 +1,84 @@ +// Copyright 2022 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 cmd + +import ( + "context" + "fmt" + + "github.com/google/subcommands" + "gvisor.dev/gvisor/pkg/sentry/control" + "gvisor.dev/gvisor/runsc/cmd/util" + "gvisor.dev/gvisor/runsc/config" + "gvisor.dev/gvisor/runsc/container" + "gvisor.dev/gvisor/runsc/flag" +) + +// WriteControl implements subcommands.Command for the "write-control" command. +type WriteControl struct{} + +// Name implements subcommands.Command.Name. +func (*WriteControl) Name() string { + return "write-control" +} + +// Synopsis implements subcommands.Command.Synopsis. +func (*WriteControl) Synopsis() string { + return "write a cgroups control value inside the container" +} + +// Usage implements subcommands.Command.Usage. +func (*WriteControl) Usage() string { + return `write-control + +Where "" is the name for the instance of the container, +"" is the name of an active cgroupv1 controller, is +the path to the cgroup to write and is the name of the +control file to write. + +EXAMPLE: + # runsc write-control memory / memory.limit_in_bytes 536870912 +` +} + +// SetFlags implements subcommands.Command.SetFlags. +func (r *WriteControl) SetFlags(f *flag.FlagSet) {} + +// Execute implements subcommands.Command.Execute. +func (r *WriteControl) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus { + if f.NArg() < 5 { + f.Usage() + return subcommands.ExitUsageError + } + + id := f.Arg(0) + conf := args[0].(*config.Config) + + c, err := container.Load(conf.RootDir, container.FullID{ContainerID: id}, container.LoadOpts{}) + if err != nil { + util.Fatalf("loading sandbox: %v", err) + } + + err = c.Sandbox.CgroupsWriteControlFile(control.CgroupControlFile{ + Controller: f.Arg(1), + Path: f.Arg(2), + Name: f.Arg(3), + }, f.Arg(4)) + if err != nil { + fmt.Printf("ERROR: %s\n", err) + return subcommands.ExitFailure + } + return subcommands.ExitSuccess +} diff --git a/runsc/sandbox/sandbox.go b/runsc/sandbox/sandbox.go index 2c0a6ae0d..f7156eac8 100644 --- a/runsc/sandbox/sandbox.go +++ b/runsc/sandbox/sandbox.go @@ -1422,3 +1422,58 @@ func checkBinaryPermissions(conf *config.Config) error { } return nil } + +// CgroupsReadControlFile reads a single cgroupfs control file in the sandbox. +func (s *Sandbox) CgroupsReadControlFile(file control.CgroupControlFile) (string, error) { + log.Debugf("CgroupsReadControlFiles sandbox %q", s.ID) + conn, err := s.sandboxConnect() + if err != nil { + return "", err + } + defer conn.Close() + + args := control.CgroupsReadArgs{ + Args: []control.CgroupsReadArg{ + { + File: file, + }, + }, + } + var out control.CgroupsResults + err = conn.Call(boot.CgroupsReadControlFiles, &args, &out) + if err != nil { + return "", err + } + if len(out.Results) != 1 { + return "", fmt.Errorf("expected 1 result, got %d, raw: %+v", len(out.Results), out) + } + return out.Results[0].Unpack() +} + +// CgroupsWriteControlFile writes a single cgroupfs control file in the sandbox. +func (s *Sandbox) CgroupsWriteControlFile(file control.CgroupControlFile, value string) error { + log.Debugf("CgroupsReadControlFiles sandbox %q", s.ID) + conn, err := s.sandboxConnect() + if err != nil { + return err + } + defer conn.Close() + + args := control.CgroupsWriteArgs{ + Args: []control.CgroupsWriteArg{ + { + File: file, + Value: value, + }, + }, + } + var out control.CgroupsResults + err = conn.Call(boot.CgroupsWriteControlFiles, &args, &out) + if err != nil { + return err + } + if len(out.Results) != 1 { + return fmt.Errorf("expected 1 result, got %d, raw: %+v", len(out.Results), out) + } + return out.Results[0].AsError() +}