27 Commits

Author SHA1 Message Date
8cb0b433b2 release: 0.3.3
All checks were successful
Release / Create release (push) Successful in 42s
Test / Sandbox (push) Successful in 43s
Test / Hakurei (push) Successful in 3m2s
Test / Create distribution (push) Successful in 27s
Test / Hpkg (push) Successful in 3m57s
Test / Sandbox (race detector) (push) Successful in 4m41s
Test / Hakurei (race detector) (push) Successful in 5m0s
Test / Flake checks (push) Successful in 1m43s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-15 20:34:45 +09:00
767f1844d2 test: check shim private dir cleanup
All checks were successful
Test / Create distribution (push) Successful in 38s
Test / Hpkg (push) Successful in 45s
Test / Sandbox (push) Successful in 1m35s
Test / Sandbox (race detector) (push) Successful in 2m28s
Test / Hakurei (push) Successful in 2m32s
Test / Hakurei (race detector) (push) Successful in 3m17s
Test / Flake checks (push) Successful in 1m31s
This asserts that no shim private dir was left behind after all containers terminate.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-15 20:30:19 +09:00
54610aaddc internal/outcome: expose pipewire via pipewire-pulse
All checks were successful
Test / Create distribution (push) Successful in 28s
Test / Sandbox (push) Successful in 42s
Test / Hakurei (push) Successful in 3m20s
Test / Hpkg (push) Successful in 2m13s
Test / Sandbox (race detector) (push) Successful in 4m25s
Test / Hakurei (race detector) (push) Successful in 3m21s
Test / Flake checks (push) Successful in 1m30s
This no longer exposes the pipewire socket to the container, and instead mediates access via pipewire-pulse. This makes insecure parts of the protocol inaccessible as explained in the doc comment in hst.

Closes #29.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-15 12:57:06 +09:00
2e80660169 internal/outcome: look up pipewire-pulse path
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m27s
Test / Hakurei (push) Successful in 3m19s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m20s
Test / Hakurei (race detector) (push) Successful in 5m16s
Test / Flake checks (push) Successful in 1m29s
This is for setting up the pipewire-pulse container in shim, for #29.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-15 12:38:39 +09:00
d0a3c6a2f3 internal/outcome: optional shim private dir
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 3m24s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m32s
Test / Hakurei (race detector) (push) Successful in 5m18s
Test / Flake checks (push) Successful in 1m40s
This is a private work directory owned by the specific shim. Useful for sockets owned by this instance of the shim and requires no direct assistance from the priv-side process.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-15 12:32:46 +09:00
0c0e3d6fc2 hst: add direct hardware option
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 47s
Test / Sandbox (race detector) (push) Successful in 2m17s
Test / Hakurei (race detector) (push) Successful in 3m12s
Test / Hpkg (push) Successful in 3m26s
Test / Flake checks (push) Successful in 1m37s
This is unfortunately the only possible setup to securely expose PipeWire to the container. Further explanation explained in the doc comment and #29.

This will be implemented in a future commit.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-15 12:29:32 +09:00
fae910a1ad container: sync stubbed wait4 loop after notify
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m22s
Test / Hakurei (push) Successful in 3m29s
Test / Hpkg (push) Successful in 4m19s
Test / Sandbox (race detector) (push) Successful in 4m27s
Test / Hakurei (race detector) (push) Successful in 5m28s
Test / Flake checks (push) Successful in 1m30s
This ensures consistent state observed by wait4 loop when running against stub.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-14 10:22:48 +09:00
178c8bc28b internal/pipewire: handle SecurityContext::Create error
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m24s
Test / Hpkg (push) Successful in 4m14s
Test / Sandbox (race detector) (push) Successful in 4m22s
Test / Hakurei (race detector) (push) Successful in 5m21s
Test / Hakurei (push) Successful in 2m24s
Test / Flake checks (push) Successful in 1m31s
This method can result in an error targeting it, so it is handled here. This change also causes a call to Create to also Core::Sync, as it should have done.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-14 09:41:28 +09:00
30dcab0734 internal/pipewire: SecurityContext as destructible
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m24s
Test / Hakurei (push) Successful in 3m28s
Test / Sandbox (race detector) (push) Successful in 4m23s
Test / Hpkg (push) Successful in 4m25s
Test / Hakurei (race detector) (push) Successful in 5m23s
Test / Flake checks (push) Successful in 1m33s
This proxy can be destroyed by sending a Core::Destroy targeting it. This change implements the Destroy method by embedding destructible.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-14 09:31:50 +09:00
0ea051062b internal/pipewire: reorder context struct
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m25s
Test / Hakurei (push) Successful in 3m25s
Test / Hpkg (push) Successful in 4m15s
Test / Sandbox (race detector) (push) Successful in 4m28s
Test / Hakurei (race detector) (push) Successful in 3m12s
Test / Flake checks (push) Successful in 1m39s
This change reorders and groups struct elements. This improves readability since this struct holds a lot of state loosely related to each other.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-14 09:26:30 +09:00
b0f2ab6fff internal/pipewire: implement Core::Destroy
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m28s
Test / Hakurei (push) Successful in 3m25s
Test / Hpkg (push) Successful in 4m19s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hakurei (race detector) (push) Successful in 5m21s
Test / Flake checks (push) Successful in 1m43s
This change also implements pending destructible check on Sync. Destruction method should always be implemented as a wrapper of destructible.destroy.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-14 09:20:58 +09:00
00a5bdf006 internal/pipewire: do not emit None for spa_dict
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m23s
Test / Hakurei (push) Successful in 3m26s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m30s
Test / Hakurei (race detector) (push) Successful in 3m18s
Test / Flake checks (push) Successful in 1m41s
Turns out the PipeWire server does not expect a value of type None here at all.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-14 09:06:44 +09:00
a27dfdc058 internal/pipewire: implement Core::CreateObject
All checks were successful
Test / Create distribution (push) Successful in 42s
Test / Sandbox (push) Successful in 3m23s
Test / Hakurei (push) Successful in 4m49s
Test / Sandbox (race detector) (push) Successful in 5m28s
Test / Hpkg (push) Successful in 6m12s
Test / Hakurei (race detector) (push) Successful in 6m37s
Test / Flake checks (push) Successful in 1m34s
Nothing uses this right now, this would have to be called by wrapper methods on Registry that would search the objects

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-14 08:10:57 +09:00
6d0d9cecd1 internal/pipewire: handle nil spa_dict correctly
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m25s
Test / Hakurei (push) Successful in 3m27s
Test / Hpkg (push) Successful in 4m15s
Test / Sandbox (race detector) (push) Successful in 4m28s
Test / Hakurei (race detector) (push) Successful in 5m21s
Test / Flake checks (push) Successful in 1m37s
This now marshals into a value of type None when the slice is nil, and correctly unmarshals from type None.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-14 07:12:00 +09:00
17248d7d61 internal/pipewire: unmarshal nil pointer correctly
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m21s
Test / Hakurei (push) Successful in 3m27s
Test / Sandbox (race detector) (push) Successful in 4m21s
Test / Hpkg (push) Successful in 4m28s
Test / Hakurei (race detector) (push) Successful in 5m21s
Test / Flake checks (push) Successful in 1m41s
This now calls unmarshalCheckTypeBounds to advance to the next message. Additionally, handling for None value is relocated to a function for reuse by other types.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-14 06:58:53 +09:00
41e5628c67 internal/pipewire: return correct size for nil spa_dict
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m30s
Test / Hakurei (push) Successful in 3m30s
Test / Hpkg (push) Successful in 4m15s
Test / Sandbox (race detector) (push) Successful in 4m22s
Test / Hakurei (race detector) (push) Successful in 5m25s
Test / Flake checks (push) Successful in 1m39s
A nil spa_dict results in a None type value being sent over the wire.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-14 06:07:46 +09:00
ffbec828e1 internal/pipewire: move Core wrapper methods under Core
All checks were successful
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m35s
Test / Hpkg (push) Successful in 4m31s
Test / Sandbox (race detector) (push) Successful in 4m37s
Test / Hakurei (race detector) (push) Successful in 5m31s
Test / Hakurei (push) Successful in 2m30s
Test / Flake checks (push) Successful in 1m42s
These do not belong under Context, and is an early implementation limitation that carried over.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-14 05:37:21 +09:00
de0467a65e internal/pipewire: treat noAck violation as fatal
All checks were successful
Test / Create distribution (push) Successful in 52s
Test / Sandbox (push) Successful in 2m52s
Test / Sandbox (race detector) (push) Successful in 4m56s
Test / Hpkg (push) Successful in 5m0s
Test / Hakurei (race detector) (push) Successful in 5m51s
Test / Hakurei (push) Successful in 2m50s
Test / Flake checks (push) Successful in 2m3s
Receiving this event indicates something has gone terribly wrong somehow, and ignoring Core::BoundProps causes inconsistent state anyway.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-13 22:30:06 +09:00
b5999b8814 internal/pipewire: implement Core::RemoveId
All checks were successful
Test / Create distribution (push) Successful in 1m15s
Test / Sandbox (push) Successful in 3m15s
Test / Hakurei (push) Successful in 4m19s
Test / Hakurei (race detector) (push) Successful in 3m24s
Test / Sandbox (race detector) (push) Successful in 2m34s
Test / Hpkg (push) Successful in 3m34s
Test / Flake checks (push) Successful in 1m50s
This is emitted by the server when a proxy id is removed for any reason. Currently, the only path for this to be emitted is when a global object is destroyed while some proxy is still bound to it.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-13 21:50:32 +09:00
ebc67bb8ad nix: update flake lock
All checks were successful
Test / Create distribution (push) Successful in 1m1s
Test / Sandbox (push) Successful in 4m13s
Test / Hakurei (push) Successful in 5m11s
Test / Sandbox (race detector) (push) Successful in 5m46s
Test / Hakurei (race detector) (push) Successful in 6m50s
Test / Hpkg (push) Successful in 13m44s
Test / Flake checks (push) Successful in 2m14s
NixOS 25.11 introduces a crash in cage and an intermittent crash in foot.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-12 08:39:55 +09:00
e60ff660f6 internal/pipewire: treat unknown opcode as fatal
All checks were successful
Test / Create distribution (push) Successful in 58s
Test / Hakurei (push) Successful in 8m27s
Test / Hakurei (race detector) (push) Successful in 10m24s
Test / Sandbox (push) Successful in 1m43s
Test / Sandbox (race detector) (push) Successful in 2m13s
Test / Hpkg (push) Successful in 3m21s
Test / Flake checks (push) Successful in 1m30s
Skipping events can cause local state to diverge from the server.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-11 04:22:03 +09:00
47db461546 internal/pipewire: generic Core::Error handling
All checks were successful
Test / Create distribution (push) Successful in 58s
Test / Hakurei (push) Successful in 8m1s
Test / Hakurei (race detector) (push) Successful in 10m19s
Test / Sandbox (push) Successful in 1m30s
Test / Sandbox (race detector) (push) Successful in 2m18s
Test / Hpkg (push) Successful in 3m21s
Test / Flake checks (push) Successful in 1m31s
This flushes message buffer before queueing the event expecting the error. Since this is quite useful and relatively complex, it is relocated to a method of Context.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-11 04:07:55 +09:00
0a3fe5f907 internal/pipewire: export Registry::Destroy
All checks were successful
Test / Create distribution (push) Successful in 1m32s
Test / Hakurei (push) Successful in 8m23s
Test / Sandbox (push) Successful in 1m35s
Test / Sandbox (race detector) (push) Successful in 2m18s
Test / Hakurei (race detector) (push) Successful in 3m9s
Test / Hpkg (push) Successful in 3m19s
Test / Flake checks (push) Successful in 1m31s
This handles the error returned by Sync.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-11 03:34:33 +09:00
b72d502f1c internal/outcome: populate instance metadata for PipeWire
All checks were successful
Test / Create distribution (push) Successful in 1m10s
Test / Hakurei (push) Successful in 12m25s
Test / Sandbox (push) Successful in 1m40s
Test / Sandbox (race detector) (push) Successful in 2m29s
Test / Hakurei (race detector) (push) Successful in 4m54s
Test / Hpkg (push) Successful in 3m56s
Test / Flake checks (push) Successful in 2m42s
These have similar semantics to equivalent Wayland security-context-v1 fields.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-10 03:01:30 +09:00
f8b3db3f66 internal/pipewire: cleaner error message for unsupported type
All checks were successful
Test / Create distribution (push) Successful in 43s
Test / Sandbox (push) Successful in 2m55s
Test / Sandbox (race detector) (push) Successful in 5m10s
Test / Hpkg (push) Successful in 5m24s
Test / Hakurei (race detector) (push) Successful in 6m53s
Test / Hakurei (push) Successful in 7m30s
Test / Flake checks (push) Successful in 2m49s
The error string itself is descriptive enough, so use it as the error message directly.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-10 01:51:06 +09:00
0e2fb1788f internal/pipewire: implement Registry::Destroy
All checks were successful
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m44s
Test / Sandbox (race detector) (push) Successful in 4m56s
Test / Hakurei (push) Successful in 5m7s
Test / Hpkg (push) Successful in 5m7s
Test / Hakurei (race detector) (push) Successful in 7m0s
Test / Flake checks (push) Successful in 2m0s
This requires error handling infrastructure in Core that does not yet exist, so it is not exported for now. It has been manually tested via linkname against PipeWire.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-10 01:25:30 +09:00
d8417e2927 internal/pipewire: implement Registry::GlobalRemove
All checks were successful
Test / Create distribution (push) Successful in 41s
Test / Sandbox (push) Successful in 2m47s
Test / Sandbox (race detector) (push) Successful in 5m6s
Test / Hakurei (push) Successful in 5m30s
Test / Hpkg (push) Successful in 5m39s
Test / Hakurei (race detector) (push) Successful in 7m18s
Test / Flake checks (push) Successful in 1m41s
This is emitted by PipeWire when a global object disappears, because PipeWire insists that all clients that had called Core::GetRegistry must constantly sync its local registry state with the remote.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-10 01:00:03 +09:00
40 changed files with 1118 additions and 280 deletions

View File

