Repository: opencontainers/runc
Pull request #4025 (2023-09-25 10:52:57)

[Carry 3962] Support `process.scheduler`
_(this is a carry of #3962)_

Spec: https://github.com/opencontainers/runtime-spec/pull/1188
Close: #3895 #3962 

lifubang: > @lifubang thanks for the carry! Left some comments, mostly minor.

Thanks, there are all resolved, PTAL.

Patch:
diff --git a/docs/spec-conformance.md b/docs/spec-conformance.md
index 5c04210a..8ec1e8c3 100644
--- a/docs/spec-conformance.md
+++ b/docs/spec-conformance.md
@@ -7,10 +7,11 @@ The following features are not implemented yet:
 
 Spec version | Feature                                  | PR
 -------------|------------------------------------------|----------------------------------------------------------
+v1.0.0       | `SCMP_ARCH_PARISC`                       | Unplanned, due to lack of users
+v1.0.0       | `SCMP_ARCH_PARISC64`                     | Unplanned, due to lack of users
 v1.0.2       | `.linux.personality`                     | [#3126](https://github.com/opencontainers/runc/pull/3126)
 v1.1.0       | `SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV` | [#3862](https://github.com/opencontainers/runc/pull/3862)
 v1.1.0       | rsvd hugetlb cgroup                      | TODO ([#3859](https://github.com/opencontainers/runc/issues/3859))
-v1.1.0       | `.process.scheduler`                     | TODO ([#3895](https://github.com/opencontainers/runc/issues/3895))
 v1.1.0       | `.process.ioPriority`                    | [#3783](https://github.com/opencontainers/runc/pull/3783)
 
 
@@ -19,20 +20,3 @@ Spec version | Feature                                  | Limitation
 -------------|------------------------------------------|----------------------------------------------------------
 v1.1.0       | `.[]mounts.uidMappings`                  | Requires using UserNS with identical uidMappings
 v1.1.0       | `.[]mounts.gidMappings`                  | Requires using UserNS with identical gidMappings
-
-## Architectures
-
-The following architectures are supported:
-
-runc binary  | seccomp
--------------|-------------------------------------------------------
-`amd64`      | `SCMP_ARCH_X86`, `SCMP_ARCH_X86_64`, `SCMP_ARCH_X32`
-`arm64`      | `SCMP_ARCH_ARM`, `SCMP_ARCH_AARCH64`
-`armel`      | `SCMP_ARCH_ARM`
-`armhf`      | `SCMP_ARCH_ARM`
-`ppc64le`    | `SCMP_ARCH_PPC64LE`
-`riscv64`    | `SCMP_ARCH_RISCV64`
-`s390x`      | `SCMP_ARCH_S390`, `SCMP_ARCH_S390X`
-
-The runc binary might be compilable for i386, big-endian PPC64,
-and several MIPS variants too, but these architectures are not officially supported.
diff --git a/libcontainer/configs/config.go b/libcontainer/configs/config.go
index 19541293..1ece49c3 100644
--- a/libcontainer/configs/config.go
+++ b/libcontainer/configs/config.go
@@ -8,6 +8,7 @@ import (
 	"time"
 
 	"github.com/sirupsen/logrus"
+	"golang.org/x/sys/unix"
 
 	"github.com/opencontainers/runc/libcontainer/devices"
 	"github.com/opencontainers/runtime-spec/specs-go"
@@ -219,6 +220,68 @@ type Config struct {
 
 	// TimeOffsets specifies the offset for supporting time namespaces.
 	TimeOffsets map[string]specs.LinuxTimeOffset `json:"time_offsets,omitempty"`
+
+	// Scheduler represents the scheduling attributes for a process.
+	Scheduler *Scheduler `json:"scheduler,omitempty"`
+}
+
+// Scheduler is based on the Linux sched_setattr(2) syscall.
+type Scheduler = specs.Scheduler
+
+// ToSchedAttr is to convert *configs.Scheduler to *unix.SchedAttr.
+func ToSchedAttr(scheduler *Scheduler) (*unix.SchedAttr, error) {
+	var policy uint32
+	switch scheduler.Policy {
+	case specs.SchedOther:
+		policy = 0
+	case specs.SchedFIFO:
+		policy = 1
+	case specs.SchedRR:
+		policy = 2
+	case specs.SchedBatch:
+		policy = 3
+	case specs.SchedISO:
+		policy = 4
+	case specs.SchedIdle:
+		policy = 5
+	case specs.SchedDeadline:
+		policy = 6
+	default:
+		return nil, fmt.Errorf("invalid scheduler policy: %s", scheduler.Policy)
+	}
+
+	var flags uint64
+	for _, flag := range scheduler.Flags {
+		switch flag {
+		case specs.SchedFlagResetOnFork:
+			flags |= 0x01
+		case specs.SchedFlagReclaim:
+			flags |= 0x02
+		case specs.SchedFlagDLOverrun:
+			flags |= 0x04
+		case specs.SchedFlagKeepPolicy:
+			flags |= 0x08
+		case specs.SchedFlagKeepParams:
+			flags |= 0x10
+		case specs.SchedFlagUtilClampMin:
+			flags |= 0x20
+		case specs.SchedFlagUtilClampMax:
+			flags |= 0x40
+		default:
+			return nil, fmt.Errorf("invalid scheduler flag: %s", flag)
+		}
+	}
+
+	return &unix.SchedAttr{
+		Size:     unix.SizeofSchedAttr,
+		Policy:   policy,
+		Flags:    flags,
+		Nice:     scheduler.Nice,
+		Priority: uint32(scheduler.Priority),
+		Runtime:  scheduler.Runtime,
+		Deadline: scheduler.Deadline,
+		Period:   scheduler.Period,
+	}, nil
 }
 
 type (
diff --git a/libcontainer/configs/validate/validator.go b/libcontainer/configs/validate/validator.go
index 11b80dda..a6736f26 100644
--- a/libcontainer/configs/validate/validator.go
+++ b/libcontainer/configs/validate/validator.go
@@ -11,6 +11,7 @@ import (
 	"github.com/opencontainers/runc/libcontainer/cgroups"
 	"github.com/opencontainers/runc/libcontainer/configs"
 	"github.com/opencontainers/runc/libcontainer/intelrdt"
+	"github.com/opencontainers/runtime-spec/specs-go"
 	selinux "github.com/opencontainers/selinux/go-selinux"
 	"github.com/sirupsen/logrus"
 	"golang.org/x/sys/unix"
@@ -30,6 +31,7 @@ func Validate(config *configs.Config) error {
 		intelrdtCheck,
 		rootlessEUIDCheck,
 		mountsStrict,
+		scheduler,
 	}
 	for _, c := range checks {
 		if err := c(config); err != nil {
@@ -353,3 +355,24 @@ func isHostNetNS(path string) (bool, error) {
 
 	return (st1.Dev == st2.Dev) && (st1.Ino == st2.Ino), nil
 }
+
+// scheduler is to validate scheduler configs according to https://man7.org/linux/man-pages/man2/sched_setattr.2.html
+func scheduler(config *configs.Config) error {
+	s := config.Scheduler
+	if s == nil {
+		return nil
+	}
+	if s.Policy == "" {
+		return errors.New("scheduler policy is required")
+	}
+	if s.Nice < -20 || s.Nice > 19 {
+		return fmt.Errorf("invalid scheduler.nice: %d", s.Nice)
+	}
+	if s.Priority != 0 && (s.Policy != specs.SchedFIFO && s.Policy != specs.SchedRR) {
+		return errors.New("scheduler.priority can only be specified for SchedFIFO or SchedRR policy")
+	}
+	if s.Policy != specs.SchedDeadline && (s.Runtime != 0 || s.Deadline != 0 || s.Period != 0) {
+		return errors.New("scheduler runtime/deadline/period can only be specified for SchedDeadline policy")
+	}
+	return nil
+}
diff --git a/libcontainer/console_linux.go b/libcontainer/console_linux.go
index e506853e..d2b9cf59 100644
--- a/libcontainer/console_linux.go
+++ b/libcontainer/console_linux.go
@@ -9,15 +9,13 @@ import (
 // mount initializes the console inside the rootfs mounting with the specified mount label
 // and applying the correct ownership of the console.
 func mountConsole(slavePath string) error {
+	oldMask := unix.Umask(0o000)
+	defer unix.Umask(oldMask)
 	f, err := os.Create("/dev/console")
 	if err != nil && !os.IsExist(err) {
 		return err
 	}
 	if f != nil {
-		// Ensure permission bits (can be different because of umask).
-		if err := f.Chmod(0o666); err != nil {
-			return err
-		}
 		f.Close()
 	}
 	return mount(slavePath, "/dev/console", "bind", unix.MS_BIND, "")
diff --git a/libcontainer/container_linux.go b/libcontainer/container_linux.go
index 35f4f5df..618128e2 100644
--- a/libcontainer/container_linux.go
+++ b/libcontainer/container_linux.go
@@ -36,7 +36,7 @@ const stdioFdCount = 3
 // Container is a libcontainer container object.
 type Container struct {
 	id                   string
-	stateDir             string
+	root                 string
 	config               *configs.Config
 	cgroupManager        cgroups.Manager
 	intelRdtManager      *intelrdt.Manager
@@ -242,7 +242,7 @@ func (c *Container) Exec() error {
 }
 
 func (c *Container) exec() error {
-	path := filepath.Join(c.stateDir, execFifoFilename)
+	path := filepath.Join(c.root, execFifoFilename)
 	pid := c.initProcess.pid()
 	blockingFifoOpenCh := awaitFifoOpen(path)
 	for {
@@ -409,22 +409,21 @@ func (c *Container) createExecFifo() error {
 		return err
 	}
 
-	fifoName := filepath.Join(c.stateDir, execFifoFilename)
+	fifoName := filepath.Join(c.root, execFifoFilename)
 	if _, err := os.Stat(fifoName); err == nil {
 		return fmt.Errorf("exec fifo %s already exists", fifoName)
 	}
+	oldMask := unix.Umask(0o000)
 	if err := unix.Mkfifo(fifoName, 0o622); err != nil {
-		return &os.PathError{Op: "mkfifo", Path: fifoName, Err: err}
-	}
-	// Ensure permission bits (can be different because of umask).
-	if err := os.Chmod(fifoName, 0o622); err != nil {
+		unix.Umask(oldMask)
 		return err
 	}
+	unix.Umask(oldMask)
 	return os.Chown(fifoName, rootuid, rootgid)
 }
 
 func (c *Container) deleteExecFifo() {
-	fifoName := filepath.Join(c.stateDir, execFifoFilename)
+	fifoName := filepath.Join(c.root, execFifoFilename)
 	os.Remove(fifoName)
 }
 
@@ -433,7 +432,7 @@ func (c *Container) deleteExecFifo() {
 // un-opened). It then adds the FifoFd to the given exec.Cmd as an inherited
 // fd, with _LIBCONTAINER_FIFOFD set to its fd number.
 func (c *Container) includeExecFifo(cmd *exec.Cmd) error {
-	fifoName := filepath.Join(c.stateDir, execFifoFilename)
+	fifoName := filepath.Join(c.root, execFifoFilename)
 	fifo, err := os.OpenFile(fifoName, unix.O_PATH|unix.O_CLOEXEC, 0)
 	if err != nil {
 		return err
@@ -512,7 +511,7 @@ func (c *Container) newParentProcess(p *Process) (parentProcess, error) {
 	} else {
 		var err error
 		if isDmzBinarySafe(c.config) {
-			dmzExe, err = dmz.Binary(c.stateDir)
+			dmzExe, err = dmz.Binary(c.root)
 			if err == nil {
 				// We can use our own executable without cloning if we are using
 				// runc-dmz.
@@ -531,7 +530,7 @@ func (c *Container) newParentProcess(p *Process) (parentProcess, error) {
 			err = dmz.ErrNoDmzBinary
 		}
 		if errors.Is(err, dmz.ErrNoDmzBinary) {
-			safeExe, err = dmz.CloneSelfExe(c.stateDir)
+			safeExe, err = dmz.CloneSelfExe(c.root)
 			if err != nil {
 				return nil, fmt.Errorf("unable to create safe /proc/self/exe clone for runc init: %w", err)
 			}
@@ -563,6 +562,7 @@ func (c *Container) newParentProcess(p *Process) (parentProcess, error) {
 			"_LIBCONTAINER_CONSOLE="+strconv.Itoa(stdioFdCount+len(cmd.ExtraFiles)-1),
 		)
 	}
+	cmd.Env = append(cmd.Env, "_LIBCONTAINER_STATEDIR="+c.root)
 
 	cmd.ExtraFiles = append(cmd.ExtraFiles, comm.initSockChild)
 	cmd.Env = append(cmd.Env,
@@ -951,7 +951,7 @@ func (c *Container) updateState(process parentProcess) (*State, error) {
 }
 
 func (c *Container) saveState(s *State) (retErr error) {
-	tmpFile, err := os.CreateTemp(c.stateDir, "state-")
+	tmpFile, err := os.CreateTemp(c.root, "state-")
 	if err != nil {
 		return err
 	}
@@ -972,7 +972,7 @@ func (c *Container) saveState(s *State) (retErr error) {
 		return err
 	}
 
-	stateFilePath := filepath.Join(c.stateDir, stateFilename)
+	stateFilePath := filepath.Join(c.root, stateFilename)
 	return os.Rename(tmpFile.Name(), stateFilePath)
 }
 
@@ -1019,7 +1019,7 @@ func (c *Container) runType() Status {
 	}
 	// We'll create exec fifo and blocking on it after container is created,
 	// and delete it after start container.
-	if _, err := os.Stat(filepath.Join(c.stateDir, execFifoFilename)); err == nil {
+	if _, err := os.Stat(filepath.Join(c.root, execFifoFilename)); err == nil {
 		return Created
 	}
 	return Running
diff --git a/libcontainer/criu_linux.go b/libcontainer/criu_linux.go
index 360639e7..03a86c30 100644
--- a/libcontainer/criu_linux.go
+++ b/libcontainer/criu_linux.go
@@ -662,7 +662,7 @@ func (c *Container) Restore(process *Process, criuOpts *CriuOpts) error {
 	// * its parent must not be overmounted
 	// c.config.Rootfs is bind-mounted to a temporary directory
 	// to satisfy these requirements.
-	root := filepath.Join(c.stateDir, "criu-root")
+	root := filepath.Join(c.root, "criu-root")
 	if err := os.Mkdir(root, 0o755); err != nil {
 		return err
 	}
@@ -1113,7 +1113,7 @@ func (c *Container) criuNotifications(resp *criurpc.CriuResp, process *Process,
 	logrus.Debugf("notify: %s\n", script)
 	switch script {
 	case "post-dump":
-		f, err := os.Create(filepath.Join(c.stateDir, "checkpoint"))
+		f, err := os.Create(filepath.Join(c.root, "checkpoint"))
 		if err != nil {
 			return err
 		}
@@ -1166,7 +1166,7 @@ func (c *Container) criuNotifications(resp *criurpc.CriuResp, process *Process,
 		if _, err := c.updateState(r); err != nil {
 			return err
 		}
-		if err := os.Remove(filepath.Join(c.stateDir, "checkpoint")); err != nil {
+		if err := os.Remove(filepath.Join(c.root, "checkpoint")); err != nil {
 			if !os.IsNotExist(err) {
 				logrus.Error(err)
 			}
diff --git a/libcontainer/factory_linux.go b/libcontainer/factory_linux.go
index b0bf6142..4cb4a88b 100644
--- a/libcontainer/factory_linux.go
+++ b/libcontainer/factory_linux.go
@@ -46,11 +46,11 @@ func Create(root, id string, config *configs.Config) (*Container, error) {
 	if err := os.MkdirAll(root, 0o700); err != nil {
 		return nil, err
 	}
-	stateDir, err := securejoin.SecureJoin(root, id)
+	containerRoot, err := securejoin.SecureJoin(root, id)
 	if err != nil {
 		return nil, err
 	}
-	if _, err := os.Stat(stateDir); err == nil {
+	if _, err := os.Stat(containerRoot); err == nil {
 		return nil, ErrExist
 	} else if !os.IsNotExist(err) {
 		return nil, err
@@ -89,12 +89,12 @@ func Create(root, id string, config *configs.Config) (*Container, error) {
 	}
 
 	// Parent directory is already created above, so Mkdir is enough.
-	if err := os.Mkdir(stateDir, 0o711); err != nil {
+	if err := os.Mkdir(containerRoot, 0o711); err != nil {
 		return nil, err
 	}
 	c := &Container{
 		id:              id,
-		stateDir:        stateDir,
+		root:            containerRoot,
 		config:          config,
 		cgroupManager:   cm,
 		intelRdtManager: intelrdt.NewManager(config, id, ""),
@@ -114,11 +114,11 @@ func Load(root, id string) (*Container, error) {
 	if err := validateID(id); err != nil {
 		return nil, err
 	}
-	stateDir, err := securejoin.SecureJoin(root, id)
+	containerRoot, err := securejoin.SecureJoin(root, id)
 	if err != nil {
 		return nil, err
 	}
-	state, err := loadState(stateDir)
+	state, err := loadState(containerRoot)
 	if err != nil {
 		return nil, err
 	}
@@ -138,7 +138,7 @@ func Load(root, id string) (*Container, error) {
 		config:               &state.Config,
 		cgroupManager:        cm,
 		intelRdtManager:      intelrdt.NewManager(&state.Config, id, state.IntelRdtPath),
-		stateDir:             stateDir,
+		root:                 containerRoot,
 		created:              state.Created,
 	}
 	c.state = &loadedState{c: c}
diff --git a/libcontainer/init_linux.go b/libcontainer/init_linux.go
index 0abbccf5..20587a10 100644
--- a/libcontainer/init_linux.go
+++ b/libcontainer/init_linux.go
@@ -640,6 +640,20 @@ func setupRlimits(limits []configs.Rlimit, pid int) error {
 	return nil
 }
 
+func setupScheduler(config *configs.Config) error {
+	attr, err := configs.ToSchedAttr(config.Scheduler)
+	if err != nil {
+		return err
+	}
+	if err := unix.SchedSetAttr(0, attr, 0); err != nil {
+		if errors.Is(err, unix.EPERM) && config.Cgroups.CpusetCpus != "" {
+			return errors.New("process scheduler can't be used together with AllowedCPUs")
+		}
+		return fmt.Errorf("error setting scheduler: %w", err)
+	}
+	return nil
+}
+
 // signalAllProcesses freezes then iterates over all the processes inside the
 // manager's cgroups sending the signal s to them.
 func signalAllProcesses(m cgroups.Manager, s unix.Signal) error {
diff --git a/libcontainer/process.go b/libcontainer/process.go
index d2c7bfcd..08c2396f 100644
--- a/libcontainer/process.go
+++ b/libcontainer/process.go
@@ -95,6 +95,8 @@ type Process struct {
 	//
 	// For cgroup v2, the only key allowed is "".
 	SubCgroupPaths map[string]string
+
+	Scheduler *configs.Scheduler
 }
 
 // Wait waits for the process to exit.
diff --git a/libcontainer/rootfs_linux.go b/libcontainer/rootfs_linux.go
index eafd6c82..d76dadcd 100644
--- a/libcontainer/rootfs_linux.go
+++ b/libcontainer/rootfs_linux.go
@@ -704,6 +704,7 @@ func reOpenDevNull() error {
 // Create the device nodes in the container.
 func createDevices(config *configs.Config) error {
 	useBindMount := userns.RunningInUserNS() || config.Namespaces.Contains(configs.NEWUSER)
+	oldMask := unix.Umask(0o000)
 	for _, node := range config.Devices {
 
 		// The /dev/ptmx device is setup by setupPtmx()
@@ -714,9 +715,11 @@ func createDevices(config *configs.Config) error {
 		// containers running in a user namespace are not allowed to mknod
 		// devices so we can just bind mount it from the host.
 		if err := createDeviceNode(config.Rootfs, node, useBindMount); err != nil {
+			unix.Umask(oldMask)
 			return err
 		}
 	}
+	unix.Umask(oldMask)
 	return nil
 }
 
@@ -779,10 +782,6 @@ func mknodDevice(dest string, node *devices.Device) error {
 	if err := unix.Mknod(dest, uint32(fileMode), int(dev)); err != nil {
 		return &os.PathError{Op: "mknod", Path: dest, Err: err}
 	}
-	// Ensure permission bits (can be different because of umask).
-	if err := os.Chmod(dest, fileMode); err != nil {
-		return err
-	}
 	return os.Chown(dest, int(node.Uid), int(node.Gid))
 }
 
diff --git a/libcontainer/setns_init_linux.go b/libcontainer/setns_init_linux.go
index 82bcfec2..f3edb710 100644
--- a/libcontainer/setns_init_linux.go
+++ b/libcontainer/setns_init_linux.go
@@ -65,6 +65,12 @@ func (l *linuxSetnsInit) Init() error {
 		unix.Umask(int(*l.config.Config.Umask))
 	}
 
+	if l.config.Config.Scheduler != nil {
+		if err := setupScheduler(l.config.Config); err != nil {
+			return err
+		}
+	}
+
 	if err := selinux.SetExecLabel(l.config.ProcessLabel); err != nil {
 		return err
 	}
diff --git a/libcontainer/specconv/spec_linux.go b/libcontainer/specconv/spec_linux.go
index cc4e3d25..c5553832 100644
--- a/libcontainer/specconv/spec_linux.go
+++ b/libcontainer/specconv/spec_linux.go
@@ -494,6 +494,10 @@ func CreateLibcontainerConfig(opts *CreateOpts) (*configs.Config, error) {
 				Ambient:     spec.Process.Capabilities.Ambient,
 			}
 		}
+		if spec.Process.Scheduler != nil {
+			s := *spec.Process.Scheduler
+			config.Scheduler = &s
+		}
 	}
 	createHooks(spec, config)
 	config.Version = specs.Version
diff --git a/libcontainer/standard_init_linux.go b/libcontainer/standard_init_linux.go
index 86750700..4fab50c0 100644
--- a/libcontainer/standard_init_linux.go
+++ b/libcontainer/standard_init_linux.go
@@ -159,6 +159,13 @@ func (l *linuxStandardInit) Init() error {
 			return &os.SyscallError{Syscall: "prctl(SET_NO_NEW_PRIVS)", Err: err}
 		}
 	}
+
+	if l.config.Config.Scheduler != nil {
+		if err := setupScheduler(l.config.Config); err != nil {
+			return err
+		}
+	}
+
 	// Tell our parent that we're ready to Execv. This must be done before the
 	// Seccomp rules have been applied, because we need to be able to read and
 	// write to a socket.
diff --git a/libcontainer/state_linux.go b/libcontainer/state_linux.go
index c6967e5f..8d8f31d3 100644
--- a/libcontainer/state_linux.go
+++ b/libcontainer/state_linux.go
@@ -41,7 +41,7 @@ func destroy(c *Container) error {
 			err = ierr
 		}
 	}
-	if rerr := os.RemoveAll(c.stateDir); err == nil {
+	if rerr := os.RemoveAll(c.root); err == nil {
 		err = rerr
 	}
 	c.initProcess = nil
@@ -200,7 +200,7 @@ func (r *restoredState) transition(s containerState) error {
 }
 
 func (r *restoredState) destroy() error {
-	if _, err := os.Stat(filepath.Join(r.c.stateDir, "checkpoint")); err != nil {
+	if _, err := os.Stat(filepath.Join(r.c.root, "checkpoint")); err != nil {
 		if !os.IsNotExist(err) {
 			return err
 		}
diff --git a/utils_linux.go b/utils_linux.go
index 0f787cb3..b5c855cd 100644
--- a/utils_linux.go
+++ b/utils_linux.go
@@ -61,6 +61,11 @@ func newProcess(p specs.Process) (*libcontainer.Process, error) {
 		lp.ConsoleHeight = uint16(p.ConsoleSize.Height)
 	}
 
+	if p.Scheduler != nil {
+		s := *p.Scheduler
+		lp.Scheduler = &s
+	}
+
 	if p.Capabilities != nil {
 		lp.Capabilities = &configs.Capabilities{}
 		lp.Capabilities.Bounding = p.Capabilities.Bounding


Test patch:
diff --git a/libcontainer/configs/validate/validator_test.go b/libcontainer/configs/validate/validator_test.go
index d2b3c70a..176527ec 100644
--- a/libcontainer/configs/validate/validator_test.go
+++ b/libcontainer/configs/validate/validator_test.go
@@ -616,3 +616,53 @@ func TestValidateIDMapMounts(t *testing.T) {
 		})
 	}
 }
+
+func TestValidateScheduler(t *testing.T) {
+	testCases := []struct {
+		isErr     bool
+		policy    string
+		niceValue int32
+		priority  int32
+		runtime   uint64
+		deadline  uint64
+		period    uint64
+	}{
+		{isErr: true, niceValue: 0},
+		{isErr: false, policy: "SCHED_OTHER", niceValue: 19},
+		{isErr: false, policy: "SCHED_OTHER", niceValue: -20},
+		{isErr: true, policy: "SCHED_OTHER", niceValue: 20},
+		{isErr: true, policy: "SCHED_OTHER", niceValue: -21},
+		{isErr: true, policy: "SCHED_OTHER", priority: 100},
+		{isErr: false, policy: "SCHED_FIFO", priority: 100},
+		{isErr: true, policy: "SCHED_FIFO", runtime: 20},
+		{isErr: true, policy: "SCHED_BATCH", deadline: 30},
+		{isErr: true, policy: "SCHED_IDLE", period: 40},
+		{isErr: true, policy: "SCHED_DEADLINE", priority: 100},
+		{isErr: false, policy: "SCHED_DEADLINE", runtime: 200},
+		{isErr: false, policy: "SCHED_DEADLINE", deadline: 300},
+		{isErr: false, policy: "SCHED_DEADLINE", period: 400},
+	}
+
+	for _, tc := range testCases {
+		scheduler := configs.Scheduler{
+			Policy:   specs.LinuxSchedulerPolicy(tc.policy),
+			Nice:     tc.niceValue,
+			Priority: tc.priority,
+			Runtime:  tc.runtime,
+			Deadline: tc.deadline,
+			Period:   tc.period,
+		}
+		config := &configs.Config{
+			Rootfs:    "/var",
+			Scheduler: &scheduler,
+		}
+
+		err := Validate(config)
+		if tc.isErr && err == nil {
+			t.Errorf("scheduler: %d, expected error, got nil", tc.niceValue)
+		}
+		if !tc.isErr && err != nil {
+			t.Errorf("scheduler: %d, expected nil, got error %v", tc.niceValue, err)
+		}
+	}
+}
diff --git a/libcontainer/container_linux_test.go b/libcontainer/container_linux_test.go
index 99908376..6551de80 100644
--- a/libcontainer/container_linux_test.go
+++ b/libcontainer/container_linux_test.go
@@ -233,8 +233,8 @@ func TestGetContainerStateAfterUpdate(t *testing.T) {
 	}
 
 	container := &Container{
-		stateDir: t.TempDir(),
-		id:       "myid",
+		root: t.TempDir(),
+		id:   "myid",
 		config: &configs.Config{
 			Namespaces: []configs.Namespace{
 				{Type: configs.NEWPID},
diff --git a/tests/integration/scheduler.bats b/tests/integration/scheduler.bats
new file mode 100644
index 00000000..c07b760d
--- /dev/null
+++ b/tests/integration/scheduler.bats
@@ -0,0 +1,34 @@
+#!/usr/bin/env bats
+
+load helpers
+
+function setup() {
+	requires root
+	setup_debian
+}
+
+function teardown() {
+	teardown_bundle
+}
+
+@test "scheduler is applied" {
+	update_config ' .process.scheduler = {"policy": "SCHED_DEADLINE", "nice": 19, "priority": 0, "runtime": 42000, "deadline": 1000000, "period": 1000000, }'
+
+	runc run -d --console-socket "$CONSOLE_SOCKET" test_scheduler
+	[ "$status" -eq 0 ]
+
+	runc exec test_scheduler chrt -p 1
+	[ "$status" -eq 0 ]
+
+	[[ "${lines[0]}" == *"scheduling policy: SCHED_DEADLINE" ]]
+	[[ "${lines[1]}" == *"priority: 0" ]]
+	[[ "${lines[2]}" == *"runtime/deadline/period parameters: 42000/1000000/1000000" ]]
+}
+
+@test "scheduler vs cpus" {
+	update_config ' .linux.resources.cpu.cpus = "0"
+		| .process.scheduler = {"policy": "SCHED_DEADLINE", "nice": 19, "runtime": 42000, "deadline": 1000000, "period": 1000000, }'
+
+	runc run -d --console-socket "$CONSOLE_SOCKET" test_scheduler
+	[ "$status" -eq 1 ]
+}