130 Commits

Author SHA1 Message Date
cat c538df7daa internal/pipewire: expose connection props
Test / Create distribution (push) Successful in 29s
Test / Sandbox (push) Successful in 2m53s
Test / Sandbox (race detector) (push) Successful in 4m47s
Test / Hpkg (push) Successful in 5m10s
Test / Hakurei (race detector) (push) Successful in 6m29s
Test / Hakurei (push) Successful in 45s
Test / Flake checks (push) Successful in 1m37s
Unused in hakurei but could be useful when the package is moved out of internal.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-09 06:51:12 +09:00
cat 44e5aa1a36 internal/pipewire: include remaining size in recvmsg wrapper
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m36s
Test / Sandbox (race detector) (push) Successful in 4m44s
Test / Hpkg (push) Successful in 4m44s
Test / Hakurei (race detector) (push) Successful in 6m20s
Test / Hakurei (push) Successful in 3m35s
Test / Flake checks (push) Successful in 1m22s
This otherwise truncates the received data by len(remaining) bytes.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-09 06:36:46 +09:00
cat cf0e7d8c27 internal/pipewire: reset per-roundtrip state once per call
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m38s
Test / Sandbox (race detector) (push) Successful in 4m40s
Test / Hakurei (push) Successful in 5m1s
Test / Hpkg (push) Successful in 4m58s
Test / Hakurei (race detector) (push) Successful in 6m22s
Test / Flake checks (push) Successful in 1m21s
This was left in consume when relocating per-roundtrip code out of the per-receive consume method.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-09 06:28:33 +09:00
cat 130add21e5 internal/pipewire: increment remote sequence after establishing bounds
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m39s
Test / Hakurei (push) Successful in 4m45s
Test / Sandbox (race detector) (push) Successful in 4m47s
Test / Hpkg (push) Successful in 4m53s
Test / Hakurei (race detector) (push) Successful in 6m41s
Test / Flake checks (push) Successful in 1m39s
This avoids incrementing it twice proceeding from a partial message.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-09 06:21:41 +09:00
cat 5ec4045e24 internal/pipewire: do not clobber error parsing SCMs
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m35s
Test / Sandbox (race detector) (push) Successful in 4m43s
Test / Hakurei (push) Successful in 4m50s
Test / Hpkg (push) Successful in 4m54s
Test / Hakurei (race detector) (push) Successful in 6m29s
Test / Flake checks (push) Successful in 1m21s
The error is handled later, clobbering it here breaks error handling when SCMs are present.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-09 06:02:15 +09:00
cat be2075f169 Revert "internal/pipewire: work around remote sequence quirk"
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m38s
Test / Hakurei (push) Successful in 4m44s
Test / Sandbox (race detector) (push) Successful in 4m50s
Test / Hpkg (push) Successful in 4m52s
Test / Hakurei (race detector) (push) Successful in 6m28s
Test / Flake checks (push) Successful in 1m21s
This reverts commit 564db6863b.
2025-12-09 05:25:41 +09:00
cat e9fb1d7be5 container/initdaemon: copy wstatus from wait4 loop
Test / Create distribution (push) Successful in 27s
Test / Sandbox (push) Successful in 44s
Test / Sandbox (race detector) (push) Successful in 42s
Test / Hakurei (push) Successful in 48s
Test / Hpkg (push) Successful in 44s
Test / Hakurei (race detector) (push) Successful in 47s
Test / Flake checks (push) Successful in 1m37s
Due to the special nature of the init process, direct use of wait outside the wait4 loop is racy. This change copies the wstatus from wait4 loop state instead.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 22:58:42 +09:00
cat dafe9f8efc container: spin instead of block on wait4 ECHILD
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m44s
Test / Sandbox (race detector) (push) Successful in 4m43s
Test / Hpkg (push) Successful in 4m57s
Test / Hakurei (push) Successful in 5m2s
Test / Hakurei (race detector) (push) Successful in 6m27s
Test / Flake checks (push) Successful in 1m27s
Blocking prevents further wait4 processing causing ops to never receive their signals.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 22:56:13 +09:00
cat 96dd7abd80 container: improve error message fallback
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m38s
Test / Sandbox (race detector) (push) Successful in 4m42s
Test / Hpkg (push) Successful in 4m52s
Test / Hakurei (push) Successful in 5m0s
Test / Hakurei (race detector) (push) Successful in 6m27s
Test / Flake checks (push) Successful in 1m28s
This now falls back to message.Error if no other concrete type is matched.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 22:45:54 +09:00
cat d5fb179012 cmd/hakurei: exec instead of fork/exec from shell
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m44s
Test / Sandbox (race detector) (push) Successful in 4m40s
Test / Hakurei (push) Successful in 4m53s
Test / Hpkg (push) Successful in 5m5s
Test / Hakurei (race detector) (push) Successful in 6m26s
Test / Flake checks (push) Successful in 1m27s
There is no reason to keep the shell process around.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 22:29:41 +09:00
cat 462863e290 container: friendlier error message for op timing out
Test / Create distribution (push) Successful in 40s
Test / Sandbox (push) Successful in 2m38s
Test / Hakurei (push) Successful in 4m50s
Test / Sandbox (race detector) (push) Successful in 4m52s
Test / Hpkg (push) Successful in 5m4s
Test / Hakurei (race detector) (push) Successful in 6m36s
Test / Flake checks (push) Successful in 1m26s
This includes the string for the failing op which helps with troubleshooting.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 22:19:03 +09:00
cat 2786611b88 test/interactive: add app with bad daemon
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 40s
Test / Sandbox (race detector) (push) Successful in 41s
Test / Hakurei (push) Successful in 44s
Test / Hakurei (race detector) (push) Successful in 45s
Test / Hpkg (push) Successful in 41s
Test / Flake checks (push) Successful in 1m28s
This is useful for testing daemon error handling behaviour.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 22:12:07 +09:00
cat 791a1dfa55 container: make wait4 loop available to ops
Test / Create distribution (push) Successful in 38s
Test / Sandbox (push) Successful in 2m36s
Test / Sandbox (race detector) (push) Successful in 4m43s
Test / Hpkg (push) Successful in 4m44s
Test / Hakurei (push) Successful in 4m55s
Test / Hakurei (race detector) (push) Successful in 6m23s
Test / Flake checks (push) Successful in 1m32s
Due to the special nature of the init process, regular wait calls are unavailable. This change provides infrastructure to access wait4 loop state from Op.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 21:43:49 +09:00
cat 564db6863b internal/pipewire: work around remote sequence quirk
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m39s
Test / Sandbox (race detector) (push) Successful in 4m41s
Test / Hakurei (push) Successful in 4m57s
Test / Hpkg (push) Successful in 4m58s
Test / Hakurei (race detector) (push) Successful in 6m17s
Test / Flake checks (push) Successful in 1m26s
Remote sequence sometimes start with a non-zero value, and keeps the same value for a while before going back to zero. Conditions for reproducing this behaviour is not yet known. This change works around this behaviour.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 20:10:01 +09:00
cat 87781c7658 treewide: include PipeWire op and enforce PulseAudio check
Test / Create distribution (push) Successful in 29s
Test / Sandbox (push) Successful in 40s
Test / Sandbox (race detector) (push) Successful in 41s
Test / Hakurei (push) Successful in 44s
Test / Hpkg (push) Successful in 41s
Test / Hakurei (race detector) (push) Successful in 45s
Test / Flake checks (push) Successful in 1m29s
This fully replaces PulseAudio with PipeWire and enforces the PulseAudio check and error message. The pipewire-pulse daemon is handled in the NixOS module.

Closes #26.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 08:53:04 +09:00
cat 0c38fb7b6a hst: expose daemon as fs entry
Test / Create distribution (push) Successful in 38s
Test / Sandbox (push) Successful in 2m48s
Test / Sandbox (race detector) (push) Successful in 4m48s
Test / Hakurei (push) Successful in 5m15s
Test / Hpkg (push) Successful in 5m18s
Test / Hakurei (race detector) (push) Successful in 6m38s
Test / Flake checks (push) Successful in 1m25s
This is slightly counterintuitive, but it turned out well under this framework since the daemon backs its target file.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 07:38:47 +09:00
cat 357cfcddee container: start daemons within container
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m32s
Test / Sandbox (race detector) (push) Successful in 4m41s
Test / Hakurei (push) Successful in 5m7s
Test / Hpkg (push) Successful in 5m5s
Test / Hakurei (race detector) (push) Successful in 6m26s
Test / Flake checks (push) Successful in 1m27s
This is useful for daemons internal to the container. The only current use case is pipewire-pulse.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 07:21:04 +09:00
cat 6bf245cf1b container: pass context as setup state
Test / Create distribution (push) Successful in 29s
Test / Sandbox (push) Successful in 2m42s
Test / Sandbox (race detector) (push) Successful in 4m52s
Test / Hakurei (push) Successful in 5m26s
Test / Hpkg (push) Successful in 5m28s
Test / Hakurei (race detector) (push) Successful in 7m1s
Test / Flake checks (push) Successful in 1m32s
This is useful currently for daemon Op, but could be used for many other things.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 06:06:19 +09:00
cat c8eeb4a4d1 internal/outcome: integrate pipewire server
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m37s
Test / Sandbox (race detector) (push) Successful in 4m37s
Test / Hakurei (push) Successful in 4m47s
Test / Hpkg (push) Successful in 4m57s
Test / Hakurei (race detector) (push) Successful in 6m23s
Test / Flake checks (push) Successful in 1m31s
This is very simple and takes almost no inputs. This is not yet hooked up to anything to prevent breaking any existing behaviour.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 05:03:16 +09:00
cat 5785714b64 container: call op method right before initial process
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m39s
Test / Sandbox (race detector) (push) Successful in 4m42s
Test / Hakurei (push) Successful in 4m51s
Test / Hpkg (push) Successful in 5m1s
Test / Hakurei (race detector) (push) Successful in 6m23s
Test / Flake checks (push) Successful in 1m41s
This is at a point considered to be already "within" the container. Daemons internal to the container can be started here.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 04:57:24 +09:00
cat 422efcf258 hst: check for insecure PulseAudio enablement
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 43s
Test / Sandbox (race detector) (push) Successful in 42s
Test / Hakurei (push) Successful in 47s
Test / Hakurei (race detector) (push) Successful in 46s
Test / Hpkg (push) Successful in 5m39s
Test / Flake checks (push) Successful in 1m32s
This is currently still a noop, but required for #26.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 03:13:02 +09:00
cat 104eeecf65 cmd/hakurei: add pipewire flag
Test / Create distribution (push) Successful in 38s
Test / Sandbox (push) Successful in 2m38s
Test / Sandbox (race detector) (push) Successful in 4m43s
Test / Hakurei (push) Successful in 5m3s
Test / Hpkg (push) Successful in 5m1s
Test / Hakurei (race detector) (push) Successful in 6m34s
Test / Flake checks (push) Successful in 1m30s
This is for "run" command, formerly permissive defaults behaviour.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 02:39:55 +09:00
cat bf856f06e5 internal/pipewire: constant for PIPEWIRE_REMOTE
Test / Create distribution (push) Successful in 41s
Test / Sandbox (push) Successful in 2m43s
Test / Sandbox (race detector) (push) Successful in 4m45s
Test / Hakurei (push) Successful in 5m2s
Test / Hpkg (push) Successful in 5m7s
Test / Hakurei (race detector) (push) Successful in 6m36s
Test / Flake checks (push) Successful in 1m30s
This is not defined anywhere in upstream PipeWire, no idea why.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-07 23:35:29 +09:00
cat 1931b54600 hst: add pipewire flag
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m36s
Test / Sandbox (race detector) (push) Successful in 4m47s
Test / Hpkg (push) Successful in 4m55s
Test / Hakurei (push) Successful in 4m59s
Test / Hakurei (race detector) (push) Successful in 6m26s
Test / Flake checks (push) Successful in 1m31s
These are for #26. None of them are implemented yet. This fixes up test cases for the change to happen. Existing source code and JSON configuration continue to have the same effect. Existing flags get its EPulse bit replaced by EPipeWire.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-07 22:34:40 +09:00
cat 093e30c788 internal/system: integrate PipeWire SecurityContext
Test / Create distribution (push) Successful in 29s
Test / Sandbox (race detector) (push) Successful in 42s
Test / Sandbox (push) Successful in 43s
Test / Hakurei (push) Successful in 47s
Test / Hakurei (race detector) (push) Successful in 46s
Test / Hpkg (push) Successful in 43s
Test / Flake checks (push) Successful in 1m32s
Tests for this Op happens to be the best out of everything due to the robust infrastructure offered by internal/pipewire.