@@ -14,7 +14,6 @@ import (
_ "unsafe" // for go:linkname
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/hst"
@@ -187,14 +186,6 @@ 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", "exec pipewire-pulse"},
}})
}
config.Container.Filesystem = append(config.Container.Filesystem,
// opportunistically bind kvm
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{

View File

@@ -1,5 +1,5 @@
{
nixosTest,
testers,
callPackage,
system,
@@ -8,7 +8,7 @@
let
buildPackage = self.buildPackage.${system};
in
nixosTest {
testers.nixosTest {
name = "hpkg";
nodes.machine = {
environment.etc = {

View File

@@ -202,7 +202,7 @@ func TestIsAutoRootBindable(t *testing.T) {
t.Parallel()
var msg message.Msg
if tc.log {
msg = &kstub{nil, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { panic("unreachable") }, stub.Expect{Calls: []stub.Call{
msg = &kstub{nil, nil, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { panic("unreachable") }, stub.Expect{Calls: []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"got unexpected root entry"}}, nil, nil),
}})}
}

View File

@@ -162,7 +162,8 @@ func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) {
t.Parallel()
wait4signal := make(chan struct{})
k := &kstub{wait4signal, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, s} }, tc.want)}
lockNotify := make(chan struct{})
k := &kstub{wait4signal, lockNotify, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, lockNotify, s} }, tc.want)}
defer stub.HandleExit(t)
if err := tc.f(k); !reflect.DeepEqual(err, tc.wantErr) {
t.Errorf("%s: error = %v, want %v", fname, err, tc.wantErr)
@@ -200,8 +201,8 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
t.Helper()
t.Parallel()
k := &kstub{nil, stub.New(t,
func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, s} },
k := &kstub{nil, nil, stub.New(t,
func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, nil, s} },
stub.Expect{Calls: slices.Concat(tc.early, []stub.Call{{Name: stub.CallSeparator}}, tc.apply)},
)}
state := &setupState{Params: tc.params, Msg: k}
@@ -322,12 +323,19 @@ const (
type kstub struct {
wait4signal chan struct{}
lockNotify chan struct{}
*stub.Stub[syscallDispatcher]
}
func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) }
func (k *kstub) lockOSThread() { k.Helper(); k.Expects("lockOSThread") }
func (k *kstub) lockOSThread() {
k.Helper()
expect := k.Expects("lockOSThread")
if k.lockNotify != nil && expect.Ret == magicWait4Signal {
<-k.lockNotify
}
}
func (k *kstub) setPtracer(pid uintptr) error {
k.Helper()
@@ -472,6 +480,10 @@ func (k *kstub) notify(c chan<- os.Signal, sig ...os.Signal) {
k.FailNow()
}
if k.lockNotify != nil && expect.Ret == magicWait4Signal {
defer close(k.lockNotify)
}
// export channel for external instrumentation
if chanf, ok := expect.Args[0].(func(c chan<- os.Signal)); ok && chanf != nil {
chanf(c)

View File

@@ -1992,7 +1992,7 @@ func TestInitEntrypoint(t *testing.T) {
/* wait4 */
Tracks: []stub.Expect{{Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
// this terminates the goroutine at the call, preventing it from leaking while preserving behaviour
call("wait4", stub.ExpectArgs{-1, nil, 0, nil, stub.PanicExit}, 0, syscall.ECHILD),
@@ -2075,7 +2075,7 @@ func TestInitEntrypoint(t *testing.T) {
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(10)}}, nil, nil),
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- CancelSignal }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- CancelSignal }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
call("verbose", stub.ExpectArgs{[]any{"forwarding context cancellation"}}, nil, nil),
// magicWait4Signal as ret causes wait4 stub to unblock
call("signal", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent", os.Interrupt}, magicWait4Signal, stub.UniqueError(9)),
@@ -2090,7 +2090,7 @@ func TestInitEntrypoint(t *testing.T) {
/* wait4 */
Tracks: []stub.Expect{{Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
// magicWait4Signal as args[4] causes this to block until simulated signal is delivered
call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil, magicWait4Signal}, 0xbad, nil),
@@ -2175,7 +2175,7 @@ func TestInitEntrypoint(t *testing.T) {
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(7)}}, nil, nil),
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- syscall.SIGQUIT }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- syscall.SIGQUIT }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
call("verbosef", stub.ExpectArgs{"got %s, forwarding to initial process", []any{"quit"}}, nil, nil),
// magicWait4Signal as ret causes wait4 stub to unblock
call("signal", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent", syscall.SIGQUIT}, magicWait4Signal, stub.UniqueError(0xfe)),
@@ -2190,7 +2190,7 @@ func TestInitEntrypoint(t *testing.T) {
/* wait4 */
Tracks: []stub.Expect{{Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
// magicWait4Signal as args[4] causes this to block until simulated signal is delivered
call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil, magicWait4Signal}, 0xbad, nil),
@@ -2275,7 +2275,7 @@ func TestInitEntrypoint(t *testing.T) {
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(7)}}, nil, nil),
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- os.Interrupt }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- os.Interrupt }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
call("verbosef", stub.ExpectArgs{"got %s", []any{"interrupt"}}, nil, nil),
call("beforeExit", stub.ExpectArgs{}, nil, nil),
call("exit", stub.ExpectArgs{0}, nil, nil),
@@ -2283,7 +2283,7 @@ func TestInitEntrypoint(t *testing.T) {
/* wait4 */
Tracks: []stub.Expect{{Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
// this terminates the goroutine at the call, preventing it from leaking while preserving behaviour
call("wait4", stub.ExpectArgs{-1, nil, 0, nil, stub.PanicExit}, 0, syscall.ECHILD),
@@ -2366,7 +2366,7 @@ func TestInitEntrypoint(t *testing.T) {
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(5)}}, nil, nil),
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
call("verbosef", stub.ExpectArgs{"initial process exited with signal %s", []any{syscall.Signal(0x4e)}}, nil, nil),
@@ -2377,7 +2377,7 @@ func TestInitEntrypoint(t *testing.T) {
/* wait4 */
Tracks: []stub.Expect{{Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil}, 0xbad, nil),
// this terminates the goroutine at the call, preventing it from leaking while preserving behaviour
@@ -2461,7 +2461,7 @@ func TestInitEntrypoint(t *testing.T) {
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(3)}}, nil, nil),
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
call("verbosef", stub.ExpectArgs{"initial process exited with signal %s", []any{syscall.Signal(0x4e)}}, nil, nil),
@@ -2471,7 +2471,7 @@ func TestInitEntrypoint(t *testing.T) {
/* wait4 */
Tracks: []stub.Expect{{Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
@@ -2599,7 +2599,7 @@ func TestInitEntrypoint(t *testing.T) {
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(1)}}, nil, nil),
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil),
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
call("verbosef", stub.ExpectArgs{"initial process exited with code %d", []any{1}}, nil, nil),
@@ -2609,7 +2609,7 @@ func TestInitEntrypoint(t *testing.T) {
/* wait4 */
Tracks: []stub.Expect{{Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
@@ -2741,7 +2741,7 @@ func TestInitEntrypoint(t *testing.T) {
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(0)}}, nil, nil),
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/bin/zsh")}}, nil, nil),
call("start", stub.ExpectArgs{"/bin/zsh", []string{"zsh", "-c", "exec vim"}, []string{"DISPLAY=:0"}, "/.hakurei"}, &os.Process{Pid: 0xcafe}, nil),
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil),
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
@@ -2752,7 +2752,7 @@ func TestInitEntrypoint(t *testing.T) {
/* wait4 */
Tracks: []stub.Expect{{Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil),
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),

16
flake.lock generated
View File

@@ -7,32 +7,32 @@
]
},
"locked": {
"lastModified": 1756679287,
"narHash": "sha256-Xd1vOeY9ccDf5VtVK12yM0FS6qqvfUop8UQlxEB+gTQ=",
"lastModified": 1765384171,
"narHash": "sha256-FuFtkJrW1Z7u+3lhzPRau69E0CNjADku1mLQQflUORo=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "07fc025fe10487dd80f2ec694f1cd790e752d0e8",
"rev": "44777152652bc9eacf8876976fa72cc77ca8b9d8",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "release-25.05",
"ref": "release-25.11",
"repo": "home-manager",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1757020766,
"narHash": "sha256-PLoSjHRa2bUbi1x9HoXgTx2AiuzNXs54c8omhadyvp0=",
"lastModified": 1765311797,
"narHash": "sha256-mSD5Ob7a+T2RNjvPvOA1dkJHGVrNVl8ZOrAwBjKBDQo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "fe83bbdde2ccdc2cb9573aa846abe8363f79a97a",
"rev": "09eb77e94fa25202af8f3e81ddc7353d9970ac1b",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.05",
"ref": "nixos-25.11",
"repo": "nixpkgs",
"type": "github"
}

View File

@@ -2,10 +2,10 @@
description = "hakurei container tool and nixos module";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
home-manager = {
url = "github:nix-community/home-manager/release-25.05";
url = "github:nix-community/home-manager/release-25.11";
inputs.nixpkgs.follows = "nixpkgs";
};
};
@@ -185,13 +185,13 @@
hakurei =
let
# this is used for interactive vm testing during development, where tests might be broken
package = self.packages.${pkgs.system}.hakurei.override {
package = self.packages.${pkgs.stdenv.hostPlatform.system}.hakurei.override {
buildGoModule = previousArgs: pkgs.pkgsStatic.buildGoModule (previousArgs // { doCheck = false; });
};
in
{
inherit package;
hsuPackage = self.packages.${pkgs.system}.hsu.override { hakurei = package; };
hsuPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.hsu.override { hakurei = package; };
};
};
}

View File

