treewide: include PipeWire op and enforce PulseAudio check
Some checks failed
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Failing after 2m44s
Test / Sandbox (race detector) (push) Failing after 2m49s
Test / Hakurei (push) Successful in 4m18s
Test / Hpkg (push) Successful in 4m18s
Test / Hakurei (race detector) (push) Successful in 4m25s
Test / Flake checks (push) Has been skipped

This fully replaces PulseAudio with PipeWire and enforces the PulseAudio check and error message. The pipewire-pulse daemon is handled in the NixOS module.

Closes #26.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
2025-12-08 08:03:50 +09:00
parent 0c38fb7b6a
commit 96395d4043
18 changed files with 99 additions and 94 deletions

View File

@@ -14,6 +14,7 @@ import (
_ "unsafe" // for go:linkname _ "unsafe" // for go:linkname
"hakurei.app/command" "hakurei.app/command"
"hakurei.app/container"
"hakurei.app/container/check" "hakurei.app/container/check"
"hakurei.app/container/fhs" "hakurei.app/container/fhs"
"hakurei.app/hst" "hakurei.app/hst"
@@ -149,9 +150,6 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
if flagPipeWire || flagPulse { if flagPipeWire || flagPulse {
et |= hst.EPipeWire et |= hst.EPipeWire
} }
if flagPulse {
et |= hst.EPulse
}
config := &hst.Config{ config := &hst.Config{
ID: flagID, ID: flagID,
@@ -189,6 +187,14 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
}}) }})
} }
// start pipewire-pulse: this most likely exists on host if PipeWire is available
if flagPulse {
config.Container.Filesystem = append(config.Container.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSDaemon{
Target: fhs.AbsRunUser.Append(strconv.Itoa(container.OverflowUid(msg)), "pulse/native"),
Exec: shell, Args: []string{"-lc", "pipewire-pulse"},
}})
}
config.Container.Filesystem = append(config.Container.Filesystem, config.Container.Filesystem = append(config.Container.Filesystem,
// opportunistically bind kvm // opportunistically bind kvm
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{ hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{

View File

@@ -62,7 +62,7 @@ func TestPrintShowInstance(t *testing.T) {
{"nil", nil, nil, false, false, "Error: invalid configuration!\n\n", false}, {"nil", nil, nil, false, false, "Error: invalid configuration!\n\n", false},
{"config", nil, hst.Template(), false, false, `App {"config", nil, hst.Template(), false, false, `App
Identity: 9 (org.chromium.Chromium) Identity: 9 (org.chromium.Chromium)
Enablements: wayland, dbus, pipewire, pulseaudio Enablements: wayland, dbus, pipewire
Groups: video, dialout, plugdev Groups: video, dialout, plugdev
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir
Home: /data/data/org.chromium.Chromium Home: /data/data/org.chromium.Chromium
@@ -159,7 +159,7 @@ Session bus
App App
Identity: 9 (org.chromium.Chromium) Identity: 9 (org.chromium.Chromium)
Enablements: wayland, dbus, pipewire, pulseaudio Enablements: wayland, dbus, pipewire
Groups: video, dialout, plugdev Groups: video, dialout, plugdev
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir
Home: /data/data/org.chromium.Chromium Home: /data/data/org.chromium.Chromium
@@ -215,8 +215,7 @@ App
"enablements": { "enablements": {
"wayland": true, "wayland": true,
"dbus": true, "dbus": true,
"pipewire": true, "pipewire": true
"pulse": true
}, },
"session_bus": { "session_bus": {
"see": null, "see": null,
@@ -367,8 +366,7 @@ App
"enablements": { "enablements": {
"wayland": true, "wayland": true,
"dbus": true, "dbus": true,
"pipewire": true, "pipewire": true
"pulse": true
}, },
"session_bus": { "session_bus": {
"see": null, "see": null,
@@ -566,8 +564,7 @@ func TestPrintPs(t *testing.T) {
"enablements": { "enablements": {
"wayland": true, "wayland": true,
"dbus": true, "dbus": true,
"pipewire": true, "pipewire": true
"pulse": true
}, },
"session_bus": { "session_bus": {
"see": null, "see": null,

View File

@@ -176,7 +176,6 @@ let
x11 = allow_x11; x11 = allow_x11;
dbus = allow_dbus; dbus = allow_dbus;
pipewire = allow_audio; pipewire = allow_audio;
pulse = allow_audio;
}; };
mesa = if gpu then mesaWrappers else null; mesa = if gpu then mesaWrappers else null;

View File

@@ -90,13 +90,13 @@ wait_for_window("hakurei@machine-foot")
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n") machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
machine.wait_for_file("/tmp/hakurei.0/tmpdir/2/success-client") machine.wait_for_file("/tmp/hakurei.0/tmpdir/2/success-client")
collect_state_ui("app_wayland") collect_state_ui("app_wayland")
check_state("foot", {"wayland": True, "dbus": True, "pipewire": True, "pulse": True}) check_state("foot", {"wayland": True, "dbus": True, "pipewire": True})
# Verify acl on XDG_RUNTIME_DIR: # Verify acl on XDG_RUNTIME_DIR:
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 10002")) print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10002"))
machine.send_chars("exit\n") machine.send_chars("exit\n")
machine.wait_until_fails("pgrep foot") machine.wait_until_fails("pgrep foot")
# Verify acl cleanup on XDG_RUNTIME_DIR: # Verify acl cleanup on XDG_RUNTIME_DIR:
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 10002") machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10002")
# Exit Sway and verify process exit status 0: # Exit Sway and verify process exit status 0:
swaymsg("exit", succeed=False) swaymsg("exit", succeed=False)
@@ -107,4 +107,4 @@ print(machine.succeed("find /tmp/hakurei.0 "
+ "-path '/tmp/hakurei.0/runtime/*/*' -prune -o " + "-path '/tmp/hakurei.0/runtime/*/*' -prune -o "
+ "-path '/tmp/hakurei.0/tmpdir/*/*' -prune -o " + "-path '/tmp/hakurei.0/tmpdir/*/*' -prune -o "
+ "-print")) + "-print"))
print(machine.succeed("find /run/user/1000/hakurei")) print(machine.fail("ls /run/user/1000/hakurei"))

View File

@@ -109,11 +109,9 @@ func (config *Config) Validate() error {
} }
} }
// EPulse without EPipeWire is insecure if et := config.Enablements.Unwrap(); !config.DirectPulse && et&EPulse != 0 {
if et := config.Enablements.Unwrap(); !config.DirectPulse &&
et&EPipeWire == 0 && et&EPulse != 0 {
return &AppError{Step: "validate configuration", Err: ErrInsecure, return &AppError{Step: "validate configuration", Err: ErrInsecure,
Msg: "enablement PulseAudio requires PipeWire, which is not set"} Msg: "enablement PulseAudio is insecure and no longer supported"}
} }
return nil return nil

View File

@@ -58,7 +58,7 @@ func TestConfigValidate(t *testing.T) {
Shell: fhs.AbsTmp, Shell: fhs.AbsTmp,
Path: fhs.AbsTmp, Path: fhs.AbsTmp,
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure, }}, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
Msg: "enablement PulseAudio requires PipeWire, which is not set"}}, Msg: "enablement PulseAudio is insecure and no longer supported"}},
{"valid", &hst.Config{Container: &hst.ContainerConfig{ {"valid", &hst.Config{Container: &hst.ContainerConfig{
Home: fhs.AbsTmp, Home: fhs.AbsTmp,
Shell: fhs.AbsTmp, Shell: fhs.AbsTmp,

View File

@@ -70,7 +70,7 @@ func Template() *Config {
return &Config{ return &Config{
ID: "org.chromium.Chromium", ID: "org.chromium.Chromium",
Enablements: NewEnablements(EWayland | EDBus | EPipeWire | EPulse), Enablements: NewEnablements(EWayland | EDBus | EPipeWire),
SessionBus: &BusConfig{ SessionBus: &BusConfig{
See: nil, See: nil,

View File

@@ -105,8 +105,7 @@ func TestTemplate(t *testing.T) {
"enablements": { "enablements": {
"wayland": true, "wayland": true,
"dbus": true, "dbus": true,
"pipewire": true, "pipewire": true
"pulse": true
}, },
"session_bus": { "session_bus": {
"see": null, "see": null,

View File

@@ -295,6 +295,7 @@ func (state *outcomeStateSys) toSystem() error {
// optional via enablements // optional via enablements
&spWaylandOp{}, &spWaylandOp{},
&spX11Op{}, &spX11Op{},
spPipeWireOp{},
&spPulseOp{}, &spPulseOp{},
&spDBusOp{}, &spDBusOp{},

View File

@@ -27,7 +27,7 @@ import (
"hakurei.app/message" "hakurei.app/message"
) )
func TestOutcomeMain(t *testing.T) { func TestOutcomeRun(t *testing.T) {
t.Parallel() t.Parallel()
msg := message.New(nil) msg := message.New(nil)
msg.SwapVerbose(testing.Verbose()) msg.SwapVerbose(testing.Verbose())
@@ -67,18 +67,8 @@ func TestOutcomeMain(t *testing.T) {
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
). ).
// ensureRuntimeDir // spPipeWireOp
Ensure(m("/run/user/1971"), 0700). PipeWire(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pipewire")).
UpdatePermType(system.User, m("/run/user/1971"), acl.Execute).
Ensure(m("/run/user/1971/hakurei"), 0700).
UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
// runtime
Ephemeral(system.Process, m("/run/user/1971/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), 0700).
UpdatePerm(m("/run/user/1971/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), acl.Execute).
// spPulseOp
Link(m("/run/user/1971/pulse/native"), m("/run/user/1971/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pulse")).
// spDBusOp // spDBusOp
MustProxyDBus( MustProxyDBus(
@@ -106,8 +96,7 @@ func TestOutcomeMain(t *testing.T) {
"GOOGLE_DEFAULT_CLIENT_ID=77185425430.apps.googleusercontent.com", "GOOGLE_DEFAULT_CLIENT_ID=77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET=OTJgUOQcT7lO7GsGZq2G4IlT", "GOOGLE_DEFAULT_CLIENT_SECRET=OTJgUOQcT7lO7GsGZq2G4IlT",
"HOME=/data/data/org.chromium.Chromium", "HOME=/data/data/org.chromium.Chromium",
"PULSE_COOKIE=/.hakurei/pulse-cookie", "PIPEWIRE_REMOTE=/run/user/1971/pipewire-0",
"PULSE_SERVER=unix:/run/user/1971/pulse/native",
"SHELL=/run/current-system/sw/bin/zsh", "SHELL=/run/current-system/sw/bin/zsh",
"TERM=xterm-256color", "TERM=xterm-256color",
"USER=chronos", "USER=chronos",
@@ -157,9 +146,8 @@ func TestOutcomeMain(t *testing.T) {
// spWaylandOp // spWaylandOp
Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/wayland"), m("/run/user/1971/wayland-0"), 0). Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/wayland"), m("/run/user/1971/wayland-0"), 0).
// spPulseOp // spPipeWireOp
Bind(m("/run/user/1971/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pulse"), m("/run/user/1971/pulse/native"), 0). Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pipewire"), m("/run/user/1971/pipewire-0"), 0).
Place(m("/.hakurei/pulse-cookie"), bytes.Repeat([]byte{0}, pulseCookieSizeMax)).
// spDBusOp // spDBusOp
Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bus"), m("/run/user/1971/bus"), 0). Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bus"), m("/run/user/1971/bus"), 0).
@@ -298,7 +286,7 @@ func TestOutcomeMain(t *testing.T) {
}, },
Filter: true, Filter: true,
}, },
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPulse), Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []hst.FilesystemConfigJSON{
@@ -347,10 +335,7 @@ func TestOutcomeMain(t *testing.T) {
Ensure(m("/tmp/hakurei.0/tmpdir/9"), 01700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/9"), acl.Read, acl.Write, acl.Execute). Ensure(m("/tmp/hakurei.0/tmpdir/9"), 01700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/9"), acl.Read, acl.Write, acl.Execute).
Ephemeral(system.Process, m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c"), 0711). Ephemeral(system.Process, m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c"), 0711).
Wayland(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/1971/wayland-0"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"). Wayland(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/1971/wayland-0"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
Ensure(m("/run/user/1971"), 0700).UpdatePermType(system.User, m("/run/user/1971"), acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset PipeWire(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/pipewire")).
Ensure(m("/run/user/1971/hakurei"), 0700).UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
Ephemeral(system.Process, m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c"), 0700).UpdatePermType(system.Process, m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c"), acl.Execute).
Link(m("/run/user/1971/pulse/native"), m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse")).
MustProxyDBus(&hst.BusConfig{ MustProxyDBus(&hst.BusConfig{
Talk: []string{ Talk: []string{
"org.freedesktop.Notifications", "org.freedesktop.Notifications",
@@ -397,8 +382,7 @@ func TestOutcomeMain(t *testing.T) {
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus", "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus",
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket", "DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket",
"HOME=/home/chronos", "HOME=/home/chronos",
"PULSE_COOKIE=" + hst.PrivateTmp + "/pulse-cookie", "PIPEWIRE_REMOTE=/run/user/65534/pipewire-0",
"PULSE_SERVER=unix:/run/user/65534/pulse/native",
"SHELL=/run/current-system/sw/bin/zsh", "SHELL=/run/current-system/sw/bin/zsh",
"TERM=xterm-256color", "TERM=xterm-256color",
"USER=chronos", "USER=chronos",
@@ -419,8 +403,7 @@ func TestOutcomeMain(t *testing.T) {
Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")). Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")).
Place(m("/etc/group"), []byte("hakurei:x:65534:\n")). Place(m("/etc/group"), []byte("hakurei:x:65534:\n")).
Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/65534/wayland-0"), 0). Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/65534/wayland-0"), 0).
Bind(m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse"), m("/run/user/65534/pulse/native"), 0). Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/pipewire"), m("/run/user/65534/pipewire-0"), 0).
Place(m(hst.PrivateTmp+"/pulse-cookie"), bytes.Repeat([]byte{0}, pulseCookieSizeMax)).
Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus"), m("/run/user/65534/bus"), 0). Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus"), m("/run/user/65534/bus"), 0).
Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), m("/var/run/dbus/system_bus_socket"), 0). Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), m("/var/run/dbus/system_bus_socket"), 0).
Bind(m("/dev/dri"), m("/dev/dri"), std.BindWritable|std.BindDevice|std.BindOptional). Bind(m("/dev/dri"), m("/dev/dri"), std.BindWritable|std.BindDevice|std.BindOptional).
@@ -440,7 +423,7 @@ func TestOutcomeMain(t *testing.T) {
{"nixos chromium direct wayland", new(stubNixOS), &hst.Config{ {"nixos chromium direct wayland", new(stubNixOS), &hst.Config{
ID: "org.chromium.Chromium", ID: "org.chromium.Chromium",
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPulse), Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Env: nil, Env: nil,
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []hst.FilesystemConfigJSON{
@@ -502,9 +485,8 @@ func TestOutcomeMain(t *testing.T) {
Ensure(m("/run/user/1971"), 0700).UpdatePermType(system.User, m("/run/user/1971"), acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset Ensure(m("/run/user/1971"), 0700).UpdatePermType(system.User, m("/run/user/1971"), acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ensure(m("/run/user/1971/hakurei"), 0700).UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute). Ensure(m("/run/user/1971/hakurei"), 0700).UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
UpdatePermType(hst.EWayland, m("/run/user/1971/wayland-0"), acl.Read, acl.Write, acl.Execute). UpdatePermType(hst.EWayland, m("/run/user/1971/wayland-0"), acl.Read, acl.Write, acl.Execute).
Ephemeral(system.Process, m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1"), 0700).UpdatePermType(system.Process, m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1"), acl.Execute).
Link(m("/run/user/1971/pulse/native"), m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse")).
Ephemeral(system.Process, m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1"), 0711). Ephemeral(system.Process, m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1"), 0711).
PipeWire(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/pipewire")).
MustProxyDBus(&hst.BusConfig{ MustProxyDBus(&hst.BusConfig{
Talk: []string{ Talk: []string{
"org.freedesktop.FileManager1", "org.freedesktop.Notifications", "org.freedesktop.FileManager1", "org.freedesktop.Notifications",
@@ -544,8 +526,7 @@ func TestOutcomeMain(t *testing.T) {
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus", "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus",
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket", "DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket",
"HOME=/var/lib/persist/module/hakurei/0/1", "HOME=/var/lib/persist/module/hakurei/0/1",
"PULSE_COOKIE=" + hst.PrivateTmp + "/pulse-cookie", "PIPEWIRE_REMOTE=/run/user/1971/pipewire-0",
"PULSE_SERVER=unix:/run/user/1971/pulse/native",
"SHELL=/run/current-system/sw/bin/zsh", "SHELL=/run/current-system/sw/bin/zsh",
"TERM=xterm-256color", "TERM=xterm-256color",
"USER=u0_a1", "USER=u0_a1",
@@ -565,8 +546,7 @@ func TestOutcomeMain(t *testing.T) {
Place(m("/etc/passwd"), []byte("u0_a1:x:1971:100:Hakurei:/var/lib/persist/module/hakurei/0/1:/run/current-system/sw/bin/zsh\n")). Place(m("/etc/passwd"), []byte("u0_a1:x:1971:100:Hakurei:/var/lib/persist/module/hakurei/0/1:/run/current-system/sw/bin/zsh\n")).
Place(m("/etc/group"), []byte("hakurei:x:100:\n")). Place(m("/etc/group"), []byte("hakurei:x:100:\n")).
Bind(m("/run/user/1971/wayland-0"), m("/run/user/1971/wayland-0"), 0). Bind(m("/run/user/1971/wayland-0"), m("/run/user/1971/wayland-0"), 0).
Bind(m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse"), m("/run/user/1971/pulse/native"), 0). Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/pipewire"), m("/run/user/1971/pipewire-0"), 0).
Place(m(hst.PrivateTmp+"/pulse-cookie"), bytes.Repeat([]byte{0}, pulseCookieSizeMax)).
Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus"), m("/run/user/1971/bus"), 0). Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus"), m("/run/user/1971/bus"), 0).
Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), m("/var/run/dbus/system_bus_socket"), 0). Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), m("/var/run/dbus/system_bus_socket"), 0).
Bind(m("/bin"), m("/bin"), 0). Bind(m("/bin"), m("/bin"), 0).

View File

@@ -29,7 +29,7 @@ type spPulseOp struct {
} }
func (s *spPulseOp) toSystem(state *outcomeStateSys) error { func (s *spPulseOp) toSystem(state *outcomeStateSys) error {
if state.et&hst.EPulse == 0 { if !state.directPulse || state.et&hst.EPulse == 0 {
return errNotEnabled return errNotEnabled
} }

View File

@@ -18,24 +18,40 @@ import (
func TestSpPulseOp(t *testing.T) { func TestSpPulseOp(t *testing.T) {
t.Parallel() t.Parallel()
config := hst.Template() newConfig := func() *hst.Config {
config := hst.Template()
config.DirectPulse = true
config.Enablements = hst.NewEnablements(hst.EPulse)
return config
}
config := newConfig()
sampleCookie := bytes.Repeat([]byte{0xfc}, pulseCookieSizeMax) sampleCookie := bytes.Repeat([]byte{0xfc}, pulseCookieSizeMax)
checkOpBehaviour(t, []opBehaviourTestCase{ checkOpBehaviour(t, []opBehaviourTestCase{
{"not enabled", func(bool, bool) outcomeOp { {"not enabled", func(bool, bool) outcomeOp {
return new(spPulseOp) return new(spPulseOp)
}, func() *hst.Config { }, func() *hst.Config {
c := hst.Template() c := newConfig()
c.DirectPulse = true
*c.Enablements = 0 *c.Enablements = 0
return c return c
}, nil, nil, nil, nil, errNotEnabled, nil, nil, nil, nil, nil}, }, nil, nil, nil, nil, errNotEnabled, nil, nil, nil, nil, nil},
{"not enabled direct", func(bool, bool) outcomeOp {
return new(spPulseOp)
}, func() *hst.Config {
c := newConfig()
c.DirectPulse = false
return c
}, nil, nil, nil, nil, errNotEnabled, nil, nil, nil, nil, nil},
{"socketDir stat", func(isShim, _ bool) outcomeOp { {"socketDir stat", func(isShim, _ bool) outcomeOp {
if !isShim { if !isShim {
return new(spPulseOp) return new(spPulseOp)
} }
return &spPulseOp{Cookie: (*[256]byte)(sampleCookie)} return &spPulseOp{Cookie: (*[256]byte)(sampleCookie)}
}, hst.Template, nil, []stub.Call{ }, newConfig, nil, []stub.Call{
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), stub.UniqueError(2)), call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), stub.UniqueError(2)),
}, nil, nil, &hst.AppError{ }, nil, nil, &hst.AppError{
Step: `access PulseAudio directory "/proc/nonexistent/xdg_runtime_dir/pulse"`, Step: `access PulseAudio directory "/proc/nonexistent/xdg_runtime_dir/pulse"`,
@@ -44,7 +60,7 @@ func TestSpPulseOp(t *testing.T) {
{"socketDir nonexistent", func(bool, bool) outcomeOp { {"socketDir nonexistent", func(bool, bool) outcomeOp {
return new(spPulseOp) return new(spPulseOp)
}, hst.Template, nil, []stub.Call{ }, newConfig, nil, []stub.Call{
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), os.ErrNotExist), call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), os.ErrNotExist),
}, nil, nil, &hst.AppError{ }, nil, nil, &hst.AppError{
Step: "finalise", Step: "finalise",
@@ -54,7 +70,7 @@ func TestSpPulseOp(t *testing.T) {
{"socket stat", func(bool, bool) outcomeOp { {"socket stat", func(bool, bool) outcomeOp {
return new(spPulseOp) return new(spPulseOp)
}, hst.Template, nil, []stub.Call{ }, newConfig, nil, []stub.Call{
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil), call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, (*stubFi)(nil), stub.UniqueError(1)), call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, (*stubFi)(nil), stub.UniqueError(1)),
}, nil, nil, &hst.AppError{ }, nil, nil, &hst.AppError{
@@ -64,7 +80,7 @@ func TestSpPulseOp(t *testing.T) {
{"socket nonexistent", func(bool, bool) outcomeOp { {"socket nonexistent", func(bool, bool) outcomeOp {
return new(spPulseOp) return new(spPulseOp)
}, hst.Template, nil, []stub.Call{ }, newConfig, nil, []stub.Call{
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil), call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, (*stubFi)(nil), os.ErrNotExist), call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, (*stubFi)(nil), os.ErrNotExist),
}, nil, nil, &hst.AppError{ }, nil, nil, &hst.AppError{
@@ -75,7 +91,7 @@ func TestSpPulseOp(t *testing.T) {
{"socket mode", func(bool, bool) outcomeOp { {"socket mode", func(bool, bool) outcomeOp {
return new(spPulseOp) return new(spPulseOp)
}, hst.Template, nil, []stub.Call{ }, newConfig, nil, []stub.Call{
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil), call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0660}, nil), call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0660}, nil),
}, nil, nil, &hst.AppError{ }, nil, nil, &hst.AppError{
@@ -86,7 +102,7 @@ func TestSpPulseOp(t *testing.T) {
{"cookie notAbs", func(bool, bool) outcomeOp { {"cookie notAbs", func(bool, bool) outcomeOp {
return new(spPulseOp) return new(spPulseOp)
}, hst.Template, nil, []stub.Call{ }, newConfig, nil, []stub.Call{
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil), call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil), call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil),
call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "proc/nonexistent/cookie", nil), call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "proc/nonexistent/cookie", nil),
@@ -97,7 +113,7 @@ func TestSpPulseOp(t *testing.T) {
{"cookie loadFile", func(bool, bool) outcomeOp { {"cookie loadFile", func(bool, bool) outcomeOp {
return new(spPulseOp) return new(spPulseOp)
}, hst.Template, nil, []stub.Call{ }, newConfig, nil, []stub.Call{
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil), call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil), call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil),
call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "/proc/nonexistent/cookie", nil), call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "/proc/nonexistent/cookie", nil),
@@ -118,7 +134,7 @@ func TestSpPulseOp(t *testing.T) {
op.CookieSize += +0xfd op.CookieSize += +0xfd
} }
return op return op
}, hst.Template, nil, []stub.Call{ }, newConfig, nil, []stub.Call{
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil), call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil), call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil),
call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "/proc/nonexistent/cookie", nil), call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "/proc/nonexistent/cookie", nil),
@@ -150,7 +166,7 @@ func TestSpPulseOp(t *testing.T) {
sampleCookieTrunc := make([]byte, pulseCookieSizeMax) sampleCookieTrunc := make([]byte, pulseCookieSizeMax)
copy(sampleCookieTrunc, sampleCookie[:len(sampleCookie)-0xe]) copy(sampleCookieTrunc, sampleCookie[:len(sampleCookie)-0xe])
return &spPulseOp{Cookie: (*[pulseCookieSizeMax]byte)(sampleCookieTrunc), CookieSize: pulseCookieSizeMax - 0xe} return &spPulseOp{Cookie: (*[pulseCookieSizeMax]byte)(sampleCookieTrunc), CookieSize: pulseCookieSizeMax - 0xe}
}, hst.Template, nil, []stub.Call{ }, newConfig, nil, []stub.Call{
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil), call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil), call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil),
call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "/proc/nonexistent/cookie", nil), call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "/proc/nonexistent/cookie", nil),
@@ -183,7 +199,7 @@ func TestSpPulseOp(t *testing.T) {
return new(spPulseOp) return new(spPulseOp)
} }
return &spPulseOp{Cookie: (*[pulseCookieSizeMax]byte)(sampleCookie), CookieSize: pulseCookieSizeMax} return &spPulseOp{Cookie: (*[pulseCookieSizeMax]byte)(sampleCookie), CookieSize: pulseCookieSizeMax}
}, hst.Template, nil, []stub.Call{ }, newConfig, nil, []stub.Call{
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil), call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil), call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil),
call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "/proc/nonexistent/cookie", nil), call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "/proc/nonexistent/cookie", nil),
@@ -213,7 +229,7 @@ func TestSpPulseOp(t *testing.T) {
{"success", func(bool, bool) outcomeOp { {"success", func(bool, bool) outcomeOp {
return new(spPulseOp) return new(spPulseOp)
}, hst.Template, nil, []stub.Call{ }, newConfig, nil, []stub.Call{
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil), call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil), call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil),
call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, nil, nil), call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, nil, nil),

View File

@@ -47,9 +47,9 @@ func TestEntryData(t *testing.T) {
{"inconsistent enablement", "\x00\xff\xca\xfe\x00\x00\xff\x00" + templateStateGob, NewTemplateState(), &hst.AppError{ {"inconsistent enablement", "\x00\xff\xca\xfe\x00\x00\xff\x00" + templateStateGob, NewTemplateState(), &hst.AppError{
Step: "validate state enablement", Err: os.ErrInvalid, Step: "validate state enablement", Err: os.ErrInvalid,
Msg: "state entry aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa has unexpected enablement byte 0x1d, 0xff"}}, Msg: "state entry aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa has unexpected enablement byte 0xd, 0xff"}},
{"template", "\x00\xff\xca\xfe\x00\x00\x1d\xe2" + templateStateGob, NewTemplateState(), nil}, {"template", "\x00\xff\xca\xfe\x00\x00\x0d\xf2" + templateStateGob, NewTemplateState(), nil},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {

View File

@@ -196,6 +196,15 @@ in
} }
] ]
) )
++ optional (app.enablements.pipewire && app.pulse) {
type = "daemon";
dst = if app.mapRealUid then "/run/user/${toString config.users.users.${username}.uid}/pulse/native" else "/run/user/65534/pulse/native";
path = cfg.shell;
args = [
"-lc"
"pipewire-pulse"
];
}
++ [ ++ [
{ {
type = "bind"; type = "bind";

View File

@@ -245,14 +245,14 @@ in
Whether to share the PipeWire server via SecurityContext. Whether to share the PipeWire server via SecurityContext.
''; '';
}; };
};
pulse = mkOption { pulse = mkOption {
type = nullOr bool; type = nullOr bool;
default = true; default = true;
description = '' description = ''
Whether to run the PulseAudio compatibility daemon. Whether to run the PulseAudio compatibility daemon.
''; '';
};
}; };
share = mkOption { share = mkOption {

View File

@@ -134,7 +134,6 @@
enablements = { enablements = {
wayland = false; wayland = false;
pipewire = false; pipewire = false;
pulse = false;
}; };
}; };
@@ -154,7 +153,6 @@
enablements = { enablements = {
dbus = false; dbus = false;
pipewire = false; pipewire = false;
pulse = false;
}; };
}; };
@@ -170,7 +168,6 @@
enablements = { enablements = {
dbus = false; dbus = false;
pipewire = false; pipewire = false;
pulse = false;
}; };
}; };
@@ -203,7 +200,6 @@
x11 = true; x11 = true;
dbus = false; dbus = false;
pipewire = false; pipewire = false;
pulse = false;
}; };
}; };
@@ -223,7 +219,6 @@
enablements = { enablements = {
dbus = false; dbus = false;
pipewire = false; pipewire = false;
pulse = false;
}; };
}; };
@@ -238,7 +233,6 @@
x11 = false; x11 = false;
dbus = false; dbus = false;
pipewire = false; pipewire = false;
pulse = false;
}; };
}; };
}; };

View File

@@ -15,7 +15,7 @@
command = "foot"; command = "foot";
enablements = { enablements = {
dbus = false; dbus = false;
pulse = false; pipewire = false;
}; };
}; };
}; };