This is now ready to use in internal/outcome for implementing #26.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-07 17:39:34 +09:00
cat 1b17ccda91 internal/system: optional op check parallelism
Test / Create distribution (push) Successful in 40s
Test / Sandbox (push) Successful in 2m46s
Test / Sandbox (race detector) (push) Successful in 4m46s
Test / Hpkg (push) Successful in 5m17s
Test / Hakurei (push) Successful in 5m32s
Test / Hakurei (race detector) (push) Successful in 6m47s
Test / Flake checks (push) Successful in 1m33s
The PipeWire Op check cannot be made parallel due to the OS interaction.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-07 17:31:10 +09:00
cat 7c6fc1128b internal/pipewire: set finalizer on scc
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 45s
Test / Sandbox (race detector) (push) Successful in 2m38s
Test / Hpkg (push) Successful in 4m16s
Test / Hakurei (push) Successful in 4m27s
Test / Hakurei (race detector) (push) Successful in 4m52s
Test / Flake checks (push) Successful in 1m38s
This prevents leaking the socket and pipe fds.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-07 17:29:22 +09:00
cat 8cdd659239 internal/pipewire: seq access method for consume
Test / Create distribution (push) Successful in 40s
Test / Sandbox (push) Successful in 2m41s
Test / Sandbox (race detector) (push) Successful in 4m48s
Test / Hakurei (push) Successful in 4m56s
Test / Hpkg (push) Successful in 5m7s
Test / Hakurei (race detector) (push) Successful in 6m32s
Test / Flake checks (push) Successful in 1m33s
This improves readability as the offset is not immediately obvious.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-07 17:07:30 +09:00
cat 15c2839a09 internal/pipewire: respond to Core::Ping
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m39s
Test / Sandbox (race detector) (push) Successful in 4m41s
Test / Hpkg (push) Successful in 5m4s
Test / Hakurei (race detector) (push) Successful in 6m17s
Test / Hakurei (push) Successful in 4m11s
Test / Flake checks (push) Successful in 1m39s
There is currently no known message that will get the PipeWire server to emit this event. It should be handled regardless.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-07 14:55:56 +09:00
cat b9b9705b52 internal/pipewire: specify opcode and file count with message
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m39s
Test / Sandbox (race detector) (push) Successful in 4m41s
Test / Hakurei (push) Successful in 4m56s
Test / Hpkg (push) Successful in 4m57s
Test / Hakurei (race detector) (push) Successful in 6m31s
Test / Flake checks (push) Successful in 1m30s
This adds checking of FileCount while writing a message. Message encoding is relocated to an exported method to be used externally, probably for test stubbing.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-07 13:54:11 +09:00
cat 246e04214a internal/system: pass syscall error message
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m43s
Test / Sandbox (race detector) (push) Successful in 4m43s
Test / Hakurei (push) Successful in 5m2s
Test / Hpkg (push) Successful in 5m0s
Test / Hakurei (race detector) (push) Successful in 6m25s
Test / Flake checks (push) Successful in 1m32s
This makes wrapped syscall errors produce a cleaner error message.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-07 02:08:05 +09:00
cat 503bfc6468 internal/system: port connect by name
Test / Create distribution (push) Successful in 38s
Test / Sandbox (push) Successful in 2m33s
Test / Sandbox (race detector) (push) Successful in 4m42s
Test / Hpkg (push) Successful in 5m1s
Test / Hakurei (push) Successful in 5m13s
Test / Hakurei (race detector) (push) Successful in 6m23s
Test / Flake checks (push) Successful in 1m31s
This behaviour is a bit messy and checks what appears to be a windows-specific environment variable for some reason. Keeping everything intact regardless to match upstream behaviour.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-07 02:00:19 +09:00
cat d837628b4c internal/system: remove ineffectual join reverting wayland
Test / Create distribution (push) Successful in 38s
Test / Sandbox (push) Successful in 2m35s
Test / Sandbox (race detector) (push) Successful in 4m44s
Test / Hpkg (push) Successful in 4m59s
Test / Hakurei (push) Successful in 5m5s
Test / Hakurei (race detector) (push) Successful in 6m38s
Test / Flake checks (push) Successful in 1m33s
Removing the pathname socket used to be handled separately, now it is done during the Close call.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-07 00:43:50 +09:00
cat 3cb58b4b72 internal/pipewire: high level SecurityContext helper
Test / Create distribution (push) Successful in 40s
Test / Sandbox (push) Successful in 2m47s
Test / Sandbox (race detector) (push) Successful in 4m47s
Test / Hpkg (push) Successful in 5m10s
Test / Hakurei (push) Successful in 5m19s
Test / Hakurei (race detector) (push) Successful in 6m34s
Test / Flake checks (push) Successful in 1m33s
This sets up close pipe and socket internally, and exposes the resulting pathname socket and close_fd cleanup as an io.Closer.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-06 21:16:27 +09:00
cat bb1fc4c7bc internal/pipewire: check pending ids after done
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m41s
Test / Hakurei (push) Successful in 4m49s
Test / Sandbox (race detector) (push) Successful in 4m49s
Test / Hpkg (push) Successful in 5m3s
Test / Hakurei (race detector) (push) Successful in 47s
Test / Flake checks (push) Successful in 1m42s
This is not guaranteed to have completed after a roundtrip. This is leftover from when Roundtrip also sent and waited for sync.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-06 21:09:55 +09:00
cat f44923da29 internal/pipewire: post-sync cleanup functions
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m42s
Test / Sandbox (race detector) (push) Successful in 4m47s
Test / Hpkg (push) Successful in 5m40s
Test / Hakurei (push) Successful in 5m45s
Test / Hakurei (race detector) (push) Successful in 6m33s
Test / Flake checks (push) Successful in 1m43s
This makes it easier to handle resources who only needs to stay alive before the next sync.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-06 21:03:21 +09:00
cat 5e7861bb00 internal/pipewire: handle dangling files in roundtrip
Test / Create distribution (push) Successful in 39s
Test / Sandbox (push) Successful in 2m40s
Test / Sandbox (race detector) (push) Successful in 4m47s
Test / Hakurei (push) Successful in 5m3s
Test / Hpkg (push) Successful in 5m5s
Test / Hakurei (race detector) (push) Successful in 6m50s
Test / Flake checks (push) Successful in 1m32s
This should not be handled on every receive as it could cause valid (though impossible in current upstream implementation) messages to be rejected and raise a protocol error.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-06 19:21:57 +09:00
cat 7cb3308a53 internal/pipewire: store proxy errors in context
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m37s
Test / Sandbox (race detector) (push) Successful in 4m37s
Test / Hakurei (push) Successful in 4m50s
Test / Hpkg (push) Successful in 5m6s
Test / Hakurei (race detector) (push) Successful in 4m39s
Test / Flake checks (push) Successful in 1m41s
This change fixes handling of non-fatal errors during a roundtrip as there can be multiple receive calls per roundtrip.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-06 19:13:46 +09:00
cat 490093a659 internal/pipewire: set errno on an empty message
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m40s
Test / Sandbox (race detector) (push) Successful in 4m42s
Test / Hakurei (push) Successful in 5m0s
Test / Hpkg (push) Successful in 5m3s
Test / Hakurei (race detector) (push) Successful in 6m27s
Test / Flake checks (push) Successful in 1m30s
This matches upstream behaviour.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-06 16:50:49 +09:00
cat 2b22efcdf1 internal/pipewire: rename context consume method
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m45s
Test / Sandbox (race detector) (push) Successful in 4m43s
Test / Hakurei (push) Successful in 5m3s
Test / Hpkg (push) Successful in 5m7s
Test / Hakurei (race detector) (push) Successful in 46s
Test / Flake checks (push) Successful in 1m37s
This name is more correct since it does not roundtrip, but receives messages. This is also more consistent with the method on event proxies.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-06 16:44:39 +09:00
cat 8a2f9edcf9 internal/pipewire: use sendmsg/recvmsg directly
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m41s
Test / Sandbox (race detector) (push) Successful in 4m46s
Test / Hakurei (push) Successful in 4m58s
Test / Hpkg (push) Successful in 5m4s
Test / Hakurei (race detector) (push) Successful in 6m32s
Test / Flake checks (push) Successful in 1m29s
The PipeWire protocol does not work with Go abstractions. This change makes relevant methods call sendmsg/recvmsg directly.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-06 02:12:47 +09:00
cat 0d3f332d45 internal/pipewire: do not send ancillary msg without files
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m41s
Test / Sandbox (race detector) (push) Successful in 4m46s
Test / Hakurei (push) Successful in 4m55s
Test / Hpkg (push) Successful in 4m53s
Test / Hakurei (race detector) (push) Successful in 6m31s
Test / Flake checks (push) Successful in 1m30s
This is unnecessary and does not match upstream behaviour.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-05 04:20:04 +09:00
cat d5509cc6e5 internal/pipewire: constants from pipewire/keys.h
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m37s
Test / Sandbox (race detector) (push) Successful in 4m46s
Test / Hpkg (push) Successful in 4m53s
Test / Hakurei (push) Successful in 5m1s
Test / Hakurei (race detector) (push) Successful in 6m13s
Test / Flake checks (push) Successful in 1m22s
These are PipeWire spa_dict keys. Interestingly many keys in the sample are undefined and appear as magic strings in upstream source code.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-05 03:27:30 +09:00
cat 0d3ae6cb23 internal/pipewire: improve protocol error messages
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m41s
Test / Sandbox (race detector) (push) Successful in 4m40s
Test / Hakurei (push) Successful in 4m45s
Test / Hpkg (push) Successful in 5m1s
Test / Hakurei (race detector) (push) Successful in 6m13s
Test / Flake checks (push) Successful in 1m27s
These are mostly small formatting changes, with the biggest change being to UnexpectedEOFError where its kind is now described as part of the error type.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-04 03:15:58 +09:00
cat 69b1131d66 internal/pipewire: use type name in error strings
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m44s
Test / Sandbox (race detector) (push) Successful in 4m39s
Test / Hakurei (push) Successful in 4m52s
Test / Hpkg (push) Successful in 4m53s
Test / Hakurei (race detector) (push) Successful in 6m28s
Test / Flake checks (push) Successful in 1m33s
This provides more useful messages for protocol errors.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-04 02:29:27 +09:00
cat 2c0b92771a internal/pipewire: relocate constants
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m40s
Test / Sandbox (race detector) (push) Successful in 4m37s
Test / Hakurei (push) Successful in 4m48s
Test / Hpkg (push) Successful in 4m50s
Test / Hakurei (race detector) (push) Successful in 6m27s
Test / Flake checks (push) Successful in 1m21s
This should make things easier to navigate, and possible to fully automatically generate the constants in the future.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-03 03:52:17 +09:00
cat 054c91879f internal/pipewire: finalizers for dangling files
Test / Create distribution (push) Successful in 27s
Test / Sandbox (race detector) (push) Successful in 41s
Test / Sandbox (push) Successful in 43s
Test / Hpkg (push) Successful in 43s
Test / Hakurei (push) Successful in 47s
Test / Hakurei (race detector) (push) Successful in 3m47s
Test / Flake checks (push) Successful in 1m26s
This makes their handling much less error-prone.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-03 03:36:02 +09:00
cat c34439fc5f internal/pipewire: collect non-protocol errors
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m44s
Test / Sandbox (race detector) (push) Successful in 4m39s
Test / Hpkg (push) Successful in 4m45s
Test / Hakurei (push) Successful in 4m59s
Test / Hakurei (race detector) (push) Successful in 6m31s
Test / Flake checks (push) Successful in 1m30s
These errors are recoverable and should not terminate event handling. Only terminate event handling for protocol errors or inconsistent state that makes further event handling impossible.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-03 01:35:43 +09:00
cat 32fb137bb2 internal/pipewire: fail on unacknowledged proxies
Test / Create distribution (push) Successful in 39s
Test / Sandbox (push) Successful in 2m39s
Test / Sandbox (race detector) (push) Successful in 4m42s
Test / Hakurei (push) Successful in 5m0s
Test / Hpkg (push) Successful in 4m58s
Test / Hakurei (race detector) (push) Successful in 6m32s
Test / Flake checks (push) Successful in 1m28s
These proxies (with special cases documented in the implementation) are only safe for use after acknowledgement from the server.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-02 21:17:46 +09:00
cat e7a665e043 internal/pipewire: handle Core::Error
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m37s
Test / Sandbox (race detector) (push) Successful in 4m40s
Test / Hakurei (push) Successful in 4m59s
Test / Hpkg (push) Successful in 5m2s
Test / Hakurei (race detector) (push) Successful in 6m24s
Test / Flake checks (push) Successful in 1m28s
This event is not encountered in the pw-container sample, but already has existing sample from an excerpt.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-02 06:16:18 +09:00
cat af741f20a0 internal/pipewire: implement client context
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m35s
Test / Sandbox (race detector) (push) Successful in 4m45s
Test / Hakurei (push) Successful in 5m0s
Test / Hpkg (push) Successful in 5m7s
Test / Hakurei (race detector) (push) Successful in 6m37s
Test / Flake checks (push) Successful in 1m34s
This consumes the entire sample, is validated to send identical messages and correctly handle received messages.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-02 06:03:21 +09:00
cat 39c6716fb0 internal/pipewire: use correct types in header
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m37s
Test / Sandbox (race detector) (push) Successful in 4m41s
Test / Hpkg (push) Successful in 4m54s
Test / Hakurei (push) Successful in 5m6s
Test / Hakurei (race detector) (push) Successful in 6m30s
Test / Flake checks (push) Successful in 1m32s
This was written when the protocol was still barely understood, so none of the types here are correct and match the rest of the protocol. This change corrects these types.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-02 03:32:10 +09:00
cat 7bc73afadd internal/pipewire: wrap EOF error for deserialisation
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m41s
Test / Sandbox (race detector) (push) Successful in 4m37s
Test / Hpkg (push) Successful in 4m58s
Test / Hakurei (push) Successful in 5m5s
Test / Hakurei (race detector) (push) Successful in 6m26s
Test / Flake checks (push) Successful in 1m34s
The io.ErrUnexpectedEOF error can be returned from multiple places. This change eases error handling.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-02 03:19:37 +09:00
cat 647aa9d02f internal/pipewire: preallocate for footer
Test / Create distribution (push) Successful in 38s
Test / Sandbox (push) Successful in 2m36s
Test / Sandbox (race detector) (push) Successful in 4m43s
Test / Hakurei (push) Successful in 5m11s
Test / Hpkg (push) Successful in 5m5s
Test / Hakurei (race detector) (push) Successful in 6m30s
Test / Flake checks (push) Successful in 1m31s
This is useful during serialisation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-02 03:06:27 +09:00
cat 91aaabaa1b internal/pipewire: benchmarks against Gob and JSON
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m17s
Test / Hpkg (push) Successful in 4m8s
Test / Sandbox (race detector) (push) Successful in 4m15s
Test / Hakurei (race detector) (push) Successful in 5m11s
Test / Flake checks (push) Successful in 1m33s
Performance does not matter for the use case of this library, but it is still interesting to know.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-30 12:13:46 +09:00
cat 3d4c7cdd9e internal/pipewire: implement Core::Error
Test / Create distribution (push) Successful in 39s
Test / Sandbox (push) Successful in 2m29s
Test / Hakurei (push) Successful in 3m23s
Test / Hpkg (push) Successful in 4m19s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 5m13s
Test / Flake checks (push) Successful in 1m31s
Sample was captured from pw-cli.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-30 01:39:39 +09:00
cat 4fd6d6c037 internal/pipewire: implement Core::Ping, Core::Pong
Test / Create distribution (push) Successful in 53s
Test / Sandbox (push) Successful in 2m31s
Test / Hakurei (push) Successful in 3m33s
Test / Hpkg (push) Successful in 4m17s
Test / Sandbox (race detector) (push) Successful in 4m27s
Test / Hakurei (race detector) (push) Successful in 5m21s
Test / Flake checks (push) Successful in 1m34s
I could not get the server to produce these events, however I am confident enough with the implementation to do it by hand.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-29 23:25:29 +09:00
cat de3fc7ba38 internal/pipewire: implement SecurityContext::Create
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m26s
Test / Hakurei (push) Successful in 3m18s
Test / Hpkg (push) Successful in 4m16s
Test / Sandbox (race detector) (push) Successful in 4m22s
Test / Hakurei (race detector) (push) Successful in 5m12s
Test / Flake checks (push) Successful in 1m27s
This is finally the thing we are after.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-29 16:46:39 +09:00
cat 5a5c4705dd internal/pipewire: implement Registry::Bind
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m27s
Test / Hakurei (push) Successful in 3m16s
Test / Hpkg (push) Successful in 4m5s
Test / Sandbox (race detector) (push) Successful in 4m21s
Test / Hakurei (race detector) (push) Successful in 5m10s
Test / Flake checks (push) Successful in 1m30s
This change also adds test cases for messages before this one.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-29 03:01:53 +09:00
cat f703aa20a5 internal/pipewire: implement client generation footer
Test / Create distribution (push) Successful in 38s
Test / Sandbox (push) Successful in 2m26s
Test / Hakurei (push) Successful in 3m16s
Test / Hpkg (push) Successful in 4m13s
Test / Sandbox (race detector) (push) Successful in 4m22s
Test / Hakurei (race detector) (push) Successful in 5m12s
Test / Flake checks (push) Successful in 1m29s
This corresponds with the core generation footer and seem to be the only other possible footer type.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-29 02:55:30 +09:00
cat 5c12425d48 internal/pipewire: implement Registry::Global
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m29s
Test / Hakurei (push) Successful in 3m20s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hakurei (race detector) (push) Successful in 5m11s
Test / Flake checks (push) Successful in 1m31s
Dealing with this event reawakened my burning hatred for OOP.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-28 02:32:45 +09:00
cat cbe86dc4f0 internal/pipewire: add json struct tags
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m26s
Test / Hakurei (push) Successful in 3m19s
Test / Hpkg (push) Successful in 4m11s
Test / Sandbox (race detector) (push) Successful in 4m23s
Test / Hakurei (race detector) (push) Successful in 5m12s
Test / Flake checks (push) Successful in 1m30s
These match the names found in documentation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-28 01:33:32 +09:00
cat d08a1081bd internal/pipewire: do not store spa_dict fields
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m29s
Test / Hakurei (push) Successful in 3m21s
Test / Hpkg (push) Successful in 4m12s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 5m15s
Test / Flake checks (push) Successful in 1m30s
This is effectively a poor man's slice, it is entirely unnecessary here and can be handled internally.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-28 01:25:18 +09:00
cat 72a2601d74 internal/pipewire: store sample iovec continuously
Test / Create distribution (push) Successful in 39s
Test / Sandbox (push) Successful in 2m22s
Test / Hpkg (push) Successful in 4m8s
Test / Sandbox (race detector) (push) Successful in 4m28s
Test / Hakurei (race detector) (push) Successful in 5m14s
Test / Hakurei (push) Successful in 2m26s
Test / Flake checks (push) Successful in 1m39s
This removes the need for manual splitting. The understanding of the format is robust enough to allow this to happen anyway.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-28 00:35:10 +09:00
cat 1dab87aaf0 internal/pipewire: add missing constants
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m16s
Test / Hpkg (push) Successful in 4m15s
Test / Sandbox (race detector) (push) Successful in 4m27s
Test / Hakurei (race detector) (push) Successful in 5m12s
Test / Flake checks (push) Successful in 1m24s
These did not appear useful at first since it was assumed to be filenames for loading modules.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-27 21:28:16 +09:00
cat 2bafde99e3 internal/pipewire: shorten test data filenames
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m25s
Test / Hakurei (push) Successful in 3m24s
Test / Hpkg (push) Successful in 4m10s
Test / Sandbox (race detector) (push) Successful in 4m22s
Test / Hakurei (race detector) (push) Successful in 5m13s
Test / Flake checks (push) Successful in 1m30s
These were getting very annoying to type.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-27 20:06:01 +09:00
cat 91efeb101a internal/pipewire: spa_dict size nil check
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m21s
Test / Hakurei (push) Successful in 3m14s
Test / Hpkg (push) Successful in 4m7s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hakurei (race detector) (push) Successful in 5m9s
Test / Flake checks (push) Successful in 1m24s
This fixes serialisation of NULL spa_dict.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-27 02:51:36 +09:00
cat dcb22a61c0 internal/pipewire: require appending marshaler
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m24s
Test / Hakurei (push) Successful in 3m22s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m17s
Test / Hakurei (race detector) (push) Successful in 5m8s
Test / Flake checks (push) Successful in 1m23s
This eliminates all non-reflect allocations.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-27 02:33:19 +09:00
cat e028a61fc1 internal/pipewire: preallocate for known size
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m23s
Test / Hakurei (push) Successful in 3m19s
Test / Hpkg (push) Successful in 4m13s
Test / Sandbox (race detector) (push) Successful in 4m27s
Test / Hakurei (race detector) (push) Successful in 5m10s
Test / Flake checks (push) Successful in 1m27s
This is still not efficient by any means, but it should eliminate most non-reflect allocation (all allocation if PODMarshaler is not used).

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-27 02:26:31 +09:00
cat 73987be7d4 internal/pipewire: size without serialisation
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m21s
Test / Hakurei (push) Successful in 3m16s
Test / Hpkg (push) Successful in 4m12s
Test / Sandbox (race detector) (push) Successful in 4m29s
Test / Hakurei (race detector) (push) Successful in 5m13s
Test / Flake checks (push) Successful in 1m28s
This is required to achieve zero allocation (other than reflect).

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-27 02:17:38 +09:00
cat 563b5e66fc internal/pipewire: simplify spa_dict appends
Test / Create distribution (push) Successful in 43s
Test / Sandbox (push) Successful in 2m33s
Test / Hakurei (push) Successful in 3m31s
Test / Hpkg (push) Successful in 4m16s
Test / Sandbox (race detector) (push) Successful in 4m29s
Test / Hakurei (race detector) (push) Successful in 5m20s
Test / Flake checks (push) Successful in 1m30s
This change uses the (somewhat) newly exposed MarshalAppend which improves readability.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-27 01:52:13 +09:00
cat 2edcfe1e68 internal/pipewire: define size constants
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m26s
Test / Hakurei (push) Successful in 3m17s
Test / Hpkg (push) Successful in 4m13s
Test / Sandbox (race detector) (push) Successful in 4m23s
Test / Hakurei (race detector) (push) Successful in 5m11s
Test / Flake checks (push) Successful in 1m26s
This gets rid of magic numbers in marshal/unmarshal.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-27 00:54:56 +09:00
cat 2698ca00e8 internal/pipewire: implement Core::Done
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m18s
Test / Hpkg (push) Successful in 4m8s
Test / Sandbox (race detector) (push) Successful in 4m19s
Test / Hakurei (race detector) (push) Successful in 5m9s
Test / Flake checks (push) Successful in 1m20s
The message in the sample does not correspond to any known method call. The spec does not mention what to do with messages like this, but all existing usage code simply drops it.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-26 19:02:21 +09:00
cat 1d0143386d internal/pipewire: optional final trailing garbage check
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m10s
Test / Sandbox (race detector) (push) Successful in 4m18s
Test / Hakurei (race detector) (push) Successful in 5m8s
Test / Flake checks (push) Successful in 1m20s
Omitting the check is only useful for custom unmarshaler.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-26 18:50:39 +09:00
cat a55c209099 internal/pipewire: additional Client::Info test case
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m10s
Test / Sandbox (race detector) (push) Successful in 4m20s
Test / Hakurei (race detector) (push) Successful in 5m6s
Test / Flake checks (push) Successful in 1m20s
This appears to add *one single entry* compared to the message before it. The inefficiency of this protocol is beyond imagination.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-26 16:28:57 +09:00
cat 10ff276da1 internal/pipewire: additional Client::Info test case
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m21s
Test / Hakurei (push) Successful in 3m16s
Test / Hpkg (push) Successful in 4m12s
Test / Sandbox (race detector) (push) Successful in 4m18s
Test / Hakurei (race detector) (push) Successful in 5m6s
Test / Flake checks (push) Successful in 1m19s
This message follows the other Client::Info event before it. No idea why.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-26 16:17:38 +09:00
cat fd4d379b67 internal/pipewire: implement Client::Info
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m16s
Test / Hpkg (push) Successful in 4m5s
Test / Sandbox (race detector) (push) Successful in 4m20s
Test / Hakurei (race detector) (push) Successful in 5m6s
Test / Flake checks (push) Successful in 1m22s
Everything is already supported, as usual.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-26 16:05:46 +09:00
cat 77f5b89a41 internal/pipewire: implement Core::BoundProps
Test / Create distribution (push) Successful in 41s
Test / Sandbox (push) Successful in 2m28s
Test / Hakurei (push) Successful in 3m26s
Test / Hpkg (push) Successful in 4m15s
Test / Sandbox (race detector) (push) Successful in 4m25s
Test / Hakurei (race detector) (push) Successful in 5m19s
Test / Flake checks (push) Successful in 1m20s
Very straightforward type, everything is already supported.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 18:40:19 +09:00
cat 14e33f17e5 internal/pipewire: check nil marshaler
Test / Create distribution (push) Successful in 43s
Test / Sandbox (push) Successful in 2m26s
Test / Sandbox (race detector) (push) Successful in 2m19s
Test / Hakurei (push) Successful in 2m34s
Test / Hakurei (race detector) (push) Successful in 3m9s
Test / Hpkg (push) Successful in 3m22s
Test / Flake checks (push) Successful in 1m33s
NULL values have special case in the format. This check ensures correctness serialising nil pointers.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 18:36:08 +09:00
cat cfeb7818eb internal/pipewire: implement Core::Info and generation footer
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m17s
Test / Hpkg (push) Successful in 4m15s
Test / Sandbox (race detector) (push) Successful in 4m20s
Test / Hakurei (race detector) (push) Successful in 5m7s
Test / Flake checks (push) Successful in 1m21s
These are not directly related but are first encountered on the same message in the capture.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 15:16:12 +09:00
cat 05391da556 internal/pipewire: implement footer
Test / Create distribution (push) Successful in 39s
Test / Sandbox (push) Successful in 2m26s
Test / Hakurei (push) Successful in 3m19s
Test / Hpkg (push) Successful in 4m13s
Test / Sandbox (race detector) (push) Successful in 4m25s
Test / Hakurei (race detector) (push) Successful in 5m15s
Test / Flake checks (push) Successful in 1m39s
The POD itself is serialised without requiring a special case, however its presence is only indicated by the difference in size recorded in the header and payload.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 15:11:22 +09:00
cat 463f8836e6 internal/pipewire: implement Long type
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 1m42s
Test / Hakurei (push) Successful in 2m30s
Test / Hpkg (push) Successful in 3m20s
Test / Sandbox (race detector) (push) Successful in 4m19s
Test / Hakurei (race detector) (push) Successful in 5m10s
Test / Flake checks (push) Successful in 1m37s
Thankfully no special case here.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 15:05:37 +09:00
cat 2e465c94da internal/pipewire: implement Id type
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m29s
Test / Hakurei (push) Successful in 3m19s
Test / Hpkg (push) Successful in 4m7s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hakurei (race detector) (push) Successful in 5m10s
Test / Flake checks (push) Successful in 1m22s
This is, in fact, just a glorified Int type.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 15:01:58 +09:00
cat 26009fd3f7 internal/pipewire: slice at POD boundary
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 47s
Test / Sandbox (race detector) (push) Successful in 2m16s
Test / Hakurei (race detector) (push) Successful in 3m5s
Test / Hpkg (push) Successful in 3m18s
Test / Flake checks (push) Successful in 1m34s
This prevents incorrectly reading trailing data as part of the current POD.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 14:58:56 +09:00
cat 2d7b896a8c internal/pipewire: bounds check against wire size
Test / Create distribution (push) Successful in 40s
Test / Sandbox (push) Successful in 2m23s
Test / Hakurei (push) Successful in 3m22s
Test / Hpkg (push) Successful in 4m13s
Test / Sandbox (race detector) (push) Successful in 4m25s
Test / Hakurei (race detector) (push) Successful in 5m14s
Test / Flake checks (push) Successful in 1m25s
This covers cases where wire size is not known ahead of time.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 13:42:31 +09:00
cat a0eb010aab internal/pipewire: spa_dict trailing garbage within POD
Test / Create distribution (push) Successful in 42s
Test / Sandbox (push) Successful in 1m30s
Test / Hakurei (push) Successful in 2m26s
Test / Hpkg (push) Successful in 3m20s
Test / Sandbox (race detector) (push) Successful in 4m21s
Test / Hakurei (race detector) (push) Successful in 5m11s
Test / Flake checks (push) Successful in 1m24s
This performs the check within the bounds of the POD only. This was not caught since spa_dict was only used as the final struct field until now.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 13:39:02 +09:00
cat b1b27ac1df internal/pipewire: zero size before validation
Test / Create distribution (push) Successful in 38s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m16s
Test / Hpkg (push) Successful in 4m8s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 5m9s
Test / Flake checks (push) Successful in 1m21s
Leftover values from previous invocations cause incorrect behaviour here.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 12:21:37 +09:00
cat fc3d78fe01 internal/pipewire: implement Core::Sync
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 3m13s
Test / Hpkg (push) Successful in 4m13s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 2m59s
Test / Flake checks (push) Successful in 1m31s
Once again, already entirely supported, the offset is not yet fully verified but makes intuitive sense. Will verify this on future occurrences of the message.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 08:52:06 +09:00
cat 591637264a internal/pipewire: implement Core::GetRegistry
Test / Create distribution (push) Successful in 38s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m13s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m19s
Test / Hakurei (race detector) (push) Successful in 5m10s
Test / Flake checks (push) Successful in 1m28s
This struct is entirely supported, so this change is very straightforward.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 08:34:19 +09:00
cat e77652bf89 internal/pipewire: move test data to files
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m25s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m21s
Test / Hakurei (race detector) (push) Successful in 5m10s
Test / Flake checks (push) Successful in 1m28s
These get very big later on, and would be painful to represent as the compound literal.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 08:09:10 +09:00
cat 88d3e46413 internal/pipewire: implement Client::UpdateProperties
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m17s
Test / Hakurei (push) Successful in 3m14s
Test / Hpkg (push) Successful in 4m14s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hakurei (race detector) (push) Successful in 5m10s
Test / Flake checks (push) Successful in 1m29s
This is the second message on the captured sample.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 04:16:11 +09:00
cat e51e81bb22 internal/pipewire: implement spa_dict type
Test / Create distribution (push) Successful in 40s
Test / Sandbox (push) Successful in 2m33s
Test / Hakurei (push) Successful in 3m22s
Test / Hpkg (push) Successful in 4m18s
Test / Sandbox (race detector) (push) Successful in 4m35s
Test / Hakurei (race detector) (push) Successful in 5m18s
Test / Flake checks (push) Successful in 1m26s
This is a terrible type that defies the type system. It is implemented on the concrete type to avoid special cases.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 04:08:52 +09:00
cat 8f4a3bcf9f internal/pipewire: use custom marshaler when available
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 45s
Test / Sandbox (race detector) (push) Successful in 2m19s
Test / Hakurei (push) Successful in 2m27s
Test / Hakurei (race detector) (push) Successful in 3m12s
Test / Hpkg (push) Successful in 3m31s
Test / Flake checks (push) Successful in 1m34s
This reduces special cases. This change also exposes unmarshalled message size on the wire.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 04:05:22 +09:00
cat 827dc9e1ba internal/pipewire: implement string type
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m23s
Test / Hakurei (push) Successful in 3m14s
Test / Hpkg (push) Successful in 4m16s
Test / Sandbox (race detector) (push) Successful in 4m20s
Test / Hakurei (race detector) (push) Successful in 5m11s
Test / Flake checks (push) Successful in 1m39s
This is still NUL terminated strings, and an extra NUL character on an 8-byte string does cause an extra 7 bytes of padding.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 04:00:59 +09:00
cat d92de1c709 internal/pipewire: check for trailing garbage
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m18s
Test / Hpkg (push) Successful in 4m8s
Test / Sandbox (race detector) (push) Successful in 4m20s
Test / Hakurei (race detector) (push) Successful in 5m9s
Test / Flake checks (push) Successful in 1m28s
This is useful during development.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 01:59:29 +09:00
cat 5bcafcf734 internal/pipewire: implement Core::Hello
Test / Create distribution (push) Successful in 39s
Test / Sandbox (push) Successful in 2m30s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m13s
Test / Sandbox (race detector) (push) Successful in 4m25s
Test / Hakurei (race detector) (push) Successful in 5m18s
Test / Flake checks (push) Successful in 1m28s
This implements enough types to correctly marshal and unmarshal Core::Hello.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-25 01:20:30 +09:00
cat 9f7b0c2f46 internal/pipewire: add type constants
Test / Create distribution (push) Successful in 34s
Test / Hakurei (race detector) (push) Successful in 5m10s
Test / Hakurei (push) Successful in 3m18s
Test / Sandbox (push) Successful in 1m29s
Test / Sandbox (race detector) (push) Successful in 2m27s
Test / Hpkg (push) Successful in 3m22s
Test / Flake checks (push) Successful in 1m28s
This change also centralises encoding testing.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-24 22:00:09 +09:00
cat 3e87187c4c internal/pipewire: implement message header
Test / Create distribution (push) Successful in 52s
Test / Sandbox (push) Successful in 2m42s
Test / Hakurei (push) Successful in 3m41s
Test / Hpkg (push) Successful in 4m20s
Test / Sandbox (race detector) (push) Successful in 4m39s
Test / Hakurei (race detector) (push) Successful in 5m30s
Test / Flake checks (push) Successful in 1m25s
Test cases are from interactions between pw-container and PipeWire. Results are validated against corresponding body.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-23 16:20:35 +09:00
cat b651d95e77 workflows: do not duplicate on pulls
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m18s
Test / Hakurei (push) Successful in 3m23s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m21s
Test / Hakurei (race detector) (push) Successful in 5m14s
Test / Flake checks (push) Successful in 1m32s
This condition causes two runs to be created on a pull, as gitea does not check whether a run has already been created for the current commit.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-19 06:59:32 +09:00
cat aab92ce3c1 internal/wayland: clean up pathname socket
Test / Hakurei (push) Successful in 10m33s
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 1m32s
Test / Hpkg (push) Successful in 3m24s
Test / Sandbox (race detector) (push) Successful in 4m19s
Test / Hakurei (race detector) (push) Successful in 5m12s
Test / Flake checks (push) Successful in 1m36s
This is cleaner than cleaning up in internal/system as it covers the failure paths.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-19 06:37:04 +09:00
cat a495e09a8f internal/wayland: do not double close fd
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m16s
Test / Hpkg (push) Successful in 4m7s
Test / Sandbox (race detector) (push) Successful in 4m14s
Test / Hakurei (race detector) (push) Successful in 5m5s
Test / Flake checks (push) Successful in 1m29s
These are already closed during securityContextBindPipe on a non-nil error.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-17 22:03:29 +09:00
cat 3afca2bd5b internal/wayland: expose WAYLAND_VERSION
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m13s
Test / Hakurei (race detector) (push) Successful in 5m6s
Test / Flake checks (push) Successful in 1m31s
This might be useful troubleshooting information.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-17 01:46:01 +09:00
cat b73a789dfe .clang-format: increase indent width
Test / Create distribution (push) Successful in 26s
Test / Sandbox (push) Successful in 2m27s
Test / Hakurei (push) Successful in 3m17s
Test / Hpkg (push) Successful in 3m27s
Test / Sandbox (race detector) (push) Successful in 4m21s
Test / Hakurei (race detector) (push) Successful in 4m59s
Test / Flake checks (push) Successful in 1m31s
This significantly increases readability. This patch is pretty big so it is being done after mostly everything has settled.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-16 20:57:29 +09:00
cat 38b5ff0cec internal/wayland: check pathname size
Test / Create distribution (push) Successful in 26s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Hakurei (push) Successful in 43s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Hpkg (push) Successful in 41s
Test / Flake checks (push) Successful in 1m22s
This avoids passing a truncated pathname to the kernel.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-16 03:34:05 +09:00
cat 3c204b9b40 internal/wayland: increase error detail
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m16s
Test / Hpkg (push) Successful in 4m5s
Test / Sandbox (race detector) (push) Successful in 4m18s
Test / Hakurei (race detector) (push) Successful in 5m8s
Test / Flake checks (push) Successful in 1m21s
This includes targeted paths in the returned errors.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-16 02:09:50 +09:00
cat 00771efeb4 internal/wayland: remove fd typecasts
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m21s
Test / Hakurei (push) Successful in 3m14s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m18s
Test / Hakurei (race detector) (push) Successful in 5m5s
Test / Flake checks (push) Successful in 1m24s
These are no longer necessary since RawConn is no longer used.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-16 01:45:37 +09:00
cat 61972d61f6 internal/wayland: reimplement connect/bind code
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m18s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m14s
Test / Hakurei (race detector) (push) Successful in 5m7s
Test / Flake checks (push) Successful in 1m26s
The old implementation is relocated to system/wayland/deprecated.go.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-16 01:23:16 +09:00
cat fe40af7b7e internal/wayland: relocate connection struct
Test / Create distribution (push) Successful in 44s
Test / Sandbox (push) Successful in 2m24s
Test / Hakurei (push) Successful in 3m23s
Test / Hpkg (push) Successful in 4m14s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 5m20s
Test / Flake checks (push) Successful in 1m33s
This interface is getting replaced, so relocating it to the deprecated wrapper package before working on its replacement.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-15 23:25:46 +09:00
cat 12751932d1 internal/wayland: improve error handling
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m21s
Test / Hakurei (push) Successful in 3m20s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 5m16s
Test / Flake checks (push) Successful in 1m32s
Note: wl_registry_add_listener is undocumented everywhere. Its implementation calls wl_proxy_add_listener which returns 0 on success or -1 on failure.
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-15 21:26:31 +09:00
cat 41b49137a8 .clang-format: do not limit line length
Test / Create distribution (push) Successful in 38s
Test / Sandbox (push) Successful in 2m18s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m5s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hakurei (race detector) (push) Successful in 5m10s
Test / Flake checks (push) Successful in 1m27s
This hard limit destroys readability in some places.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-15 17:06:43 +09:00
cat c761e1de4d nix: build with clang
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 41s
Test / Sandbox (race detector) (push) Successful in 41s
Test / Hakurei (push) Successful in 44s
Test / Hakurei (race detector) (push) Successful in 45s
Test / Hpkg (push) Successful in 42s
Test / Flake checks (push) Successful in 1m29s
Clang is better than gcc in various ways. This also pulls in clang-format which is very helpful.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-15 16:36:36 +09:00
cat a91920310d internal: relocate packages
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 3m19s
Test / Hpkg (push) Successful in 4m12s
Test / Sandbox (race detector) (push) Successful in 4m31s
Test / Hakurei (race detector) (push) Successful in 5m12s
Test / Flake checks (push) Successful in 1m32s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-15 13:58:34 +09:00
cat 16e674782a cmd/hakurei: reorder show entries
Test / Create distribution (push) Successful in 25s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Sandbox (push) Successful in 40s
Test / Hakurei (push) Successful in 44s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Hpkg (push) Successful in 41s
Test / Flake checks (push) Successful in 1m20s
This order semantically makes more sense and generally looks tidier.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-14 22:14:49 +09:00
cat 47244daefb treewide: migrate ldd callers
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m22s
Test / Hakurei (push) Successful in 3m17s
Test / Hpkg (push) Successful in 4m5s
Test / Sandbox (race detector) (push) Successful in 4m15s
Test / Hakurei (race detector) (push) Successful in 5m6s
Test / Flake checks (push) Successful in 1m22s
This discontinues use of the deprecated ldd.Exec function for #25.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-14 21:59:59 +09:00
cat 46fa104419 ldd: require absolute pathname
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m22s
Test / Hakurei (push) Successful in 3m18s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m12s
Test / Hakurei (race detector) (push) Successful in 5m7s
Test / Flake checks (push) Successful in 1m23s
The sandbox which ldd(1) runs in does not inherit parent work directory, so relative pathnames will not work correctly. While it is trivial to support such a use case, the use of relative pathnames is highly error-prone and generally frowned against in this project. The Exec function remains available under the same signature until v0.4.0 where it will be removed.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-14 21:53:10 +09:00
cat 45953b3d9c ldd: cancel on decoder error
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 3m14s
Test / Hpkg (push) Successful in 4m8s
Test / Sandbox (race detector) (push) Successful in 4m22s
Test / Hakurei (race detector) (push) Successful in 5m7s
Test / Flake checks (push) Successful in 1m28s
This prevents blocking from failures caused by ldd(1) emitting output that is not anticipated by the decoder.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-14 21:43:34 +09:00
cat 42759e7a9f ldd: create musl entry representation
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m16s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 3m59s
Test / Sandbox (race detector) (push) Successful in 4m15s
Test / Hakurei (race detector) (push) Successful in 5m4s
Test / Flake checks (push) Successful in 1m39s
This mostly helps with debugging.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-14 21:38:35 +09:00
cat 8e2d2c8246 ldd: check decoder scan guard
Test / Create distribution (push) Successful in 38s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m23s
Test / Hpkg (push) Successful in 4m8s
Test / Sandbox (race detector) (push) Successful in 4m19s
Test / Hakurei (race detector) (push) Successful in 5m12s
Test / Flake checks (push) Successful in 1m27s
This was unreachable via the Parse wrapper.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-14 18:32:47 +09:00
cat 299685775a container: provide usage example
Test / Create distribution (push) Successful in 39s
Test / Sandbox (push) Successful in 2m17s
Test / Hakurei (push) Successful in 3m15s
Test / Sandbox (race detector) (push) Successful in 4m12s
Test / Hpkg (push) Successful in 4m16s
Test / Hakurei (race detector) (push) Successful in 5m7s
Test / Flake checks (push) Successful in 1m29s
This requires cgo so unfortunately will not run in the playground.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-14 18:25:22 +09:00
cat b7406cc4c4 ldd: update package doc comment
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m18s
Test / Sandbox (race detector) (push) Successful in 4m16s
Test / Hpkg (push) Successful in 4m14s
Test / Hakurei (race detector) (push) Successful in 5m7s
Test / Flake checks (push) Successful in 1m34s
This should hopefully deter misuse of this package.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-14 17:49:01 +09:00
cat 690a0ed0d6 ldd: decode from reader
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m27s
Test / Hakurei (push) Successful in 3m20s
Test / Hpkg (push) Successful in 4m10s
Test / Sandbox (race detector) (push) Successful in 4m18s
Test / Hakurei (race detector) (push) Successful in 5m5s
Test / Flake checks (push) Successful in 1m31s
This should reduce memory footprint of the parsing process and allow decoding part of the stream.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-14 08:33:19 +09:00
cat a9d72a5eb1 internal/outcome: rename run from main
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m31s
Test / Hakurei (push) Successful in 3m23s
Test / Sandbox (race detector) (push) Successful in 4m14s
Test / Hpkg (push) Successful in 4m16s
Test / Hakurei (race detector) (push) Successful in 5m4s
Test / Flake checks (push) Successful in 1m30s
The "main.go" name is quite confusing as this is often only present in main packages.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-14 01:06:14 +09:00
cat 6d14bb814f container/fhs: add constant for /dev/shm/
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m21s
Test / Hakurei (push) Successful in 3m16s
Test / Hpkg (push) Successful in 4m11s
Test / Sandbox (race detector) (push) Successful in 4m12s
Test / Hakurei (race detector) (push) Successful in 5m6s
Test / Flake checks (push) Successful in 1m32s
This is mounted for the default read-only /dev/ when programs want to use shm_open(3). Defining it here is less error-prone and saves the extra append at runtime.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-14 01:03:26 +09:00
cat be0e387ab0 internal/info: relocate from internal
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m27s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m11s
Test / Sandbox (race detector) (push) Successful in 4m18s
Test / Hakurei (race detector) (push) Successful in 5m2s
Test / Flake checks (push) Successful in 1m30s
This is cleaner and makes more sense. The longer LDFLAGS was never a valid concern since it is always inserted by a script.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-13 07:29:46 +09:00
cat abeb67964f treewide: document linkname uses
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m17s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m6s
Test / Sandbox (race detector) (push) Successful in 4m12s
Test / Hakurei (race detector) (push) Successful in 5m2s
Test / Flake checks (push) Successful in 1m26s
These provide justification for each use of linkname. Poorly thought out uses of linkname are removed.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-13 07:14:16 +09:00
cat bf5d10743f treewide: import internal/system
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m18s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m12s
Test / Sandbox (race detector) (push) Successful in 4m16s
Test / Hakurei (race detector) (push) Successful in 5m8s
Test / Flake checks (push) Successful in 1m31s
For #24.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-13 01:22:47 +09:00
cat 4e7aab07d5 internal/system: relocate from system
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m18s
Test / Hakurei (push) Successful in 3m17s
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hpkg (push) Successful in 4m13s
Test / Hakurei (race detector) (push) Successful in 5m3s
Test / Flake checks (push) Successful in 1m40s
These packages are highly specific to hakurei and are difficult to use safely from other pieces of code.