@@ -30,12 +30,46 @@ type Config struct {
// This option is unsupported and most likely enables full control over the Wayland
// session. Do not set this to true unless you are sure you know what you are doing.
DirectWayland bool `json:"direct_wayland,omitempty"`
// Direct access to the PipeWire socket established via SecurityContext::Create, no
// attempt is made to start the pipewire-pulse server.
//
// The SecurityContext machinery is fatally flawed, it blindly sets read and execute
// bits on all objects for clients with the lowest achievable privilege level (by
// setting PW_KEY_ACCESS to "restricted"). This enables them to call any method
// targeting any object, and since Registry::Destroy checks for the read and execute bit,
// allows the destruction of any object other than PW_ID_CORE as well. This behaviour
// is implemented separately in media-session and wireplumber, with the wireplumber
// implementation in Lua via an embedded Lua vm. In all known setups, wireplumber is
// in use, and there is no known way to change its behaviour and set permissions
// differently without replacing the Lua script. Also, since PipeWire relies on these
// permissions to work, reducing them is not possible.
//
// Currently, the only other sandboxed use case is flatpak, which is not aware of
// PipeWire and blindly exposes the bare PulseAudio socket to the container (behaves
// like DirectPulse). This socket is backed by the pipewire-pulse compatibility daemon,
// which obtains client pid via the SO_PEERCRED option. The PipeWire daemon, pipewire-pulse
// daemon and the session manager daemon then separately performs the /.flatpak-info hack
// described in https://git.gensokyo.uk/security/hakurei/issues/21. Under such use case,
// since the client has no direct access to PipeWire, insecure parts of the protocol are
// obscured by pipewire-pulse simply not implementing them, and thus hiding the flaws
// described above.
//
// Hakurei does not rely on the /.flatpak-info hack. Instead, a socket is sets up via
// SecurityContext. A pipewire-pulse server connected through it achieves the same
// permissions as flatpak does via the /.flatpak-info hack and is maintained for the
// life of the container.
//
// This option is unsupported and enables a denial-of-service attack as the sandboxed
// client is able to destroy any client object and thus disconnecting them from PipeWire,
// or destroy the SecurityContext object preventing any further container creation.
// Do not set this to true, it is insecure under any configuration.
DirectPipeWire bool `json:"direct_pipewire,omitempty"`
// Direct access to PulseAudio socket, no attempt is made to establish pipewire-pulse
// server via a PipeWire socket with a SecurityContext attached and the bare socket
// is made available to the container.
//
// This option is unsupported and enables arbitrary code execution as the PulseAudio
// server. Do not set this to true, this is insecure under any configuration.
// server. Do not set this to true, it is insecure under any configuration.
DirectPulse bool `json:"direct_pulse,omitempty"`
// Extra acl updates to perform before setuid.

View File

@@ -53,6 +53,10 @@ type syscallDispatcher interface {
readdir(name string) ([]os.DirEntry, error)
// tempdir provides [os.TempDir].
tempdir() string
// mkdir provides [os.Mkdir].
mkdir(name string, perm os.FileMode) error
// removeAll provides [os.RemoveAll].
removeAll(path string) error
// exit provides [os.Exit].
exit(code int)
@@ -62,6 +66,8 @@ type syscallDispatcher interface {
// lookupGroupId calls [user.LookupGroup] and returns the Gid field of the resulting [user.Group] struct.
lookupGroupId(name string) (string, error)
// lookPath provides exec.LookPath.
lookPath(file string) (string, error)
// cmdOutput provides the Output method of [exec.Cmd].
cmdOutput(cmd *exec.Cmd) ([]byte, error)
@@ -121,6 +127,8 @@ func (direct) stat(name string) (os.FileInfo, error) { return os.Stat(name)
func (direct) open(name string) (osFile, error) { return os.Open(name) }
func (direct) readdir(name string) ([]os.DirEntry, error) { return os.ReadDir(name) }
func (direct) tempdir() string { return os.TempDir() }
func (direct) mkdir(name string, perm os.FileMode) error { return os.Mkdir(name, perm) }
func (direct) removeAll(path string) error { return os.RemoveAll(path) }
func (direct) exit(code int) { os.Exit(code) }
func (direct) evalSymlinks(path string) (string, error) { return filepath.EvalSymlinks(path) }
@@ -134,6 +142,7 @@ func (direct) lookupGroupId(name string) (gid string, err error) {
return
}
func (direct) lookPath(file string) (string, error) { return exec.LookPath(file) }
func (direct) cmdOutput(cmd *exec.Cmd) ([]byte, error) { return cmd.Output() }
func (direct) notifyContext(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc) {

View File

@@ -701,10 +701,13 @@ func (panicDispatcher) stat(string) (os.FileInfo, error) { pa
func (panicDispatcher) open(string) (osFile, error) { panic("unreachable") }
func (panicDispatcher) readdir(string) ([]os.DirEntry, error) { panic("unreachable") }
func (panicDispatcher) tempdir() string { panic("unreachable") }
func (panicDispatcher) mkdir(string, os.FileMode) error { panic("unreachable") }
func (panicDispatcher) removeAll(string) error { panic("unreachable") }
func (panicDispatcher) exit(int) { panic("unreachable") }
func (panicDispatcher) evalSymlinks(string) (string, error) { panic("unreachable") }
func (panicDispatcher) prctl(uintptr, uintptr, uintptr) error { panic("unreachable") }
func (panicDispatcher) lookupGroupId(string) (string, error) { panic("unreachable") }
func (panicDispatcher) lookPath(string) (string, error) { panic("unreachable") }
func (panicDispatcher) cmdOutput(*exec.Cmd) ([]byte, error) { panic("unreachable") }
func (panicDispatcher) overflowUid(message.Msg) int { panic("unreachable") }
func (panicDispatcher) overflowGid(message.Msg) int { panic("unreachable") }

View File

@@ -70,7 +70,7 @@ type outcomeState struct {
// Copied from their respective exported values.
mapuid, mapgid *stringPair[int]
// Copied from [EnvPaths] per-process.
// Copied from [env.Paths] per-process.
sc hst.Paths
*env.Paths
@@ -172,6 +172,8 @@ type outcomeStateSys struct {
// Copied from [hst.Config]. Safe for read by spWaylandOp.toSystem only.
directWayland bool
// Copied from [hst.Config]. Safe for read by spPipeWireOp.toSystem only.
directPipeWire bool
// Copied from [hst.Config]. Safe for read by spPulseOp.toSystem only.
directPulse bool
// Copied header from [hst.Config]. Safe for read by spFilesystemOp.toSystem only.
@@ -187,9 +189,8 @@ type outcomeStateSys struct {
func (s *outcomeState) newSys(config *hst.Config, sys *system.I) *outcomeStateSys {
return &outcomeStateSys{
appId: config.ID, et: config.Enablements.Unwrap(),
directWayland: config.DirectWayland, directPulse: config.DirectPulse,
extraPerms: config.ExtraPerms,
sessionBus: config.SessionBus, systemBus: config.SystemBus,
directWayland: config.DirectWayland, directPipeWire: config.DirectPipeWire, directPulse: config.DirectPulse,
extraPerms: config.ExtraPerms, sessionBus: config.SessionBus, systemBus: config.SystemBus,
sys: sys, outcomeState: s,
}
}
@@ -256,6 +257,10 @@ type outcomeStateParams struct {
// Populated by spRuntimeOp.
runtimeDir *check.Absolute
// Path to pipewire-pulse server.
// Populated by spPipeWireOp if DirectPipeWire is false.
pipewirePulsePath *check.Absolute
as hst.ApplyState
*outcomeState
}
@@ -295,7 +300,7 @@ func (state *outcomeStateSys) toSystem() error {
// optional via enablements
&spWaylandOp{},
&spX11Op{},
spPipeWireOp{},
&spPipeWireOp{},
&spPulseOp{},
&spDBusOp{},

View File

@@ -68,7 +68,11 @@ func TestOutcomeRun(t *testing.T) {
).
// spPipeWireOp
PipeWire(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pipewire")).
PipeWire(
m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pipewire"),
"org.chromium.Chromium",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
).
// spDBusOp
MustProxyDBus(
@@ -96,7 +100,6 @@ func TestOutcomeRun(t *testing.T) {
"GOOGLE_DEFAULT_CLIENT_ID=77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET=OTJgUOQcT7lO7GsGZq2G4IlT",
"HOME=/data/data/org.chromium.Chromium",
"PIPEWIRE_REMOTE=/run/user/1971/pipewire-0",
"SHELL=/run/current-system/sw/bin/zsh",
"TERM=xterm-256color",
"USER=chronos",
@@ -146,9 +149,6 @@ func TestOutcomeRun(t *testing.T) {
// spWaylandOp
Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/wayland"), m("/run/user/1971/wayland-0"), 0).
// spPipeWireOp
Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pipewire"), m("/run/user/1971/pipewire-0"), 0).
// spDBusOp
Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bus"), m("/run/user/1971/bus"), 0).
Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/system_bus_socket"), m("/var/run/dbus/system_bus_socket"), 0).
@@ -170,7 +170,7 @@ func TestOutcomeRun(t *testing.T) {
Remount(fhs.AbsRoot, syscall.MS_RDONLY),
}},
{"nixos permissive defaults no enablements", new(stubNixOS), &hst.Config{Container: &hst.ContainerConfig{
{"nixos permissive defaults no enablements", new(stubNixOS), &hst.Config{DirectPipeWire: true, Container: &hst.ContainerConfig{
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{
Target: fhs.AbsRoot,
@@ -252,6 +252,8 @@ func TestOutcomeRun(t *testing.T) {
}},
{"nixos permissive defaults chromium", new(stubNixOS), &hst.Config{
DirectPipeWire: true,
ID: "org.chromium.Chromium",
Identity: 9,
Groups: []string{"video"},
@@ -335,7 +337,7 @@ func TestOutcomeRun(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).
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").
PipeWire(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/pipewire")).
PipeWire(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/pipewire"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
MustProxyDBus(&hst.BusConfig{
Talk: []string{
"org.freedesktop.Notifications",
@@ -422,6 +424,8 @@ func TestOutcomeRun(t *testing.T) {
}},
{"nixos chromium direct wayland", new(stubNixOS), &hst.Config{
DirectPipeWire: true,
ID: "org.chromium.Chromium",
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
Container: &hst.ContainerConfig{
@@ -486,7 +490,7 @@ func TestOutcomeRun(t *testing.T) {
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).
Ephemeral(system.Process, m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1"), 0711).
PipeWire(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/pipewire")).
PipeWire(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/pipewire"), "org.chromium.Chromium", "8e2c76b066dabe574cf073bdb46eb5c1").
MustProxyDBus(&hst.BusConfig{
Talk: []string{
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
@@ -879,6 +883,16 @@ func (k *stubNixOS) lookupGroupId(name string) (string, error) {
}
}
func (k *stubNixOS) lookPath(file string) (string, error) {
switch file {
case "pipewire-pulse":
return "/run/current-system/sw/bin/pipewire-pulse", nil
default:
panic(fmt.Sprintf("unexpected file %q", file))
}
}
func (k *stubNixOS) cmdOutput(cmd *exec.Cmd) ([]byte, error) {
switch cmd.Path {
case "/proc/nonexistent/hsu":

View File

@@ -14,9 +14,12 @@ import (
"time"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/hst"
"hakurei.app/internal/pipewire"
"hakurei.app/message"
)
@@ -83,6 +86,55 @@ func Shim(msg message.Msg) {
shimEntrypoint(direct{msg})
}
// A shimPrivate holds state of the private work directory owned by shim.
type shimPrivate struct {
// Path to directory if created.
pathname *check.Absolute
k syscallDispatcher
id *stringPair[hst.ID]
}
// unwrap returns the underlying pathname.
func (sp *shimPrivate) unwrap() *check.Absolute {
if sp.pathname == nil {
if a, err := check.NewAbs(sp.k.tempdir()); err != nil {
sp.k.fatal(err)
panic("unreachable")
} else {
pathname := a.Append(".hakurei-shim-" + sp.id.String())
sp.k.getMsg().Verbosef("creating private work directory %q", pathname)
if err = sp.k.mkdir(pathname.String(), 0700); err != nil {
sp.k.fatal(err)
panic("unreachable")
}
sp.pathname = pathname
return sp.unwrap()
}
} else {
return sp.pathname
}
}
// String returns the absolute pathname to the directory held by shimPrivate.
func (sp *shimPrivate) String() string { return sp.unwrap().String() }
// destroy removes the directory held by shimPrivate.
func (sp *shimPrivate) destroy() {
defer func() { sp.pathname = nil }()
if sp.pathname != nil {
sp.k.getMsg().Verbosef("destroying private work directory %q", sp.pathname)
if err := sp.k.removeAll(sp.pathname.String()); err != nil {
sp.k.getMsg().GetLogger().Println(err)
}
}
}
const (
// shimPipeWireTimeout is the duration pipewire-pulse is allowed to run before its socket becomes available.
shimPipeWireTimeout = 5 * time.Second
)
func shimEntrypoint(k syscallDispatcher) {
msg := k.getMsg()
if msg == nil {
@@ -208,6 +260,7 @@ func shimEntrypoint(k syscallDispatcher) {
ctx, stop := k.notifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
cancelContainer.Store(&stop)
sp := shimPrivate{k: k, id: state.id}
z := container.New(ctx, msg)
z.Params = *stateParams.params
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
@@ -215,6 +268,79 @@ func shimEntrypoint(k syscallDispatcher) {
// bounds and default enforced in finalise.go
z.WaitDelay = state.Shim.WaitDelay
if stateParams.pipewirePulsePath != nil {
zpw := container.NewCommand(ctx, msg, stateParams.pipewirePulsePath, pipewirePulseName)
zpw.Hostname = "hakurei-" + pipewirePulseName
zpw.SeccompFlags |= seccomp.AllowMultiarch
zpw.SeccompPresets |= std.PresetStrict
zpw.Env = []string{
// pipewire SecurityContext socket path
pipewire.Remote + "=" + stateParams.instancePath().Append("pipewire").String(),
// pipewire-pulse socket directory path
envXDGRuntimeDir + "=" + sp.String(),
}
if msg.IsVerbose() {
zpw.Stdin, zpw.Stdout, zpw.Stderr = os.Stdin, os.Stdout, os.Stderr
}
zpw.
Bind(fhs.AbsRoot, fhs.AbsRoot, 0).
Bind(sp.unwrap(), sp.unwrap(), std.BindWritable).
Proc(fhs.AbsProc).Dev(fhs.AbsDev, true)
socketPath := sp.unwrap().Append("pulse", "native")
innerSocketPath := stateParams.runtimeDir.Append("pulse", "native")
if err := k.containerStart(zpw); err != nil {
sp.destroy()
printMessageError(func(v ...any) { k.fatal(fmt.Sprintln(v...)) },
"cannot start "+pipewirePulseName+" container:", err)
}
if err := k.containerServe(zpw); err != nil {
sp.destroy()
printMessageError(func(v ...any) { k.fatal(fmt.Sprintln(v...)) },
"cannot configure "+pipewirePulseName+" container:", err)
}
done := make(chan error, 1)
k.new(func(k syscallDispatcher, msg message.Msg) { done <- k.containerWait(zpw) })
socketTimer := time.NewTimer(shimPipeWireTimeout)
for {
select {
case <-socketTimer.C:
sp.destroy()
k.fatal(pipewirePulseName + " exceeded deadline before socket appeared")
break
case err := <-done:
var exitError *exec.ExitError
if !errors.As(err, &exitError) {
msg.Verbosef("cannot wait: %v", err)
k.exit(127)
}
sp.destroy()
k.fatal(pipewirePulseName + " " + exitError.ProcessState.String())
break
default:
if _, err := k.stat(socketPath.String()); err != nil {
if !errors.Is(err, os.ErrNotExist) {
sp.destroy()
k.fatal(err)
break
}
time.Sleep(500 * time.Microsecond)
continue
}
}
break
}
z.Bind(socketPath, innerSocketPath, 0)
z.Env = append(z.Env, "PULSE_SERVER=unix:"+innerSocketPath.String())
}
if err := k.containerStart(z); err != nil {
var f func(v ...any)
if logger := msg.GetLogger(); logger != nil {
@@ -225,9 +351,11 @@ func shimEntrypoint(k syscallDispatcher) {
}
}
printMessageError(f, "cannot start container:", err)
sp.destroy()
k.exit(hst.ExitFailure)
}
if err := k.containerServe(z); err != nil {
sp.destroy()
printMessageError(func(v ...any) { k.fatal(fmt.Sprintln(v...)) },
"cannot configure container:", err)
}
@@ -236,10 +364,13 @@ func shimEntrypoint(k syscallDispatcher) {
seccomp.Preset(std.PresetStrict, seccomp.AllowMultiarch),
seccomp.AllowMultiarch,
); err != nil {
sp.destroy()
k.fatalf("cannot load syscall filter: %v", err)
}
if err := k.containerWait(z); err != nil {
sp.destroy()
var exitError *exec.ExitError
if !errors.As(err, &exitError) {
if errors.Is(err, context.Canceled) {
@@ -250,4 +381,5 @@ func shimEntrypoint(k syscallDispatcher) {
}
k.exit(exitError.ExitCode())
}
sp.destroy()
}

View File

@@ -3,29 +3,51 @@ package outcome
import (
"encoding/gob"
"hakurei.app/container/check"
"hakurei.app/hst"
"hakurei.app/internal/pipewire"
)
func init() { gob.Register(spPipeWireOp{}) }
const pipewirePulseName = "pipewire-pulse"
func init() { gob.Register(new(spPipeWireOp)) }
// spPipeWireOp exports the PipeWire server to the container via SecurityContext.
// Runs after spRuntimeOp.
type spPipeWireOp struct{}
type spPipeWireOp struct {
// Path to pipewire-pulse server. Populated during toSystem if DirectPipeWire is false.
CompatServerPath *check.Absolute
}
func (s spPipeWireOp) toSystem(state *outcomeStateSys) error {
func (s *spPipeWireOp) toSystem(state *outcomeStateSys) error {
if state.et&hst.EPipeWire == 0 {
return errNotEnabled
}
if !state.directPipeWire {
if n, err := state.k.lookPath(pipewirePulseName); err != nil {
return &hst.AppError{Step: "look up " + pipewirePulseName, Err: err}
} else if s.CompatServerPath, err = check.NewAbs(n); err != nil {
return err
}
}
state.sys.PipeWire(state.instance().Append("pipewire"))
appId := state.appId
if appId == "" {
// use instance ID in case app id is not set
appId = "app.hakurei." + state.id.String()
}
state.sys.PipeWire(state.instance().Append("pipewire"), appId, state.id.String())
return nil
}
func (s spPipeWireOp) toContainer(state *outcomeStateParams) error {
innerPath := state.runtimeDir.Append(pipewire.PW_DEFAULT_REMOTE)
state.env[pipewire.Remote] = innerPath.String()
state.params.Bind(state.instancePath().Append("pipewire"), innerPath, 0)
func (s *spPipeWireOp) toContainer(state *outcomeStateParams) error {
if s.CompatServerPath == nil {
innerPath := state.runtimeDir.Append(pipewire.PW_DEFAULT_REMOTE)
state.env[pipewire.Remote] = innerPath.String()
state.params.Bind(state.instancePath().Append("pipewire"), innerPath, 0)
}
// pipewire-pulse behaviour implemented in shim.go
state.pipewirePulsePath = s.CompatServerPath
return nil
}

View File

@@ -16,7 +16,7 @@ func TestSpPipeWireOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{
{"not enabled", func(bool, bool) outcomeOp {
return spPipeWireOp{}
return new(spPipeWireOp)
}, func() *hst.Config {
c := hst.Template()
*c.Enablements = 0
@@ -24,13 +24,19 @@ func TestSpPipeWireOp(t *testing.T) {
}, nil, nil, nil, nil, errNotEnabled, nil, nil, nil, nil, nil},
{"success", func(bool, bool) outcomeOp {
return spPipeWireOp{}
}, hst.Template, nil, []stub.Call{}, newI().
return new(spPipeWireOp)
}, func() *hst.Config {
c := hst.Template()
c.DirectPipeWire = true
return c
}, nil, []stub.Call{}, newI().
// state.instance
Ephemeral(system.Process, m(wantInstancePrefix), 0711).
// toSystem
PipeWire(
m(wantInstancePrefix + "/pipewire"),
m(wantInstancePrefix+"/pipewire"),
"org.chromium.Chromium",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
), sysUsesInstance(nil), nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{
// this op configures the container state and does not make calls during toContainer
}, &container.Params{

View File

@@ -103,6 +103,8 @@ type Client struct {
// Populated by [CoreBoundProps] events targeting [Client].
Properties SPADict `json:"props"`
noRemove
}
func (client *Client) consume(opcode byte, files []int, unmarshal func(v any)) error {
@@ -113,7 +115,7 @@ func (client *Client) consume(opcode byte, files []int, unmarshal func(v any)) e
return nil
default:
return &UnsupportedOpcodeError{opcode, client.String()}
panic(&UnsupportedOpcodeError{opcode, client.String()})
}
}

View File

@@ -40,13 +40,14 @@ const (
PW_CORE_EVENT_ADD_MEM
PW_CORE_EVENT_REMOVE_MEM
PW_CORE_EVENT_BOUND_PROPS
PW_CORE_EVENT_NUM
PW_CORE_EVENT_NUM
PW_VERSION_CORE_EVENTS = 1
)
const (
PW_CORE_METHOD_ADD_LISTENER = iota
PW_CORE_METHOD_HELLO
PW_CORE_METHOD_SYNC
PW_CORE_METHOD_PONG
@@ -54,25 +55,26 @@ const (
PW_CORE_METHOD_GET_REGISTRY
PW_CORE_METHOD_CREATE_OBJECT
PW_CORE_METHOD_DESTROY
PW_CORE_METHOD_NUM
PW_CORE_METHOD_NUM
PW_VERSION_CORE_METHODS = 0
)
const (
PW_REGISTRY_EVENT_GLOBAL = iota
PW_REGISTRY_EVENT_GLOBAL_REMOVE
PW_REGISTRY_EVENT_NUM
PW_REGISTRY_EVENT_NUM
PW_VERSION_REGISTRY_EVENTS = 0
)
const (
PW_REGISTRY_METHOD_ADD_LISTENER = iota
PW_REGISTRY_METHOD_BIND
PW_REGISTRY_METHOD_DESTROY
PW_REGISTRY_METHOD_NUM
PW_REGISTRY_METHOD_NUM
PW_VERSION_REGISTRY_METHODS = 0
)
@@ -266,6 +268,31 @@ type CoreErrorEvent struct{ CoreError }
// Opcode satisfies [Message] with a constant value.
func (c *CoreErrorEvent) Opcode() byte { return PW_CORE_EVENT_ERROR }
// The CoreRemoveId event is used internally by the object ID management logic.
//
// When a client deletes an object, the server will send this event to acknowledge
// that it has seen the delete request. When the client receives this event, it
// will know that it can safely reuse the object ID.
type CoreRemoveId struct {
// A proxy id that was removed.
ID Int `json:"id"`
}
// Opcode satisfies [Message] with a constant value.
func (c *CoreRemoveId) Opcode() byte { return PW_CORE_EVENT_REMOVE_ID }
// FileCount satisfies [Message] with a constant value.
func (c *CoreRemoveId) FileCount() Int { return 0 }
// Size satisfies [KnownSize] with a constant value.
func (c *CoreRemoveId) Size() Word { return SizePrefix + Size(SizeInt) }
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreRemoveId) MarshalBinary() ([]byte, error) { return Marshal(c) }
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreRemoveId) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
// The CoreBoundProps event is emitted when a local object ID is bound to a global ID.
// It is emitted before the global becomes visible in the registry.
type CoreBoundProps struct {
@@ -299,13 +326,96 @@ func (c *CoreBoundProps) UnmarshalBinary(data []byte) error { return Unmarshal(d
// ErrBadBoundProps is returned when a [CoreBoundProps] event targeting a proxy
// that should never be targeted is received and processed.
var ErrBadBoundProps = errors.New("attempted to store bound props on proxy that should never be targeted")
var ErrBadBoundProps = errors.New("attempting to store bound props on a proxy that should never be targeted")
// noAck is embedded by proxies that are never targeted by [CoreBoundProps].
type noAck struct{}
// setBoundProps should never be called as this proxy should never be targeted by [CoreBoundProps].
func (noAck) setBoundProps(*CoreBoundProps) error { return ErrBadBoundProps }
func (noAck) setBoundProps(*CoreBoundProps) error { panic(ErrBadBoundProps) }
// ErrBadRemove is returned when a [CoreRemoveId] event targeting a proxy
// that should never be targeted is received and processed.
var ErrBadRemove = errors.New("attempting to remove a proxy that should never be targeted")
// noRemove is embedded by proxies that are never targeted by [CoreRemoveId].
type noRemove struct{}
// remove should never be called as this proxy should never be targeted by [CoreRemoveId].
func (noRemove) remove() error { panic(ErrBadRemove) }
// ErrInvalidRemove is returned when a proxy is somehow removed twice. This is only reached for
// an implementation error as the proxy struct should no longer be reachable after the first call.
var ErrInvalidRemove = errors.New("attempting to remove an already freed proxy")
// removable is embedded by proxies that can be targeted by [CoreRemoveId] and requires no cleanup.
type removable bool
// remove checks against removal of a freed proxy and marks the proxy as removed.
func (s *removable) remove() error {
if *s {
panic(ErrInvalidRemove)
}
*s = true
return nil
}
// ErrProxyDestroyed is returned when attempting to use a proxy method when the underlying
// proxy has already been targeted by a [CoreRemoveId] event.
var ErrProxyDestroyed = errors.New("underlying proxy has been removed")
// checkDestroy returns [ErrProxyDestroyed] if the current proxy has been destroyed.
// Must be called at the beginning of any exported method of a proxy embedding removable.
func (s *removable) checkDestroy() error {
if *s {
// not fatal: the caller is allowed to recover from this and allocate a new proxy
return ErrProxyDestroyed
}
return nil
}
// mustCheckDestroy calls checkDestroy and panics if a non-nil error is returned.
// This is useful for non-exported methods as they should become unreachable.
func (s *removable) mustCheckDestroy() {
if err := s.checkDestroy(); err != nil {
panic(err)
}
}
// destructible is embedded by proxies that can be targeted by the [CoreRemoveId] event and the
// [CoreDestroy] method and requires no cleanup. destructible purposefully does not override
// removable.mustCheckDestroy because it is used by unexported methods called during event handling
// and are exempt from the destruction check.
type destructible struct {
destroyed bool
removable
}
// checkDestroy overrides removable.checkDestroy to also check the destroyed field.
func (s *destructible) checkDestroy() error {
if s.destroyed {
return ErrProxyDestroyed
}
if err := s.removable.checkDestroy(); err != nil {
return err
}
return nil
}
// destroy calls removable.checkDestroy then queues a [CoreDestroy] event if it succeeds.
func (s *destructible) destroy(ctx *Context, id Int) error {
if err := s.checkDestroy(); err != nil {
return err
}
l := len(ctx.pendingDestruction)
ctx.pendingDestruction[id] = struct{}{}
if len(ctx.pendingDestruction) != l+1 {
return ErrProxyDestroyed
}
s.destroyed = true
return ctx.GetCore().destroy(id)
}
// An InconsistentIdError describes an inconsistent state where the server claims an impossible
// proxy or global id. This is only generated by the [CoreBoundProps] event.
@@ -349,10 +459,10 @@ func (c *CoreHello) MarshalBinary() ([]byte, error) { return Marshal(c) }
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreHello) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
// coreHello queues a [CoreHello] message for the PipeWire server.
// This method should not be called directly, the New function queues this message.
func (ctx *Context) coreHello() error {
return ctx.writeMessage(
// hello queues a [CoreHello] message for the PipeWire server.
// This method should not be called directly, the [New] function queues this message.
func (core *Core) hello() error {
return core.ctx.writeMessage(
PW_ID_CORE,
&CoreHello{PW_VERSION_CORE},
)
@@ -388,12 +498,12 @@ func (c *CoreSync) MarshalBinary() ([]byte, error) { return Marshal(c) }
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreSync) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
// coreSync queues a [CoreSync] message for the PipeWire server.
// sync queues a [CoreSync] message for the PipeWire server.
// This is not safe to use directly, callers should use Sync instead.
func (ctx *Context) coreSync(id Int) error {
return ctx.writeMessage(
func (core *Core) sync(id Int) error {
return core.ctx.writeMessage(
PW_ID_CORE,
&CoreSync{id, CoreSyncSequenceOffset + Int(ctx.sequence)},
&CoreSync{id, CoreSyncSequenceOffset + Int(core.ctx.sequence)},
)
}
@@ -410,7 +520,7 @@ const (
// Sync queues a [CoreSync] message for the PipeWire server and initiates a Roundtrip.
func (core *Core) Sync() error {
core.done = false
if err := core.ctx.coreSync(roundtripSyncID); err != nil {
if err := core.sync(roundtripSyncID); err != nil {
return err
}
deadline := time.Now().Add(syncTimeout)
@@ -429,6 +539,10 @@ func (core *Core) Sync() error {
core.ctx.closeReceivedFiles()
return &ProxyFatalError{Err: UnacknowledgedProxyError(slices.Collect(maps.Keys(core.ctx.pendingIds))), ProxyErrs: core.ctx.cloneAsProxyErrors()}
}
if len(core.ctx.pendingDestruction) != 0 {
core.ctx.closeReceivedFiles()
return &ProxyFatalError{Err: UnacknowledgedProxyDestructionError(slices.Collect(maps.Keys(core.ctx.pendingDestruction))), ProxyErrs: core.ctx.cloneAsProxyErrors()}
}
return core.ctx.doSyncComplete()
}
@@ -497,6 +611,89 @@ func (ctx *Context) GetRegistry() (*Registry, error) {
)
}
// CoreCreateObject is sent when the client requests to create a
// new object from a factory of a certain type.
//
// The client allocates a new_id for the proxy. The server will
// allocate a new resource with the same new_id and from then on,
// Methods and Events will be exchanged between the new object of
// the given type.
type CoreCreateObject struct {
// The name of a server factory object to use.
FactoryName String `json:"factory_name"`
// The type of the object to create, this is also the type of
// the interface of the new_id proxy.
Type String `json:"type"`
// Undocumented, assumed to be the local version of the proxy.
Version Int `json:"version"`
// Extra properties to create the object.
Properties *SPADict `json:"props"`
// The proxy id of the new object.
NewID Int `json:"new_id"`
}
// Opcode satisfies [Message] with a constant value.
func (c *CoreCreateObject) Opcode() byte { return PW_CORE_METHOD_CREATE_OBJECT }
// FileCount satisfies [Message] with a constant value.
func (c *CoreCreateObject) FileCount() Int { return 0 }
// Size satisfies [KnownSize] with a value computed at runtime.
func (c *CoreCreateObject) Size() Word {
return SizePrefix +
SizeString[Word](c.FactoryName) +
SizeString[Word](c.Type) +
Size(SizeInt) +
c.Properties.Size() +
Size(SizeInt)
}
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreCreateObject) MarshalBinary() ([]byte, error) { return Marshal(c) }
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreCreateObject) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
// createObject queues a [CoreCreateObject] message for the PipeWire server.
// This is not safe to use directly, callers should use typed wrapper methods on [Registry] instead.
func (core *Core) createObject(factoryName, typeName String, version Int, props SPADict, newId Int) error {
return core.ctx.writeMessage(
PW_ID_CORE,
&CoreCreateObject{factoryName, typeName, version, &props, newId},
)
}
// CoreDestroy is sent when the client requests to destroy an object.
type CoreDestroy struct {
// The proxy id of the object to destroy.
ID Int `json:"id"`
}
// Opcode satisfies [Message] with a constant value.
func (c *CoreDestroy) Opcode() byte { return PW_CORE_METHOD_DESTROY }
// FileCount satisfies [Message] with a constant value.
func (c *CoreDestroy) FileCount() Int { return 0 }
// Size satisfies [KnownSize] with a constant value.
func (c *CoreDestroy) Size() Word { return SizePrefix + Size(SizeInt) }
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreDestroy) MarshalBinary() ([]byte, error) { return Marshal(c) }
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreDestroy) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
// destroy queues a [CoreDestroy] message for the PipeWire server.
// This is not safe to use directly, callers should use the exported method
// on the proxy implementation instead.
func (core *Core) destroy(id Int) error {
return core.ctx.writeMessage(
PW_ID_CORE,
&CoreDestroy{id},
)
}
// A RegistryGlobal event is emitted to notify a client about a new global object.
type RegistryGlobal struct {
// The global id.
@@ -533,6 +730,30 @@ func (c *RegistryGlobal) MarshalBinary() ([]byte, error) { return Marshal(c) }
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *RegistryGlobal) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
// A RegistryGlobalRemove event is emitted when a global with id was removed.
type RegistryGlobalRemove struct {
// The global id that was removed.
ID Int `json:"id"`
}
// Opcode satisfies [Message] with a constant value.
func (c *RegistryGlobalRemove) Opcode() byte { return PW_REGISTRY_EVENT_GLOBAL_REMOVE }
// FileCount satisfies [Message] with a constant value.
func (c *RegistryGlobalRemove) FileCount() Int { return 0 }
// Size satisfies [KnownSize] with a constant value.
func (c *RegistryGlobalRemove) Size() Word {
return SizePrefix +
Size(SizeInt)
}
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *RegistryGlobalRemove) MarshalBinary() ([]byte, error) { return Marshal(c) }
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *RegistryGlobalRemove) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
// RegistryBind is sent when the client requests to bind to the
// global object with id and use the client proxy with new_id as
// the proxy. After this call, methods can be sent to the remote
@@ -584,10 +805,70 @@ func (registry *Registry) bind(proxy eventProxy, id, version Int) (Int, error) {
)
}
// RegistryDestroy is sent to try to destroy the global object with id.
// This might fail when the client does not have permission.
type RegistryDestroy struct {
// The global id to destroy.
ID Int `json:"id"`
}
// Opcode satisfies [Message] with a constant value.
func (c *RegistryDestroy) Opcode() byte { return PW_REGISTRY_METHOD_DESTROY }
// FileCount satisfies [Message] with a constant value.
func (c *RegistryDestroy) FileCount() Int { return 0 }
// Size satisfies [KnownSize] with a constant value.
func (c *RegistryDestroy) Size() Word {
return SizePrefix +
Size(SizeInt)
}
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *RegistryDestroy) MarshalBinary() ([]byte, error) { return Marshal(c) }
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *RegistryDestroy) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
// destroy queues a [RegistryDestroy] message for the PipeWire server.
func (registry *Registry) destroy(id Int) error {
return registry.ctx.writeMessage(
registry.ID,
&RegistryDestroy{id},
)
}
// Destroy tries to destroy the global object with id.
func (registry *Registry) Destroy(id Int) (err error) {
asCoreError := registry.ctx.expectsCoreError(registry.ID, &err)
if err != nil {
return
}
if err = registry.destroy(id); err != nil {
return err
}
if err = registry.ctx.GetCore().Sync(); err == nil {
return nil
}
if coreError := asCoreError(); coreError == nil {
return
} else {
switch syscall.Errno(-coreError.Result) {
case syscall.EPERM:
return &PermissionError{registry.ID, coreError.Message}
default:
return coreError
}
}
}
// An UnsupportedObjectTypeError is the name of a type not known by the server [Registry].
type UnsupportedObjectTypeError string
func (e UnsupportedObjectTypeError) Error() string { return "unsupported object type " + string(e) }
func (e UnsupportedObjectTypeError) Error() string { return "unsupported object type " + string(e) }
func (e UnsupportedObjectTypeError) Message() string { return e.Error() }
// Core holds state of [PW_TYPE_INTERFACE_Core].
type Core struct {
@@ -598,22 +879,24 @@ type Core struct {
done bool
ctx *Context
noAck
noRemove
}
// ErrUnexpectedDone is a [CoreDone] event with unexpected values.
var ErrUnexpectedDone = errors.New("multiple Core::Done events targeting Core::Sync")
// An UnknownBoundIdError describes the server claiming to have bound a proxy id that was never allocated.
type UnknownBoundIdError[E any] struct {
// An UnknownProxyIdError describes an event targeting a proxy id that was never allocated.
type UnknownProxyIdError[E any] struct {
// Offending id decoded from Data.
Id Int
// Event received from the server.
Event E
}
func (e *UnknownBoundIdError[E]) Error() string {
return "unknown bound proxy id " + strconv.Itoa(int(e.Id))
func (e *UnknownProxyIdError[E]) Error() string {
return "unknown proxy id " + strconv.Itoa(int(e.Id))
}
// An InvalidPingError is a [CorePing] event targeting a proxy id that was never allocated.
@@ -668,6 +951,19 @@ func (core *Core) consume(opcode byte, files []int, unmarshal func(v any)) error
unmarshal(&coreError)
return &coreError
case PW_CORE_EVENT_REMOVE_ID:
var coreRemoveId CoreRemoveId
unmarshal(&coreRemoveId)
if proxy, ok := core.ctx.proxy[coreRemoveId.ID]; !ok {
// this should never happen so is non-recoverable if it does
panic(&UnknownProxyIdError[*CoreRemoveId]{Id: coreRemoveId.ID, Event: &coreRemoveId})
} else {
delete(core.ctx.proxy, coreRemoveId.ID)
// not always populated so this is not checked
delete(core.ctx.pendingDestruction, coreRemoveId.ID)
return proxy.remove()
}
case PW_CORE_EVENT_BOUND_PROPS:
var boundProps CoreBoundProps
unmarshal(&boundProps)
@@ -675,12 +971,12 @@ func (core *Core) consume(opcode byte, files []int, unmarshal func(v any)) error
delete(core.ctx.pendingIds, boundProps.ID)
proxy, ok := core.ctx.proxy[boundProps.ID]
if !ok {
return &UnknownBoundIdError[*CoreBoundProps]{Id: boundProps.ID, Event: &boundProps}
return &UnknownProxyIdError[*CoreBoundProps]{Id: boundProps.ID, Event: &boundProps}
}
return proxy.setBoundProps(&boundProps)
default:
return &UnsupportedOpcodeError{opcode, core.String()}
panic(&UnsupportedOpcodeError{opcode, core.String()})
}
}
@@ -698,7 +994,9 @@ type Registry struct {
Objects map[Int]RegistryGlobal `json:"objects"`
ctx *Context
noAck
noRemove
}
// A GlobalIDCollisionError describes a [RegistryGlobal] event stepping on a previous instance of itself.
@@ -714,6 +1012,14 @@ func (e *GlobalIDCollisionError) Error() string {
" stepping on previous id " + strconv.Itoa(int(e.ID)) + " for " + e.Previous.Type
}
// An UnknownGlobalIDRemoveError describes a [RegistryGlobalRemove] event announcing the removal of
// a global id that is not yet known to [Registry] or was already deleted.
type UnknownGlobalIDRemoveError Int
func (e UnknownGlobalIDRemoveError) Error() string {
return "Registry::GlobalRemove event targets unknown id " + strconv.Itoa(int(e))
}
func (registry *Registry) consume(opcode byte, files []int, unmarshal func(v any)) error {
closeReceivedFiles(files...)
switch opcode {
@@ -727,8 +1033,21 @@ func (registry *Registry) consume(opcode byte, files []int, unmarshal func(v any
registry.Objects[global.ID] = global
return nil
case PW_REGISTRY_EVENT_GLOBAL_REMOVE:
var globalRemove RegistryGlobalRemove
unmarshal(&globalRemove)
// server emits PW_CORE_EVENT_REMOVE_ID events targeting
// affected proxies so they do not need to be handled here
l := len(registry.Objects)
delete(registry.Objects, globalRemove.ID)
if len(registry.Objects) != l-1 {
// this should never happen so is non-recoverable if it does
panic(UnknownGlobalIDRemoveError(globalRemove.ID))
}
return nil
default:
return &UnsupportedOpcodeError{opcode, registry.String()}
panic(&UnsupportedOpcodeError{opcode, registry.String()})
}
}

View File

@@ -171,7 +171,7 @@ func TestCoreError(t *testing.T) {
/* padding */ 0, 0, 0, 0,
/* size: 0x1b bytes */ 0x1b, 0, 0, 0,
/*type: String*/ 8, 0, 0, 0,
/* type: String */ 8, 0, 0, 0,
// value: "no permission to destroy 0\x00"
0x6e, 0x6f, 0x20, 0x70,
@@ -192,6 +192,24 @@ func TestCoreError(t *testing.T) {
}.run(t)
}
func TestCoreRemoveId(t *testing.T) {
t.Parallel()
encodingTestCases[pipewire.CoreRemoveId, *pipewire.CoreRemoveId]{
{"sample", []byte{
/* size: rest of data */ 0x10, 0, 0, 0,
/* type: Struct */ 0xe, 0, 0, 0,
/* size: 4 bytes */ 4, 0, 0, 0,
/* type: Int */ 4, 0, 0, 0,
/* value: 3 */ 3, 0, 0, 0,
/* padding */ 0, 0, 0, 0,
}, pipewire.CoreRemoveId{
ID: 3,
}, nil},
}.run(t)
}
func TestCoreBoundProps(t *testing.T) {
t.Parallel()
@@ -288,6 +306,83 @@ func TestCoreGetRegistry(t *testing.T) {
}.run(t)
}
func TestCoreCreateObject(t *testing.T) {
t.Parallel()
encodingTestCases[pipewire.CoreCreateObject, *pipewire.CoreCreateObject]{
{"sample", []byte{
/* size: rest of data */ 0x80, 0, 0, 0,
/* type: Struct */ 0xe, 0, 0, 0,
/* size: 0x13 bytes */ 0x13, 0, 0, 0,
/* type: String */ 8, 0, 0, 0,
// value: "spa-device-factory\x00"
0x73, 0x70, 0x61, 0x2d,
0x64, 0x65, 0x76, 0x69,
0x63, 0x65, 0x2d, 0x66,
0x61, 0x63, 0x74, 0x6f,
0x72, 0x79, 0, 0,
0, 0, 0, 0,
/* size: 0x1a bytes */ 0x1a, 0, 0, 0,
/* type: String */ 8, 0, 0, 0,
// value: "PipeWire:Interface:Device\x00"
0x50, 0x69, 0x70, 0x65,
0x57, 0x69, 0x72, 0x65,
0x3a, 0x49, 0x6e, 0x74,
0x65, 0x72, 0x66, 0x61,
0x63, 0x65, 0x3a, 0x44,
0x65, 0x76, 0x69, 0x63,
0x65, 0, 0, 0,
0, 0, 0, 0,
/* size: 4 bytes */ 4, 0, 0, 0,
/* type: Int */ 4, 0, 0, 0,
/* value: 3 */ 3, 0, 0, 0,
/* padding */ 0, 0, 0, 0,
/* size */ 0x10, 0, 0, 0,
/* type: Struct */ 0xe, 0, 0, 0,
/* size: 4 bytes */ 4, 0, 0, 0,
/* type: Int */ 4, 0, 0, 0,
/* value: 0 */ 0, 0, 0, 0,
/* padding */ 0, 0, 0, 0,
/* size: 4 bytes */ 4, 0, 0, 0,
/* type: Int */ 4, 0, 0, 0,
/* value: 0xbad */ 0xad, 0xb, 0, 0,
/* padding */ 0, 0, 0, 0,
}, pipewire.CoreCreateObject{
FactoryName: "spa-device-factory",
Type: pipewire.PW_TYPE_INTERFACE_Device,
Version: pipewire.PW_VERSION_FACTORY,
Properties: &pipewire.SPADict{},
NewID: 0xbad,
}, nil},
}.run(t)
}
func TestCoreDestroy(t *testing.T) {
t.Parallel()
encodingTestCases[pipewire.CoreDestroy, *pipewire.CoreDestroy]{
{"sample", []byte{
/* size: rest of data */ 0x10, 0, 0, 0,
/* type: Struct */ 0xe, 0, 0, 0,
/* size: 4 bytes */ 4, 0, 0, 0,
/* type: Int */ 4, 0, 0, 0,
/* value: 3 */ 3, 0, 0, 0,
/* padding */ 0, 0, 0, 0,
}, pipewire.CoreDestroy{
ID: 3,
}, nil},
}.run(t)
}
func TestRegistryGlobal(t *testing.T) {
t.Parallel()
@@ -745,6 +840,23 @@ func TestRegistryGlobal(t *testing.T) {
}.run(t)
}
func TestRegistryGlobalRemove(t *testing.T) {
t.Parallel()
encodingTestCases[pipewire.RegistryGlobalRemove, *pipewire.RegistryGlobalRemove]{
{"sample", []byte{
/* size: rest of data*/ 0x10, 0, 0, 0,
/* type: Struct */ 0xe, 0, 0, 0,
/* size: 4 bytes */ 4, 0, 0, 0,
/* type: Int */ 4, 0, 0, 0,
/* value: 0xbad */ 0xad, 0xb, 0, 0,
/* padding */ 0, 0, 0, 0,
}, pipewire.RegistryGlobalRemove{
ID: 0xbad,
}, nil},
}.run(t)
}
func TestRegistryBind(t *testing.T) {
t.Parallel()
@@ -757,3 +869,20 @@ func TestRegistryBind(t *testing.T) {
}, nil},
}.run(t)
}
func TestRegistryDestroy(t *testing.T) {
t.Parallel()
encodingTestCases[pipewire.RegistryDestroy, *pipewire.RegistryDestroy]{
{"sample", []byte{
/* size: rest of data*/ 0x10, 0, 0, 0,
/* type: Struct */ 0xe, 0, 0, 0,
/* size: 4 bytes */ 4, 0, 0, 0,
/* type: Int */ 4, 0, 0, 0,
/* value: 0xbad */ 0xad, 0xb, 0, 0,
/* padding */ 0, 0, 0, 0,
}, pipewire.RegistryDestroy{
ID: 0xbad,
}, nil},
}.run(t)
}

View File

@@ -16,6 +16,7 @@ package pipewire
import (
"encoding/binary"
"errors"
"fmt"
"io"
"net"
@@ -51,20 +52,27 @@ type Context struct {
buf []byte
// Current [Header.Sequence] value, incremented every write.
sequence Int
// Current server-side [Header.Sequence] value, incremented on every event processed.
remoteSequence Int
// Pending file descriptors to be sent with the next message.
pendingFiles []int
// File count already kept track of in [Header].
headerFiles int
// Proxy id associations.
proxy map[Int]eventProxy
// Newly allocated proxies pending acknowledgement from the server.
pendingIds map[Int]struct{}
// Smallest available Id for the next proxy.
nextId Int
// Server side registry generation number.
generation Long
// Pending file descriptors to be sent with the next message.
pendingFiles []int
// File count already kept track of in [Header].
headerFiles int
// Proxies targeted by the [CoreDestroy] event pending until next [CoreSync].
pendingDestruction map[Int]struct{}
// Proxy for built-in core events.
core Core
// Proxy for built-in client events.
client Client
// Current server-side [Header.Sequence] value, incremented on every event processed.
remoteSequence Int
// Files from the server. This is discarded on every Roundtrip so eventProxy
// implementations must make sure to close them to avoid leaking fds.
//
@@ -80,13 +88,11 @@ type Context struct {
// Pending footer value deferred to the next round trip,
// sent if pendingFooter is nil. This is for emulating upstream behaviour
deferredPendingFooter KnownSize
// Server side registry generation number.
generation Long
// Deferred operations ran after a [Core.Sync] completes or Close is called. Errors
//are reported as part of [ProxyConsumeError] and is not considered fatal unless panicked.
syncComplete []func() error
// Proxy for built-in core events.
core Core
// Proxy for built-in client events.
client Client
// Passed to [Conn.Recvmsg]. Not copied if sufficient for all received messages.
iovecBuf [1 << 15]byte
@@ -120,8 +126,9 @@ func New(conn Conn, props SPADict) (*Context, error) {
PW_ID_CLIENT: {},
}
ctx.nextId = Int(len(ctx.proxy))
ctx.pendingDestruction = make(map[Int]struct{})
if err := ctx.coreHello(); err != nil {
if err := ctx.core.hello(); err != nil {
return nil, err
}
if err := ctx.clientUpdateProperties(props); err != nil {
@@ -419,6 +426,9 @@ type eventProxy interface {
consume(opcode byte, files []int, unmarshal func(v any)) error
// setBoundProps stores a [CoreBoundProps] event received from the server.
setBoundProps(event *CoreBoundProps) error
// remove is called when the proxy is removed for any reason, usually from
// being targeted by a [PW_CORE_EVENT_REMOVE_ID] event.
remove() error
// Stringer returns the PipeWire interface name.
fmt.Stringer
@@ -499,13 +509,21 @@ func (e DanglingFilesError) Error() string {
}
// An UnacknowledgedProxyError holds newly allocated proxy ids that the server failed
// to acknowledge after an otherwise successful [Context.Roundtrip].
// to acknowledge after an otherwise successful [Core.Sync].
type UnacknowledgedProxyError []Int
func (e UnacknowledgedProxyError) Error() string {
return "server did not acknowledge " + strconv.Itoa(len(e)) + " proxies"
}
// An UnacknowledgedProxyDestructionError holds destroyed proxy ids that the server failed
// to acknowledge after an otherwise successful [Core.Sync].
type UnacknowledgedProxyDestructionError []Int
func (e UnacknowledgedProxyDestructionError) Error() string {
return "server did not acknowledge " + strconv.Itoa(len(e)) + " proxy destructions"
}
// A ProxyFatalError describes an error that terminates event handling during a
// [Context.Roundtrip] and makes further event processing no longer possible.
type ProxyFatalError struct {
@@ -636,7 +654,7 @@ func (ctx *Context) roundtrip() (err error) {
}
// currentSeq returns the current sequence number.
// This must only be called from eventProxy.consume.
// This must only be called immediately after queueing a message.
func (ctx *Context) currentSeq() Int { return ctx.sequence - 1 }
// currentRemoteSeq returns the current remote sequence number.
@@ -786,6 +804,52 @@ func (ctx *Context) Close() (err error) {
}
}
// expectsCoreError returns a function that inspects an error value and
// returns the address of a [CoreError] if it is the only error present
// and targets the specified proxy and sequence.
//
// The behaviour of expectsCoreError is only correct for an empty buf
// prior to calling. If buf is not empty, [Core.Sync] is called, with
// its return value stored to the value pointed to by errP if not nil,
// and the function is not populated.
//
// The caller must queue a message and call [Core.Sync] immediately
// after calling expectsCoreError.
func (ctx *Context) expectsCoreError(id Int, errP *error) (asCoreError func() (coreError *CoreError)) {
if len(ctx.buf) > 0 {
if err := ctx.GetCore().Sync(); err != nil {
*errP = err
return nil
}
}
sequence := ctx.sequence
return func() (coreError *CoreError) {
if proxyErrors, ok := (*errP).(ProxyConsumeError); !ok ||
len(proxyErrors) != 1 ||
!errors.As(proxyErrors[0], &coreError) ||
coreError == nil ||
coreError.ID != id ||
coreError.Sequence != sequence {
// do not return a non-matching CoreError
coreError = nil
}
return
}
}
// A PermissionError describes an error emitted by the server when trying to
// perform an operation that the client has no permission for.
type PermissionError struct {
// The id of the resource (proxy if emitted by the client) that is in error.
ID Int `json:"id"`
// An error message.
Message string `json:"message"`
}
func (*PermissionError) Unwrap() error { return syscall.EPERM }
func (e *PermissionError) Error() string { return e.Message }
// Remote is the environment (sic) with the remote name.
const Remote = "PIPEWIRE_REMOTE"

View File

@@ -680,9 +680,6 @@ func TestContext(t *testing.T) {
}); err != nil {
t.Fatalf("SecurityContext.Create: error = %v", err)
}
if err := ctx.GetCore().Sync(); err != nil {
t.Fatalf("Sync: error = %v", err)
}
// none of these should change
if coreInfo := ctx.GetCore().Info; !reflect.DeepEqual(coreInfo, &wantCoreInfo0) {
@@ -836,6 +833,7 @@ func TestContextErrors(t *testing.T) {
{"UnexpectedFileCountError", &pipewire.UnexpectedFileCountError{0, -1}, "received -1 files instead of the expected 0"},
{"UnacknowledgedProxyError", make(pipewire.UnacknowledgedProxyError, 1<<4), "server did not acknowledge 16 proxies"},
{"UnacknowledgedProxyDestructionError", make(pipewire.UnacknowledgedProxyDestructionError, 1<<4), "server did not acknowledge 16 proxy destructions"},
{"DanglingFilesError", make(pipewire.DanglingFilesError, 1<<4), "received 16 dangling files"},
{"UnexpectedFilesError", pipewire.UnexpectedFilesError(1 << 4), "server message headers claim to have sent more files than actually received"},
{"UnexpectedSequenceError", pipewire.UnexpectedSequenceError(1 << 4), "unexpected seq 16"},
@@ -862,6 +860,19 @@ func TestContextErrors(t *testing.T) {
ID: 0xbad,
Sequence: 0xcafe,
}, "received Core::Ping seq 51966 targeting unknown proxy id 2989"},
{"GlobalIDCollisionError", &pipewire.GlobalIDCollisionError{
ID: 0xbad,
Previous: &pipewire.RegistryGlobal{Type: "PipeWire:Interface:Invalid"},
Current: &pipewire.RegistryGlobal{Type: "PipeWire:Interface:NewInvalid"},
}, "new Registry::Global event for PipeWire:Interface:NewInvalid stepping on previous id 2989 for PipeWire:Interface:Invalid"},
{"UnknownGlobalIDRemoveError", pipewire.UnknownGlobalIDRemoveError(0xbad), "Registry::GlobalRemove event targets unknown id 2989"},
{"PermissionError", &pipewire.PermissionError{
ID: 2,
Message: "no permission to destroy 0",
}, "no permission to destroy 0"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {

View File

@@ -384,19 +384,16 @@ func unmarshalValue(data []byte, v reflect.Value, wireSizeP *Word) error {
return nil
case reflect.Pointer:
if len(data) < SizePrefix {
return ErrEOFPrefix
}
switch SPAKind(binary.NativeEndian.Uint32(data[SizeSPrefix:])) {
case SPA_TYPE_None:
if ok, err := unmarshalHandleNone(&data, wireSizeP); err != nil {
return err
} else if ok {
v.SetZero()
return nil
default:
v.Set(reflect.New(v.Type().Elem()))
return unmarshalValue(data, v.Elem(), wireSizeP)
}
v.Set(reflect.New(v.Type().Elem()))
return unmarshalValue(data, v.Elem(), wireSizeP)
case reflect.String:
*wireSizeP = 0
if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_String, wireSizeP); err != nil {
@@ -422,6 +419,29 @@ func unmarshalValue(data []byte, v reflect.Value, wireSizeP *Word) error {
}
}
// unmarshalHandleNone establishes prefix bounds and, for a value of type [SPA_TYPE_None],
// validates its size and skips the header. This is for unmarshalling values that can be nil.
func unmarshalHandleNone(data *[]byte, wireSizeP *Word) (bool, error) {
if len(*data) < SizePrefix {
return false, ErrEOFPrefix
}
if SPAKind(binary.NativeEndian.Uint32((*data)[SizeSPrefix:])) != SPA_TYPE_None {
return false, nil
}
*wireSizeP = 0
if err := unmarshalCheckTypeBounds(data, SPA_TYPE_None, wireSizeP); err != nil {
return true, err
}
if len(*data) != 0 {
return true, TrailingGarbageError(*data)
}
return true, nil
}
// An InconsistentSizeError describes an inconsistent size prefix encountered
// in data passed to [Unmarshal].
type InconsistentSizeError struct{ Prefix, Expect Word }

View File

@@ -26,9 +26,10 @@ const (
const (
PW_SECURITY_CONTEXT_METHOD_ADD_LISTENER = iota
PW_SECURITY_CONTEXT_METHOD_CREATE
PW_SECURITY_CONTEXT_METHOD_NUM
PW_SECURITY_CONTEXT_METHOD_CREATE
PW_SECURITY_CONTEXT_METHOD_NUM
PW_VERSION_SECURITY_CONTEXT_METHODS = 0
)
@@ -90,6 +91,8 @@ type SecurityContext struct {
GlobalID Int `json:"id"`
ctx *Context
destructible
}
// GetSecurityContext queues a [RegistryBind] message for the PipeWire server
@@ -108,13 +111,39 @@ func (registry *Registry) GetSecurityContext() (securityContext *SecurityContext
}
// Create queues a [SecurityContextCreate] message for the PipeWire server.
func (securityContext *SecurityContext) Create(listenFd, closeFd int, props SPADict) error {
func (securityContext *SecurityContext) Create(listenFd, closeFd int, props SPADict) (err error) {
if err = securityContext.checkDestroy(); err != nil {
return
}
asCoreError := securityContext.ctx.expectsCoreError(securityContext.ID, &err)
if err != nil {
return
}
// queued in reverse based on upstream behaviour, unsure why
offset := securityContext.ctx.queueFiles(closeFd, listenFd)
return securityContext.ctx.writeMessage(
if err = securityContext.ctx.writeMessage(
securityContext.ID,
&SecurityContextCreate{ListenFd: offset + 1, CloseFd: offset + 0, Properties: &props},
)
); err != nil {
return
}
if err = securityContext.ctx.GetCore().Sync(); err == nil {
return nil
}
if coreError := asCoreError(); coreError == nil {
return
} else {
switch syscall.Errno(-coreError.Result) {
case syscall.EPERM:
return &PermissionError{securityContext.ID, coreError.Message}
default:
return coreError
}
}
}
// securityContextCloser holds onto resources associated to the security context.
@@ -144,6 +173,9 @@ func (scc *securityContextCloser) Close() (err error) {
// BindAndCreate binds a new socket to the specified pathname and pass it to Create.
// It returns an [io.Closer] corresponding to [SecurityContextCreate.CloseFd].
func (securityContext *SecurityContext) BindAndCreate(pathname string, props SPADict) (io.Closer, error) {
if err := securityContext.checkDestroy(); err != nil {
return nil, err
}
var scc securityContextCloser
// ensure pathname is available
@@ -185,17 +217,19 @@ func (securityContext *SecurityContext) BindAndCreate(pathname string, props SPA
}
func (securityContext *SecurityContext) consume(opcode byte, files []int, _ func(v any)) error {
securityContext.mustCheckDestroy()
closeReceivedFiles(files...)
switch opcode {
// SecurityContext does not receive any events
default:
return &UnsupportedOpcodeError{opcode, securityContext.String()}
panic(&UnsupportedOpcodeError{opcode, securityContext.String()})
}
}
func (securityContext *SecurityContext) setBoundProps(event *CoreBoundProps) error {
securityContext.mustCheckDestroy()
if securityContext.ID != event.ID {
return &InconsistentIdError{Proxy: securityContext, ID: securityContext.ID, ServerID: event.ID}
}
@@ -205,4 +239,9 @@ func (securityContext *SecurityContext) setBoundProps(event *CoreBoundProps) err
return nil
}
// Destroy destroys this [SecurityContext] proxy.
func (securityContext *SecurityContext) Destroy() error {
return securityContext.destroy(securityContext.ctx, securityContext.ID)
}
func (securityContext *SecurityContext) String() string { return PW_TYPE_INTERFACE_SecurityContext }

View File

@@ -14,8 +14,8 @@ import (
// PipeWire maintains a pipewire socket with SecurityContext attached via [pipewire].
// The socket stops accepting connections once the pipe referred to by sync is closed.
// The socket is pathname only and is destroyed on revert.
func (sys *I) PipeWire(dst *check.Absolute) *I {
sys.ops = append(sys.ops, &pipewireOp{nil, dst})
func (sys *I) PipeWire(dst *check.Absolute, appID, instanceID string) *I {
sys.ops = append(sys.ops, &pipewireOp{nil, dst, appID, instanceID})
return sys
}
@@ -23,6 +23,8 @@ func (sys *I) PipeWire(dst *check.Absolute) *I {
type pipewireOp struct {
scc io.Closer
dst *check.Absolute
appID, instanceID string
}
func (p *pipewireOp) Type() hst.Enablement { return Process }
@@ -56,12 +58,11 @@ func (p *pipewireOp) apply(sys *I) (err error) {
if p.scc, err = securityContext.BindAndCreate(p.dst.String(), pipewire.SPADict{
{Key: pipewire.PW_KEY_SEC_ENGINE, Value: "app.hakurei"},
{Key: pipewire.PW_KEY_SEC_APP_ID, Value: p.appID},
{Key: pipewire.PW_KEY_SEC_INSTANCE_ID, Value: p.instanceID},
{Key: pipewire.PW_KEY_ACCESS, Value: "restricted"},
}); err != nil {
return newOpError("pipewire", err, false)
} else if err = ctx.GetCore().Sync(); err != nil {
_ = p.scc.Close()
return newOpError("pipewire", err, false)
}
if err = sys.chmod(p.dst.String(), 0); err != nil {
@@ -92,7 +93,9 @@ func (p *pipewireOp) revert(sys *I, _ *Criteria) error {
func (p *pipewireOp) Is(o Op) bool {
target, ok := o.(*pipewireOp)
return ok && p != nil && target != nil &&
p.dst.Is(target.dst)
p.dst.Is(target.dst) &&
p.appID == target.appID &&
p.instanceID == target.instanceID
}
func (p *pipewireOp) Path() string { return p.dst.String() }

View File

@@ -18,6 +18,8 @@ func TestPipeWireOp(t *testing.T) {
checkOpBehaviour(t, checkNoParallel, []opBehaviourTestCase{
{"success", 0xbeef, 0xff, &pipewireOp{nil,
m(path.Join(t.TempDir(), "pipewire")),
"org.chromium.Chromium",
"ebf083d1b175911782d413369b64ce7c",
}, []stub.Call{
call("pipewireConnect", stub.ExpectArgs{}, func() *pipewire.Context {
if ctx, err := pipewire.New(&stubPipeWireConn{sendmsg: []string{
@@ -154,11 +156,11 @@ func TestPipeWireOp(t *testing.T) {
string([]byte{
// header: SecurityContext::Create
3, 0, 0, 0,
0xa8, 0, 0, 1,
0x40, 1, 0, 1,
5, 0, 0, 0,
2, 0, 0, 0,
// Struct
0xa0, 0, 0, 0,
0x38, 1, 0, 0,
0xe, 0, 0, 0,
// Fd: listen_fd = 1
8, 0, 0, 0,
@@ -171,12 +173,12 @@ func TestPipeWireOp(t *testing.T) {
0, 0, 0, 0,
0, 0, 0, 0,
// Struct: spa_dict
0x78, 0, 0, 0,
0x10, 1, 0, 0,
0xe, 0, 0, 0,
// Int: n_items = 2
// Int: n_items = 4
4, 0, 0, 0,
4, 0, 0, 0,
4, 0, 0, 0,
2, 0, 0, 0,
0, 0, 0, 0,
// String: key = "pipewire.sec.engine"
0x14, 0, 0, 0,
@@ -194,6 +196,48 @@ func TestPipeWireOp(t *testing.T) {
0x68, 0x61, 0x6b, 0x75,
0x72, 0x65, 0x69, 0,
0, 0, 0, 0,
// String: key = "pipewire.sec.app-id"
0x14, 0, 0, 0,
8, 0, 0, 0,
0x70, 0x69, 0x70, 0x65,
0x77, 0x69, 0x72, 0x65,
0x2e, 0x73, 0x65, 0x63,
0x2e, 0x61, 0x70, 0x70,
0x2d, 0x69, 0x64, 0,
0, 0, 0, 0,
// String: value = "org.chromium.Chromium"
0x16, 0, 0, 0,
8, 0, 0, 0,
0x6f, 0x72, 0x67, 0x2e,
0x63, 0x68, 0x72, 0x6f,
0x6d, 0x69, 0x75, 0x6d,
0x2e, 0x43, 0x68, 0x72,
0x6f, 0x6d, 0x69, 0x75,
// String: key = "pipewire.sec.instance-id"
0x6d, 0, 0, 0,
0x19, 0, 0, 0,
8, 0, 0, 0,
0x70, 0x69, 0x70, 0x65,
0x77, 0x69, 0x72, 0x65,
0x2e, 0x73, 0x65, 0x63,
0x2e, 0x69, 0x6e, 0x73,
0x74, 0x61, 0x6e, 0x63,
0x65, 0x2d, 0x69, 0x64,
0, 0, 0, 0,
0, 0, 0, 0,
// String: value = "ebf083d1b175911782d413369b64ce7c"
0x21, 0, 0, 0,
8, 0, 0, 0,
0x65, 0x62, 0x66, 0x30,
0x38, 0x33, 0x64, 0x31,
0x62, 0x31, 0x37, 0x35,
0x39, 0x31, 0x31, 0x37,
0x38, 0x32, 0x64, 0x34,
0x31, 0x33, 0x33, 0x36,
0x39, 0x62, 0x36, 0x34,
0x63, 0x65, 0x37, 0x63,
0, 0, 0, 0,
0, 0, 0, 0,
// String: key = "pipewire.access"
0x10, 0, 0, 0,
8, 0, 0, 0,
@@ -386,29 +430,43 @@ func TestPipeWireOp(t *testing.T) {
checkOpsBuilder(t, "PipeWire", []opsBuilderTestCase{
{"sample", 0xcafe, func(_ *testing.T, sys *I) {
sys.PipeWire(m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"))
sys.PipeWire(m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
"org.chromium.Chromium",
"ebf083d1b175911782d413369b64ce7c")
}, []Op{&pipewireOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
"org.chromium.Chromium",
"ebf083d1b175911782d413369b64ce7c",
}}, stub.Expect{}},
})
checkOpIs(t, []opIsTestCase{
{"dst differs", &pipewireOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7d/pipewire"),
"org.chromium.Chromium",
"ebf083d1b175911782d413369b64ce7c",
}, &pipewireOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
"org.chromium.Chromium",
"ebf083d1b175911782d413369b64ce7c",
}, false},
{"equals", &pipewireOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
"org.chromium.Chromium",
"ebf083d1b175911782d413369b64ce7c",
}, &pipewireOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
"org.chromium.Chromium",
"ebf083d1b175911782d413369b64ce7c",
}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"sample", &pipewireOp{nil,
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"),
"org.chromium.Chromium",
"ebf083d1b175911782d413369b64ce7c",
}, Process, "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire",
`pipewire socket at "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/pipewire"`},
})

View File

@@ -68,7 +68,7 @@ in
home-manager =
let
privPackages = mapAttrs (username: userid: {
privPackages = mapAttrs (_: userid: {
home.packages = foldlAttrs (
acc: id: app:
[
@@ -196,15 +196,6 @@ 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"
"exec pipewire-pulse"
];
}
++ [
{
type = "bind";

View File

@@ -35,7 +35,7 @@ package
*Default:*
` <derivation hakurei-static-x86_64-unknown-linux-musl-0.3.1> `
` <derivation hakurei-static-x86_64-unknown-linux-musl-0.3.3> `
@@ -73,11 +73,11 @@ null or boolean
## environment\.hakurei\.apps\.\<name>\.enablements\.pulse
## environment\.hakurei\.apps\.\<name>\.enablements\.pipewire
Whether to share the PulseAudio socket and cookie\.
Whether to share the PipeWire server via pipewire-pulse on a SecurityContext socket\.
@@ -95,7 +95,7 @@ null or boolean
Whether to share the Wayland socket\.
Whether to share the Wayland server via security-context-v1\.
@@ -805,7 +805,7 @@ package
*Default:*
` <derivation hakurei-hsu-0.3.1> `
` <derivation hakurei-hsu-0.3.3> `

View File

@@ -12,13 +12,13 @@ in
package = mkOption {
type = types.package;
default = packages.${pkgs.system}.hakurei;
default = packages.${pkgs.stdenv.hostPlatform.system}.hakurei;
description = "The hakurei package to use.";
};
hsuPackage = mkOption {
type = types.package;
default = packages.${pkgs.system}.hsu;
default = packages.${pkgs.stdenv.hostPlatform.system}.hsu;
description = "The hsu package to use.";
};
@@ -242,19 +242,11 @@ in
type = nullOr bool;
default = true;
description = ''
Whether to share the PipeWire server via SecurityContext.
Whether to share the PipeWire server via pipewire-pulse on a SecurityContext socket.
'';
};
};
pulse = mkOption {
type = nullOr bool;
default = true;
description = ''
Whether to run the PulseAudio compatibility daemon.
'';
};
share = mkOption {
type = nullOr package;
default = null;

View File

@@ -32,7 +32,7 @@
buildGoModule rec {
pname = "hakurei";
version = "0.3.2";
version = "0.3.3";
srcFiltered = builtins.path {
name = "${pname}-src";

View File

@@ -84,7 +84,7 @@
virtualisation = {
# Hopefully reduces spurious test failures:
memorySize = if pkgs.hostPlatform.is32bit then 2046 else 8192;
memorySize = if pkgs.stdenv.hostPlatform.is32bit then 2046 else 8192;
qemu.options = [
# Need to switch to a different GPU driver than the default one (-vga std) so that Sway can launch:

View File

@@ -1,6 +1,6 @@
{
lib,
nixosTest,
testers,
buildFHSEnv,
writeShellScriptBin,
@@ -9,7 +9,7 @@
withRace ? false,
}:
nixosTest {
testers.nixosTest {
name = "hakurei" + (if withRace then "-race" else "");
nodes.machine =
{ options, pkgs, ... }:

View File

@@ -41,7 +41,6 @@ in
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus"
"DISPLAY=unix:/tmp/.X11-unix/X0"
"HOME=/var/lib/hakurei/u0/a4"
"PIPEWIRE_REMOTE=/run/user/65534/pipewire-0"
"SHELL=/run/current-system/sw/bin/bash"
"TERM=linux"
"USER=u0_a4"
@@ -49,6 +48,7 @@ in
"XDG_RUNTIME_DIR=/run/user/65534"
"XDG_SESSION_CLASS=user"
"XDG_SESSION_TYPE=wayland"
"PULSE_SERVER=unix:/run/user/65534/pulse/native"
];
fs = fs "dead" {
@@ -75,6 +75,7 @@ in
"fstab" = fs "80001ff" null null;
"hsurc" = fs "80001ff" null null;
"fuse.conf" = fs "80001ff" null null;
"gai.conf" = fs "80001ff" null null;
"group" = fs "180" null "hakurei:x:65534:\n";
"host.conf" = fs "80001ff" null null;
"hostname" = fs "80001ff" null null;
@@ -137,12 +138,8 @@ in
user = fs "800001ed" {
"65534" = fs "800001c0" {
bus = fs "10001fd" null null;
pulse = fs "800001c0" {
native = fs "10001ff" null null;
pid = fs "1a4" null null;
} null;
pulse = fs "800001c0" { native = fs "10001ff" null null; } null;
wayland-0 = fs "1000038" null null;
pipewire-0 = fs "1000038" null null;
} null;
} null;
} null;
@@ -193,8 +190,6 @@ in
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;
nix = fs "800001ed" {
profiles = fs "800001ed" {
home-manager = fs "80001ff" null null;
home-manager-1-link = fs "80001ff" null null;
profile = fs "80001ff" null null;
profile-1-link = fs "80001ff" null null;
} null;
@@ -225,15 +220,14 @@ in
(ent "/" ignore ignore ignore ignore ignore)
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10004,gid=10004")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=16384k,mode=755,uid=10004,gid=10004")
(ent "/tmp/hakurei.0/tmpdir/4" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/hakurei.0/tmpdir/4" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10004,gid=10004")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10004,gid=10004")
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/.X11-unix" "/tmp/.X11-unix" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/pipewire-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/tmp/.X11-unix" "/tmp/.X11-unix" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
(ent "/block" "/sys/block" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/bus" "/sys/bus" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
@@ -241,12 +235,13 @@ in
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/" "/.hakurei/.ro-store" "rw,relatime" "overlay" "overlay" "ro,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,redirect_dir=nofollow,userxattr")
(ent "/" "/.hakurei/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,upperdir=/host/tmp/.hakurei-store-rw/upper,workdir=/host/tmp/.hakurei-store-rw/work,redirect_dir=nofollow,userxattr")
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/lib/hakurei/u0/a4" "/var/lib/hakurei/u0/a4" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/var/lib/hakurei/u0/a4" "/var/lib/hakurei/u0/a4" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
];
seccomp = true;

View File

@@ -49,7 +49,6 @@ in
env = [
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus"
"HOME=/var/lib/hakurei/u0/a3"
"PIPEWIRE_REMOTE=/run/user/1000/pipewire-0"
"SHELL=/run/current-system/sw/bin/bash"
"TERM=linux"
"USER=u0_a3"
@@ -57,6 +56,7 @@ in
"XDG_RUNTIME_DIR=/run/user/1000"
"XDG_SESSION_CLASS=user"
"XDG_SESSION_TYPE=wayland"
"PULSE_SERVER=unix:/run/user/1000/pulse/native"
];
fs = fs "dead" {
@@ -100,6 +100,7 @@ in
"fstab" = fs "80001ff" null null;
"hsurc" = fs "80001ff" null null;
"fuse.conf" = fs "80001ff" null null;
"gai.conf" = fs "80001ff" null null;
"group" = fs "180" null "hakurei:x:100:\n";
"host.conf" = fs "80001ff" null null;
"hostname" = fs "80001ff" null null;
@@ -162,12 +163,8 @@ in
user = fs "800001ed" {
"1000" = fs "800001f8" {
bus = fs "10001fd" null null;
pulse = fs "800001c0" {
native = fs "10001ff" null null;
pid = fs "1a4" null null;
} null;
pulse = fs "800001c0" { native = fs "10001ff" null null; } null;
wayland-0 = fs "1000038" null null;
pipewire-0 = fs "1000038" null null;
} null;
} null;
} null;
@@ -216,8 +213,6 @@ in
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;
nix = fs "800001ed" {
profiles = fs "800001ed" {
home-manager = fs "80001ff" null null;
home-manager-1-link = fs "80001ff" null null;
profile = fs "80001ff" null null;
profile-1-link = fs "80001ff" null null;
} null;
@@ -252,15 +247,14 @@ in
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10003,gid=10003")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=16384k,mode=755,uid=10003,gid=10003")
(ent "/tmp/hakurei.0/runtime/3" "/run/user/1000" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/hakurei.0/tmpdir/3" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/hakurei.0/runtime/3" "/run/user/1000" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/tmp/hakurei.0/tmpdir/3" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10003,gid=10003")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10003,gid=10003")
(ent ignore "/run/user/1000/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/1000/pipewire-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/1000/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/1000/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent ignore "/run/user/1000/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
(ent "/block" "/sys/block" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/bus" "/sys/bus" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
@@ -268,12 +262,13 @@ in
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/" "/.hakurei/.ro-store" "rw,relatime" "overlay" "overlay" "ro,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,redirect_dir=nofollow,userxattr")
(ent "/" "/.hakurei/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,upperdir=/host/tmp/.hakurei-store-rw/upper,workdir=/host/tmp/.hakurei-store-rw/work,redirect_dir=nofollow,userxattr")
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/lib/hakurei/u0/a3" "/var/lib/hakurei/u0/a3" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/var/lib/hakurei/u0/a3" "/var/lib/hakurei/u0/a3" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent ignore "/run/user/1000/pulse/native" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
];
seccomp = true;

View File

@@ -61,6 +61,7 @@
"fstab" = fs "80001ff" null null;
"hsurc" = fs "80001ff" null null;
"fuse.conf" = fs "80001ff" null null;
"gai.conf" = fs "80001ff" null null;
"group" = fs "180" null "hakurei:x:65534:\n";
"host.conf" = fs "80001ff" null null;
"hostname" = fs "80001ff" null null;
@@ -139,23 +140,23 @@
mount = [
(ent "/sysroot" "/" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10000,gid=10000")
(ent "/bin" "/bin" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/home" "/home" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/lib64" "/lib64" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/lost+found" "/lost+found" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/nix" "/nix" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/bin" "/bin" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/home" "/home" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/lib64" "/lib64" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/lost+found" "/lost+found" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/nix" "/nix" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/" "/nix/.ro-store" "rw,nosuid,nodev,relatime" "9p" "nix-store" ignore)
(ent "/" "/nix/.rw-store" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,mode=755")
(ent "/" "/nix/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
(ent "/root" "/root" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/root" "/root" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/" "/run" "rw,nosuid,nodev" "tmpfs" "tmpfs" ignore)
(ent "/" "/run/keys" "rw,nosuid,nodev,relatime" "ramfs" "ramfs" "rw,mode=750")
(ent "/" "/run/credentials/systemd-journald.service" "ro,nosuid,nodev,noexec,relatime,nosymfollow" "tmpfs" "tmpfs" "rw,size=1024k,nr_inodes=1024,mode=700,noswap")
(ent "/" "/run/wrappers" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent "/" "/run/credentials/getty@tty1.service" "ro,nosuid,nodev,noexec,relatime,nosymfollow" "tmpfs" "tmpfs" "rw,size=1024k,nr_inodes=1024,mode=700,noswap")
(ent "/" "/run/user/1000" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent "/srv" "/srv" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/srv" "/srv" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/" "/sys" "rw,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/" "/sys/kernel/security" "rw,nosuid,nodev,noexec,relatime" "securityfs" "securityfs" "rw")
(ent "/../../.." "/sys/fs/cgroup" "rw,nosuid,nodev,noexec,relatime" "cgroup2" "cgroup2" "rw,nsdelegate,memory_recursiveprot")
@@ -166,8 +167,8 @@
(ent "/" ignore "rw,nosuid,nodev,noexec,relatime" ignore ignore "rw")
(ent "/" ignore "rw,nosuid,nodev,noexec,relatime" ignore ignore "rw")
(ent "/" ignore "rw,nosuid,nodev,noexec,relatime" ignore ignore "rw")
(ent "/usr" "/usr" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var" "/var" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/usr" "/usr" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/var" "/var" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw")
(ent "/" "/.hakurei" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=10000,gid=10000")
(ent "/" "/dev" "ro,nosuid,nodev,relatime" "tmpfs" "devtmpfs" "rw,mode=755,uid=10000,gid=10000")
@@ -182,12 +183,12 @@
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10000,gid=10000")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=16384k,mode=755,uid=10000,gid=10000")
(ent "/tmp/hakurei.0/runtime/0" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/hakurei.0/tmpdir/0" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/hakurei.0/runtime/0" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/tmp/hakurei.0/tmpdir/0" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10000,gid=10000")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10000,gid=10000")
(ent "/kvm" "/dev/kvm" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/" "/run/user/1000" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=8k,mode=755,uid=10000,gid=10000")
(ent "/" "/run/nscd" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=8k,mode=755,uid=10000,gid=10000")
(ent "/" "/run/dbus" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=8k,mode=755,uid=10000,gid=10000")

View File

@@ -49,7 +49,6 @@ in
env = [
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus"
"HOME=/var/lib/hakurei/u0/a5"
"PIPEWIRE_REMOTE=/run/user/65534/pipewire-0"
"SHELL=/run/current-system/sw/bin/bash"
"TERM=linux"
"USER=u0_a5"
@@ -57,6 +56,7 @@ in
"XDG_RUNTIME_DIR=/run/user/65534"
"XDG_SESSION_CLASS=user"
"XDG_SESSION_TYPE=wayland"
"PULSE_SERVER=unix:/run/user/65534/pulse/native"
];
fs = fs "dead" {
@@ -98,6 +98,7 @@ in
"fstab" = fs "80001ff" null null;
"hsurc" = fs "80001ff" null null;
"fuse.conf" = fs "80001ff" null null;
"gai.conf" = fs "80001ff" null null;
"group" = fs "180" null "hakurei:x:65534:\n";
"host.conf" = fs "80001ff" null null;
"hostname" = fs "80001ff" null null;
@@ -160,12 +161,8 @@ in
user = fs "800001ed" {
"65534" = fs "800001f8" {
bus = fs "10001fd" null null;
pulse = fs "800001c0" {
native = fs "10001ff" null null;
pid = fs "1a4" null null;
} null;
pulse = fs "800001c0" { native = fs "10001ff" null null; } null;
wayland-0 = fs "1000038" null null;
pipewire-0 = fs "1000038" null null;
} null;
} null;
} null;
@@ -214,8 +211,6 @@ in
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;
nix = fs "800001ed" {
profiles = fs "800001ed" {
home-manager = fs "80001ff" null null;
home-manager-1-link = fs "80001ff" null null;
profile = fs "80001ff" null null;
profile-1-link = fs "80001ff" null null;
} null;
@@ -250,15 +245,14 @@ in
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10005,gid=10005")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=16384k,mode=755,uid=10005,gid=10005")
(ent "/tmp/hakurei.0/runtime/5" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/hakurei.0/tmpdir/5" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/hakurei.0/runtime/5" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/tmp/hakurei.0/tmpdir/5" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10005,gid=10005")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10005,gid=10005")
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/pipewire-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
(ent "/block" "/sys/block" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/bus" "/sys/bus" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
@@ -266,9 +260,10 @@ in
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/lib/hakurei/u0/a5" "/var/lib/hakurei/u0/a5" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/var/lib/hakurei/u0/a5" "/var/lib/hakurei/u0/a5" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
];
seccomp = true;

View File

@@ -49,7 +49,6 @@ in
env = [
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus"
"HOME=/var/lib/hakurei/u0/a1"
"PIPEWIRE_REMOTE=/run/user/65534/pipewire-0"
"SHELL=/run/current-system/sw/bin/bash"
"TERM=linux"
"USER=u0_a1"
@@ -57,6 +56,7 @@ in
"XDG_RUNTIME_DIR=/run/user/65534"
"XDG_SESSION_CLASS=user"
"XDG_SESSION_TYPE=wayland"
"PULSE_SERVER=unix:/run/user/65534/pulse/native"
];
fs = fs "dead" {
@@ -97,6 +97,7 @@ in
"fstab" = fs "80001ff" null null;
"hsurc" = fs "80001ff" null null;
"fuse.conf" = fs "80001ff" null null;
"gai.conf" = fs "80001ff" null null;
"group" = fs "180" null "hakurei:x:65534:\n";
"host.conf" = fs "80001ff" null null;
"hostname" = fs "80001ff" null null;
@@ -159,12 +160,8 @@ in
user = fs "800001ed" {
"65534" = fs "800001c0" {
bus = fs "10001fd" null null;
pulse = fs "800001c0" {
native = fs "10001ff" null null;
pid = fs "1a4" null null;
} null;
pulse = fs "800001c0" { native = fs "10001ff" null null; } null;
wayland-0 = fs "1000038" null null;
pipewire-0 = fs "1000038" null null;
} null;
} null;
} null;
@@ -213,8 +210,6 @@ in
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;
nix = fs "800001ed" {
profiles = fs "800001ed" {
home-manager = fs "80001ff" null null;
home-manager-1-link = fs "80001ff" null null;
profile = fs "80001ff" null null;
profile-1-link = fs "80001ff" null null;
} null;
@@ -251,11 +246,10 @@ in
(ent "/" "/tmp" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10001,gid=10001")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10001,gid=10001")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10001,gid=10001")
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/pipewire-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
(ent "/block" "/sys/block" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/bus" "/sys/bus" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
@@ -263,9 +257,10 @@ in
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/lib/hakurei/u0/a1" "/var/lib/hakurei/u0/a1" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/var/lib/hakurei/u0/a1" "/var/lib/hakurei/u0/a1" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
];
seccomp = true;

View File

@@ -50,7 +50,6 @@ in
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus"
"DISPLAY=:0"
"HOME=/var/lib/hakurei/u0/a2"
"PIPEWIRE_REMOTE=/run/user/65534/pipewire-0"
"SHELL=/run/current-system/sw/bin/bash"
"TERM=linux"
"USER=u0_a2"
@@ -58,6 +57,7 @@ in
"XDG_RUNTIME_DIR=/run/user/65534"
"XDG_SESSION_CLASS=user"
"XDG_SESSION_TYPE=wayland"
"PULSE_SERVER=unix:/run/user/65534/pulse/native"
];
fs = fs "dead" {
@@ -102,6 +102,7 @@ in
"fstab" = fs "80001ff" null null;
"hsurc" = fs "80001ff" null null;
"fuse.conf" = fs "80001ff" null null;
"gai.conf" = fs "80001ff" null null;
"group" = fs "180" null "hakurei:x:65534:\n";
"host.conf" = fs "80001ff" null null;
"hostname" = fs "80001ff" null null;
@@ -164,12 +165,8 @@ in
user = fs "800001ed" {
"65534" = fs "800001f8" {
bus = fs "10001fd" null null;
pulse = fs "800001c0" {
native = fs "10001ff" null null;
pid = fs "1a4" null null;
} null;
pulse = fs "800001c0" { native = fs "10001ff" null null; } null;
wayland-0 = fs "1000038" null null;
pipewire-0 = fs "1000038" null null;
} null;
} null;
} null;
@@ -220,8 +217,6 @@ in
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;
nix = fs "800001ed" {
profiles = fs "800001ed" {
home-manager = fs "80001ff" null null;
home-manager-1-link = fs "80001ff" null null;
profile = fs "80001ff" null null;
profile-1-link = fs "80001ff" null null;
} null;
@@ -257,16 +252,15 @@ in
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10002,gid=10002")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=16384k,mode=755,uid=10002,gid=10002")
(ent "/tmp/hakurei.0/runtime/2" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/hakurei.0/runtime/2" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/" "/tmp" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10002,gid=10002")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10002,gid=10002")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10002,gid=10002")
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/.X11-unix" "/tmp/.X11-unix" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/pipewire-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/tmp/.X11-unix" "/tmp/.X11-unix" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
(ent "/block" "/sys/block" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/bus" "/sys/bus" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
@@ -274,12 +268,13 @@ in
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/" "/.hakurei/.ro-store" "rw,relatime" "overlay" "overlay" "ro,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,redirect_dir=nofollow,userxattr")
(ent "/" "/.hakurei/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,upperdir=/host/tmp/.hakurei-store-rw/upper,workdir=/host/tmp/.hakurei-store-rw/work,redirect_dir=nofollow,uuid=on,userxattr")
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/lib/hakurei/u0/a2" "/var/lib/hakurei/u0/a2" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent "/var/lib/hakurei/u0/a2" "/var/lib/hakurei/u0/a2" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw")
];
seccomp = true;

View File

@@ -6,7 +6,7 @@
}:
let
testProgram = pkgs.callPackage ./tool/package.nix { inherit (config.environment.hakurei.package) version; };
testCases = import ./case pkgs.system lib testProgram;
testCases = import ./case pkgs.stdenv.hostPlatform.system lib testProgram;
in
{
users.users = {
@@ -33,7 +33,7 @@ in
hakurei -v run hakurei-test \
-p "/var/tmp/.hakurei-check-ok.0" \
-t ${toString (builtins.toFile "hakurei-pd-want.json" (builtins.toJSON testCases.pd.want))} \
-s ${testCases.pd.expectedFilter.${pkgs.system}} "$@"
-s ${testCases.pd.expectedFilter.${pkgs.stdenv.hostPlatform.system}} "$@"
'')
];

View File

@@ -1,12 +1,12 @@
{
lib,
nixosTest,
testers,
self,
withRace ? false,
}:
nixosTest {
testers.nixosTest {
name = "hakurei-sandbox" + (if withRace then "-race" else "");
nodes.machine =
{ options, pkgs, ... }:

View File

@@ -79,8 +79,10 @@ check_sandbox("device")
check_sandbox("pdlike")
# Exit Sway and verify process exit status 0:
machine.wait_until_fails("pgrep -x hakurei", timeout=5)
swaymsg("exit", succeed=False)
machine.wait_for_file("/tmp/sway-exit-ok")
# Print hakurei runDir contents:
print(machine.fail("ls /run/user/1000/hakurei"))
machine.succeed("find /tmp -maxdepth 1 -type d -name '.hakurei-shim-*' -print -exec false '{}' +")

View File

@@ -226,15 +226,17 @@ machine.send_chars("clear; pactl info && touch /var/tmp/pulse-ok\n")
machine.wait_for_file("/var/tmp/pulse-ok", timeout=15)
collect_state_ui("pulse_wayland")
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.fail("find /tmp -maxdepth 1 -type d -name '.hakurei-shim-*' -print -exec false '{}' +")
machine.send_chars("exit\n")
machine.wait_until_fails("pgrep foot", timeout=5)
machine.wait_until_fails("pgrep -x hakurei", timeout=5)
machine.succeed("find /tmp -maxdepth 1 -type d -name '.hakurei-shim-*' -print -exec false '{}' +")
# Test PipeWire SecurityContext:
machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v run --pulse pactl info")
machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v run --pulse pactl set-sink-mute @DEFAULT_SINK@ toggle")
# Test PipeWire direct access:
machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 pw-dump")
machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v run --pipewire pw-dump")
# Test XWayland (foot does not support X):
swaymsg("exec x11-alacritty")
@@ -280,6 +282,7 @@ machine.send_key("ctrl-c")
machine.wait_until_fails("pgrep foot", timeout=5)
# Exit Sway and verify process exit status 0:
machine.wait_until_fails("pgrep -x hakurei", timeout=5)
swaymsg("exit", succeed=False)
machine.wait_for_file("/tmp/sway-exit-ok")
@@ -289,6 +292,7 @@ print(machine.succeed("find /tmp/hakurei.0 "
+ "-path '/tmp/hakurei.0/tmpdir/*/*' -prune -o "
+ "-print"))
print(machine.succeed("find /run/user/1000/hakurei"))
machine.succeed("find /tmp -maxdepth 1 -type d -name '.hakurei-shim-*' -print -exec false '{}' +")
# Verify go test status:
machine.wait_for_file("/tmp/hakurei-test-done")