diff --git a/pkg/sentry/fsimpl/cgroupfs/BUILD b/pkg/sentry/fsimpl/cgroupfs/BUILD index fda2006ba..0e31bb719 100644 --- a/pkg/sentry/fsimpl/cgroupfs/BUILD +++ b/pkg/sentry/fsimpl/cgroupfs/BUILD @@ -40,6 +40,7 @@ go_library( "cpu.go", "cpuacct.go", "cpuset.go", + "devices.go", "dir_refs.go", "job.go", "memory.go", diff --git a/pkg/sentry/fsimpl/cgroupfs/cgroupfs.go b/pkg/sentry/fsimpl/cgroupfs/cgroupfs.go index e186c351d..3bce26e90 100644 --- a/pkg/sentry/fsimpl/cgroupfs/cgroupfs.go +++ b/pkg/sentry/fsimpl/cgroupfs/cgroupfs.go @@ -89,13 +89,14 @@ var allControllers = []kernel.CgroupControllerType{ kernel.CgroupControllerCPU, kernel.CgroupControllerCPUAcct, kernel.CgroupControllerCPUSet, + kernel.CgroupControllerDevices, kernel.CgroupControllerJob, kernel.CgroupControllerMemory, kernel.CgroupControllerPIDs, } // SupportedMountOptions is the set of supported mount options for cgroupfs. -var SupportedMountOptions = []string{"all", "cpu", "cpuacct", "cpuset", "job", "memory", "pids"} +var SupportedMountOptions = []string{"all", "cpu", "cpuacct", "cpuset", "devices", "job", "memory", "pids"} // FilesystemType implements vfs.FilesystemType. // @@ -227,6 +228,10 @@ func (fsType FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt delete(mopts, "cpuset") wantControllers = append(wantControllers, kernel.CgroupControllerCPUSet) } + if _, ok := mopts["devices"]; ok { + delete(mopts, "devices") + wantControllers = append(wantControllers, kernel.CgroupControllerDevices) + } if _, ok := mopts["job"]; ok { delete(mopts, "job") wantControllers = append(wantControllers, kernel.CgroupControllerJob) @@ -341,6 +346,8 @@ func (fsType FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt c = newCPUAcctController(fs) case kernel.CgroupControllerCPUSet: c = newCPUSetController(k, fs) + case kernel.CgroupControllerDevices: + c = newDevicesController(fs) case kernel.CgroupControllerJob: c = newJobController(fs) case kernel.CgroupControllerMemory: diff --git a/pkg/sentry/fsimpl/cgroupfs/devices.go b/pkg/sentry/fsimpl/cgroupfs/devices.go new file mode 100644 index 000000000..c91e52b4c --- /dev/null +++ b/pkg/sentry/fsimpl/cgroupfs/devices.go @@ -0,0 +1,163 @@ +// Copyright 2023 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 cgroupfs + +import ( + "bytes" + "fmt" + + "gvisor.dev/gvisor/pkg/context" + "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/sync" + "gvisor.dev/gvisor/pkg/usermem" +) + +const ( + allowedDevices = "devices.allow" + deniedDevices = "devices.deny" + wildcardDevice = 'a' +) + +// permission represents a device access, read, write, and mknod. +type permission string + +// +stateify savable +type deviceRule struct { + // Device type, when the type is all, the following fields are ignored. + controllerType rune + // The device's major number. + major *int64 + // The device's minor number. + minor *int64 + // Cgroup access permission. + access permission +} + +// +stateify savable +type devicesController struct { + controllerCommon + controllerStateless + controllerNoResource + + // mu protects the fields below. + mu sync.Mutex `state:"nosave"` + + // Allow or deny the device rules below. + allow bool + deviceRules []deviceRule +} + +// +stateify savable +type allowedDevicesData struct { + c *devicesController +} + +// Generate implements vfs.DynamicBytesSource.Generate. +func (d *allowedDevicesData) Generate(ctx context.Context, buf *bytes.Buffer) error { + return generate(ctx, buf, d.c, true) +} + +// Write implements vfs.WritableDynamicBytesSource.Write. +func (d *allowedDevicesData) Write(ctx context.Context, _ *vfs.FileDescription, src usermem.IOSequence, offset int64) (int64, error) { + return write(ctx, src, offset, d.c, true) +} + +// +stateify savable +type deniedDevicesData struct { + c *devicesController +} + +// Generate implements vfs.DynamicBytesSource.Generate. +func (d *deniedDevicesData) Generate(ctx context.Context, buf *bytes.Buffer) error { + return generate(ctx, buf, d.c, false) +} + +// Write implements vfs.WritableDynamicBytesSource.Write. +func (d *deniedDevicesData) Write(ctx context.Context, _ *vfs.FileDescription, src usermem.IOSequence, offset int64) (int64, error) { + return write(ctx, src, offset, d.c, true) +} + +func generate(ctx context.Context, buf *bytes.Buffer, c *devicesController, allow bool) error { + c.mu.Lock() + defer c.mu.Unlock() + if allow == c.allow && len(c.deviceRules) > 0 { + for i, rule := range c.deviceRules { + if rule.controllerType == wildcardDevice { + buf.Reset() + buf.WriteRune(wildcardDevice) + return nil + } + buf.WriteString(deviceRuleString(rule)) + if i < len(c.deviceRules)-1 { + buf.WriteRune(',') + } + } + } else { + buf.WriteString("") + } + return nil +} + +func write(ctx context.Context, src usermem.IOSequence, offset int64, c *devicesController, allow bool) (int64, error) { + // TODO(b/289099718): add functions to add and remove rules when writing to device controller data. + return 0, nil +} + +var _ controller = (*devicesController)(nil) + +func newDevicesController(fs *filesystem) *devicesController { + // The root device cgroup starts with rwm to all. + c := &devicesController{ + allow: true, + deviceRules: []deviceRule{}, + } + c.controllerCommon.init(kernel.CgroupControllerJob, fs) + return c +} + +// Clone implements controller.Clone. +func (c *devicesController) Clone() controller { + c.mu.Lock() + defer c.mu.Unlock() + newRules := make([]deviceRule, len(c.deviceRules)) + copy(newRules, c.deviceRules) + new := &devicesController{ + allow: c.allow, + deviceRules: newRules, + } + new.controllerCommon.cloneFromParent(c) + return new +} + +// AddControlFiles implements controller.AddControlFiles. +func (c *devicesController) AddControlFiles(ctx context.Context, creds *auth.Credentials, _ *cgroupInode, contents map[string]kernfs.Inode) { + contents[allowedDevices] = c.fs.newControllerWritableFile(ctx, creds, &allowedDevicesData{c: c}, true) + contents[deniedDevices] = c.fs.newControllerWritableFile(ctx, creds, &deniedDevicesData{c: c}, true) +} + +func deviceRuleString(rule deviceRule) string { + return fmt.Sprintf("%c %s:%s %s", rule.controllerType, deviceNumber(rule.major), deviceNumber(rule.minor), rule.access) +} + +// deviceNumber converts a device number to string. +func deviceNumber(number *int64) string { + if number == nil { + return "*" + } + return fmt.Sprint(number) +} diff --git a/pkg/sentry/kernel/cgroup.go b/pkg/sentry/kernel/cgroup.go index b7ab1963d..52f031d30 100644 --- a/pkg/sentry/kernel/cgroup.go +++ b/pkg/sentry/kernel/cgroup.go @@ -42,6 +42,7 @@ const ( CgroupControllerCPU = CgroupControllerType("cpu") CgroupControllerCPUAcct = CgroupControllerType("cpuacct") CgroupControllerCPUSet = CgroupControllerType("cpuset") + CgroupControllerDevices = CgroupControllerType("devices") CgroupControllerJob = CgroupControllerType("job") CgroupControllerMemory = CgroupControllerType("memory") CgroupControllerPIDs = CgroupControllerType("pids") @@ -56,6 +57,8 @@ func ParseCgroupController(val string) (CgroupControllerType, error) { return CgroupControllerCPUAcct, nil case "cpuset": return CgroupControllerCPUSet, nil + case "devices": + return CgroupControllerDevices, nil case "job": return CgroupControllerJob, nil case "memory": diff --git a/test/syscalls/linux/cgroup.cc b/test/syscalls/linux/cgroup.cc index 8bd51ad97..db7ee4084 100644 --- a/test/syscalls/linux/cgroup.cc +++ b/test/syscalls/linux/cgroup.cc @@ -1428,6 +1428,17 @@ TEST(PIDsCgroup, RaceFSDestructionChargeUncharge) { }); } +TEST(DevicesCgroup, ControlFilesExist) { + SKIP_IF(!CgroupsAvailable()); + + Mounter m(ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir())); + Cgroup c = ASSERT_NO_ERRNO_AND_VALUE(m.MountCgroupfs("devices")); + + // The root group starts with allowing rwm to all. + EXPECT_THAT(c.ReadControlFile("devices.allow"), IsPosixErrorOkAndHolds("")); + EXPECT_THAT(c.ReadControlFile("devices.deny"), IsPosixErrorOkAndHolds("")); +} + } // namespace } // namespace testing } // namespace gvisor