View File

@@ -160,17 +160,17 @@ machine.succeed("pkill -9 mako")
# Check revert type selection: # Check revert type selection:
hakurei("-v run --wayland -X --dbus --pulse -u p0 foot && touch /tmp/p0-exit-ok") hakurei("-v run --wayland -X --dbus --pulse -u p0 foot && touch /tmp/p0-exit-ok")
wait_for_window("p0@machine") wait_for_window("p0@machine")
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 10000")) print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000"))
hakurei("-v run --wayland -X --dbus --pulse -u p1 foot && touch /tmp/p1-exit-ok") hakurei("-v run --wayland -X --dbus --pulse -u p1 foot && touch /tmp/p1-exit-ok")
wait_for_window("p1@machine") wait_for_window("p1@machine")
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 10000")) print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000"))
machine.send_chars("exit\n") machine.send_chars("exit\n")
machine.wait_for_file("/tmp/p1-exit-ok", timeout=15) machine.wait_for_file("/tmp/p1-exit-ok", timeout=15)
# Verify acl is kept alive: # Verify acl is kept alive:
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 10000")) print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000"))
machine.send_chars("exit\n") machine.send_chars("exit\n")
machine.wait_for_file("/tmp/p0-exit-ok", timeout=15) machine.wait_for_file("/tmp/p0-exit-ok", timeout=15)
machine.fail("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 10000") machine.fail("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000")
# Check invalid identifier fd behaviour: # Check invalid identifier fd behaviour:
machine.fail('echo \'{"container":{"shell":"/proc/nonexistent","home":"/proc/nonexistent","path":"/proc/nonexistent"}}\' | sudo -u alice -i hakurei -v app --identifier-fd 32767 - 2>&1 | tee > /tmp/invalid-identifier-fd') machine.fail('echo \'{"container":{"shell":"/proc/nonexistent","home":"/proc/nonexistent","path":"/proc/nonexistent"}}\' | sudo -u alice -i hakurei -v app --identifier-fd 32767 - 2>&1 | tee > /tmp/invalid-identifier-fd')
@@ -219,15 +219,21 @@ machine.send_chars("exit\n")
machine.wait_until_fails("pgrep foot", timeout=5) machine.wait_until_fails("pgrep foot", timeout=5)
machine.fail(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {hakurei_identity(0) + 10000}", timeout=5) machine.fail(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {hakurei_identity(0) + 10000}", timeout=5)
# Test PulseAudio (hakurei does not support PipeWire yet): # Test pipewire-pulse:
swaymsg("exec pa-foot") swaymsg("exec pa-foot")
wait_for_window(f"u0_a{hakurei_identity(1)}@machine") wait_for_window(f"u0_a{hakurei_identity(1)}@machine")
machine.send_chars("clear; pactl info && touch /var/tmp/pulse-ok\n") machine.send_chars("clear; pactl info && touch /var/tmp/pulse-ok\n")
machine.wait_for_file("/var/tmp/pulse-ok", timeout=15) machine.wait_for_file("/var/tmp/pulse-ok", timeout=15)
collect_state_ui("pulse_wayland") collect_state_ui("pulse_wayland")
check_state("pa-foot", {"wayland": True, "pipewire": True, "pulse": True}) check_state("pa-foot", {"wayland": True, "pipewire": True})
# Test PipeWire:
machine.send_chars("clear; pw-cli i 0 && touch /var/tmp/pw-ok\n")
machine.wait_for_file("/var/tmp/pw-ok", timeout=15)
collect_state_ui("pipewire_wayland")
machine.send_chars("exit\n") machine.send_chars("exit\n")
machine.wait_until_fails("pgrep foot", timeout=5) machine.wait_until_fails("pgrep foot", timeout=5)
# Test PipeWire SecurityContext:
machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v run --pulse pactl set-sink-mute @DEFAULT_SINK@ toggle")
# Test XWayland (foot does not support X): # Test XWayland (foot does not support X):
swaymsg("exec x11-alacritty") swaymsg("exec x11-alacritty")