diff --git a/cmd/hakurei/command.go b/cmd/hakurei/command.go index 26c48d7..242e138 100644 --- a/cmd/hakurei/command.go +++ b/cmd/hakurei/command.go @@ -88,6 +88,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr flagGroups command.RepeatableFlag flagHomeDir string flagUserName string + flagSched string flagPrivateRuntime, flagPrivateTmpdir bool @@ -131,7 +132,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr log.Fatal(optionalErrorUnwrap(err)) return err } else if progPath, err = check.NewAbs(p); err != nil { - log.Fatal(err.Error()) + log.Fatal(err) return err } } @@ -150,7 +151,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr et |= hst.EPipeWire } - config := &hst.Config{ + config := hst.Config{ ID: flagID, Identity: flagIdentity, Groups: flagGroups, @@ -177,6 +178,10 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr }, } + if err := config.SchedPolicy.UnmarshalText([]byte(flagSched)); err != nil { + log.Fatal(err) + } + // bind GPU stuff if et&(hst.EX11|hst.EWayland) != 0 { config.Container.Filesystem = append(config.Container.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{ @@ -214,7 +219,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr homeDir = passwd.HomeDir } if a, err := check.NewAbs(homeDir); err != nil { - log.Fatal(err.Error()) + log.Fatal(err) return err } else { config.Container.Home = a @@ -234,11 +239,11 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr config.SessionBus = dbus.NewConfig(flagID, true, flagDBusMpris) } else { if f, err := os.Open(flagDBusConfigSession); err != nil { - log.Fatal(err.Error()) + log.Fatal(err) } else { decodeJSON(log.Fatal, "load session bus proxy config", f, &config.SessionBus) if err = f.Close(); err != nil { - log.Fatal(err.Error()) + log.Fatal(err) } } } @@ -246,11 +251,11 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr // system bus proxy is optional if flagDBusConfigSystem != "nil" { if f, err := os.Open(flagDBusConfigSystem); err != nil { - log.Fatal(err.Error()) + log.Fatal(err) } else { decodeJSON(log.Fatal, "load system bus proxy config", f, &config.SystemBus) if err = f.Close(); err != nil { - log.Fatal(err.Error()) + log.Fatal(err) } } } @@ -266,7 +271,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr } } - outcome.Main(ctx, msg, config, -1) + outcome.Main(ctx, msg, &config, -1) panic("unreachable") }). Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"), @@ -287,6 +292,8 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr "Container home directory"). Flag(&flagUserName, "u", command.StringFlag("chronos"), "Passwd user name within sandbox"). + Flag(&flagSched, "sched", command.StringFlag(""), + "Scheduling policy to set for the container"). Flag(&flagPrivateRuntime, "private-runtime", command.BoolFlag(false), "Do not share XDG_RUNTIME_DIR between containers under the same identity"). Flag(&flagPrivateTmpdir, "private-tmpdir", command.BoolFlag(false), diff --git a/cmd/hakurei/command_test.go b/cmd/hakurei/command_test.go index 4edf4a0..8b5ae63 100644 --- a/cmd/hakurei/command_test.go +++ b/cmd/hakurei/command_test.go @@ -36,7 +36,7 @@ Commands: }, { "run", []string{"run", "-h"}, ` -Usage: hakurei run [-h | --help] [--dbus-config ] [--dbus-system ] [--mpris] [--dbus-log] [--id ] [-a ] [-g ] [-d ] [-u ] [--private-runtime] [--private-tmpdir] [--wayland] [-X] [--dbus] [--pipewire] [--pulse] COMMAND [OPTIONS] +Usage: hakurei run [-h | --help] [--dbus-config ] [--dbus-system ] [--mpris] [--dbus-log] [--id ] [-a ] [-g ] [-d ] [-u ] [--sched ] [--private-runtime] [--private-tmpdir] [--wayland] [-X] [--dbus] [--pipewire] [--pulse] COMMAND [OPTIONS] Flags: -X Enable direct connection to X11 @@ -66,6 +66,8 @@ Flags: Do not share TMPDIR between containers under the same identity -pulse Enable PulseAudio compatibility daemon + -sched string + Scheduling policy to set for the container -u string Passwd user name within sandbox (default "chronos") -wayland diff --git a/hst/config.go b/hst/config.go index 51ac62e..5c02063 100644 --- a/hst/config.go +++ b/hst/config.go @@ -6,6 +6,7 @@ import ( "strings" "hakurei.app/container/check" + "hakurei.app/container/std" ) // Config configures an application container. @@ -103,6 +104,10 @@ type Config struct { // Init user namespace supplementary groups inherited by all container processes. Groups []string `json:"groups"` + // Scheduling policy to set for the container. The zero value retains the + // current scheduling policy. + SchedPolicy std.SchedPolicy `json:"sched_policy,omitempty"` + // High level configuration applied to the underlying [container]. Container *ContainerConfig `json:"container"` } @@ -116,6 +121,10 @@ var ( // [Config.Identity] value. ErrIdentityBounds = errors.New("identity out of bounds") + // ErrSchedPolicyBounds is returned by [Config.Validate] for an out of bounds + // [Config.SchedPolicy] value. + ErrSchedPolicyBounds = errors.New("scheduling policy out of bounds") + // ErrEnviron is returned by [Config.Validate] if an environment variable // name contains '=' or NUL. ErrEnviron = errors.New("invalid environment variable name") @@ -138,6 +147,13 @@ func (config *Config) Validate() error { Msg: "identity " + strconv.Itoa(config.Identity) + " out of range"} } + if config.SchedPolicy < 0 || config.SchedPolicy > std.SCHED_LAST { + return &AppError{Step: "validate configuration", Err: ErrSchedPolicyBounds, + Msg: "scheduling policy " + + strconv.Itoa(int(config.SchedPolicy)) + + " out of range"} + } + if err := config.SessionBus.CheckInterfaces("session"); err != nil { return err } diff --git a/hst/config_test.go b/hst/config_test.go index 776c21e..ac50f12 100644 --- a/hst/config_test.go +++ b/hst/config_test.go @@ -22,6 +22,10 @@ func TestConfigValidate(t *testing.T) { Msg: "identity -1 out of range"}}, {"identity upper", &hst.Config{Identity: 10000}, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds, Msg: "identity 10000 out of range"}}, + {"sched lower", &hst.Config{SchedPolicy: -1}, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds, + Msg: "scheduling policy -1 out of range"}}, + {"sched upper", &hst.Config{SchedPolicy: 0xcafe}, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds, + Msg: "scheduling policy 51966 out of range"}}, {"dbus session", &hst.Config{SessionBus: &hst.BusConfig{See: []string{""}}}, &hst.BadInterfaceError{Interface: "", Segment: "session"}}, {"dbus system", &hst.Config{SystemBus: &hst.BusConfig{See: []string{""}}}, diff --git a/internal/outcome/outcome.go b/internal/outcome/outcome.go index 42b5038..cb86b39 100644 --- a/internal/outcome/outcome.go +++ b/internal/outcome/outcome.go @@ -99,12 +99,17 @@ func newOutcomeState(k syscallDispatcher, msg message.Msg, id *hst.ID, config *h Shim: &shimParams{ PrivPID: k.getpid(), Verbose: msg.IsVerbose(), + + SchedPolicy: config.SchedPolicy, }, - ID: id, - Identity: config.Identity, - UserID: hsu.MustID(msg), - Paths: env.CopyPathsFunc(k.fatalf, k.tempdir, func(key string) string { v, _ := k.lookupEnv(key); return v }), + ID: id, + Identity: config.Identity, + UserID: hsu.MustID(msg), + Paths: env.CopyPathsFunc(k.fatalf, k.tempdir, func(key string) string { + v, _ := k.lookupEnv(key) + return v + }), Container: config.Container, } diff --git a/internal/outcome/shim.go b/internal/outcome/shim.go index 8fc46f2..53b3731 100644 --- a/internal/outcome/shim.go +++ b/internal/outcome/shim.go @@ -274,6 +274,7 @@ func shimEntrypoint(k syscallDispatcher) { cancelContainer.Store(&stop) sp := shimPrivate{k: k, id: state.id} z := container.New(ctx, msg) + z.SchedPolicy = state.Shim.SchedPolicy z.Params = *stateParams.params z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr diff --git a/nixos.nix b/nixos.nix index 800c81e..926a1d2 100644 --- a/nixos.nix +++ b/nixos.nix @@ -139,6 +139,7 @@ in inherit (app) identity groups enablements; inherit (dbusConfig) session_bus system_bus; direct_wayland = app.insecureWayland; + sched_policy = app.schedPolicy; container = { inherit (app) diff --git a/options.nix b/options.nix index 581ce18..ffce4dd 100644 --- a/options.nix +++ b/options.nix @@ -98,6 +98,7 @@ in ints str bool + enum package anything submodule @@ -237,6 +238,22 @@ in }; hostAbstract = mkEnableOption "share abstract unix socket scope"; + schedPolicy = mkOption { + type = nullOr (enum [ + "fifo" + "rr" + "batch" + "idle" + "deadline" + "ext" + ]); + default = null; + description = '' + Scheduling policy to set for the container. + The zero value retains the current scheduling policy. + ''; + }; + nix = mkEnableOption "nix daemon access"; mapRealUid = mkEnableOption "mapping to priv-user uid"; device = mkEnableOption "access to all devices"; diff --git a/test/configuration.nix b/test/configuration.nix index 5724ec4..62d8239 100644 --- a/test/configuration.nix +++ b/test/configuration.nix @@ -28,6 +28,15 @@ # Automatically login on tty1 as a normal user: services.getty.autologinUser = "alice"; + security.pam.loginLimits = [ + { + domain = "@users"; + item = "rtprio"; + type = "-"; + value = 1; + } + ]; + environment = { systemPackages = with pkgs; [ # For D-Bus tests: diff --git a/test/interactive/configuration.nix b/test/interactive/configuration.nix index 49c9034..9d1bddd 100644 --- a/test/interactive/configuration.nix +++ b/test/interactive/configuration.nix @@ -23,6 +23,14 @@ security = { sudo.wheelNeedsPassword = false; rtkit.enable = true; + pam.loginLimits = [ + { + domain = "@users"; + item = "rtprio"; + type = "-"; + value = 1; + } + ]; }; services = { diff --git a/test/test.py b/test/test.py index 485e475..9071f56 100644 --- a/test/test.py +++ b/test/test.py @@ -206,6 +206,14 @@ machine.wait_until_fails("pgrep foot", timeout=5) machine.wait_for_file("/tmp/shim-cont-unexpected-pid") print(machine.succeed('grep "shim: got SIGCONT from unexpected process$" /tmp/shim-cont-unexpected-pid')) +# Check setscheduler: +sched_unset = int(machine.succeed("sudo -u alice -i hakurei -v run cat /proc/self/sched | grep '^policy' | tr -d ' ' | cut -d ':' -f 2")) +if sched_unset != 0: + raise Exception(f"unexpected unset policy: {sched_unset}") +sched_idle = int(machine.succeed("sudo -u alice -i hakurei -v run --sched=idle cat /proc/self/sched | grep '^policy' | tr -d ' ' | cut -d ':' -f 2")) +if sched_idle != 5: + raise Exception(f"unexpected idle policy: {sched_idle}") + # Start app (foot) with Wayland enablement: swaymsg("exec ne-foot") wait_for_window(f"u0_a{hakurei_identity(0)}@machine")