Their exported symbols are made available until v0.4.0 where they will be removed for #24.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-13 01:17:47 +09:00
cat 15a66a2b31 treewide: import internal/helper
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m4s
Test / Sandbox (race detector) (push) Successful in 4m8s
Test / Hakurei (race detector) (push) Successful in 5m3s
Test / Flake checks (push) Successful in 1m27s
For #24.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-12 23:19:34 +09:00
cat f347d44c22 internal/helper: relocate from helper
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m26s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m8s
Test / Sandbox (race detector) (push) Successful in 4m16s
Test / Hakurei (race detector) (push) Successful in 5m5s
Test / Flake checks (push) Successful in 1m23s
This package is ugly and is pending removal only kept alive by xdg-dbus-proxy.

Its exported symbols are made available until v0.4.0 where it will be removed for #24.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-12 23:16:13 +09:00
cat b5630f6883 test: move package sandbox internal
Test / Create distribution (push) Successful in 33s
Test / Hakurei (push) Successful in 43s
Test / Hpkg (push) Successful in 40s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Sandbox (push) Successful in 1m56s
Test / Sandbox (race detector) (push) Successful in 2m39s
Test / Flake checks (push) Successful in 1m24s
This should never be used outside vm tests.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-12 23:03:22 +09:00
211 changed files with 9755 additions and 1338 deletions
+2
View File
@@ -0,0 +1,2 @@
ColumnLimit: 0
IndentWidth: 4
-1
View File
@@ -2,7 +2,6 @@ name: Test
on: on:
- push - push
- pull_request
jobs: jobs:
hakurei: hakurei:
+23 -9
View File
@@ -11,21 +11,25 @@ import (
"strconv" "strconv"
"sync" "sync"
"time" "time"
_ "unsafe" _ "unsafe" // for go:linkname
"hakurei.app/command" "hakurei.app/command"
"hakurei.app/container"
"hakurei.app/container/check" "hakurei.app/container/check"
"hakurei.app/container/fhs" "hakurei.app/container/fhs"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal" "hakurei.app/internal/dbus"
"hakurei.app/internal/env" "hakurei.app/internal/env"
"hakurei.app/internal/info"
"hakurei.app/internal/outcome" "hakurei.app/internal/outcome"
"hakurei.app/message" "hakurei.app/message"
"hakurei.app/system/dbus"
) )
// optionalErrorUnwrap calls [errors.Unwrap] and returns the resulting value
// if it is not nil, or the original value if it is.
//
//go:linkname optionalErrorUnwrap hakurei.app/container.optionalErrorUnwrap //go:linkname optionalErrorUnwrap hakurei.app/container.optionalErrorUnwrap
func optionalErrorUnwrap(_ error) error func optionalErrorUnwrap(err error) error
func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command { func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
var ( var (
@@ -88,7 +92,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
flagPrivateRuntime, flagPrivateTmpdir bool flagPrivateRuntime, flagPrivateTmpdir bool
flagWayland, flagX11, flagDBus, flagPulse bool flagWayland, flagX11, flagDBus, flagPipeWire, flagPulse bool
) )
c.NewCommand("run", "Configure and start a permissive container", func(args []string) error { c.NewCommand("run", "Configure and start a permissive container", func(args []string) error {
@@ -143,8 +147,8 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
if flagDBus { if flagDBus {
et |= hst.EDBus et |= hst.EDBus
} }
if flagPulse { if flagPipeWire || flagPulse {
et |= hst.EPulse et |= hst.EPipeWire
} }
config := &hst.Config{ config := &hst.Config{
@@ -183,6 +187,14 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
}}) }})
} }
// start pipewire-pulse: this most likely exists on host if PipeWire is available
if flagPulse {
config.Container.Filesystem = append(config.Container.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSDaemon{
Target: fhs.AbsRunUser.Append(strconv.Itoa(container.OverflowUid(msg)), "pulse/native"),
Exec: shell, Args: []string{"-lc", "exec pipewire-pulse"},
}})
}
config.Container.Filesystem = append(config.Container.Filesystem, config.Container.Filesystem = append(config.Container.Filesystem,
// opportunistically bind kvm // opportunistically bind kvm
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{ hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
@@ -294,8 +306,10 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
"Enable direct connection to X11"). "Enable direct connection to X11").
Flag(&flagDBus, "dbus", command.BoolFlag(false), Flag(&flagDBus, "dbus", command.BoolFlag(false),
"Enable proxied connection to D-Bus"). "Enable proxied connection to D-Bus").
Flag(&flagPipeWire, "pipewire", command.BoolFlag(false),
"Enable connection to PipeWire via SecurityContext").
Flag(&flagPulse, "pulse", command.BoolFlag(false), Flag(&flagPulse, "pulse", command.BoolFlag(false),
"Enable direct connection to PulseAudio") "Enable PulseAudio compatibility daemon")
} }
{ {
@@ -350,7 +364,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
}).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id") }).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id")
} }
c.Command("version", "Display version information", func(args []string) error { fmt.Println(internal.Version()); return errSuccess }) c.Command("version", "Display version information", func(args []string) error { fmt.Println(info.Version()); return errSuccess })
c.Command("license", "Show full license text", func(args []string) error { fmt.Println(license); return errSuccess }) c.Command("license", "Show full license text", func(args []string) error { fmt.Println(license); return errSuccess })
c.Command("template", "Produce a config template", func(args []string) error { encodeJSON(log.Fatal, os.Stdout, false, hst.Template()); return errSuccess }) c.Command("template", "Produce a config template", func(args []string) error { encodeJSON(log.Fatal, os.Stdout, false, hst.Template()); return errSuccess })
c.Command("help", "Show this help message", func([]string) error { c.PrintHelp(); return errSuccess }) c.Command("help", "Show this help message", func([]string) error { c.PrintHelp(); return errSuccess })
+4 -2
View File
@@ -36,7 +36,7 @@ Commands:
}, },
{ {
"run", []string{"run", "-h"}, ` "run", []string{"run", "-h"}, `
Usage: hakurei run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--private-runtime] [--private-tmpdir] [--wayland] [-X] [--dbus] [--pulse] COMMAND [OPTIONS] Usage: hakurei run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--private-runtime] [--private-tmpdir] [--wayland] [-X] [--dbus] [--pipewire] [--pulse] COMMAND [OPTIONS]
Flags: Flags:
-X Enable direct connection to X11 -X Enable direct connection to X11
@@ -58,12 +58,14 @@ Flags:
Reverse-DNS style Application identifier, leave empty to inherit instance identifier Reverse-DNS style Application identifier, leave empty to inherit instance identifier
-mpris -mpris
Allow owning MPRIS D-Bus path, has no effect if custom config is available Allow owning MPRIS D-Bus path, has no effect if custom config is available
-pipewire
Enable connection to PipeWire via SecurityContext
-private-runtime -private-runtime
Do not share XDG_RUNTIME_DIR between containers under the same identity Do not share XDG_RUNTIME_DIR between containers under the same identity
-private-tmpdir -private-tmpdir
Do not share TMPDIR between containers under the same identity Do not share TMPDIR between containers under the same identity
-pulse -pulse
Enable direct connection to PulseAudio Enable PulseAudio compatibility daemon
-u string -u string
Passwd user name within sandbox (default "chronos") Passwd user name within sandbox (default "chronos")
-wayland -wayland
+2 -10
View File
@@ -1,18 +1,13 @@
package main_test package main
import ( import (
"io"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
_ "unsafe"
"hakurei.app/container/stub" "hakurei.app/container/stub"
) )
//go:linkname decodeJSON hakurei.app/cmd/hakurei.decodeJSON
func decodeJSON(fatal func(v ...any), op string, r io.Reader, v any)
func TestDecodeJSON(t *testing.T) { func TestDecodeJSON(t *testing.T) {
t.Parallel() t.Parallel()
@@ -62,9 +57,6 @@ func TestDecodeJSON(t *testing.T) {
} }
} }
//go:linkname encodeJSON hakurei.app/cmd/hakurei.encodeJSON
func encodeJSON(fatal func(v ...any), output io.Writer, short bool, v any)
func TestEncodeJSON(t *testing.T) { func TestEncodeJSON(t *testing.T) {
t.Parallel() t.Parallel()
@@ -74,7 +66,7 @@ func TestEncodeJSON(t *testing.T) {
want string want string
}{ }{
{"marshaler", errorJSONMarshaler{}, {"marshaler", errorJSONMarshaler{},
`cannot encode json for main_test.errorJSONMarshaler: unique error 3735928559 injected by the test suite`}, `cannot encode json for main.errorJSONMarshaler: unique error 3735928559 injected by the test suite`},
{"default", func() {}, {"default", func() {},
`cannot write json: json: unsupported type: func()`}, `cannot write json: json: unsupported type: func()`},
} }
+14 -18
View File
@@ -12,8 +12,6 @@ import (
"time" "time"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/env"
"hakurei.app/internal/outcome" "hakurei.app/internal/outcome"
"hakurei.app/internal/store" "hakurei.app/internal/store"
"hakurei.app/message" "hakurei.app/message"
@@ -23,21 +21,19 @@ import (
func printShowSystem(output io.Writer, short, flagJSON bool) { func printShowSystem(output io.Writer, short, flagJSON bool) {
t := newPrinter(output) t := newPrinter(output)
defer t.MustFlush() defer t.MustFlush()
hi := outcome.Info()
info := &hst.Info{Version: internal.Version(), User: new(outcome.Hsu).MustID(nil)}
env.CopyPaths().Copy(&info.Paths, info.User)
if flagJSON { if flagJSON {
encodeJSON(log.Fatal, output, short, info) encodeJSON(log.Fatal, output, short, hi)
return return
} }
t.Printf("Version:\t%s\n", info.Version) t.Printf("Version:\t%s (libwayland %s)\n", hi.Version, hi.WaylandVersion)
t.Printf("User:\t%d\n", info.User) t.Printf("User:\t%d\n", hi.User)
t.Printf("TempDir:\t%s\n", info.TempDir) t.Printf("TempDir:\t%s\n", hi.TempDir)
t.Printf("SharePath:\t%s\n", info.SharePath) t.Printf("SharePath:\t%s\n", hi.SharePath)
t.Printf("RuntimePath:\t%s\n", info.RuntimePath) t.Printf("RuntimePath:\t%s\n", hi.RuntimePath)
t.Printf("RunDirPath:\t%s\n", info.RunDirPath) t.Printf("RunDirPath:\t%s\n", hi.RunDirPath)
} }
// printShowInstance writes a representation of [hst.State] or [hst.Config] to output. // printShowInstance writes a representation of [hst.State] or [hst.Config] to output.
@@ -90,12 +86,6 @@ func printShowInstance(
t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", ")) t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", "))
} }
if config.Container != nil { if config.Container != nil {
if config.Container.Home != nil {
t.Printf(" Home:\t%s\n", config.Container.Home)
}
if config.Container.Hostname != "" {
t.Printf(" Hostname:\t%s\n", config.Container.Hostname)
}
flags := config.Container.Flags.String() flags := config.Container.Flags.String()
// this is included in the upper hst.Config struct but is relevant here // this is included in the upper hst.Config struct but is relevant here
@@ -110,6 +100,12 @@ func printShowInstance(
} }
t.Printf(" Flags:\t%s\n", flags) t.Printf(" Flags:\t%s\n", flags)
if config.Container.Home != nil {
t.Printf(" Home:\t%s\n", config.Container.Home)
}
if config.Container.Hostname != "" {
t.Printf(" Hostname:\t%s\n", config.Container.Hostname)
}
if config.Container.Path != nil { if config.Container.Path != nil {
t.Printf(" Path:\t%s\n", config.Container.Path) t.Printf(" Path:\t%s\n", config.Container.Path)
} }
+9 -9
View File
@@ -32,7 +32,7 @@ var (
PID: 0xbeef, PID: 0xbeef,
ShimPID: 0xcafe, ShimPID: 0xcafe,
Config: &hst.Config{ Config: &hst.Config{
Enablements: hst.NewEnablements(hst.EWayland | hst.EPulse), Enablements: hst.NewEnablements(hst.EWayland | hst.EPipeWire),
Identity: 1, Identity: 1,
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Shell: check.MustAbs("/bin/sh"), Shell: check.MustAbs("/bin/sh"),
@@ -62,11 +62,11 @@ func TestPrintShowInstance(t *testing.T) {
{"nil", nil, nil, false, false, "Error: invalid configuration!\n\n", false}, {"nil", nil, nil, false, false, "Error: invalid configuration!\n\n", false},
{"config", nil, hst.Template(), false, false, `App {"config", nil, hst.Template(), false, false, `App
Identity: 9 (org.chromium.Chromium) Identity: 9 (org.chromium.Chromium)
Enablements: wayland, dbus, pulseaudio Enablements: wayland, dbus, pipewire
Groups: video, dialout, plugdev Groups: video, dialout, plugdev
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir
Home: /data/data/org.chromium.Chromium Home: /data/data/org.chromium.Chromium
Hostname: localhost Hostname: localhost
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir
Path: /run/current-system/sw/bin/chromium Path: /run/current-system/sw/bin/chromium
Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
@@ -159,11 +159,11 @@ Session bus
App App
Identity: 9 (org.chromium.Chromium) Identity: 9 (org.chromium.Chromium)
Enablements: wayland, dbus, pulseaudio Enablements: wayland, dbus, pipewire
Groups: video, dialout, plugdev Groups: video, dialout, plugdev
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir
Home: /data/data/org.chromium.Chromium Home: /data/data/org.chromium.Chromium
Hostname: localhost Hostname: localhost
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir
Path: /run/current-system/sw/bin/chromium Path: /run/current-system/sw/bin/chromium
Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
@@ -215,7 +215,7 @@ App
"enablements": { "enablements": {
"wayland": true, "wayland": true,
"dbus": true, "dbus": true,
"pulse": true "pipewire": true
}, },
"session_bus": { "session_bus": {
"see": null, "see": null,
@@ -366,7 +366,7 @@ App
"enablements": { "enablements": {
"wayland": true, "wayland": true,
"dbus": true, "dbus": true,
"pulse": true "pipewire": true
}, },
"session_bus": { "session_bus": {
"see": null, "see": null,
@@ -564,7 +564,7 @@ func TestPrintPs(t *testing.T) {
"enablements": { "enablements": {
"wayland": true, "wayland": true,
"dbus": true, "dbus": true,
"pulse": true "pipewire": true
}, },
"session_bus": { "session_bus": {
"see": null, "see": null,
@@ -715,7 +715,7 @@ func TestPrintPs(t *testing.T) {
"shim_pid": 51966, "shim_pid": 51966,
"enablements": { "enablements": {
"wayland": true, "wayland": true,
"pulse": true "pipewire": true
}, },
"identity": 1, "identity": 1,
"groups": null, "groups": null,
+2 -2
View File
@@ -45,7 +45,7 @@
allow_wayland ? true, allow_wayland ? true,
allow_x11 ? false, allow_x11 ? false,
allow_dbus ? true, allow_dbus ? true,
allow_pulse ? true, allow_audio ? true,
gpu ? allow_wayland || allow_x11, gpu ? allow_wayland || allow_x11,
}: }:
@@ -175,7 +175,7 @@ let
wayland = allow_wayland; wayland = allow_wayland;
x11 = allow_x11; x11 = allow_x11;
dbus = allow_dbus; dbus = allow_dbus;
pulse = allow_pulse; pipewire = allow_audio;
}; };
mesa = if gpu then mesaWrappers else null; mesa = if gpu then mesaWrappers else null;
+2 -2
View File
@@ -10,11 +10,11 @@ import (
"os/exec" "os/exec"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal" "hakurei.app/internal/info"
"hakurei.app/message" "hakurei.app/message"
) )
var hakureiPathVal = internal.MustHakureiPath().String() var hakureiPathVal = info.MustHakureiPath().String()
func mustRunApp(ctx context.Context, msg message.Msg, config *hst.Config, beforeFail func()) { func mustRunApp(ctx context.Context, msg message.Msg, config *hst.Config, beforeFail func()) {
var ( var (
+4 -4
View File
@@ -90,13 +90,13 @@ wait_for_window("hakurei@machine-foot")
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n") machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
machine.wait_for_file("/tmp/hakurei.0/tmpdir/2/success-client") machine.wait_for_file("/tmp/hakurei.0/tmpdir/2/success-client")
collect_state_ui("app_wayland") collect_state_ui("app_wayland")
check_state("foot", {"wayland": True, "dbus": True, "pulse": True}) check_state("foot", {"wayland": True, "dbus": True, "pipewire": True})
# Verify acl on XDG_RUNTIME_DIR: # Verify acl on XDG_RUNTIME_DIR:
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 10002")) print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10002"))
machine.send_chars("exit\n") machine.send_chars("exit\n")
machine.wait_until_fails("pgrep foot") machine.wait_until_fails("pgrep foot")
# Verify acl cleanup on XDG_RUNTIME_DIR: # Verify acl cleanup on XDG_RUNTIME_DIR:
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 10002") machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10002")
# Exit Sway and verify process exit status 0: # Exit Sway and verify process exit status 0:
swaymsg("exit", succeed=False) swaymsg("exit", succeed=False)
@@ -107,4 +107,4 @@ print(machine.succeed("find /tmp/hakurei.0 "
+ "-path '/tmp/hakurei.0/runtime/*/*' -prune -o " + "-path '/tmp/hakurei.0/runtime/*/*' -prune -o "
+ "-path '/tmp/hakurei.0/tmpdir/*/*' -prune -o " + "-path '/tmp/hakurei.0/tmpdir/*/*' -prune -o "
+ "-print")) + "-print"))
print(machine.succeed("find /run/user/1000/hakurei")) print(machine.fail("ls /run/user/1000/hakurei"))
+1
View File
@@ -59,6 +59,7 @@ func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
return nil return nil
} }
func (e *AutoEtcOp) late(*setupState, syscallDispatcher) error { return nil }
func (e *AutoEtcOp) hostPath() *check.Absolute { return fhs.AbsEtc.Append(e.hostRel()) } func (e *AutoEtcOp) hostPath() *check.Absolute { return fhs.AbsEtc.Append(e.hostRel()) }
func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix } func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix }
+1
View File
@@ -69,6 +69,7 @@ func (r *AutoRootOp) apply(state *setupState, k syscallDispatcher) error {
} }
return nil return nil
} }
func (r *AutoRootOp) late(*setupState, syscallDispatcher) error { return nil }
func (r *AutoRootOp) Is(op Op) bool { func (r *AutoRootOp) Is(op Op) bool {
vr, ok := op.(*AutoRootOp) vr, ok := op.(*AutoRootOp)
+1 -1
View File
@@ -56,7 +56,7 @@ func NewAbs(pathname string) (*Absolute, error) {
// MustAbs calls [NewAbs] and panics on error. // MustAbs calls [NewAbs] and panics on error.
func MustAbs(pathname string) *Absolute { func MustAbs(pathname string) *Absolute {
if a, err := NewAbs(pathname); err != nil { if a, err := NewAbs(pathname); err != nil {
panic(err.Error()) panic(err)
} else { } else {
return a return a
} }
+5 -3
View File
@@ -14,8 +14,10 @@ import (
. "hakurei.app/container/check" . "hakurei.app/container/check"
) )
// unsafeAbs returns check.Absolute on any string value.
//
//go:linkname unsafeAbs hakurei.app/container/check.unsafeAbs //go:linkname unsafeAbs hakurei.app/container/check.unsafeAbs
func unsafeAbs(_ string) *Absolute func unsafeAbs(pathname string) *Absolute
func TestAbsoluteError(t *testing.T) { func TestAbsoluteError(t *testing.T) {
t.Parallel() t.Parallel()
@@ -82,9 +84,9 @@ func TestNewAbs(t *testing.T) {
t.Parallel() t.Parallel()
defer func() { defer func() {
wantPanic := `path "etc" is not absolute` wantPanic := &AbsoluteError{Pathname: "etc"}
if r := recover(); r != wantPanic { if r := recover(); !reflect.DeepEqual(r, wantPanic) {
t.Errorf("MustAbs: panic = %v; want %v", r, wantPanic) t.Errorf("MustAbs: panic = %v; want %v", r, wantPanic)
} }
}() }()
+44 -2
View File
@@ -21,6 +21,7 @@ import (
"hakurei.app/command" "hakurei.app/command"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/check" "hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/container/std" "hakurei.app/container/std"
"hakurei.app/container/vfs" "hakurei.app/container/vfs"
@@ -29,6 +30,45 @@ import (
"hakurei.app/message" "hakurei.app/message"
) )
// Note: this package requires cgo, which is unavailable in the Go playground.
func Example() {
// Must be called early if the current process starts containers.
container.TryArgv0(nil)
// Configure the container.
z := container.New(context.Background(), nil)
z.Hostname = "hakurei-example"
z.Proc(fhs.AbsProc).Dev(fhs.AbsDev, true)
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
// Bind / for demonstration.
z.Bind(fhs.AbsRoot, fhs.AbsRoot, 0)
if name, err := exec.LookPath("hostname"); err != nil {
panic(err)
} else {
z.Path = check.MustAbs(name)
}
// This completes the first stage of container setup and starts the container init process.
// The new process blocks until the Serve method is called.
if err := z.Start(); err != nil {
panic(err)
}
// This serves the setup payload to the container init process,
// starting the second stage of container setup.
if err := z.Serve(); err != nil {
panic(err)
}
// Must be called if the Start method succeeds.
if err := z.Wait(); err != nil {
panic(err)
}
// Output: hakurei-example
}
func TestStartError(t *testing.T) { func TestStartError(t *testing.T) {
t.Parallel() t.Parallel()
@@ -722,12 +762,14 @@ func TestMain(m *testing.M) {
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*check.Absolute, args ...string) (c *container.Container) { func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*check.Absolute, args ...string) (c *container.Container) {
msg := message.New(nil) msg := message.New(nil)
msg.SwapVerbose(testing.Verbose()) msg.SwapVerbose(testing.Verbose())
executable := check.MustAbs(container.MustExecutable(msg))
c = container.NewCommand(ctx, msg, absHelperInnerPath, "helper", args...) c = container.NewCommand(ctx, msg, absHelperInnerPath, "helper", args...)
c.Env = append(c.Env, envDoCheck+"=1") c.Env = append(c.Env, envDoCheck+"=1")
c.Bind(check.MustAbs(os.Args[0]), absHelperInnerPath, 0) c.Bind(executable, absHelperInnerPath, 0)
// in case test has cgo enabled // in case test has cgo enabled
if entries, err := ldd.Exec(ctx, msg, os.Args[0]); err != nil { if entries, err := ldd.Resolve(ctx, msg, executable); err != nil {
log.Fatalf("ldd: %v", err) log.Fatalf("ldd: %v", err)
} else { } else {
*libPaths = ldd.Path(entries) *libPaths = ldd.Path(entries)
+2 -1
View File
@@ -759,7 +759,8 @@ func (k *kstub) checkMsg(msg message.Msg) {
} }
func (k *kstub) GetLogger() *log.Logger { panic("unreachable") } func (k *kstub) GetLogger() *log.Logger { panic("unreachable") }
func (k *kstub) IsVerbose() bool { panic("unreachable") }
func (k *kstub) IsVerbose() bool { k.Helper(); return k.Expects("isVerbose").Ret.(bool) }
func (k *kstub) SwapVerbose(verbose bool) bool { func (k *kstub) SwapVerbose(verbose bool) bool {
k.Helper() k.Helper()
+20 -15
View File
@@ -7,31 +7,36 @@ import (
"hakurei.app/container/check" "hakurei.app/container/check"
"hakurei.app/container/vfs" "hakurei.app/container/vfs"
"hakurei.app/message"
) )
// messageFromError returns a printable error message for a supported concrete type. // messageFromError returns a printable error message for a supported concrete type.
func messageFromError(err error) (string, bool) { func messageFromError(err error) (m string, ok bool) {
if m, ok := messagePrefixP[MountError]("cannot ", err); ok { if m, ok = messagePrefixP[MountError]("cannot ", err); ok {
return m, ok return
} }
if m, ok := messagePrefixP[os.PathError]("cannot ", err); ok { if m, ok = messagePrefixP[os.PathError]("cannot ", err); ok {
return m, ok return
} }
if m, ok := messagePrefixP[check.AbsoluteError]("", err); ok { if m, ok = messagePrefixP[check.AbsoluteError](zeroString, err); ok {
return m, ok return
} }
if m, ok := messagePrefix[OpRepeatError]("", err); ok { if m, ok = messagePrefix[OpRepeatError](zeroString, err); ok {
return m, ok return
} }
if m, ok := messagePrefix[OpStateError]("", err); ok { if m, ok = messagePrefix[OpStateError](zeroString, err); ok {
return m, ok return
} }
if m, ok := messagePrefixP[vfs.DecoderError]("cannot ", err); ok { if m, ok = messagePrefixP[vfs.DecoderError]("cannot ", err); ok {
return m, ok return
} }
if m, ok := messagePrefix[TmpfsSizeError]("", err); ok { if m, ok = messagePrefix[TmpfsSizeError](zeroString, err); ok {
return m, ok return
}
if m, ok = message.GetMessage(err); ok {
return
} }
return zeroString, false return zeroString, false
+5 -1
View File
@@ -8,8 +8,10 @@ import (
/* constants in this file bypass abs check, be extremely careful when changing them! */ /* constants in this file bypass abs check, be extremely careful when changing them! */
// unsafeAbs returns check.Absolute on any string value.
//
//go:linkname unsafeAbs hakurei.app/container/check.unsafeAbs //go:linkname unsafeAbs hakurei.app/container/check.unsafeAbs
func unsafeAbs(_ string) *check.Absolute func unsafeAbs(pathname string) *check.Absolute
var ( var (
// AbsRoot is [Root] as [check.Absolute]. // AbsRoot is [Root] as [check.Absolute].
@@ -34,6 +36,8 @@ var (
// AbsDev is [Dev] as [check.Absolute]. // AbsDev is [Dev] as [check.Absolute].
AbsDev = unsafeAbs(Dev) AbsDev = unsafeAbs(Dev)
// AbsDevShm is [DevShm] as [check.Absolute].
AbsDevShm = unsafeAbs(DevShm)
// AbsProc is [Proc] as [check.Absolute]. // AbsProc is [Proc] as [check.Absolute].
AbsProc = unsafeAbs(Proc) AbsProc = unsafeAbs(Proc)
// AbsSys is [Sys] as [check.Absolute]. // AbsSys is [Sys] as [check.Absolute].
+2
View File
@@ -29,6 +29,8 @@ const (
// Dev points to the root directory for device nodes. // Dev points to the root directory for device nodes.
Dev = "/dev/" Dev = "/dev/"
// DevShm is the place for POSIX shared memory segments, as created via shm_open(3).
DevShm = "/dev/shm/"
// Proc points to a virtual kernel file system exposing the process list and other functionality. // Proc points to a virtual kernel file system exposing the process list and other functionality.
Proc = "/proc/" Proc = "/proc/"
// ProcSys points to a hierarchy below /proc/ that exposes a number of kernel tunables. // ProcSys points to a hierarchy below /proc/ that exposes a number of kernel tunables.
+109 -32
View File
@@ -1,6 +1,7 @@
package container package container
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"log" "log"
@@ -9,6 +10,8 @@ import (
"path" "path"
"slices" "slices"
"strconv" "strconv"
"sync"
"sync/atomic"
. "syscall" . "syscall"
"time" "time"
@@ -18,24 +21,28 @@ import (
) )
const ( const (
/* intermediate tmpfs mount point /* intermediateHostPath is the pathname of the intermediate tmpfs mount point.
this path might seem like a weird choice, however there are many good reasons to use it: This path might seem like a weird choice, however there are many good reasons to use it:
- the contents of this path is never exposed to the container: - The contents of this path is never exposed to the container:
the tmpfs root established here effectively becomes anonymous after pivot_root The tmpfs root established here effectively becomes anonymous after pivot_root.
- it is safe to assume this path exists and is a directory: - It is safe to assume this path exists and is a directory:
this program will not work correctly without a proper /proc and neither will most others This program will not work correctly without a proper /proc and neither will most others.
- this path belongs to the container init: - This path belongs to the container init:
the container init is not any more privileged or trusted than the rest of the container The container init is not any more privileged or trusted than the rest of the container.
- this path is only accessible by init and root: - This path is only accessible by init and root:
the container init sets SUID_DUMP_DISABLE and terminates if that fails; The container init sets SUID_DUMP_DISABLE and terminates if that fails.
it should be noted that none of this should become relevant at any point since the resulting It should be noted that none of this should become relevant at any point since the resulting
intermediate root tmpfs should be effectively anonymous */ intermediate root tmpfs should be effectively anonymous. */
intermediateHostPath = fhs.Proc + "self/fd" intermediateHostPath = fhs.Proc + "self/fd"
// setup params file descriptor // setupEnv is the name of the environment variable holding the string representation of
// the read end file descriptor of the setup params pipe.
setupEnv = "HAKUREI_SETUP" setupEnv = "HAKUREI_SETUP"
// exitUnexpectedWait4 is the exit code if wait4 returns an unexpected errno.
exitUnexpectedWait4 = 2
) )
type ( type (
@@ -49,6 +56,8 @@ type (
early(state *setupState, k syscallDispatcher) error early(state *setupState, k syscallDispatcher) error
// apply is called in intermediate root. // apply is called in intermediate root.
apply(state *setupState, k syscallDispatcher) error apply(state *setupState, k syscallDispatcher) error
// late is called right before starting the initial process.
late(state *setupState, k syscallDispatcher) error
// prefix returns a log message prefix, and whether this Op prints no identifying message on its own. // prefix returns a log message prefix, and whether this Op prints no identifying message on its own.
prefix() (string, bool) prefix() (string, bool)
@@ -61,11 +70,29 @@ type (
// setupState persists context between Ops. // setupState persists context between Ops.
setupState struct { setupState struct {
nonrepeatable uintptr nonrepeatable uintptr
// Whether early reaping has concluded. Must only be accessed in the wait4 loop.
processConcluded bool
// Process to syscall.WaitStatus populated in the wait4 loop. Freed after early reaping concludes.
process map[int]WaitStatus
// Synchronises access to process.
processMu sync.RWMutex
*Params *Params
context.Context
message.Msg message.Msg
} }
) )
// terminated returns whether the specified pid has been reaped, and its
// syscall.WaitStatus if it had. This is only usable by [Op].
func (state *setupState) terminated(pid int) (wstatus WaitStatus, ok bool) {
state.processMu.RLock()
wstatus, ok = state.process[pid]
state.processMu.RUnlock()
return
}
// Grow grows the slice Ops points to using [slices.Grow]. // Grow grows the slice Ops points to using [slices.Grow].
func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) } func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) }
@@ -180,7 +207,9 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
k.fatalf(msg, "cannot make / rslave: %v", optionalErrorUnwrap(err)) k.fatalf(msg, "cannot make / rslave: %v", optionalErrorUnwrap(err))
} }
state := &setupState{Params: &params.Params, Msg: msg} ctx, cancel := context.WithCancel(context.Background())
state := &setupState{process: make(map[int]WaitStatus), Params: &params.Params, Msg: msg, Context: ctx}
defer cancel()
/* early is called right before pivot_root into intermediate root; /* early is called right before pivot_root into intermediate root;
this step is mostly for gathering information that would otherwise be difficult to obtain this step is mostly for gathering information that would otherwise be difficult to obtain
@@ -330,22 +359,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
} }
k.umask(oldmask) k.umask(oldmask)
if err := closeSetup(); err != nil { // winfo represents an exited process from wait4.
k.fatalf(msg, "cannot close setup pipe: %v", err)
}
cmd := exec.Command(params.Path.String())
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Args = params.Args
cmd.Env = params.Env
cmd.ExtraFiles = extraFiles
cmd.Dir = params.Dir.String()
msg.Verbosef("starting initial program %s", params.Path)
if err := k.start(cmd); err != nil {
k.fatalf(msg, "%v", err)
}
type winfo struct { type winfo struct {
wpid int wpid int
wstatus WaitStatus wstatus WaitStatus
@@ -355,9 +369,13 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
// when there are no longer any processes left to reap // when there are no longer any processes left to reap
info := make(chan winfo, 1) info := make(chan winfo, 1)
// whether initial process has started
var initialProcessStarted atomic.Bool
k.new(func(k syscallDispatcher) { k.new(func(k syscallDispatcher) {
k.lockOSThread() k.lockOSThread()
wait4:
var ( var (
err error err error
wpid = -2 wpid = -2
@@ -371,7 +389,21 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
} }
if wpid != -2 { if wpid != -2 {
info <- winfo{wpid, wstatus} if !state.processConcluded {
state.processMu.Lock()
if state.process == nil {
// early reaping has already concluded at this point
state.processConcluded = true
info <- winfo{wpid, wstatus}
} else {
// initial process has not yet been created, and the
// info channel is not yet being received from
state.process[wpid] = wstatus
}
state.processMu.Unlock()
} else {
info <- winfo{wpid, wstatus}
}
} }
err = EINTR err = EINTR
@@ -379,13 +411,55 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
wpid, err = k.wait4(-1, &wstatus, 0, nil) wpid, err = k.wait4(-1, &wstatus, 0, nil)
} }
} }
if !errors.Is(err, ECHILD) { if !errors.Is(err, ECHILD) {
k.printf(msg, "unexpected wait4 response: %v", err) k.printf(msg, "unexpected wait4 response: %v", err)
} else if !initialProcessStarted.Load() {
// initial process has not yet been reached and all daemons
// terminated or none were started in the first place
time.Sleep(500 * time.Microsecond)
goto wait4
} }
close(info) close(info)
}) })
// called right before startup of initial process, all state changes to the
// current process is prohibited during late
for i, op := range *params.Ops {
// ops already checked during early setup
if err := op.late(state, k); err != nil {
if m, ok := messageFromError(err); ok {
k.fatal(msg, m)
} else if errors.Is(err, context.DeadlineExceeded) {
k.fatalf(msg, "%s deadline exceeded", op.String())
} else {
k.fatalf(msg, "cannot complete op at index %d: %v", i, err)
}
}
}
// early reaping has concluded, this must happen before initial process is created
state.processMu.Lock()
state.process = nil
state.processMu.Unlock()
if err := closeSetup(); err != nil {
k.fatalf(msg, "cannot close setup pipe: %v", err)
}
cmd := exec.Command(params.Path.String())
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Args = params.Args
cmd.Env = params.Env
cmd.ExtraFiles = extraFiles
cmd.Dir = params.Dir.String()
msg.Verbosef("starting initial process %s", params.Path)
if err := k.start(cmd); err != nil {
k.fatalf(msg, "%v", err)
}
initialProcessStarted.Store(true)
// handle signals to dump withheld messages // handle signals to dump withheld messages
sig := make(chan os.Signal, 2) sig := make(chan os.Signal, 2)
k.notify(sig, CancelSignal, k.notify(sig, CancelSignal,
@@ -394,7 +468,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
// closed after residualProcessTimeout has elapsed after initial process death // closed after residualProcessTimeout has elapsed after initial process death
timeout := make(chan struct{}) timeout := make(chan struct{})
r := 2 r := exitUnexpectedWait4
for { for {
select { select {
case s := <-sig: case s := <-sig:
@@ -426,6 +500,9 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
} }
if w.wpid == cmd.Process.Pid { if w.wpid == cmd.Process.Pid {
// cancel Op context early
cancel()
// start timeout early // start timeout early
go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }() go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
+16 -7
View File
@@ -1983,11 +1983,20 @@ func TestInitEntrypoint(t *testing.T) {
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil), call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil), call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
call("umask", stub.ExpectArgs{022}, 0, nil), call("umask", stub.ExpectArgs{022}, 0, nil),
call("New", stub.ExpectArgs{}, nil, nil),
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(13)}}, nil, nil), call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(13)}}, nil, nil),
call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, 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"}, nil, stub.UniqueError(12)), call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, nil, stub.UniqueError(12)),
call("fatalf", stub.ExpectArgs{"%v", []any{stub.UniqueError(12)}}, nil, nil), call("fatalf", stub.ExpectArgs{"%v", []any{stub.UniqueError(12)}}, nil, nil),
}, },
/* wait4 */
Tracks: []stub.Expect{{Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, 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),
}}},
}, nil}, }, nil},
{"lowlastcap signaled cancel forward error", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ {"lowlastcap signaled cancel forward error", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
@@ -2062,10 +2071,10 @@ func TestInitEntrypoint(t *testing.T) {
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil), call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil), call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
call("umask", stub.ExpectArgs{022}, 0, nil), call("umask", stub.ExpectArgs{022}, 0, nil),
call("New", stub.ExpectArgs{}, nil, nil),
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(10)}}, nil, nil), 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("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("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
call("New", stub.ExpectArgs{}, nil, 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}}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{"forwarding context cancellation"}}, nil, nil), call("verbose", stub.ExpectArgs{[]any{"forwarding context cancellation"}}, nil, nil),
// magicWait4Signal as ret causes wait4 stub to unblock // magicWait4Signal as ret causes wait4 stub to unblock
@@ -2162,10 +2171,10 @@ func TestInitEntrypoint(t *testing.T) {
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil), call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil), call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
call("umask", stub.ExpectArgs{022}, 0, nil), call("umask", stub.ExpectArgs{022}, 0, nil),
call("New", stub.ExpectArgs{}, nil, nil),
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(7)}}, nil, nil), 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("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("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
call("New", stub.ExpectArgs{}, nil, 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}}, nil, nil),
call("verbosef", stub.ExpectArgs{"got %s, forwarding to initial process", []any{"quit"}}, nil, nil), call("verbosef", stub.ExpectArgs{"got %s, forwarding to initial process", []any{"quit"}}, nil, nil),
// magicWait4Signal as ret causes wait4 stub to unblock // magicWait4Signal as ret causes wait4 stub to unblock
@@ -2262,10 +2271,10 @@ func TestInitEntrypoint(t *testing.T) {
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil), call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil), call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
call("umask", stub.ExpectArgs{022}, 0, nil), call("umask", stub.ExpectArgs{022}, 0, nil),
call("New", stub.ExpectArgs{}, nil, nil),
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(7)}}, nil, nil), 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("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("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
call("New", stub.ExpectArgs{}, nil, 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}}, nil, nil),
call("verbosef", stub.ExpectArgs{"got %s", []any{"interrupt"}}, nil, nil), call("verbosef", stub.ExpectArgs{"got %s", []any{"interrupt"}}, nil, nil),
call("beforeExit", stub.ExpectArgs{}, nil, nil), call("beforeExit", stub.ExpectArgs{}, nil, nil),
@@ -2353,10 +2362,10 @@ func TestInitEntrypoint(t *testing.T) {
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil), call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil), call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
call("umask", stub.ExpectArgs{022}, 0, nil), call("umask", stub.ExpectArgs{022}, 0, nil),
call("New", stub.ExpectArgs{}, nil, nil),
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(5)}}, nil, nil), 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("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("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
call("New", stub.ExpectArgs{}, nil, 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}}, nil, 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), call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
@@ -2448,10 +2457,10 @@ func TestInitEntrypoint(t *testing.T) {
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil), call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil), call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
call("umask", stub.ExpectArgs{022}, 0, nil), call("umask", stub.ExpectArgs{022}, 0, nil),
call("New", stub.ExpectArgs{}, nil, nil),
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(3)}}, nil, nil), 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("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("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
call("New", stub.ExpectArgs{}, nil, 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}}, nil, 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), call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
@@ -2586,10 +2595,10 @@ func TestInitEntrypoint(t *testing.T) {
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil), call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil), call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
call("umask", stub.ExpectArgs{022}, 0, nil), call("umask", stub.ExpectArgs{022}, 0, nil),
call("New", stub.ExpectArgs{}, nil, nil),
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(1)}}, nil, nil), 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("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("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil),
call("New", stub.ExpectArgs{}, nil, 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}}, nil, 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), call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
@@ -2728,10 +2737,10 @@ func TestInitEntrypoint(t *testing.T) {
call("newFile", stub.ExpectArgs{uintptr(11), "extra file 1"}, (*os.File)(nil), nil), call("newFile", stub.ExpectArgs{uintptr(11), "extra file 1"}, (*os.File)(nil), nil),
call("newFile", stub.ExpectArgs{uintptr(12), "extra file 2"}, (*os.File)(nil), nil), call("newFile", stub.ExpectArgs{uintptr(12), "extra file 2"}, (*os.File)(nil), nil),
call("umask", stub.ExpectArgs{022}, 0, nil), call("umask", stub.ExpectArgs{022}, 0, nil),
call("New", stub.ExpectArgs{}, nil, nil),
call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(0)}}, nil, nil), 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("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("start", stub.ExpectArgs{"/bin/zsh", []string{"zsh", "-c", "exec vim"}, []string{"DISPLAY=:0"}, "/.hakurei"}, &os.Process{Pid: 0xcafe}, nil),
call("New", stub.ExpectArgs{}, nil, 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}}, nil, 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), call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil),
+1
View File
@@ -90,6 +90,7 @@ func (b *BindMountOp) apply(state *setupState, k syscallDispatcher) error {
} }
return k.bindMount(state, source, target, flags) return k.bindMount(state, source, target, flags)
} }
func (b *BindMountOp) late(*setupState, syscallDispatcher) error { return nil }
func (b *BindMountOp) Is(op Op) bool { func (b *BindMountOp) Is(op Op) bool {
vb, ok := op.(*BindMountOp) vb, ok := op.(*BindMountOp)
+134
View File
@@ -0,0 +1,134 @@
package container
import (
"context"
"encoding/gob"
"errors"
"fmt"
"os"
"os/exec"
"slices"
"strconv"
"syscall"
"time"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
)
func init() { gob.Register(new(DaemonOp)) }
const (
// daemonTimeout is the duration a [DaemonOp] is allowed to block before the
// [DaemonOp.Target] marker becomes available.
daemonTimeout = 5 * time.Second
)
// Daemon appends an [Op] that starts a daemon in the container and blocks until
// [DaemonOp.Target] appears.
func (f *Ops) Daemon(target, path *check.Absolute, args ...string) *Ops {
*f = append(*f, &DaemonOp{target, path, args})
return f
}
// DaemonOp starts a daemon in the container and blocks until Target appears.
type DaemonOp struct {
// Pathname indicating readiness of daemon.
Target *check.Absolute
// Absolute pathname passed to [exec.Cmd].
Path *check.Absolute
// Arguments (excl. first) passed to [exec.Cmd].
Args []string
}
// earlyTerminationError is returned by [DaemonOp] when a daemon terminates
// before [DaemonOp.Target] appears.
type earlyTerminationError struct {
// Returned by [DaemonOp.String].
op string
// Copied from wait4 loop.
wstatus syscall.WaitStatus
}
func (e *earlyTerminationError) Error() string {
res := ""
switch {
case e.wstatus.Exited():
res = "exit status " + strconv.Itoa(e.wstatus.ExitStatus())
case e.wstatus.Signaled():
res = "signal: " + e.wstatus.Signal().String()
case e.wstatus.Stopped():
res = "stop signal: " + e.wstatus.StopSignal().String()
if e.wstatus.StopSignal() == syscall.SIGTRAP && e.wstatus.TrapCause() != 0 {
res += " (trap " + strconv.Itoa(e.wstatus.TrapCause()) + ")"
}
case e.wstatus.Continued():
res = "continued"
}
if e.wstatus.CoreDump() {
res += " (core dumped)"
}
return res
}
func (e *earlyTerminationError) Message() string { return e.op + " " + e.Error() }
func (d *DaemonOp) Valid() bool { return d != nil && d.Target != nil && d.Path != nil }
func (d *DaemonOp) early(*setupState, syscallDispatcher) error { return nil }
func (d *DaemonOp) apply(*setupState, syscallDispatcher) error { return nil }
func (d *DaemonOp) late(state *setupState, k syscallDispatcher) error {
cmd := exec.CommandContext(state.Context, d.Path.String(), d.Args...)
cmd.Env = state.Env
cmd.Dir = fhs.Root
if state.IsVerbose() {
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
}
// WaitDelay: left unset because lifetime is bound by AdoptWaitDelay on cancellation
cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGTERM) }
state.Verbosef("starting %s", d.String())
if err := k.start(cmd); err != nil {
return err
}
deadline := time.Now().Add(daemonTimeout)
var wstatusErr error
for {
if _, err := k.stat(d.Target.String()); err != nil {
if !errors.Is(err, os.ErrNotExist) {
_ = k.signal(cmd, os.Kill)
return err
}
if time.Now().After(deadline) {
_ = k.signal(cmd, os.Kill)
return context.DeadlineExceeded
}
if wstatusErr != nil {
return wstatusErr
}
if wstatus, ok := state.terminated(cmd.Process.Pid); ok {
// check once again: process could have satisfied Target between stat and the lookup
wstatusErr = &earlyTerminationError{d.String(), wstatus}
continue
}
time.Sleep(500 * time.Microsecond)
continue
}
state.Verbosef("daemon process %d ready", cmd.Process.Pid)
return nil
}
}
func (d *DaemonOp) Is(op Op) bool {
vd, ok := op.(*DaemonOp)
return ok && d.Valid() && vd.Valid() &&
d.Target.Is(vd.Target) && d.Path.Is(vd.Path) &&
slices.Equal(d.Args, vd.Args)
}
func (*DaemonOp) prefix() (string, bool) { return zeroString, false }
func (d *DaemonOp) String() string { return fmt.Sprintf("daemon providing %q", d.Target) }
+127
View File
@@ -0,0 +1,127 @@
package container
import (
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
"hakurei.app/message"
)
func TestEarlyTerminationError(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
err error
want string
msg string
}{
{"exited", &earlyTerminationError{
`daemon providing "/run/user/1971/pulse/native"`, 127 << 8,
}, "exit status 127", `daemon providing "/run/user/1971/pulse/native" exit status 127`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.err.Error(); got != tc.want {
t.Errorf("Error: %q, want %q", got, tc.want)
}
if got := tc.err.(message.Error).Message(); got != tc.msg {
t.Errorf("Message: %s, want %s", got, tc.msg)
}
})
}
}
func TestDaemonOp(t *testing.T) {
t.Parallel()
checkSimple(t, "DaemonOp.late", []simpleTestCase{
{"success", func(k *kstub) error {
state := setupState{Params: &Params{Env: []string{"\x00"}}, Context: t.Context(), Msg: k}
return (&DaemonOp{
Target: check.MustAbs("/run/user/1971/pulse/native"),
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
Args: []string{"-v"},
}).late(&state, k)
}, stub.Expect{Calls: []stub.Call{
call("isVerbose", stub.ExpectArgs{}, true, nil),
call("verbosef", stub.ExpectArgs{"starting %s", []any{`daemon providing "/run/user/1971/pulse/native"`}}, nil, nil),
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/pipewire-pulse", []string{"/run/current-system/sw/bin/pipewire-pulse", "-v"}, []string{"\x00"}, "/"}, &os.Process{Pid: 0xcafe}, nil),
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), os.ErrNotExist),
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), os.ErrNotExist),
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), os.ErrNotExist),
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), nil),
call("verbosef", stub.ExpectArgs{"daemon process %d ready", []any{0xcafe}}, nil, nil),
}}, nil},
})
checkOpsValid(t, []opValidTestCase{
{"nil", (*DaemonOp)(nil), false},
{"zero", new(DaemonOp), false},
{"valid", &DaemonOp{
Target: check.MustAbs("/run/user/1971/pulse/native"),
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
Args: []string{"-v"},
}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"pipewire-pulse", new(Ops).Daemon(
check.MustAbs("/run/user/1971/pulse/native"),
check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"), "-v",
), Ops{
&DaemonOp{
Target: check.MustAbs("/run/user/1971/pulse/native"),
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
Args: []string{"-v"},
},
}},
})
checkOpIs(t, []opIsTestCase{
{"zero", new(DaemonOp), new(DaemonOp), false},
{"args differs", &DaemonOp{
Target: check.MustAbs("/run/user/1971/pulse/native"),
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
Args: []string{"-v"},
}, &DaemonOp{
Target: check.MustAbs("/run/user/1971/pulse/native"),
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
}, false},
{"path differs", &DaemonOp{
Target: check.MustAbs("/run/user/1971/pulse/native"),
Path: check.MustAbs("/run/current-system/sw/bin/pipewire"),
}, &DaemonOp{
Target: check.MustAbs("/run/user/1971/pulse/native"),
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
}, false},
{"target differs", &DaemonOp{
Target: check.MustAbs("/run/user/65534/pulse/native"),
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
}, &DaemonOp{
Target: check.MustAbs("/run/user/1971/pulse/native"),
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
}, false},
{"equals", &DaemonOp{
Target: check.MustAbs("/run/user/1971/pulse/native"),
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
}, &DaemonOp{
Target: check.MustAbs("/run/user/1971/pulse/native"),
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"pipewire-pulse", &DaemonOp{
Target: check.MustAbs("/run/user/1971/pulse/native"),
}, zeroString, `daemon providing "/run/user/1971/pulse/native"`},
})
}
+1
View File
@@ -126,6 +126,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
} }
return k.mountTmpfs(SourceTmpfs, devShmPath, MS_NOSUID|MS_NODEV, 0, 01777) return k.mountTmpfs(SourceTmpfs, devShmPath, MS_NOSUID|MS_NODEV, 0, 01777)
} }
func (d *MountDevOp) late(*setupState, syscallDispatcher) error { return nil }
func (d *MountDevOp) Is(op Op) bool { func (d *MountDevOp) Is(op Op) bool {
vd, ok := op.(*MountDevOp) vd, ok := op.(*MountDevOp)
+1
View File
@@ -27,6 +27,7 @@ func (m *MkdirOp) early(*setupState, syscallDispatcher) error { return nil }
func (m *MkdirOp) apply(_ *setupState, k syscallDispatcher) error { func (m *MkdirOp) apply(_ *setupState, k syscallDispatcher) error {
return k.mkdirAll(toSysroot(m.Path.String()), m.Perm) return k.mkdirAll(toSysroot(m.Path.String()), m.Perm)
} }
func (m *MkdirOp) late(*setupState, syscallDispatcher) error { return nil }
func (m *MkdirOp) Is(op Op) bool { func (m *MkdirOp) Is(op Op) bool {
vm, ok := op.(*MkdirOp) vm, ok := op.(*MkdirOp)
+2
View File
@@ -205,6 +205,8 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
return k.mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, check.SpecialOverlayOption)) return k.mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, check.SpecialOverlayOption))
} }
func (o *MountOverlayOp) late(*setupState, syscallDispatcher) error { return nil }
func (o *MountOverlayOp) Is(op Op) bool { func (o *MountOverlayOp) Is(op Op) bool {
vo, ok := op.(*MountOverlayOp) vo, ok := op.(*MountOverlayOp)
return ok && o.Valid() && vo.Valid() && return ok && o.Valid() && vo.Valid() &&
+1
View File
@@ -57,6 +57,7 @@ func (t *TmpfileOp) apply(state *setupState, k syscallDispatcher) error {
} }
return nil return nil
} }
func (t *TmpfileOp) late(*setupState, syscallDispatcher) error { return nil }
func (t *TmpfileOp) Is(op Op) bool { func (t *TmpfileOp) Is(op Op) bool {
vt, ok := op.(*TmpfileOp) vt, ok := op.(*TmpfileOp)
+1
View File
@@ -28,6 +28,7 @@ func (p *MountProcOp) apply(state *setupState, k syscallDispatcher) error {
} }
return k.mount(SourceProc, target, FstypeProc, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString) return k.mount(SourceProc, target, FstypeProc, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString)
} }
func (p *MountProcOp) late(*setupState, syscallDispatcher) error { return nil }
func (p *MountProcOp) Is(op Op) bool { func (p *MountProcOp) Is(op Op) bool {
vp, ok := op.(*MountProcOp) vp, ok := op.(*MountProcOp)
+1
View File
@@ -26,6 +26,7 @@ func (*RemountOp) early(*setupState, syscallDispatcher) error { return nil }
func (r *RemountOp) apply(state *setupState, k syscallDispatcher) error { func (r *RemountOp) apply(state *setupState, k syscallDispatcher) error {
return k.remount(state, toSysroot(r.Target.String()), r.Flags) return k.remount(state, toSysroot(r.Target.String()), r.Flags)
} }
func (r *RemountOp) late(*setupState, syscallDispatcher) error { return nil }
func (r *RemountOp) Is(op Op) bool { func (r *RemountOp) Is(op Op) bool {
vr, ok := op.(*RemountOp) vr, ok := op.(*RemountOp)
+2
View File
@@ -50,6 +50,8 @@ func (l *SymlinkOp) apply(state *setupState, k syscallDispatcher) error {
return k.symlink(l.LinkName, target) return k.symlink(l.LinkName, target)
} }
func (l *SymlinkOp) late(*setupState, syscallDispatcher) error { return nil }
func (l *SymlinkOp) Is(op Op) bool { func (l *SymlinkOp) Is(op Op) bool {
vl, ok := op.(*SymlinkOp) vl, ok := op.(*SymlinkOp)
return ok && l.Valid() && vl.Valid() && return ok && l.Valid() && vl.Valid() &&
+1
View File
@@ -48,6 +48,7 @@ func (t *MountTmpfsOp) apply(_ *setupState, k syscallDispatcher) error {
} }
return k.mountTmpfs(t.FSName, toSysroot(t.Path.String()), t.Flags, t.Size, t.Perm) return k.mountTmpfs(t.FSName, toSysroot(t.Path.String()), t.Flags, t.Size, t.Perm)
} }
func (t *MountTmpfsOp) late(*setupState, syscallDispatcher) error { return nil }
func (t *MountTmpfsOp) Is(op Op) bool { func (t *MountTmpfsOp) Is(op Op) bool {
vt, ok := op.(*MountTmpfsOp) vt, ok := op.(*MountTmpfsOp)
+112 -118
View File
@@ -9,136 +9,130 @@
#define LEN(arr) (sizeof(arr) / sizeof((arr)[0])) #define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
int32_t hakurei_scmp_make_filter(int *ret_p, uintptr_t allocate_p, int32_t hakurei_scmp_make_filter(
uint32_t arch, uint32_t multiarch, int *ret_p, uintptr_t allocate_p,
struct hakurei_syscall_rule *rules, uint32_t arch, uint32_t multiarch,
size_t rules_sz, hakurei_export_flag flags) { struct hakurei_syscall_rule *rules,
int i; size_t rules_sz, hakurei_export_flag flags) {
int last_allowed_family; int i;
int disallowed; int last_allowed_family;
struct hakurei_syscall_rule *rule; int disallowed;
void *buf; struct hakurei_syscall_rule *rule;
size_t len = 0; void *buf;
size_t len = 0;
int32_t res = 0; /* refer to resPrefix for message */ int32_t res = 0; /* refer to resPrefix for message */
/* Blocklist all but unix, inet, inet6 and netlink */ /* Blocklist all but unix, inet, inet6 and netlink */
struct { struct {
int family; int family;
hakurei_export_flag flags_mask; hakurei_export_flag flags_mask;
} socket_family_allowlist[] = { } socket_family_allowlist[] = {
/* NOTE: Keep in numerical order */ /* NOTE: Keep in numerical order */
{AF_UNSPEC, 0}, {AF_UNSPEC, 0},
{AF_LOCAL, 0}, {AF_LOCAL, 0},
{AF_INET, 0}, {AF_INET, 0},
{AF_INET6, 0}, {AF_INET6, 0},
{AF_NETLINK, 0}, {AF_NETLINK, 0},
{AF_CAN, HAKUREI_EXPORT_CAN}, {AF_CAN, HAKUREI_EXPORT_CAN},
{AF_BLUETOOTH, HAKUREI_EXPORT_BLUETOOTH}, {AF_BLUETOOTH, HAKUREI_EXPORT_BLUETOOTH},
}; };
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW); scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
if (ctx == NULL) { if (ctx == NULL) {
res = 1; res = 1;
goto out;
} else
errno = 0;
/* We only really need to handle arches on multiarch systems.
* If only one arch is supported the default is fine */
if (arch != 0) {
/* This *adds* the target arch, instead of replacing the
* native one. This is not ideal, because we'd like to only
* allow the target arch, but we can't really disallow the
* native arch at this point, because then bubblewrap
* couldn't continue running. */
*ret_p = seccomp_arch_add(ctx, arch);
if (*ret_p < 0 && *ret_p != -EEXIST) {
res = 2;
goto out;
}
if (flags & HAKUREI_EXPORT_MULTIARCH && multiarch != 0) {
*ret_p = seccomp_arch_add(ctx, multiarch);
if (*ret_p < 0 && *ret_p != -EEXIST) {
res = 3;
goto out; goto out;
} } else
} errno = 0;
}
for (i = 0; i < rules_sz; i++) { /* We only really need to handle arches on multiarch systems.
rule = &rules[i]; * If only one arch is supported the default is fine */
assert(rule->m_errno == EPERM || rule->m_errno == ENOSYS); if (arch != 0) {
/* This *adds* the target arch, instead of replacing the
* native one. This is not ideal, because we'd like to only
* allow the target arch, but we can't really disallow the
* native arch at this point, because then bubblewrap
* couldn't continue running. */
*ret_p = seccomp_arch_add(ctx, arch);
if (*ret_p < 0 && *ret_p != -EEXIST) {
res = 2;
goto out;
}
if (rule->arg) if (flags & HAKUREI_EXPORT_MULTIARCH && multiarch != 0) {
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno), *ret_p = seccomp_arch_add(ctx, multiarch);
rule->syscall, 1, *rule->arg); if (*ret_p < 0 && *ret_p != -EEXIST) {
else res = 3;
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno), goto out;
rule->syscall, 0); }
}
if (*ret_p == -EFAULT) {
res = 4;
goto out;
} else if (*ret_p < 0) {
res = 5;
goto out;
}
}
/* Socket filtering doesn't work on e.g. i386, so ignore failures here
* However, we need to user seccomp_rule_add_exact to avoid libseccomp doing
* something else: https://github.com/seccomp/libseccomp/issues/8 */
last_allowed_family = -1;
for (i = 0; i < LEN(socket_family_allowlist); i++) {
if (socket_family_allowlist[i].flags_mask != 0 &&
(socket_family_allowlist[i].flags_mask & flags) !=
socket_family_allowlist[i].flags_mask)
continue;
for (disallowed = last_allowed_family + 1;
disallowed < socket_family_allowlist[i].family; disallowed++) {
/* Blocklist the in-between valid families */
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT),
SCMP_SYS(socket), 1,
SCMP_A0(SCMP_CMP_EQ, disallowed));
}
last_allowed_family = socket_family_allowlist[i].family;
}
/* Blocklist the rest */
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1,
SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1));
if (allocate_p == 0) {
*ret_p = seccomp_load(ctx);
if (*ret_p != 0) {
res = 7;
goto out;
}
} else {
*ret_p = seccomp_export_bpf_mem(ctx, NULL, &len);
if (*ret_p != 0) {
res = 6;
goto out;
} }
buf = hakurei_scmp_allocate(allocate_p, len); for (i = 0; i < rules_sz; i++) {
if (buf == NULL) { rule = &rules[i];
res = 4; assert(rule->m_errno == EPERM || rule->m_errno == ENOSYS);
goto out;
if (rule->arg)
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno), rule->syscall, 1, *rule->arg);
else
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno), rule->syscall, 0);
if (*ret_p == -EFAULT) {
res = 4;
goto out;
} else if (*ret_p < 0) {
res = 5;
goto out;
}
} }
*ret_p = seccomp_export_bpf_mem(ctx, buf, &len); /* Socket filtering doesn't work on e.g. i386, so ignore failures here
if (*ret_p != 0) { * However, we need to user seccomp_rule_add_exact to avoid libseccomp doing
res = 6; * something else: https://github.com/seccomp/libseccomp/issues/8 */
goto out; last_allowed_family = -1;
for (i = 0; i < LEN(socket_family_allowlist); i++) {
if (socket_family_allowlist[i].flags_mask != 0 &&
(socket_family_allowlist[i].flags_mask & flags) != socket_family_allowlist[i].flags_mask)
continue;
for (disallowed = last_allowed_family + 1; disallowed < socket_family_allowlist[i].family; disallowed++) {
/* Blocklist the in-between valid families */
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_EQ, disallowed));
}
last_allowed_family = socket_family_allowlist[i].family;
}
/* Blocklist the rest */
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1));
if (allocate_p == 0) {
*ret_p = seccomp_load(ctx);
if (*ret_p != 0) {
res = 7;
goto out;
}
} else {
*ret_p = seccomp_export_bpf_mem(ctx, NULL, &len);
if (*ret_p != 0) {
res = 6;
goto out;
}
buf = hakurei_scmp_allocate(allocate_p, len);
if (buf == NULL) {
res = 4;
goto out;
}
*ret_p = seccomp_export_bpf_mem(ctx, buf, &len);
if (*ret_p != 0) {
res = 6;
goto out;
}
} }
}
out: out:
if (ctx) if (ctx)
seccomp_release(ctx); seccomp_release(ctx);
return res; return res;
} }
+12 -11
View File
@@ -1,25 +1,26 @@
#include <seccomp.h> #include <seccomp.h>
#include <stdint.h> #include <stdint.h>
#if (SCMP_VER_MAJOR < 2) || (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR < 5) || \ #if (SCMP_VER_MAJOR < 2) || (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR < 5) || \
(SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR == 5 && SCMP_VER_MICRO < 1) (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR == 5 && SCMP_VER_MICRO < 1)
#error This package requires libseccomp >= v2.5.1 #error This package requires libseccomp >= v2.5.1
#endif #endif
typedef enum { typedef enum {
HAKUREI_EXPORT_MULTIARCH = 1 << 0, HAKUREI_EXPORT_MULTIARCH = 1 << 0,
HAKUREI_EXPORT_CAN = 1 << 1, HAKUREI_EXPORT_CAN = 1 << 1,
HAKUREI_EXPORT_BLUETOOTH = 1 << 2, HAKUREI_EXPORT_BLUETOOTH = 1 << 2,
} hakurei_export_flag; } hakurei_export_flag;
struct hakurei_syscall_rule { struct hakurei_syscall_rule {
int syscall; int syscall;
int m_errno; int m_errno;
struct scmp_arg_cmp *arg; struct scmp_arg_cmp *arg;
}; };
extern void *hakurei_scmp_allocate(uintptr_t f, size_t len); extern void *hakurei_scmp_allocate(uintptr_t f, size_t len);
int32_t hakurei_scmp_make_filter(int *ret_p, uintptr_t allocate_p, int32_t hakurei_scmp_make_filter(
uint32_t arch, uint32_t multiarch, int *ret_p, uintptr_t allocate_p,
struct hakurei_syscall_rule *rules, uint32_t arch, uint32_t multiarch,
size_t rules_sz, hakurei_export_flag flags); struct hakurei_syscall_rule *rules,
size_t rules_sz, hakurei_export_flag flags);
+3 -1
View File
@@ -7,8 +7,10 @@ import (
"hakurei.app/container/stub" "hakurei.app/container/stub"
) )
// Made available here to check panic recovery behaviour.
//
//go:linkname handleExitNew hakurei.app/container/stub.handleExitNew //go:linkname handleExitNew hakurei.app/container/stub.handleExitNew
func handleExitNew(_ testing.TB) func handleExitNew(t testing.TB)
// overrideTFailNow overrides the Fail and FailNow method. // overrideTFailNow overrides the Fail and FailNow method.
type overrideTFailNow struct { type overrideTFailNow struct {
+2 -1
View File
@@ -17,7 +17,8 @@ _hakurei_run() {
'--wayland[Enable connection to Wayland via security-context-v1]' \ '--wayland[Enable connection to Wayland via security-context-v1]' \
'-X[Enable direct connection to X11]' \ '-X[Enable direct connection to X11]' \
'--dbus[Enable proxied connection to D-Bus]' \ '--dbus[Enable proxied connection to D-Bus]' \
'--pulse[Enable direct connection to PulseAudio]' \ '--pipewire[Enable connection to PipeWire via SecurityContext]' \
'--pulse[Enable PulseAudio compatibility daemon]' \
'--dbus-config[Path to session bus proxy config file]: :_files -g "*.json"' \ '--dbus-config[Path to session bus proxy config file]: :_files -g "*.json"' \
'--dbus-system[Path to system bus proxy config file]: :_files -g "*.json"' \ '--dbus-system[Path to system bus proxy config file]: :_files -g "*.json"' \
'--mpris[Allow owning MPRIS D-Bus path]' \ '--mpris[Allow owning MPRIS D-Bus path]' \
+3 -3
View File
@@ -10,9 +10,9 @@ cp -rv "dist/comp" "${out}"
go generate ./... go generate ./...
go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w -buildid= -extldflags '-static' go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w -buildid= -extldflags '-static'
-X hakurei.app/internal.buildVersion=${VERSION} -X hakurei.app/internal/info.buildVersion=${VERSION}
-X hakurei.app/internal.hakureiPath=/usr/bin/hakurei -X hakurei.app/internal/info.hakureiPath=/usr/bin/hakurei
-X hakurei.app/internal.hsuPath=/usr/bin/hsu -X hakurei.app/internal/info.hsuPath=/usr/bin/hsu
-X main.hakureiPath=/usr/bin/hakurei" ./... -X main.hakureiPath=/usr/bin/hakurei" ./...
rm -f "./${out}.tar.gz" && tar -C dist -czf "${out}.tar.gz" "${pname}" rm -f "./${out}.tar.gz" && tar -C dist -czf "${out}.tar.gz" "${pname}"
+6 -2
View File
@@ -114,7 +114,7 @@
inherit (pkgs) inherit (pkgs)
# passthru.buildInputs # passthru.buildInputs
go go
gcc clang
# nativeBuildInputs # nativeBuildInputs
pkg-config pkg-config
@@ -129,6 +129,10 @@
zstd zstd
gnutar gnutar
coreutils coreutils
# for check
util-linux
nettools
; ;
}; };
hsu = pkgs.callPackage ./cmd/hsu/package.nix { inherit (self.packages.${system}) hakurei; }; hsu = pkgs.callPackage ./cmd/hsu/package.nix { inherit (self.packages.${system}) hakurei; };
@@ -144,7 +148,7 @@
&& chmod -R +w . && chmod -R +w .
export HAKUREI_VERSION="v${hakurei.version}" export HAKUREI_VERSION="v${hakurei.version}"
./dist/release.sh && mkdir $out && cp -v "dist/hakurei-$HAKUREI_VERSION.tar.gz"* $out CC="clang -O3 -Werror" ./dist/release.sh && mkdir $out && cp -v "dist/hakurei-$HAKUREI_VERSION.tar.gz"* $out
''; '';
} }
); );
+73
View File
@@ -0,0 +1,73 @@
// Package helper exposes the internal/helper package.
//
// Deprecated: This package will be removed in 0.4.
package helper
import (
"context"
"io"
"os"
"os/exec"
"time"
_ "unsafe" // for go:linkname
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/internal/helper"
"hakurei.app/message"
)
//go:linkname WaitDelay hakurei.app/internal/helper.WaitDelay
var WaitDelay time.Duration
const (
// HakureiHelper is set to 1 when args fd is enabled and 0 otherwise.
HakureiHelper = helper.HakureiHelper
// HakureiStatus is set to 1 when stat fd is enabled and 0 otherwise.
HakureiStatus = helper.HakureiStatus
)
type Helper = helper.Helper
// NewCheckedArgs returns a checked null-terminated argument writer for a copy of args.
//
//go:linkname NewCheckedArgs hakurei.app/internal/helper.NewCheckedArgs
func NewCheckedArgs(args ...string) (wt io.WriterTo, err error)
// MustNewCheckedArgs returns a checked null-terminated argument writer for a copy of args.
// If s contains a NUL byte this function panics instead of returning an error.
//
//go:linkname MustNewCheckedArgs hakurei.app/internal/helper.MustNewCheckedArgs
func MustNewCheckedArgs(args ...string) io.WriterTo
// NewDirect initialises a new direct Helper instance with wt as the null-terminated argument writer.
// Function argF returns an array of arguments passed directly to the child process.
//
//go:linkname NewDirect hakurei.app/internal/helper.NewDirect
func NewDirect(
ctx context.Context,
name string,
wt io.WriterTo,
stat bool,
argF func(argsFd, statFd int) []string,
cmdF func(cmd *exec.Cmd),
extraFiles []*os.File,
) Helper
// New initialises a Helper instance with wt as the null-terminated argument writer.
//
//go:linkname New hakurei.app/internal/helper.New
func New(
ctx context.Context,
msg message.Msg,
pathname *check.Absolute, name string,
wt io.WriterTo,
stat bool,
argF func(argsFd, statFd int) []string,
cmdF func(z *container.Container),
extraFiles []*os.File,
) Helper
// InternalHelperStub is an internal function but exported because it is cross-package;
// it is part of the implementation of the helper stub.
func InternalHelperStub() { helper.InternalHelperStub() }
+63
View File
@@ -0,0 +1,63 @@
// Deprecated: This package will be removed in 0.4.
package proc
import (
"context"
"io"
"os"
"os/exec"
"time"
_ "unsafe" // for go:linkname
"hakurei.app/internal/helper/proc"
)
//go:linkname FulfillmentTimeout hakurei.app/internal/helper/proc.FulfillmentTimeout
var FulfillmentTimeout time.Duration
// A File is an extra file with deferred initialisation.
type File = proc.File
// ExtraFilesPre is a linked list storing addresses of [os.File].
type ExtraFilesPre = proc.ExtraFilesPre
// Fulfill calls the [File.Fulfill] method on all files, starts cmd and blocks until all fulfillment completes.
//
//go:linkname Fulfill hakurei.app/internal/helper/proc.Fulfill
func Fulfill(ctx context.Context,
v *[]*os.File, start func() error,
files []File, extraFiles *ExtraFilesPre,
) (err error)
// InitFile initialises f as part of the slice extraFiles points to,
// and returns its final fd value.
//
//go:linkname InitFile hakurei.app/internal/helper/proc.InitFile
func InitFile(f File, extraFiles *ExtraFilesPre) (fd uintptr)
// BaseFile implements the Init method of the File interface and provides indirect access to extra file state.
type BaseFile = proc.BaseFile
//go:linkname ExtraFile hakurei.app/internal/helper/proc.ExtraFile
func ExtraFile(cmd *exec.Cmd, f *os.File) (fd uintptr)
//go:linkname ExtraFileSlice hakurei.app/internal/helper/proc.ExtraFileSlice
func ExtraFileSlice(extraFiles *[]*os.File, f *os.File) (fd uintptr)
// NewWriterTo returns a [File] that receives content from wt on fulfillment.
//
//go:linkname NewWriterTo hakurei.app/internal/helper/proc.NewWriterTo
func NewWriterTo(wt io.WriterTo) File
// NewStat returns a [File] implementing the behaviour
// of the receiving end of xdg-dbus-proxy stat fd.
//
//go:linkname NewStat hakurei.app/internal/helper/proc.NewStat
func NewStat(s *io.Closer) File
var (
//go:linkname ErrStatFault hakurei.app/internal/helper/proc.ErrStatFault
ErrStatFault error
//go:linkname ErrStatRead hakurei.app/internal/helper/proc.ErrStatRead
ErrStatRead error
)
+19
View File
@@ -23,9 +23,20 @@ type Config struct {
// System D-Bus proxy configuration. // System D-Bus proxy configuration.
// If set to nil, system bus proxy is disabled. // If set to nil, system bus proxy is disabled.
SystemBus *BusConfig `json:"system_bus,omitempty"` SystemBus *BusConfig `json:"system_bus,omitempty"`
// Direct access to wayland socket, no attempt is made to attach security-context-v1 // Direct access to wayland socket, no attempt is made to attach security-context-v1
// and the bare socket is made available to the container. // and the bare socket is made available to the container.
//
// 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"` DirectWayland bool `json:"direct_wayland,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.
DirectPulse bool `json:"direct_pulse,omitempty"`
// Extra acl updates to perform before setuid. // Extra acl updates to perform before setuid.
ExtraPerms []ExtraPermConfig `json:"extra_perms,omitempty"` ExtraPerms []ExtraPermConfig `json:"extra_perms,omitempty"`
@@ -49,6 +60,9 @@ var (
// ErrEnviron is returned by [Config.Validate] if an environment variable name contains '=' or NUL. // ErrEnviron is returned by [Config.Validate] if an environment variable name contains '=' or NUL.
ErrEnviron = errors.New("invalid environment variable name") ErrEnviron = errors.New("invalid environment variable name")
// ErrInsecure is returned by [Config.Validate] if the configuration is considered insecure.
ErrInsecure = errors.New("configuration is insecure")
) )
// Validate checks [Config] and returns [AppError] if an invalid value is encountered. // Validate checks [Config] and returns [AppError] if an invalid value is encountered.
@@ -95,6 +109,11 @@ func (config *Config) Validate() error {
} }
} }
if et := config.Enablements.Unwrap(); !config.DirectPulse && et&EPulse != 0 {
return &AppError{Step: "validate configuration", Err: ErrInsecure,
Msg: "enablement PulseAudio is insecure and no longer supported"}
}
return nil return nil
} }
+6
View File
@@ -53,6 +53,12 @@ func TestConfigValidate(t *testing.T) {
Env: map[string]string{"TERM\x00": ""}, Env: map[string]string{"TERM\x00": ""},
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron, }}, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
Msg: `invalid environment variable "TERM\x00"`}}, Msg: `invalid environment variable "TERM\x00"`}},
{"insecure pulse", &hst.Config{Enablements: hst.NewEnablements(hst.EPulse), Container: &hst.ContainerConfig{
Home: fhs.AbsTmp,
Shell: fhs.AbsTmp,
Path: fhs.AbsTmp,
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
Msg: "enablement PulseAudio is insecure and no longer supported"}},
{"valid", &hst.Config{Container: &hst.ContainerConfig{ {"valid", &hst.Config{Container: &hst.ContainerConfig{
Home: fhs.AbsTmp, Home: fhs.AbsTmp,
Shell: fhs.AbsTmp, Shell: fhs.AbsTmp,
+17 -8
View File
@@ -17,6 +17,8 @@ const (
EX11 EX11
// EDBus enables the per-container xdg-dbus-proxy daemon. // EDBus enables the per-container xdg-dbus-proxy daemon.
EDBus EDBus
// EPipeWire exposes a pipewire pathname socket via SecurityContext.
EPipeWire
// EPulse copies the PulseAudio cookie to [hst.PrivateTmp] and exposes the PulseAudio socket. // EPulse copies the PulseAudio cookie to [hst.PrivateTmp] and exposes the PulseAudio socket.
EPulse EPulse
@@ -35,6 +37,8 @@ func (e Enablement) String() string {
return "x11" return "x11"
case EDBus: case EDBus:
return "dbus" return "dbus"
case EPipeWire:
return "pipewire"
case EPulse: case EPulse:
return "pulseaudio" return "pulseaudio"
default: default:
@@ -62,10 +66,11 @@ type Enablements Enablement
// enablementsJSON is the [json] representation of [Enablements]. // enablementsJSON is the [json] representation of [Enablements].
type enablementsJSON = struct { type enablementsJSON = struct {
Wayland bool `json:"wayland,omitempty"` Wayland bool `json:"wayland,omitempty"`
X11 bool `json:"x11,omitempty"` X11 bool `json:"x11,omitempty"`
DBus bool `json:"dbus,omitempty"` DBus bool `json:"dbus,omitempty"`
Pulse bool `json:"pulse,omitempty"` PipeWire bool `json:"pipewire,omitempty"`
Pulse bool `json:"pulse,omitempty"`
} }
// Unwrap returns the underlying [Enablement]. // Unwrap returns the underlying [Enablement].
@@ -81,10 +86,11 @@ func (e *Enablements) MarshalJSON() ([]byte, error) {
return nil, syscall.EINVAL return nil, syscall.EINVAL
} }
return json.Marshal(&enablementsJSON{ return json.Marshal(&enablementsJSON{
Wayland: Enablement(*e)&EWayland != 0, Wayland: Enablement(*e)&EWayland != 0,
X11: Enablement(*e)&EX11 != 0, X11: Enablement(*e)&EX11 != 0,
DBus: Enablement(*e)&EDBus != 0, DBus: Enablement(*e)&EDBus != 0,
Pulse: Enablement(*e)&EPulse != 0, PipeWire: Enablement(*e)&EPipeWire != 0,
Pulse: Enablement(*e)&EPulse != 0,
}) })
} }
@@ -108,6 +114,9 @@ func (e *Enablements) UnmarshalJSON(data []byte) error {
if v.DBus { if v.DBus {
ve |= EDBus ve |= EDBus
} }
if v.PipeWire {
ve |= EPipeWire
}
if v.Pulse { if v.Pulse {
ve |= EPulse ve |= EPulse
} }
+3 -1
View File
@@ -32,6 +32,7 @@ func TestEnablementString(t *testing.T) {
{hst.EWayland | hst.EDBus | hst.EPulse, "wayland, dbus, pulseaudio"}, {hst.EWayland | hst.EDBus | hst.EPulse, "wayland, dbus, pulseaudio"},
{hst.EX11 | hst.EDBus | hst.EPulse, "x11, dbus, pulseaudio"}, {hst.EX11 | hst.EDBus | hst.EPulse, "x11, dbus, pulseaudio"},
{hst.EWayland | hst.EX11 | hst.EDBus | hst.EPulse, "wayland, x11, dbus, pulseaudio"}, {hst.EWayland | hst.EX11 | hst.EDBus | hst.EPulse, "wayland, x11, dbus, pulseaudio"},
{hst.EM - 1, "wayland, x11, dbus, pipewire, pulseaudio"},
{1 << 5, "e20"}, {1 << 5, "e20"},
{1 << 6, "e40"}, {1 << 6, "e40"},
@@ -62,8 +63,9 @@ func TestEnablements(t *testing.T) {
{"wayland", hst.NewEnablements(hst.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`}, {"wayland", hst.NewEnablements(hst.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`},
{"x11", hst.NewEnablements(hst.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`}, {"x11", hst.NewEnablements(hst.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`},
{"dbus", hst.NewEnablements(hst.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`}, {"dbus", hst.NewEnablements(hst.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`},
{"pipewire", hst.NewEnablements(hst.EPipeWire), `{"pipewire":true}`, `{"value":{"pipewire":true},"magic":3236757504}`},
{"pulse", hst.NewEnablements(hst.EPulse), `{"pulse":true}`, `{"value":{"pulse":true},"magic":3236757504}`}, {"pulse", hst.NewEnablements(hst.EPulse), `{"pulse":true}`, `{"value":{"pulse":true},"magic":3236757504}`},
{"all", hst.NewEnablements(hst.EWayland | hst.EX11 | hst.EDBus | hst.EPulse), `{"wayland":true,"x11":true,"dbus":true,"pulse":true}`, `{"value":{"wayland":true,"x11":true,"dbus":true,"pulse":true},"magic":3236757504}`}, {"all", hst.NewEnablements(hst.EM - 1), `{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true}`, `{"value":{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true},"magic":3236757504}`},
} }
for _, tc := range testCases { for _, tc := range testCases {
+12
View File
@@ -45,6 +45,9 @@ type Ops interface {
Root(host *check.Absolute, flags int) Ops Root(host *check.Absolute, flags int) Ops
// Etc appends an op that expands host /etc into a toplevel symlink mirror with /etc semantics. // Etc appends an op that expands host /etc into a toplevel symlink mirror with /etc semantics.
Etc(host *check.Absolute, prefix string) Ops Etc(host *check.Absolute, prefix string) Ops
// Daemon appends an op that starts a daemon in the container and blocks until target appears.
Daemon(target, path *check.Absolute, args ...string) Ops
} }
// ApplyState holds the address of [Ops] and any relevant application state. // ApplyState holds the address of [Ops] and any relevant application state.
@@ -124,6 +127,12 @@ func (f *FilesystemConfigJSON) MarshalJSON() ([]byte, error) {
*FSLink *FSLink
}{fsType{FilesystemLink}, cv} }{fsType{FilesystemLink}, cv}
case *FSDaemon:
v = &struct {
fsType
*FSDaemon
}{fsType{FilesystemDaemon}, cv}
default: default:
return nil, FSImplError{f.FilesystemConfig} return nil, FSImplError{f.FilesystemConfig}
} }
@@ -152,6 +161,9 @@ func (f *FilesystemConfigJSON) UnmarshalJSON(data []byte) error {
case FilesystemLink: case FilesystemLink:
*f = FilesystemConfigJSON{new(FSLink)} *f = FilesystemConfigJSON{new(FSLink)}
case FilesystemDaemon:
*f = FilesystemConfigJSON{new(FSDaemon)}
default: default:
return FSTypeError(t.Type) return FSTypeError(t.Type)
} }
+14
View File
@@ -84,6 +84,16 @@ func TestFilesystemConfigJSON(t *testing.T) {
}, nil, }, nil,
`{"type":"link","dst":"/run/current-system","linkname":"/run/current-system","dereference":true}`, `{"type":"link","dst":"/run/current-system","linkname":"/run/current-system","dereference":true}`,
`{"fs":{"type":"link","dst":"/run/current-system","linkname":"/run/current-system","dereference":true},"magic":3236757504}`}, `{"fs":{"type":"link","dst":"/run/current-system","linkname":"/run/current-system","dereference":true},"magic":3236757504}`},
{"daemon", hst.FilesystemConfigJSON{
FilesystemConfig: &hst.FSDaemon{
Target: m("/run/user/1971/pulse/native"),
Exec: m("/run/current-system/sw/bin/pipewire-pulse"),
Args: []string{"-v"},
},
}, nil,
`{"type":"daemon","dst":"/run/user/1971/pulse/native","path":"/run/current-system/sw/bin/pipewire-pulse","args":["-v"]}`,
`{"fs":{"type":"daemon","dst":"/run/user/1971/pulse/native","path":"/run/current-system/sw/bin/pipewire-pulse","args":["-v"]},"magic":3236757504}`},
} }
for _, tc := range testCases { for _, tc := range testCases {
@@ -345,6 +355,10 @@ func (p opsAdapter) Etc(host *check.Absolute, prefix string) hst.Ops {
return opsAdapter{p.Ops.Etc(host, prefix)} return opsAdapter{p.Ops.Etc(host, prefix)}
} }
func (p opsAdapter) Daemon(target, path *check.Absolute, args ...string) hst.Ops {
return opsAdapter{p.Ops.Daemon(target, path, args...)}
}
func m(pathname string) *check.Absolute { return check.MustAbs(pathname) } func m(pathname string) *check.Absolute { return check.MustAbs(pathname) }
func ms(pathnames ...string) []*check.Absolute { func ms(pathnames ...string) []*check.Absolute {
as := make([]*check.Absolute, len(pathnames)) as := make([]*check.Absolute, len(pathnames))
+48
View File
@@ -0,0 +1,48 @@
package hst
import (
"encoding/gob"
"hakurei.app/container/check"
)
func init() { gob.Register(new(FSDaemon)) }
// FilesystemDaemon is the type string of a daemon.
const FilesystemDaemon = "daemon"
// FSDaemon represents a daemon to be started in the container.
type FSDaemon struct {
// Pathname indicating readiness of daemon.
Target *check.Absolute `json:"dst"`
// Absolute pathname to daemon executable file.
Exec *check.Absolute `json:"path"`
// Arguments (excl. first) passed to daemon.
Args []string `json:"args"`
}
func (d *FSDaemon) Valid() bool { return d != nil && d.Target != nil && d.Exec != nil }
func (d *FSDaemon) Path() *check.Absolute {
if !d.Valid() {
return nil
}
return d.Target
}
func (d *FSDaemon) Host() []*check.Absolute { return nil }
func (d *FSDaemon) Apply(z *ApplyState) {
if !d.Valid() {
return
}
z.Daemon(d.Target, d.Exec, d.Args...)
}
func (d *FSDaemon) String() string {
if !d.Valid() {
return "<invalid>"
}
return "daemon:" + d.Target.String()
}
+29
View File
@@ -0,0 +1,29 @@
package hst_test
import (
"testing"
"hakurei.app/container"
"hakurei.app/hst"
)
func TestFSDaemon(t *testing.T) {
t.Parallel()
checkFs(t, []fsTestCase{
{"nil", (*hst.FSDaemon)(nil), false, nil, nil, nil, "<invalid>"},
{"zero", new(hst.FSDaemon), false, nil, nil, nil, "<invalid>"},
{"pipewire-pulse", &hst.FSDaemon{
Target: m("/run/user/1971/pulse/native"),
Exec: m("/run/current-system/sw/bin/pipewire-pulse"),
Args: []string{"-v"},
}, true, container.Ops{
&container.DaemonOp{
Target: m("/run/user/1971/pulse/native"),
Path: m("/run/current-system/sw/bin/pipewire-pulse"),
Args: []string{"-v"},
},
}, m("/run/user/1971/pulse/native"), nil, `daemon:/run/user/1971/pulse/native`},
})
}
+4 -2
View File
@@ -54,6 +54,9 @@ type Paths struct {
// Info holds basic system information collected from the implementation. // Info holds basic system information collected from the implementation.
type Info struct { type Info struct {
// WaylandVersion is the libwayland value of WAYLAND_VERSION.
WaylandVersion string `json:"WAYLAND_VERSION"`
// Version is a hardcoded version string. // Version is a hardcoded version string.
Version string `json:"version"` Version string `json:"version"`
// User is the userid according to hsu. // User is the userid according to hsu.
@@ -67,7 +70,7 @@ func Template() *Config {
return &Config{ return &Config{
ID: "org.chromium.Chromium", ID: "org.chromium.Chromium",
Enablements: NewEnablements(EWayland | EDBus | EPulse), Enablements: NewEnablements(EWayland | EDBus | EPipeWire),
SessionBus: &BusConfig{ SessionBus: &BusConfig{
See: nil, See: nil,
@@ -89,7 +92,6 @@ func Template() *Config {
Log: false, Log: false,
Filter: true, Filter: true,
}, },
DirectWayland: false,
ExtraPerms: []ExtraPermConfig{ ExtraPerms: []ExtraPermConfig{
{Path: fhs.AbsVarLib.Append("hakurei/u0"), Ensure: true, Execute: true}, {Path: fhs.AbsVarLib.Append("hakurei/u0"), Ensure: true, Execute: true},
+1 -1
View File
@@ -105,7 +105,7 @@ func TestTemplate(t *testing.T) {
"enablements": { "enablements": {
"wayland": true, "wayland": true,
"dbus": true, "dbus": true,
"pulse": true "pipewire": true
}, },
"session_bus": { "session_bus": {
"see": null, "see": null,
+3 -1
View File
@@ -6,11 +6,13 @@ import (
"reflect" "reflect"
"testing" "testing"
"time" "time"
_ "unsafe" _ "unsafe" // for go:linkname
"hakurei.app/hst" "hakurei.app/hst"
) )
// Made available here to check time encoding behaviour of [hst.ID].
//
//go:linkname newInstanceID hakurei.app/hst.newInstanceID //go:linkname newInstanceID hakurei.app/hst.newInstanceID
func newInstanceID(id *hst.ID, p uint64) error func newInstanceID(id *hst.ID, p uint64) error
@@ -13,7 +13,7 @@ import (
"strconv" "strconv"
"testing" "testing"
"hakurei.app/system/acl" "hakurei.app/internal/acl"
) )
const testFileName = "acl.test" const testFileName = "acl.test"
+90
View File
@@ -0,0 +1,90 @@
#include "libacl-helper.h"
#include <acl/libacl.h>
#include <stdbool.h>
#include <stdlib.h>
#include <sys/acl.h>
int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid,
acl_perm_t *perms, size_t plen) {
int ret;
bool v;
int i;
acl_t acl;
acl_entry_t entry;
acl_tag_t tag_type;
void *qualifier_p;
acl_permset_t permset;
ret = -1; /* acl_get_file */
acl = acl_get_file(path_p, ACL_TYPE_ACCESS);
if (acl == NULL)
goto out;
/* prune entries by uid */
for (i = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); i == 1;
i = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) {
ret = -2; /* acl_get_tag_type */
if (acl_get_tag_type(entry, &tag_type) != 0)
goto out;
if (tag_type != ACL_USER)
continue;
ret = -3; /* acl_get_qualifier */
qualifier_p = acl_get_qualifier(entry);
if (qualifier_p == NULL)
goto out;
v = *(uid_t *)qualifier_p == uid;
acl_free(qualifier_p);
if (!v)
continue;
ret = -4; /* acl_delete_entry */
if (acl_delete_entry(acl, entry) != 0)
goto out;
}
if (plen == 0)
goto set;
ret = -5; /* acl_create_entry */
if (acl_create_entry(&acl, &entry) != 0)
goto out;
ret = -6; /* acl_get_permset */
if (acl_get_permset(entry, &permset) != 0)
goto out;
ret = -7; /* acl_add_perm */
for (i = 0; i < plen; i++) {
if (acl_add_perm(permset, perms[i]) != 0)
goto out;
}
ret = -8; /* acl_set_tag_type */
if (acl_set_tag_type(entry, ACL_USER) != 0)
goto out;
ret = -9; /* acl_set_qualifier */
if (acl_set_qualifier(entry, (void *)&uid) != 0)
goto out;
set:
ret = -10; /* acl_calc_mask */
if (acl_calc_mask(&acl) != 0)
goto out;
ret = -11; /* acl_valid */
if (acl_valid(acl) != 0)
goto out;
ret = -12; /* acl_set_file */
if (acl_set_file(path_p, ACL_TYPE_ACCESS, acl) == 0)
ret = 0;
out:
free((void *)path_p);
if (acl != NULL)
acl_free((void *)acl);
return ret;
}
@@ -3,7 +3,7 @@ package acl_test
import ( import (
"testing" "testing"
"hakurei.app/system/acl" "hakurei.app/internal/acl"
) )
func TestPerms(t *testing.T) { func TestPerms(t *testing.T) {
@@ -5,7 +5,7 @@ import (
"reflect" "reflect"
"testing" "testing"
"hakurei.app/system/dbus" "hakurei.app/internal/dbus"
) )
func TestParse(t *testing.T) { func TestParse(t *testing.T) {
@@ -7,7 +7,7 @@ import (
"testing" "testing"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/system/dbus" "hakurei.app/internal/dbus"
) )
func TestConfigArgs(t *testing.T) { func TestConfigArgs(t *testing.T) {
@@ -11,9 +11,9 @@ import (
"testing" "testing"
"time" "time"
"hakurei.app/helper" "hakurei.app/internal/dbus"
"hakurei.app/internal/helper"
"hakurei.app/message" "hakurei.app/message"
"hakurei.app/system/dbus"
) )
func TestFinalise(t *testing.T) { func TestFinalise(t *testing.T) {
@@ -12,7 +12,7 @@ import (
"hakurei.app/container/check" "hakurei.app/container/check"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/container/std" "hakurei.app/container/std"
"hakurei.app/helper" "hakurei.app/internal/helper"
"hakurei.app/ldd" "hakurei.app/ldd"
) )
@@ -54,7 +54,7 @@ func (p *Proxy) Start() error {
} }
var libPaths []*check.Absolute var libPaths []*check.Absolute
if entries, err := ldd.Exec(ctx, p.msg, toolPath.String()); err != nil { if entries, err := ldd.Resolve(ctx, p.msg, toolPath); err != nil {
return err return err
} else { } else {
libPaths = ldd.Path(entries) libPaths = ldd.Path(entries)
@@ -5,7 +5,7 @@ import (
"testing" "testing"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/helper" "hakurei.app/internal/helper"
) )
func TestMain(m *testing.M) { container.TryArgv0(nil); helper.InternalHelperStub(); os.Exit(m.Run()) } func TestMain(m *testing.M) { container.TryArgv0(nil); helper.InternalHelperStub(); os.Exit(m.Run()) }
@@ -6,8 +6,8 @@ import (
"sync" "sync"
"syscall" "syscall"
"hakurei.app/helper"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/helper"
"hakurei.app/message" "hakurei.app/message"
) )
@@ -7,7 +7,7 @@ import (
"syscall" "syscall"
"testing" "testing"
"hakurei.app/helper" "hakurei.app/internal/helper"
) )
func TestArgsString(t *testing.T) { func TestArgsString(t *testing.T) {
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"sync" "sync"
"syscall" "syscall"
"hakurei.app/helper/proc" "hakurei.app/internal/helper/proc"
) )
// NewDirect initialises a new direct Helper instance with wt as the null-terminated argument writer. // NewDirect initialises a new direct Helper instance with wt as the null-terminated argument writer.
@@ -9,7 +9,7 @@ import (
"testing" "testing"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/helper" "hakurei.app/internal/helper"
) )
func TestCmd(t *testing.T) { func TestCmd(t *testing.T) {
@@ -11,7 +11,7 @@ import (
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/check" "hakurei.app/container/check"
"hakurei.app/helper/proc" "hakurei.app/internal/helper/proc"
"hakurei.app/message" "hakurei.app/message"
) )
@@ -9,7 +9,7 @@ import (
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/check" "hakurei.app/container/check"
"hakurei.app/container/fhs" "hakurei.app/container/fhs"
"hakurei.app/helper" "hakurei.app/internal/helper"
) )
func TestContainer(t *testing.T) { func TestContainer(t *testing.T) {
@@ -8,7 +8,7 @@ import (
"os" "os"
"time" "time"
"hakurei.app/helper/proc" "hakurei.app/internal/helper/proc"
) )
var WaitDelay = 2 * time.Second var WaitDelay = 2 * time.Second
@@ -13,7 +13,7 @@ import (
"testing" "testing"
"time" "time"
"hakurei.app/helper" "hakurei.app/internal/helper"
) )
var ( var (
@@ -5,7 +5,7 @@ import (
"testing" "testing"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/helper" "hakurei.app/internal/helper"
) )
func TestMain(m *testing.M) { container.TryArgv0(nil); helper.InternalHelperStub(); os.Exit(m.Run()) } func TestMain(m *testing.M) { container.TryArgv0(nil); helper.InternalHelperStub(); os.Exit(m.Run()) }
+1 -1
View File
@@ -1,4 +1,4 @@
package internal package info
import ( import (
"log" "log"
@@ -1,4 +1,4 @@
package internal package info
import ( import (
"reflect" "reflect"
@@ -1,4 +1,4 @@
package internal package info
// FallbackVersion is returned when a version string was not set by the linker. // FallbackVersion is returned when a version string was not set by the linker.
const FallbackVersion = "dirty" const FallbackVersion = "dirty"
+3 -3
View File
@@ -14,9 +14,9 @@ import (
"hakurei.app/container/check" "hakurei.app/container/check"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/container/std" "hakurei.app/container/std"
"hakurei.app/internal" "hakurei.app/internal/dbus"
"hakurei.app/internal/info"
"hakurei.app/message" "hakurei.app/message"
"hakurei.app/system/dbus"
) )
// osFile represents [os.File]. // osFile represents [os.File].
@@ -156,7 +156,7 @@ func (direct) seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) erro
return seccomp.Load(rules, flags) return seccomp.Load(rules, flags)
} }
func (direct) mustHsuPath() *check.Absolute { return internal.MustHsuPath() } func (direct) mustHsuPath() *check.Absolute { return info.MustHsuPath() }
func (direct) dbusAddress() (session, system string) { return dbus.Address() } func (direct) dbusAddress() (session, system string) { return dbus.Address() }
+1 -1
View File
@@ -24,8 +24,8 @@ import (
"hakurei.app/container/std" "hakurei.app/container/std"
"hakurei.app/container/stub" "hakurei.app/container/stub"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/system"
"hakurei.app/message" "hakurei.app/message"
"hakurei.app/system"
) )
// call initialises a [stub.Call]. // call initialises a [stub.Call].
+1 -1
View File
@@ -8,8 +8,8 @@ import (
"os/user" "os/user"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/system"
"hakurei.app/message" "hakurei.app/message"
"hakurei.app/system"
) )
func newWithMessage(msg string) error { return newWithMessageError(msg, os.ErrInvalid) } func newWithMessage(msg string) error { return newWithMessageError(msg, os.ErrInvalid) }
+19 -3
View File
@@ -9,12 +9,24 @@ import (
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/check" "hakurei.app/container/check"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/acl"
"hakurei.app/internal/env" "hakurei.app/internal/env"
"hakurei.app/internal/info"
"hakurei.app/internal/system"
"hakurei.app/internal/wayland"
"hakurei.app/message" "hakurei.app/message"
"hakurei.app/system"
"hakurei.app/system/acl"
) )
// Info returns the address to a populated [hst.Info].
//
// This must not be called from within package outcome.
func Info() *hst.Info {
hi := hst.Info{WaylandVersion: wayland.Version,
Version: info.Version(), User: new(Hsu).MustID(nil)}
env.CopyPaths().Copy(&hi.Paths, hi.User)
return &hi
}
// envAllocSize is the initial size of the env map pre-allocated when the configured env map is nil. // envAllocSize is the initial size of the env map pre-allocated when the configured env map is nil.
// It should be large enough to fit all insertions by outcomeOp.toContainer. // It should be large enough to fit all insertions by outcomeOp.toContainer.
const envAllocSize = 1 << 6 const envAllocSize = 1 << 6
@@ -160,6 +172,8 @@ type outcomeStateSys struct {
// Copied from [hst.Config]. Safe for read by spWaylandOp.toSystem only. // Copied from [hst.Config]. Safe for read by spWaylandOp.toSystem only.
directWayland bool directWayland 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. // Copied header from [hst.Config]. Safe for read by spFilesystemOp.toSystem only.
extraPerms []hst.ExtraPermConfig extraPerms []hst.ExtraPermConfig
// Copied address from [hst.Config]. Safe for read by spDBusOp.toSystem only. // Copied address from [hst.Config]. Safe for read by spDBusOp.toSystem only.
@@ -173,7 +187,8 @@ type outcomeStateSys struct {
func (s *outcomeState) newSys(config *hst.Config, sys *system.I) *outcomeStateSys { func (s *outcomeState) newSys(config *hst.Config, sys *system.I) *outcomeStateSys {
return &outcomeStateSys{ return &outcomeStateSys{
appId: config.ID, et: config.Enablements.Unwrap(), appId: config.ID, et: config.Enablements.Unwrap(),
directWayland: config.DirectWayland, extraPerms: config.ExtraPerms, directWayland: config.DirectWayland, directPulse: config.DirectPulse,
extraPerms: config.ExtraPerms,
sessionBus: config.SessionBus, systemBus: config.SystemBus, sessionBus: config.SessionBus, systemBus: config.SystemBus,
sys: sys, outcomeState: s, sys: sys, outcomeState: s,
} }
@@ -280,6 +295,7 @@ func (state *outcomeStateSys) toSystem() error {
// optional via enablements // optional via enablements
&spWaylandOp{}, &spWaylandOp{},
&spX11Op{}, &spX11Op{},
spPipeWireOp{},
&spPulseOp{}, &spPulseOp{},
&spDBusOp{}, &spDBusOp{},
+3 -3
View File
@@ -16,10 +16,10 @@ import (
"hakurei.app/container/check" "hakurei.app/container/check"
"hakurei.app/container/fhs" "hakurei.app/container/fhs"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal" "hakurei.app/internal/info"
"hakurei.app/internal/store" "hakurei.app/internal/store"
"hakurei.app/internal/system"
"hakurei.app/message" "hakurei.app/message"
"hakurei.app/system"
) )
const ( const (
@@ -39,7 +39,7 @@ func (k *outcome) main(msg message.Msg, identifierFd int) {
} }
// read comp value early for early failure // read comp value early for early failure
hsuPath := internal.MustHsuPath() hsuPath := info.MustHsuPath()
const ( const (
// transitions to processCommit, or processFinal on failure // transitions to processCommit, or processFinal on failure
@@ -12,6 +12,8 @@ import (
// IsPollDescriptor reports whether fd is the descriptor being used by the poller. // IsPollDescriptor reports whether fd is the descriptor being used by the poller.
// //
// Made available here to determine and reject impossible fd.
//
//go:linkname IsPollDescriptor internal/poll.IsPollDescriptor //go:linkname IsPollDescriptor internal/poll.IsPollDescriptor
func IsPollDescriptor(fd uintptr) bool func IsPollDescriptor(fd uintptr) bool
@@ -21,13 +21,13 @@ import (
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/container/std" "hakurei.app/container/std"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/acl"
"hakurei.app/internal/dbus"
"hakurei.app/internal/system"
"hakurei.app/message" "hakurei.app/message"
"hakurei.app/system"
"hakurei.app/system/acl"
"hakurei.app/system/dbus"
) )
func TestOutcomeMain(t *testing.T) { func TestOutcomeRun(t *testing.T) {
t.Parallel() t.Parallel()
msg := message.New(nil) msg := message.New(nil)
msg.SwapVerbose(testing.Verbose()) msg.SwapVerbose(testing.Verbose())
@@ -67,18 +67,8 @@ func TestOutcomeMain(t *testing.T) {
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
). ).
// ensureRuntimeDir // spPipeWireOp
Ensure(m("/run/user/1971"), 0700). PipeWire(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pipewire")).
UpdatePermType(system.User, m("/run/user/1971"), acl.Execute).
Ensure(m("/run/user/1971/hakurei"), 0700).
UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
// runtime
Ephemeral(system.Process, m("/run/user/1971/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), 0700).
UpdatePerm(m("/run/user/1971/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), acl.Execute).
// spPulseOp
Link(m("/run/user/1971/pulse/native"), m("/run/user/1971/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pulse")).
// spDBusOp // spDBusOp
MustProxyDBus( MustProxyDBus(
@@ -106,8 +96,7 @@ func TestOutcomeMain(t *testing.T) {
"GOOGLE_DEFAULT_CLIENT_ID=77185425430.apps.googleusercontent.com", "GOOGLE_DEFAULT_CLIENT_ID=77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET=OTJgUOQcT7lO7GsGZq2G4IlT", "GOOGLE_DEFAULT_CLIENT_SECRET=OTJgUOQcT7lO7GsGZq2G4IlT",
"HOME=/data/data/org.chromium.Chromium", "HOME=/data/data/org.chromium.Chromium",
"PULSE_COOKIE=/.hakurei/pulse-cookie", "PIPEWIRE_REMOTE=/run/user/1971/pipewire-0",
"PULSE_SERVER=unix:/run/user/1971/pulse/native",
"SHELL=/run/current-system/sw/bin/zsh", "SHELL=/run/current-system/sw/bin/zsh",
"TERM=xterm-256color", "TERM=xterm-256color",
"USER=chronos", "USER=chronos",
@@ -141,10 +130,10 @@ func TestOutcomeMain(t *testing.T) {
Proc(fhs.AbsProc). Proc(fhs.AbsProc).
Tmpfs(hst.AbsPrivateTmp, 1<<12, 0755). Tmpfs(hst.AbsPrivateTmp, 1<<12, 0755).
Bind(fhs.AbsDev, fhs.AbsDev, std.BindWritable|std.BindDevice). Bind(fhs.AbsDev, fhs.AbsDev, std.BindWritable|std.BindDevice).
Tmpfs(fhs.AbsDev.Append("shm"), 0, 01777). Tmpfs(fhs.AbsDevShm, 0, 01777).
// spRuntimeOp // spRuntimeOp
Tmpfs(fhs.AbsRunUser, 1<<12, 0755). Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755).
Bind(m("/tmp/hakurei.0/runtime/9"), m("/run/user/1971"), std.BindWritable). Bind(m("/tmp/hakurei.0/runtime/9"), m("/run/user/1971"), std.BindWritable).
// spTmpdirOp // spTmpdirOp
@@ -157,9 +146,8 @@ func TestOutcomeMain(t *testing.T) {
// spWaylandOp // spWaylandOp
Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/wayland"), m("/run/user/1971/wayland-0"), 0). Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/wayland"), m("/run/user/1971/wayland-0"), 0).
// spPulseOp // spPipeWireOp
Bind(m("/run/user/1971/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pulse"), m("/run/user/1971/pulse/native"), 0). Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pipewire"), m("/run/user/1971/pipewire-0"), 0).
Place(m("/.hakurei/pulse-cookie"), bytes.Repeat([]byte{0}, pulseCookieSizeMax)).
// spDBusOp // spDBusOp
Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bus"), m("/run/user/1971/bus"), 0). Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bus"), m("/run/user/1971/bus"), 0).
@@ -243,8 +231,8 @@ func TestOutcomeMain(t *testing.T) {
Proc(m("/proc/")). Proc(m("/proc/")).
Tmpfs(hst.AbsPrivateTmp, 4096, 0755). Tmpfs(hst.AbsPrivateTmp, 4096, 0755).
DevWritable(m("/dev/"), true). DevWritable(m("/dev/"), true).
Tmpfs(m("/dev/shm"), 0, 01777). Tmpfs(m("/dev/shm/"), 0, 01777).
Tmpfs(m("/run/user/"), 4096, 0755). Tmpfs(m("/run/user/"), xdgRuntimeDirSize, 0755).
Bind(m("/tmp/hakurei.0/runtime/0"), m("/run/user/65534"), std.BindWritable). Bind(m("/tmp/hakurei.0/runtime/0"), m("/run/user/65534"), std.BindWritable).
Bind(m("/tmp/hakurei.0/tmpdir/0"), m("/tmp/"), std.BindWritable). Bind(m("/tmp/hakurei.0/tmpdir/0"), m("/tmp/"), std.BindWritable).
Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")). Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")).
@@ -298,7 +286,7 @@ func TestOutcomeMain(t *testing.T) {
}, },
Filter: true, Filter: true,
}, },
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPulse), Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []hst.FilesystemConfigJSON{
@@ -347,10 +335,7 @@ func TestOutcomeMain(t *testing.T) {
Ensure(m("/tmp/hakurei.0/tmpdir/9"), 01700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/9"), acl.Read, acl.Write, acl.Execute). Ensure(m("/tmp/hakurei.0/tmpdir/9"), 01700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/9"), acl.Read, acl.Write, acl.Execute).
Ephemeral(system.Process, m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c"), 0711). Ephemeral(system.Process, m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c"), 0711).
Wayland(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/1971/wayland-0"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"). Wayland(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/1971/wayland-0"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
Ensure(m("/run/user/1971"), 0700).UpdatePermType(system.User, m("/run/user/1971"), acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset PipeWire(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/pipewire")).
Ensure(m("/run/user/1971/hakurei"), 0700).UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
Ephemeral(system.Process, m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c"), 0700).UpdatePermType(system.Process, m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c"), acl.Execute).
Link(m("/run/user/1971/pulse/native"), m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse")).
MustProxyDBus(&hst.BusConfig{ MustProxyDBus(&hst.BusConfig{
Talk: []string{ Talk: []string{
"org.freedesktop.Notifications", "org.freedesktop.Notifications",
@@ -397,8 +382,7 @@ func TestOutcomeMain(t *testing.T) {
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus", "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus",
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket", "DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket",
"HOME=/home/chronos", "HOME=/home/chronos",
"PULSE_COOKIE=" + hst.PrivateTmp + "/pulse-cookie", "PIPEWIRE_REMOTE=/run/user/65534/pipewire-0",
"PULSE_SERVER=unix:/run/user/65534/pulse/native",
"SHELL=/run/current-system/sw/bin/zsh", "SHELL=/run/current-system/sw/bin/zsh",
"TERM=xterm-256color", "TERM=xterm-256color",
"USER=chronos", "USER=chronos",
@@ -412,15 +396,14 @@ func TestOutcomeMain(t *testing.T) {
Proc(m("/proc/")). Proc(m("/proc/")).
Tmpfs(hst.AbsPrivateTmp, 4096, 0755). Tmpfs(hst.AbsPrivateTmp, 4096, 0755).
DevWritable(m("/dev/"), true). DevWritable(m("/dev/"), true).
Tmpfs(m("/dev/shm"), 0, 01777). Tmpfs(m("/dev/shm/"), 0, 01777).
Tmpfs(m("/run/user/"), 4096, 0755). Tmpfs(m("/run/user/"), xdgRuntimeDirSize, 0755).
Bind(m("/tmp/hakurei.0/runtime/9"), m("/run/user/65534"), std.BindWritable). Bind(m("/tmp/hakurei.0/runtime/9"), m("/run/user/65534"), std.BindWritable).
Bind(m("/tmp/hakurei.0/tmpdir/9"), m("/tmp/"), std.BindWritable). Bind(m("/tmp/hakurei.0/tmpdir/9"), m("/tmp/"), std.BindWritable).
Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")). Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")).
Place(m("/etc/group"), []byte("hakurei:x:65534:\n")). Place(m("/etc/group"), []byte("hakurei:x:65534:\n")).
Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/65534/wayland-0"), 0). Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/65534/wayland-0"), 0).
Bind(m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse"), m("/run/user/65534/pulse/native"), 0). Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/pipewire"), m("/run/user/65534/pipewire-0"), 0).
Place(m(hst.PrivateTmp+"/pulse-cookie"), bytes.Repeat([]byte{0}, pulseCookieSizeMax)).
Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus"), m("/run/user/65534/bus"), 0). Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus"), m("/run/user/65534/bus"), 0).
Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), m("/var/run/dbus/system_bus_socket"), 0). Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), m("/var/run/dbus/system_bus_socket"), 0).
Bind(m("/dev/dri"), m("/dev/dri"), std.BindWritable|std.BindDevice|std.BindOptional). Bind(m("/dev/dri"), m("/dev/dri"), std.BindWritable|std.BindDevice|std.BindOptional).
@@ -440,7 +423,7 @@ func TestOutcomeMain(t *testing.T) {
{"nixos chromium direct wayland", new(stubNixOS), &hst.Config{ {"nixos chromium direct wayland", new(stubNixOS), &hst.Config{
ID: "org.chromium.Chromium", ID: "org.chromium.Chromium",
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPulse), Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Env: nil, Env: nil,
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []hst.FilesystemConfigJSON{
@@ -502,9 +485,8 @@ func TestOutcomeMain(t *testing.T) {
Ensure(m("/run/user/1971"), 0700).UpdatePermType(system.User, m("/run/user/1971"), acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset Ensure(m("/run/user/1971"), 0700).UpdatePermType(system.User, m("/run/user/1971"), acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ensure(m("/run/user/1971/hakurei"), 0700).UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute). Ensure(m("/run/user/1971/hakurei"), 0700).UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
UpdatePermType(hst.EWayland, m("/run/user/1971/wayland-0"), acl.Read, acl.Write, acl.Execute). UpdatePermType(hst.EWayland, m("/run/user/1971/wayland-0"), acl.Read, acl.Write, acl.Execute).
Ephemeral(system.Process, m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1"), 0700).UpdatePermType(system.Process, m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1"), acl.Execute).
Link(m("/run/user/1971/pulse/native"), m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse")).
Ephemeral(system.Process, m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1"), 0711). Ephemeral(system.Process, m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1"), 0711).
PipeWire(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/pipewire")).
MustProxyDBus(&hst.BusConfig{ MustProxyDBus(&hst.BusConfig{
Talk: []string{ Talk: []string{
"org.freedesktop.FileManager1", "org.freedesktop.Notifications", "org.freedesktop.FileManager1", "org.freedesktop.Notifications",
@@ -544,8 +526,7 @@ func TestOutcomeMain(t *testing.T) {
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus", "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus",
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket", "DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket",
"HOME=/var/lib/persist/module/hakurei/0/1", "HOME=/var/lib/persist/module/hakurei/0/1",
"PULSE_COOKIE=" + hst.PrivateTmp + "/pulse-cookie", "PIPEWIRE_REMOTE=/run/user/1971/pipewire-0",
"PULSE_SERVER=unix:/run/user/1971/pulse/native",
"SHELL=/run/current-system/sw/bin/zsh", "SHELL=/run/current-system/sw/bin/zsh",
"TERM=xterm-256color", "TERM=xterm-256color",
"USER=u0_a1", "USER=u0_a1",
@@ -558,15 +539,14 @@ func TestOutcomeMain(t *testing.T) {
Proc(m("/proc/")). Proc(m("/proc/")).
Tmpfs(hst.AbsPrivateTmp, 4096, 0755). Tmpfs(hst.AbsPrivateTmp, 4096, 0755).
DevWritable(m("/dev/"), true). DevWritable(m("/dev/"), true).
Tmpfs(m("/dev/shm"), 0, 01777). Tmpfs(m("/dev/shm/"), 0, 01777).
Tmpfs(m("/run/user/"), 4096, 0755). Tmpfs(m("/run/user/"), xdgRuntimeDirSize, 0755).
Bind(m("/tmp/hakurei.0/runtime/1"), m("/run/user/1971"), std.BindWritable). Bind(m("/tmp/hakurei.0/runtime/1"), m("/run/user/1971"), std.BindWritable).
Bind(m("/tmp/hakurei.0/tmpdir/1"), m("/tmp/"), std.BindWritable). Bind(m("/tmp/hakurei.0/tmpdir/1"), m("/tmp/"), std.BindWritable).
Place(m("/etc/passwd"), []byte("u0_a1:x:1971:100:Hakurei:/var/lib/persist/module/hakurei/0/1:/run/current-system/sw/bin/zsh\n")). Place(m("/etc/passwd"), []byte("u0_a1:x:1971:100:Hakurei:/var/lib/persist/module/hakurei/0/1:/run/current-system/sw/bin/zsh\n")).
Place(m("/etc/group"), []byte("hakurei:x:100:\n")). Place(m("/etc/group"), []byte("hakurei:x:100:\n")).
Bind(m("/run/user/1971/wayland-0"), m("/run/user/1971/wayland-0"), 0). Bind(m("/run/user/1971/wayland-0"), m("/run/user/1971/wayland-0"), 0).
Bind(m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse"), m("/run/user/1971/pulse/native"), 0). Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/pipewire"), m("/run/user/1971/pipewire-0"), 0).
Place(m(hst.PrivateTmp+"/pulse-cookie"), bytes.Repeat([]byte{0}, pulseCookieSizeMax)).
Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus"), m("/run/user/1971/bus"), 0). Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus"), m("/run/user/1971/bus"), 0).
Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), m("/var/run/dbus/system_bus_socket"), 0). Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), m("/var/run/dbus/system_bus_socket"), 0).
Bind(m("/bin"), m("/bin"), 0). Bind(m("/bin"), m("/bin"), 0).
+36 -36
View File
@@ -10,53 +10,53 @@ static int hakurei_shim_fd = -1;
/* see shim.go for handling of the message */ /* see shim.go for handling of the message */
static inline ssize_t hakurei_shim_write(hakurei_shim_msg msg) { static inline ssize_t hakurei_shim_write(hakurei_shim_msg msg) {
int savedErrno = errno; int savedErrno = errno;
unsigned char buf = (unsigned char)msg; unsigned char buf = (unsigned char)msg;
ssize_t ret = write(hakurei_shim_fd, &buf, 1); ssize_t ret = write(hakurei_shim_fd, &buf, 1);
if (ret == -1 && errno != EAGAIN) if (ret == -1 && errno != EAGAIN)
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
errno = savedErrno; errno = savedErrno;
return ret; return ret;
} }
static void hakurei_shim_sigaction(int sig, siginfo_t *si, void *ucontext) { static void hakurei_shim_sigaction(int sig, siginfo_t *si, void *ucontext) {
if (sig != SIGCONT || si == NULL) { if (sig != SIGCONT || si == NULL) {
hakurei_shim_write(HAKUREI_SHIM_INVALID); hakurei_shim_write(HAKUREI_SHIM_INVALID);
return; return;
} }
if (si->si_pid == hakurei_shim_param_ppid) { if (si->si_pid == hakurei_shim_param_ppid) {
hakurei_shim_write(HAKUREI_SHIM_EXIT_REQUESTED); hakurei_shim_write(HAKUREI_SHIM_EXIT_REQUESTED);
return; return;
} }
hakurei_shim_write(HAKUREI_SHIM_BAD_PID); hakurei_shim_write(HAKUREI_SHIM_BAD_PID);
if (getppid() != hakurei_shim_param_ppid) if (getppid() != hakurei_shim_param_ppid)
hakurei_shim_write(HAKUREI_SHIM_ORPHAN); hakurei_shim_write(HAKUREI_SHIM_ORPHAN);
} }
void hakurei_shim_setup_cont_signal(pid_t ppid, int fd) { void hakurei_shim_setup_cont_signal(pid_t ppid, int fd) {
if (hakurei_shim_param_ppid != -1 || hakurei_shim_fd != -1) if (hakurei_shim_param_ppid != -1 || hakurei_shim_fd != -1)
*(int *)NULL = 0; /* unreachable */ *(volatile int *)NULL = 0; /* unreachable */
struct sigaction new_action = {0}, old_action = {0}; struct sigaction new_action = {0}, old_action = {0};
if (sigaction(SIGCONT, NULL, &old_action) != 0) if (sigaction(SIGCONT, NULL, &old_action) != 0)
return; return;
if (old_action.sa_handler != SIG_DFL) { if (old_action.sa_handler != SIG_DFL) {
errno = ENOTRECOVERABLE; errno = ENOTRECOVERABLE;
return; return;
} }
new_action.sa_sigaction = hakurei_shim_sigaction; new_action.sa_sigaction = hakurei_shim_sigaction;
if (sigemptyset(&new_action.sa_mask) != 0) if (sigemptyset(&new_action.sa_mask) != 0)
return; return;
new_action.sa_flags = SA_ONSTACK | SA_SIGINFO; new_action.sa_flags = SA_ONSTACK | SA_SIGINFO;
if (sigaction(SIGCONT, &new_action, NULL) != 0) if (sigaction(SIGCONT, &new_action, NULL) != 0)
return; return;
errno = 0; errno = 0;
hakurei_shim_param_ppid = ppid; hakurei_shim_param_ppid = ppid;
hakurei_shim_fd = fd; hakurei_shim_fd = fd;
} }
+4 -4
View File
@@ -2,10 +2,10 @@
/* see shim.go for documentation */ /* see shim.go for documentation */
typedef enum { typedef enum {
HAKUREI_SHIM_EXIT_REQUESTED, HAKUREI_SHIM_EXIT_REQUESTED,
HAKUREI_SHIM_ORPHAN, HAKUREI_SHIM_ORPHAN,
HAKUREI_SHIM_INVALID, HAKUREI_SHIM_INVALID,
HAKUREI_SHIM_BAD_PID, HAKUREI_SHIM_BAD_PID,
} hakurei_shim_msg; } hakurei_shim_msg;
void hakurei_shim_setup_cont_signal(pid_t ppid, int fd); void hakurei_shim_setup_cont_signal(pid_t ppid, int fd);
+2 -2
View File
@@ -66,10 +66,10 @@ func TestShimEntrypoint(t *testing.T) {
Proc(fhs.AbsProc). Proc(fhs.AbsProc).
Tmpfs(hst.AbsPrivateTmp, 1<<12, 0755). Tmpfs(hst.AbsPrivateTmp, 1<<12, 0755).
Bind(fhs.AbsDev, fhs.AbsDev, std.BindWritable|std.BindDevice). Bind(fhs.AbsDev, fhs.AbsDev, std.BindWritable|std.BindDevice).
Tmpfs(fhs.AbsDev.Append("shm"), 0, 01777). Tmpfs(fhs.AbsDevShm, 0, 01777).
// spRuntimeOp // spRuntimeOp
Tmpfs(fhs.AbsRunUser, 1<<12, 0755). Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755).
Bind(m("/tmp/hakurei.10/runtime/9999"), m("/run/user/1000"), std.BindWritable). Bind(m("/tmp/hakurei.10/runtime/9999"), m("/run/user/1000"), std.BindWritable).
// spTmpdirOp // spTmpdirOp

Some files were not shown because too many files have changed in this diff Show More