aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-03-11 21:06:44 +0900
committerOphestra <cat@gensokyo.uk>2026-03-12 00:52:18 +0900
commit04e6bc3c5c99d6a99f17cfa5f69f34fe8941aee2 (patch)
tree2ad64307c13e2f967fdf17c5f02e7682189b22e1
parent5c540f90aa5ac069a370cbef0ddd81fa268b43c5 (diff)
hst: expose scheduling policy
This is primarily useful for poorly written music players for now. Signed-off-by: Ophestra <cat@gensokyo.uk>
-rw-r--r--cmd/hakurei/command.go23
-rw-r--r--cmd/hakurei/command_test.go4
-rw-r--r--hst/config.go16
-rw-r--r--hst/config_test.go4
-rw-r--r--internal/outcome/outcome.go13
-rw-r--r--internal/outcome/shim.go1
-rw-r--r--nixos.nix1
-rw-r--r--options.nix17
-rw-r--r--test/configuration.nix9
-rw-r--r--test/interactive/configuration.nix8
-rw-r--r--test/test.py8
11 files changed, 91 insertions, 13 deletions
diff --git a/cmd/hakurei/command.go b/cmd/hakurei/command.go
index 26c48d78..242e1389 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 4edf4a0b..8b5ae638 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 <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--private-runtime] [--private-tmpdir] [--wayland] [-X] [--dbus] [--pipewire] [--pulse] COMMAND [OPTIONS]
+Usage: hakurei run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--sched <value>] [--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 51ac62ec..5c020636 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 776c21e9..ac50f127 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 42b50389..cb86b390 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 8fc46f2a..53b37310 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 800c81e3..926a1d2a 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 581ce181..ffce4dd0 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 5724ec4f..62d8239d 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 49c90343..9d1bddd8 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 485e475f..9071f563 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")