420 Commits

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

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

Closes #29.

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

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

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

This will be implemented in a future commit.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-10 01:00:03 +09:00
ccc0d98bd7 release: 0.3.2
All checks were successful
Release / Create release (push) Successful in 48s
Test / Create distribution (push) Successful in 28s
Test / Sandbox (push) Successful in 42s
Test / Sandbox (race detector) (push) Successful in 41s
Test / Hpkg (push) Successful in 44s
Test / Hakurei (race detector) (push) Successful in 7m4s
Test / Hakurei (push) Successful in 4m2s
Test / Flake checks (push) Successful in 1m40s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-09 08:12:52 +09:00
a3fd05765e container: load initial process started before syscall
All checks were successful
Test / Create distribution (push) Successful in 27s
Test / Sandbox (push) Successful in 2m50s
Test / Sandbox (race detector) (push) Successful in 5m3s
Test / Hpkg (push) Successful in 5m21s
Test / Hakurei (push) Successful in 5m47s
Test / Hakurei (race detector) (push) Successful in 7m7s
Test / Flake checks (push) Successful in 1m39s
This avoids a race between returning from syscall and checking the state.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-09 08:12:22 +09:00
c538df7daa internal/pipewire: expose connection props
All checks were successful
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
44e5aa1a36 internal/pipewire: include remaining size in recvmsg wrapper
All checks were successful
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
cf0e7d8c27 internal/pipewire: reset per-roundtrip state once per call
All checks were successful
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
130add21e5 internal/pipewire: increment remote sequence after establishing bounds
All checks were successful
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
5ec4045e24 internal/pipewire: do not clobber error parsing SCMs
All checks were successful
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
be2075f169 Revert "internal/pipewire: work around remote sequence quirk"
All checks were successful
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
e9fb1d7be5 container/initdaemon: copy wstatus from wait4 loop
All checks were successful
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
dafe9f8efc container: spin instead of block on wait4 ECHILD
All checks were successful
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
96dd7abd80 container: improve error message fallback
All checks were successful
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
d5fb179012 cmd/hakurei: exec instead of fork/exec from shell
All checks were successful
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
462863e290 container: friendlier error message for op timing out
All checks were successful
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
2786611b88 test/interactive: add app with bad daemon
All checks were successful
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
791a1dfa55 container: make wait4 loop available to ops
All checks were successful
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
564db6863b internal/pipewire: work around remote sequence quirk
All checks were successful
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
87781c7658 treewide: include PipeWire op and enforce PulseAudio check
All checks were successful
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
0c38fb7b6a hst: expose daemon as fs entry
All checks were successful
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
357cfcddee container: start daemons within container
All checks were successful
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
6bf245cf1b container: pass context as setup state
All checks were successful
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
c8eeb4a4d1 internal/outcome: integrate pipewire server
All checks were successful
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
5785714b64 container: call op method right before initial process
All checks were successful
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
422efcf258 hst: check for insecure PulseAudio enablement
All checks were successful
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
104eeecf65 cmd/hakurei: add pipewire flag
All checks were successful
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
bf856f06e5 internal/pipewire: constant for PIPEWIRE_REMOTE
All checks were successful
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
1931b54600 hst: add pipewire flag
All checks were successful
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
093e30c788 internal/system: integrate PipeWire SecurityContext
All checks were successful
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
1b17ccda91 internal/system: optional op check parallelism
All checks were successful
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
7c6fc1128b internal/pipewire: set finalizer on scc
All checks were successful
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
8cdd659239 internal/pipewire: seq access method for consume
All checks were successful
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
15c2839a09 internal/pipewire: respond to Core::Ping
All checks were successful
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
b9b9705b52 internal/pipewire: specify opcode and file count with message
All checks were successful
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
246e04214a internal/system: pass syscall error message
All checks were successful
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
503bfc6468 internal/system: port connect by name
All checks were successful
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
d837628b4c internal/system: remove ineffectual join reverting wayland
All checks were successful
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
3cb58b4b72 internal/pipewire: high level SecurityContext helper
All checks were successful
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
bb1fc4c7bc internal/pipewire: check pending ids after done
All checks were successful
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
f44923da29 internal/pipewire: post-sync cleanup functions
All checks were successful
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
5e7861bb00 internal/pipewire: handle dangling files in roundtrip
All checks were successful
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
7cb3308a53 internal/pipewire: store proxy errors in context
All checks were successful
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
490093a659 internal/pipewire: set errno on an empty message
All checks were successful
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
2b22efcdf1 internal/pipewire: rename context consume method
All checks were successful
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
8a2f9edcf9 internal/pipewire: use sendmsg/recvmsg directly
All checks were successful
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
0d3f332d45 internal/pipewire: do not send ancillary msg without files
All checks were successful
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
d5509cc6e5 internal/pipewire: constants from pipewire/keys.h
All checks were successful
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
0d3ae6cb23 internal/pipewire: improve protocol error messages
All checks were successful
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
69b1131d66 internal/pipewire: use type name in error strings
All checks were successful
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
2c0b92771a internal/pipewire: relocate constants
All checks were successful
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
054c91879f internal/pipewire: finalizers for dangling files
All checks were successful
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
c34439fc5f internal/pipewire: collect non-protocol errors
All checks were successful
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
32fb137bb2 internal/pipewire: fail on unacknowledged proxies
All checks were successful
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
e7a665e043 internal/pipewire: handle Core::Error
All checks were successful
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
af741f20a0 internal/pipewire: implement client context
All checks were successful
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
39c6716fb0 internal/pipewire: use correct types in header
All checks were successful
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
7bc73afadd internal/pipewire: wrap EOF error for deserialisation
All checks were successful
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
647aa9d02f internal/pipewire: preallocate for footer
All checks were successful
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
91aaabaa1b internal/pipewire: benchmarks against Gob and JSON
All checks were successful
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
3d4c7cdd9e internal/pipewire: implement Core::Error
All checks were successful
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
4fd6d6c037 internal/pipewire: implement Core::Ping, Core::Pong
All checks were successful
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
de3fc7ba38 internal/pipewire: implement SecurityContext::Create
All checks were successful
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
5a5c4705dd internal/pipewire: implement Registry::Bind
All checks were successful
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
f703aa20a5 internal/pipewire: implement client generation footer
All checks were successful
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
5c12425d48 internal/pipewire: implement Registry::Global
All checks were successful
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
cbe86dc4f0 internal/pipewire: add json struct tags
All checks were successful
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
d08a1081bd internal/pipewire: do not store spa_dict fields
All checks were successful
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
72a2601d74 internal/pipewire: store sample iovec continuously
All checks were successful
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
1dab87aaf0 internal/pipewire: add missing constants
All checks were successful
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
2bafde99e3 internal/pipewire: shorten test data filenames
All checks were successful
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
91efeb101a internal/pipewire: spa_dict size nil check
All checks were successful
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
dcb22a61c0 internal/pipewire: require appending marshaler
All checks were successful
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
e028a61fc1 internal/pipewire: preallocate for known size
All checks were successful
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
73987be7d4 internal/pipewire: size without serialisation
All checks were successful
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
563b5e66fc internal/pipewire: simplify spa_dict appends
All checks were successful
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
2edcfe1e68 internal/pipewire: define size constants
All checks were successful
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
2698ca00e8 internal/pipewire: implement Core::Done
All checks were successful
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
1d0143386d internal/pipewire: optional final trailing garbage check
All checks were successful
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
a55c209099 internal/pipewire: additional Client::Info test case
All checks were successful
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
10ff276da1 internal/pipewire: additional Client::Info test case
All checks were successful
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
fd4d379b67 internal/pipewire: implement Client::Info
All checks were successful
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
77f5b89a41 internal/pipewire: implement Core::BoundProps
All checks were successful
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
14e33f17e5 internal/pipewire: check nil marshaler
All checks were successful
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
cfeb7818eb internal/pipewire: implement Core::Info and generation footer
All checks were successful
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
05391da556 internal/pipewire: implement footer
All checks were successful
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
463f8836e6 internal/pipewire: implement Long type
All checks were successful
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
2e465c94da internal/pipewire: implement Id type
All checks were successful
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
26009fd3f7 internal/pipewire: slice at POD boundary
All checks were successful
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
2d7b896a8c internal/pipewire: bounds check against wire size
All checks were successful
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
a0eb010aab internal/pipewire: spa_dict trailing garbage within POD
All checks were successful
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
b1b27ac1df internal/pipewire: zero size before validation
All checks were successful
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
fc3d78fe01 internal/pipewire: implement Core::Sync
All checks were successful
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
591637264a internal/pipewire: implement Core::GetRegistry
All checks were successful
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
e77652bf89 internal/pipewire: move test data to files
All checks were successful
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
88d3e46413 internal/pipewire: implement Client::UpdateProperties
All checks were successful
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
e51e81bb22 internal/pipewire: implement spa_dict type
All checks were successful
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
8f4a3bcf9f internal/pipewire: use custom marshaler when available
All checks were successful
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
827dc9e1ba internal/pipewire: implement string type
All checks were successful
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
d92de1c709 internal/pipewire: check for trailing garbage
All checks were successful
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
5bcafcf734 internal/pipewire: implement Core::Hello
All checks were successful
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
9f7b0c2f46 internal/pipewire: add type constants
All checks were successful
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
3e87187c4c internal/pipewire: implement message header
All checks were successful
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
b651d95e77 workflows: do not duplicate on pulls
All checks were successful
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
aab92ce3c1 internal/wayland: clean up pathname socket
All checks were successful
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
a495e09a8f internal/wayland: do not double close fd
All checks were successful
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
3afca2bd5b internal/wayland: expose WAYLAND_VERSION
All checks were successful
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
b73a789dfe .clang-format: increase indent width
All checks were successful
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
38b5ff0cec internal/wayland: check pathname size
All checks were successful
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
3c204b9b40 internal/wayland: increase error detail
All checks were successful
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
00771efeb4 internal/wayland: remove fd typecasts
All checks were successful
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
61972d61f6 internal/wayland: reimplement connect/bind code
All checks were successful
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
fe40af7b7e internal/wayland: relocate connection struct
All checks were successful
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
12751932d1 internal/wayland: improve error handling
All checks were successful
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
41b49137a8 .clang-format: do not limit line length
All checks were successful
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
c761e1de4d nix: build with clang
All checks were successful
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
a91920310d internal: relocate packages
All checks were successful
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
16e674782a cmd/hakurei: reorder show entries
All checks were successful
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
47244daefb treewide: migrate ldd callers
All checks were successful
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
46fa104419 ldd: require absolute pathname
All checks were successful
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
45953b3d9c ldd: cancel on decoder error
All checks were successful
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
42759e7a9f ldd: create musl entry representation
All checks were successful
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
8e2d2c8246 ldd: check decoder scan guard
All checks were successful
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
299685775a container: provide usage example
All checks were successful
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
b7406cc4c4 ldd: update package doc comment
All checks were successful
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
690a0ed0d6 ldd: decode from reader
All checks were successful
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
a9d72a5eb1 internal/outcome: rename run from main
All checks were successful
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
6d14bb814f container/fhs: add constant for /dev/shm/
All checks were successful
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
be0e387ab0 internal/info: relocate from internal
All checks were successful
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
abeb67964f treewide: document linkname uses
All checks were successful
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
bf5d10743f treewide: import internal/system
All checks were successful
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
4e7aab07d5 internal/system: relocate from system
All checks were successful
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
15a66a2b31 treewide: import internal/helper
All checks were successful
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
f347d44c22 internal/helper: relocate from helper
All checks were successful
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
b5630f6883 test: move package sandbox internal
All checks were successful
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
17ffdb2dcf release: 0.3.1
All checks were successful
Release / Create release (push) Successful in 39s
Test / Sandbox (push) Successful in 40s
Test / Hakurei (push) Successful in 1m36s
Test / Create distribution (push) Successful in 26s
Test / Sandbox (race detector) (push) Successful in 4m33s
Test / Hpkg (push) Successful in 4m23s
Test / Hakurei (race detector) (push) Successful in 5m11s
Test / Flake checks (push) Successful in 1m37s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-12 00:53:14 +09:00
ac34635890 container: set FD_CLOEXEC on all open files
All checks were successful
Test / Create distribution (push) Successful in 29s
Test / Sandbox (race detector) (push) Successful in 40s
Test / Hakurei (race detector) (push) Successful in 46s
Test / Hakurei (push) Successful in 47s
Test / Sandbox (push) Successful in 44s
Test / Hpkg (push) Successful in 43s
Test / Flake checks (push) Successful in 1m31s
While fd created from this side always has the FD_CLOEXEC flag, the same is not true for files left open by the parent. This change prevents those files from leaking into the container.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-12 00:18:29 +09:00
9dec9dbc4b container/init: close setup pipe early
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m32s
Test / Sandbox (race detector) (push) Successful in 4m30s
Test / Hpkg (push) Successful in 4m48s
Test / Hakurei (race detector) (push) Successful in 6m17s
Test / Hakurei (push) Successful in 3m17s
Test / Flake checks (push) Successful in 1m21s
This prevents leaking the setup pipe.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-11 07:31:58 +09:00
2f74adc8bd container/init: close initial process files on termination
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m29s
Test / Sandbox (race detector) (push) Successful in 4m28s
Test / Hakurei (push) Successful in 4m34s
Test / Hpkg (push) Successful in 4m42s
Test / Hakurei (race detector) (push) Successful in 6m9s
Test / Flake checks (push) Successful in 1m26s
This closes them during the adopt wait delay. This also keeps them alive.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-10 20:35:59 +09:00
d7e0104ae4 treewide: reject impossible user-supplied fd
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m36s
Test / Hakurei (push) Successful in 4m33s
Test / Sandbox (race detector) (push) Successful in 4m30s
Test / Hpkg (push) Successful in 4m53s
Test / Hakurei (race detector) (push) Successful in 6m12s
Test / Flake checks (push) Successful in 1m31s
These are all trusted user input, however this check reduces the likelihood of hard to debug errors.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-10 20:31:26 +09:00
bb92e3ada9 cmd/hakurei: expose current instance identifier
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m25s
Test / Hakurei (push) Successful in 4m36s
Test / Sandbox (race detector) (push) Successful in 4m31s
Test / Hpkg (push) Successful in 4m52s
Test / Hakurei (race detector) (push) Successful in 6m4s
Test / Flake checks (push) Successful in 1m24s
This writes the 16-byte instance identifier to file descriptor specified by --identifier-fd if set, and closes the file.

This enables safely obtaining the new instance's identifier.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-10 07:52:35 +09:00
fad419c2a2 internal/outcome: handle group lookup message
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m37s
Test / Sandbox (race detector) (push) Successful in 4m37s
Test / Hpkg (push) Successful in 5m1s
Test / Hakurei (push) Successful in 5m3s
Test / Hakurei (race detector) (push) Successful in 6m39s
Test / Flake checks (push) Successful in 1m35s
This results in slightly less messy error reporting.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-09 02:15:48 +09:00
b1a1e73238 nix: update names to reflect new terminology
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 40s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Hakurei (race detector) (push) Successful in 44s
Test / Hakurei (push) Successful in 46s
Test / Hpkg (push) Successful in 41s
Test / Flake checks (push) Successful in 1m30s
These are terminology from way early days. Update them now to be less confusing.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-09 01:48:31 +09:00
38e9128a8c container/std/seccomp: remove ineffectual typecast
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m17s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 4m6s
Test / Sandbox (race detector) (push) Successful in 4m11s
Test / Hakurei (race detector) (push) Successful in 5m1s
Test / Flake checks (push) Successful in 1m26s
This is no longer necessary since the return type changed.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-07 05:45:51 +09:00
7ee702a44e container/seccomp/presets: add fields to literals
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m11s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m10s
Test / Hakurei (race detector) (push) Successful in 5m2s
Test / Flake checks (push) Successful in 1m26s
This keeps composites analysis happy.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-07 05:11:57 +09:00
3d188ef884 std: separate seccomp constants
All checks were successful
Test / Create distribution (push) Successful in 39s
Test / Sandbox (push) Successful in 2m18s
Test / Hakurei (push) Successful in 3m26s
Test / Sandbox (race detector) (push) Successful in 4m13s
Test / Hpkg (push) Successful in 4m12s
Test / Hakurei (race detector) (push) Successful in 5m9s
Test / Flake checks (push) Successful in 1m28s
This avoids inadvertently using PNRs as syscall numbers.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-07 04:30:06 +09:00
34ccda84b2 release: 0.3.0
All checks were successful
Release / Create release (push) Successful in 39s
Test / Sandbox (push) Successful in 39s
Test / Hakurei (push) Successful in 3m20s
Test / Create distribution (push) Successful in 24s
Test / Sandbox (race detector) (push) Successful in 4m0s
Test / Hpkg (push) Successful in 3m37s
Test / Hakurei (race detector) (push) Successful in 4m53s
Test / Flake checks (push) Successful in 1m37s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-06 01:37:15 +09:00
042013bb04 container/std: syscall JSON adapter
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 39s
Test / Hakurei (push) Successful in 43s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Hpkg (push) Successful in 40s
Test / Flake checks (push) Successful in 1m36s
This provides cross-platform JSON adapter for syscall number.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-06 00:57:53 +09:00
5c2b63a7f1 container: add 386 constants
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m17s
Test / Hakurei (push) Successful in 3m11s
Test / Hpkg (push) Successful in 4m0s
Test / Sandbox (race detector) (push) Successful in 4m16s
Test / Hakurei (race detector) (push) Successful in 5m2s
Test / Flake checks (push) Successful in 1m24s
While it is unlikely a use case for hakurei on i686 exists, it does not hurt to have this support.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-05 20:21:14 +09:00
9fd97e71d0 treewide: fit test untyped int literals in 32-bit
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m17s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 3m56s
Test / Sandbox (race detector) (push) Successful in 4m6s
Test / Hakurei (race detector) (push) Successful in 5m2s
Test / Flake checks (push) Successful in 1m24s
This enables hakurei test suite to run on 32-bit targets.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-05 20:13:19 +09:00
fba201c995 container/std: relocate rule types
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m13s
Test / Hpkg (push) Successful in 3m56s
Test / Sandbox (race detector) (push) Successful in 4m14s
Test / Hakurei (race detector) (push) Successful in 5m3s
Test / Flake checks (push) Successful in 1m28s
This enables its use in hst for #15.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-05 06:00:39 +09:00
7f27a6dc51 container/seccomp: use native types
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m16s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m12s
Test / Hakurei (race detector) (push) Successful in 5m1s
Test / Flake checks (push) Successful in 1m30s
This prepares NativeRule for relocation to std for #15.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-05 05:48:59 +09:00
b65aba9446 container/seccomp: alias libseccomp types
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m18s
Test / Hpkg (push) Successful in 4m6s
Test / Sandbox (race detector) (push) Successful in 4m20s
Test / Hakurei (race detector) (push) Successful in 5m2s
Test / Flake checks (push) Successful in 1m29s
This enables tests to refer to these types and check its size.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-05 05:21:43 +09:00
becaf8b6d7 std: relocate seccomp lookup tables
All checks were successful
Test / Create distribution (push) Successful in 33s
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 4m9s
Test / Hakurei (race detector) (push) Successful in 5m0s
Test / Flake checks (push) Successful in 1m28s
This should enable resolving NativeRule in hst.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-05 04:48:05 +09:00
54c0d6bf48 container/seccomp/pnr: define pseudo syscalls
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m21s
Test / Hakurei (push) Successful in 3m12s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m5s
Test / Hakurei (race detector) (push) Successful in 4m58s
Test / Flake checks (push) Successful in 1m27s
This eliminates the cgo dependency from syscall lookup.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-05 04:32:41 +09:00
c1399f5030 std: rename from comp
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 3m59s
Test / Sandbox (race detector) (push) Successful in 4m10s
Test / Hakurei (race detector) (push) Successful in 5m4s
Test / Flake checks (push) Successful in 1m28s
Seccomp lookup tables are going to be relocated here, and PNR constants.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-05 02:47:43 +09:00
9ac63aac0c hst/grp_pwd: add extra test cases
All checks were successful
Test / Create distribution (push) Successful in 45s
Test / Sandbox (push) Successful in 2m31s
Test / Hakurei (push) Successful in 3m37s
Test / Hpkg (push) Successful in 4m15s
Test / Sandbox (race detector) (push) Successful in 4m21s
Test / Hakurei (race detector) (push) Successful in 5m16s
Test / Flake checks (push) Successful in 1m26s
Does not change coverage but this helps me crosscheck with my phone.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-05 01:42:42 +09:00
cb9ebf0e15 hst/grp_pwd: specify new uid format
All checks were successful
Test / Create distribution (push) Successful in 27s
Test / Sandbox (push) Successful in 41s
Test / Sandbox (race detector) (push) Successful in 41s
Test / Hpkg (push) Successful in 42s
Test / Hakurei (push) Successful in 47s
Test / Hakurei (race detector) (push) Successful in 46s
Test / Flake checks (push) Successful in 1m31s
This leaves slots available for additional uid ranges in Rosa OS.

This breaks all existing installations! Users are required to fix ownership manually.

Closes #18.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-04 08:24:41 +09:00
9a2a7b749f cmd/hakurei/print: handle nil config
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Sandbox (race detector) (push) Successful in 40s
Test / Sandbox (push) Successful in 41s
Test / Hakurei (push) Successful in 44s
Test / Hpkg (push) Successful in 42s
Test / Hakurei (race detector) (push) Successful in 45s
Test / Flake checks (push) Successful in 1m37s
There is nothing to print in this case, and such a nil check is missing.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-03 02:20:18 +09:00
ec5cb9400c cmd/hpkg/test: print share directory
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 40s
Test / Hakurei (push) Successful in 43s
Test / Hakurei (race detector) (push) Successful in 44s
Test / Hpkg (push) Successful in 40s
Test / Flake checks (push) Successful in 1m30s
This is more useful now that state is tracked here.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-03 01:51:57 +09:00
ae66b3d2fb message: rename NewMsg to New
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m7s
Test / Sandbox (race detector) (push) Successful in 4m14s
Test / Hakurei (race detector) (push) Successful in 5m7s
Test / Flake checks (push) Successful in 1m37s
Should have done this when relocating this from container. Now is a good time to rename it before v0.3.x.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-03 01:49:27 +09:00
149bc3671a internal/store: remove compat adapter
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m16s
Test / Hakurei (push) Successful in 3m17s
Test / Sandbox (race detector) (push) Successful in 4m12s
Test / Hpkg (push) Successful in 4m18s
Test / Hakurei (race detector) (push) Successful in 5m3s
Test / Flake checks (push) Successful in 1m30s
This is no longer used as everything has been migrated.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-03 01:26:01 +09:00
24435694a5 hst/config: make identifier omitempty
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m11s
Test / Hakurei (push) Successful in 3m17s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m12s
Test / Hakurei (race detector) (push) Successful in 5m7s
Test / Flake checks (push) Successful in 1m33s
This is an optional field. Serialise it as such.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-03 01:23:15 +09:00
1c168babf2 cmd/hakurei/print: use new store interface
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m11s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m11s
Test / Hakurei (race detector) (push) Successful in 5m3s
Test / Flake checks (push) Successful in 1m40s
This removes the final uses of the compat interfaces.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-03 01:19:16 +09:00
0edcb7c1d3 test: print share directory
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (race detector) (push) Successful in 41s
Test / Sandbox (push) Successful in 41s
Test / Hpkg (push) Successful in 41s
Test / Hakurei (push) Successful in 2m24s
Test / Hakurei (race detector) (push) Successful in 3m3s
Test / Flake checks (push) Successful in 1m29s
This is more useful now that state is tracked here.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 17:00:59 +09:00
0e5ca74b98 cmd/hakurei/print: serialise array for ps
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 40s
Test / Sandbox (race detector) (push) Successful in 42s
Test / Hakurei (push) Successful in 2m25s
Test / Hakurei (race detector) (push) Successful in 3m7s
Test / Hpkg (push) Successful in 3m13s
Test / Flake checks (push) Successful in 1m27s
Wanted to do this for a long time, since the key is redundant. This also makes it easier to migrate to the new store interface.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 16:37:08 +09:00
23ae7822bf cmd/hakurei/parse: use new store interface
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m21s
Test / Sandbox (race detector) (push) Successful in 4m16s
Test / Hpkg (push) Successful in 4m15s
Test / Hakurei (race detector) (push) Successful in 4m58s
Test / Hakurei (push) Successful in 2m16s
Test / Flake checks (push) Successful in 1m28s
This greatly reduces overhead. The iterator also significantly cleans up the usage code.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 16:00:41 +09:00
898b5aed3d internal/store: iterator over all entries
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m27s
Test / Hakurei (push) Successful in 3m13s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m10s
Test / Hakurei (race detector) (push) Successful in 4m59s
Test / Flake checks (push) Successful in 1m31s
This is quite convenient for searching the store or printing active instance information.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 15:54:00 +09:00
7c3c3135d8 internal/outcome: track state in TMPDIR
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m7s
Test / Hpkg (push) Successful in 4m3s
Test / Sandbox (race detector) (push) Successful in 4m10s
Test / Hakurei (race detector) (push) Successful in 4m56s
Test / Flake checks (push) Successful in 1m30s
The SharePath is a more stable path than RunDirPath, since it is available all the time and should remain consistent. This also fits better into the intended use case of XDG_RUNTIME_DIR.

Closes #17.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 12:40:58 +09:00
f33aea9ff9 internal/env: cleaner runtime dir fallback
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m16s
Test / Hakurei (push) Successful in 3m10s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m14s
Test / Hakurei (race detector) (push) Successful in 4m57s
Test / Flake checks (push) Successful in 1m28s
This now places rundir inside the fallback runtime dir, so special case in internal/outcome is avoided.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 12:22:32 +09:00
e7fc311d0b internal/outcome/shim: cover reparent and exit request paths
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Hakurei (push) Successful in 42s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Hpkg (push) Successful in 41s
Test / Flake checks (push) Successful in 1m31s
These test cases were missed when making the changes.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 11:58:09 +09:00
f5274067f6 internal/outcome/process: nil-safe unlock when failing to lock
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Hpkg (push) Successful in 4m13s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m9s
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m57s
Test / Flake checks (push) Successful in 1m26s
This also prints a debug message which might be useful.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 11:47:51 +09:00
e7161f8e61 internal/outcome: measure finalise time
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m16s
Test / Hakurei (push) Successful in 3m11s
Test / Hpkg (push) Successful in 4m8s
Test / Flake checks (push) Successful in 1m19s
Test / Sandbox (race detector) (push) Successful in 4m4s
Test / Hakurei (race detector) (push) Successful in 4m56s
This also increases precision of state time output.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 05:17:33 +09:00
6931ad95c3 internal/outcome/shim: EOF as exit request fallback
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 55s
Test / Sandbox (race detector) (push) Successful in 53s
Test / Hpkg (push) Successful in 53s
Test / Hakurei (race detector) (push) Successful in 1m1s
Test / Hakurei (push) Successful in 1m3s
Test / Flake checks (push) Successful in 1m34s
In some cases the signal might be delivered before the signal handler is installed, and synchronising against such a case is too expensive. Instead, use the pipe being closed as a fallback to the regular exit request. This change also moves installation of the signal handler early.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 04:41:26 +09:00
2ba599b399 internal/outcome/process: use new store interface
All checks were successful
Test / Create distribution (push) Successful in 42s
Test / Sandbox (push) Successful in 2m26s
Test / Hakurei (push) Successful in 3m20s
Test / Hpkg (push) Successful in 4m7s
Test / Sandbox (race detector) (push) Successful in 4m15s
Test / Flake checks (push) Successful in 1m32s
Test / Hakurei (race detector) (push) Successful in 5m5s
This change also spawns shim before committing system state, leaving it blocking on the setup pipe. The internal/outcome/process structure is also entirely reworked to be much more readable and less error-prone, while enabling basic performance measurements. A long-standing bug where segment lock is not held during Commit is also resolved.

Closes #19.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 04:25:45 +09:00
d3d3417125 internal/outcome/process: relocate start and serve
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m11s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m5s
Test / Flake checks (push) Successful in 1m30s
Test / Hakurei (race detector) (push) Successful in 4m57s
This is useful for reordering these operations for further cleanup.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-01 19:14:59 +09:00
651cdf9ccb internal/outcome: remove guard on main
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 3m7s
Test / Sandbox (race detector) (push) Successful in 4m8s
Test / Hpkg (push) Successful in 4m9s
Test / Hakurei (race detector) (push) Successful in 4m54s
Test / Flake checks (push) Successful in 1m29s
This is no longer exported. Such a check is pointless.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-31 22:58:26 +09:00
68ff0a2ba6 container/params: expose pipe
All checks were successful
Test / Hpkg (push) Successful in 4m11s
Test / Sandbox (race detector) (push) Successful in 4m13s
Test / Hakurei (race detector) (push) Successful in 5m3s
Test / Flake checks (push) Successful in 1m30s
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m16s
Test / Hakurei (push) Successful in 3m16s
This increases flexibility of how caller wants to handle the I/O. Also makes it no longer rely on finalizer.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-31 22:39:02 +09:00
6a0ecced90 internal/store: expose save via handle
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Sandbox (push) Successful in 42s
Test / Sandbox (race detector) (push) Successful in 42s
Test / Hakurei (push) Successful in 46s
Test / Hakurei (race detector) (push) Successful in 46s
Test / Hpkg (push) Successful in 42s
Test / Flake checks (push) Successful in 1m30s
The handle is otherwise inaccessible without the compat interface. This change also moves compatibility methods to separate adapter structs to avoid inadvertently using them.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-31 04:20:22 +09:00
b667fea1cb internal/store: export new interface
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m13s
Test / Hpkg (push) Successful in 4m4s
Test / Sandbox (race detector) (push) Successful in 4m16s
Test / Hakurei (race detector) (push) Successful in 4m58s
Test / Flake checks (push) Successful in 1m30s
This exposes store operations safe for direct access, and enables #19 to be implemented in internal/outcome.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-31 03:41:26 +09:00
b25ade5f3d internal/store: rename compat interface
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m17s
Test / Hakurei (push) Successful in 3m9s
Test / Sandbox (race detector) (push) Successful in 4m3s
Test / Hpkg (push) Successful in 4m4s
Test / Flake checks (push) Successful in 1m25s
Test / Hakurei (race detector) (push) Successful in 4m54s
The new store implementation will be exported as Store.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-30 18:53:59 +09:00
ebdcff1049 internal/store: rename from state
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m55s
Test / Flake checks (push) Successful in 1m25s
This reduces collision with local variable names, and generally makes sense for the new store package, since it no longer specifies the state struct.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-30 18:43:55 +09:00
46c5ce4936 internal/outcome/shim: check full behaviour
All checks were successful
Test / Create distribution (push) Successful in 24s
Test / Hakurei (push) Successful in 42s
Test / Sandbox (push) Successful in 38s
Test / Hakurei (race detector) (push) Successful in 42s
Test / Sandbox (race detector) (push) Successful in 38s
Test / Hpkg (push) Successful in 39s
Test / Flake checks (push) Successful in 1m21s
This took significant effort to stub out, and achieves full coverage after c5aefe5e9d.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-30 05:20:49 +09:00
36f8064905 internal/outcome/process: output via msg
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 3m57s
Test / Sandbox (race detector) (push) Successful in 4m8s
Test / Hakurei (race detector) (push) Successful in 4m54s
Test / Flake checks (push) Successful in 1m27s
This makes it possible to instrument output behaviour through stub.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-30 03:41:38 +09:00
eeb9f98e5b internal/outcome/shim: move signal constants
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m17s
Test / Hpkg (push) Successful in 4m11s
Test / Sandbox (race detector) (push) Successful in 4m16s
Test / Hakurei (race detector) (push) Successful in 5m1s
Test / Flake checks (push) Successful in 1m30s
The magic numbers hurt readability.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-30 01:20:51 +09:00
3f9f331501 internal/outcome/shim: remove noop resume
All checks were successful
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m54s
Test / Flake checks (push) Successful in 1m27s
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m11s
Test / Hpkg (push) Successful in 3m59s
The shim does not suspend output to begin with. These are leftovers from when container startup code suspends output.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-29 23:31:39 +09:00
2563391086 internal/outcome/shim: params check early
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m16s
Test / Hakurei (push) Successful in 3m18s
Test / Hpkg (push) Successful in 4m0s
Test / Hakurei (race detector) (push) Successful in 4m56s
Test / Sandbox (race detector) (push) Successful in 4m11s
Test / Flake checks (push) Successful in 1m29s
This is unreachable, but keeping it here as a failsafe until more test cases are added.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-29 23:10:12 +09:00
a0b4e47acc internal/outcome: rename from app
All checks were successful
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m55s
Test / Flake checks (push) Successful in 1m27s
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m11s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 4m1s
This is less ambiguous, and more accurately describes the purpose of the package.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-29 04:33:13 +09:00
a52f7038e5 internal/env: relocate from app
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m8s
Test / Hakurei (push) Successful in 3m10s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m53s
Test / Flake checks (push) Successful in 1m27s
This package is much cleaner to stub independently, and makes no sense to lump into app.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-29 04:11:49 +09:00
274686d10d internal/validate: relocate from app
All checks were successful
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m23s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 4m7s
Test / Sandbox (race detector) (push) Successful in 4m11s
Test / Hakurei (race detector) (push) Successful in 5m1s
Test / Flake checks (push) Successful in 1m30s
These are free of the dispatcher from internal/app. This change relocates them into their own package.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-29 03:40:09 +09:00
65342d588f internal/app/state: improve store internals
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m6s
Test / Hakurei (race detector) (push) Successful in 4m50s
Test / Flake checks (push) Successful in 1m27s
This fully exposes the store internals for #19 and are final preparations for removing the legacy store interface.

This change also fixes a potential deadlock in the handle initialisation mkdir failure path. This however is never reachable in hakurei as the store is never accessed concurrently.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-29 03:21:00 +09:00
5e5826459e internal/app/state: improve handles internals
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m8s
Test / Hakurei (push) Successful in 3m10s
Test / Hpkg (push) Successful in 3m56s
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m53s
Test / Flake checks (push) Successful in 1m31s
This replaces the Store interface with something better reflecting the underlying data format for #19. An implementation of Store is provided on top of the new code to ease transition.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-28 22:00:54 +09:00
4a463b7f03 internal/app/state: use absolute pathnames
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 3m56s
Test / Sandbox (race detector) (push) Successful in 4m6s
Test / Hakurei (race detector) (push) Successful in 4m48s
Test / Flake checks (push) Successful in 1m26s
This is less error-prone and fits better into internal/app which already uses check.Absolute for all pathnames.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-26 03:41:19 +09:00
dacd9550e0 internal/app/state: acquire big lock for toplevel operations
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 3m5s
Test / Hpkg (push) Successful in 3m54s
Test / Sandbox (race detector) (push) Successful in 4m3s
Test / Hakurei (race detector) (push) Successful in 4m50s
Test / Flake checks (push) Successful in 1m24s
This avoids getting into an inconsistent state for simultaneous calls to List and Do on a previously unknown identity.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-26 03:27:56 +09:00
546b00429f treewide: update doc comments
All checks were successful
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Flake checks (push) Successful in 1m29s
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m22s
Test / Hakurei (push) Successful in 3m10s
Test / Hpkg (push) Successful in 3m58s
Test / Hakurei (race detector) (push) Successful in 4m57s
Some internal/app/state types were relocated to hst as part of the API. This change updates doc comments referring to them.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-26 03:00:04 +09:00
86f4219062 internal/app/state/data: check full entry behaviour
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m5s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m13s
Test / Hakurei (race detector) (push) Successful in 4m55s
Test / Flake checks (push) Successful in 1m29s
This eventually gets relocated to internal/app.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-26 01:49:14 +09:00
fe2929d5f7 internal/app/state: include et header
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m17s
Test / Hakurei (push) Successful in 3m5s
Test / Hpkg (push) Successful in 3m55s
Test / Sandbox (race detector) (push) Successful in 4m2s
Test / Hakurei (race detector) (push) Successful in 4m49s
Test / Flake checks (push) Successful in 1m22s
This is the initial step of implementing #19.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-25 22:01:26 +09:00
470e545d27 internal/app/state: use internal/lockedfile
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m11s
Test / Hpkg (push) Successful in 4m0s
Test / Sandbox (race detector) (push) Successful in 4m4s
Test / Hakurei (race detector) (push) Successful in 4m52s
Test / Flake checks (push) Successful in 1m30s
This is a pretty solid implementation backed by robust tests, with a much cleaner interface.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-25 21:29:24 +09:00
8d3381821f internal/app/state: export correct backend value
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m17s
Test / Hakurei (push) Successful in 3m7s
Test / Sandbox (race detector) (push) Successful in 3m52s
Test / Hpkg (push) Successful in 3m59s
Test / Hakurei (race detector) (push) Successful in 4m46s
Test / Flake checks (push) Successful in 1m25s
This references the underlying multiBackend due to a typo, making the whole dance with c a noop.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-25 21:11:05 +09:00
e9d00b9071 container/executable: handle nil msg
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m4s
Test / Sandbox (race detector) (push) Successful in 3m59s
Test / Hpkg (push) Successful in 4m2s
Test / Hakurei (race detector) (push) Successful in 4m45s
Test / Flake checks (push) Successful in 1m37s
This is useful in some tests.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-25 21:08:54 +09:00
4f41afee0f internal/app/state: fixed size et-only header
All checks were successful
Test / Create distribution (push) Successful in 46s
Test / Sandbox (push) Successful in 2m29s
Test / Hakurei (push) Successful in 3m26s
Test / Sandbox (race detector) (push) Successful in 4m15s
Test / Hpkg (push) Successful in 4m14s
Test / Hakurei (race detector) (push) Successful in 5m3s
Test / Flake checks (push) Successful in 1m21s
This header improves the robustness of the format and significantly reduces cleanup overhead.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-25 19:15:06 +09:00
7de593e816 cmd/hakurei: short identifier from lower half
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 40s
Test / Hakurei (push) Successful in 2m14s
Test / Hakurei (race detector) (push) Successful in 2m57s
Test / Hpkg (push) Successful in 3m12s
Test / Flake checks (push) Successful in 1m25s
The upper half is now a nanosecond timestamp. Lower half is still random bytes, so use lower half for short identifier.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-24 00:47:39 +09:00
2442eda8d9 hst/instance: embed config struct
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 41s
Test / Sandbox (race detector) (push) Successful in 40s
Test / Hakurei (push) Successful in 2m20s
Test / Hakurei (race detector) (push) Successful in 2m59s
Test / Hpkg (push) Successful in 3m20s
Test / Flake checks (push) Successful in 1m28s
This makes the resulting json easier to parse since it can now be deserialised into the config struct.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-24 00:42:16 +09:00
05488bfb8f hst/instance: store priv side pid
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m8s
Test / Sandbox (race detector) (push) Successful in 3m58s
Test / Hpkg (push) Successful in 4m1s
Test / Hakurei (race detector) (push) Successful in 4m44s
Test / Flake checks (push) Successful in 1m29s
This can receive signals, so is more useful to the caller.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-23 23:19:55 +09:00
dd94818f20 hst/instance: define instance state
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m6s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m5s
Test / Hakurei (race detector) (push) Successful in 4m51s
Test / Flake checks (push) Successful in 1m30s
This is now part of the hst API. This change also improves identifier generation and serialisation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-23 22:59:02 +09:00
0fd357e7f6 container/init: do not suspend output
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Hakurei (push) Successful in 42s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Hpkg (push) Successful in 41s
Test / Flake checks (push) Successful in 1m20s
Init is not very talkative after process start even when verbose. Suspending output here is pointless and does more harm than good.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-23 08:11:00 +09:00
57231d4acf container/init: improve signal handling
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m9s
Test / Sandbox (race detector) (push) Successful in 3m57s
Test / Hpkg (push) Successful in 3m58s
Test / Hakurei (race detector) (push) Successful in 4m43s
Test / Flake checks (push) Successful in 1m30s
The SIGTERM signal is delivered in many other cases and can lead to strange behaviour. The unconditional resume of the logger also causes strange behaviour in the cancellation forwarding path. This change also passes through additional signals.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-23 08:02:03 +09:00
c5aefe5e9d internal/app/shim: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Hakurei (push) Successful in 44s
Test / Hpkg (push) Successful in 41s
Test / Flake checks (push) Successful in 1m20s
This does not yet have full coverage. Test cases covering failsafe paths and error injection will be added eventually.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-23 06:07:41 +09:00
0f8ffee44d internal/app: test case for hst template
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m8s
Test / Hakurei (push) Successful in 3m5s
Test / Hpkg (push) Successful in 3m58s
Test / Sandbox (race detector) (push) Successful in 4m2s
Test / Hakurei (race detector) (push) Successful in 4m42s
Test / Flake checks (push) Successful in 1m21s
This helps with other areas of the test suite as they're all based on hst.Template. This also helps contributors understand the behaviour of internal/app as hst.Template covers almost every aspect of it.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-23 04:46:58 +09:00
1685a4d000 cmd/hsu: reduce excessive test range
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m40s
Test / Sandbox (race detector) (push) Successful in 2m25s
Test / Hakurei (push) Successful in 2m36s
Test / Hakurei (race detector) (push) Successful in 3m13s
Test / Hpkg (push) Successful in 3m33s
Test / Flake checks (push) Successful in 1m24s
This is quite a simple piece of code, this many test cases is excessive and wastes time in the integration vm.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-23 04:32:32 +09:00
6c338b433a internal/app: reduce test case indentation
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m9s
Test / Sandbox (race detector) (push) Successful in 4m3s
Test / Hpkg (push) Successful in 4m4s
Test / Hakurei (race detector) (push) Successful in 4m44s
Test / Flake checks (push) Successful in 1m28s
This improves readability on narrower displays.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-22 07:40:32 +09:00
8accd3b219 internal/app/shim: use syscall dispatcher
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m9s
Test / Sandbox (race detector) (push) Successful in 3m58s
Test / Hpkg (push) Successful in 4m5s
Test / Hakurei (race detector) (push) Successful in 4m46s
Test / Flake checks (push) Successful in 1m28s
This enables instrumented testing of the shim.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-22 06:58:45 +09:00
c5f59c5488 container/syscall: export prctl wrapper
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m3s
Test / Sandbox (race detector) (push) Successful in 3m58s
Test / Hpkg (push) Successful in 4m4s
Test / Hakurei (race detector) (push) Successful in 4m46s
Test / Flake checks (push) Successful in 1m27s
This is useful as package "syscall" does not provide such a wrapper. This change also improves error handling to fully conform to the manpage.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-22 05:26:54 +09:00
fcd9becf9a cmd/hsu: run in locked thread
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m41s
Test / Sandbox (race detector) (push) Successful in 2m23s
Test / Hakurei (push) Successful in 2m35s
Test / Hakurei (race detector) (push) Successful in 3m14s
Test / Hpkg (push) Successful in 3m39s
Test / Flake checks (push) Successful in 1m27s
Goroutine scheduling is not helpful in the setuid wrapper, it is not particularly harmful but lock here anyway.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-22 05:09:08 +09:00
622f945c22 container/init: check msg in entrypoint
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m10s
Test / Sandbox (race detector) (push) Successful in 3m59s
Test / Hpkg (push) Successful in 4m8s
Test / Hakurei (race detector) (push) Successful in 4m46s
Test / Flake checks (push) Successful in 1m27s
This covers invalid call to Init.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-22 04:20:08 +09:00
e94acc424c container/comp: rename from bits
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 3m53s
Test / Sandbox (race detector) (push) Successful in 4m2s
Test / Hakurei (race detector) (push) Successful in 4m43s
Test / Flake checks (push) Successful in 1m23s
This package will also hold syscall lookup tables for seccomp.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-21 20:54:03 +09:00
b1a4d801be hst/container: flags string representation
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m9s
Test / Sandbox (race detector) (push) Successful in 3m56s
Test / Hpkg (push) Successful in 4m5s
Test / Hakurei (race detector) (push) Successful in 4m42s
Test / Hakurei (push) Successful in 2m9s
Test / Flake checks (push) Successful in 1m28s
This is useful for a user-facing representation other than JSON. This also gets rid of the ugly, outdated flags string builder in cmd/hakurei.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-21 20:29:52 +09:00
56beae17fe test: assert hst CGO_ENABLED=0
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 40s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Hpkg (push) Successful in 41s
Test / Hakurei (push) Successful in 2m29s
Test / Hakurei (race detector) (push) Successful in 3m7s
Test / Flake checks (push) Successful in 1m24s
The hst package only deals with data serialisation, however since many parts of hakurei make use of C libraries in some way it can be easy to inadvertently depend on cgo.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-21 19:49:04 +09:00
ea978101b1 cmd/hakurei/parse: close config fd
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m5s
Test / Sandbox (race detector) (push) Successful in 3m54s
Test / Hpkg (push) Successful in 3m57s
Test / Hakurei (race detector) (push) Successful in 4m43s
Test / Flake checks (push) Successful in 1m20s
This is cleaner than relying on the finalizer.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-21 06:05:36 +09:00
fbd1638e7f test/interactive/trace: update nix attribute
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 40s
Test / Sandbox (race detector) (push) Successful in 40s
Test / Hakurei (race detector) (push) Successful in 44s
Test / Hakurei (push) Successful in 45s
Test / Hpkg (push) Successful in 42s
Test / Flake checks (push) Successful in 1m28s
Updated according to evaluation warning.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-21 06:03:09 +09:00
d42067df7c cmd/hakurei/json: friendly error messages
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 39s
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 1m23s
This change handles errors returned by encoding/json and prints significantly cleaner messages.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-21 05:17:25 +09:00
b9459a80c7 container/init: check use constants for open flags
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m11s
Test / Hakurei (push) Successful in 3m8s
Test / Sandbox (race detector) (push) Successful in 3m58s
Test / Hpkg (push) Successful in 4m6s
Test / Hakurei (race detector) (push) Successful in 4m45s
Test / Flake checks (push) Successful in 1m28s
These bits are arch-specific.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-21 03:13:58 +09:00
f8189d1488 container/syscall: dot-import syscall
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m7s
Test / Hpkg (push) Successful in 3m57s
Test / Sandbox (race detector) (push) Successful in 4m2s
Test / Hakurei (race detector) (push) Successful in 4m44s
Test / Flake checks (push) Successful in 1m38s
This avoids having arch-specific constants for arm64.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-21 03:09:14 +09:00
5063b774c1 hst: expose version string
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m6s
Test / Hakurei (push) Successful in 3m0s
Test / Hpkg (push) Successful in 3m56s
Test / Sandbox (race detector) (push) Successful in 4m0s
Test / Hakurei (race detector) (push) Successful in 4m44s
Test / Flake checks (push) Successful in 1m20s
The hst API is tied to this version string.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-21 01:56:44 +09:00
766dd89ffa internal: clean up build strings
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m5s
Test / Hpkg (push) Successful in 4m4s
Test / Sandbox (race detector) (push) Successful in 4m9s
Test / Hakurei (race detector) (push) Successful in 4m46s
Test / Flake checks (push) Successful in 1m30s
These names are less ambiguous and should be understandable without reading the source code.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-21 01:49:36 +09:00
699c19e972 hst/container: optional runtime and tmpdir sharing
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Hakurei (push) Successful in 42s
Test / Hpkg (push) Successful in 40s
Test / Hakurei (race detector) (push) Successful in 44s
Test / Flake checks (push) Successful in 1m23s
Sharing and persisting these directories do not always make sense. Make it optional here.

Closes #16.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-19 04:11:38 +09:00
b5b30aea2e test: place marker in common path
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Sandbox (push) Successful in 41s
Test / Hakurei (race detector) (push) Successful in 45s
Test / Hpkg (push) Successful in 42s
Test / Hakurei (push) Successful in 46s
Test / Flake checks (push) Successful in 1m33s
This discontinues the dependency on shared tmpdir and xdg_runtime_dir implementation detail, for #16.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-19 03:50:48 +09:00
c0e860000a internal/app: remove spfinal
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 1m39s
Test / Sandbox (race detector) (push) Successful in 4m3s
Test / Hpkg (push) Successful in 4m12s
Test / Hakurei (race detector) (push) Successful in 4m10s
Test / Hakurei (push) Successful in 4m9s
Test / Flake checks (push) Successful in 1m36s
This no longer needs to be an independent outcomeOp since spFilesystemOp is moved late.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-19 02:58:46 +09:00
d87020f0ca hst/config: validate env early
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m8s
Test / Sandbox (race detector) (push) Successful in 3m58s
Test / Hpkg (push) Successful in 4m3s
Test / Hakurei (race detector) (push) Successful in 4m44s
Test / Flake checks (push) Successful in 1m26s
This should happen in hst since it requires no system state.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-19 02:39:49 +09:00
e47aebb7a0 internal/app/outcome: apply configured filesystems late
All checks were successful
Test / Create distribution (push) Successful in 27s
Test / Sandbox (push) Successful in 1m42s
Test / Hakurei (push) Successful in 2m37s
Test / Hpkg (push) Successful in 3m33s
Test / Sandbox (race detector) (push) Successful in 4m10s
Test / Hakurei (race detector) (push) Successful in 4m49s
Test / Flake checks (push) Successful in 1m29s
This enables configured filesystems to cover system mount points.

Closes #8.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-19 01:41:52 +09:00
543bf69102 internal/app/spx11: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m16s
Test / Hakurei (push) Successful in 3m7s
Test / Sandbox (race detector) (push) Successful in 4m0s
Test / Hpkg (push) Successful in 3m59s
Test / Hakurei (race detector) (push) Successful in 4m47s
Test / Flake checks (push) Successful in 1m29s
This outcomeOp will likely never change.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-19 01:00:12 +09:00
4cfb1fda8f internal/app/spwayland: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m5s
Test / Hpkg (push) Successful in 3m57s
Test / Sandbox (race detector) (push) Successful in 4m3s
Test / Hakurei (race detector) (push) Successful in 4m45s
Test / Flake checks (push) Successful in 1m28s
This op is quite clean. Might get slightly more complex at some point passing socket fd.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-19 00:30:56 +09:00
c12183959a internal/app/dispatcher: report correct field
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m5s
Test / Sandbox (race detector) (push) Successful in 3m59s
Test / Hpkg (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m51s
Test / Flake checks (push) Successful in 1m30s
This was mistakenly reporting sharePath on inequivalence causing very confusing output.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 23:59:10 +09:00
f5845e312e internal/app/sptmpdir: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m6s
Test / Sandbox (race detector) (push) Successful in 3m58s
Test / Hpkg (push) Successful in 3m59s
Test / Hakurei (race detector) (push) Successful in 4m43s
Test / Flake checks (push) Successful in 1m27s
Another simple one. This will change when shared tmpdir and xdg runtime dir becomes optional.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 23:46:10 +09:00
a103c4a7c7 internal/app/hsu: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m4s
Test / Hpkg (push) Successful in 4m0s
Test / Sandbox (race detector) (push) Successful in 4m3s
Test / Hakurei (race detector) (push) Successful in 4m44s
Test / Flake checks (push) Successful in 1m22s
The stub exec.ExitError is hairy as usual, but internal/app is not cross-platform, so this is okay.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 20:45:42 +09:00
67ec82ae1b ldd/exec: raise timeout
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m4s
Test / Sandbox (race detector) (push) Successful in 3m58s
Test / Hpkg (push) Successful in 3m58s
Test / Hakurei (race detector) (push) Successful in 6m9s
Test / Flake checks (push) Successful in 1m28s
This mostly helps with tests.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 18:03:09 +09:00
f6f0cb56ae internal/app/hsu: remove wrapper method
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m11s
Test / Sandbox (race detector) (push) Successful in 3m53s
Test / Hpkg (push) Successful in 3m54s
Test / Hakurei (race detector) (push) Successful in 4m43s
Test / Hakurei (push) Successful in 2m13s
Test / Flake checks (push) Successful in 1m27s
This was added to reduce the size of diffs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 17:35:20 +09:00
d4284c109d internal/app/spruntime: emulate pam_systemd type
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Hakurei (push) Successful in 44s
Test / Hakurei (race detector) (push) Successful in 44s
Test / Hpkg (push) Successful in 42s
Test / Sandbox (push) Successful in 1m42s
Test / Sandbox (race detector) (push) Successful in 2m29s
Test / Flake checks (push) Successful in 1m22s
This sets XDG_SESSION_TYPE to the corresponding values specified in pam_systemd(8) according to enablements.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 04:33:04 +09:00
030ad2a73b internal/app/spruntime: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 38s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 3m9s
Test / Sandbox (race detector) (push) Successful in 4m2s
Test / Hpkg (push) Successful in 4m11s
Test / Hakurei (race detector) (push) Successful in 4m48s
Test / Flake checks (push) Successful in 1m25s
This one is quite simple and has no state. Needs to emulate pam_systemd behaviour so that will change.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 03:41:49 +09:00
78d7955abd internal/app/sppulse: check cookie discovery
All checks were successful
Test / Create distribution (push) Successful in 48s
Test / Sandbox (push) Successful in 2m22s
Test / Hakurei (push) Successful in 3m17s
Test / Sandbox (race detector) (push) Successful in 4m13s
Test / Hpkg (push) Successful in 4m18s
Test / Hakurei (race detector) (push) Successful in 5m0s
Test / Flake checks (push) Successful in 1m37s
There's quite a bit of code duplication here, but since this is already quite simple it is best to leave it as is for now.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 01:30:33 +09:00
b066495a7d internal/app/sppulse: check buf error injection
All checks were successful
Test / Create distribution (push) Successful in 54s
Test / Hpkg (push) Successful in 4m16s
Test / Sandbox (push) Successful in 1m45s
Test / Hakurei (push) Successful in 2m27s
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m53s
Test / Flake checks (push) Successful in 1m38s
The loadFile behaviour does not guarantee the buffer to be zeroed or not clobbered if an error is returned, but for the current implementation it is good to check.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 01:01:52 +09:00
82299d34c6 internal/app/sppulse: correctly handle small cookie
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m6s
Test / Sandbox (race detector) (push) Successful in 3m55s
Test / Hpkg (push) Successful in 4m8s
Test / Hakurei (race detector) (push) Successful in 4m46s
Test / Flake checks (push) Successful in 1m19s
The trailing zero bytes need to be sliced off, so send cookie size alongside buffer content.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-17 08:03:03 +09:00
792013cefb internal/app/sppulse: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m5s
Test / Sandbox (race detector) (push) Successful in 3m58s
Test / Hpkg (push) Successful in 4m9s
Test / Hakurei (race detector) (push) Successful in 4m42s
Test / Flake checks (push) Successful in 1m27s
Still needs to check the relocated functions separately.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-17 06:32:21 +09:00
3f39132935 internal/app/dispatcher: reduce check code duplication
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m6s
Test / Hakurei (push) Successful in 3m3s
Test / Sandbox (race detector) (push) Successful in 3m56s
Test / Hpkg (push) Successful in 3m58s
Test / Hakurei (race detector) (push) Successful in 4m42s
Test / Flake checks (push) Successful in 1m28s
This also improves readability of test cases.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-17 05:47:12 +09:00
c922c3f80e internal/app/sppulse: relocate hard to test code
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m1s
Test / Sandbox (race detector) (push) Successful in 3m59s
Test / Hpkg (push) Successful in 4m8s
Test / Hakurei (race detector) (push) Successful in 4m48s
Test / Flake checks (push) Successful in 1m19s
These are better tested separately instead of creating many op test cases.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-16 05:47:49 +09:00
6cf58ca1b3 internal/app/spfinal: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m2s
Test / Hpkg (push) Successful in 3m56s
Test / Sandbox (race detector) (push) Successful in 4m1s
Test / Hakurei (race detector) (push) Successful in 4m45s
Test / Flake checks (push) Successful in 1m25s
This will be merged with spFilesystemOp eventually.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-16 02:08:31 +09:00
425421d9b1 hst/container: rename constants
All checks were successful
Test / Create distribution (push) Successful in 1m16s
Test / Sandbox (push) Successful in 3m4s
Test / Hakurei (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m50s
Test / Hpkg (push) Successful in 5m4s
Test / Hakurei (race detector) (push) Successful in 5m38s
Test / Flake checks (push) Successful in 1m30s
The shim is an implementation detail and should not be mentioned in the API.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-16 00:27:00 +09:00
5e0f15d76b hst/container: additional shim exit codes
All checks were successful
Test / Create distribution (push) Successful in 57s
Test / Sandbox (push) Successful in 4m26s
Test / Sandbox (race detector) (push) Successful in 6m36s
Test / Hakurei (push) Successful in 6m58s
Test / Hakurei (race detector) (push) Successful in 8m54s
Test / Hpkg (push) Successful in 9m13s
Test / Flake checks (push) Successful in 3m13s
These are now considered stable, defined behaviour and can be used by external programs to determine shim outcome.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-15 22:09:33 +09:00
ae65491223 container/init: use one channel for wait4
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 3m12s
Test / Hpkg (push) Successful in 4m3s
Test / Sandbox (race detector) (push) Successful in 4m6s
Test / Hakurei (race detector) (push) Successful in 4m51s
Test / Flake checks (push) Successful in 1m31s
When using two channels it is possible for the other case to be reached before all pending winfo are consumed, causing incorrect reporting.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-15 21:35:19 +09:00
52e3324ef4 test/sandbox: ignore nondeterministic mount point
All checks were successful
Test / Create distribution (push) Successful in 27s
Test / Sandbox (race detector) (push) Successful in 42s
Test / Sandbox (push) Successful in 43s
Test / Hakurei (race detector) (push) Successful in 46s
Test / Hpkg (push) Successful in 43s
Test / Hakurei (push) Successful in 47s
Test / Flake checks (push) Successful in 1m30s
No idea what systemd is doing with this to cause its options to change.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-14 07:08:39 +09:00
f95e0a7568 hst/config: hold acl struct by value
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (race detector) (push) Successful in 4m6s
Test / Hpkg (push) Successful in 4m12s
Test / Hakurei (race detector) (push) Successful in 4m46s
Test / Sandbox (push) Successful in 1m22s
Test / Hakurei (push) Successful in 2m18s
Test / Flake checks (push) Successful in 1m37s
Doc comments are also reworded for clarity.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-14 07:02:14 +09:00
4c647add0d hst/container: pack boolean options
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 4m2s
Test / Hakurei (race detector) (push) Successful in 4m46s
Test / Sandbox (race detector) (push) Successful in 2m11s
Test / Flake checks (push) Successful in 1m37s
The memory saving is relatively insignificant, however this increases serialisation efficiency.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-14 06:39:00 +09:00
a341466942 hst: separate container config
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m11s
Test / Hakurei (push) Successful in 3m7s
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hpkg (push) Successful in 4m9s
Test / Hakurei (race detector) (push) Successful in 4m47s
Test / Flake checks (push) Successful in 1m31s
The booleans are getting packed into a single field. This requires non-insignificant amount of code for JSON serialisation to stay compatible.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-14 04:23:05 +09:00
e4ee8df83c internal/app/spdbus: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m16s
Test / Sandbox (race detector) (push) Successful in 4m2s
Test / Hpkg (push) Successful in 4m13s
Test / Hakurei (race detector) (push) Successful in 4m47s
Test / Hakurei (push) Successful in 2m11s
Test / Flake checks (push) Successful in 1m30s
This is not done very cleanly, however this op is pending removal for the in-process dbus proxy so not worth spending too much effort here. As long as it checks all paths it is good enough.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-14 01:51:01 +09:00
048c1957f1 helper/args: variadic check function
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 1m30s
Test / Hakurei (push) Successful in 2m21s
Test / Hpkg (push) Successful in 3m23s
Test / Sandbox (race detector) (push) Successful in 4m1s
Test / Hakurei (race detector) (push) Successful in 4m46s
Test / Flake checks (push) Successful in 1m27s
This package turns out to be much less widely used than anticipated, and might be facing removal. This change makes test cases cleaner.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-14 01:48:56 +09:00
790d77075e system/dbus: remove builder state leak
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (race detector) (push) Successful in 3m56s
Test / Hpkg (push) Successful in 4m2s
Test / Hakurei (race detector) (push) Successful in 4m44s
Test / Sandbox (push) Successful in 1m23s
Test / Hakurei (push) Successful in 2m14s
Test / Flake checks (push) Successful in 1m26s
This enables external testing of system.I state.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-14 01:33:44 +09:00
e5ff40e7d3 container: synchronise after notify
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m18s
Test / Hakurei (push) Successful in 3m7s
Test / Sandbox (race detector) (push) Successful in 3m59s
Test / Hpkg (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m45s
Test / Flake checks (push) Successful in 1m23s
This should eliminate intermittent failures in the forward test.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-13 19:17:19 +09:00
123d7fbfd5 container/seccomp: remove export pipe
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m11s
Test / Sandbox (race detector) (push) Successful in 4m2s
Test / Hpkg (push) Successful in 4m19s
Test / Hakurei (race detector) (push) Successful in 4m47s
Test / Hakurei (push) Successful in 2m13s
Test / Flake checks (push) Successful in 1m32s
This was only useful when wrapping bwrap.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-13 18:51:35 +09:00
7638a44fa6 treewide: parallel tests
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Hakurei (push) Successful in 44s
Test / Sandbox (push) Successful in 41s
Test / Hakurei (race detector) (push) Successful in 44s
Test / Sandbox (race detector) (push) Successful in 41s
Test / Hpkg (push) Successful in 41s
Test / Flake checks (push) Successful in 1m24s
Most tests already had no global state, however parallel was never enabled. This change enables it for all applicable tests.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-13 04:38:48 +09:00
a14b6535a6 helper/stub: write ready byte late
All checks were successful
Test / Create distribution (push) Successful in 27s
Test / Sandbox (race detector) (push) Successful in 41s
Test / Sandbox (push) Successful in 41s
Test / Hakurei (push) Successful in 44s
Test / Hakurei (race detector) (push) Successful in 44s
Test / Hpkg (push) Successful in 42s
Test / Flake checks (push) Successful in 1m30s
Hopefully eliminates spurious failures.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-13 01:55:44 +09:00
763ab27e09 system: remove tmpfiles
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m33s
Test / Hakurei (race detector) (push) Successful in 5m21s
Test / Flake checks (push) Successful in 1m32s
This is no longer used.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-13 01:12:44 +09:00
bff2a1e748 container/initplace: remove indirect method
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m30s
Test / Sandbox (push) Successful in 1m24s
Test / Hakurei (race detector) (push) Successful in 5m20s
Test / Hakurei (push) Successful in 2m13s
Test / Flake checks (push) Successful in 1m29s
This is no longer useful and is highly error-prone.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-13 01:06:45 +09:00
8a91234cb4 hst: reword and improve doc comments
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m9s
Test / Hpkg (push) Successful in 3m58s
Test / Sandbox (race detector) (push) Successful in 4m31s
Test / Hakurei (race detector) (push) Successful in 5m19s
Test / Hakurei (push) Successful in 2m12s
Test / Flake checks (push) Successful in 1m31s
This corrects minor mistakes in doc comments and adds them for undocumented constants.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-12 05:03:14 +09:00
db7051a368 internal/app/spcontainer: check fs init behaviour
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 3m53s
Test / Sandbox (race detector) (push) Successful in 4m34s
Test / Sandbox (push) Successful in 1m21s
Test / Hakurei (race detector) (push) Successful in 5m22s
Test / Flake checks (push) Successful in 1m34s
This covers every statement. Some of them are unreachable unless the kernel returns garbage.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-12 03:58:53 +09:00
36f312b3ba internal/app/spcontainer: resolve path through dispatcher
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m13s
Test / Hpkg (push) Successful in 4m2s
Test / Hakurei (race detector) (push) Successful in 5m23s
Test / Hakurei (push) Successful in 2m14s
Test / Sandbox (race detector) (push) Successful in 2m7s
Test / Flake checks (push) Successful in 1m32s
This prevents state from os tainting the test data.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-11 20:20:41 +09:00
037144b06e system/dbus: use well-known address in spec
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m13s
Test / Hpkg (push) Successful in 4m8s
Test / Hakurei (race detector) (push) Successful in 5m26s
Test / Hakurei (push) Successful in 2m14s
Test / Sandbox (race detector) (push) Successful in 2m4s
Test / Flake checks (push) Successful in 1m32s
The session bus still performs non-standard formatting since it makes no sense for hakurei to start the session bus.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-11 18:52:06 +09:00
f5a597c406 hst: rename /.hakurei constant
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m3s
Test / Hpkg (push) Successful in 3m57s
Test / Sandbox (race detector) (push) Successful in 4m30s
Test / Hakurei (race detector) (push) Successful in 5m16s
Test / Flake checks (push) Successful in 1m20s
This provides disambiguation from fhs.AbsTmp.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-11 14:32:35 +09:00
8874aaf81b hst: remove template bind nix store
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 3m5s
Test / Hpkg (push) Successful in 3m59s
Test / Sandbox (race detector) (push) Successful in 4m35s
Test / Hakurei (race detector) (push) Successful in 5m25s
Test / Flake checks (push) Successful in 1m28s
This does not add anything meaningful to the template, since there are already prior examples showing src-only bind ops. Remove this since it causes confusion by covering the previous mount point targeting /nix/store.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-11 13:59:10 +09:00
04a27c8e47 hst: use plausible overlay template
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m11s
Test / Hakurei (push) Successful in 3m6s
Test / Hpkg (push) Successful in 3m57s
Test / Hakurei (race detector) (push) Successful in 5m19s
Test / Sandbox (race detector) (push) Successful in 2m7s
Test / Flake checks (push) Successful in 1m39s
The current value is copied from a test case, and does not resemble its intended use case.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-11 13:51:08 +09:00
9e3df0905b internal/app/spcontainer: check params init behaviour
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Hakurei (push) Successful in 3m6s
Test / Hpkg (push) Successful in 4m4s
Test / Sandbox (push) Successful in 1m21s
Test / Hakurei (race detector) (push) Successful in 5m23s
Test / Sandbox (race detector) (push) Successful in 2m8s
Test / Flake checks (push) Successful in 1m31s
This change also significantly reduces duplicate information in test case.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-11 02:44:02 +09:00
9290748761 internal/app/spaccount: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Hakurei (push) Successful in 3m7s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m27s
Test / Hakurei (race detector) (push) Successful in 5m19s
Test / Sandbox (push) Successful in 1m18s
Test / Flake checks (push) Successful in 1m30s
This begins the effort of fully covering internal/app.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-11 00:54:04 +09:00
23084888a0 internal/app/spaccount: apply default in shim
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m19s
Test / Hpkg (push) Successful in 4m6s
Test / Hakurei (race detector) (push) Successful in 5m20s
Test / Sandbox (race detector) (push) Successful in 2m10s
Test / Hakurei (push) Successful in 2m13s
Test / Flake checks (push) Successful in 1m37s
The original code clobbers hst.Config, and was not changed when being ported over.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-11 00:38:06 +09:00
50f6fcb326 container/stub: mark test overrides as helper
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m35s
Test / Sandbox (push) Successful in 1m24s
Test / Hakurei (race detector) (push) Successful in 5m23s
Test / Hakurei (push) Successful in 2m16s
Test / Flake checks (push) Successful in 1m21s
This fixes line information in test reporting messages.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-10 22:15:20 +09:00
070e346587 internal/app: relocate params state initialisation
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m5s
Test / Hpkg (push) Successful in 4m9s
Test / Hakurei (race detector) (push) Successful in 5m18s
Test / Sandbox (race detector) (push) Successful in 2m9s
Test / Flake checks (push) Successful in 1m40s
This is useful for testing.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-10 22:00:49 +09:00
24de7c50a0 internal/app: relocate state initialisation
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m4s
Test / Hpkg (push) Successful in 4m4s
Test / Sandbox (race detector) (push) Successful in 4m37s
Test / Hakurei (race detector) (push) Successful in 5m18s
Test / Flake checks (push) Successful in 1m28s
This is useful for testing.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-10 20:15:58 +09:00
f6dd9dab6a internal/app: hold path hiding in op
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 4m12s
Test / Sandbox (race detector) (push) Successful in 4m37s
Test / Hakurei (race detector) (push) Successful in 5m21s
Test / Flake checks (push) Successful in 1m34s
This makes no sense to be part of the global state.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-10 19:56:30 +09:00
776650af01 hst/config: negative WaitDelay bypasses default
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m19s
Test / Hpkg (push) Successful in 4m4s
Test / Sandbox (race detector) (push) Successful in 4m44s
Test / Hakurei (race detector) (push) Successful in 5m25s
Test / Hakurei (push) Successful in 2m16s
Test / Flake checks (push) Successful in 1m30s
This behaviour might be useful, so do not lock it out. This change also fixes an oversight where the unchecked value is used to determine ForwardCancel.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-10 05:11:32 +09:00
109aaee659 internal/app: copy parts of config to state
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m7s
Test / Hpkg (push) Successful in 4m4s
Test / Sandbox (race detector) (push) Successful in 4m30s
Test / Hakurei (race detector) (push) Successful in 5m20s
Test / Flake checks (push) Successful in 1m34s
This is less error-prone than passing the address to the entire hst.Config struct, and reduces the likelihood of accidentally clobbering hst.Config. This also improves ease of testing.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-10 03:19:09 +09:00
22ee5ae151 internal/app: filter ops in implementation
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m18s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m28s
Test / Hakurei (race detector) (push) Successful in 5m19s
Test / Hakurei (push) Successful in 2m14s
Test / Flake checks (push) Successful in 1m33s
This is cleaner and less error-prone, and should also result in negligibly less memory allocation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-10 02:23:34 +09:00
4246256d78 internal/app: hold config address in state
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m6s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m32s
Test / Hakurei (race detector) (push) Successful in 5m22s
Test / Flake checks (push) Successful in 1m34s
This can be removed eventually as it is barely used.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-10 01:21:01 +09:00
a941ac025f container/init: unwrap descriptive fatal error
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m6s
Test / Hpkg (push) Successful in 4m0s
Test / Hakurei (race detector) (push) Successful in 5m20s
Test / Sandbox (race detector) (push) Successful in 2m3s
Test / Flake checks (push) Successful in 1m27s
These errors are printed with a descriptive message prefixed to them, so it is more readable to expose the underlying errno.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-09 22:04:35 +09:00
87b5c30ef6 message: relocate from container
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m22s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m28s
Test / Hakurei (race detector) (push) Successful in 5m21s
Test / Hakurei (push) Successful in 2m9s
Test / Flake checks (push) Successful in 1m29s
This package is quite useful. This change allows it to be imported without importing container.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-09 05:18:19 +09:00
df9b77b077 internal/app: do not encode config early
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m11s
Test / Hpkg (push) Successful in 4m10s
Test / Sandbox (race detector) (push) Successful in 4m40s
Test / Hakurei (race detector) (push) Successful in 5m21s
Test / Hakurei (push) Successful in 2m18s
Test / Flake checks (push) Successful in 1m32s
Finalise no longer clobbers hst.Config.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-09 04:38:54 +09:00
a40d182706 internal/app: build container state in shim
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 40s
Test / Hakurei (race detector) (push) Successful in 44s
Test / Hakurei (push) Successful in 44s
Test / Hpkg (push) Successful in 41s
Test / Flake checks (push) Successful in 1m21s
This significantly decreases ipc overhead.

Closes #3.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-08 22:30:40 +09:00
e5baaf416f internal/app: check transmitted ops
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Hakurei (push) Successful in 3m13s
Test / Hpkg (push) Successful in 4m5s
Test / Sandbox (race detector) (push) Successful in 4m28s
Test / Hakurei (race detector) (push) Successful in 5m23s
Test / Sandbox (push) Successful in 1m25s
Test / Flake checks (push) Successful in 1m33s
This simulates params to shim and this is the last step before params to shim is merged.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-08 20:02:09 +09:00
ee6c471fe6 internal/app: relocate ops condition
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m14s
Test / Hpkg (push) Successful in 4m4s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hakurei (race detector) (push) Successful in 5m24s
Test / Hakurei (push) Successful in 2m18s
Test / Flake checks (push) Successful in 1m32s
This allows reuse and finer grained testing of fromConfig.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-08 19:39:00 +09:00
16bf3178d3 internal/app: relocate dynamic exported state
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 3m55s
Test / Sandbox (race detector) (push) Successful in 4m30s
Test / Hakurei (race detector) (push) Successful in 5m18s
Test / Flake checks (push) Successful in 1m30s
This allows reuse of the populateEarly method in test instrumentation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-08 18:34:17 +09:00
034c59a26a internal/app: relocate late sys/params outcome
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m11s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m30s
Test / Hakurei (race detector) (push) Successful in 5m22s
Test / Flake checks (push) Successful in 1m29s
This will end up merged with another op after reordering. For now relocate it into its dedicated op for test instrumentation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-08 18:26:50 +09:00
5bf28901a4 cmd/hsu: check against setgid bit
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m10s
Test / Hpkg (push) Successful in 4m5s
Test / Sandbox (race detector) (push) Successful in 4m33s
Test / Hakurei (race detector) (push) Successful in 5m20s
Test / Hakurei (push) Successful in 2m18s
Test / Flake checks (push) Successful in 1m31s
The getgroups behaviour is already checked for, but it never hurts to be more careful in a setuid program.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-08 18:22:24 +09:00
9b507715d4 hst/dbus: validate interface strings
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m3s
Test / Hpkg (push) Successful in 3m58s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 5m11s
Test / Flake checks (push) Successful in 1m22s
This is relocated to hst to validate early.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-08 04:57:22 +09:00
12ab7ea3b4 hst/fs: access ops through interface
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Hakurei (push) Successful in 3m14s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m28s
Test / Hakurei (race detector) (push) Successful in 5m22s
Test / Sandbox (push) Successful in 1m28s
Test / Flake checks (push) Successful in 1m29s
This removes the final hakurei.app/container import from hst.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-07 23:59:48 +09:00
1f0226f7e0 container/check: relocate overlay escape
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m31s
Test / Hakurei (race detector) (push) Successful in 5m25s
Test / Flake checks (push) Successful in 1m40s
This is used in hst to format strings.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-07 23:56:19 +09:00
584ce3da68 container/bits: move bind bits
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 4m14s
Test / Sandbox (race detector) (push) Successful in 4m29s
Test / Hakurei (race detector) (push) Successful in 5m21s
Test / Flake checks (push) Successful in 1m31s
This allows referring to the bits without importing container.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-07 21:38:31 +09:00
5d18af0007 container/fhs: move pathname constants
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m6s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m29s
Test / Hakurei (race detector) (push) Successful in 3m5s
Test / Hakurei (push) Successful in 2m10s
Test / Flake checks (push) Successful in 1m21s
This allows referencing FHS pathnames without importing container.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-07 21:29:16 +09:00
0e6c1a5026 container/check: move absolute pathname
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Hpkg (push) Successful in 4m3s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hakurei (race detector) (push) Successful in 5m19s
Test / Sandbox (push) Successful in 1m28s
Test / Hakurei (push) Successful in 2m16s
Test / Flake checks (push) Successful in 1m37s
This allows use of absolute pathname values without importing container.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-07 20:57:58 +09:00
d23b4dc9e6 hst/dbus: move dbus config struct
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m11s
Test / Hakurei (push) Successful in 3m12s
Test / Hpkg (push) Successful in 4m0s
Test / Hakurei (race detector) (push) Successful in 5m20s
Test / Sandbox (race detector) (push) Successful in 2m11s
Test / Flake checks (push) Successful in 1m31s
This allows holding a xdg-dbus-proxy configuration without importing system/dbus.

It also makes more sense in the project structure since the config struct is part of the hst API however the rest of the implementation is not.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-07 19:03:51 +09:00
3ce63e95d7 container: move seccomp preset bits
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m13s
Test / Hpkg (push) Successful in 4m2s
Test / Hakurei (race detector) (push) Successful in 5m16s
Test / Sandbox (race detector) (push) Successful in 2m5s
Test / Hakurei (push) Successful in 2m16s
Test / Flake checks (push) Successful in 1m33s
This allows holding the bits without cgo.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-07 18:28:20 +09:00
2489766efe hst/config: identity bounds check early
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m4s
Test / Hpkg (push) Successful in 3m53s
Test / Sandbox (race detector) (push) Successful in 4m28s
Test / Hakurei (race detector) (push) Successful in 5m16s
Test / Flake checks (push) Successful in 1m30s
This makes sense to do here instead of in internal/app.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-07 17:58:28 +09:00
9e48d7f562 hst/config: move container fields from toplevel
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m7s
Test / Hpkg (push) Successful in 3m54s
Test / Hakurei (race detector) (push) Successful in 5m18s
Test / Sandbox (race detector) (push) Successful in 2m10s
Test / Hakurei (push) Successful in 2m13s
Test / Flake checks (push) Successful in 1m33s
This change also moves pd behaviour to cmd/hakurei, as this does not belong in the hst API.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-07 04:24:45 +09:00
f280994957 internal/app: check nscd socket for path hiding
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Hakurei (push) Successful in 45s
Test / Hakurei (race detector) (push) Successful in 45s
Test / Hpkg (push) Successful in 42s
Test / Sandbox (push) Successful in 1m32s
Test / Sandbox (race detector) (push) Successful in 2m19s
Test / Flake checks (push) Successful in 1m26s
This can seriously break things, and exposes extra host attack surface, so include it here.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-05 20:47:30 +09:00
ae7b343cde hst: reword and move constants
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 4m0s
Test / Sandbox (race detector) (push) Successful in 4m25s
Test / Hakurei (race detector) (push) Successful in 5m14s
Test / Sandbox (push) Successful in 1m26s
Test / Flake checks (push) Successful in 1m32s
These values are considered part of the stable, exported API, so move them to hst.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-05 17:40:32 +09:00
a63a372fe0 internal/app: merge static stub
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Hakurei (push) Successful in 3m4s
Test / Hpkg (push) Successful in 3m58s
Test / Hakurei (race detector) (push) Successful in 5m16s
Test / Sandbox (push) Successful in 1m20s
Test / Sandbox (race detector) (push) Successful in 2m9s
Test / Flake checks (push) Successful in 1m32s
These tests now serve as integration tests, and finer grained tests for each op will be added slowly.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-05 17:15:14 +09:00
16f9001f5f hst/config: update doc comments
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m11s
Test / Hpkg (push) Successful in 4m0s
Test / Sandbox (race detector) (push) Successful in 4m28s
Test / Hakurei (race detector) (push) Successful in 5m15s
Test / Hakurei (push) Successful in 2m15s
Test / Flake checks (push) Successful in 1m21s
Some information here are horribly out of date. This change updates and improves all doc comments.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-05 04:12:53 +09:00
80ad2e4e23 internal/app: do not offset base value
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m12s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m23s
Test / Hakurei (race detector) (push) Successful in 5m16s
Test / Hakurei (push) Successful in 2m9s
Test / Flake checks (push) Successful in 1m25s
This value is applied to the shim, it is incorrect to offset the base value as well.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-05 03:59:52 +09:00
92b83bd599 internal/app: apply pd behaviour to outcomeState
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m6s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m29s
Test / Hakurei (race detector) (push) Successful in 2m56s
Test / Flake checks (push) Successful in 1m34s
This avoids needlessly clobbering hst.Config.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-05 03:53:23 +09:00
8ace214832 system/wayland: hang up security-context-v1 internally
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 40s
Test / Hakurei (push) Successful in 43s
Test / Hakurei (race detector) (push) Successful in 44s
Test / Hpkg (push) Successful in 41s
Test / Flake checks (push) Successful in 1m26s
This should have been an implementation detail and should not be up to the caller to close.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-05 03:25:13 +09:00
eb5ee4fece internal/app: modularise outcome finalise
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m10s
Test / Hpkg (push) Successful in 4m8s
Test / Sandbox (race detector) (push) Successful in 4m35s
Test / Hakurei (race detector) (push) Successful in 5m16s
Test / Flake checks (push) Successful in 1m30s
This is the initial effort of splitting up host and container side of finalisation for params to shim. The new layout also enables much finer grained unit testing of each step, as well as partition access to per-app state for each step.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-05 02:52:50 +09:00
9462af08f3 system/dbus: dump buffer internally
All checks were successful
Test / Create distribution (push) Successful in 44s
Test / Sandbox (push) Successful in 2m32s
Test / Hpkg (push) Successful in 4m13s
Test / Sandbox (race detector) (push) Successful in 4m49s
Test / Hakurei (race detector) (push) Successful in 5m31s
Test / Hakurei (push) Successful in 2m11s
Test / Flake checks (push) Successful in 1m28s
This should have been an implementation detail and should not be up to the caller to call it.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-04 20:31:14 +09:00
a5f0aa3f30 internal/app: declutter and merge small files
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m4s
Test / Hakurei (push) Successful in 3m2s
Test / Hpkg (push) Successful in 4m3s
Test / Hakurei (race detector) (push) Successful in 5m8s
Test / Sandbox (race detector) (push) Successful in 2m4s
Test / Flake checks (push) Successful in 1m26s
This should make internal/app easier to work with for the upcoming params to shim.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-03 16:59:29 +09:00
dd0bb0a391 internal/app: check username validation
All checks were successful
Test / Create distribution (push) Successful in 38s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m6s
Test / Hpkg (push) Successful in 3m59s
Test / Sandbox (race detector) (push) Successful in 4m36s
Test / Hakurei (race detector) (push) Successful in 5m18s
Test / Flake checks (push) Successful in 1m25s
This stuff should be hardcoded in libc, but check it anyway.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-03 16:42:42 +09:00
d16da6da8c system: enforce absolute paths
All checks were successful
Test / Create distribution (push) Successful in 1m17s
Test / Sandbox (push) Successful in 2m56s
Test / Hakurei (push) Successful in 3m54s
Test / Hpkg (push) Successful in 4m51s
Test / Sandbox (race detector) (push) Successful in 5m3s
Test / Hakurei (race detector) (push) Successful in 6m0s
Test / Flake checks (push) Successful in 1m38s
This is less error-prone, and is quite easy to integrate considering internal/app has already migrated to container.Absolute.

Closes #11.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-03 02:26:14 +09:00
e58181a930 internal/app/paths: defer extra formatting
All checks were successful
Test / Create distribution (push) Successful in 1m14s
Test / Hakurei (push) Successful in 3m50s
Test / Hpkg (push) Successful in 4m44s
Test / Sandbox (race detector) (push) Successful in 4m51s
Test / Sandbox (push) Successful in 1m37s
Test / Hakurei (race detector) (push) Successful in 3m12s
Test / Flake checks (push) Successful in 1m41s
This reduces payload size for params to shim.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-30 00:21:26 +09:00
71e70b7b5f internal/app/paths: do not print messages
All checks were successful
Test / Create distribution (push) Successful in 56s
Test / Sandbox (push) Successful in 2m32s
Test / Hakurei (push) Successful in 3m36s
Test / Hpkg (push) Successful in 4m30s
Test / Hakurei (race detector) (push) Successful in 5m40s
Test / Sandbox (race detector) (push) Successful in 2m12s
Test / Flake checks (push) Successful in 1m32s
This change was missed while merging the rest of the logging changes.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-29 09:30:57 +09:00
afa1a8043e helper/proc: raise FulfillmentTimeout in tests
All checks were successful
Test / Create distribution (push) Successful in 1m1s
Test / Sandbox (push) Successful in 2m30s
Test / Hakurei (push) Successful in 3m36s
Test / Hpkg (push) Successful in 4m22s
Test / Sandbox (race detector) (push) Successful in 4m41s
Test / Hakurei (race detector) (push) Successful in 5m41s
Test / Flake checks (push) Successful in 1m32s
This appears to be yet another source of spurious test failures.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-29 07:44:33 +09:00
1ba1cb8865 hst/config: remove seccomp bit fields
All checks were successful
Test / Create distribution (push) Successful in 1m12s
Test / Sandbox (push) Successful in 2m46s
Test / Hpkg (push) Successful in 4m40s
Test / Sandbox (race detector) (push) Successful in 4m50s
Test / Hakurei (race detector) (push) Successful in 5m51s
Test / Hakurei (push) Successful in 2m36s
Test / Flake checks (push) Successful in 1m41s
These serve little purpose and are not friendly for use from other languages.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-29 07:07:16 +09:00
44ba7a5f02 hst/enablement: move bits from system
All checks were successful
Test / Create distribution (push) Successful in 54s
Test / Sandbox (push) Successful in 2m33s
Test / Hakurei (push) Successful in 3m36s
Test / Hpkg (push) Successful in 4m30s
Test / Sandbox (race detector) (push) Successful in 4m48s
Test / Hakurei (race detector) (push) Successful in 5m47s
Test / Flake checks (push) Successful in 1m40s
This is part of the hst API, should not be in the implementation package.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-29 06:34:29 +09:00
dc467493d8 internal: remove hlog
All checks were successful
Test / Create distribution (push) Successful in 1m11s
Test / Sandbox (push) Successful in 2m37s
Test / Hpkg (push) Successful in 4m41s
Test / Sandbox (race detector) (push) Successful in 4m53s
Test / Hakurei (race detector) (push) Successful in 5m53s
Test / Hakurei (push) Successful in 2m44s
Test / Flake checks (push) Successful in 1m48s
This package has been fully replaced by container.Msg.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-29 06:21:04 +09:00
46cd3a28c8 container: remove global msg
All checks were successful
Test / Create distribution (push) Successful in 1m10s
Test / Sandbox (push) Successful in 2m40s
Test / Hakurei (push) Successful in 3m58s
Test / Hpkg (push) Successful in 4m44s
Test / Sandbox (race detector) (push) Successful in 5m1s
Test / Hakurei (race detector) (push) Successful in 6m2s
Test / Flake checks (push) Successful in 1m47s
This frees all container instances of side effects.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-29 06:11:47 +09:00
ad1bc6794f release: 0.2.2
All checks were successful
Release / Create release (push) Successful in 1m8s
Test / Sandbox (push) Successful in 51s
Test / Hakurei (push) Successful in 1m9s
Test / Create distribution (push) Successful in 37s
Test / Hpkg (push) Successful in 4m38s
Test / Sandbox (race detector) (push) Successful in 4m33s
Test / Hakurei (race detector) (push) Successful in 3m11s
Test / Flake checks (push) Successful in 1m42s
Unfortunately removal of internal/hlog brought about some changes that breaks API. This will likely be the last 0.2.x release.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-28 21:58:19 +09:00
e55822c62f container/init: reduce verbose noise
All checks were successful
Test / Create distribution (push) Successful in 56s
Test / Sandbox (push) Successful in 2m38s
Test / Hakurei (push) Successful in 3m45s
Test / Hpkg (push) Successful in 4m36s
Test / Sandbox (race detector) (push) Successful in 4m45s
Test / Hakurei (race detector) (push) Successful in 5m43s
Test / Flake checks (push) Successful in 1m41s
This makes it possible to optionally omit the identifying verbose message, for when the Op implementation can provide a much more useful message in its case, using information not yet available to the String method.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-28 21:51:10 +09:00
802e6afa34 container/output: move global output to msg
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m10s
Test / Sandbox (race detector) (push) Successful in 4m27s
Test / Hpkg (push) Successful in 4m36s
Test / Hakurei (race detector) (push) Successful in 5m14s
Test / Flake checks (push) Successful in 1m22s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-27 19:55:37 +09:00
e906cae9ee container/output: export suspendable writer
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m13s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m34s
Test / Hakurei (race detector) (push) Successful in 5m14s
Test / Flake checks (push) Successful in 1m27s
This is quite useful for other packages as well. This change prepares internal/hlog for removal.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-27 19:46:35 +09:00
ae2df2c450 internal: remove sys package
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m14s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m39s
Test / Hakurei (race detector) (push) Successful in 5m19s
Test / Flake checks (push) Successful in 1m19s
This package is replaced by container/stub. Remove and replace it with unexported implementation for the upcoming test suite rewrite.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-25 13:51:54 +09:00
6e3f34f2ec internal/app: merge finalise test cases
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m17s
Test / Hakurei (push) Successful in 3m6s
Test / Hpkg (push) Successful in 4m6s
Test / Sandbox (race detector) (push) Successful in 4m27s
Test / Hakurei (race detector) (push) Successful in 5m14s
Test / Flake checks (push) Successful in 1m28s
This cleans everything up a bit for the upcoming test suite rewrite.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-25 12:11:02 +09:00
65a0bb9729 internal/sys/hsu: expose hsurc identifier
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Hakurei (push) Successful in 3m10s
Test / Hpkg (push) Successful in 4m5s
Test / Sandbox (race detector) (push) Successful in 4m35s
Test / Hakurei (race detector) (push) Successful in 5m17s
Test / Sandbox (push) Successful in 1m16s
Test / Flake checks (push) Successful in 1m24s
This maintains a compatible interface for now, to ease merging of the upcoming changes to internal/app.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-24 21:17:04 +09:00
afa7a0800d cmd/hsu: return hsurc id
All checks were successful
Test / Create distribution (push) Successful in 24s
Test / Sandbox (push) Successful in 2m19s
Test / Hpkg (push) Successful in 3m28s
Test / Sandbox (race detector) (push) Successful in 3m53s
Test / Hakurei (race detector) (push) Successful in 5m18s
Test / Hakurei (push) Successful in 43s
Test / Flake checks (push) Successful in 1m34s
The uid format is stable, this value is what caller has to obtain through hsu.

Closes #14.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-24 21:10:13 +09:00
773253fdf5 test/sandbox: raise timeout
All checks were successful
Test / Create distribution (push) Successful in 37s
Test / Hpkg (push) Successful in 46s
Test / Hakurei (push) Successful in 51s
Test / Hakurei (race detector) (push) Successful in 51s
Test / Sandbox (push) Successful in 1m31s
Test / Sandbox (race detector) (push) Successful in 2m13s
Test / Flake checks (push) Successful in 1m36s
The integration vm is being very slow for some reason. This change should reduce spurious timeouts.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-24 19:41:59 +09:00
409ed172c8 internal/app: handle LookupGroup error
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 3m57s
Test / Sandbox (race detector) (push) Successful in 4m32s
Test / Hakurei (race detector) (push) Successful in 5m18s
Test / Flake checks (push) Successful in 1m32s
This could return errnos from the cgo calls.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-24 19:36:55 +09:00
1c4f593566 internal/app: unexport outcome, remove app struct
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (race detector) (push) Successful in 5m20s
Test / Hpkg (push) Successful in 41s
Test / Hakurei (push) Successful in 2m20s
Test / Sandbox (race detector) (push) Successful in 2m9s
Test / Flake checks (push) Successful in 1m30s
The App struct no longer does anything, and the outcome struct is entirely opaque.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-24 18:44:14 +09:00
b99c63337d internal/app: do not return from shim start
All checks were successful
Test / Create distribution (push) Successful in 49s
Test / Sandbox (push) Successful in 2m37s
Test / Hakurei (push) Successful in 3m32s
Test / Hpkg (push) Successful in 4m21s
Test / Hakurei (race detector) (push) Successful in 5m37s
Test / Sandbox (race detector) (push) Successful in 2m7s
Test / Flake checks (push) Successful in 1m20s
The whole RunState ugliness and the other horrendous error handling conditions for internal/app come from an old design proposal for maintaining all app containers under the same daemon process for a user. The proposal was ultimately rejected but the implementation remained. It is removed here to alleviate internal/app from much of its ugliness and unreadability.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-24 13:37:38 +09:00
f09133a224 test: check init lingering timeout behaviour
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (race detector) (push) Successful in 41s
Test / Sandbox (push) Successful in 40s
Test / Hpkg (push) Successful in 41s
Test / Hakurei (race detector) (push) Successful in 4m7s
Test / Hakurei (push) Successful in 2m35s
Test / Flake checks (push) Successful in 1m35s
This checks init timeout on lingering process after initial process termination.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-22 21:56:29 +09:00
16409b37a2 internal/app: compensate shim timeout
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m13s
Test / Hpkg (push) Successful in 4m0s
Test / Sandbox (race detector) (push) Successful in 4m32s
Test / Hakurei (race detector) (push) Successful in 5m9s
Test / Flake checks (push) Successful in 1m23s
This catches cases where the shim has somehow locked up, so it should wait out the full shim WaitDelay as well.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-16 02:23:19 +09:00
a2a291791c internal/sys: separate hsu uid cache
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 3m56s
Test / Sandbox (race detector) (push) Successful in 4m34s
Test / Hakurei (race detector) (push) Successful in 5m6s
Test / Sandbox (push) Successful in 1m23s
Test / Flake checks (push) Successful in 1m22s
This begins the effort of the removal of the sys package.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-15 02:30:47 +09:00
8690419c2d hst: replace internal/app error
All checks were successful
Test / Create distribution (push) Successful in 43s
Test / Hpkg (push) Successful in 4m3s
Test / Sandbox (race detector) (push) Successful in 4m36s
Test / Hakurei (race detector) (push) Successful in 5m17s
Test / Sandbox (push) Successful in 1m27s
Test / Hakurei (push) Successful in 2m15s
Test / Flake checks (push) Successful in 1m28s
This turns out to still be quite useful across internal/app and its relatives. Perhaps a cleaner replacement for baseError.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-15 01:44:43 +09:00
1cdc6b4246 test/sandbox: create marker in /var/tmp
All checks were successful
Test / Hakurei (push) Successful in 49s
Test / Create distribution (push) Successful in 39s
Test / Hakurei (race detector) (push) Successful in 48s
Test / Hpkg (push) Successful in 49s
Test / Sandbox (push) Successful in 1m41s
Test / Sandbox (race detector) (push) Successful in 2m31s
Test / Flake checks (push) Successful in 1m29s
This prepares the test suite for private TMPDIR.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-14 16:45:17 +09:00
56aad8dc11 test/sandbox/tool: marker pathname from flag
All checks were successful
Test / Hakurei (push) Successful in 45s
Test / Create distribution (push) Successful in 37s
Test / Hakurei (race detector) (push) Successful in 45s
Test / Hpkg (push) Successful in 45s
Test / Sandbox (push) Successful in 1m26s
Test / Sandbox (race detector) (push) Successful in 2m10s
Test / Flake checks (push) Successful in 1m32s
Since this is going to be placed in a shared directory, it needs to be unique to the identity. Instead of trying to figure out identity from mountinfo, just have the test script pass hardcoded values.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-14 15:57:41 +09:00
83c4f8b767 test/sandbox: check extra writable paths
All checks were successful
Test / Hakurei (push) Successful in 48s
Test / Create distribution (push) Successful in 39s
Test / Hakurei (race detector) (push) Successful in 49s
Test / Hpkg (push) Successful in 47s
Test / Sandbox (push) Successful in 1m52s
Test / Sandbox (race detector) (push) Successful in 2m54s
Test / Flake checks (push) Successful in 1m21s
This is not always obvious from mountinfo.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-14 15:12:51 +09:00
d0ddd71934 test/sandbox: bind /var/tmp writable
All checks were successful
Test / Hakurei (push) Successful in 45s
Test / Hakurei (race detector) (push) Successful in 45s
Test / Create distribution (push) Successful in 38s
Test / Hpkg (push) Successful in 46s
Test / Sandbox (push) Successful in 1m36s
Test / Sandbox (race detector) (push) Successful in 2m29s
Test / Flake checks (push) Successful in 1m23s
This makes it possible to place markers with private tmpdir.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-14 14:59:53 +09:00
70e02090f7 nix: use slightly less ambiguous type
All checks were successful
Test / Hakurei (push) Successful in 41s
Test / Sandbox (push) Successful in 38s
Test / Create distribution (push) Successful in 33s
Test / Hakurei (race detector) (push) Successful in 41s
Test / Sandbox (race detector) (push) Successful in 38s
Test / Hpkg (push) Successful in 39s
Test / Flake checks (push) Successful in 1m24s
I had trouble getting Nix to merge json arrays properly, I am not sure that this helps.

At this point I have given up trying to understand Nix type system, and I am just trying to keep the Nix stuff going with extensive tests until it can be replaced by lkl for testing and planterette for general usage.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-14 14:45:14 +09:00
ca247b8037 internal/app: mount /dev/shm early
All checks were successful
Test / Create distribution (push) Successful in 38s
Test / Hakurei (race detector) (push) Successful in 49s
Test / Hpkg (push) Successful in 47s
Test / Sandbox (push) Successful in 1m40s
Test / Sandbox (race detector) (push) Successful in 2m10s
Test / Hakurei (push) Successful in 2m15s
Test / Flake checks (push) Successful in 1m30s
This avoids covering /dev/shm mounts from hst.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-14 01:49:42 +09:00
3f25c3f0af container: initialise cmd early
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m29s
Test / Hakurei (push) Successful in 4m15s
Test / Sandbox (race detector) (push) Successful in 4m43s
Test / Hpkg (push) Successful in 4m48s
Test / Hakurei (race detector) (push) Successful in 6m9s
Test / Flake checks (push) Successful in 1m18s
This allows use of more cmd methods.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-13 20:01:33 +09:00
e271fa77aa nix: update flake lock
All checks were successful
Test / Create distribution (push) Successful in 2m2s
Test / Sandbox (push) Successful in 7m8s
Test / Sandbox (race detector) (push) Successful in 2m36s
Test / Hakurei (push) Successful in 3m22s
Test / Hakurei (race detector) (push) Successful in 3m56s
Test / Hpkg (push) Successful in 16m49s
Test / Flake checks (push) Successful in 2m59s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-13 12:07:57 +09:00
f876043844 internal/hlog: remove error wrapping
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m29s
Test / Hakurei (push) Successful in 4m6s
Test / Hpkg (push) Successful in 4m45s
Test / Sandbox (race detector) (push) Successful in 4m48s
Test / Hakurei (race detector) (push) Successful in 6m4s
Test / Flake checks (push) Successful in 1m26s
This was a stopgap solution that lasted for way too long. This finally removes it and prepares internal/app for some major changes.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-12 06:52:35 +09:00
6265aea73a system: partial I inherit dispatcher
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m38s
Test / Hakurei (push) Successful in 4m23s
Test / Sandbox (race detector) (push) Successful in 5m34s
Test / Hpkg (push) Successful in 6m42s
Test / Hakurei (race detector) (push) Successful in 7m19s
Test / Flake checks (push) Successful in 3m1s
This enables I struct methods to be checked.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-11 02:02:31 +09:00
c8a0effe90 system/wayland: use syscall dispatcher
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m6s
Test / Hpkg (push) Successful in 3m46s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hakurei (race detector) (push) Successful in 5m4s
Test / Hakurei (push) Successful in 2m8s
Test / Flake checks (push) Successful in 1m27s
This enables wayland op methods to be instrumented.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-11 01:48:18 +09:00
8df01b71d4 system: remove test package
All checks were successful
Test / Create distribution (push) Successful in 45s
Test / Sandbox (push) Successful in 2m28s
Test / Hakurei (push) Successful in 3m35s
Test / Hpkg (push) Successful in 4m17s
Test / Sandbox (race detector) (push) Successful in 4m41s
Test / Hakurei (race detector) (push) Successful in 5m26s
Test / Flake checks (push) Successful in 1m25s
This prepares the Commit and Revert methods for testing via stub.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-10 23:50:22 +09:00
985c4dd2fc system/xhost: wrap revert error correctly
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m7s
Test / Hakurei (push) Successful in 3m5s
Test / Hpkg (push) Successful in 3m55s
Test / Sandbox (race detector) (push) Successful in 4m25s
Test / Hakurei (race detector) (push) Successful in 5m8s
Test / Flake checks (push) Successful in 1m26s
This otherwise creates a confusing error message on a revert failure.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-08 04:17:39 +09:00
da2b9c01ce system/tmpfiles: do not fail for smaller files
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m23s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m31s
Test / Hakurei (race detector) (push) Successful in 5m15s
Test / Hakurei (push) Successful in 2m11s
Test / Flake checks (push) Successful in 1m27s
The limit is meant to be an upper bound. Handle EOF and print verbose message for it instead of failing.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-08 03:22:10 +09:00
323d132c40 system/mkdir: use syscall dispatcher
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m7s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 3m55s
Test / Sandbox (race detector) (push) Successful in 4m29s
Test / Hakurei (race detector) (push) Successful in 5m4s
Test / Flake checks (push) Successful in 1m25s
This enables mkdir op methods to be instrumented.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-06 22:30:08 +09:00
6cc2b406a4 system/link: use syscall dispatcher
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m5s
Test / Hakurei (push) Successful in 3m4s
Test / Hpkg (push) Successful in 3m45s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hakurei (race detector) (push) Successful in 5m6s
Test / Flake checks (push) Successful in 1m49s
This enables hardlink op methods to be instrumented.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-06 19:47:58 +09:00
fcd0f2ede7 system/output: pass through LinkError
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m50s
Test / Sandbox (race detector) (push) Successful in 4m54s
Test / Hpkg (push) Successful in 5m16s
Test / Hakurei (race detector) (push) Successful in 5m51s
Test / Hakurei (push) Successful in 2m57s
Test / Flake checks (push) Successful in 2m10s
This has similar formatting to PathError.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-06 17:41:06 +09:00
e68db7fbfc system: unexport Op implementations
All checks were successful
Test / Create distribution (push) Successful in 52s
Test / Sandbox (push) Successful in 3m30s
Test / Hakurei (push) Successful in 5m40s
Test / Sandbox (race detector) (push) Successful in 6m30s
Test / Hpkg (push) Successful in 7m21s
Test / Hakurei (race detector) (push) Successful in 3m22s
Test / Flake checks (push) Successful in 2m2s
None of these are valid with their zero value, and the implementations assume they are created by the builder methods. They are by all means an implementation detail and exporting them makes no sense.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-06 16:16:03 +09:00
ac81cfbedc system/dbus: print incomplete string in buffer
All checks were successful
Test / Create distribution (push) Successful in 1m8s
Test / Sandbox (push) Successful in 3m43s
Test / Hakurei (push) Successful in 5m27s
Test / Sandbox (race detector) (push) Successful in 6m17s
Test / Hpkg (push) Successful in 7m36s
Test / Hakurei (race detector) (push) Successful in 7m44s
Test / Flake checks (push) Successful in 2m29s
Not sure if this will ever be reached, but nice to have nonetheless.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-06 15:50:29 +09:00
05db06c87b system/dbus: use syscall dispatcher
All checks were successful
Test / Create distribution (push) Successful in 1m4s
Test / Sandbox (push) Successful in 5m35s
Test / Sandbox (race detector) (push) Successful in 8m16s
Test / Hakurei (push) Successful in 10m43s
Test / Hpkg (push) Successful in 11m20s
Test / Hakurei (race detector) (push) Successful in 12m54s
Test / Flake checks (push) Successful in 3m5s
This allows dbus op methods and builder to be instrumented.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-06 14:25:19 +09:00
e603b688ca system/dispatcher: expose test reporting to builder
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m10s
Test / Hpkg (push) Successful in 3m50s
Test / Sandbox (race detector) (push) Successful in 4m22s
Test / Hakurei (race detector) (push) Successful in 5m4s
Test / Flake checks (push) Successful in 1m23s
This is currently unused but useful for builders with errors.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-06 12:59:33 +09:00
a9def08533 system/dbus: drop proxy output beyond threshold
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m5s
Test / Hpkg (push) Successful in 4m12s
Test / Sandbox (race detector) (push) Successful in 4m31s
Test / Hakurei (race detector) (push) Successful in 5m5s
Test / Flake checks (push) Successful in 1m27s
This prevents xdg-dbus-proxy from running the priv process out of memory.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-06 02:56:21 +09:00
ecaf43358d system/dbus: create context in subtest
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 41s
Test / Hakurei (push) Successful in 43s
Test / Sandbox (race detector) (push) Successful in 2m11s
Test / Hakurei (race detector) (push) Successful in 2m53s
Test / Hpkg (push) Successful in 3m17s
Test / Flake checks (push) Successful in 1m29s
This is causing a huge amount of spurious test failures due to the poor performance of the integration vm. This should finally put an end to the annoyance.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-05 05:15:40 +09:00
197fa65b8f system/dbus: remove redundant proxy pairs
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m16s
Test / Hpkg (push) Successful in 4m5s
Test / Sandbox (race detector) (push) Successful in 4m30s
Test / Hakurei (race detector) (push) Successful in 5m16s
Test / Flake checks (push) Successful in 1m40s
This is left over from before dbus.Final. Remove them now as they serve no purpose.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-05 02:07:56 +09:00
e81a45e849 container/dispatcher: optional stub wait4 signal association
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 3m26s
Test / Hpkg (push) Successful in 4m20s
Test / Sandbox (race detector) (push) Successful in 4m37s
Test / Hakurei (race detector) (push) Successful in 5m27s
Test / Flake checks (push) Successful in 1m41s
This synchronises the wait4 return after the toplevel signal call in lowlastcap_signaled_cancel_forward_error.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-04 20:28:49 +09:00
3920acf8c2 container/stub: remove function call in handleExit
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m21s
Test / Hpkg (push) Successful in 4m21s
Test / Sandbox (race detector) (push) Successful in 4m44s
Test / Hakurei (race detector) (push) Successful in 5m24s
Test / Hakurei (push) Successful in 2m26s
Test / Flake checks (push) Successful in 1m36s
This gets inlined and does not cause problems usually but turns out -coverpkg uninlines it and breaks the recovery.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-04 19:39:12 +09:00
19630a9593 container/dispatcher: remove wait4 test log
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m55s
Test / Sandbox (race detector) (push) Successful in 3m29s
Test / Hpkg (push) Successful in 3m44s
Test / Hakurei (race detector) (push) Successful in 5m24s
Test / Hakurei (push) Successful in 2m18s
Test / Flake checks (push) Successful in 1m34s
Turns out the reporting methods are not safe for concurrent use, despite the claim in testing.T doc comment.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-04 05:30:57 +09:00
4051577d6b container/stub: override goexit methods
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 1m52s
Test / Hpkg (push) Successful in 3m34s
Test / Sandbox (race detector) (push) Successful in 4m29s
Test / Hakurei (race detector) (push) Successful in 5m25s
Test / Hakurei (push) Successful in 2m25s
Test / Flake checks (push) Successful in 1m36s
FailNow, Fatal, Fatalf, SkipNow, Skip and Skipf must be called from the goroutine created by the test.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-04 04:51:49 +09:00
ddfb865e2d system/dispatcher: wrap syscall helper functions
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m6s
Test / Hakurei (push) Successful in 3m22s
Test / Hpkg (push) Successful in 3m49s
Test / Sandbox (race detector) (push) Successful in 5m34s
Test / Hakurei (race detector) (push) Successful in 3m12s
Test / Flake checks (push) Successful in 1m35s
This allows tests to stub all kernel behaviour, like in the container package.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-04 04:15:25 +09:00
024d2ff782 system: improve tests of the I struct
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m5s
Test / Hakurei (push) Successful in 3m20s
Test / Hpkg (push) Successful in 3m57s
Test / Sandbox (race detector) (push) Successful in 4m41s
Test / Hakurei (race detector) (push) Successful in 5m25s
Test / Flake checks (push) Successful in 1m39s
This cleans up for the test overhaul of this package.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-03 02:16:10 +09:00
6f719bc3c1 system: update doc commands and remove mutex
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m6s
Test / Hakurei (push) Successful in 3m19s
Test / Hpkg (push) Successful in 3m54s
Test / Sandbox (race detector) (push) Successful in 4m17s
Test / Hakurei (race detector) (push) Successful in 5m19s
Test / Flake checks (push) Successful in 1m39s
The mutex is not really doing anything, none of these methods make sense when called concurrently anyway. The copylocks analysis is still satisfied by the noCopy struct.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-02 04:54:34 +09:00
1b5d20a39b container/dispatcher: stub.Call initialisation helper function
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m11s
Test / Hakurei (push) Successful in 3m19s
Test / Hpkg (push) Successful in 3m34s
Test / Sandbox (race detector) (push) Successful in 4m33s
Test / Hakurei (race detector) (push) Successful in 5m28s
Test / Flake checks (push) Successful in 1m39s
This keeps composites analysis happy without making the test cases (too) bloated.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-02 04:44:08 +09:00
49600a6f46 container/stub: export stub helpers
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m53s
Test / Hakurei (push) Successful in 3m18s
Test / Sandbox (race detector) (push) Successful in 3m40s
Test / Hpkg (push) Successful in 3m35s
Test / Hakurei (race detector) (push) Successful in 5m19s
Test / Flake checks (push) Successful in 1m39s
These are very useful in many packages containing relatively large amount of code making calls to difficult or impossible to stub functions.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-31 23:11:25 +09:00
b489a3bba1 system/output: implement MessageError
All checks were successful
Test / Hakurei (push) Successful in 43s
Test / Create distribution (push) Successful in 26s
Test / Sandbox (push) Successful in 1m40s
Test / Hpkg (push) Successful in 3m35s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 5m20s
Test / Flake checks (push) Successful in 1m37s
This error is also formatted differently based on state.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-31 13:51:21 +09:00
780e3e5465 container/msg: optionally provide error messages
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m18s
Test / Hakurei (push) Successful in 3m22s
Test / Hpkg (push) Successful in 3m43s
Test / Sandbox (race detector) (push) Successful in 4m20s
Test / Hakurei (race detector) (push) Successful in 5m21s
Test / Flake checks (push) Successful in 1m38s
This makes handling of fatal errors a lot less squirmy.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-31 11:57:59 +09:00
712cfc06d7 container: wrap container init start errors
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 1m59s
Test / Hakurei (push) Successful in 3m20s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hpkg (push) Successful in 3m47s
Test / Hakurei (race detector) (push) Successful in 5m21s
Test / Flake checks (push) Successful in 1m35s
This helps indicate the exact origin and nature of the error. This eliminates generic WrapErr from container.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-30 23:44:48 +09:00
f5abce9df5 system: wrap op errors
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 1m51s
Test / Hakurei (push) Successful in 3m18s
Test / Hpkg (push) Successful in 3m41s
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 5m18s
Test / Flake checks (push) Successful in 1m35s
This passes more information allowing for better error handling. This eliminates generic WrapErr from system.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-30 22:49:12 +09:00
ddb003e39b system/internal/xcb: refactor and clean up
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 1m52s
Test / Hakurei (push) Successful in 3m16s
Test / Hpkg (push) Successful in 3m39s
Test / Sandbox (race detector) (push) Successful in 4m17s
Test / Hakurei (race detector) (push) Successful in 5m22s
Test / Flake checks (push) Successful in 1m36s
This package still does not deserve to be out of internal, but at least it is less haunting now. I am still not handling the xcb error though, the struct is almost entirely undocumented and the implementation is unreadable. Not even going to try.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-30 20:02:18 +09:00
b12c290f12 system/wayland: improve error descriptions
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m52s
Test / Hpkg (push) Successful in 3m42s
Test / Sandbox (race detector) (push) Successful in 3m57s
Test / Hakurei (race detector) (push) Successful in 5m17s
Test / Hakurei (push) Successful in 2m18s
Test / Flake checks (push) Successful in 1m35s
A lot of these errors have very short and nondescript descriptions. These are only returned on incorrect API usage, but it makes sense to make them more descriptive anyway.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-30 16:51:40 +09:00
0122593312 system/acl: wrap libacl errors in PathError
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 1m47s
Test / Hakurei (push) Successful in 3m20s
Test / Hpkg (push) Successful in 3m49s
Test / Sandbox (race detector) (push) Successful in 5m48s
Test / Hakurei (race detector) (push) Successful in 3m9s
Test / Flake checks (push) Successful in 1m35s
This helps determine which libacl function the errno came from.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-30 13:19:15 +09:00
6aa431d57a system/acl: update test log messages
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m46s
Test / Hakurei (push) Successful in 3m19s
Test / Hpkg (push) Successful in 3m45s
Test / Sandbox (race detector) (push) Successful in 5m5s
Test / Hakurei (race detector) (push) Successful in 3m8s
Test / Flake checks (push) Successful in 1m34s
Most of these were never updated after UpdatePerm was renamed to Update.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-30 12:55:49 +09:00
08eeafe817 container/mount: unwrap vfs decoder errors
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m6s
Test / Hakurei (push) Successful in 3m21s
Test / Hpkg (push) Successful in 3m40s
Test / Sandbox (race detector) (push) Successful in 4m32s
Test / Hakurei (race detector) (push) Successful in 5m19s
Test / Flake checks (push) Successful in 1m36s
These are now handled by init. This eliminates generic WrapErr from mount and procPaths.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-29 22:15:05 +09:00
d7c7c69a13 container/dispatcher: check simple test errors via reflect
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m11s
Test / Hpkg (push) Successful in 3m40s
Test / Sandbox (race detector) (push) Successful in 4m28s
Test / Hakurei (race detector) (push) Successful in 5m13s
Test / Hakurei (push) Successful in 2m17s
Test / Flake checks (push) Successful in 1m31s
Again, avoids the errors package concealing unexpected behaviours.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-29 22:12:21 +09:00
50972096cd container/vfs: wrap decoder errors
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m8s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 3m33s
Test / Sandbox (race detector) (push) Successful in 4m30s
Test / Hakurei (race detector) (push) Successful in 5m19s
Test / Flake checks (push) Successful in 1m35s
This passes line information and handles strconv errors so it reads better.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-29 21:51:31 +09:00
905b9f9785 container/initoverlay: invalid argument type
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 1m46s
Test / Hakurei (push) Successful in 3m19s
Test / Hpkg (push) Successful in 3m32s
Test / Sandbox (race detector) (push) Successful in 4m13s
Test / Hakurei (race detector) (push) Successful in 5m20s
Test / Flake checks (push) Successful in 1m37s
This eliminates generic WrapErr from overlay.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-29 02:56:56 +09:00
1c7e634f09 container/dispatcher: check test errors via reflect
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 1m51s
Test / Hpkg (push) Successful in 3m40s
Test / Sandbox (race detector) (push) Successful in 4m17s
Test / Hakurei (race detector) (push) Successful in 5m23s
Test / Hakurei (push) Successful in 2m21s
Test / Flake checks (push) Successful in 1m33s
Using the errors package might conceal some incorrect behaviour.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-29 02:35:24 +09:00
8d472ebf2b container/inittmpfs: unwrap out of bounds error
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m44s
Test / Hakurei (push) Successful in 3m17s
Test / Hpkg (push) Successful in 3m40s
Test / Sandbox (race detector) (push) Successful in 4m13s
Test / Hakurei (race detector) (push) Successful in 5m18s
Test / Flake checks (push) Successful in 1m36s
This eliminates generic WrapErr from tmpfs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-29 02:15:48 +09:00
4da6463135 container/init: unwrap path errors
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 1m49s
Test / Hakurei (push) Successful in 3m18s
Test / Hpkg (push) Successful in 3m43s
Test / Sandbox (race detector) (push) Successful in 3m53s
Test / Hakurei (race detector) (push) Successful in 5m16s
Test / Flake checks (push) Successful in 1m36s
These are also now handled by init properly, so wrapping them in self is meaningless and unreachable.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-29 02:04:09 +09:00
eb3385d490 container/initsymlink: unwrap mount errors
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 1m49s
Test / Hakurei (push) Successful in 3m17s
Test / Hpkg (push) Successful in 3m42s
Test / Sandbox (race detector) (push) Successful in 4m10s
Test / Hakurei (race detector) (push) Successful in 5m18s
Test / Flake checks (push) Successful in 1m38s
The mount function now wraps its own errors in a much more descriptive type with proper message formatting. Wrapping them no longer makes any sense.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-29 01:46:54 +09:00
b8669338da container/initsymlink: unwrap absolute error
All checks were successful
Test / Create distribution (push) Successful in 27s
Test / Sandbox (push) Successful in 1m47s
Test / Hakurei (push) Successful in 3m17s
Test / Hpkg (push) Successful in 3m44s
Test / Sandbox (race detector) (push) Successful in 3m52s
Test / Hakurei (race detector) (push) Successful in 5m18s
Test / Flake checks (push) Successful in 1m36s
This is now handled properly by the init.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-29 01:43:11 +09:00
f24dd4ab8c container/init: handle unwrapped errors
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 1m59s
Test / Hpkg (push) Successful in 3m32s
Test / Sandbox (race detector) (push) Successful in 3m54s
Test / Hakurei (race detector) (push) Successful in 5m16s
Test / Hakurei (push) Successful in 2m12s
Test / Flake checks (push) Successful in 1m29s
This is much cleaner from both the return statement and the error handling.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-29 01:37:13 +09:00
a462341a0a container: repeat and impossible state types
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 1m45s
Test / Hakurei (push) Successful in 3m18s
Test / Hpkg (push) Successful in 3m35s
Test / Sandbox (race detector) (push) Successful in 3m57s
Test / Hakurei (race detector) (push) Successful in 5m13s
Test / Flake checks (push) Successful in 1m36s
This moves repeated Op errors and impossible internal state errors off of msg.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-29 01:12:02 +09:00
84ad9791e2 container: wrap mount syscall errno
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m39s
Test / Hakurei (push) Successful in 3m16s
Test / Hpkg (push) Successful in 3m37s
Test / Sandbox (race detector) (push) Successful in 3m56s
Test / Hakurei (race detector) (push) Successful in 5m17s
Test / Flake checks (push) Successful in 1m36s
This is the first step to deprecating the generalised error wrapping error message pattern.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-29 01:06:12 +09:00
b14690aa77 internal/app: remove seal interface
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m1s
Test / Hakurei (push) Successful in 3m13s
Test / Hpkg (push) Successful in 3m55s
Test / Sandbox (race detector) (push) Successful in 4m33s
Test / Hakurei (race detector) (push) Successful in 5m19s
Test / Flake checks (push) Successful in 1m36s
This further cleans up the package for the restructure.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-28 01:07:51 +09:00
d0b6852cd7 internal/app: remove app interface
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m6s
Test / Hakurei (push) Successful in 3m21s
Test / Hpkg (push) Successful in 3m47s
Test / Sandbox (race detector) (push) Successful in 4m22s
Test / Hakurei (race detector) (push) Successful in 5m16s
Test / Flake checks (push) Successful in 1m36s
It is very clear at this point that there will not be multiple implementations of App, and the internal/app package will never move out of internal due to hsu.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-28 00:54:44 +09:00
da0459aca1 internal/app: update doc comments
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m15s
Test / Sandbox (race detector) (push) Successful in 3m50s
Test / Hpkg (push) Successful in 3m40s
Test / Hakurei (race detector) (push) Successful in 5m15s
Test / Flake checks (push) Successful in 1m36s
A lot of these comments are quite old and have not been updated to reflect changes.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-28 00:45:57 +09:00
1be8de6f5c internal/app: less strict username regex
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m50s
Test / Hpkg (push) Successful in 3m34s
Test / Sandbox (race detector) (push) Successful in 4m8s
Test / Hakurei (race detector) (push) Successful in 2m46s
Test / Hakurei (push) Successful in 2m15s
Test / Flake checks (push) Successful in 1m29s
Use the default value of NAME_REGEX from adduser. Should not hurt compatibility while being less strict.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-28 00:22:55 +09:00
0f41d96671 internal: move sysconf wrapper to app
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 3m56s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hakurei (race detector) (push) Successful in 5m3s
Test / Flake checks (push) Successful in 1m29s
This should not be used and is not useful in other packages.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-28 00:04:58 +09:00
92f510a647 cmd/hakurei/command: pd run dbus-verbose nil check
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Sandbox (push) Successful in 40s
Test / Sandbox (race detector) (push) Successful in 40s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Hpkg (push) Successful in 41s
Test / Hakurei (push) Successful in 2m23s
Test / Flake checks (push) Successful in 1m33s
This otherwise dereferences a nil pointer when dbus-verbose is set and either session or system bus are nil.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-06 00:09:25 +09:00
acb6931f3e app/seal: leave $DISPLAY as is on host abstract
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Hakurei (push) Successful in 42s
Test / Hakurei (race detector) (push) Successful in 42s
Test / Sandbox (race detector) (push) Successful in 40s
Test / Sandbox (push) Successful in 40s
Test / Hpkg (push) Successful in 40s
Test / Flake checks (push) Successful in 1m24s
This helps work around faulty software that misinterprets unix: DISPLAY string.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-27 20:42:03 +09:00
9d932d1039 release: 0.2.1
All checks were successful
Release / Create release (push) Successful in 43s
Test / Sandbox (push) Successful in 40s
Test / Hakurei (push) Successful in 3m17s
Test / Create distribution (push) Successful in 24s
Test / Hpkg (push) Successful in 3m36s
Test / Sandbox (race detector) (push) Successful in 3m56s
Test / Hakurei (race detector) (push) Successful in 5m6s
Test / Flake checks (push) Successful in 1m31s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-26 03:33:45 +09:00
9bc8532d56 container/initdev: mount tmpfs on shm for ro dev
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 2m51s
Test / Hpkg (push) Successful in 3m58s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hakurei (race detector) (push) Successful in 4m46s
Test / Flake checks (push) Successful in 1m26s
Programs expect /dev/shm to be a writable tmpfs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-26 03:27:07 +09:00
419 changed files with 38400 additions and 14581 deletions

2
.clang-format Normal file
View File

@@ -0,0 +1,2 @@
ColumnLimit: 0
IndentWidth: 4

View File

@@ -2,7 +2,6 @@ name: Test
on:
- push
- pull_request
jobs:
hakurei:

View File

@@ -6,73 +6,97 @@ import (
"io"
"log"
"os"
"os/signal"
"os/exec"
"os/user"
"strconv"
"sync"
"syscall"
"time"
_ "unsafe" // for go:linkname
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/app"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/system"
"hakurei.app/system/dbus"
"hakurei.app/internal/dbus"
"hakurei.app/internal/env"
"hakurei.app/internal/info"
"hakurei.app/internal/outcome"
"hakurei.app/message"
)
func buildCommand(out io.Writer) command.Command {
// 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
func optionalErrorUnwrap(err error) error
func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
var (
flagVerbose bool
flagJSON bool
)
c := command.New(out, log.Printf, "hakurei", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
c := command.New(out, log.Printf, "hakurei", func([]string) error {
msg.SwapVerbose(flagVerbose)
if early.yamaLSM != nil {
msg.Verbosef("cannot enable ptrace protection via Yama LSM: %v", early.yamaLSM)
// not fatal
}
if early.dumpable != nil {
log.Printf("cannot set SUID_DUMP_DISABLE: %s", early.dumpable)
// not fatal
}
return nil
}).
Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")
c.Command("shim", command.UsageInternal, func([]string) error { app.ShimMain(); return errSuccess })
c.Command("app", "Load app from configuration file", func(args []string) error {
if len(args) < 1 {
log.Fatal("app requires at least 1 argument")
}
// config extraArgs...
config := tryPath(args[0])
config.Args = append(config.Args, args[1:]...)
runApp(config)
panic("unreachable")
})
c.Command("shim", command.UsageInternal, func([]string) error { outcome.Shim(msg); return errSuccess })
{
var (
dbusConfigSession string
dbusConfigSystem string
mpris bool
dbusVerbose bool
fid string
aid int
groups command.RepeatableFlag
homeDir string
userName string
wayland, x11, dBus, pulse bool
flagIdentifierFile int
)
c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error {
// initialise config from flags
config := &hst.Config{
ID: fid,
Args: args,
c.NewCommand("app", "Load and start container from configuration file", func(args []string) error {
if len(args) < 1 {
log.Fatal("app requires at least 1 argument")
}
if aid < 0 || aid > 9999 {
log.Fatalf("aid %d out of range", aid)
config := tryPath(msg, args[0])
if config != nil && config.Container != nil {
config.Container.Args = append(config.Container.Args, args[1:]...)
}
outcome.Main(ctx, msg, config, flagIdentifierFile)
panic("unreachable")
}).
Flag(&flagIdentifierFile, "identifier-fd", command.IntFlag(-1),
"Write identifier of current instance to fd after successful startup")
}
{
var (
flagDBusConfigSession string
flagDBusConfigSystem string
flagDBusMpris bool
flagDBusVerbose bool
flagID string
flagIdentity int
flagGroups command.RepeatableFlag
flagHomeDir string
flagUserName string
flagPrivateRuntime, flagPrivateTmpdir bool
flagWayland, flagX11, flagDBus, flagPipeWire, flagPulse bool
)
c.NewCommand("run", "Configure and start a permissive container", func(args []string) error {
if flagIdentity < hst.IdentityStart || flagIdentity > hst.IdentityEnd {
log.Fatalf("identity %d out of range", flagIdentity)
}
// resolve home/username from os when flag is unset
@@ -80,22 +104,15 @@ func buildCommand(out io.Writer) command.Command {
passwd *user.User
passwdOnce sync.Once
passwdFunc = func() {
var us string
if uid, err := std.Uid(aid); err != nil {
hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:")
os.Exit(1)
} else {
us = strconv.Itoa(uid)
}
us := strconv.Itoa(hst.ToUser(new(outcome.Hsu).MustID(msg), flagIdentity))
if u, err := user.LookupId(us); err != nil {
hlog.Verbosef("cannot look up uid %s", us)
msg.Verbosef("cannot look up uid %s", us)
passwd = &user.User{
Uid: us,
Gid: us,
Username: "chronos",
Name: "Hakurei Permissive Default",
HomeDir: container.FHSVarEmpty,
HomeDir: fhs.VarEmpty,
}
} else {
passwd = u
@@ -103,164 +120,245 @@ func buildCommand(out io.Writer) command.Command {
}
)
if homeDir == "os" {
// paths are identical, resolve inner shell and program path
shell := fhs.AbsRoot.Append("bin", "sh")
if a, err := check.NewAbs(os.Getenv("SHELL")); err == nil {
shell = a
}
progPath := shell
if len(args) > 0 {
if p, err := exec.LookPath(args[0]); err != nil {
log.Fatal(optionalErrorUnwrap(err))
return err
} else if progPath, err = check.NewAbs(p); err != nil {
log.Fatal(err.Error())
return err
}
}
var et hst.Enablement
if flagWayland {
et |= hst.EWayland
}
if flagX11 {
et |= hst.EX11
}
if flagDBus {
et |= hst.EDBus
}
if flagPipeWire || flagPulse {
et |= hst.EPipeWire
}
config := &hst.Config{
ID: flagID,
Identity: flagIdentity,
Groups: flagGroups,
Enablements: hst.NewEnablements(et),
Container: &hst.ContainerConfig{
Filesystem: []hst.FilesystemConfigJSON{
// autoroot, includes the home directory
{FilesystemConfig: &hst.FSBind{
Target: fhs.AbsRoot,
Source: fhs.AbsRoot,
Write: true,
Special: true,
}},
},
Username: flagUserName,
Shell: shell,
Path: progPath,
Args: args,
Flags: hst.FUserns | hst.FHostNet | hst.FHostAbstract | hst.FTty,
},
}
// bind GPU stuff
if et&(hst.EX11|hst.EWayland) != 0 {
config.Container.Filesystem = append(config.Container.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
Source: fhs.AbsDev.Append("dri"),
Device: true,
Optional: true,
}})
}
config.Container.Filesystem = append(config.Container.Filesystem,
// opportunistically bind kvm
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
Source: fhs.AbsDev.Append("kvm"),
Device: true,
Optional: true,
}},
// do autoetc last
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
Target: fhs.AbsEtc,
Source: fhs.AbsEtc,
Special: true,
}},
)
if config.Container.Username == "chronos" {
passwdOnce.Do(passwdFunc)
homeDir = passwd.HomeDir
config.Container.Username = passwd.Username
}
if userName == "chronos" {
passwdOnce.Do(passwdFunc)
userName = passwd.Username
{
homeDir := flagHomeDir
if homeDir == "os" {
passwdOnce.Do(passwdFunc)
homeDir = passwd.HomeDir
}
if a, err := check.NewAbs(homeDir); err != nil {
log.Fatal(err.Error())
return err
} else {
config.Container.Home = a
}
}
config.Identity = aid
config.Groups = groups
config.Username = userName
if a, err := container.NewAbs(homeDir); err != nil {
log.Fatal(err.Error())
return err
} else {
config.Home = a
if !flagPrivateRuntime {
config.Container.Flags |= hst.FShareRuntime
}
var e system.Enablement
if wayland {
e |= system.EWayland
if !flagPrivateTmpdir {
config.Container.Flags |= hst.FShareTmpdir
}
if x11 {
e |= system.EX11
}
if dBus {
e |= system.EDBus
}
if pulse {
e |= system.EPulse
}
config.Enablements = hst.NewEnablements(e)
// parse D-Bus config file from flags if applicable
if dBus {
if dbusConfigSession == "builtin" {
config.SessionBus = dbus.NewConfig(fid, true, mpris)
if flagDBus {
if flagDBusConfigSession == "builtin" {
config.SessionBus = dbus.NewConfig(flagID, true, flagDBusMpris)
} else {
if conf, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
log.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err)
if f, err := os.Open(flagDBusConfigSession); err != nil {
log.Fatal(err.Error())
} else {
config.SessionBus = conf
decodeJSON(log.Fatal, "load session bus proxy config", f, &config.SessionBus)
if err = f.Close(); err != nil {
log.Fatal(err.Error())
}
}
}
// system bus proxy is optional
if dbusConfigSystem != "nil" {
if conf, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil {
log.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err)
if flagDBusConfigSystem != "nil" {
if f, err := os.Open(flagDBusConfigSystem); err != nil {
log.Fatal(err.Error())
} else {
config.SystemBus = conf
decodeJSON(log.Fatal, "load system bus proxy config", f, &config.SystemBus)
if err = f.Close(); err != nil {
log.Fatal(err.Error())
}
}
}
// override log from configuration
if dbusVerbose {
config.SessionBus.Log = true
config.SystemBus.Log = true
if flagDBusVerbose {
if config.SessionBus != nil {
config.SessionBus.Log = true
}
if config.SystemBus != nil {
config.SystemBus.Log = true
}
}
}
// invoke app
runApp(config)
outcome.Main(ctx, msg, config, -1)
panic("unreachable")
}).
Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"),
Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),
"Path to session bus proxy config file, or \"builtin\" for defaults").
Flag(&dbusConfigSystem, "dbus-system", command.StringFlag("nil"),
Flag(&flagDBusConfigSystem, "dbus-system", command.StringFlag("nil"),
"Path to system bus proxy config file, or \"nil\" to disable").
Flag(&mpris, "mpris", command.BoolFlag(false),
Flag(&flagDBusMpris, "mpris", command.BoolFlag(false),
"Allow owning MPRIS D-Bus path, has no effect if custom config is available").
Flag(&dbusVerbose, "dbus-log", command.BoolFlag(false),
Flag(&flagDBusVerbose, "dbus-log", command.BoolFlag(false),
"Force buffered logging in the D-Bus proxy").
Flag(&fid, "id", command.StringFlag(""),
Flag(&flagID, "id", command.StringFlag(""),
"Reverse-DNS style Application identifier, leave empty to inherit instance identifier").
Flag(&aid, "a", command.IntFlag(0),
Flag(&flagIdentity, "a", command.IntFlag(0),
"Application identity").
Flag(nil, "g", &groups,
Flag(nil, "g", &flagGroups,
"Groups inherited by all container processes").
Flag(&homeDir, "d", command.StringFlag("os"),
Flag(&flagHomeDir, "d", command.StringFlag("os"),
"Container home directory").
Flag(&userName, "u", command.StringFlag("chronos"),
Flag(&flagUserName, "u", command.StringFlag("chronos"),
"Passwd user name within sandbox").
Flag(&wayland, "wayland", command.BoolFlag(false),
Flag(&flagPrivateRuntime, "private-runtime", command.BoolFlag(false),
"Do not share XDG_RUNTIME_DIR between containers under the same identity").
Flag(&flagPrivateTmpdir, "private-tmpdir", command.BoolFlag(false),
"Do not share TMPDIR between containers under the same identity").
Flag(&flagWayland, "wayland", command.BoolFlag(false),
"Enable connection to Wayland via security-context-v1").
Flag(&x11, "X", command.BoolFlag(false),
Flag(&flagX11, "X", command.BoolFlag(false),
"Enable direct connection to X11").
Flag(&dBus, "dbus", command.BoolFlag(false),
Flag(&flagDBus, "dbus", command.BoolFlag(false),
"Enable proxied connection to D-Bus").
Flag(&pulse, "pulse", command.BoolFlag(false),
"Enable direct connection to PulseAudio")
Flag(&flagPipeWire, "pipewire", command.BoolFlag(false),
"Enable connection to PipeWire via SecurityContext").
Flag(&flagPulse, "pulse", command.BoolFlag(false),
"Enable PulseAudio compatibility daemon")
}
var showFlagShort bool
c.NewCommand("show", "Show live or local app configuration", func(args []string) error {
switch len(args) {
case 0: // system
printShowSystem(os.Stdout, showFlagShort, flagJSON)
{
var (
flagShort bool
flagNoStore bool
)
c.NewCommand("show", "Show live or local app configuration", func(args []string) error {
switch len(args) {
case 0: // system
printShowSystem(os.Stdout, flagShort, flagJSON)
case 1: // instance
name := args[0]
config, entry := tryShort(name)
if config == nil {
config = tryPath(name)
case 1: // instance
name := args[0]
var (
config *hst.Config
entry *hst.State
)
if !flagNoStore {
var sc hst.Paths
env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil))
entry = tryIdentifier(msg, name, outcome.NewStore(&sc))
}
if entry == nil {
config = tryPath(msg, name)
} else {
config = entry.Config
}
if !printShowInstance(os.Stdout, time.Now().UTC(), entry, config, flagShort, flagJSON) {
os.Exit(1)
}
default:
log.Fatal("show requires 1 argument")
}
printShowInstance(os.Stdout, time.Now().UTC(), entry, config, showFlagShort, flagJSON)
return errSuccess
}).
Flag(&flagShort, "short", command.BoolFlag(false), "Omit filesystem information").
Flag(&flagNoStore, "no-store", command.BoolFlag(false), "Do not attempt to match from active instances")
}
default:
log.Fatal("show requires 1 argument")
}
return errSuccess
}).Flag(&showFlagShort, "short", command.BoolFlag(false), "Omit filesystem information")
{
var flagShort bool
c.NewCommand("ps", "List active instances", func(args []string) error {
var sc hst.Paths
env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil))
printPs(msg, os.Stdout, time.Now().UTC(), outcome.NewStore(&sc), flagShort, flagJSON)
return errSuccess
}).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id")
}
var psFlagShort bool
c.NewCommand("ps", "List active instances", func(args []string) error {
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(std.Paths().RunDirPath.String()), psFlagShort, flagJSON)
return errSuccess
}).Flag(&psFlagShort, "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("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 {
printJSON(os.Stdout, false, hst.Template())
return errSuccess
})
c.Command("help", "Show this help message", func([]string) error {
c.PrintHelp()
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("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 })
return c
}
func runApp(config *hst.Config) {
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer stop() // unreachable
a := app.MustNew(ctx, std)
rs := new(app.RunState)
if sa, err := a.Seal(config); err != nil {
hlog.PrintBaseError(err, "cannot seal app:")
internal.Exit(1)
} else {
internal.Exit(app.PrintRunStateErr(rs, sa.Run(rs)))
}
*(*int)(nil) = 0 // not reached
}

View File

@@ -7,9 +7,12 @@ import (
"testing"
"hakurei.app/command"
"hakurei.app/message"
)
func TestHelp(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
args []string
@@ -20,8 +23,8 @@ func TestHelp(t *testing.T) {
Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS]
Commands:
app Load app from configuration file
run Configure and start a permissive default sandbox
app Load and start container from configuration file
run Configure and start a permissive container
show Show live or local app configuration
ps List active instances
version Display version information
@@ -33,7 +36,7 @@ Commands:
},
{
"run", []string{"run", "-h"}, `
Usage: hakurei run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--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:
-X Enable direct connection to X11
@@ -55,8 +58,14 @@ Flags:
Reverse-DNS style Application identifier, leave empty to inherit instance identifier
-mpris
Allow owning MPRIS D-Bus path, has no effect if custom config is available
-pipewire
Enable connection to PipeWire via SecurityContext
-private-runtime
Do not share XDG_RUNTIME_DIR between containers under the same identity
-private-tmpdir
Do not share TMPDIR between containers under the same identity
-pulse
Enable direct connection to PulseAudio
Enable PulseAudio compatibility daemon
-u string
Passwd user name within sandbox (default "chronos")
-wayland
@@ -67,8 +76,10 @@ Flags:
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
out := new(bytes.Buffer)
c := buildCommand(out)
c := buildCommand(t.Context(), message.New(nil), new(earlyHardeningErrs), out)
if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) {
t.Errorf("Parse: error = %v; want %v",
err, command.ErrHelp)

60
cmd/hakurei/json.go Normal file
View File

@@ -0,0 +1,60 @@
package main
import (
"encoding/json"
"errors"
"io"
"strconv"
)
// decodeJSON decodes json from r and stores it in v. A non-nil error results in a call to fatal.
func decodeJSON(fatal func(v ...any), op string, r io.Reader, v any) {
err := json.NewDecoder(r).Decode(v)
if err == nil {
return
}
var (
syntaxError *json.SyntaxError
unmarshalTypeError *json.UnmarshalTypeError
msg string
)
switch {
case errors.As(err, &syntaxError) && syntaxError != nil:
msg = syntaxError.Error() +
" at byte " + strconv.FormatInt(syntaxError.Offset, 10)
case errors.As(err, &unmarshalTypeError) && unmarshalTypeError != nil:
msg = "inappropriate " + unmarshalTypeError.Value +
" at byte " + strconv.FormatInt(unmarshalTypeError.Offset, 10)
default:
// InvalidUnmarshalError: incorrect usage, does not need to be handled
// io.ErrUnexpectedEOF: no additional error information available
msg = err.Error()
}
fatal("cannot " + op + ": " + msg)
}
// encodeJSON encodes v to output. A non-nil error results in a call to fatal.
func encodeJSON(fatal func(v ...any), output io.Writer, short bool, v any) {
encoder := json.NewEncoder(output)
if !short {
encoder.SetIndent("", " ")
}
if err := encoder.Encode(v); err != nil {
var marshalerError *json.MarshalerError
if errors.As(err, &marshalerError) && marshalerError != nil {
// this likely indicates an implementation error in hst
fatal("cannot encode json for " + marshalerError.Type.String() + ": " + marshalerError.Err.Error())
return
}
// UnsupportedTypeError, UnsupportedValueError: incorrect usage, does not need to be handled
fatal("cannot write json: " + err.Error())
}
}

99
cmd/hakurei/json_test.go Normal file
View File

@@ -0,0 +1,99 @@
package main
import (
"reflect"
"strings"
"testing"
"hakurei.app/container/stub"
)
func TestDecodeJSON(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
t reflect.Type
data string
want any
msg string
}{
{"success", reflect.TypeFor[uintptr](), "3735928559\n", uintptr(0xdeadbeef), ""},
{"syntax", reflect.TypeFor[*int](), "\x00", nil,
`cannot load sample: invalid character '\x00' looking for beginning of value at byte 1`},
{"type", reflect.TypeFor[uintptr](), "-1", nil,
`cannot load sample: inappropriate number -1 at byte 2`},
{"default", reflect.TypeFor[*int](), "{", nil,
"cannot load sample: unexpected EOF"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var (
gotP = reflect.New(tc.t)
gotMsg *string
)
decodeJSON(func(v ...any) {
if gotMsg != nil {
t.Fatal("fatal called twice")
}
msg := v[0].(string)
gotMsg = &msg
}, "load sample", strings.NewReader(tc.data), gotP.Interface())
if tc.msg != "" {
if gotMsg == nil {
t.Errorf("decodeJSON: success, want fatal %q", tc.msg)
} else if *gotMsg != tc.msg {
t.Errorf("decodeJSON: fatal = %q, want %q", *gotMsg, tc.msg)
}
} else if gotMsg != nil {
t.Errorf("decodeJSON: fatal = %q", *gotMsg)
} else if !reflect.DeepEqual(gotP.Elem().Interface(), tc.want) {
t.Errorf("decodeJSON: %#v, want %#v", gotP.Elem().Interface(), tc.want)
}
})
}
}
func TestEncodeJSON(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
v any
want string
}{
{"marshaler", errorJSONMarshaler{},
`cannot encode json for main.errorJSONMarshaler: unique error 3735928559 injected by the test suite`},
{"default", func() {},
`cannot write json: json: unsupported type: func()`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var called bool
encodeJSON(func(v ...any) {
if called {
t.Fatal("fatal called twice")
}
called = true
if v[0].(string) != tc.want {
t.Errorf("encodeJSON: fatal = %q, want %q", v[0].(string), tc.want)
}
}, nil, false, tc.v)
if !called {
t.Errorf("encodeJSON: success, want fatal %q", tc.want)
}
})
}
}
// errorJSONMarshaler implements json.Marshaler.
type errorJSONMarshaler struct{}
func (errorJSONMarshaler) MarshalJSON() ([]byte, error) { return nil, stub.UniqueError(0xdeadbeef) }

View File

@@ -4,15 +4,16 @@ package main
//go:generate cp ../../LICENSE .
import (
"context"
_ "embed"
"errors"
"log"
"os"
"os/signal"
"syscall"
"hakurei.app/container"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/internal/sys"
"hakurei.app/message"
)
var (
@@ -22,32 +23,34 @@ var (
license string
)
func init() { hlog.Prepare("hakurei") }
var std sys.State = new(sys.Std)
// earlyHardeningErrs are errors collected while setting up early hardening feature.
type earlyHardeningErrs struct{ yamaLSM, dumpable error }
func main() {
// early init path, skips root check and duplicate PR_SET_DUMPABLE
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
container.TryArgv0(nil)
if err := container.SetPtracer(0); err != nil {
hlog.Verbosef("cannot enable ptrace protection via Yama LSM: %v", err)
// not fatal: this program runs as the privileged user
}
log.SetPrefix("hakurei: ")
log.SetFlags(0)
msg := message.New(log.Default())
if err := container.SetDumpable(container.SUID_DUMP_DISABLE); err != nil {
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
// not fatal: this program runs as the privileged user
early := earlyHardeningErrs{
yamaLSM: container.SetPtracer(0),
dumpable: container.SetDumpable(container.SUID_DUMP_DISABLE),
}
if os.Geteuid() == 0 {
log.Fatal("this program must not run as root")
}
buildCommand(os.Stderr).MustParse(os.Args[1:], func(err error) {
hlog.Verbosef("command returned %v", err)
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer stop() // unreachable
buildCommand(ctx, msg, &early, os.Stderr).MustParse(os.Args[1:], func(err error) {
msg.Verbosef("command returned %v", err)
if errors.Is(err, errSuccess) {
hlog.BeforeExit()
msg.BeforeExit()
os.Exit(0)
}
// this catches faulty command handlers that fail to return before this point

View File

@@ -1,7 +1,7 @@
package main
import (
"encoding/json"
"encoding/hex"
"errors"
"io"
"log"
@@ -11,66 +11,93 @@ import (
"syscall"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/internal/outcome"
"hakurei.app/internal/store"
"hakurei.app/message"
)
func tryPath(name string) (config *hst.Config) {
var r io.Reader
// tryPath attempts to read [hst.Config] from multiple sources.
// tryPath reads from [os.Stdin] if name has value "-".
// Otherwise, name is passed to tryFd, and if that returns nil, name is passed to [os.Open].
func tryPath(msg message.Msg, name string) (config *hst.Config) {
var r io.ReadCloser
config = new(hst.Config)
if name != "-" {
r = tryFd(name)
r = tryFd(msg, name)
if r == nil {
hlog.Verbose("load configuration from file")
msg.Verbose("load configuration from file")
if f, err := os.Open(name); err != nil {
log.Fatalf("cannot access configuration file %q: %s", name, err)
log.Fatal(err.Error())
return
} else {
// finalizer closes f
r = f
}
} else {
defer func() {
if err := r.(io.ReadCloser).Close(); err != nil {
log.Printf("cannot close config fd: %v", err)
}
}()
}
} else {
r = os.Stdin
}
if err := json.NewDecoder(r).Decode(&config); err != nil {
log.Fatalf("cannot load configuration: %v", err)
decodeJSON(log.Fatal, "load configuration", r, &config)
if err := r.Close(); err != nil {
log.Fatal(err.Error())
}
return
}
func tryFd(name string) io.ReadCloser {
// tryFd returns a [io.ReadCloser] if name represents an integer corresponding to a valid file descriptor.
func tryFd(msg message.Msg, name string) io.ReadCloser {
if v, err := strconv.Atoi(name); err != nil {
if !errors.Is(err, strconv.ErrSyntax) {
hlog.Verbosef("name cannot be interpreted as int64: %v", err)
msg.Verbosef("name cannot be interpreted as int64: %v", err)
}
return nil
} else {
hlog.Verbosef("trying config stream from %d", v)
if v < 3 { // reject standard streams
return nil
}
msg.Verbosef("trying config stream from %d", v)
fd := uintptr(v)
if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 {
if errors.Is(errno, syscall.EBADF) {
if errors.Is(errno, syscall.EBADF) { // reject bad fd
return nil
}
log.Fatalf("cannot get fd %d: %v", fd, errno)
}
if outcome.IsPollDescriptor(fd) { // reject runtime internals
log.Fatalf("invalid config stream %d", fd)
}
return os.NewFile(fd, strconv.Itoa(v))
}
}
func tryShort(name string) (config *hst.Config, entry *state.State) {
likePrefix := false
if len(name) <= 32 {
likePrefix = true
// shortLengthMin is the minimum length a short form identifier can have and still be interpreted as an identifier.
const shortLengthMin = 1 << 3
// shortIdentifier returns an eight character short representation of [hst.ID] from its random bytes.
func shortIdentifier(id *hst.ID) string {
return shortIdentifierString(id.String())
}
// shortIdentifierString implements shortIdentifier on an arbitrary string.
func shortIdentifierString(s string) string {
return s[len(hst.ID{}) : len(hst.ID{})+shortLengthMin]
}
// tryIdentifier attempts to match [hst.State] from a [hex] representation of [hst.ID] or a prefix of its lower half.
func tryIdentifier(msg message.Msg, name string, s *store.Store) *hst.State {
const (
likeShort = 1 << iota
likeFull
)
var likely uintptr
if len(name) >= shortLengthMin && len(name) <= len(hst.ID{}) { // half the hex representation
// cannot safely decode here due to unknown alignment
for _, c := range name {
if c >= '0' && c <= '9' {
continue
@@ -78,33 +105,68 @@ func tryShort(name string) (config *hst.Config, entry *state.State) {
if c >= 'a' && c <= 'f' {
continue
}
likePrefix = false
break
return nil
}
likely |= likeShort
} else if len(name) == hex.EncodedLen(len(hst.ID{})) {
likely |= likeFull
}
// try to match from state store
if likePrefix && len(name) >= 8 {
hlog.Verbose("argument looks like prefix")
if likely == 0 {
return nil
}
s := state.NewMulti(std.Paths().RunDirPath.String())
if entries, err := state.Join(s); err != nil {
log.Printf("cannot join store: %v", err)
// drop to fetch from file
} else {
for id := range entries {
v := id.String()
if strings.HasPrefix(v, name) {
// match, use config from this state entry
entry = entries[id]
config = entry.Config
break
entries, copyError := s.All()
defer func() {
if err := copyError(); err != nil {
msg.GetLogger().Println(getMessage("cannot iterate over store:", err))
}
}()
switch {
case likely&likeShort != 0:
msg.Verbose("argument looks like short identifier")
for eh := range entries {
if eh.DecodeErr != nil {
msg.Verbose(getMessage("skipping instance:", eh.DecodeErr))
continue
}
if strings.HasPrefix(eh.ID.String()[len(hst.ID{}):], name) {
var entry hst.State
if _, err := eh.Load(&entry); err != nil {
msg.GetLogger().Println(getMessage("cannot load state entry:", err))
continue
}
hlog.Verbosef("instance %s skipped", v)
return &entry
}
}
}
return nil
return
case likely&likeFull != 0:
var likelyID hst.ID
if likelyID.UnmarshalText([]byte(name)) != nil {
return nil
}
msg.Verbose("argument looks like identifier")
for eh := range entries {
if eh.DecodeErr != nil {
msg.Verbose(getMessage("skipping instance:", eh.DecodeErr))
continue
}
if eh.ID == likelyID {
var entry hst.State
if _, err := eh.Load(&entry); err != nil {
msg.GetLogger().Println(getMessage("cannot load state entry:", err))
continue
}
return &entry
}
}
return nil
default:
panic("unreachable")
}
}

117
cmd/hakurei/parse_test.go Normal file
View File

@@ -0,0 +1,117 @@
package main
import (
"bytes"
"reflect"
"testing"
"time"
"hakurei.app/container/check"
"hakurei.app/hst"
"hakurei.app/internal/store"
"hakurei.app/message"
)
func TestShortIdentifier(t *testing.T) {
t.Parallel()
id := hst.ID{
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10,
}
const want = "fedcba98"
if got := shortIdentifier(&id); got != want {
t.Errorf("shortIdentifier: %q, want %q", got, want)
}
}
func TestTryIdentifier(t *testing.T) {
t.Parallel()
msg := message.New(nil)
id := hst.ID{
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10,
}
withBase := func(extra ...hst.State) []hst.State {
return append([]hst.State{
{ID: (hst.ID)(bytes.Repeat([]byte{0xaa}, len(hst.ID{}))), PID: 0xbeef, ShimPID: 0xcafe, Config: hst.Template(), Time: time.Unix(0, 0xdeadbeef0)},
{ID: (hst.ID)(bytes.Repeat([]byte{0xab}, len(hst.ID{}))), PID: 0x1beef, ShimPID: 0x1cafe, Config: hst.Template(), Time: time.Unix(0, 0xdeadbeef1)},
{ID: (hst.ID)(bytes.Repeat([]byte{0xf0}, len(hst.ID{}))), PID: 0x2beef, ShimPID: 0x2cafe, Config: hst.Template(), Time: time.Unix(0, 0xdeadbeef2)},
{ID: (hst.ID)(bytes.Repeat([]byte{0xfe}, len(hst.ID{}))), PID: 0xbed, ShimPID: 0xfff, Config: func() *hst.Config {
template := hst.Template()
template.Identity = hst.IdentityEnd
return template
}(), Time: time.Unix(0, 0xcafebabe0)},
{ID: (hst.ID)(bytes.Repeat([]byte{0xfc}, len(hst.ID{}))), PID: 0x1bed, ShimPID: 0x1fff, Config: func() *hst.Config {
template := hst.Template()
template.Identity = 0xfc
return template
}(), Time: time.Unix(0, 0xcafebabe1)},
{ID: (hst.ID)(bytes.Repeat([]byte{0xce}, len(hst.ID{}))), PID: 0x2bed, ShimPID: 0x2fff, Config: func() *hst.Config {
template := hst.Template()
template.Identity = 0xce
return template
}(), Time: time.Unix(0, 0xcafebabe2)},
}, extra...)
}
sampleEntry := hst.State{
ID: id,
PID: 0xcafe,
ShimPID: 0xdead,
Config: hst.Template(),
}
testCases := []struct {
name string
s string
data []hst.State
want *hst.State
}{
{"likely entries fault", "ffffffff", nil, nil},
{"likely short too short", "ff", nil, nil},
{"likely short too long", "fffffffffffffffff", nil, nil},
{"likely short invalid lower", "fffffff\x00", nil, nil},
{"likely short invalid higher", "0000000\xff", nil, nil},
{"short no match", "fedcba98", withBase(), nil},
{"short match", "fedcba98", withBase(sampleEntry), &sampleEntry},
{"short match single", "fedcba98", []hst.State{sampleEntry}, &sampleEntry},
{"short match longer", "fedcba98765", withBase(sampleEntry), &sampleEntry},
{"likely long invalid", "0123456789abcdeffedcba987654321\x00", nil, nil},
{"long no match", "0123456789abcdeffedcba9876543210", withBase(), nil},
{"long match", "0123456789abcdeffedcba9876543210", withBase(sampleEntry), &sampleEntry},
{"long match single", "0123456789abcdeffedcba9876543210", []hst.State{sampleEntry}, &sampleEntry},
}
for _, tc := range testCases {
base := check.MustAbs(t.TempDir()).Append("store")
s := store.New(base)
for i := range tc.data {
if h, err := s.Handle(tc.data[i].Identity); err != nil {
t.Fatalf("Handle: error = %v", err)
} else {
var unlock func()
if unlock, err = h.Lock(); err != nil {
t.Fatalf("Lock: error = %v", err)
}
_, err = h.Save(&tc.data[i])
unlock()
if err != nil {
t.Fatalf("Save: error = %v", err)
}
}
}
// store must not be written to beyond this point
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := tryIdentifier(msg, tc.s, store.New(base))
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("tryIdentifier: %#v, want %#v", got, tc.want)
}
})
}
}

View File

@@ -1,11 +1,10 @@
package main
import (
"encoding/json"
"bytes"
"fmt"
"io"
"log"
"os"
"slices"
"strconv"
"strings"
@@ -13,46 +12,43 @@ import (
"time"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/system/dbus"
"hakurei.app/internal/outcome"
"hakurei.app/internal/store"
"hakurei.app/message"
)
// printShowSystem populates and writes a representation of [hst.Info] to output.
func printShowSystem(output io.Writer, short, flagJSON bool) {
t := newPrinter(output)
defer t.MustFlush()
info := &hst.Info{Paths: std.Paths()}
// get hid by querying uid of identity 0
if uid, err := std.Uid(0); err != nil {
hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:")
os.Exit(1)
} else {
info.User = (uid / 10000) - 100
}
hi := outcome.Info()
if flagJSON {
printJSON(output, short, info)
encodeJSON(log.Fatal, output, short, hi)
return
}
t.Printf("User:\t%d\n", info.User)
t.Printf("TempDir:\t%s\n", info.TempDir)
t.Printf("SharePath:\t%s\n", info.SharePath)
t.Printf("RuntimePath:\t%s\n", info.RuntimePath)
t.Printf("RunDirPath:\t%s\n", info.RunDirPath)
t.Printf("Version:\t%s (libwayland %s)\n", hi.Version, hi.WaylandVersion)
t.Printf("User:\t%d\n", hi.User)
t.Printf("TempDir:\t%s\n", hi.TempDir)
t.Printf("SharePath:\t%s\n", hi.SharePath)
t.Printf("RuntimePath:\t%s\n", hi.RuntimePath)
t.Printf("RunDirPath:\t%s\n", hi.RunDirPath)
}
// printShowInstance writes a representation of [hst.State] or [hst.Config] to output.
func printShowInstance(
output io.Writer, now time.Time,
instance *state.State, config *hst.Config,
short, flagJSON bool) {
instance *hst.State, config *hst.Config,
short, flagJSON bool,
) (valid bool) {
valid = true
if flagJSON {
if instance != nil {
printJSON(output, short, instance)
encodeJSON(log.Fatal, output, short, instance)
} else {
printJSON(output, short, config)
encodeJSON(log.Fatal, output, short, config)
}
return
}
@@ -60,13 +56,21 @@ func printShowInstance(
t := newPrinter(output)
defer t.MustFlush()
if config.Container == nil {
mustPrint(output, "Warning: this configuration uses permissive defaults!\n\n")
if err := config.Validate(); err != nil {
valid = false
if m, ok := message.GetMessage(err); ok {
mustPrint(output, "Error: "+m+"!\n\n")
}
}
if config == nil {
// nothing to print
return
}
if instance != nil {
t.Printf("State\n")
t.Printf(" Instance:\t%s (%d)\n", instance.ID.String(), instance.PID)
t.Printf(" Instance:\t%s (%d -> %d)\n", instance.ID.String(), instance.PID, instance.ShimPID)
t.Printf(" Uptime:\t%s\n", now.Sub(instance.Time).Round(time.Second).String())
t.Printf("\n")
}
@@ -81,39 +85,33 @@ func printShowInstance(
if len(config.Groups) > 0 {
t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", "))
}
if config.Home != nil {
t.Printf(" Home:\t%s\n", config.Home)
}
if config.Container != nil {
params := config.Container
if params.Hostname != "" {
t.Printf(" Hostname:\t%s\n", params.Hostname)
}
flags := make([]string, 0, 7)
writeFlag := func(name string, value bool) {
if value {
flags = append(flags, name)
flags := config.Container.Flags.String()
// this is included in the upper hst.Config struct but is relevant here
const flagDirectWayland = "directwl"
if config.DirectWayland {
// hardcoded value when every flag is unset
if flags == "none" {
flags = flagDirectWayland
} else {
flags += ", " + flagDirectWayland
}
}
writeFlag("userns", params.Userns)
writeFlag("devel", params.Devel)
writeFlag("net", params.HostNet)
writeFlag("abstract", params.HostAbstract)
writeFlag("device", params.Device)
writeFlag("tty", params.Tty)
writeFlag("mapuid", params.MapRealUID)
writeFlag("directwl", config.DirectWayland)
if len(flags) == 0 {
flags = append(flags, "none")
}
t.Printf(" Flags:\t%s\n", strings.Join(flags, " "))
t.Printf(" Flags:\t%s\n", flags)
if config.Path != nil {
t.Printf(" Path:\t%s\n", config.Path)
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 {
t.Printf(" Path:\t%s\n", config.Container.Path)
}
if len(config.Container.Args) > 0 {
t.Printf(" Arguments:\t%s\n", strings.Join(config.Container.Args, " "))
}
}
if len(config.Args) > 0 {
t.Printf(" Arguments:\t%s\n", strings.Join(config.Args, " "))
}
t.Printf("\n")
@@ -122,6 +120,7 @@ func printShowInstance(
t.Printf("Filesystem\n")
for _, f := range config.Container.Filesystem {
if !f.Valid() {
valid = false
t.Println(" <invalid>")
continue
}
@@ -131,17 +130,14 @@ func printShowInstance(
}
if len(config.ExtraPerms) > 0 {
t.Printf("Extra ACL\n")
for _, p := range config.ExtraPerms {
if p == nil {
continue
}
t.Printf(" %s\n", p.String())
for i := range config.ExtraPerms {
t.Printf(" %s\n", config.ExtraPerms[i].String())
}
t.Printf("\n")
}
}
printDBus := func(c *dbus.Config) {
printDBus := func(c *hst.BusConfig) {
t.Printf(" Filter:\t%v\n", c.Filter)
if len(c.See) > 0 {
t.Printf(" See:\t%q\n", c.See)
@@ -169,59 +165,57 @@ func printShowInstance(
printDBus(config.SystemBus)
t.Printf("\n")
}
return
}
func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON bool) {
var entries state.Entries
if e, err := state.Join(s); err != nil {
log.Fatalf("cannot join store: %v", err)
} else {
entries = e
}
if err := s.Close(); err != nil {
log.Printf("cannot close store: %v", err)
// printPs writes a representation of active instances to output.
func printPs(msg message.Msg, output io.Writer, now time.Time, s *store.Store, short, flagJSON bool) {
f := func(a func(eh *store.EntryHandle)) {
entries, copyError := s.All()
for eh := range entries {
a(eh)
}
if err := copyError(); err != nil {
msg.GetLogger().Println(getMessage("cannot iterate over store:", err))
}
}
if !short && flagJSON {
es := make(map[string]*state.State, len(entries))
for id, instance := range entries {
es[id.String()] = instance
if short { // short output requires identifier only
var identifiers []*hst.ID
f(func(eh *store.EntryHandle) {
if _, err := eh.Load(nil); err != nil { // passes through decode error
msg.GetLogger().Println(getMessage("cannot validate state entry header:", err))
return
}
identifiers = append(identifiers, &eh.ID)
})
slices.SortFunc(identifiers, func(a, b *hst.ID) int { return bytes.Compare(a[:], b[:]) })
if flagJSON {
encodeJSON(log.Fatal, output, short, identifiers)
} else {
for _, id := range identifiers {
mustPrintln(output, shortIdentifier(id))
}
}
printJSON(output, short, es)
return
}
// sort state entries by id string to ensure consistency between runs
exp := make([]*expandedStateEntry, 0, len(entries))
for id, instance := range entries {
// gracefully skip nil states
if instance == nil {
log.Printf("got invalid state entry %s", id.String())
continue
// long output requires full instance state
var instances []*hst.State
f(func(eh *store.EntryHandle) {
var state hst.State
if _, err := eh.Load(&state); err != nil { // passes through decode error
msg.GetLogger().Println(getMessage("cannot load state entry:", err))
return
}
instances = append(instances, &state)
})
slices.SortFunc(instances, func(a, b *hst.State) int { return bytes.Compare(a.ID[:], b.ID[:]) })
// gracefully skip inconsistent states
if id != instance.ID {
log.Printf("possible store corruption: entry %s has id %s",
id.String(), instance.ID.String())
continue
}
exp = append(exp, &expandedStateEntry{s: id.String(), State: instance})
}
slices.SortFunc(exp, func(a, b *expandedStateEntry) int { return a.Time.Compare(b.Time) })
if short {
if flagJSON {
v := make([]string, len(exp))
for i, e := range exp {
v[i] = e.s
}
printJSON(output, short, v)
} else {
for _, e := range exp {
mustPrintln(output, e.s[:8])
}
}
if flagJSON {
encodeJSON(log.Fatal, output, short, instances)
return
}
@@ -229,61 +223,48 @@ func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON boo
defer t.MustFlush()
t.Println("\tInstance\tPID\tApplication\tUptime")
for _, e := range exp {
if len(e.s) != 1<<5 {
// unreachable
log.Printf("possible store corruption: invalid instance string %s", e.s)
continue
}
for _, instance := range instances {
as := "(No configuration information)"
if e.Config != nil {
as = strconv.Itoa(e.Config.Identity)
id := e.Config.ID
if instance.Config != nil {
as = strconv.Itoa(instance.Config.Identity)
id := instance.Config.ID
if id == "" {
id = "app.hakurei." + e.s[:8]
id = "app.hakurei." + shortIdentifier(&instance.ID)
}
as += " (" + id + ")"
}
t.Printf("\t%s\t%d\t%s\t%s\n",
e.s[:8], e.PID, as, now.Sub(e.Time).Round(time.Second).String())
}
}
type expandedStateEntry struct {
s string
*state.State
}
func printJSON(output io.Writer, short bool, v any) {
encoder := json.NewEncoder(output)
if !short {
encoder.SetIndent("", " ")
}
if err := encoder.Encode(v); err != nil {
log.Fatalf("cannot serialise: %v", err)
shortIdentifier(&instance.ID), instance.PID, as, now.Sub(instance.Time).Round(time.Second).String())
}
}
// newPrinter returns a configured, wrapped [tabwriter.Writer].
func newPrinter(output io.Writer) *tp { return &tp{tabwriter.NewWriter(output, 0, 1, 4, ' ', 0)} }
// tp wraps [tabwriter.Writer] to provide additional formatting methods.
type tp struct{ *tabwriter.Writer }
// Printf calls [fmt.Fprintf] on the underlying [tabwriter.Writer].
func (p *tp) Printf(format string, a ...any) {
if _, err := fmt.Fprintf(p, format, a...); err != nil {
log.Fatalf("cannot write to tabwriter: %v", err)
}
}
// Println calls [fmt.Fprintln] on the underlying [tabwriter.Writer].
func (p *tp) Println(a ...any) {
if _, err := fmt.Fprintln(p, a...); err != nil {
log.Fatalf("cannot write to tabwriter: %v", err)
}
}
// MustFlush calls the Flush method of [tabwriter.Writer] and calls [log.Fatalf] on a non-nil error.
func (p *tp) MustFlush() {
if err := p.Writer.Flush(); err != nil {
log.Fatalf("cannot flush tabwriter: %v", err)
}
}
func mustPrint(output io.Writer, a ...any) {
if _, err := fmt.Fprint(output, a...); err != nil {
log.Fatalf("cannot print: %v", err)
@@ -294,3 +275,11 @@ func mustPrintln(output io.Writer, a ...any) {
log.Fatalf("cannot print: %v", err)
}
}
// getMessage returns a [message.Error] message if available, or err prefixed with fallback otherwise.
func getMessage(fallback string, err error) string {
if m, ok := message.GetMessage(err); ok {
return m
}
return fmt.Sprintln(fallback, err)
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,10 +5,9 @@ import (
"log"
"os"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/hst"
"hakurei.app/system/dbus"
)
type appInfo struct {
@@ -38,9 +37,9 @@ type appInfo struct {
// passed through to [hst.Config]
DirectWayland bool `json:"direct_wayland,omitempty"`
// passed through to [hst.Config]
SystemBus *dbus.Config `json:"system_bus,omitempty"`
SystemBus *hst.BusConfig `json:"system_bus,omitempty"`
// passed through to [hst.Config]
SessionBus *dbus.Config `json:"session_bus,omitempty"`
SessionBus *hst.BusConfig `json:"session_bus,omitempty"`
// passed through to [hst.Config]
Enablements *hst.Enablements `json:"enablements,omitempty"`
@@ -56,69 +55,82 @@ type appInfo struct {
// store path to nixGL source
NixGL string `json:"nix_gl,omitempty"`
// store path to activate-and-exec script
Launcher *container.Absolute `json:"launcher"`
Launcher *check.Absolute `json:"launcher"`
// store path to /run/current-system
CurrentSystem *container.Absolute `json:"current_system"`
CurrentSystem *check.Absolute `json:"current_system"`
// store path to home-manager activation package
ActivationPackage string `json:"activation_package"`
}
func (app *appInfo) toHst(pathSet *appPathSet, pathname *container.Absolute, argv []string, flagDropShell bool) *hst.Config {
func (app *appInfo) toHst(pathSet *appPathSet, pathname *check.Absolute, argv []string, flagDropShell bool) *hst.Config {
config := &hst.Config{
ID: app.ID,
Path: pathname,
Args: argv,
Enablements: app.Enablements,
SystemBus: app.SystemBus,
SessionBus: app.SessionBus,
DirectWayland: app.DirectWayland,
Username: "hakurei",
Shell: pathShell,
Home: pathDataData.Append(app.ID),
Identity: app.Identity,
Groups: app.Groups,
Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name),
Devel: app.Devel,
Userns: app.Userns,
HostNet: app.HostNet,
HostAbstract: app.HostAbstract,
Device: app.Device,
Tty: app.Tty || flagDropShell,
MapRealUID: app.MapRealUID,
Hostname: formatHostname(app.Name),
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
{FilesystemConfig: &hst.FSBind{Target: fhs.AbsEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append("store"), Target: pathNixStore}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.metaPath, Target: hst.AbsTmp.Append("app")}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSEtc.Append("resolv.conf"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("block"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("bus"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("class"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("dev"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("devices"), Optional: true}},
{FilesystemConfig: &hst.FSLink{Target: fhs.AbsUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.metaPath, Target: hst.AbsPrivateTmp.Append("app")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsEtc.Append("resolv.conf"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("block"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("bus"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("class"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("dev"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("devices"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID), Source: pathSet.homeDir, Write: true, Ensure: true}},
},
Username: "hakurei",
Shell: pathShell,
Home: pathDataData.Append(app.ID),
Path: pathname,
Args: argv,
},
ExtraPerms: []*hst.ExtraPermConfig{
ExtraPerms: []hst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
},
}
if app.Devel {
config.Container.Flags |= hst.FDevel
}
if app.Userns {
config.Container.Flags |= hst.FUserns
}
if app.HostNet {
config.Container.Flags |= hst.FHostNet
}
if app.HostAbstract {
config.Container.Flags |= hst.FHostAbstract
}
if app.Device {
config.Container.Flags |= hst.FDevice
}
if app.Tty || flagDropShell {
config.Container.Flags |= hst.FTty
}
if app.MapRealUID {
config.Container.Flags |= hst.FMapRealUID
}
if app.Multiarch {
config.Container.SeccompFlags |= seccomp.AllowMultiarch
}
if app.Bluetooth {
config.Container.SeccompFlags |= seccomp.AllowBluetooth
config.Container.Flags |= hst.FMultiarch
}
config.Container.Flags |= hst.FShareRuntime | hst.FShareTmpdir
return config
}

View File

@@ -45,7 +45,7 @@
allow_wayland ? true,
allow_x11 ? false,
allow_dbus ? true,
allow_pulse ? true,
allow_audio ? true,
gpu ? allow_wayland || allow_x11,
}:
@@ -175,7 +175,7 @@ let
wayland = allow_wayland;
x11 = allow_x11;
dbus = allow_dbus;
pulse = allow_pulse;
pipewire = allow_audio;
};
mesa = if gpu then mesaWrappers else null;

View File

@@ -11,24 +11,25 @@ import (
"syscall"
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/message"
)
var (
errSuccess = errors.New("success")
)
func init() {
hlog.Prepare("hpkg")
func main() {
log.SetPrefix("hpkg: ")
log.SetFlags(0)
msg := message.New(log.Default())
if err := os.Setenv("SHELL", pathShell.String()); err != nil {
log.Fatalf("cannot set $SHELL: %v", err)
}
}
func main() {
if os.Geteuid() == 0 {
log.Fatal("this program must not run as root")
}
@@ -41,7 +42,7 @@ func main() {
flagVerbose bool
flagDropShell bool
)
c := command.New(os.Stderr, log.Printf, "hpkg", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
c := command.New(os.Stderr, log.Printf, "hpkg", func([]string) error { msg.SwapVerbose(flagVerbose); return nil }).
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next hakurei action")
@@ -80,22 +81,22 @@ func main() {
Extract package and set up for cleanup.
*/
var workDir *container.Absolute
var workDir *check.Absolute
if p, err := os.MkdirTemp("", "hpkg.*"); err != nil {
log.Printf("cannot create temporary directory: %v", err)
return err
} else if workDir, err = container.NewAbs(p); err != nil {
} else if workDir, err = check.NewAbs(p); err != nil {
log.Printf("invalid temporary directory: %v", err)
return err
}
cleanup := func() {
// should be faster than a native implementation
mustRun(chmod, "-R", "+w", workDir.String())
mustRun(rm, "-rf", workDir.String())
mustRun(msg, chmod, "-R", "+w", workDir.String())
mustRun(msg, rm, "-rf", workDir.String())
}
beforeRunFail.Store(&cleanup)
mustRun(tar, "-C", workDir.String(), "-xf", pkgPath)
mustRun(msg, tar, "-C", workDir.String(), "-xf", pkgPath)
/*
Parse bundle and app metadata, do pre-install checks.
@@ -148,10 +149,10 @@ func main() {
}
// sec: should compare version string
hlog.Verbosef("installing application %q version %q over local %q",
msg.Verbosef("installing application %q version %q over local %q",
bundle.ID, bundle.Version, a.Version)
} else {
hlog.Verbosef("application %q clean installation", bundle.ID)
msg.Verbosef("application %q clean installation", bundle.ID)
// sec: should install credentials
}
@@ -159,9 +160,9 @@ func main() {
Setup steps for files owned by the target user.
*/
withCacheDir(ctx, "install", []string{
withCacheDir(ctx, msg, "install", []string{
// export inner bundle path in the environment
"export BUNDLE=" + hst.Tmp + "/bundle",
"export BUNDLE=" + hst.PrivateTmp + "/bundle",
// replace inner /etc
"mkdir -p etc",
"chmod -R +w etc",
@@ -181,7 +182,7 @@ func main() {
}, workDir, bundle, pathSet, flagDropShell, cleanup)
if bundle.GPU {
withCacheDir(ctx, "mesa-wrappers", []string{
withCacheDir(ctx, msg, "mesa-wrappers", []string{
// link nixGL mesa wrappers
"mkdir -p nix/.nixGL",
"ln -s " + bundle.Mesa + "/bin/nixGLIntel nix/.nixGL/nixGL",
@@ -193,7 +194,7 @@ func main() {
Activate home-manager generation.
*/
withNixDaemon(ctx, "activate", []string{
withNixDaemon(ctx, msg, "activate", []string{
// clean up broken links
"mkdir -p .local/state/{nix,home-manager}",
"chmod -R +w .local/state/{nix,home-manager}",
@@ -261,7 +262,7 @@ func main() {
*/
if a.GPU && flagAutoDrivers {
withNixDaemon(ctx, "nix-gl", []string{
withNixDaemon(ctx, msg, "nix-gl", []string{
"mkdir -p /nix/.nixGL/auto",
"rm -rf /nix/.nixGL/auto",
"export NIXPKGS_ALLOW_UNFREE=1",
@@ -275,12 +276,12 @@ func main() {
"path:" + a.NixGL + "#nixVulkanNvidia",
}, true, func(config *hst.Config) *hst.Config {
config.Container.Filesystem = append(config.Container.Filesystem, []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSEtc.Append("resolv.conf"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("block"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("bus"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("class"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("dev"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("devices"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsEtc.Append("resolv.conf"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("block"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("bus"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("class"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("dev"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("devices"), Optional: true}},
}...)
appendGPUFilesystem(config)
return config
@@ -308,7 +309,7 @@ func main() {
if a.GPU {
config.Container.Filesystem = append(config.Container.Filesystem,
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append(".nixGL"), Target: hst.AbsTmp.Append("nixGL")}})
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append(".nixGL"), Target: hst.AbsPrivateTmp.Append("nixGL")}})
appendGPUFilesystem(config)
}
@@ -316,7 +317,7 @@ func main() {
Spawn app.
*/
mustRunApp(ctx, config, func() {})
mustRunApp(ctx, msg, config, func() {})
return errSuccess
}).
Flag(&flagDropShellNixGL, "s", command.BoolFlag(false), "Drop to a shell on nixGL build").
@@ -324,9 +325,9 @@ func main() {
}
c.MustParse(os.Args[1:], func(err error) {
hlog.Verbosef("command returned %v", err)
msg.Verbosef("command returned %v", err)
if errors.Is(err, errSuccess) {
hlog.BeforeExit()
msg.BeforeExit()
os.Exit(0)
}
})

View File

@@ -7,36 +7,37 @@ import (
"strconv"
"sync/atomic"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/hst"
"hakurei.app/internal/hlog"
"hakurei.app/message"
)
const bash = "bash"
var (
dataHome *container.Absolute
dataHome *check.Absolute
)
func init() {
// dataHome
if a, err := container.NewAbs(os.Getenv("HAKUREI_DATA_HOME")); err == nil {
if a, err := check.NewAbs(os.Getenv("HAKUREI_DATA_HOME")); err == nil {
dataHome = a
} else {
dataHome = container.AbsFHSVarLib.Append("hakurei/" + strconv.Itoa(os.Getuid()))
dataHome = fhs.AbsVarLib.Append("hakurei/" + strconv.Itoa(os.Getuid()))
}
}
var (
pathBin = container.AbsFHSRoot.Append("bin")
pathBin = fhs.AbsRoot.Append("bin")
pathNix = container.MustAbs("/nix/")
pathNix = check.MustAbs("/nix/")
pathNixStore = pathNix.Append("store/")
pathCurrentSystem = container.AbsFHSRun.Append("current-system")
pathCurrentSystem = fhs.AbsRun.Append("current-system")
pathSwBin = pathCurrentSystem.Append("sw/bin/")
pathShell = pathSwBin.Append(bash)
pathData = container.MustAbs("/data")
pathData = check.MustAbs("/data")
pathDataData = pathData.Append("data")
)
@@ -51,8 +52,8 @@ func lookPath(file string) string {
var beforeRunFail = new(atomic.Pointer[func()])
func mustRun(name string, arg ...string) {
hlog.Verbosef("spawning process: %q %q", name, arg)
func mustRun(msg message.Msg, name string, arg ...string) {
msg.Verbosef("spawning process: %q %q", name, arg)
cmd := exec.Command(name, arg...)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
@@ -65,15 +66,15 @@ func mustRun(name string, arg ...string) {
type appPathSet struct {
// ${dataHome}/${id}
baseDir *container.Absolute
baseDir *check.Absolute
// ${baseDir}/app
metaPath *container.Absolute
metaPath *check.Absolute
// ${baseDir}/files
homeDir *container.Absolute
homeDir *check.Absolute
// ${baseDir}/cache
cacheDir *container.Absolute
cacheDir *check.Absolute
// ${baseDir}/cache/nix
nixPath *container.Absolute
nixPath *check.Absolute
}
func pathSetByApp(id string) *appPathSet {
@@ -89,28 +90,28 @@ func pathSetByApp(id string) *appPathSet {
func appendGPUFilesystem(config *hst.Config) {
config.Container.Filesystem = append(config.Container.Filesystem, []hst.FilesystemConfigJSON{
// flatpak commit 763a686d874dd668f0236f911de00b80766ffe79
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("dri"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("dri"), Device: true, Optional: true}},
// mali
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("mali"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("mali0"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("umplock"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("mali"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("mali0"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("umplock"), Device: true, Optional: true}},
// nvidia
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidiactl"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia-modeset"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidiactl"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia-modeset"), Device: true, Optional: true}},
// nvidia OpenCL/CUDA
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia-uvm"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia-uvm-tools"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia-uvm"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia-uvm-tools"), Device: true, Optional: true}},
// flatpak commit d2dff2875bb3b7e2cd92d8204088d743fd07f3ff
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia0"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia1"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia2"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia3"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia4"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia5"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia6"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia7"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia8"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia9"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia10"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia11"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia12"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia13"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia14"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia15"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia16"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia17"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia18"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia19"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia0"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia1"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia2"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia3"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia4"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia5"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia6"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia7"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia8"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia9"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia10"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia11"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia12"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia13"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia14"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia15"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia16"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia17"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia18"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia19"), Device: true, Optional: true}},
}...)
}

View File

@@ -10,13 +10,13 @@ import (
"os/exec"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/internal/info"
"hakurei.app/message"
)
var hakureiPath = internal.MustHakureiPath()
var hakureiPathVal = info.MustHakureiPath().String()
func mustRunApp(ctx context.Context, config *hst.Config, beforeFail func()) {
func mustRunApp(ctx context.Context, msg message.Msg, config *hst.Config, beforeFail func()) {
var (
cmd *exec.Cmd
st io.WriteCloser
@@ -26,10 +26,10 @@ func mustRunApp(ctx context.Context, config *hst.Config, beforeFail func()) {
beforeFail()
log.Fatalf("cannot pipe: %v", err)
} else {
if hlog.Load() {
cmd = exec.CommandContext(ctx, hakureiPath, "-v", "app", "3")
if msg.IsVerbose() {
cmd = exec.CommandContext(ctx, hakureiPathVal, "-v", "app", "3")
} else {
cmd = exec.CommandContext(ctx, hakureiPath, "app", "3")
cmd = exec.CommandContext(ctx, hakureiPathVal, "app", "3")
}
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.ExtraFiles = []*os.File{r}
@@ -51,7 +51,8 @@ func mustRunApp(ctx context.Context, config *hst.Config, beforeFail func()) {
var exitError *exec.ExitError
if errors.As(err, &exitError) {
beforeFail()
internal.Exit(exitError.ExitCode())
msg.BeforeExit()
os.Exit(exitError.ExitCode())
} else {
beforeFail()
log.Fatalf("cannot wait: %v", err)

View File

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

View File

@@ -58,15 +58,13 @@ def check_state(name, enablements):
instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei --json ps"))
if len(instances) != 1:
raise Exception(f"unexpected state length {len(instances)}")
instance = next(iter(instances.values()))
instance = instances[0]
config = instance['config']
if len(instance['container']['args']) != 1 or not (instance['container']['args'][0].startswith("/nix/store/")) or f"hakurei-{name}-" not in (instance['container']['args'][0]):
raise Exception(f"unexpected args {instance['container']['args']}")
if len(config['args']) != 1 or not (config['args'][0].startswith("/nix/store/")) or f"hakurei-{name}-" not in (config['args'][0]):
raise Exception(f"unexpected args {instance['config']['args']}")
if config['enablements'] != enablements:
raise Exception(f"unexpected enablements {instance['config']['enablements']}")
if instance['enablements'] != enablements:
raise Exception(f"unexpected enablements {instance['enablements']}")
start_all()
@@ -92,17 +90,21 @@ wait_for_window("hakurei@machine-foot")
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
machine.wait_for_file("/tmp/hakurei.0/tmpdir/2/success-client")
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:
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002"))
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10002"))
machine.send_chars("exit\n")
machine.wait_until_fails("pgrep foot")
# Verify acl cleanup on XDG_RUNTIME_DIR:
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002")
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10002")
# Exit Sway and verify process exit status 0:
swaymsg("exit", succeed=False)
machine.wait_for_file("/tmp/sway-exit-ok")
# Print hakurei runDir contents:
print(machine.succeed("find /run/user/1000/hakurei"))
# Print hakurei share and rundir contents:
print(machine.succeed("find /tmp/hakurei.0 "
+ "-path '/tmp/hakurei.0/runtime/*/*' -prune -o "
+ "-path '/tmp/hakurei.0/tmpdir/*/*' -prune -o "
+ "-print"))
print(machine.fail("ls /run/user/1000/hakurei"))

View File

@@ -2,39 +2,33 @@ package main
import (
"context"
"os"
"strings"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/message"
)
func withNixDaemon(
ctx context.Context,
msg message.Msg,
action string, command []string, net bool, updateConfig func(config *hst.Config) *hst.Config,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
) {
mustRunAppDropShell(ctx, updateConfig(&hst.Config{
flags := hst.FMultiarch | hst.FUserns // nix sandbox requires userns
if net {
flags |= hst.FHostNet
}
if dropShell {
flags |= hst.FTty
}
mustRunAppDropShell(ctx, msg, updateConfig(&hst.Config{
ID: app.ID,
Path: pathShell,
Args: []string{bash, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
// start nix-daemon
"nix-daemon --store / & " +
// wait for socket to appear
"(while [ ! -S /nix/var/nix/daemon-socket/socket ]; do sleep 0.01; done) && " +
// create directory so nix stops complaining
"mkdir -p /nix/var/nix/profiles/per-user/root/channels && " +
strings.Join(command, " && ") +
// terminate nix-daemon
" && pkill nix-daemon",
},
Username: "hakurei",
Shell: pathShell,
Home: pathDataData.Append(app.ID),
ExtraPerms: []*hst.ExtraPermConfig{
ExtraPerms: []hst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
},
@@ -42,37 +36,54 @@ func withNixDaemon(
Identity: app.Identity,
Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action,
Userns: true, // nix sandbox requires userns
HostNet: net,
SeccompFlags: seccomp.AllowMultiarch,
Tty: dropShell,
Hostname: formatHostname(app.Name) + "-" + action,
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
{FilesystemConfig: &hst.FSBind{Target: fhs.AbsEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath, Target: pathNix, Write: true}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: fhs.AbsUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID), Source: pathSet.homeDir, Write: true, Ensure: true}},
},
Username: "hakurei",
Shell: pathShell,
Home: pathDataData.Append(app.ID),
Path: pathShell,
Args: []string{bash, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
// start nix-daemon
"nix-daemon --store / & " +
// wait for socket to appear
"(while [ ! -S /nix/var/nix/daemon-socket/socket ]; do sleep 0.01; done) && " +
// create directory so nix stops complaining
"mkdir -p /nix/var/nix/profiles/per-user/root/channels && " +
strings.Join(command, " && ") +
// terminate nix-daemon
" && pkill nix-daemon",
},
Flags: flags,
},
}), dropShell, beforeFail)
}
func withCacheDir(
ctx context.Context,
action string, command []string, workDir *container.Absolute,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
mustRunAppDropShell(ctx, &hst.Config{
msg message.Msg,
action string, command []string, workDir *check.Absolute,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
) {
flags := hst.FMultiarch
if dropShell {
flags |= hst.FTty
}
mustRunAppDropShell(ctx, msg, &hst.Config{
ID: app.ID,
Path: pathShell,
Args: []string{bash, "-lc", strings.Join(command, " && ")},
Username: "nixos",
Shell: pathShell,
Home: pathDataData.Append(app.ID, "cache"),
ExtraPerms: []*hst.ExtraPermConfig{
ExtraPerms: []hst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
{Path: workDir, Execute: true},
@@ -81,28 +92,39 @@ func withCacheDir(
Identity: app.Identity,
Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action,
SeccompFlags: seccomp.AllowMultiarch,
Tty: dropShell,
Hostname: formatHostname(app.Name) + "-" + action,
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: workDir.Append(container.FHSEtc), Special: true}},
{FilesystemConfig: &hst.FSBind{Target: fhs.AbsEtc, Source: workDir.Append(fhs.Etc), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: workDir.Append("nix"), Target: pathNix}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Source: workDir, Target: hst.AbsTmp.Append("bundle")}},
{FilesystemConfig: &hst.FSLink{Target: fhs.AbsUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Source: workDir, Target: hst.AbsPrivateTmp.Append("bundle")}},
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID, "cache"), Source: pathSet.cacheDir, Write: true, Ensure: true}},
},
Username: "nixos",
Shell: pathShell,
Home: pathDataData.Append(app.ID, "cache"),
Path: pathShell,
Args: []string{bash, "-lc", strings.Join(command, " && ")},
Flags: flags,
},
}, dropShell, beforeFail)
}
func mustRunAppDropShell(ctx context.Context, config *hst.Config, dropShell bool, beforeFail func()) {
func mustRunAppDropShell(ctx context.Context, msg message.Msg, config *hst.Config, dropShell bool, beforeFail func()) {
if dropShell {
config.Args = []string{bash, "-l"}
mustRunApp(ctx, config, beforeFail)
if config.Container != nil {
config.Container.Args = []string{bash, "-l"}
}
mustRunApp(ctx, msg, config, beforeFail)
beforeFail()
internal.Exit(0)
msg.BeforeExit()
os.Exit(0)
}
mustRunApp(ctx, config, beforeFail)
mustRunApp(ctx, msg, config, beforeFail)
}

16
cmd/hsu/hst.go Normal file
View File

@@ -0,0 +1,16 @@
package main
/* copied from hst and must never be changed */
const (
userOffset = 100000
rangeSize = userOffset / 10
identityStart = 0
identityEnd = appEnd - appStart
appStart = rangeSize * 1
appEnd = appStart + rangeSize - 1
)
func toUser(userid, appid uint32) uint32 { return userid*userOffset + appStart + appid }

View File

@@ -1,11 +1,14 @@
package main
// minimise imports to avoid inadvertently calling init or global variable functions
import (
"bytes"
"fmt"
"log"
"os"
"path"
"runtime"
"slices"
"strconv"
"strings"
@@ -13,15 +16,23 @@ import (
)
const (
hsuConfFile = "/etc/hsurc"
envShim = "HAKUREI_SHIM"
envAID = "HAKUREI_APP_ID"
envGroups = "HAKUREI_GROUPS"
PR_SET_NO_NEW_PRIVS = 0x26
// envIdentity is the name of the environment variable holding a
// single byte representing the shim setup pipe file descriptor.
envShim = "HAKUREI_SHIM"
// envGroups holds a ' ' separated list of string representations of
// supplementary group gid. Membership requirements are enforced.
envGroups = "HAKUREI_GROUPS"
)
// hakureiPath is the absolute path to Hakurei.
//
// This is set by the linker.
var hakureiPath string
func main() {
const PR_SET_NO_NEW_PRIVS = 0x26
runtime.LockOSThread()
log.SetFlags(0)
log.SetPrefix("hsu: ")
log.SetOutput(os.Stderr)
@@ -29,31 +40,34 @@ func main() {
if os.Geteuid() != 0 {
log.Fatal("this program must be owned by uid 0 and have the setuid bit set")
}
if os.Getegid() != os.Getgid() {
log.Fatal("this program must not have the setgid bit set")
}
puid := os.Getuid()
if puid == 0 {
log.Fatal("this program must not be started by root")
}
if !path.IsAbs(hakureiPath) {
log.Fatal("this program is compiled incorrectly")
return
}
var toolPath string
pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
if p, err := os.Readlink(pexe); err != nil {
log.Fatalf("cannot read parent executable path: %v", err)
} else if strings.HasSuffix(p, " (deleted)") {
log.Fatal("hakurei executable has been deleted")
} else if p != mustCheckPath(hmain) {
} else if p != hakureiPath {
log.Fatal("this program must be started by hakurei")
} else {
toolPath = p
}
// uid = 1000000 +
// fid * 10000 +
// aid
uid := 1000000
// refuse to run if hsurc is not protected correctly
if s, err := os.Stat(hsuConfFile); err != nil {
if s, err := os.Stat(hsuConfPath); err != nil {
log.Fatal(err)
} else if s.Mode().Perm() != 0400 {
log.Fatal("bad hsurc perm")
@@ -62,29 +76,13 @@ func main() {
}
// authenticate before accepting user input
if f, err := os.Open(hsuConfFile); err != nil {
log.Fatal(err)
} else if fid, ok := mustParseConfig(f, puid); !ok {
log.Fatalf("uid %d is not in the hsurc file", puid)
} else {
uid += fid * 10000
}
// allowed aid range 0 to 9999
if as, ok := os.LookupEnv(envAID); !ok {
log.Fatal("HAKUREI_APP_ID not set")
} else if aid, err := parseUint32Fast(as); err != nil || aid < 0 || aid > 9999 {
log.Fatal("invalid aid")
} else {
uid += aid
}
userid := mustParseConfig(puid)
// pass through setup fd to shim
var shimSetupFd string
if s, ok := os.LookupEnv(envShim); !ok {
// hakurei requests target uid
// print resolved uid and exit
fmt.Print(uid)
// hakurei requests hsurc user id
fmt.Print(userid)
os.Exit(0)
} else if len(s) != 1 || s[0] > '9' || s[0] < '3' {
log.Fatal("HAKUREI_SHIM holds an invalid value")
@@ -92,6 +90,24 @@ func main() {
shimSetupFd = s
}
// start is going ahead at this point
identity := mustReadIdentity()
const (
// first possible uid outcome
uidStart = 10000
// last possible uid outcome
uidEnd = 999919999
)
// cast to int for use with library functions
uid := int(toUser(userid, identity))
// final bounds check to catch any bugs
if uid < uidStart || uid >= uidEnd {
panic("uid out of bounds")
}
// supplementary groups
var suppGroups, suppCurrent []int
@@ -119,11 +135,6 @@ func main() {
suppGroups = []int{uid}
}
// final bounds check to catch any bugs
if uid < 1000000 || uid >= 2000000 {
panic("uid out of bounds")
}
// careful! users in the allowlist is effectively allowed to drop groups via hsu
if err := syscall.Setresgid(uid, uid, uid); err != nil {

View File

@@ -19,5 +19,5 @@ buildGoModule {
ldflags = lib.attrsets.foldlAttrs (
ldflags: name: value:
ldflags ++ [ "-X main.${name}=${value}" ]
) [ "-s -w" ] { hmain = "${hakurei}/libexec/hakurei"; };
) [ "-s -w" ] { hakureiPath = "${hakurei}/libexec/hakurei"; };
}

View File

@@ -6,62 +6,128 @@ import (
"fmt"
"io"
"log"
"math"
"os"
"strings"
)
func parseUint32Fast(s string) (int, error) {
const (
// useridStart is the first userid.
useridStart = 0
// useridEnd is the last userid.
useridEnd = useridStart + rangeSize - 1
)
// parseUint32Fast parses a string representation of an unsigned 32-bit integer value
// using the fast path only. This limits the range of values it is defined in.
func parseUint32Fast(s string) (uint32, error) {
sLen := len(s)
if sLen < 1 {
return -1, errors.New("zero length string")
return 0, errors.New("zero length string")
}
if sLen > 10 {
return -1, errors.New("string too long")
return 0, errors.New("string too long")
}
n := 0
var n uint32
for i, ch := range []byte(s) {
ch -= '0'
if ch > 9 {
return -1, fmt.Errorf("invalid character '%s' at index %d", string(ch+'0'), i)
return 0, fmt.Errorf("invalid character '%s' at index %d", string(ch+'0'), i)
}
n = n*10 + int(ch)
n = n*10 + uint32(ch)
}
return n, nil
}
func parseConfig(r io.Reader, puid int) (fid int, ok bool, err error) {
// parseConfig reads a list of allowed users from r until it encounters puid or [io.EOF].
//
// Each line of the file specifies a hakurei userid to kernel uid mapping. A line consists
// of the string representation of the uid of the user wishing to start hakurei containers,
// followed by a space, followed by the string representation of its userid. Duplicate uid
// entries are ignored, with the first occurrence taking effect.
//
// All string representations are parsed by calling parseUint32Fast.
func parseConfig(r io.Reader, puid uint32) (userid uint32, ok bool, err error) {
s := bufio.NewScanner(r)
var line, puid0 int
var (
line uintptr
puid0 uint32
)
for s.Scan() {
line++
// <puid> <fid>
// <puid> <userid>
lf := strings.SplitN(s.Text(), " ", 2)
if len(lf) != 2 {
return -1, false, fmt.Errorf("invalid entry on line %d", line)
return useridEnd + 1, false, fmt.Errorf("invalid entry on line %d", line)
}
puid0, err = parseUint32Fast(lf[0])
if err != nil || puid0 < 1 {
return -1, false, fmt.Errorf("invalid parent uid on line %d", line)
return useridEnd + 1, false, fmt.Errorf("invalid parent uid on line %d", line)
}
ok = puid0 == puid
if ok {
// allowed fid range 0 to 99
if fid, err = parseUint32Fast(lf[1]); err != nil || fid < 0 || fid > 99 {
return -1, false, fmt.Errorf("invalid identity on line %d", line)
// userid bound to a range, uint32 size allows this to be increased if needed
if userid, err = parseUint32Fast(lf[1]); err != nil ||
userid < useridStart || userid > useridEnd {
return useridEnd + 1, false, fmt.Errorf("invalid userid on line %d", line)
}
return
}
}
return -1, false, s.Err()
return useridEnd + 1, false, s.Err()
}
func mustParseConfig(r io.Reader, puid int) (int, bool) {
fid, ok, err := parseConfig(r, puid)
if err != nil {
// hsuConfPath is an absolute pathname to the hsu configuration file.
// Its contents are interpreted by parseConfig.
const hsuConfPath = "/etc/hsurc"
// mustParseConfig calls parseConfig to interpret the contents of hsuConfPath,
// terminating the program if an error is encountered, the syntax is incorrect,
// or the current user is not authorised to use hsu because its uid is missing.
//
// Therefore, code after this function call can assume an authenticated state.
//
// mustParseConfig returns the userid value of the current user.
func mustParseConfig(puid int) (userid uint32) {
if puid > math.MaxUint32 {
log.Fatalf("got impossible uid %d", puid)
}
var ok bool
if f, err := os.Open(hsuConfPath); err != nil {
log.Fatal(err)
} else if userid, ok, err = parseConfig(f, uint32(puid)); err != nil {
log.Fatal(err)
} else if err = f.Close(); err != nil {
log.Fatal(err)
}
return fid, ok
if !ok {
log.Fatalf("uid %d is not in the hsurc file", puid)
}
return
}
// envIdentity is the name of the environment variable holding a
// string representation of the current application identity.
var envIdentity = "HAKUREI_IDENTITY"
// mustReadIdentity calls parseUint32Fast to interpret the value stored in envIdentity,
// terminating the program if the value is not set, malformed, or out of bounds.
func mustReadIdentity() uint32 {
// ranges defined in hst and copied to this package to avoid importing hst
if as, ok := os.LookupEnv(envIdentity); !ok {
log.Fatal("HAKUREI_IDENTITY not set")
panic("unreachable")
} else if identity, err := parseUint32Fast(as); err != nil ||
identity < identityStart || identity > identityEnd {
log.Fatal("invalid identity")
panic("unreachable")
} else {
return identity
}
}

View File

@@ -2,94 +2,105 @@ package main
import (
"bytes"
"math"
"strconv"
"testing"
)
func Test_parseUint32Fast(t *testing.T) {
func TestParseUint32Fast(t *testing.T) {
t.Parallel()
t.Run("zero-length", func(t *testing.T) {
t.Parallel()
if _, err := parseUint32Fast(""); err == nil || err.Error() != "zero length string" {
t.Errorf(`parseUint32Fast(""): error = %v`, err)
return
}
})
t.Run("overflow", func(t *testing.T) {
t.Parallel()
if _, err := parseUint32Fast("10000000000"); err == nil || err.Error() != "string too long" {
t.Errorf("parseUint32Fast: error = %v", err)
return
}
})
t.Run("invalid byte", func(t *testing.T) {
t.Parallel()
if _, err := parseUint32Fast("meow"); err == nil || err.Error() != "invalid character 'm' at index 0" {
t.Errorf(`parseUint32Fast("meow"): error = %v`, err)
return
}
})
t.Run("full range", func(t *testing.T) {
testRange := func(i, end int) {
t.Run("range", func(t *testing.T) {
t.Parallel()
testRange := func(i, end uint32) {
for ; i < end; i++ {
s := strconv.Itoa(i)
s := strconv.Itoa(int(i))
w := i
t.Run("parse "+s, func(t *testing.T) {
t.Parallel()
v, err := parseUint32Fast(s)
if err != nil {
t.Errorf("parseUint32Fast(%q): error = %v",
s, err)
t.Errorf("parseUint32Fast(%q): error = %v", s, err)
return
}
if v != w {
t.Errorf("parseUint32Fast(%q): got %v",
s, v)
t.Errorf("parseUint32Fast(%q): got %v", s, v)
return
}
})
}
}
testRange(0, 5000)
testRange(105000, 110000)
testRange(23005000, 23010000)
testRange(456005000, 456010000)
testRange(7890005000, 7890010000)
testRange(0, 2500)
testRange(23002500, 23005000)
testRange(math.MaxUint32-2500, math.MaxUint32)
})
}
func Test_parseConfig(t *testing.T) {
func TestParseConfig(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
puid, want int
puid, want uint32
wantErr string
rc string
}{
{"empty", 0, -1, "", ``},
{"invalid field", 0, -1, "invalid entry on line 1", `9`},
{"invalid puid", 0, -1, "invalid parent uid on line 1", `f 9`},
{"invalid fid", 1000, -1, "invalid identity on line 1", `1000 f`},
{"empty", 0, useridEnd + 1, "", ``},
{"invalid field", 0, useridEnd + 1, "invalid entry on line 1", `9`},
{"invalid puid", 0, useridEnd + 1, "invalid parent uid on line 1", `f 9`},
{"invalid userid", 1000, useridEnd + 1, "invalid userid on line 1", `1000 f`},
{"match", 1000, 0, "", `1000 0`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
fid, ok, err := parseConfig(bytes.NewBufferString(tc.rc), tc.puid)
t.Parallel()
userid, ok, err := parseConfig(bytes.NewBufferString(tc.rc), tc.puid)
if err == nil && tc.wantErr != "" {
t.Errorf("parseConfig: error = %v; wantErr %q",
err, tc.wantErr)
t.Errorf("parseConfig: error = %v; want %q", err, tc.wantErr)
return
}
if err != nil && err.Error() != tc.wantErr {
t.Errorf("parseConfig: error = %q; wantErr %q",
err, tc.wantErr)
t.Errorf("parseConfig: error = %q; want %q", err, tc.wantErr)
return
}
if ok == (tc.want == -1) {
t.Errorf("parseConfig: ok = %v; want %v",
ok, tc.want)
if ok == (tc.want == useridEnd+1) {
t.Errorf("parseConfig: ok = %v; want %v", ok, tc.want)
return
}
if fid != tc.want {
t.Errorf("parseConfig: fid = %v; want %v",
fid, tc.want)
if userid != tc.want {
t.Errorf("parseConfig: %v; want %v", userid, tc.want)
}
})
}

View File

@@ -1,20 +0,0 @@
package main
import (
"log"
"path"
)
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
var (
hmain = compPoison
)
func mustCheckPath(p string) string {
if p != compPoison && p != "" && path.IsAbs(p) {
return p
}
log.Fatal("this program is compiled incorrectly")
return compPoison
}

View File

@@ -7,6 +7,7 @@ import (
)
func TestBuild(t *testing.T) {
t.Parallel()
c := command.New(nil, nil, "test", nil)
stubHandler := func([]string) error { panic("unreachable") }

View File

@@ -14,6 +14,8 @@ import (
)
func TestParse(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
buildTree func(wout, wlog io.Writer) command.Command
@@ -251,6 +253,7 @@ Commands:
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
wout, wlog := new(bytes.Buffer), new(bytes.Buffer)
c := tc.buildTree(wout, wlog)

View File

@@ -6,15 +6,19 @@ import (
)
func TestParseUnreachable(t *testing.T) {
t.Parallel()
// top level bypasses name matching and recursive calls to Parse
// returns when encountering zero-length args
t.Run("zero-length args", func(t *testing.T) {
t.Parallel()
defer checkRecover(t, "Parse", "attempted to parse with zero length args")
_ = newNode(panicWriter{}, nil, " ", " ").Parse(nil)
})
// top level must not have siblings
t.Run("toplevel siblings", func(t *testing.T) {
t.Parallel()
defer checkRecover(t, "Parse", "invalid toplevel state")
n := newNode(panicWriter{}, nil, " ", "")
n.append(newNode(panicWriter{}, nil, " ", " "))
@@ -23,6 +27,7 @@ func TestParseUnreachable(t *testing.T) {
// a node with descendents must not have a direct handler
t.Run("sub handle conflict", func(t *testing.T) {
t.Parallel()
defer checkRecover(t, "Parse", "invalid subcommand tree state")
n := newNode(panicWriter{}, nil, " ", " ")
n.adopt(newNode(panicWriter{}, nil, " ", " "))
@@ -32,6 +37,7 @@ func TestParseUnreachable(t *testing.T) {
// this would only happen if a node was matched twice
t.Run("parsed flag set", func(t *testing.T) {
t.Parallel()
defer checkRecover(t, "Parse", "invalid set state")
n := newNode(panicWriter{}, nil, " ", "")
set := flag.NewFlagSet("parsed", flag.ContinueOnError)

View File

@@ -3,16 +3,18 @@ package container
import (
"encoding/gob"
"fmt"
"io/fs"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
)
func init() { gob.Register(new(AutoEtcOp)) }
// Etc appends an [Op] that expands host /etc into a toplevel symlink mirror with /etc semantics.
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
func (f *Ops) Etc(host *Absolute, prefix string) *Ops {
func (f *Ops) Etc(host *check.Absolute, prefix string) *Ops {
e := &AutoEtcOp{prefix}
f.Mkdir(AbsFHSEtc, 0755)
f.Mkdir(fhs.AbsEtc, 0755)
f.Bind(host, e.hostPath(), 0)
*f = append(*f, e)
return f
@@ -24,18 +26,18 @@ func (e *AutoEtcOp) Valid() bool { return e != ni
func (e *AutoEtcOp) early(*setupState, syscallDispatcher) error { return nil }
func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
if state.nonrepeatable&nrAutoEtc != 0 {
return msg.WrapErr(fs.ErrInvalid, "autoetc is not repeatable")
return OpRepeatError("autoetc")
}
state.nonrepeatable |= nrAutoEtc
const target = sysrootPath + FHSEtc
const target = sysrootPath + fhs.Etc
rel := e.hostRel() + "/"
if err := k.mkdirAll(target, 0755); err != nil {
return wrapErrSelf(err)
return err
}
if d, err := k.readdir(toSysroot(e.hostPath().String())); err != nil {
return wrapErrSelf(err)
return err
} else {
for _, ent := range d {
n := ent.Name()
@@ -43,13 +45,13 @@ func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
case ".host", "passwd", "group":
case "mtab":
if err = k.symlink(FHSProc+"mounts", target+n); err != nil {
return wrapErrSelf(err)
if err = k.symlink(fhs.Proc+"mounts", target+n); err != nil {
return err
}
default:
if err = k.symlink(rel+n, target+n); err != nil {
return wrapErrSelf(err)
return err
}
}
}
@@ -57,13 +59,14 @@ func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
return nil
}
func (e *AutoEtcOp) late(*setupState, syscallDispatcher) error { return nil }
func (e *AutoEtcOp) hostPath() *Absolute { return AbsFHSEtc.Append(e.hostRel()) }
func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix }
func (e *AutoEtcOp) hostPath() *check.Absolute { return fhs.AbsEtc.Append(e.hostRel()) }
func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix }
func (e *AutoEtcOp) Is(op Op) bool {
ve, ok := op.(*AutoEtcOp)
return ok && e.Valid() && ve.Valid() && *e == *ve
}
func (*AutoEtcOp) prefix() string { return "setting up" }
func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }
func (*AutoEtcOp) prefix() (string, bool) { return "setting up", true }
func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }

View File

@@ -2,14 +2,19 @@ package container
import (
"errors"
"io/fs"
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
)
func TestAutoEtcOp(t *testing.T) {
t.Parallel()
t.Run("nonrepeatable", func(t *testing.T) {
wantErr := msg.WrapErr(fs.ErrInvalid, "autoetc is not repeatable")
t.Parallel()
wantErr := OpRepeatError("autoetc")
if err := (&AutoEtcOp{Prefix: "81ceabb30d37bbdb3868004629cb84e9"}).apply(&setupState{nonrepeatable: nrAutoEtc}, nil); !errors.Is(err, wantErr) {
t.Errorf("apply: error = %v, want %v", err, wantErr)
}
@@ -18,22 +23,22 @@ func TestAutoEtcOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{
{"mkdirAll", new(Params), &AutoEtcOp{
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
}, nil, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, errUnique},
}, wrapErrSelf(errUnique)},
}, nil, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, stub.UniqueError(3)),
}, stub.UniqueError(3)},
{"readdir", new(Params), &AutoEtcOp{
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
}, nil, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(), errUnique},
}, wrapErrSelf(errUnique)},
}, nil, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil),
call("readdir", stub.ExpectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(), stub.UniqueError(2)),
}, stub.UniqueError(2)},
{"symlink", new(Params), &AutoEtcOp{
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
}, nil, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
}, nil, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil),
call("readdir", stub.ExpectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
@@ -41,15 +46,15 @@ func TestAutoEtcOp(t *testing.T) {
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, errUnique},
}, wrapErrSelf(errUnique)},
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, stub.UniqueError(1)),
}, stub.UniqueError(1)},
{"symlink mtab", new(Params), &AutoEtcOp{
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
}, nil, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
}, nil, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil),
call("readdir", stub.ExpectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
@@ -57,41 +62,41 @@ func TestAutoEtcOp(t *testing.T) {
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil},
{"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, errUnique},
}, wrapErrSelf(errUnique)},
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil),
call("symlink", stub.ExpectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, stub.UniqueError(0)),
}, stub.UniqueError(0)},
{"success nested", new(Params), &AutoEtcOp{
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
}, nil, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
}, nil, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil),
call("readdir", stub.ExpectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
@@ -99,79 +104,79 @@ func TestAutoEtcOp(t *testing.T) {
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil},
{"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil},
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil),
call("symlink", stub.ExpectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil),
}, nil},
{"success", new(Params), &AutoEtcOp{
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
}, nil, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(
}, nil, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil),
call("readdir", stub.ExpectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
@@ -179,72 +184,72 @@ func TestAutoEtcOp(t *testing.T) {
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil},
{"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil},
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil),
call("symlink", stub.ExpectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil),
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil),
}, nil},
})
@@ -255,11 +260,11 @@ func TestAutoEtcOp(t *testing.T) {
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"pd", new(Ops).Etc(MustAbs("/etc/"), "048090b6ed8f9ebb10e275ff5d8c0659"), Ops{
&MkdirOp{Path: MustAbs("/etc/"), Perm: 0755},
{"pd", new(Ops).Etc(check.MustAbs("/etc/"), "048090b6ed8f9ebb10e275ff5d8c0659"), Ops{
&MkdirOp{Path: check.MustAbs("/etc/"), Perm: 0755},
&BindMountOp{
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Source: check.MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
},
&AutoEtcOp{Prefix: "048090b6ed8f9ebb10e275ff5d8c0659"},
}},
@@ -278,6 +283,7 @@ func TestAutoEtcOp(t *testing.T) {
})
t.Run("host path rel", func(t *testing.T) {
t.Parallel()
op := &AutoEtcOp{Prefix: "048090b6ed8f9ebb10e275ff5d8c0659"}
wantHostPath := "/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"
wantHostRel := ".host/048090b6ed8f9ebb10e275ff5d8c0659"

View File

@@ -3,42 +3,46 @@ package container
import (
"encoding/gob"
"fmt"
"io/fs"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/message"
)
func init() { gob.Register(new(AutoRootOp)) }
// Root appends an [Op] that expands a directory into a toplevel bind mount mirror on container root.
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
func (f *Ops) Root(host *Absolute, flags int) *Ops {
func (f *Ops) Root(host *check.Absolute, flags int) *Ops {
*f = append(*f, &AutoRootOp{host, flags, nil})
return f
}
type AutoRootOp struct {
Host *Absolute
Host *check.Absolute
// passed through to bindMount
Flags int
// obtained during early;
// these wrap the underlying Op because BindMountOp is relatively complex,
// so duplicating that code would be unwise
resolved []Op
resolved []*BindMountOp
}
func (r *AutoRootOp) Valid() bool { return r != nil && r.Host != nil }
func (r *AutoRootOp) early(state *setupState, k syscallDispatcher) error {
if d, err := k.readdir(r.Host.String()); err != nil {
return wrapErrSelf(err)
return err
} else {
r.resolved = make([]Op, 0, len(d))
r.resolved = make([]*BindMountOp, 0, len(d))
for _, ent := range d {
name := ent.Name()
if IsAutoRootBindable(name) {
if IsAutoRootBindable(state, name) {
// careful: the Valid method is skipped, make sure this is always valid
op := &BindMountOp{
Source: r.Host.Append(name),
Target: AbsFHSRoot.Append(name),
Target: fhs.AbsRoot.Append(name),
Flags: r.Flags,
}
if err = op.early(state, k); err != nil {
@@ -53,18 +57,19 @@ func (r *AutoRootOp) early(state *setupState, k syscallDispatcher) error {
func (r *AutoRootOp) apply(state *setupState, k syscallDispatcher) error {
if state.nonrepeatable&nrAutoRoot != 0 {
return msg.WrapErr(fs.ErrInvalid, "autoroot is not repeatable")
return OpRepeatError("autoroot")
}
state.nonrepeatable |= nrAutoRoot
for _, op := range r.resolved {
k.verbosef("%s %s", op.prefix(), op)
// these are exclusively BindMountOp, do not attempt to print identifying message
if err := op.apply(state, k); err != nil {
return err
}
}
return nil
}
func (r *AutoRootOp) late(*setupState, syscallDispatcher) error { return nil }
func (r *AutoRootOp) Is(op Op) bool {
vr, ok := op.(*AutoRootOp)
@@ -72,13 +77,13 @@ func (r *AutoRootOp) Is(op Op) bool {
r.Host.Is(vr.Host) &&
r.Flags == vr.Flags
}
func (*AutoRootOp) prefix() string { return "setting up" }
func (*AutoRootOp) prefix() (string, bool) { return "setting up", true }
func (r *AutoRootOp) String() string {
return fmt.Sprintf("auto root %q flags %#x", r.Host, r.Flags)
}
// IsAutoRootBindable returns whether a dir entry name is selected for AutoRoot.
func IsAutoRootBindable(name string) bool {
func IsAutoRootBindable(msg message.Msg, name string) bool {
switch name {
case "proc", "dev", "tmp", "mnt", "etc":

View File

@@ -2,14 +2,19 @@ package container
import (
"errors"
"io/fs"
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/std"
"hakurei.app/container/stub"
"hakurei.app/message"
)
func TestAutoRootOp(t *testing.T) {
t.Run("nonrepeatable", func(t *testing.T) {
wantErr := msg.WrapErr(fs.ErrInvalid, "autoroot is not repeatable")
t.Parallel()
wantErr := OpRepeatError("autoroot")
if err := new(AutoRootOp).apply(&setupState{nonrepeatable: nrAutoRoot}, nil); !errors.Is(err, wantErr) {
t.Errorf("apply: error = %v, want %v", err, wantErr)
}
@@ -17,116 +22,115 @@ func TestAutoRootOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{
{"readdir", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable,
}, []kexpect{
{"readdir", expectArgs{"/"}, stubDir(), errUnique},
}, wrapErrSelf(errUnique), nil, nil},
Host: check.MustAbs("/"),
Flags: std.BindWritable,
}, []stub.Call{
call("readdir", stub.ExpectArgs{"/"}, stubDir(), stub.UniqueError(2)),
}, stub.UniqueError(2), nil, nil},
{"early", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable,
}, []kexpect{
{"readdir", expectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil},
{"evalSymlinks", expectArgs{"/bin"}, "", errUnique},
}, wrapErrSelf(errUnique), nil, nil},
Host: check.MustAbs("/"),
Flags: std.BindWritable,
}, []stub.Call{
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
call("evalSymlinks", stub.ExpectArgs{"/bin"}, "", stub.UniqueError(1)),
}, stub.UniqueError(1), nil, nil},
{"apply", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable,
}, []kexpect{
{"readdir", expectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil},
{"evalSymlinks", expectArgs{"/bin"}, "/usr/bin", nil},
{"evalSymlinks", expectArgs{"/home"}, "/home", nil},
{"evalSymlinks", expectArgs{"/lib64"}, "/lib64", nil},
{"evalSymlinks", expectArgs{"/lost+found"}, "/lost+found", nil},
{"evalSymlinks", expectArgs{"/nix"}, "/nix", nil},
{"evalSymlinks", expectArgs{"/root"}, "/root", nil},
{"evalSymlinks", expectArgs{"/run"}, "/run", nil},
{"evalSymlinks", expectArgs{"/srv"}, "/srv", nil},
{"evalSymlinks", expectArgs{"/sys"}, "/sys", nil},
{"evalSymlinks", expectArgs{"/usr"}, "/usr", nil},
{"evalSymlinks", expectArgs{"/var"}, "/var", nil},
}, nil, []kexpect{
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr/bin"), MustAbs("/bin"), MustAbs("/bin"), BindWritable}}}, nil, nil},
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(false), errUnique},
}, wrapErrSelf(errUnique)},
Host: check.MustAbs("/"),
Flags: std.BindWritable,
}, []stub.Call{
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
call("evalSymlinks", stub.ExpectArgs{"/bin"}, "/usr/bin", nil),
call("evalSymlinks", stub.ExpectArgs{"/home"}, "/home", nil),
call("evalSymlinks", stub.ExpectArgs{"/lib64"}, "/lib64", nil),
call("evalSymlinks", stub.ExpectArgs{"/lost+found"}, "/lost+found", nil),
call("evalSymlinks", stub.ExpectArgs{"/nix"}, "/nix", nil),
call("evalSymlinks", stub.ExpectArgs{"/root"}, "/root", nil),
call("evalSymlinks", stub.ExpectArgs{"/run"}, "/run", nil),
call("evalSymlinks", stub.ExpectArgs{"/srv"}, "/srv", nil),
call("evalSymlinks", stub.ExpectArgs{"/sys"}, "/sys", nil),
call("evalSymlinks", stub.ExpectArgs{"/usr"}, "/usr", nil),
call("evalSymlinks", stub.ExpectArgs{"/var"}, "/var", nil),
}, nil, []stub.Call{
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(false), stub.UniqueError(0)),
}, stub.UniqueError(0)},
{"success pd", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable,
}, []kexpect{
{"readdir", expectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil},
{"evalSymlinks", expectArgs{"/bin"}, "/usr/bin", nil},
{"evalSymlinks", expectArgs{"/home"}, "/home", nil},
{"evalSymlinks", expectArgs{"/lib64"}, "/lib64", nil},
{"evalSymlinks", expectArgs{"/lost+found"}, "/lost+found", nil},
{"evalSymlinks", expectArgs{"/nix"}, "/nix", nil},
{"evalSymlinks", expectArgs{"/root"}, "/root", nil},
{"evalSymlinks", expectArgs{"/run"}, "/run", nil},
{"evalSymlinks", expectArgs{"/srv"}, "/srv", nil},
{"evalSymlinks", expectArgs{"/sys"}, "/sys", nil},
{"evalSymlinks", expectArgs{"/usr"}, "/usr", nil},
{"evalSymlinks", expectArgs{"/var"}, "/var", nil},
}, nil, []kexpect{
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr/bin"), MustAbs("/bin"), MustAbs("/bin"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4004), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/home"), MustAbs("/home"), MustAbs("/home"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/home"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/home", "/sysroot/home", uintptr(0x4004), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/lib64"), MustAbs("/lib64"), MustAbs("/lib64"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/lib64"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/lib64", "/sysroot/lib64", uintptr(0x4004), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/lost+found"), MustAbs("/lost+found"), MustAbs("/lost+found"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/lost+found"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/lost+found", "/sysroot/lost+found", uintptr(0x4004), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/nix"), MustAbs("/nix"), MustAbs("/nix"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/nix"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/nix", "/sysroot/nix", uintptr(0x4004), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/root"), MustAbs("/root"), MustAbs("/root"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/root"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/root", "/sysroot/root", uintptr(0x4004), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/run"), MustAbs("/run"), MustAbs("/run"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/run"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/run", "/sysroot/run", uintptr(0x4004), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/srv"), MustAbs("/srv"), MustAbs("/srv"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/srv"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/srv", "/sysroot/srv", uintptr(0x4004), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/sys"), MustAbs("/sys"), MustAbs("/sys"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/sys"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/sys", "/sysroot/sys", uintptr(0x4004), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr"), MustAbs("/usr"), MustAbs("/usr"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/usr"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/usr", "/sysroot/usr", uintptr(0x4004), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var"), MustAbs("/var"), MustAbs("/var"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/var"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var", "/sysroot/var", uintptr(0x4004), false}, nil, nil},
Host: check.MustAbs("/"),
Flags: std.BindWritable,
}, []stub.Call{
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
call("evalSymlinks", stub.ExpectArgs{"/bin"}, "/usr/bin", nil),
call("evalSymlinks", stub.ExpectArgs{"/home"}, "/home", nil),
call("evalSymlinks", stub.ExpectArgs{"/lib64"}, "/lib64", nil),
call("evalSymlinks", stub.ExpectArgs{"/lost+found"}, "/lost+found", nil),
call("evalSymlinks", stub.ExpectArgs{"/nix"}, "/nix", nil),
call("evalSymlinks", stub.ExpectArgs{"/root"}, "/root", nil),
call("evalSymlinks", stub.ExpectArgs{"/run"}, "/run", nil),
call("evalSymlinks", stub.ExpectArgs{"/srv"}, "/srv", nil),
call("evalSymlinks", stub.ExpectArgs{"/sys"}, "/sys", nil),
call("evalSymlinks", stub.ExpectArgs{"/usr"}, "/usr", nil),
call("evalSymlinks", stub.ExpectArgs{"/var"}, "/var", nil),
}, nil, []stub.Call{
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/usr/bin", "/sysroot/bin", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4004), false}, nil, nil),
call("stat", stub.ExpectArgs{"/host/home"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/home", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/home", "/sysroot/home", uintptr(0x4004), false}, nil, nil),
call("stat", stub.ExpectArgs{"/host/lib64"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/lib64", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/lib64", "/sysroot/lib64", uintptr(0x4004), false}, nil, nil),
call("stat", stub.ExpectArgs{"/host/lost+found"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/lost+found", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/lost+found", "/sysroot/lost+found", uintptr(0x4004), false}, nil, nil),
call("stat", stub.ExpectArgs{"/host/nix"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/nix", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", uintptr(0x4004), false}, nil, nil),
call("stat", stub.ExpectArgs{"/host/root"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/root", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/root", "/sysroot/root", uintptr(0x4004), false}, nil, nil),
call("stat", stub.ExpectArgs{"/host/run"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/run", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/run", "/sysroot/run", uintptr(0x4004), false}, nil, nil),
call("stat", stub.ExpectArgs{"/host/srv"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/srv", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/srv", "/sysroot/srv", uintptr(0x4004), false}, nil, nil),
call("stat", stub.ExpectArgs{"/host/sys"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/sys", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/sys", "/sysroot/sys", uintptr(0x4004), false}, nil, nil),
call("stat", stub.ExpectArgs{"/host/usr"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/usr", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/usr", "/sysroot/usr", uintptr(0x4004), false}, nil, nil),
call("stat", stub.ExpectArgs{"/host/var"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/var", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var", "/sysroot/var", uintptr(0x4004), false}, nil, nil),
}, nil},
{"success", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: MustAbs("/var/lib/planterette/base/debian:f92c9052"),
}, []kexpect{
{"readdir", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, stubDir("bin", "dev", "etc", "home", "lib64",
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/bin"}, "/var/lib/planterette/base/debian:f92c9052/usr/bin", nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/home"}, "/var/lib/planterette/base/debian:f92c9052/home", nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/lib64"}, "/var/lib/planterette/base/debian:f92c9052/lib64", nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/lost+found"}, "/var/lib/planterette/base/debian:f92c9052/lost+found", nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/nix"}, "/var/lib/planterette/base/debian:f92c9052/nix", nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/root"}, "/var/lib/planterette/base/debian:f92c9052/root", nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/run"}, "/var/lib/planterette/base/debian:f92c9052/run", nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/srv"}, "/var/lib/planterette/base/debian:f92c9052/srv", nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/sys"}, "/var/lib/planterette/base/debian:f92c9052/sys", nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/usr"}, "/var/lib/planterette/base/debian:f92c9052/usr", nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/var"}, "/var/lib/planterette/base/debian:f92c9052/var", nil},
}, nil, []kexpect{
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/usr/bin"), MustAbs("/var/lib/planterette/base/debian:f92c9052/bin"), MustAbs("/bin"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/home"), MustAbs("/var/lib/planterette/base/debian:f92c9052/home"), MustAbs("/home"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home", "/sysroot/home", uintptr(0x4005), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/lib64"), MustAbs("/var/lib/planterette/base/debian:f92c9052/lib64"), MustAbs("/lib64"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64", "/sysroot/lib64", uintptr(0x4005), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/lost+found"), MustAbs("/var/lib/planterette/base/debian:f92c9052/lost+found"), MustAbs("/lost+found"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found", "/sysroot/lost+found", uintptr(0x4005), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/nix"), MustAbs("/var/lib/planterette/base/debian:f92c9052/nix"), MustAbs("/nix"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix", "/sysroot/nix", uintptr(0x4005), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/root"), MustAbs("/var/lib/planterette/base/debian:f92c9052/root"), MustAbs("/root"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root", "/sysroot/root", uintptr(0x4005), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/run"), MustAbs("/var/lib/planterette/base/debian:f92c9052/run"), MustAbs("/run"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run", "/sysroot/run", uintptr(0x4005), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/srv"), MustAbs("/var/lib/planterette/base/debian:f92c9052/srv"), MustAbs("/srv"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv", "/sysroot/srv", uintptr(0x4005), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/sys"), MustAbs("/var/lib/planterette/base/debian:f92c9052/sys"), MustAbs("/sys"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys", "/sysroot/sys", uintptr(0x4005), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/usr"), MustAbs("/var/lib/planterette/base/debian:f92c9052/usr"), MustAbs("/usr"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr", "/sysroot/usr", uintptr(0x4005), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/var"), MustAbs("/var/lib/planterette/base/debian:f92c9052/var"), MustAbs("/var"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var", "/sysroot/var", uintptr(0x4005), false}, nil, nil},
Host: check.MustAbs("/var/lib/planterette/base/debian:f92c9052"),
}, []stub.Call{
call("readdir", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052"}, stubDir("bin", "dev", "etc", "home", "lib64",
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/bin"}, "/var/lib/planterette/base/debian:f92c9052/usr/bin", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/home"}, "/var/lib/planterette/base/debian:f92c9052/home", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/lib64"}, "/var/lib/planterette/base/debian:f92c9052/lib64", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/lost+found"}, "/var/lib/planterette/base/debian:f92c9052/lost+found", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/nix"}, "/var/lib/planterette/base/debian:f92c9052/nix", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/root"}, "/var/lib/planterette/base/debian:f92c9052/root", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/run"}, "/var/lib/planterette/base/debian:f92c9052/run", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/srv"}, "/var/lib/planterette/base/debian:f92c9052/srv", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/sys"}, "/var/lib/planterette/base/debian:f92c9052/sys", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/usr"}, "/var/lib/planterette/base/debian:f92c9052/usr", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/var"}, "/var/lib/planterette/base/debian:f92c9052/var", nil),
}, nil, []stub.Call{
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin", "/sysroot/bin", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/home", "/sysroot/home", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home", "/sysroot/home", uintptr(0x4005), false}, nil, nil),
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/lib64", "/sysroot/lib64", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64", "/sysroot/lib64", uintptr(0x4005), false}, nil, nil),
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/lost+found", "/sysroot/lost+found", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found", "/sysroot/lost+found", uintptr(0x4005), false}, nil, nil),
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/nix", "/sysroot/nix", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix", "/sysroot/nix", uintptr(0x4005), false}, nil, nil),
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/root", "/sysroot/root", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root", "/sysroot/root", uintptr(0x4005), false}, nil, nil),
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/run", "/sysroot/run", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run", "/sysroot/run", uintptr(0x4005), false}, nil, nil),
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/srv", "/sysroot/srv", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv", "/sysroot/srv", uintptr(0x4005), false}, nil, nil),
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/sys", "/sysroot/sys", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys", "/sysroot/sys", uintptr(0x4005), false}, nil, nil),
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/usr", "/sysroot/usr", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr", "/sysroot/usr", uintptr(0x4005), false}, nil, nil),
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/var", "/sysroot/var", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var", "/sysroot/var", uintptr(0x4005), false}, nil, nil),
}, nil},
})
checkOpsValid(t, []opValidTestCase{
{"nil", (*AutoRootOp)(nil), false},
{"zero", new(AutoRootOp), false},
{"valid", &AutoRootOp{Host: MustAbs("/")}, true},
{"valid", &AutoRootOp{Host: check.MustAbs("/")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"pd", new(Ops).Root(MustAbs("/"), BindWritable), Ops{
{"pd", new(Ops).Root(check.MustAbs("/"), std.BindWritable), Ops{
&AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable,
Host: check.MustAbs("/"),
Flags: std.BindWritable,
},
}},
})
@@ -135,64 +139,74 @@ func TestAutoRootOp(t *testing.T) {
{"zero", new(AutoRootOp), new(AutoRootOp), false},
{"internal ne", &AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable,
Host: check.MustAbs("/"),
Flags: std.BindWritable,
}, &AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable,
resolved: []Op{new(BindMountOp)},
Host: check.MustAbs("/"),
Flags: std.BindWritable,
resolved: []*BindMountOp{new(BindMountOp)},
}, true},
{"flags differs", &AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable | BindDevice,
Host: check.MustAbs("/"),
Flags: std.BindWritable | std.BindDevice,
}, &AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable,
Host: check.MustAbs("/"),
Flags: std.BindWritable,
}, false},
{"host differs", &AutoRootOp{
Host: MustAbs("/tmp/"),
Flags: BindWritable,
Host: check.MustAbs("/tmp/"),
Flags: std.BindWritable,
}, &AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable,
Host: check.MustAbs("/"),
Flags: std.BindWritable,
}, false},
{"equals", &AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable,
Host: check.MustAbs("/"),
Flags: std.BindWritable,
}, &AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable,
Host: check.MustAbs("/"),
Flags: std.BindWritable,
}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"root", &AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable,
Host: check.MustAbs("/"),
Flags: std.BindWritable,
}, "setting up", `auto root "/" flags 0x2`},
})
}
func TestIsAutoRootBindable(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
want bool
log bool
}{
{"proc", false},
{"dev", false},
{"tmp", false},
{"mnt", false},
{"etc", false},
{"", false},
{"proc", false, false},
{"dev", false, false},
{"tmp", false, false},
{"mnt", false, false},
{"etc", false, false},
{"", false, true},
{"var", true},
{"var", true, false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := IsAutoRootBindable(tc.name); got != tc.want {
t.Parallel()
var msg message.Msg
if tc.log {
msg = &kstub{nil, nil, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { panic("unreachable") }, stub.Expect{Calls: []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"got unexpected root entry"}}, nil, nil),
}})}
}
if got := IsAutoRootBindable(msg, tc.name); got != tc.want {
t.Errorf("IsAutoRootBindable: %v, want %v", got, tc.want)
}
})

View File

@@ -49,41 +49,10 @@ func capset(hdrp *capHeader, datap *[2]capData) error {
}
// capBoundingSetDrop drops a capability from the calling thread's capability bounding set.
func capBoundingSetDrop(cap uintptr) error {
r, _, errno := syscall.Syscall(
syscall.SYS_PRCTL,
syscall.PR_CAPBSET_DROP,
cap, 0,
)
if r != 0 {
return errno
}
return nil
}
func capBoundingSetDrop(cap uintptr) error { return Prctl(syscall.PR_CAPBSET_DROP, cap, 0) }
// capAmbientClearAll clears the ambient capability set of the calling thread.
func capAmbientClearAll() error {
r, _, errno := syscall.Syscall(
syscall.SYS_PRCTL,
PR_CAP_AMBIENT,
PR_CAP_AMBIENT_CLEAR_ALL, 0,
)
if r != 0 {
return errno
}
return nil
}
func capAmbientClearAll() error { return Prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0) }
// capAmbientRaise adds to the ambient capability set of the calling thread.
func capAmbientRaise(cap uintptr) error {
r, _, errno := syscall.Syscall(
syscall.SYS_PRCTL,
PR_CAP_AMBIENT,
PR_CAP_AMBIENT_RAISE,
cap,
)
if r != 0 {
return errno
}
return nil
}
func capAmbientRaise(cap uintptr) error { return Prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, cap) }

View File

@@ -3,6 +3,8 @@ package container
import "testing"
func TestCapToIndex(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
cap uintptr
@@ -14,6 +16,7 @@ func TestCapToIndex(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := capToIndex(tc.cap); got != tc.want {
t.Errorf("capToIndex: %#x, want %#x", got, tc.want)
}
@@ -22,6 +25,8 @@ func TestCapToIndex(t *testing.T) {
}
func TestCapToMask(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
cap uintptr
@@ -33,6 +38,7 @@ func TestCapToMask(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := capToMask(tc.cap); got != tc.want {
t.Errorf("capToMask: %#x, want %#x", got, tc.want)
}

View File

@@ -1,4 +1,5 @@
package container
// Package check provides types yielding values checked to meet a condition.
package check
import (
"encoding/json"
@@ -11,9 +12,7 @@ import (
)
// AbsoluteError is returned by [NewAbs] and holds the invalid pathname.
type AbsoluteError struct {
Pathname string
}
type AbsoluteError struct{ Pathname string }
func (e *AbsoluteError) Error() string { return fmt.Sprintf("path %q is not absolute", e.Pathname) }
func (e *AbsoluteError) Is(target error) bool {
@@ -25,15 +24,13 @@ func (e *AbsoluteError) Is(target error) bool {
}
// Absolute holds a pathname checked to be absolute.
type Absolute struct {
pathname string
}
type Absolute struct{ pathname string }
// isAbs wraps [path.IsAbs] in case additional checks are added in the future.
func isAbs(pathname string) bool { return path.IsAbs(pathname) }
// unsafeAbs returns [check.Absolute] on any string value.
func unsafeAbs(pathname string) *Absolute { return &Absolute{pathname} }
func (a *Absolute) String() string {
if a.pathname == zeroString {
if a.pathname == "" {
panic("attempted use of zero Absolute")
}
return a.pathname
@@ -44,22 +41,22 @@ func (a *Absolute) Is(v *Absolute) bool {
return true
}
return a != nil && v != nil &&
a.pathname != zeroString && v.pathname != zeroString &&
a.pathname != "" && v.pathname != "" &&
a.pathname == v.pathname
}
// NewAbs checks pathname and returns a new [Absolute] if pathname is absolute.
func NewAbs(pathname string) (*Absolute, error) {
if !isAbs(pathname) {
if !path.IsAbs(pathname) {
return nil, &AbsoluteError{pathname}
}
return &Absolute{pathname}, nil
return unsafeAbs(pathname), nil
}
// MustAbs calls [NewAbs] and panics on error.
func MustAbs(pathname string) *Absolute {
if a, err := NewAbs(pathname); err != nil {
panic(err.Error())
panic(err)
} else {
return a
}
@@ -67,16 +64,16 @@ func MustAbs(pathname string) *Absolute {
// Append calls [path.Join] with [Absolute] as the first element.
func (a *Absolute) Append(elem ...string) *Absolute {
return &Absolute{path.Join(append([]string{a.String()}, elem...)...)}
return unsafeAbs(path.Join(append([]string{a.String()}, elem...)...))
}
// Dir calls [path.Dir] with [Absolute] as its argument.
func (a *Absolute) Dir() *Absolute { return &Absolute{path.Dir(a.String())} }
func (a *Absolute) Dir() *Absolute { return unsafeAbs(path.Dir(a.String())) }
func (a *Absolute) GobEncode() ([]byte, error) { return []byte(a.String()), nil }
func (a *Absolute) GobDecode(data []byte) error {
pathname := string(data)
if !isAbs(pathname) {
if !path.IsAbs(pathname) {
return &AbsoluteError{pathname}
}
a.pathname = pathname
@@ -89,7 +86,7 @@ func (a *Absolute) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &pathname); err != nil {
return err
}
if !isAbs(pathname) {
if !path.IsAbs(pathname) {
return &AbsoluteError{pathname}
}
a.pathname = pathname

View File

@@ -1,4 +1,4 @@
package container
package check_test
import (
"bytes"
@@ -9,9 +9,19 @@ import (
"strings"
"syscall"
"testing"
_ "unsafe" // for go:linkname
. "hakurei.app/container/check"
)
// unsafeAbs returns check.Absolute on any string value.
//
//go:linkname unsafeAbs hakurei.app/container/check.unsafeAbs
func unsafeAbs(pathname string) *Absolute
func TestAbsoluteError(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
@@ -21,8 +31,8 @@ func TestAbsoluteError(t *testing.T) {
}{
{"EINVAL", new(AbsoluteError), syscall.EINVAL, true},
{"not EINVAL", new(AbsoluteError), syscall.EBADE, false},
{"ne val", new(AbsoluteError), &AbsoluteError{"etc"}, false},
{"equals", &AbsoluteError{"etc"}, &AbsoluteError{"etc"}, true},
{"ne val", new(AbsoluteError), &AbsoluteError{Pathname: "etc"}, false},
{"equals", &AbsoluteError{Pathname: "etc"}, &AbsoluteError{Pathname: "etc"}, true},
}
for _, tc := range testCases {
@@ -32,14 +42,18 @@ func TestAbsoluteError(t *testing.T) {
}
t.Run("string", func(t *testing.T) {
t.Parallel()
want := `path "etc" is not absolute`
if got := (&AbsoluteError{"etc"}).Error(); got != want {
if got := (&AbsoluteError{Pathname: "etc"}).Error(); got != want {
t.Errorf("Error: %q, want %q", got, want)
}
})
}
func TestNewAbs(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
@@ -48,12 +62,14 @@ func TestNewAbs(t *testing.T) {
wantErr error
}{
{"good", "/etc", MustAbs("/etc"), nil},
{"not absolute", "etc", nil, &AbsoluteError{"etc"}},
{"zero", "", nil, &AbsoluteError{""}},
{"not absolute", "etc", nil, &AbsoluteError{Pathname: "etc"}},
{"zero", "", nil, &AbsoluteError{Pathname: ""}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, err := NewAbs(tc.pathname)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("NewAbs: %#v, want %#v", got, tc.want)
@@ -65,10 +81,12 @@ func TestNewAbs(t *testing.T) {
}
t.Run("must", func(t *testing.T) {
defer func() {
wantPanic := `path "etc" is not absolute`
t.Parallel()
if r := recover(); r != wantPanic {
defer func() {
wantPanic := &AbsoluteError{Pathname: "etc"}
if r := recover(); !reflect.DeepEqual(r, wantPanic) {
t.Errorf("MustAbs: panic = %v; want %v", r, wantPanic)
}
}()
@@ -79,13 +97,17 @@ func TestNewAbs(t *testing.T) {
func TestAbsoluteString(t *testing.T) {
t.Run("passthrough", func(t *testing.T) {
t.Parallel()
pathname := "/etc"
if got := (&Absolute{pathname}).String(); got != pathname {
if got := unsafeAbs(pathname).String(); got != pathname {
t.Errorf("String: %q, want %q", got, pathname)
}
})
t.Run("zero", func(t *testing.T) {
t.Parallel()
defer func() {
wantPanic := "attempted use of zero Absolute"
@@ -99,6 +121,8 @@ func TestAbsoluteString(t *testing.T) {
}
func TestAbsoluteIs(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
a, v *Absolute
@@ -114,6 +138,8 @@ func TestAbsoluteIs(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.a.Is(tc.v); got != tc.want {
t.Errorf("Is: %v, want %v", got, tc.want)
}
@@ -123,10 +149,12 @@ func TestAbsoluteIs(t *testing.T) {
type sCheck struct {
Pathname *Absolute `json:"val"`
Magic int `json:"magic"`
Magic uint64 `json:"magic"`
}
func TestCodecAbsolute(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
a *Absolute
@@ -143,31 +171,36 @@ func TestCodecAbsolute(t *testing.T) {
{"good", MustAbs("/etc"),
nil,
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\b\xff\x80\x00\x04/etc",
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x04\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x10\xff\x84\x01\x04/etc\x01\xfb\x01\x81\xda\x00\x00\x00",
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x04/etc\x01\xfc\xc0\xed\x00\x00\x00",
`"/etc"`, `{"val":"/etc","magic":3236757504}`},
{"not absolute", nil,
&AbsoluteError{"etc"},
&AbsoluteError{Pathname: "etc"},
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\a\xff\x80\x00\x03etc",
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x04\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x03etc\x01\xfb\x01\x81\xda\x00\x00\x00",
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x03etc\x01\xfb\x01\x81\xda\x00\x00\x00",
`"etc"`, `{"val":"etc","magic":3236757504}`},
{"zero", nil,
new(AbsoluteError),
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x04\xff\x80\x00\x00",
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x04\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\f\xff\x84\x01\x00\x01\xfb\x01\x81\xda\x00\x00\x00",
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\f\xff\x84\x01\x00\x01\xfb\x01\x81\xda\x00\x00\x00",
`""`, `{"val":"","magic":3236757504}`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("gob", func(t *testing.T) {
if tc.gob == "\x00" && tc.sGob == "\x00" {
// these values mark the current test to skip gob
return
}
t.Parallel()
t.Run("encode", func(t *testing.T) {
t.Parallel()
// encode is unchecked
if errors.Is(tc.wantErr, syscall.EINVAL) {
return
@@ -204,6 +237,8 @@ func TestCodecAbsolute(t *testing.T) {
})
t.Run("decode", func(t *testing.T) {
t.Parallel()
{
var gotA *Absolute
err := gob.NewDecoder(strings.NewReader(tc.gob)).Decode(&gotA)
@@ -238,7 +273,11 @@ func TestCodecAbsolute(t *testing.T) {
})
t.Run("json", func(t *testing.T) {
t.Parallel()
t.Run("marshal", func(t *testing.T) {
t.Parallel()
// marshal is unchecked
if errors.Is(tc.wantErr, syscall.EINVAL) {
return
@@ -273,6 +312,8 @@ func TestCodecAbsolute(t *testing.T) {
})
t.Run("unmarshal", func(t *testing.T) {
t.Parallel()
{
var gotA *Absolute
err := json.Unmarshal([]byte(tc.json), &gotA)
@@ -308,6 +349,8 @@ func TestCodecAbsolute(t *testing.T) {
}
t.Run("json passthrough", func(t *testing.T) {
t.Parallel()
wantErr := "invalid character ':' looking for beginning of value"
if err := new(Absolute).UnmarshalJSON([]byte(":3")); err == nil || err.Error() != wantErr {
t.Errorf("UnmarshalJSON: error = %v, want %s", err, wantErr)
@@ -316,7 +359,11 @@ func TestCodecAbsolute(t *testing.T) {
}
func TestAbsoluteWrap(t *testing.T) {
t.Parallel()
t.Run("join", func(t *testing.T) {
t.Parallel()
want := "/etc/nix/nix.conf"
if got := MustAbs("/etc").Append("nix", "nix.conf"); got.String() != want {
t.Errorf("Append: %q, want %q", got, want)
@@ -324,6 +371,8 @@ func TestAbsoluteWrap(t *testing.T) {
})
t.Run("dir", func(t *testing.T) {
t.Parallel()
want := "/"
if got := MustAbs("/etc").Dir(); got.String() != want {
t.Errorf("Dir: %q, want %q", got, want)
@@ -331,6 +380,8 @@ func TestAbsoluteWrap(t *testing.T) {
})
t.Run("sort", func(t *testing.T) {
t.Parallel()
want := []*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/sys")}
got := []*Absolute{MustAbs("/proc"), MustAbs("/sys"), MustAbs("/etc")}
SortAbs(got)
@@ -340,6 +391,8 @@ func TestAbsoluteWrap(t *testing.T) {
})
t.Run("compact", func(t *testing.T) {
t.Parallel()
want := []*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/sys")}
if got := CompactAbs([]*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/proc"), MustAbs("/sys")}); !reflect.DeepEqual(got, want) {
t.Errorf("CompactAbs: %#v, want %#v", got, want)

View File

@@ -0,0 +1,29 @@
package check
import "strings"
const (
// SpecialOverlayEscape is the escape string for overlay mount options.
SpecialOverlayEscape = `\`
// SpecialOverlayOption is the separator string between overlay mount options.
SpecialOverlayOption = ","
// SpecialOverlayPath is the separator string between overlay paths.
SpecialOverlayPath = ":"
)
// EscapeOverlayDataSegment escapes a string for formatting into the data argument of an overlay mount call.
func EscapeOverlayDataSegment(s string) string {
if s == "" {
return ""
}
if f := strings.SplitN(s, "\x00", 2); len(f) > 0 {
s = f[0]
}
return strings.NewReplacer(
SpecialOverlayEscape, SpecialOverlayEscape+SpecialOverlayEscape,
SpecialOverlayOption, SpecialOverlayEscape+SpecialOverlayOption,
SpecialOverlayPath, SpecialOverlayEscape+SpecialOverlayPath,
).Replace(s)
}

View File

@@ -0,0 +1,31 @@
package check_test
import (
"testing"
"hakurei.app/container/check"
)
func TestEscapeOverlayDataSegment(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
s string
want string
}{
{"zero", "", ""},
{"multi", `\\\:,:,\\\`, `\\\\\\\:\,\:\,\\\\\\`},
{"bwrap", `/path :,\`, `/path \:\,\\`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := check.EscapeOverlayDataSegment(tc.s); got != tc.want {
t.Errorf("escapeOverlayDataSegment: %s, want %s", got, tc.want)
}
})
}
}

View File

@@ -11,16 +11,24 @@ import (
"os/exec"
"runtime"
"strconv"
"sync"
. "syscall"
"time"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/message"
)
const (
// CancelSignal is the signal expected by container init on context cancel.
// A custom [Container.Cancel] function must eventually deliver this signal.
CancelSignal = SIGTERM
CancelSignal = SIGUSR2
// Timeout for writing initParams to Container.setup.
initSetupTimeout = 5 * time.Second
)
type (
@@ -33,8 +41,8 @@ type (
// with behaviour identical to its [exec.Cmd] counterpart.
ExtraFiles []*os.File
// param encoder for shim and init
setup *gob.Encoder
// param pipe for shim and init
setup *os.File
// cancels cmd
cancel context.CancelFunc
// closed after Wait returns
@@ -49,22 +57,23 @@ type (
cmd *exec.Cmd
ctx context.Context
msg message.Msg
Params
}
// Params holds container configuration and is safe to serialise.
Params struct {
// Working directory in the container.
Dir *Absolute
Dir *check.Absolute
// Initial process environment.
Env []string
// Pathname of initial process in the container.
Path *Absolute
Path *check.Absolute
// Initial process argv.
Args []string
// Deliver SIGINT to the initial process on context cancellation.
ForwardCancel bool
// time to wait for linger processes after death of initial process
// Time to wait for processes lingering after the initial process terminates.
AdoptWaitDelay time.Duration
// Mapped Uid in user namespace.
@@ -77,11 +86,11 @@ type (
*Ops
// Seccomp system call filter rules.
SeccompRules []seccomp.NativeRule
SeccompRules []std.NativeRule
// Extra seccomp flags.
SeccompFlags seccomp.ExportFlag
// Seccomp presets. Has no effect unless SeccompRules is zero-length.
SeccompPresets seccomp.FilterPreset
SeccompPresets std.FilterPreset
// Do not load seccomp program.
SeccompDisable bool
@@ -99,28 +108,121 @@ type (
}
)
// Start starts the container init. The init process blocks until Serve is called.
func (p *Container) Start() error {
if p.cmd != nil {
return errors.New("container: already started")
// A StartError contains additional information on a container startup failure.
type StartError struct {
// Fatal suggests whether this error should be considered fatal for the entire program.
Fatal bool
// Step refers to the part of the setup this error is returned from.
Step string
// Err is the underlying error.
Err error
// Origin is whether this error originated from the [Container.Start] method.
Origin bool
// Passthrough is whether the Error method is passed through to Err.
Passthrough bool
}
func (e *StartError) Unwrap() error { return e.Err }
func (e *StartError) Error() string {
if e.Passthrough {
return e.Err.Error()
}
if p.Ops == nil || len(*p.Ops) == 0 {
return errors.New("container: starting an empty container")
if e.Origin {
return e.Step
}
ctx, cancel := context.WithCancel(p.ctx)
p.cancel = cancel
{
var syscallError *os.SyscallError
if errors.As(e.Err, &syscallError) && syscallError != nil {
return e.Step + " " + syscallError.Error()
}
}
return e.Step + ": " + e.Err.Error()
}
// Message returns a user-facing error message.
func (e *StartError) Message() string {
if e.Passthrough {
var (
numError *strconv.NumError
)
switch {
case errors.As(e.Err, new(*os.PathError)),
errors.As(e.Err, new(*os.SyscallError)):
return "cannot " + e.Err.Error()
case errors.As(e.Err, &numError) && numError != nil:
return "cannot parse " + strconv.Quote(numError.Num) + ": " + numError.Err.Error()
default:
return e.Err.Error()
}
}
if e.Origin {
return e.Step
}
return "cannot " + e.Error()
}
// for ensureCloseOnExec
var (
closeOnExecOnce sync.Once
closeOnExecErr error
)
// ensureCloseOnExec ensures all currently open file descriptors have the syscall.FD_CLOEXEC flag set.
// This is only ran once as it is intended to handle files left open by the parent, and any file opened
// on this side should already have syscall.FD_CLOEXEC set.
func ensureCloseOnExec() error {
closeOnExecOnce.Do(func() {
const fdPrefixPath = "/proc/self/fd/"
var entries []os.DirEntry
if entries, closeOnExecErr = os.ReadDir(fdPrefixPath); closeOnExecErr != nil {
return
}
var fd int
for _, ent := range entries {
if fd, closeOnExecErr = strconv.Atoi(ent.Name()); closeOnExecErr != nil {
break // not reached
}
CloseOnExec(fd)
}
})
if closeOnExecErr == nil {
return nil
}
return &StartError{Fatal: true, Step: "set FD_CLOEXEC on all open files", Err: closeOnExecErr, Passthrough: true}
}
// Start starts the container init. The init process blocks until Serve is called.
func (p *Container) Start() error {
if p == nil || p.cmd == nil ||
p.Ops == nil || len(*p.Ops) == 0 {
return errors.New("container: starting an invalid container")
}
if p.cmd.Process != nil {
return errors.New("container: already started")
}
if err := ensureCloseOnExec(); err != nil {
return err
}
// map to overflow id to work around ownership checks
if p.Uid < 1 {
p.Uid = OverflowUid()
p.Uid = OverflowUid(p.msg)
}
if p.Gid < 1 {
p.Gid = OverflowGid()
p.Gid = OverflowGid(p.msg)
}
if !p.RetainSession {
p.SeccompPresets |= seccomp.PresetDenyTTY
p.SeccompPresets |= std.PresetDenyTTY
}
if p.AdoptWaitDelay == 0 {
@@ -131,16 +233,24 @@ func (p *Container) Start() error {
p.AdoptWaitDelay = 0
}
p.cmd = exec.CommandContext(ctx, MustExecutable())
if p.cmd.Stdin == nil {
p.cmd.Stdin = p.Stdin
}
if p.cmd.Stdout == nil {
p.cmd.Stdout = p.Stdout
}
if p.cmd.Stderr == nil {
p.cmd.Stderr = p.Stderr
}
p.cmd.Args = []string{initName}
p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
p.cmd.WaitDelay = p.WaitDelay
if p.Cancel != nil {
p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
} else {
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(CancelSignal) }
}
p.cmd.Dir = FHSRoot
p.cmd.Dir = fhs.Root
p.cmd.SysProcAttr = &SysProcAttr{
Setsid: !p.RetainSession,
Pdeathsig: SIGKILL,
@@ -166,11 +276,10 @@ func (p *Container) Start() error {
}
// place setup pipe before user supplied extra files, this is later restored by init
if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil {
return wrapErrSuffix(err,
"cannot create shim setup pipe:")
if fd, f, err := Setup(&p.cmd.ExtraFiles); err != nil {
return &StartError{true, "set up params stream", err, false, false}
} else {
p.setup = e
p.setup = f
p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
}
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...)
@@ -183,8 +292,7 @@ func (p *Container) Start() error {
done <- func() error { // setup depending on per-thread state must happen here
// PR_SET_NO_NEW_PRIVS: depends on per-thread state but acts on all processes created from that thread
if err := SetNoNewPrivs(); err != nil {
return wrapErrSuffix(err,
"prctl(PR_SET_NO_NEW_PRIVS):")
return &StartError{true, "prctl(PR_SET_NO_NEW_PRIVS)", err, false, false}
}
// landlock: depends on per-thread state but acts on a process group
@@ -200,31 +308,27 @@ func (p *Container) Start() error {
// already covered by namespaces (pid)
goto landlockOut
}
return wrapErrSuffix(err,
"landlock does not appear to be enabled:")
return &StartError{false, "get landlock ABI", err, false, false}
} else if abi < 6 {
if p.HostAbstract {
// see above comment
goto landlockOut
}
return msg.WrapErr(ENOSYS,
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET")
return &StartError{false, "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", ENOSYS, true, false}
} else {
msg.Verbosef("landlock abi version %d", abi)
p.msg.Verbosef("landlock abi version %d", abi)
}
if rulesetFd, err := rulesetAttr.Create(0); err != nil {
return wrapErrSuffix(err,
"cannot create landlock ruleset:")
return &StartError{true, "create landlock ruleset", err, false, false}
} else {
msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
p.msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
if err = LandlockRestrictSelf(rulesetFd, 0); err != nil {
_ = Close(rulesetFd)
return wrapErrSuffix(err,
"cannot enforce landlock ruleset:")
return &StartError{true, "enforce landlock ruleset", err, false, false}
}
if err = Close(rulesetFd); err != nil {
msg.Verbosef("cannot close landlock ruleset: %v", err)
p.msg.Verbosef("cannot close landlock ruleset: %v", err)
// not fatal
}
}
@@ -232,9 +336,9 @@ func (p *Container) Start() error {
landlockOut:
}
msg.Verbose("starting container init")
p.msg.Verbose("starting container init")
if err := p.cmd.Start(); err != nil {
return msg.WrapErr(err, err.Error())
return &StartError{false, "start container init", err, false, true}
}
return nil
}()
@@ -254,29 +358,31 @@ func (p *Container) Serve() error {
setup := p.setup
p.setup = nil
if err := setup.SetDeadline(time.Now().Add(initSetupTimeout)); err != nil {
return &StartError{true, "set init pipe deadline", err, false, true}
}
if p.Path == nil {
p.cancel()
return msg.WrapErr(EINVAL, "invalid executable pathname")
return &StartError{false, "invalid executable pathname", EINVAL, true, false}
}
// do not transmit nil
if p.Dir == nil {
p.Dir = AbsFHSRoot
p.Dir = fhs.AbsRoot
}
if p.SeccompRules == nil {
p.SeccompRules = make([]seccomp.NativeRule, 0)
p.SeccompRules = make([]std.NativeRule, 0)
}
err := setup.Encode(
&initParams{
p.Params,
Getuid(),
Getgid(),
len(p.ExtraFiles),
msg.IsVerbose(),
},
)
err := gob.NewEncoder(setup).Encode(&initParams{
p.Params,
Getuid(),
Getgid(),
len(p.ExtraFiles),
p.msg.IsVerbose(),
})
_ = setup.Close()
if err != nil {
p.cancel()
}
@@ -285,7 +391,7 @@ func (p *Container) Serve() error {
// Wait waits for the container init process to exit and releases any resources associated with the [Container].
func (p *Container) Wait() error {
if p.cmd == nil {
if p.cmd == nil || p.cmd.Process == nil {
return EINVAL
}
@@ -297,6 +403,36 @@ func (p *Container) Wait() error {
return err
}
// StdinPipe calls the [exec.Cmd] method with the same name.
func (p *Container) StdinPipe() (w io.WriteCloser, err error) {
if p.Stdin != nil {
return nil, errors.New("container: Stdin already set")
}
w, err = p.cmd.StdinPipe()
p.Stdin = p.cmd.Stdin
return
}
// StdoutPipe calls the [exec.Cmd] method with the same name.
func (p *Container) StdoutPipe() (r io.ReadCloser, err error) {
if p.Stdout != nil {
return nil, errors.New("container: Stdout already set")
}
r, err = p.cmd.StdoutPipe()
p.Stdout = p.cmd.Stdout
return
}
// StderrPipe calls the [exec.Cmd] method with the same name.
func (p *Container) StderrPipe() (r io.ReadCloser, err error) {
if p.Stderr != nil {
return nil, errors.New("container: Stderr already set")
}
r, err = p.cmd.StderrPipe()
p.Stderr = p.cmd.Stderr
return
}
func (p *Container) String() string {
return fmt.Sprintf("argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x",
p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets))
@@ -311,13 +447,21 @@ func (p *Container) ProcessState() *os.ProcessState {
}
// New returns the address to a new instance of [Container] that requires further initialisation before use.
func New(ctx context.Context) *Container {
return &Container{ctx: ctx, Params: Params{Ops: new(Ops)}}
func New(ctx context.Context, msg message.Msg) *Container {
if msg == nil {
msg = message.New(nil)
}
p := &Container{ctx: ctx, msg: msg, Params: Params{Ops: new(Ops)}}
c, cancel := context.WithCancel(ctx)
p.cancel = cancel
p.cmd = exec.CommandContext(c, MustExecutable(msg))
return p
}
// NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields.
func NewCommand(ctx context.Context, pathname *Absolute, name string, args ...string) *Container {
z := New(ctx)
func NewCommand(ctx context.Context, msg message.Msg, pathname *check.Absolute, name string, args ...string) *Container {
z := New(ctx, msg)
z.Path = pathname
z.Args = append([]string{name}, args...)
return z

View File

@@ -7,9 +7,11 @@ import (
"errors"
"fmt"
"log"
"net"
"os"
"os/exec"
"os/signal"
"reflect"
"strconv"
"strings"
"syscall"
@@ -18,14 +20,195 @@ import (
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/container/vfs"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/ldd"
"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) {
t.Parallel()
testCases := []struct {
name string
err error
s string
is error
isF error
msg string
}{
{"params env", &container.StartError{
Fatal: true,
Step: "set up params stream",
Err: container.ErrReceiveEnv,
}, "set up params stream: environment variable not set",
container.ErrReceiveEnv, syscall.EBADF,
"cannot set up params stream: environment variable not set"},
{"params", &container.StartError{
Fatal: true,
Step: "set up params stream",
Err: &os.SyscallError{Syscall: "pipe2", Err: syscall.EBADF},
}, "set up params stream pipe2: bad file descriptor",
syscall.EBADF, os.ErrInvalid,
"cannot set up params stream pipe2: bad file descriptor"},
{"PR_SET_NO_NEW_PRIVS", &container.StartError{
Fatal: true,
Step: "prctl(PR_SET_NO_NEW_PRIVS)",
Err: syscall.EPERM,
}, "prctl(PR_SET_NO_NEW_PRIVS): operation not permitted",
syscall.EPERM, syscall.EACCES,
"cannot prctl(PR_SET_NO_NEW_PRIVS): operation not permitted"},
{"landlock abi", &container.StartError{
Step: "get landlock ABI",
Err: syscall.ENOSYS,
}, "get landlock ABI: function not implemented",
syscall.ENOSYS, syscall.ENOEXEC,
"cannot get landlock ABI: function not implemented"},
{"landlock old", &container.StartError{
Step: "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET",
Err: syscall.ENOSYS,
Origin: true,
}, "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET",
syscall.ENOSYS, syscall.ENOSPC,
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET"},
{"landlock create", &container.StartError{
Fatal: true,
Step: "create landlock ruleset",
Err: syscall.EBADFD,
}, "create landlock ruleset: file descriptor in bad state",
syscall.EBADFD, syscall.EBADF,
"cannot create landlock ruleset: file descriptor in bad state"},
{"landlock enforce", &container.StartError{
Fatal: true,
Step: "enforce landlock ruleset",
Err: syscall.ENOTRECOVERABLE,
}, "enforce landlock ruleset: state not recoverable",
syscall.ENOTRECOVERABLE, syscall.ETIMEDOUT,
"cannot enforce landlock ruleset: state not recoverable"},
{"start", &container.StartError{
Step: "start container init",
Err: &os.PathError{
Op: "fork/exec",
Path: "/proc/nonexistent",
Err: syscall.ENOENT,
}, Passthrough: true,
}, "fork/exec /proc/nonexistent: no such file or directory",
syscall.ENOENT, syscall.ENOSYS,
"cannot fork/exec /proc/nonexistent: no such file or directory"},
{"start syscall", &container.StartError{
Step: "start container init",
Err: &os.SyscallError{
Syscall: "open",
Err: syscall.ENOSYS,
}, Passthrough: true,
}, "open: function not implemented",
syscall.ENOSYS, syscall.ENOENT,
"cannot open: function not implemented"},
{"start FD_CLOEXEC", &container.StartError{
Fatal: true,
Step: "set FD_CLOEXEC on all open files",
Err: func() error { _, err := strconv.Atoi("invalid"); return err }(),
Passthrough: true,
}, `strconv.Atoi: parsing "invalid": invalid syntax`,
strconv.ErrSyntax, os.ErrInvalid,
`cannot parse "invalid": invalid syntax`},
{"start other", &container.StartError{
Step: "start container init",
Err: &net.OpError{
Op: "dial",
Net: "unix",
Err: syscall.ECONNREFUSED,
}, Passthrough: true,
}, "dial unix: connection refused",
syscall.ECONNREFUSED, syscall.ECONNABORTED,
"dial unix: connection refused"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("error", func(t *testing.T) {
if got := tc.err.Error(); got != tc.s {
t.Errorf("Error: %q, want %q", got, tc.s)
}
})
t.Run("is", func(t *testing.T) {
if !errors.Is(tc.err, tc.is) {
t.Error("Is: unexpected false")
}
if errors.Is(tc.err, tc.isF) {
t.Errorf("Is: unexpected true")
}
})
t.Run("msg", func(t *testing.T) {
if got, ok := message.GetMessage(tc.err); !ok {
if tc.msg != "" {
t.Errorf("GetMessage: err does not implement MessageError")
}
return
} else if got != tc.msg {
t.Errorf("GetMessage: %q, want %q", got, tc.msg)
}
})
})
}
}
const (
ignore = "\x00"
ignoreV = -1
@@ -60,35 +243,35 @@ var containerTestCases = []struct {
uid int
gid int
rules []seccomp.NativeRule
rules []std.NativeRule
flags seccomp.ExportFlag
presets seccomp.FilterPreset
presets std.FilterPreset
}{
{"minimal", true, false, false, true,
emptyOps, emptyMnt,
1000, 100, nil, 0, seccomp.PresetStrict},
1000, 100, nil, 0, std.PresetStrict},
{"allow", true, true, true, false,
emptyOps, emptyMnt,
1000, 100, nil, 0, seccomp.PresetExt | seccomp.PresetDenyDevel},
1000, 100, nil, 0, std.PresetExt | std.PresetDenyDevel},
{"no filter", false, true, true, true,
emptyOps, emptyMnt,
1000, 100, nil, 0, seccomp.PresetExt},
1000, 100, nil, 0, std.PresetExt},
{"custom rules", true, true, true, false,
emptyOps, emptyMnt,
1, 31, []seccomp.NativeRule{{seccomp.ScmpSyscall(syscall.SYS_SETUID), seccomp.ScmpErrno(syscall.EPERM), nil}}, 0, seccomp.PresetExt},
1, 31, []std.NativeRule{{Syscall: std.ScmpSyscall(syscall.SYS_SETUID), Errno: std.ScmpErrno(syscall.EPERM)}}, 0, std.PresetExt},
{"tmpfs", true, false, false, true,
earlyOps(new(container.Ops).
Tmpfs(hst.AbsTmp, 0, 0755),
Tmpfs(hst.AbsPrivateTmp, 0, 0755),
),
earlyMnt(
ent("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", ignore),
ent("/", hst.PrivateTmp, "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", ignore),
),
9, 9, nil, 0, seccomp.PresetStrict},
9, 9, nil, 0, std.PresetStrict},
{"dev", true, true /* go test output is not a tty */, false, false,
earlyOps(new(container.Ops).
Dev(container.MustAbs("/dev"), true),
Dev(check.MustAbs("/dev"), true),
),
earlyMnt(
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
@@ -100,12 +283,13 @@ var containerTestCases = []struct {
ent("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
ent("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
),
1971, 100, nil, 0, seccomp.PresetStrict},
1971, 100, nil, 0, std.PresetStrict},
{"dev no mqueue", true, true /* go test output is not a tty */, false, false,
earlyOps(new(container.Ops).
Dev(container.MustAbs("/dev"), false),
Dev(check.MustAbs("/dev"), false),
),
earlyMnt(
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
@@ -116,25 +300,26 @@ var containerTestCases = []struct {
ent("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
),
1971, 100, nil, 0, seccomp.PresetStrict},
1971, 100, nil, 0, std.PresetStrict},
{"overlay", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) {
tempDir := container.MustAbs(t.TempDir())
tempDir := check.MustAbs(t.TempDir())
lower0, lower1, upper, work :=
tempDir.Append("lower0"),
tempDir.Append("lower1"),
tempDir.Append("upper"),
tempDir.Append("work")
for _, a := range []*container.Absolute{lower0, lower1, upper, work} {
for _, a := range []*check.Absolute{lower0, lower1, upper, work} {
if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err)
}
}
return new(container.Ops).
Overlay(hst.AbsTmp, upper, work, lower0, lower1),
Overlay(hst.AbsPrivateTmp, upper, work, lower0, lower1),
context.WithValue(context.WithValue(context.WithValue(context.WithValue(t.Context(),
testVal("lower1"), lower1),
testVal("lower0"), lower0),
@@ -143,81 +328,83 @@ var containerTestCases = []struct {
},
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{
ent("/", hst.Tmp, "rw", "overlay", "overlay",
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay",
"rw,lowerdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*check.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
",upperdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(*container.Absolute).String())+
container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(*check.Absolute).String())+
",workdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("work")).(*container.Absolute).String())+
container.InternalToHostOvlEscape(ctx.Value(testVal("work")).(*check.Absolute).String())+
",redirect_dir=nofollow,uuid=on,userxattr"),
}
},
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
1 << 3, 1 << 14, nil, 0, std.PresetStrict},
{"overlay ephemeral", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) {
tempDir := container.MustAbs(t.TempDir())
tempDir := check.MustAbs(t.TempDir())
lower0, lower1 :=
tempDir.Append("lower0"),
tempDir.Append("lower1")
for _, a := range []*container.Absolute{lower0, lower1} {
for _, a := range []*check.Absolute{lower0, lower1} {
if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err)
}
}
return new(container.Ops).
OverlayEphemeral(hst.AbsTmp, lower0, lower1),
OverlayEphemeral(hst.AbsPrivateTmp, lower0, lower1),
t.Context()
},
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{
// contains random suffix
ent("/", hst.Tmp, "rw", "overlay", "overlay", ignore),
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay", ignore),
}
},
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
1 << 3, 1 << 14, nil, 0, std.PresetStrict},
{"overlay readonly", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) {
tempDir := container.MustAbs(t.TempDir())
tempDir := check.MustAbs(t.TempDir())
lower0, lower1 :=
tempDir.Append("lower0"),
tempDir.Append("lower1")
for _, a := range []*container.Absolute{lower0, lower1} {
for _, a := range []*check.Absolute{lower0, lower1} {
if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err)
}
}
return new(container.Ops).
OverlayReadonly(hst.AbsTmp, lower0, lower1),
OverlayReadonly(hst.AbsPrivateTmp, lower0, lower1),
context.WithValue(context.WithValue(t.Context(),
testVal("lower1"), lower1),
testVal("lower0"), lower0)
},
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{
ent("/", hst.Tmp, "rw", "overlay", "overlay",
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay",
"ro,lowerdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*check.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
",redirect_dir=nofollow,userxattr"),
}
},
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
1 << 3, 1 << 14, nil, 0, std.PresetStrict},
}
func TestContainer(t *testing.T) {
replaceOutput(t)
t.Parallel()
t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) {
wantErr := context.Canceled
wantExitCode := 0
if err := c.Wait(); !errors.Is(err, wantErr) {
container.GetOutput().PrintBaseErr(err, "wait:")
t.Errorf("Wait: error = %v, want %v", err, wantErr)
if err := c.Wait(); !reflect.DeepEqual(err, wantErr) {
if m, ok := container.InternalMessageFromError(err); ok {
t.Error(m)
}
t.Errorf("Wait: error = %#v, want %#v", err, wantErr)
}
if ps := c.ProcessState(); ps == nil {
t.Errorf("ProcessState unexpectedly returned nil")
@@ -231,7 +418,9 @@ func TestContainer(t *testing.T) {
}, func(t *testing.T, c *container.Container) {
var exitError *exec.ExitError
if err := c.Wait(); !errors.As(err, &exitError) {
container.GetOutput().PrintBaseErr(err, "wait:")
if m, ok := container.InternalMessageFromError(err); ok {
t.Error(m)
}
t.Errorf("Wait: error = %v", err)
}
if code := exitError.ExitCode(); code != blockExitCodeInterrupt {
@@ -241,13 +430,15 @@ func TestContainer(t *testing.T) {
for i, tc := range containerTestCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
wantOps, wantOpsCtx := tc.ops(t)
wantMnt := tc.mnt(t, wantOpsCtx)
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
defer cancel()
var libPaths []*container.Absolute
var libPaths []*check.Absolute
c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i))
c.Uid = tc.uid
c.Gid = tc.gid
@@ -268,11 +459,11 @@ func TestContainer(t *testing.T) {
c.HostNet = tc.net
c.
Readonly(container.MustAbs(pathReadonly), 0755).
Tmpfs(container.MustAbs("/tmp"), 0, 0755).
Place(container.MustAbs("/etc/hostname"), []byte(c.Hostname))
Readonly(check.MustAbs(pathReadonly), 0755).
Tmpfs(check.MustAbs("/tmp"), 0, 0755).
Place(check.MustAbs("/etc/hostname"), []byte(c.Hostname))
// needs /proc to check mountinfo
c.Proc(container.MustAbs("/proc"))
c.Proc(check.MustAbs("/proc"))
// mountinfo cannot be resolved directly by helper due to libPaths nondeterminism
mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
@@ -303,25 +494,34 @@ func TestContainer(t *testing.T) {
_, _ = output.WriteTo(os.Stdout)
t.Fatalf("cannot serialise expected mount points: %v", err)
}
c.Place(container.MustAbs(pathWantMnt), want.Bytes())
c.Place(check.MustAbs(pathWantMnt), want.Bytes())
if tc.ro {
c.Remount(container.MustAbs("/"), syscall.MS_RDONLY)
c.Remount(check.MustAbs("/"), syscall.MS_RDONLY)
}
if err := c.Start(); err != nil {
_, _ = output.WriteTo(os.Stdout)
container.GetOutput().PrintBaseErr(err, "start:")
t.Fatalf("cannot start container: %v", err)
if m, ok := container.InternalMessageFromError(err); ok {
t.Fatal(m)
} else {
t.Fatalf("cannot start container: %v", err)
}
} else if err = c.Serve(); err != nil {
_, _ = output.WriteTo(os.Stdout)
container.GetOutput().PrintBaseErr(err, "serve:")
t.Errorf("cannot serve setup params: %v", err)
if m, ok := container.InternalMessageFromError(err); ok {
t.Error(m)
} else {
t.Errorf("cannot serve setup params: %v", err)
}
}
if err := c.Wait(); err != nil {
_, _ = output.WriteTo(os.Stdout)
container.GetOutput().PrintBaseErr(err, "wait:")
t.Fatalf("wait: %v", err)
if m, ok := container.InternalMessageFromError(err); ok {
t.Fatal(m)
} else {
t.Fatalf("wait: %v", err)
}
}
})
}
@@ -351,6 +551,7 @@ func testContainerCancel(
waitCheck func(t *testing.T, c *container.Container),
) func(t *testing.T) {
return func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
c := helperNewContainer(ctx, "block")
@@ -374,11 +575,17 @@ func testContainerCancel(
}
if err := c.Start(); err != nil {
container.GetOutput().PrintBaseErr(err, "start:")
t.Fatalf("cannot start container: %v", err)
if m, ok := container.InternalMessageFromError(err); ok {
t.Fatal(m)
} else {
t.Fatalf("cannot start container: %v", err)
}
} else if err = c.Serve(); err != nil {
container.GetOutput().PrintBaseErr(err, "serve:")
t.Errorf("cannot serve setup params: %v", err)
if m, ok := container.InternalMessageFromError(err); ok {
t.Error(m)
} else {
t.Errorf("cannot serve setup params: %v", err)
}
}
<-ready
cancel()
@@ -387,12 +594,14 @@ func testContainerCancel(
}
func TestContainerString(t *testing.T) {
c := container.NewCommand(t.Context(), container.MustAbs("/run/current-system/sw/bin/ldd"), "ldd", "/usr/bin/env")
t.Parallel()
msg := message.New(nil)
c := container.NewCommand(t.Context(), msg, check.MustAbs("/run/current-system/sw/bin/ldd"), "ldd", "/usr/bin/env")
c.SeccompFlags |= seccomp.AllowMultiarch
c.SeccompRules = seccomp.Preset(
seccomp.PresetExt|seccomp.PresetDenyNS|seccomp.PresetDenyTTY,
std.PresetExt|std.PresetDenyNS|std.PresetDenyTTY,
c.SeccompFlags)
c.SeccompPresets = seccomp.PresetStrict
c.SeccompPresets = std.PresetStrict
want := `argv: ["ldd" "/usr/bin/env"], filter: true, rules: 65, flags: 0x1, presets: 0xf`
if got := c.String(); got != want {
t.Errorf("String: %s, want %s", got, want)
@@ -406,14 +615,13 @@ const (
func init() {
helperCommands = append(helperCommands, func(c command.Command) {
c.Command("block", command.UsageInternal, func(args []string) error {
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt)
go func() { <-sig; os.Exit(blockExitCodeInterrupt) }()
if _, err := os.NewFile(3, "sync").Write([]byte{0}); err != nil {
return fmt.Errorf("write to sync pipe: %v", err)
}
{
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt)
go func() { <-sig; os.Exit(blockExitCodeInterrupt) }()
}
select {}
})
@@ -523,13 +731,13 @@ const (
)
var (
absHelperInnerPath = container.MustAbs(helperInnerPath)
absHelperInnerPath = check.MustAbs(helperInnerPath)
)
var helperCommands []func(c command.Command)
func TestMain(m *testing.M) {
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
container.TryArgv0(nil)
if os.Getenv(envDoCheck) == "1" {
c := command.New(os.Stderr, log.Printf, "helper", func(args []string) error {
@@ -551,13 +759,17 @@ func TestMain(m *testing.M) {
os.Exit(m.Run())
}
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*container.Absolute, args ...string) (c *container.Container) {
c = container.NewCommand(ctx, absHelperInnerPath, "helper", args...)
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*check.Absolute, args ...string) (c *container.Container) {
msg := message.New(nil)
msg.SwapVerbose(testing.Verbose())
executable := check.MustAbs(container.MustExecutable(msg))
c = container.NewCommand(ctx, msg, absHelperInnerPath, "helper", args...)
c.Env = append(c.Env, envDoCheck+"=1")
c.Bind(container.MustAbs(os.Args[0]), absHelperInnerPath, 0)
c.Bind(executable, absHelperInnerPath, 0)
// in case test has cgo enabled
if entries, err := ldd.Exec(ctx, os.Args[0]); err != nil {
if entries, err := ldd.Resolve(ctx, msg, executable); err != nil {
log.Fatalf("ldd: %v", err)
} else {
*libPaths = ldd.Path(entries)
@@ -570,5 +782,5 @@ func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*container.Abso
}
func helperNewContainer(ctx context.Context, args ...string) (c *container.Container) {
return helperNewContainerLibPaths(ctx, new([]*container.Absolute), args...)
return helperNewContainerLibPaths(ctx, new([]*check.Absolute), args...)
}

View File

@@ -3,7 +3,6 @@ package container
import (
"io"
"io/fs"
"log"
"os"
"os/exec"
"os/signal"
@@ -12,6 +11,8 @@ import (
"syscall"
"hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/message"
)
type osFile interface {
@@ -38,7 +39,7 @@ type syscallDispatcher interface {
setNoNewPrivs() error
// lastcap provides [LastCap].
lastcap() uintptr
lastcap(msg message.Msg) uintptr
// capset provides capset.
capset(hdrp *capHeader, datap *[2]capData) error
// capBoundingSetDrop provides capBoundingSetDrop.
@@ -53,16 +54,16 @@ type syscallDispatcher interface {
receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
// bindMount provides procPaths.bindMount.
bindMount(source, target string, flags uintptr, eq bool) error
bindMount(msg message.Msg, source, target string, flags uintptr) error
// remount provides procPaths.remount.
remount(target string, flags uintptr) error
remount(msg message.Msg, target string, flags uintptr) error
// mountTmpfs provides mountTmpfs.
mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error
// ensureFile provides ensureFile.
ensureFile(name string, perm, pperm os.FileMode) error
// seccompLoad provides [seccomp.Load].
seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error
seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error
// notify provides [signal.Notify].
notify(c chan<- os.Signal, sig ...os.Signal)
// start starts [os/exec.Cmd].
@@ -122,24 +123,12 @@ type syscallDispatcher interface {
// wait4 provides syscall.Wait4
wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error)
// printf provides [log.Printf].
printf(format string, v ...any)
// fatal provides [log.Fatal]
fatal(v ...any)
// fatalf provides [log.Fatalf]
fatalf(format string, v ...any)
// verbose provides [Msg.Verbose].
verbose(v ...any)
// verbosef provides [Msg.Verbosef].
verbosef(format string, v ...any)
// suspend provides [Msg.Suspend].
suspend()
// resume provides [Msg.Resume].
resume() bool
// beforeExit provides [Msg.BeforeExit].
beforeExit()
// printBaseErr provides [Msg.PrintBaseErr].
printBaseErr(err error, fallback string)
// printf provides the Printf method of [log.Logger].
printf(msg message.Msg, format string, v ...any)
// fatal provides the Fatal method of [log.Logger]
fatal(msg message.Msg, v ...any)
// fatalf provides the Fatalf method of [log.Logger]
fatalf(msg message.Msg, format string, v ...any)
}
// direct implements syscallDispatcher on the current kernel.
@@ -153,7 +142,7 @@ func (direct) setPtracer(pid uintptr) error { return SetPtracer(pid) }
func (direct) setDumpable(dumpable uintptr) error { return SetDumpable(dumpable) }
func (direct) setNoNewPrivs() error { return SetNoNewPrivs() }
func (direct) lastcap() uintptr { return LastCap() }
func (direct) lastcap(msg message.Msg) uintptr { return LastCap(msg) }
func (direct) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) }
func (direct) capBoundingSetDrop(cap uintptr) error { return capBoundingSetDrop(cap) }
func (direct) capAmbientClearAll() error { return capAmbientClearAll() }
@@ -163,11 +152,11 @@ func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
return Receive(key, e, fdp)
}
func (direct) bindMount(source, target string, flags uintptr, eq bool) error {
return hostProc.bindMount(source, target, flags, eq)
func (direct) bindMount(msg message.Msg, source, target string, flags uintptr) error {
return hostProc.bindMount(msg, source, target, flags)
}
func (direct) remount(target string, flags uintptr) error {
return hostProc.remount(target, flags)
func (direct) remount(msg message.Msg, target string, flags uintptr) error {
return hostProc.remount(msg, target, flags)
}
func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
return mountTmpfs(k, fsname, target, flags, size, perm)
@@ -176,7 +165,7 @@ func (direct) ensureFile(name string, perm, pperm os.FileMode) error {
return ensureFile(name, perm, pperm)
}
func (direct) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error {
func (direct) seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error {
return seccomp.Load(rules, flags)
}
func (direct) notify(c chan<- os.Signal, sig ...os.Signal) { signal.Notify(c, sig...) }
@@ -225,7 +214,7 @@ func (direct) pivotRoot(newroot, putold string) (err error) {
return syscall.PivotRoot(newroot, putold)
}
func (direct) mount(source, target, fstype string, flags uintptr, data string) (err error) {
return syscall.Mount(source, target, fstype, flags, data)
return mount(source, target, fstype, flags, data)
}
func (direct) unmount(target string, flags int) (err error) {
return syscall.Unmount(target, flags)
@@ -234,12 +223,6 @@ func (direct) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *s
return syscall.Wait4(pid, wstatus, options, rusage)
}
func (direct) printf(format string, v ...any) { log.Printf(format, v...) }
func (direct) fatal(v ...any) { log.Fatal(v...) }
func (direct) fatalf(format string, v ...any) { log.Fatalf(format, v...) }
func (direct) verbose(v ...any) { msg.Verbose(v...) }
func (direct) verbosef(format string, v ...any) { msg.Verbosef(format, v...) }
func (direct) suspend() { msg.Suspend() }
func (direct) resume() bool { return msg.Resume() }
func (direct) beforeExit() { msg.BeforeExit() }
func (direct) printBaseErr(err error, fallback string) { msg.PrintBaseErr(err, fallback) }
func (direct) printf(msg message.Msg, format string, v ...any) { msg.GetLogger().Printf(format, v...) }
func (direct) fatal(msg message.Msg, v ...any) { msg.GetLogger().Fatal(v...) }
func (direct) fatalf(msg message.Msg, format string, v ...any) { msg.GetLogger().Fatalf(format, v...) }

File diff suppressed because it is too large Load Diff

129
container/errors.go Normal file
View File

@@ -0,0 +1,129 @@
package container
import (
"errors"
"os"
"syscall"
"hakurei.app/container/check"
"hakurei.app/container/vfs"
"hakurei.app/message"
)
// messageFromError returns a printable error message for a supported concrete type.
func messageFromError(err error) (m string, ok bool) {
if m, ok = messagePrefixP[MountError]("cannot ", err); ok {
return
}
if m, ok = messagePrefixP[os.PathError]("cannot ", err); ok {
return
}
if m, ok = messagePrefixP[check.AbsoluteError](zeroString, err); ok {
return
}
if m, ok = messagePrefix[OpRepeatError](zeroString, err); ok {
return
}
if m, ok = messagePrefix[OpStateError](zeroString, err); ok {
return
}
if m, ok = messagePrefixP[vfs.DecoderError]("cannot ", err); ok {
return
}
if m, ok = messagePrefix[TmpfsSizeError](zeroString, err); ok {
return
}
if m, ok = message.GetMessage(err); ok {
return
}
return zeroString, false
}
// messagePrefix checks and prefixes the error message of a non-pointer error.
// While this is usable for pointer errors, such use should be avoided as nil check is omitted.
func messagePrefix[T error](prefix string, err error) (string, bool) {
var targetError T
if errors.As(err, &targetError) {
return prefix + targetError.Error(), true
}
return zeroString, false
}
// messagePrefixP checks and prefixes the error message of a pointer error.
func messagePrefixP[V any, T interface {
*V
error
}](prefix string, err error) (string, bool) {
var targetError T
if errors.As(err, &targetError) && targetError != nil {
return prefix + targetError.Error(), true
}
return zeroString, false
}
// MountError wraps errors returned by syscall.Mount.
type MountError struct {
Source, Target, Fstype string
Flags uintptr
Data string
syscall.Errno
}
func (e *MountError) Unwrap() error {
if e.Errno == 0 {
return nil
}
return e.Errno
}
func (e *MountError) Message() string { return "cannot " + e.Error() }
func (e *MountError) Error() string {
if e.Flags&syscall.MS_BIND != 0 {
if e.Flags&syscall.MS_REMOUNT != 0 {
return "remount " + e.Target + ": " + e.Errno.Error()
}
return "bind " + e.Source + " on " + e.Target + ": " + e.Errno.Error()
}
if e.Fstype != FstypeNULL {
return "mount " + e.Fstype + " on " + e.Target + ": " + e.Errno.Error()
}
// fallback case: if this is reached, the conditions for it to occur should be handled above
return "mount " + e.Target + ": " + e.Errno.Error()
}
// optionalErrorUnwrap calls [errors.Unwrap] and returns the resulting value
// if it is not nil, or the original value if it is.
func optionalErrorUnwrap(err error) error {
if underlyingErr := errors.Unwrap(err); underlyingErr != nil {
return underlyingErr
}
return err
}
// errnoFallback returns the concrete errno from an error, or a [os.PathError] fallback.
func errnoFallback(op, path string, err error) (syscall.Errno, *os.PathError) {
var errno syscall.Errno
if !errors.As(err, &errno) {
return 0, &os.PathError{Op: op, Path: path, Err: err}
}
return errno, nil
}
// mount wraps syscall.Mount for error handling.
func mount(source, target, fstype string, flags uintptr, data string) error {
err := syscall.Mount(source, target, fstype, flags, data)
if err == nil {
return nil
}
if errno, pathError := errnoFallback("mount", target, err); pathError != nil {
return pathError
} else {
return &MountError{source, target, fstype, flags, data, errno}
}
}

179
container/errors_test.go Normal file
View File

@@ -0,0 +1,179 @@
package container
import (
"errors"
"os"
"reflect"
"strconv"
"syscall"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
"hakurei.app/container/vfs"
)
func TestMessageFromError(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
err error
want string
wantOk bool
}{
{"mount", &MountError{
Source: SourceTmpfsEphemeral,
Target: "/sysroot/tmp",
Fstype: FstypeTmpfs,
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Data: zeroString,
Errno: syscall.EINVAL,
}, "cannot mount tmpfs on /sysroot/tmp: invalid argument", true},
{"path", &os.PathError{
Op: "mount",
Path: "/sysroot",
Err: stub.UniqueError(0xdeadbeef),
}, "cannot mount /sysroot: unique error 3735928559 injected by the test suite", true},
{"absolute", &check.AbsoluteError{Pathname: "etc/mtab"},
`path "etc/mtab" is not absolute`, true},
{"repeat", OpRepeatError("autoetc"),
"autoetc is not repeatable", true},
{"state", OpStateError("overlay"),
"impossible overlay state reached", true},
{"vfs parse", &vfs.DecoderError{Op: "parse", Line: 0xdead, Err: &strconv.NumError{Func: "Atoi", Num: "meow", Err: strconv.ErrSyntax}},
`cannot parse mountinfo at line 57005: numeric field "meow" invalid syntax`, true},
{"tmpfs", TmpfsSizeError(-1),
"tmpfs size -1 out of bounds", true},
{"unsupported", stub.UniqueError(0xdeadbeef), zeroString, false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, ok := messageFromError(tc.err)
if got != tc.want {
t.Errorf("messageFromError: %q, want %q", got, tc.want)
}
if ok != tc.wantOk {
t.Errorf("messageFromError: ok = %v, want %v", ok, tc.wantOk)
}
})
}
}
func TestMountError(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
err error
errno syscall.Errno
want string
}{
{"bind", &MountError{
Source: "/host/nix/store",
Target: "/sysroot/nix/store",
Fstype: FstypeNULL,
Flags: syscall.MS_SILENT | syscall.MS_BIND | syscall.MS_REC,
Data: zeroString,
Errno: syscall.ENOSYS,
}, syscall.ENOSYS,
"bind /host/nix/store on /sysroot/nix/store: function not implemented"},
{"remount", &MountError{
Source: SourceNone,
Target: "/sysroot/nix/store",
Fstype: FstypeNULL,
Flags: syscall.MS_SILENT | syscall.MS_BIND | syscall.MS_REMOUNT,
Data: zeroString,
Errno: syscall.EPERM,
}, syscall.EPERM,
"remount /sysroot/nix/store: operation not permitted"},
{"overlay", &MountError{
Source: SourceOverlay,
Target: sysrootPath,
Fstype: FstypeOverlay,
Data: `lowerdir=/host/var/lib/planterette/base/debian\:f92c9052`,
Errno: syscall.EINVAL,
}, syscall.EINVAL,
"mount overlay on /sysroot: invalid argument"},
{"fallback", &MountError{
Source: SourceNone,
Target: sysrootPath,
Fstype: FstypeNULL,
Errno: syscall.ENOTRECOVERABLE,
}, syscall.ENOTRECOVERABLE,
"mount /sysroot: state not recoverable"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("is", func(t *testing.T) {
if !errors.Is(tc.err, tc.errno) {
t.Errorf("Is: %#v is not %v", tc.err, tc.errno)
}
})
t.Run("error", func(t *testing.T) {
if got := tc.err.Error(); got != tc.want {
t.Errorf("Error: %q, want %q", got, tc.want)
}
})
})
}
t.Run("zero", func(t *testing.T) {
t.Parallel()
if errors.Is(new(MountError), syscall.Errno(0)) {
t.Errorf("Is: zero MountError unexpected true")
}
})
}
func TestErrnoFallback(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
err error
wantErrno syscall.Errno
wantPath *os.PathError
}{
{"mount", &MountError{
Errno: syscall.ENOTRECOVERABLE,
}, syscall.ENOTRECOVERABLE, nil},
{"path errno", &os.PathError{
Err: syscall.ETIMEDOUT,
}, syscall.ETIMEDOUT, nil},
{"fallback", stub.UniqueError(0xcafebabe), 0, &os.PathError{
Op: "fallback",
Path: "/proc/nonexistent",
Err: stub.UniqueError(0xcafebabe),
}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
errno, err := errnoFallback(tc.name, Nonexistent, tc.err)
if errno != tc.wantErrno {
t.Errorf("errnoFallback: errno = %v, want %v", errno, tc.wantErrno)
}
if !reflect.DeepEqual(err, tc.wantPath) {
t.Errorf("errnoFallback: pathError = %#v, want %#v", err, tc.wantPath)
}
})
}
}
// InternalMessageFromError exports messageFromError for other tests.
func InternalMessageFromError(err error) (string, bool) { return messageFromError(err) }

View File

@@ -1,9 +1,12 @@
package container
import (
"fmt"
"log"
"os"
"sync"
"hakurei.app/message"
)
var (
@@ -11,16 +14,21 @@ var (
executableOnce sync.Once
)
func copyExecutable() {
func copyExecutable(msg message.Msg) {
if name, err := os.Executable(); err != nil {
msg.BeforeExit()
log.Fatalf("cannot read executable path: %v", err)
m := fmt.Sprintf("cannot read executable path: %v", err)
if msg != nil {
msg.BeforeExit()
msg.GetLogger().Fatal(m)
} else {
log.Fatal(m)
}
} else {
executable = name
}
}
func MustExecutable() string {
executableOnce.Do(copyExecutable)
func MustExecutable(msg message.Msg) string {
executableOnce.Do(func() { copyExecutable(msg) })
return executable
}

View File

@@ -5,13 +5,14 @@ import (
"testing"
"hakurei.app/container"
"hakurei.app/message"
)
func TestExecutable(t *testing.T) {
t.Parallel()
for i := 0; i < 16; i++ {
if got := container.MustExecutable(); got != os.Args[0] {
t.Errorf("MustExecutable: %q, want %q",
got, os.Args[0])
if got := container.MustExecutable(message.New(nil)); got != os.Args[0] {
t.Errorf("MustExecutable: %q, want %q", got, os.Args[0])
}
}
}

45
container/fhs/abs.go Normal file
View File

@@ -0,0 +1,45 @@
package fhs
import (
_ "unsafe" // for go:linkname
"hakurei.app/container/check"
)
/* 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
func unsafeAbs(pathname string) *check.Absolute
var (
// AbsRoot is [Root] as [check.Absolute].
AbsRoot = unsafeAbs(Root)
// AbsEtc is [Etc] as [check.Absolute].
AbsEtc = unsafeAbs(Etc)
// AbsTmp is [Tmp] as [check.Absolute].
AbsTmp = unsafeAbs(Tmp)
// AbsRun is [Run] as [check.Absolute].
AbsRun = unsafeAbs(Run)
// AbsRunUser is [RunUser] as [check.Absolute].
AbsRunUser = unsafeAbs(RunUser)
// AbsUsrBin is [UsrBin] as [check.Absolute].
AbsUsrBin = unsafeAbs(UsrBin)
// AbsVar is [Var] as [check.Absolute].
AbsVar = unsafeAbs(Var)
// AbsVarLib is [VarLib] as [check.Absolute].
AbsVarLib = unsafeAbs(VarLib)
// AbsDev is [Dev] as [check.Absolute].
AbsDev = unsafeAbs(Dev)
// AbsDevShm is [DevShm] as [check.Absolute].
AbsDevShm = unsafeAbs(DevShm)
// AbsProc is [Proc] as [check.Absolute].
AbsProc = unsafeAbs(Proc)
// AbsSys is [Sys] as [check.Absolute].
AbsSys = unsafeAbs(Sys)
)

40
container/fhs/fhs.go Normal file
View File

@@ -0,0 +1,40 @@
// Package fhs provides constant and checked pathname values for common FHS paths.
package fhs
const (
// Root points to the file system root.
Root = "/"
// Etc points to the directory for system-specific configuration.
Etc = "/etc/"
// Tmp points to the place for small temporary files.
Tmp = "/tmp/"
// Run points to a "tmpfs" file system for system packages to place runtime data, socket files, and similar.
Run = "/run/"
// RunUser points to a directory containing per-user runtime directories,
// each usually individually mounted "tmpfs" instances.
RunUser = Run + "user/"
// Usr points to vendor-supplied operating system resources.
Usr = "/usr/"
// UsrBin points to binaries and executables for user commands that shall appear in the $PATH search path.
UsrBin = Usr + "bin/"
// Var points to persistent, variable system data. Writable during normal system operation.
Var = "/var/"
// VarLib points to persistent system data.
VarLib = Var + "lib/"
// VarEmpty points to a nonstandard directory that is usually empty.
VarEmpty = Var + "empty/"
// Dev points to the root directory for device nodes.
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 = "/proc/"
// ProcSys points to a hierarchy below /proc/ that exposes a number of kernel tunables.
ProcSys = Proc + "sys/"
// Sys points to a virtual kernel file system exposing discovered devices and other functionality.
Sys = "/sys/"
)

View File

@@ -1,38 +1,48 @@
package container
import (
"context"
"errors"
"fmt"
"log"
"os"
"os/exec"
"path"
"slices"
"strconv"
"sync"
"sync/atomic"
. "syscall"
"time"
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp"
"hakurei.app/message"
)
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:
- the contents of this path is never exposed to the container:
the tmpfs root established here effectively becomes anonymous after pivot_root
- 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 path belongs to the container init:
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:
the container init sets SUID_DUMP_DISABLE and terminates if that fails;
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 tmpfs root established here effectively becomes anonymous after pivot_root.
- 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 path belongs to the container init:
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:
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
intermediate root tmpfs should be effectively anonymous */
intermediateHostPath = FHSProc + "self/fd"
It should be noted that none of this should become relevant at any point since the resulting
intermediate root tmpfs should be effectively anonymous. */
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"
// exitUnexpectedWait4 is the exit code if wait4 returns an unexpected errno.
exitUnexpectedWait4 = 2
)
type (
@@ -46,8 +56,12 @@ type (
early(state *setupState, k syscallDispatcher) error
// apply is called in intermediate root.
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() (string, bool)
prefix() string
Is(op Op) bool
Valid() bool
fmt.Stringer
@@ -56,10 +70,29 @@ type (
// setupState persists context between Ops.
setupState struct {
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
context.Context
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].
func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) }
@@ -68,6 +101,16 @@ const (
nrAutoRoot
)
// OpRepeatError is returned applying a repeated nonrepeatable [Op].
type OpRepeatError string
func (e OpRepeatError) Error() string { return string(e) + " is not repeatable" }
// OpStateError indicates an impossible internal state has been reached in an [Op].
type OpStateError string
func (o OpStateError) Error() string { return "impossible " + string(o) + " state reached" }
// initParams are params passed from parent.
type initParams struct {
Params
@@ -79,20 +122,22 @@ type initParams struct {
Verbose bool
}
func Init(prepareLogger func(prefix string), setVerbose func(verbose bool)) {
initEntrypoint(direct{}, prepareLogger, setVerbose)
}
// Init is called by [TryArgv0] if the current process is the container init.
func Init(msg message.Msg) { initEntrypoint(direct{}, msg) }
func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setVerbose func(verbose bool)) {
func initEntrypoint(k syscallDispatcher, msg message.Msg) {
k.lockOSThread()
prepareLogger("init")
if msg == nil {
panic("attempting to call initEntrypoint with nil msg")
}
if k.getpid() != 1 {
k.fatal("this process must run as pid 1")
k.fatal(msg, "this process must run as pid 1")
}
if err := k.setPtracer(0); err != nil {
k.verbosef("cannot enable ptrace protection via Yama LSM: %v", err)
msg.Verbosef("cannot enable ptrace protection via Yama LSM: %v", err)
// not fatal: this program has no additional privileges at initial program start
}
@@ -104,65 +149,67 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
)
if f, err := k.receive(setupEnv, &params, &setupFd); err != nil {
if errors.Is(err, EBADF) {
k.fatal("invalid setup descriptor")
k.fatal(msg, "invalid setup descriptor")
}
if errors.Is(err, ErrNotSet) {
k.fatal("HAKUREI_SETUP not set")
if errors.Is(err, ErrReceiveEnv) {
k.fatal(msg, setupEnv+" not set")
}
k.fatalf("cannot decode init setup payload: %v", err)
k.fatalf(msg, "cannot decode init setup payload: %v", err)
} else {
if params.Ops == nil {
k.fatal("invalid setup parameters")
k.fatal(msg, "invalid setup parameters")
}
if params.ParentPerm == 0 {
params.ParentPerm = 0755
}
setVerbose(params.Verbose)
k.verbose("received setup parameters")
msg.SwapVerbose(params.Verbose)
msg.Verbose("received setup parameters")
closeSetup = f
offsetSetup = int(setupFd + 1)
}
// write uid/gid map here so parent does not need to set dumpable
if err := k.setDumpable(SUID_DUMP_USER); err != nil {
k.fatalf("cannot set SUID_DUMP_USER: %v", err)
k.fatalf(msg, "cannot set SUID_DUMP_USER: %v", err)
}
if err := k.writeFile(FHSProc+"self/uid_map",
if err := k.writeFile(fhs.Proc+"self/uid_map",
append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...),
0); err != nil {
k.fatalf("%v", err)
k.fatalf(msg, "%v", err)
}
if err := k.writeFile(FHSProc+"self/setgroups",
if err := k.writeFile(fhs.Proc+"self/setgroups",
[]byte("deny\n"),
0); err != nil && !os.IsNotExist(err) {
k.fatalf("%v", err)
k.fatalf(msg, "%v", err)
}
if err := k.writeFile(FHSProc+"self/gid_map",
if err := k.writeFile(fhs.Proc+"self/gid_map",
append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...),
0); err != nil {
k.fatalf("%v", err)
k.fatalf(msg, "%v", err)
}
if err := k.setDumpable(SUID_DUMP_DISABLE); err != nil {
k.fatalf("cannot set SUID_DUMP_DISABLE: %v", err)
k.fatalf(msg, "cannot set SUID_DUMP_DISABLE: %v", err)
}
oldmask := k.umask(0)
if params.Hostname != "" {
if err := k.sethostname([]byte(params.Hostname)); err != nil {
k.fatalf("cannot set hostname: %v", err)
k.fatalf(msg, "cannot set hostname: %v", err)
}
}
// cache sysctl before pivot_root
lastcap := k.lastcap()
lastcap := k.lastcap(msg)
if err := k.mount(zeroString, FHSRoot, zeroString, MS_SILENT|MS_SLAVE|MS_REC, zeroString); err != nil {
k.fatalf("cannot make / rslave: %v", err)
if err := k.mount(zeroString, fhs.Root, zeroString, MS_SILENT|MS_SLAVE|MS_REC, zeroString); err != nil {
k.fatalf(msg, "cannot make / rslave: %v", optionalErrorUnwrap(err))
}
state := &setupState{Params: &params.Params}
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;
this step is mostly for gathering information that would otherwise be difficult to obtain
@@ -170,40 +217,41 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
the state of the mount namespace */
for i, op := range *params.Ops {
if op == nil || !op.Valid() {
k.fatalf("invalid op at index %d", i)
k.fatalf(msg, "invalid op at index %d", i)
}
if err := op.early(state, k); err != nil {
k.printBaseErr(err,
fmt.Sprintf("cannot prepare op at index %d:", i))
k.beforeExit()
k.exit(1)
if m, ok := messageFromError(err); ok {
k.fatal(msg, m)
} else {
k.fatalf(msg, "cannot prepare op at index %d: %v", i, err)
}
}
}
if err := k.mount(SourceTmpfsRootfs, intermediateHostPath, FstypeTmpfs, MS_NODEV|MS_NOSUID, zeroString); err != nil {
k.fatalf("cannot mount intermediate root: %v", err)
k.fatalf(msg, "cannot mount intermediate root: %v", optionalErrorUnwrap(err))
}
if err := k.chdir(intermediateHostPath); err != nil {
k.fatalf("cannot enter intermediate host path: %v", err)
k.fatalf(msg, "cannot enter intermediate host path: %v", err)
}
if err := k.mkdir(sysrootDir, 0755); err != nil {
k.fatalf("%v", err)
k.fatalf(msg, "%v", err)
}
if err := k.mount(sysrootDir, sysrootDir, zeroString, MS_SILENT|MS_BIND|MS_REC, zeroString); err != nil {
k.fatalf("cannot bind sysroot: %v", err)
k.fatalf(msg, "cannot bind sysroot: %v", optionalErrorUnwrap(err))
}
if err := k.mkdir(hostDir, 0755); err != nil {
k.fatalf("%v", err)
k.fatalf(msg, "%v", err)
}
// pivot_root uncovers intermediateHostPath in hostDir
if err := k.pivotRoot(intermediateHostPath, hostDir); err != nil {
k.fatalf("cannot pivot into intermediate root: %v", err)
k.fatalf(msg, "cannot pivot into intermediate root: %v", err)
}
if err := k.chdir(FHSRoot); err != nil {
k.fatalf("cannot enter intermediate root: %v", err)
if err := k.chdir(fhs.Root); err != nil {
k.fatalf(msg, "cannot enter intermediate root: %v", err)
}
/* apply is called right after pivot_root and entering the new root;
@@ -212,62 +260,65 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
chdir is allowed but discouraged */
for i, op := range *params.Ops {
// ops already checked during early setup
k.verbosef("%s %s", op.prefix(), op)
if prefix, ok := op.prefix(); ok {
msg.Verbosef("%s %s", prefix, op)
}
if err := op.apply(state, k); err != nil {
k.printBaseErr(err,
fmt.Sprintf("cannot apply op at index %d:", i))
k.beforeExit()
k.exit(1)
if m, ok := messageFromError(err); ok {
k.fatal(msg, m)
} else {
k.fatalf(msg, "cannot apply op at index %d: %v", i, err)
}
}
}
// setup requiring host root complete at this point
if err := k.mount(hostDir, hostDir, zeroString, MS_SILENT|MS_REC|MS_PRIVATE, zeroString); err != nil {
k.fatalf("cannot make host root rprivate: %v", err)
k.fatalf(msg, "cannot make host root rprivate: %v", optionalErrorUnwrap(err))
}
if err := k.unmount(hostDir, MNT_DETACH); err != nil {
k.fatalf("cannot unmount host root: %v", err)
k.fatalf(msg, "cannot unmount host root: %v", err)
}
{
var fd int
if err := IgnoringEINTR(func() (err error) {
fd, err = k.open(FHSRoot, O_DIRECTORY|O_RDONLY, 0)
fd, err = k.open(fhs.Root, O_DIRECTORY|O_RDONLY, 0)
return
}); err != nil {
k.fatalf("cannot open intermediate root: %v", err)
k.fatalf(msg, "cannot open intermediate root: %v", err)
}
if err := k.chdir(sysrootPath); err != nil {
k.fatalf("cannot enter sysroot: %v", err)
k.fatalf(msg, "cannot enter sysroot: %v", err)
}
if err := k.pivotRoot(".", "."); err != nil {
k.fatalf("cannot pivot into sysroot: %v", err)
k.fatalf(msg, "cannot pivot into sysroot: %v", err)
}
if err := k.fchdir(fd); err != nil {
k.fatalf("cannot re-enter intermediate root: %v", err)
k.fatalf(msg, "cannot re-enter intermediate root: %v", err)
}
if err := k.unmount(".", MNT_DETACH); err != nil {
k.fatalf("cannot unmount intermediate root: %v", err)
k.fatalf(msg, "cannot unmount intermediate root: %v", err)
}
if err := k.chdir(FHSRoot); err != nil {
k.fatalf("cannot enter root: %v", err)
if err := k.chdir(fhs.Root); err != nil {
k.fatalf(msg, "cannot enter root: %v", err)
}
if err := k.close(fd); err != nil {
k.fatalf("cannot close intermediate root: %v", err)
k.fatalf(msg, "cannot close intermediate root: %v", err)
}
}
if err := k.capAmbientClearAll(); err != nil {
k.fatalf("cannot clear the ambient capability set: %v", err)
k.fatalf(msg, "cannot clear the ambient capability set: %v", err)
}
for i := uintptr(0); i <= lastcap; i++ {
if params.Privileged && i == CAP_SYS_ADMIN {
continue
}
if err := k.capBoundingSetDrop(i); err != nil {
k.fatalf("cannot drop capability from bounding set: %v", err)
k.fatalf(msg, "cannot drop capability from bounding set: %v", err)
}
}
@@ -276,29 +327,29 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN)
if err := k.capAmbientRaise(CAP_SYS_ADMIN); err != nil {
k.fatalf("cannot raise CAP_SYS_ADMIN: %v", err)
k.fatalf(msg, "cannot raise CAP_SYS_ADMIN: %v", err)
}
}
if err := k.capset(
&capHeader{_LINUX_CAPABILITY_VERSION_3, 0},
&[2]capData{{0, keep[0], keep[0]}, {0, keep[1], keep[1]}},
); err != nil {
k.fatalf("cannot capset: %v", err)
k.fatalf(msg, "cannot capset: %v", err)
}
if !params.SeccompDisable {
rules := params.SeccompRules
if len(rules) == 0 { // non-empty rules slice always overrides presets
k.verbosef("resolving presets %#x", params.SeccompPresets)
msg.Verbosef("resolving presets %#x", params.SeccompPresets)
rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags)
}
if err := k.seccompLoad(rules, params.SeccompFlags); err != nil {
// this also indirectly asserts PR_SET_NO_NEW_PRIVS
k.fatalf("cannot load syscall filter: %v", err)
k.fatalf(msg, "cannot load syscall filter: %v", err)
}
k.verbosef("%d filter rules loaded", len(rules))
msg.Verbosef("%d filter rules loaded", len(rules))
} else {
k.verbose("syscall filter not configured")
msg.Verbose("syscall filter not configured")
}
extraFiles := make([]*os.File, params.Count)
@@ -308,36 +359,30 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
}
k.umask(oldmask)
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()
k.verbosef("starting initial program %s", params.Path)
if err := k.start(cmd); err != nil {
k.fatalf("%v", err)
}
k.suspend()
if err := closeSetup(); err != nil {
k.printf("cannot close setup pipe: %v", err)
// not fatal
}
// winfo represents an exited process from wait4.
type winfo struct {
wpid int
wstatus WaitStatus
}
// info is closed as the wait4 thread terminates
// when there are no longer any processes left to reap
info := make(chan winfo, 1)
done := make(chan struct{})
// whether initial process has started
var initialProcessStarted atomic.Bool
k.new(func(k syscallDispatcher) {
k.lockOSThread()
wait4:
var (
err error
wpid = -2
wstatus WaitStatus
// whether initial process has started
started bool
)
// keep going until no child process is left
@@ -347,7 +392,25 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
}
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}
}
}
if !started {
started = initialProcessStarted.Load()
}
err = EINTR
@@ -355,80 +418,145 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
wpid, err = k.wait4(-1, &wstatus, 0, nil)
}
}
if !errors.Is(err, ECHILD) {
k.printf("unexpected wait4 response: %v", err)
k.printf(msg, "unexpected wait4 response: %v", err)
} else if !started {
// 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(done)
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
sig := make(chan os.Signal, 2)
k.notify(sig, os.Interrupt, CancelSignal)
k.notify(sig, CancelSignal,
os.Interrupt, SIGTERM, SIGQUIT)
// closed after residualProcessTimeout has elapsed after initial process death
timeout := make(chan struct{})
r := 2
r := exitUnexpectedWait4
for {
select {
case s := <-sig:
if k.resume() {
k.verbosef("%s after process start", s.String())
} else {
k.verbosef("got %s", s.String())
}
if s == CancelSignal && params.ForwardCancel && cmd.Process != nil {
k.verbose("forwarding context cancellation")
msg.Verbose("forwarding context cancellation")
if err := k.signal(cmd, os.Interrupt); err != nil {
k.printf("cannot forward cancellation: %v", err)
k.printf(msg, "cannot forward cancellation: %v", err)
}
continue
}
k.beforeExit()
if s == SIGTERM || s == SIGQUIT {
msg.Verbosef("got %s, forwarding to initial process", s.String())
if err := k.signal(cmd, s); err != nil {
k.printf(msg, "cannot forward signal: %v", err)
}
continue
}
msg.Verbosef("got %s", s.String())
msg.BeforeExit()
k.exit(0)
case w := <-info:
case w, ok := <-info:
if !ok {
msg.BeforeExit()
k.exit(r)
continue // unreachable
}
if w.wpid == cmd.Process.Pid {
// initial process exited, output is most likely available again
k.resume()
// cancel Op context early
cancel()
// start timeout early
go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
// close initial process files; this also keeps them alive
for _, f := range extraFiles {
if err := f.Close(); err != nil {
msg.Verbose(err.Error())
}
}
switch {
case w.wstatus.Exited():
r = w.wstatus.ExitStatus()
k.verbosef("initial process exited with code %d", w.wstatus.ExitStatus())
msg.Verbosef("initial process exited with code %d", w.wstatus.ExitStatus())
case w.wstatus.Signaled():
r = 128 + int(w.wstatus.Signal())
k.verbosef("initial process exited with signal %s", w.wstatus.Signal())
msg.Verbosef("initial process exited with signal %s", w.wstatus.Signal())
default:
r = 255
k.verbosef("initial process exited with status %#x", w.wstatus)
msg.Verbosef("initial process exited with status %#x", w.wstatus)
}
go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
}
case <-done:
k.beforeExit()
k.exit(r)
case <-timeout:
k.printf("timeout exceeded waiting for lingering processes")
k.beforeExit()
k.printf(msg, "timeout exceeded waiting for lingering processes")
msg.BeforeExit()
k.exit(r)
}
}
}
// initName is the prefix used by log.std in the init process.
const initName = "init"
// TryArgv0 calls [Init] if the last element of argv0 is "init".
func TryArgv0(v Msg, prepare func(prefix string), setVerbose func(verbose bool)) {
// If a nil msg is passed, the system logger is used instead.
func TryArgv0(msg message.Msg) {
if msg == nil {
log.SetPrefix(initName + ": ")
log.SetFlags(0)
msg = message.New(log.Default())
}
if len(os.Args) > 0 && path.Base(os.Args[0]) == initName {
msg = v
Init(prepare, setVerbose)
Init(msg)
msg.BeforeExit()
os.Exit(0)
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,12 +5,15 @@ import (
"fmt"
"os"
"syscall"
"hakurei.app/container/check"
"hakurei.app/container/std"
)
func init() { gob.Register(new(BindMountOp)) }
// Bind appends an [Op] that bind mounts host path [BindMountOp.Source] on container path [BindMountOp.Target].
func (f *Ops) Bind(source, target *Absolute, flags int) *Ops {
func (f *Ops) Bind(source, target *check.Absolute, flags int) *Ops {
*f = append(*f, &BindMountOp{nil, source, target, flags})
return f
}
@@ -18,52 +21,41 @@ func (f *Ops) Bind(source, target *Absolute, flags int) *Ops {
// BindMountOp bind mounts host path Source on container path Target.
// Note that Flags uses bits declared in this package and should not be set with constants in [syscall].
type BindMountOp struct {
sourceFinal, Source, Target *Absolute
sourceFinal, Source, Target *check.Absolute
Flags int
}
const (
// BindOptional skips nonexistent host paths.
BindOptional = 1 << iota
// BindWritable mounts filesystem read-write.
BindWritable
// BindDevice allows access to devices (special files) on this filesystem.
BindDevice
// BindEnsure attempts to create the host path if it does not exist.
BindEnsure
)
func (b *BindMountOp) Valid() bool {
return b != nil &&
b.Source != nil && b.Target != nil &&
b.Flags&(BindOptional|BindEnsure) != (BindOptional|BindEnsure)
b.Flags&(std.BindOptional|std.BindEnsure) != (std.BindOptional|std.BindEnsure)
}
func (b *BindMountOp) early(_ *setupState, k syscallDispatcher) error {
if b.Flags&BindEnsure != 0 {
if b.Flags&std.BindEnsure != 0 {
if err := k.mkdirAll(b.Source.String(), 0700); err != nil {
return wrapErrSelf(err)
return err
}
}
if pathname, err := k.evalSymlinks(b.Source.String()); err != nil {
if os.IsNotExist(err) && b.Flags&BindOptional != 0 {
if os.IsNotExist(err) && b.Flags&std.BindOptional != 0 {
// leave sourceFinal as nil
return nil
}
return wrapErrSelf(err)
return err
} else {
b.sourceFinal, err = NewAbs(pathname)
b.sourceFinal, err = check.NewAbs(pathname)
return err
}
}
func (b *BindMountOp) apply(_ *setupState, k syscallDispatcher) error {
func (b *BindMountOp) apply(state *setupState, k syscallDispatcher) error {
if b.sourceFinal == nil {
if b.Flags&BindOptional == 0 {
if b.Flags&std.BindOptional == 0 {
// unreachable
return msg.WrapErr(os.ErrClosed, "impossible bind state reached")
return OpStateError("bind")
}
return nil
}
@@ -74,25 +66,31 @@ func (b *BindMountOp) apply(_ *setupState, k syscallDispatcher) error {
// this perm value emulates bwrap behaviour as it clears bits from 0755 based on
// op->perms which is never set for any bind setup op so always results in 0700
if fi, err := k.stat(source); err != nil {
return wrapErrSelf(err)
return err
} else if fi.IsDir() {
if err = k.mkdirAll(target, 0700); err != nil {
return wrapErrSelf(err)
return err
}
} else if err = k.ensureFile(target, 0444, 0700); err != nil {
return err
}
var flags uintptr = syscall.MS_REC
if b.Flags&BindWritable == 0 {
if b.Flags&std.BindWritable == 0 {
flags |= syscall.MS_RDONLY
}
if b.Flags&BindDevice == 0 {
if b.Flags&std.BindDevice == 0 {
flags |= syscall.MS_NODEV
}
return k.bindMount(source, target, flags, b.sourceFinal == b.Target)
if b.sourceFinal.String() == b.Target.String() {
state.Verbosef("mounting %q flags %#x", target, flags)
} else {
state.Verbosef("mounting %q on %q flags %#x", 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 {
vb, ok := op.(*BindMountOp)
@@ -101,7 +99,7 @@ func (b *BindMountOp) Is(op Op) bool {
b.Target.Is(vb.Target) &&
b.Flags == vb.Flags
}
func (*BindMountOp) prefix() string { return "mounting" }
func (*BindMountOp) prefix() (string, bool) { return "mounting", false }
func (b *BindMountOp) String() string {
if b.Source == nil || b.Target == nil {
return "<invalid>"

View File

@@ -5,145 +5,172 @@ import (
"os"
"syscall"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/std"
"hakurei.app/container/stub"
)
func TestBindMountOp(t *testing.T) {
t.Parallel()
checkOpBehaviour(t, []opBehaviourTestCase{
{"ENOENT not optional", new(Params), &BindMountOp{
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/bin/"}, "", syscall.ENOENT},
}, wrapErrSelf(syscall.ENOENT), nil, nil},
Source: check.MustAbs("/bin/"),
Target: check.MustAbs("/bin/"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "", syscall.ENOENT),
}, syscall.ENOENT, nil, nil},
{"skip optional", new(Params), &BindMountOp{
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
Flags: BindOptional,
}, []kexpect{
{"evalSymlinks", expectArgs{"/bin/"}, "", syscall.ENOENT},
Source: check.MustAbs("/bin/"),
Target: check.MustAbs("/bin/"),
Flags: std.BindOptional,
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "", syscall.ENOENT),
}, nil, nil, nil},
{"success optional", new(Params), &BindMountOp{
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
Flags: BindOptional,
}, []kexpect{
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
}, nil, []kexpect{
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil},
{"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil},
{"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil},
Source: check.MustAbs("/bin/"),
Target: check.MustAbs("/bin/"),
Flags: std.BindOptional,
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []stub.Call{
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil),
call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005)}}, nil, nil),
call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
}, nil},
{"ensureFile device", new(Params), &BindMountOp{
Source: MustAbs("/dev/null"),
Target: MustAbs("/dev/null"),
Flags: BindWritable | BindDevice,
}, []kexpect{
{"evalSymlinks", expectArgs{"/dev/null"}, "/dev/null", nil},
}, nil, []kexpect{
{"stat", expectArgs{"/host/dev/null"}, isDirFi(false), nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, errUnique},
}, errUnique},
Source: check.MustAbs("/dev/null"),
Target: check.MustAbs("/dev/null"),
Flags: std.BindWritable | std.BindDevice,
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
}, nil, []stub.Call{
call("stat", stub.ExpectArgs{"/host/dev/null"}, isDirFi(false), nil),
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, stub.UniqueError(5)),
}, stub.UniqueError(5)},
{"mkdirAll ensure", new(Params), &BindMountOp{
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
Flags: BindEnsure,
}, []kexpect{
{"mkdirAll", expectArgs{"/bin/", os.FileMode(0700)}, nil, errUnique},
}, wrapErrSelf(errUnique), nil, nil},
Source: check.MustAbs("/bin/"),
Target: check.MustAbs("/bin/"),
Flags: std.BindEnsure,
}, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/bin/", os.FileMode(0700)}, nil, stub.UniqueError(4)),
}, stub.UniqueError(4), nil, nil},
{"success ensure", new(Params), &BindMountOp{
Source: MustAbs("/bin/"),
Target: MustAbs("/usr/bin/"),
Flags: BindEnsure,
}, []kexpect{
{"mkdirAll", expectArgs{"/bin/", os.FileMode(0700)}, nil, nil},
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
}, nil, []kexpect{
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil},
{"mkdirAll", expectArgs{"/sysroot/usr/bin", os.FileMode(0700)}, nil, nil},
{"bindMount", expectArgs{"/host/usr/bin", "/sysroot/usr/bin", uintptr(0x4005), false}, nil, nil},
Source: check.MustAbs("/bin/"),
Target: check.MustAbs("/usr/bin/"),
Flags: std.BindEnsure,
}, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/bin/", os.FileMode(0700)}, nil, nil),
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []stub.Call{
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
call("mkdirAll", stub.ExpectArgs{"/sysroot/usr/bin", os.FileMode(0700)}, nil, nil),
call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/usr/bin", "/sysroot/usr/bin", uintptr(0x4005)}}, nil, nil),
call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/usr/bin", uintptr(0x4005), false}, nil, nil),
}, nil},
{"success device ro", new(Params), &BindMountOp{
Source: MustAbs("/dev/null"),
Target: MustAbs("/dev/null"),
Flags: BindDevice,
}, []kexpect{
{"evalSymlinks", expectArgs{"/dev/null"}, "/dev/null", nil},
}, nil, []kexpect{
{"stat", expectArgs{"/host/dev/null"}, isDirFi(false), nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4001), false}, nil, nil},
Source: check.MustAbs("/dev/null"),
Target: check.MustAbs("/dev/null"),
Flags: std.BindDevice,
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
}, nil, []stub.Call{
call("stat", stub.ExpectArgs{"/host/dev/null"}, isDirFi(false), nil),
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, nil),
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/dev/null", uintptr(0x4001)}}, nil, nil),
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4001), false}, nil, nil),
}, nil},
{"success device", new(Params), &BindMountOp{
Source: MustAbs("/dev/null"),
Target: MustAbs("/dev/null"),
Flags: BindWritable | BindDevice,
}, []kexpect{
{"evalSymlinks", expectArgs{"/dev/null"}, "/dev/null", nil},
}, nil, []kexpect{
{"stat", expectArgs{"/host/dev/null"}, isDirFi(false), nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4000), false}, nil, nil},
Source: check.MustAbs("/dev/null"),
Target: check.MustAbs("/dev/null"),
Flags: std.BindWritable | std.BindDevice,
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
}, nil, []stub.Call{
call("stat", stub.ExpectArgs{"/host/dev/null"}, isDirFi(false), nil),
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, nil),
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/dev/null", uintptr(0x4000)}}, nil, nil),
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4000), false}, nil, nil),
}, nil},
{"evalSymlinks", new(Params), &BindMountOp{
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", errUnique},
}, wrapErrSelf(errUnique), nil, nil},
Source: check.MustAbs("/bin/"),
Target: check.MustAbs("/bin/"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", stub.UniqueError(3)),
}, stub.UniqueError(3), nil, nil},
{"stat", new(Params), &BindMountOp{
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
}, nil, []kexpect{
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), errUnique},
}, wrapErrSelf(errUnique)},
Source: check.MustAbs("/bin/"),
Target: check.MustAbs("/bin/"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []stub.Call{
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), stub.UniqueError(2)),
}, stub.UniqueError(2)},
{"mkdirAll", new(Params), &BindMountOp{
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
}, nil, []kexpect{
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil},
{"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, errUnique},
}, wrapErrSelf(errUnique)},
Source: check.MustAbs("/bin/"),
Target: check.MustAbs("/bin/"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []stub.Call{
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, stub.UniqueError(1)),
}, stub.UniqueError(1)},
{"bindMount", new(Params), &BindMountOp{
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
}, nil, []kexpect{
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil},
{"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil},
{"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, errUnique},
}, errUnique},
Source: check.MustAbs("/bin/"),
Target: check.MustAbs("/bin/"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []stub.Call{
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil),
call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005)}}, nil, nil),
call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, stub.UniqueError(0)),
}, stub.UniqueError(0)},
{"success eval equals", new(Params), &BindMountOp{
Source: check.MustAbs("/bin/"),
Target: check.MustAbs("/bin/"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/bin", nil),
}, nil, []stub.Call{
call("stat", stub.ExpectArgs{"/host/bin"}, isDirFi(true), nil),
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil),
call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/bin", "/sysroot/bin", uintptr(0x4005)}}, nil, nil),
call("bindMount", stub.ExpectArgs{"/host/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
}, nil},
{"success", new(Params), &BindMountOp{
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
}, nil, []kexpect{
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil},
{"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil},
{"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil},
Source: check.MustAbs("/bin/"),
Target: check.MustAbs("/bin/"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []stub.Call{
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil),
call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005)}}, nil, nil),
call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
}, nil},
})
t.Run("unreachable", func(t *testing.T) {
t.Parallel()
t.Run("nil sourceFinal not optional", func(t *testing.T) {
wantErr := msg.WrapErr(os.ErrClosed, "impossible bind state reached")
t.Parallel()
wantErr := OpStateError("bind")
if err := new(BindMountOp).apply(nil, nil); !errors.Is(err, wantErr) {
t.Errorf("apply: error = %v, want %v", err, wantErr)
}
@@ -153,21 +180,21 @@ func TestBindMountOp(t *testing.T) {
checkOpsValid(t, []opValidTestCase{
{"nil", (*BindMountOp)(nil), false},
{"zero", new(BindMountOp), false},
{"nil source", &BindMountOp{Target: MustAbs("/")}, false},
{"nil target", &BindMountOp{Source: MustAbs("/")}, false},
{"flag optional ensure", &BindMountOp{Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindOptional | BindEnsure}, false},
{"valid", &BindMountOp{Source: MustAbs("/"), Target: MustAbs("/")}, true},
{"nil source", &BindMountOp{Target: check.MustAbs("/")}, false},
{"nil target", &BindMountOp{Source: check.MustAbs("/")}, false},
{"flag optional ensure", &BindMountOp{Source: check.MustAbs("/"), Target: check.MustAbs("/"), Flags: std.BindOptional | std.BindEnsure}, false},
{"valid", &BindMountOp{Source: check.MustAbs("/"), Target: check.MustAbs("/")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"autoetc", new(Ops).Bind(
MustAbs("/etc/"),
MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
check.MustAbs("/etc/"),
check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
0,
), Ops{
&BindMountOp{
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Source: check.MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
},
}},
})
@@ -176,45 +203,45 @@ func TestBindMountOp(t *testing.T) {
{"zero", new(BindMountOp), new(BindMountOp), false},
{"internal ne", &BindMountOp{
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Source: check.MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, &BindMountOp{
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
sourceFinal: MustAbs("/etc/"),
Source: check.MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
sourceFinal: check.MustAbs("/etc/"),
}, true},
{"flags differs", &BindMountOp{
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Source: check.MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, &BindMountOp{
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Flags: BindOptional,
Source: check.MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Flags: std.BindOptional,
}, false},
{"source differs", &BindMountOp{
Source: MustAbs("/.hakurei/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Source: check.MustAbs("/.hakurei/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, &BindMountOp{
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Source: check.MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, false},
{"target differs", &BindMountOp{
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Source: check.MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, &BindMountOp{
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/"),
Source: check.MustAbs("/etc/"),
Target: check.MustAbs("/etc/"),
}, false},
{"equals", &BindMountOp{
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Source: check.MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, &BindMountOp{
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Source: check.MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, true},
})
@@ -222,14 +249,14 @@ func TestBindMountOp(t *testing.T) {
{"invalid", new(BindMountOp), "mounting", "<invalid>"},
{"autoetc", &BindMountOp{
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Source: check.MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, "mounting", `"/etc/" on "/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659" flags 0x0`},
{"hostdev", &BindMountOp{
Source: MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Flags: BindWritable | BindDevice,
Source: check.MustAbs("/dev/"),
Target: check.MustAbs("/dev/"),
Flags: std.BindWritable | std.BindDevice,
}, "mounting", `"/dev/" flags 0x6`},
})
}

134
container/initdaemon.go Normal file
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) }

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"`},
})
}

View File

@@ -5,19 +5,22 @@ import (
"fmt"
"path"
. "syscall"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
)
func init() { gob.Register(new(MountDevOp)) }
// Dev appends an [Op] that mounts a subset of host /dev.
func (f *Ops) Dev(target *Absolute, mqueue bool) *Ops {
func (f *Ops) Dev(target *check.Absolute, mqueue bool) *Ops {
*f = append(*f, &MountDevOp{target, mqueue, false})
return f
}
// DevWritable appends an [Op] that mounts a writable subset of host /dev.
// There is usually no good reason to write to /dev, so this should always be followed by a [RemountOp].
func (f *Ops) DevWritable(target *Absolute, mqueue bool) *Ops {
func (f *Ops) DevWritable(target *check.Absolute, mqueue bool) *Ops {
*f = append(*f, &MountDevOp{target, mqueue, true})
return f
}
@@ -26,7 +29,7 @@ func (f *Ops) DevWritable(target *Absolute, mqueue bool) *Ops {
// If Mqueue is true, a private instance of [FstypeMqueue] is mounted.
// If Write is true, the resulting mount point is left writable.
type MountDevOp struct {
Target *Absolute
Target *check.Absolute
Mqueue bool
Write bool
}
@@ -46,43 +49,43 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
return err
}
if err := k.bindMount(
toHost(FHSDev+name),
state,
toHost(fhs.Dev+name),
targetPath,
0,
true,
); err != nil {
return err
}
}
for i, name := range []string{"stdin", "stdout", "stderr"} {
if err := k.symlink(
FHSProc+"self/fd/"+string(rune(i+'0')),
fhs.Proc+"self/fd/"+string(rune(i+'0')),
path.Join(target, name),
); err != nil {
return wrapErrSelf(err)
return err
}
}
for _, pair := range [][2]string{
{FHSProc + "self/fd", "fd"},
{FHSProc + "kcore", "core"},
{fhs.Proc + "self/fd", "fd"},
{fhs.Proc + "kcore", "core"},
{"pts/ptmx", "ptmx"},
} {
if err := k.symlink(pair[0], path.Join(target, pair[1])); err != nil {
return wrapErrSelf(err)
return err
}
}
devShmPath := path.Join(target, "shm")
devPtsPath := path.Join(target, "pts")
for _, name := range []string{path.Join(target, "shm"), devPtsPath} {
for _, name := range []string{devShmPath, devPtsPath} {
if err := k.mkdir(name, state.ParentPerm); err != nil {
return wrapErrSelf(err)
return err
}
}
if err := k.mount(SourceDevpts, devPtsPath, FstypeDevpts, MS_NOSUID|MS_NOEXEC,
"newinstance,ptmxmode=0666,mode=620"); err != nil {
return wrapErrSuffix(err,
fmt.Sprintf("cannot mount devpts on %q:", devPtsPath))
return err
}
if state.RetainSession {
@@ -92,12 +95,12 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
return err
}
if name, err := k.readlink(hostProc.stdout()); err != nil {
return wrapErrSelf(err)
return err
} else if err = k.bindMount(
state,
toHost(name),
consolePath,
0,
false,
); err != nil {
return err
}
@@ -107,19 +110,23 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
if d.Mqueue {
mqueueTarget := path.Join(target, "mqueue")
if err := k.mkdir(mqueueTarget, state.ParentPerm); err != nil {
return wrapErrSelf(err)
return err
}
if err := k.mount(SourceMqueue, mqueueTarget, FstypeMqueue, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString); err != nil {
return wrapErrSuffix(err, "cannot mount mqueue:")
return err
}
}
if d.Write {
return nil
}
return wrapErrSuffix(k.remount(target, MS_RDONLY),
fmt.Sprintf("cannot remount %q:", target))
if err := k.remount(state, target, MS_RDONLY); err != nil {
return err
}
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 {
vd, ok := op.(*MountDevOp)
@@ -128,7 +135,7 @@ func (d *MountDevOp) Is(op Op) bool {
d.Mqueue == vd.Mqueue &&
d.Write == vd.Write
}
func (*MountDevOp) prefix() string { return "mounting" }
func (*MountDevOp) prefix() (string, bool) { return "mounting", true }
func (d *MountDevOp) String() string {
if d.Mqueue {
return fmt.Sprintf("dev on %q with mqueue", d.Target)

File diff suppressed because it is too large Load Diff

View File

@@ -4,27 +4,30 @@ import (
"encoding/gob"
"fmt"
"os"
"hakurei.app/container/check"
)
func init() { gob.Register(new(MkdirOp)) }
// Mkdir appends an [Op] that creates a directory in the container filesystem.
func (f *Ops) Mkdir(name *Absolute, perm os.FileMode) *Ops {
func (f *Ops) Mkdir(name *check.Absolute, perm os.FileMode) *Ops {
*f = append(*f, &MkdirOp{name, perm})
return f
}
// MkdirOp creates a directory at container Path with permission bits set to Perm.
type MkdirOp struct {
Path *Absolute
Path *check.Absolute
Perm os.FileMode
}
func (m *MkdirOp) Valid() bool { return m != nil && m.Path != nil }
func (m *MkdirOp) early(*setupState, syscallDispatcher) error { return nil }
func (m *MkdirOp) apply(_ *setupState, k syscallDispatcher) error {
return wrapErrSelf(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 {
vm, ok := op.(*MkdirOp)
@@ -32,5 +35,5 @@ func (m *MkdirOp) Is(op Op) bool {
m.Path.Is(vm.Path) &&
m.Perm == vm.Perm
}
func (*MkdirOp) prefix() string { return "creating" }
func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) }
func (*MkdirOp) prefix() (string, bool) { return "creating", true }
func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) }

View File

@@ -3,40 +3,45 @@ package container
import (
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
)
func TestMkdirOp(t *testing.T) {
t.Parallel()
checkOpBehaviour(t, []opBehaviourTestCase{
{"success", new(Params), &MkdirOp{
Path: MustAbs("/.hakurei"),
Path: check.MustAbs("/.hakurei"),
Perm: 0500,
}, nil, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/.hakurei", os.FileMode(0500)}, nil, nil},
}, nil, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/.hakurei", os.FileMode(0500)}, nil, nil),
}, nil},
})
checkOpsValid(t, []opValidTestCase{
{"nil", (*MkdirOp)(nil), false},
{"zero", new(MkdirOp), false},
{"valid", &MkdirOp{Path: MustAbs("/.hakurei")}, true},
{"valid", &MkdirOp{Path: check.MustAbs("/.hakurei")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"etc", new(Ops).Mkdir(MustAbs("/etc/"), 0), Ops{
&MkdirOp{Path: MustAbs("/etc/")},
{"etc", new(Ops).Mkdir(check.MustAbs("/etc/"), 0), Ops{
&MkdirOp{Path: check.MustAbs("/etc/")},
}},
})
checkOpIs(t, []opIsTestCase{
{"zero", new(MkdirOp), new(MkdirOp), false},
{"path differs", &MkdirOp{Path: MustAbs("/"), Perm: 0755}, &MkdirOp{Path: MustAbs("/etc/"), Perm: 0755}, false},
{"perm differs", &MkdirOp{Path: MustAbs("/")}, &MkdirOp{Path: MustAbs("/"), Perm: 0755}, false},
{"equals", &MkdirOp{Path: MustAbs("/")}, &MkdirOp{Path: MustAbs("/")}, true},
{"path differs", &MkdirOp{Path: check.MustAbs("/"), Perm: 0755}, &MkdirOp{Path: check.MustAbs("/etc/"), Perm: 0755}, false},
{"perm differs", &MkdirOp{Path: check.MustAbs("/")}, &MkdirOp{Path: check.MustAbs("/"), Perm: 0755}, false},
{"equals", &MkdirOp{Path: check.MustAbs("/")}, &MkdirOp{Path: check.MustAbs("/")}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"etc", &MkdirOp{
Path: MustAbs("/etc/"),
Path: check.MustAbs("/etc/"),
}, "creating", `directory "/etc/" perm ----------`},
})
}

View File

@@ -3,9 +3,11 @@ package container
import (
"encoding/gob"
"fmt"
"io/fs"
"slices"
"strings"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
)
const (
@@ -19,8 +21,41 @@ const (
func init() { gob.Register(new(MountOverlayOp)) }
const (
// OverlayEphemeralUnexpectedUpper is set when [MountOverlayOp.Work] is nil
// and [MountOverlayOp.Upper] holds an unexpected value.
OverlayEphemeralUnexpectedUpper = iota
// OverlayReadonlyLower is set when [MountOverlayOp.Lower] contains less than
// two entries when mounting readonly.
OverlayReadonlyLower
// OverlayEmptyLower is set when [MountOverlayOp.Lower] has length of zero.
OverlayEmptyLower
)
// OverlayArgumentError is returned for [MountOverlayOp] supplied with invalid argument.
type OverlayArgumentError struct {
Type uintptr
Value string
}
func (e *OverlayArgumentError) Error() string {
switch e.Type {
case OverlayEphemeralUnexpectedUpper:
return fmt.Sprintf("upperdir has unexpected value %q", e.Value)
case OverlayReadonlyLower:
return "readonly overlay requires at least two lowerdir"
case OverlayEmptyLower:
return "overlay requires at least one lowerdir"
default:
return fmt.Sprintf("invalid overlay argument error %#x", e.Type)
}
}
// Overlay appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target].
func (f *Ops) Overlay(target, state, work *Absolute, layers ...*Absolute) *Ops {
func (f *Ops) Overlay(target, state, work *check.Absolute, layers ...*check.Absolute) *Ops {
*f = append(*f, &MountOverlayOp{
Target: target,
Lower: layers,
@@ -32,34 +67,34 @@ func (f *Ops) Overlay(target, state, work *Absolute, layers ...*Absolute) *Ops {
// OverlayEphemeral appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target]
// with an ephemeral upperdir and workdir.
func (f *Ops) OverlayEphemeral(target *Absolute, layers ...*Absolute) *Ops {
return f.Overlay(target, AbsFHSRoot, nil, layers...)
func (f *Ops) OverlayEphemeral(target *check.Absolute, layers ...*check.Absolute) *Ops {
return f.Overlay(target, fhs.AbsRoot, nil, layers...)
}
// OverlayReadonly appends an [Op] that mounts the overlay pseudo filesystem readonly on [MountOverlayOp.Target]
func (f *Ops) OverlayReadonly(target *Absolute, layers ...*Absolute) *Ops {
func (f *Ops) OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) *Ops {
return f.Overlay(target, nil, nil, layers...)
}
// MountOverlayOp mounts [FstypeOverlay] on container path Target.
type MountOverlayOp struct {
Target *Absolute
Target *check.Absolute
// Any filesystem, does not need to be on a writable filesystem.
Lower []*Absolute
Lower []*check.Absolute
// formatted for [OptionOverlayLowerdir], resolved, prefixed and escaped during early
lower []string
// The upperdir is normally on a writable filesystem.
//
// If Work is nil and Upper holds the special value [AbsFHSRoot],
// If Work is nil and Upper holds the special value [fhs.AbsRoot],
// an ephemeral upperdir and workdir will be set up.
//
// If both Work and Upper are nil, upperdir and workdir is omitted and the overlay is mounted readonly.
Upper *Absolute
Upper *check.Absolute
// formatted for [OptionOverlayUpperdir], resolved, prefixed and escaped during early
upper string
// The workdir needs to be an empty directory on the same filesystem as upperdir.
Work *Absolute
Work *check.Absolute
// formatted for [OptionOverlayWorkdir], resolved, prefixed and escaped during early
work string
@@ -85,11 +120,11 @@ func (o *MountOverlayOp) Valid() bool {
func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
if o.Work == nil && o.Upper != nil {
switch o.Upper.String() {
case FHSRoot: // ephemeral
case fhs.Root: // ephemeral
o.ephemeral = true // intermediate root not yet available
default:
return msg.WrapErr(fs.ErrInvalid, fmt.Sprintf("upperdir has unexpected value %q", o.Upper))
return &OverlayArgumentError{OverlayEphemeralUnexpectedUpper, o.Upper.String()}
}
}
// readonly handled in apply
@@ -97,22 +132,22 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
if !o.ephemeral {
if o.Upper != o.Work && (o.Upper == nil || o.Work == nil) {
// unreachable
return msg.WrapErr(fs.ErrClosed, "impossible overlay state reached")
return OpStateError("overlay")
}
if o.Upper != nil {
if v, err := k.evalSymlinks(o.Upper.String()); err != nil {
return wrapErrSelf(err)
return err
} else {
o.upper = EscapeOverlayDataSegment(toHost(v))
o.upper = check.EscapeOverlayDataSegment(toHost(v))
}
}
if o.Work != nil {
if v, err := k.evalSymlinks(o.Work.String()); err != nil {
return wrapErrSelf(err)
return err
} else {
o.work = EscapeOverlayDataSegment(toHost(v))
o.work = check.EscapeOverlayDataSegment(toHost(v))
}
}
}
@@ -120,9 +155,9 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
o.lower = make([]string, len(o.Lower))
for i, a := range o.Lower { // nil checked in Valid
if v, err := k.evalSymlinks(a.String()); err != nil {
return wrapErrSelf(err)
return err
} else {
o.lower[i] = EscapeOverlayDataSegment(toHost(v))
o.lower[i] = check.EscapeOverlayDataSegment(toHost(v))
}
}
return nil
@@ -134,17 +169,17 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
target = toSysroot(target)
}
if err := k.mkdirAll(target, state.ParentPerm); err != nil {
return wrapErrSelf(err)
return err
}
if o.ephemeral {
var err error
// these directories are created internally, therefore early (absolute, symlink, prefix, escape) is bypassed
if o.upper, err = k.mkdirTemp(FHSRoot, intermediatePatternOverlayUpper); err != nil {
return wrapErrSelf(err)
if o.upper, err = k.mkdirTemp(fhs.Root, intermediatePatternOverlayUpper); err != nil {
return err
}
if o.work, err = k.mkdirTemp(FHSRoot, intermediatePatternOverlayWork); err != nil {
return wrapErrSelf(err)
if o.work, err = k.mkdirTemp(fhs.Root, intermediatePatternOverlayWork); err != nil {
return err
}
}
@@ -152,33 +187,34 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
if o.upper == zeroString && o.work == zeroString { // readonly
if len(o.Lower) < 2 {
return msg.WrapErr(fs.ErrInvalid, "readonly overlay requires at least two lowerdir")
return &OverlayArgumentError{OverlayReadonlyLower, zeroString}
}
// "upperdir=" and "workdir=" may be omitted. In that case the overlay will be read-only
} else {
if len(o.Lower) == 0 {
return msg.WrapErr(fs.ErrInvalid, "overlay requires at least one lowerdir")
return &OverlayArgumentError{OverlayEmptyLower, zeroString}
}
options = append(options,
OptionOverlayUpperdir+"="+o.upper,
OptionOverlayWorkdir+"="+o.work)
}
options = append(options,
OptionOverlayLowerdir+"="+strings.Join(o.lower, SpecialOverlayPath),
OptionOverlayLowerdir+"="+strings.Join(o.lower, check.SpecialOverlayPath),
OptionOverlayUserxattr)
return wrapErrSuffix(k.mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, SpecialOverlayOption)),
fmt.Sprintf("cannot mount overlay on %q:", o.Target))
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 {
vo, ok := op.(*MountOverlayOp)
return ok && o.Valid() && vo.Valid() &&
o.Target.Is(vo.Target) &&
slices.EqualFunc(o.Lower, vo.Lower, func(a *Absolute, v *Absolute) bool { return a.Is(v) }) &&
slices.EqualFunc(o.Lower, vo.Lower, func(a, v *check.Absolute) bool { return a.Is(v) }) &&
o.Upper.Is(vo.Upper) && o.Work.Is(vo.Work)
}
func (*MountOverlayOp) prefix() string { return "mounting" }
func (*MountOverlayOp) prefix() (string, bool) { return "mounting", true }
func (o *MountOverlayOp) String() string {
return fmt.Sprintf("overlay on %q with %d layers", o.Target, len(o.Lower))
}

View File

@@ -2,232 +2,266 @@ package container
import (
"errors"
"io/fs"
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
)
func TestMountOverlayOp(t *testing.T) {
t.Parallel()
t.Run("argument error", func(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
err *OverlayArgumentError
want string
}{
{"unexpected upper", &OverlayArgumentError{OverlayEphemeralUnexpectedUpper, "/proc/"},
`upperdir has unexpected value "/proc/"`},
{"lower ro short", &OverlayArgumentError{OverlayReadonlyLower, zeroString},
"readonly overlay requires at least two lowerdir"},
{"lower short", &OverlayArgumentError{OverlayEmptyLower, zeroString},
"overlay requires at least one lowerdir"},
{"oob", &OverlayArgumentError{0xdeadbeef, zeroString},
"invalid overlay argument error 0xdeadbeef"},
}
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)
}
})
}
})
checkOpBehaviour(t, []opBehaviourTestCase{
{"mkdirTemp invalid ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
Target: MustAbs("/"),
Lower: []*Absolute{
MustAbs("/var/lib/planterette/base/debian:f92c9052"),
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
Target: check.MustAbs("/"),
Lower: []*check.Absolute{
check.MustAbs("/var/lib/planterette/base/debian:f92c9052"),
check.MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
},
Upper: MustAbs("/proc/"),
}, nil, msg.WrapErr(fs.ErrInvalid, `upperdir has unexpected value "/proc/"`), nil, nil},
Upper: check.MustAbs("/proc/"),
}, nil, &OverlayArgumentError{OverlayEphemeralUnexpectedUpper, "/proc/"}, nil, nil},
{"mkdirTemp upper ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
Target: MustAbs("/"),
Lower: []*Absolute{
MustAbs("/var/lib/planterette/base/debian:f92c9052"),
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
Target: check.MustAbs("/"),
Lower: []*check.Absolute{
check.MustAbs("/var/lib/planterette/base/debian:f92c9052"),
check.MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
},
Upper: MustAbs("/"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil},
}, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot", os.FileMode(0705)}, nil, nil},
{"mkdirTemp", expectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", errUnique},
}, wrapErrSelf(errUnique)},
Upper: check.MustAbs("/"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil),
}, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0705)}, nil, nil),
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", stub.UniqueError(6)),
}, stub.UniqueError(6)},
{"mkdirTemp work ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
Target: MustAbs("/"),
Lower: []*Absolute{
MustAbs("/var/lib/planterette/base/debian:f92c9052"),
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
Target: check.MustAbs("/"),
Lower: []*check.Absolute{
check.MustAbs("/var/lib/planterette/base/debian:f92c9052"),
check.MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
},
Upper: MustAbs("/"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil},
}, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot", os.FileMode(0705)}, nil, nil},
{"mkdirTemp", expectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil},
{"mkdirTemp", expectArgs{"/", "overlay.work.*"}, "overlay.work.32768", errUnique},
}, wrapErrSelf(errUnique)},
Upper: check.MustAbs("/"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil),
}, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0705)}, nil, nil),
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil),
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.work.*"}, "overlay.work.32768", stub.UniqueError(5)),
}, stub.UniqueError(5)},
{"success ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
Target: MustAbs("/"),
Lower: []*Absolute{
MustAbs("/var/lib/planterette/base/debian:f92c9052"),
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
Target: check.MustAbs("/"),
Lower: []*check.Absolute{
check.MustAbs("/var/lib/planterette/base/debian:f92c9052"),
check.MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
},
Upper: MustAbs("/"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil},
}, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot", os.FileMode(0705)}, nil, nil},
{"mkdirTemp", expectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil},
{"mkdirTemp", expectArgs{"/", "overlay.work.*"}, "overlay.work.32768", nil},
{"mount", expectArgs{"overlay", "/sysroot", "overlay", uintptr(0), "" +
Upper: check.MustAbs("/"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil),
}, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0705)}, nil, nil),
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil),
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.work.*"}, "overlay.work.32768", nil),
call("mount", stub.ExpectArgs{"overlay", "/sysroot", "overlay", uintptr(0), "" +
"upperdir=overlay.upper.32768," +
"workdir=overlay.work.32768," +
"lowerdir=" +
`/host/var/lib/planterette/base/debian\:f92c9052:` +
`/host/var/lib/planterette/app/org.chromium.Chromium@debian\:f92c9052,` +
"userxattr"}, nil, nil},
"userxattr"}, nil, nil),
}, nil},
{"short lower ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{
MustAbs("/mnt-root/nix/.ro-store"),
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{
check.MustAbs("/mnt-root/nix/.ro-store"),
},
}, []kexpect{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil},
}, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil},
}, msg.WrapErr(fs.ErrInvalid, "readonly overlay requires at least two lowerdir")},
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil),
}, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil),
}, &OverlayArgumentError{OverlayReadonlyLower, zeroString}},
{"success ro noPrefix", &Params{ParentPerm: 0755}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{
MustAbs("/mnt-root/nix/.ro-store"),
MustAbs("/mnt-root/nix/.ro-store0"),
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{
check.MustAbs("/mnt-root/nix/.ro-store"),
check.MustAbs("/mnt-root/nix/.ro-store0"),
},
noPrefix: true,
}, []kexpect{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil},
}, nil, []kexpect{
{"mkdirAll", expectArgs{"/nix/store", os.FileMode(0755)}, nil, nil},
{"mount", expectArgs{"overlay", "/nix/store", "overlay", uintptr(0), "" +
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil),
}, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/nix/store", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"overlay", "/nix/store", "overlay", uintptr(0), "" +
"lowerdir=" +
"/host/mnt-root/nix/.ro-store:" +
"/host/mnt-root/nix/.ro-store0," +
"userxattr"}, nil, nil},
"userxattr"}, nil, nil),
}, nil},
{"success ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{
MustAbs("/mnt-root/nix/.ro-store"),
MustAbs("/mnt-root/nix/.ro-store0"),
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{
check.MustAbs("/mnt-root/nix/.ro-store"),
check.MustAbs("/mnt-root/nix/.ro-store0"),
},
}, []kexpect{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil},
}, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil},
{"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil),
}, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
"lowerdir=" +
"/host/mnt-root/nix/.ro-store:" +
"/host/mnt-root/nix/.ro-store0," +
"userxattr"}, nil, nil},
"userxattr"}, nil, nil),
}, nil},
{"nil lower", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
}, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil},
}, msg.WrapErr(fs.ErrInvalid, "overlay requires at least one lowerdir")},
Target: check.MustAbs("/nix/store"),
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
}, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
}, &OverlayArgumentError{OverlayEmptyLower, zeroString}},
{"evalSymlinks upper", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", errUnique},
}, wrapErrSelf(errUnique), nil, nil},
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", stub.UniqueError(4)),
}, stub.UniqueError(4), nil, nil},
{"evalSymlinks work", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", errUnique},
}, wrapErrSelf(errUnique), nil, nil},
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", stub.UniqueError(3)),
}, stub.UniqueError(3), nil, nil},
{"evalSymlinks lower", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", errUnique},
}, wrapErrSelf(errUnique), nil, nil},
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", stub.UniqueError(2)),
}, stub.UniqueError(2), nil, nil},
{"mkdirAll", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil},
}, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, errUnique},
}, wrapErrSelf(errUnique)},
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil),
}, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, stub.UniqueError(1)),
}, stub.UniqueError(1)},
{"mount", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil},
}, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil},
{"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "upperdir=/host/mnt-root/nix/.rw-store/.upper,workdir=/host/mnt-root/nix/.rw-store/.work,lowerdir=/host/mnt-root/nix/ro-store,userxattr"}, nil, errUnique},
}, wrapErrSuffix(errUnique, `cannot mount overlay on "/nix/store":`)},
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil),
}, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "upperdir=/host/mnt-root/nix/.rw-store/.upper,workdir=/host/mnt-root/nix/.rw-store/.work,lowerdir=/host/mnt-root/nix/ro-store,userxattr"}, nil, stub.UniqueError(0)),
}, stub.UniqueError(0)},
{"success single layer", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil},
}, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil},
{"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil),
}, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
"upperdir=/host/mnt-root/nix/.rw-store/.upper," +
"workdir=/host/mnt-root/nix/.rw-store/.work," +
"lowerdir=/host/mnt-root/nix/ro-store," +
"userxattr"}, nil, nil},
"userxattr"}, nil, nil),
}, nil},
{"success", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{
MustAbs("/mnt-root/nix/.ro-store"),
MustAbs("/mnt-root/nix/.ro-store0"),
MustAbs("/mnt-root/nix/.ro-store1"),
MustAbs("/mnt-root/nix/.ro-store2"),
MustAbs("/mnt-root/nix/.ro-store3"),
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{
check.MustAbs("/mnt-root/nix/.ro-store"),
check.MustAbs("/mnt-root/nix/.ro-store0"),
check.MustAbs("/mnt-root/nix/.ro-store1"),
check.MustAbs("/mnt-root/nix/.ro-store2"),
check.MustAbs("/mnt-root/nix/.ro-store3"),
},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/ro-store0", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store1"}, "/mnt-root/nix/ro-store1", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store2"}, "/mnt-root/nix/ro-store2", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store3"}, "/mnt-root/nix/ro-store3", nil},
}, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil},
{"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/ro-store0", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store1"}, "/mnt-root/nix/ro-store1", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store2"}, "/mnt-root/nix/ro-store2", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store3"}, "/mnt-root/nix/ro-store3", nil),
}, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
"upperdir=/host/mnt-root/nix/.rw-store/.upper," +
"workdir=/host/mnt-root/nix/.rw-store/.work," +
"lowerdir=" +
@@ -236,15 +270,18 @@ func TestMountOverlayOp(t *testing.T) {
"/host/mnt-root/nix/ro-store1:" +
"/host/mnt-root/nix/ro-store2:" +
"/host/mnt-root/nix/ro-store3," +
"userxattr"}, nil, nil},
"userxattr"}, nil, nil),
}, nil},
})
t.Run("unreachable", func(t *testing.T) {
t.Parallel()
t.Run("nil Upper non-nil Work not ephemeral", func(t *testing.T) {
wantErr := msg.WrapErr(fs.ErrClosed, "impossible overlay state reached")
t.Parallel()
wantErr := OpStateError("overlay")
if err := (&MountOverlayOp{
Work: MustAbs("/"),
Work: check.MustAbs("/"),
}).early(nil, nil); !errors.Is(err, wantErr) {
t.Errorf("apply: error = %v, want %v", err, wantErr)
}
@@ -254,39 +291,39 @@ func TestMountOverlayOp(t *testing.T) {
checkOpsValid(t, []opValidTestCase{
{"nil", (*MountOverlayOp)(nil), false},
{"zero", new(MountOverlayOp), false},
{"nil lower", &MountOverlayOp{Target: MustAbs("/"), Lower: []*Absolute{nil}}, false},
{"ro", &MountOverlayOp{Target: MustAbs("/"), Lower: []*Absolute{MustAbs("/")}}, true},
{"ro work", &MountOverlayOp{Target: MustAbs("/"), Work: MustAbs("/tmp/")}, false},
{"rw", &MountOverlayOp{Target: MustAbs("/"), Lower: []*Absolute{MustAbs("/")}, Upper: MustAbs("/"), Work: MustAbs("/")}, true},
{"nil lower", &MountOverlayOp{Target: check.MustAbs("/"), Lower: []*check.Absolute{nil}}, false},
{"ro", &MountOverlayOp{Target: check.MustAbs("/"), Lower: []*check.Absolute{check.MustAbs("/")}}, true},
{"ro work", &MountOverlayOp{Target: check.MustAbs("/"), Work: check.MustAbs("/tmp/")}, false},
{"rw", &MountOverlayOp{Target: check.MustAbs("/"), Lower: []*check.Absolute{check.MustAbs("/")}, Upper: check.MustAbs("/"), Work: check.MustAbs("/")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"full", new(Ops).Overlay(
MustAbs("/nix/store"),
MustAbs("/mnt-root/nix/.rw-store/upper"),
MustAbs("/mnt-root/nix/.rw-store/work"),
MustAbs("/mnt-root/nix/.ro-store"),
check.MustAbs("/nix/store"),
check.MustAbs("/mnt-root/nix/.rw-store/upper"),
check.MustAbs("/mnt-root/nix/.rw-store/work"),
check.MustAbs("/mnt-root/nix/.ro-store"),
), Ops{
&MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
},
}},
{"ephemeral", new(Ops).OverlayEphemeral(MustAbs("/nix/store"), MustAbs("/mnt-root/nix/.ro-store")), Ops{
{"ephemeral", new(Ops).OverlayEphemeral(check.MustAbs("/nix/store"), check.MustAbs("/mnt-root/nix/.ro-store")), Ops{
&MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/"),
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/"),
},
}},
{"readonly", new(Ops).OverlayReadonly(MustAbs("/nix/store"), MustAbs("/mnt-root/nix/.ro-store")), Ops{
{"readonly", new(Ops).OverlayReadonly(check.MustAbs("/nix/store"), check.MustAbs("/mnt-root/nix/.ro-store")), Ops{
&MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
},
}},
})
@@ -295,74 +332,74 @@ func TestMountOverlayOp(t *testing.T) {
{"zero", new(MountOverlayOp), new(MountOverlayOp), false},
{"differs target", &MountOverlayOp{
Target: MustAbs("/nix/store/differs"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
Target: check.MustAbs("/nix/store/differs"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work")}, false},
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work")}, false},
{"differs lower", &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store/differs")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store/differs")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work")}, false},
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work")}, false},
{"differs upper", &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper/differs"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper/differs"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work")}, false},
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work")}, false},
{"differs work", &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work/differs"),
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work/differs"),
}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work")}, false},
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work")}, false},
{"equals ro", &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")}}, true},
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")}}, true},
{"equals", &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work")}, true},
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work")}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"nix", &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
}, "mounting", `overlay on "/nix/store" with 1 layers`},
})
}

View File

@@ -4,6 +4,9 @@ import (
"encoding/gob"
"fmt"
"syscall"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
)
const (
@@ -14,23 +17,14 @@ const (
func init() { gob.Register(new(TmpfileOp)) }
// Place appends an [Op] that places a file in container path [TmpfileOp.Path] containing [TmpfileOp.Data].
func (f *Ops) Place(name *Absolute, data []byte) *Ops {
func (f *Ops) Place(name *check.Absolute, data []byte) *Ops {
*f = append(*f, &TmpfileOp{name, data})
return f
}
// PlaceP is like Place but writes the address of [TmpfileOp.Data] to the pointer dataP points to.
func (f *Ops) PlaceP(name *Absolute, dataP **[]byte) *Ops {
t := &TmpfileOp{Path: name}
*dataP = &t.Data
*f = append(*f, t)
return f
}
// TmpfileOp places a file on container Path containing Data.
type TmpfileOp struct {
Path *Absolute
Path *check.Absolute
Data []byte
}
@@ -38,14 +32,12 @@ func (t *TmpfileOp) Valid() bool { return t != ni
func (t *TmpfileOp) early(*setupState, syscallDispatcher) error { return nil }
func (t *TmpfileOp) apply(state *setupState, k syscallDispatcher) error {
var tmpPath string
if f, err := k.createTemp(FHSRoot, intermediatePatternTmpfile); err != nil {
return wrapErrSelf(err)
if f, err := k.createTemp(fhs.Root, intermediatePatternTmpfile); err != nil {
return err
} else if _, err = f.Write(t.Data); err != nil {
return wrapErrSuffix(err,
"cannot write to intermediate file:")
return err
} else if err = f.Close(); err != nil {
return wrapErrSuffix(err,
"cannot close intermediate file:")
return err
} else {
tmpPath = f.Name()
}
@@ -54,17 +46,18 @@ func (t *TmpfileOp) apply(state *setupState, k syscallDispatcher) error {
if err := k.ensureFile(target, 0444, state.ParentPerm); err != nil {
return err
} else if err = k.bindMount(
state,
tmpPath,
target,
syscall.MS_RDONLY|syscall.MS_NODEV,
false,
); err != nil {
return err
} else if err = k.remove(tmpPath); err != nil {
return wrapErrSelf(err)
return err
}
return nil
}
func (t *TmpfileOp) late(*setupState, syscallDispatcher) error { return nil }
func (t *TmpfileOp) Is(op Op) bool {
vt, ok := op.(*TmpfileOp)
@@ -72,7 +65,7 @@ func (t *TmpfileOp) Is(op Op) bool {
t.Path.Is(vt.Path) &&
string(t.Data) == string(vt.Data)
}
func (*TmpfileOp) prefix() string { return "placing" }
func (*TmpfileOp) prefix() (string, bool) { return "placing", true }
func (t *TmpfileOp) String() string {
return fmt.Sprintf("tmpfile %q (%d bytes)", t.Path, len(t.Data))
}

View File

@@ -3,72 +3,76 @@ package container
import (
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
)
func TestTmpfileOp(t *testing.T) {
const sampleDataString = `chronos:x:65534:65534:Hakurei:/var/empty:/bin/zsh`
var (
samplePath = MustAbs("/etc/passwd")
samplePath = check.MustAbs("/etc/passwd")
sampleData = []byte(sampleDataString)
)
t.Parallel()
checkOpBehaviour(t, []opBehaviourTestCase{
{"createTemp", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath,
Data: sampleData,
}, nil, nil, []kexpect{
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), errUnique},
}, wrapErrSelf(errUnique)},
}, nil, nil, []stub.Call{
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), stub.UniqueError(5)),
}, stub.UniqueError(5)},
{"Write", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath,
Data: sampleData,
}, nil, nil, []kexpect{
{"createTemp", expectArgs{"/", "tmp.*"}, writeErrOsFile{errUnique}, nil},
}, wrapErrSuffix(errUnique, "cannot write to intermediate file:")},
}, nil, nil, []stub.Call{
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, writeErrOsFile{stub.UniqueError(4)}, nil),
}, stub.UniqueError(4)},
{"Close", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath,
Data: sampleData,
}, nil, nil, []kexpect{
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, errUnique), nil},
}, wrapErrSuffix(errUnique, "cannot close intermediate file:")},
}, nil, nil, []stub.Call{
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, stub.UniqueError(3)), nil),
}, stub.UniqueError(3)},
{"ensureFile", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath,
Data: sampleData,
}, nil, nil, []kexpect{
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil},
{"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, errUnique},
}, errUnique},
}, nil, nil, []stub.Call{
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil),
call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, stub.UniqueError(2)),
}, stub.UniqueError(2)},
{"bindMount", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath,
Data: sampleData,
}, nil, nil, []kexpect{
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil},
{"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil},
{"bindMount", expectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, errUnique},
}, errUnique},
}, nil, nil, []stub.Call{
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil),
call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil),
call("bindMount", stub.ExpectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, stub.UniqueError(1)),
}, stub.UniqueError(1)},
{"remove", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath,
Data: sampleData,
}, nil, nil, []kexpect{
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil},
{"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil},
{"bindMount", expectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil},
{"remove", expectArgs{"tmp.32768"}, nil, errUnique},
}, wrapErrSelf(errUnique)},
}, nil, nil, []stub.Call{
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil),
call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil),
call("bindMount", stub.ExpectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil),
call("remove", stub.ExpectArgs{"tmp.32768"}, nil, stub.UniqueError(0)),
}, stub.UniqueError(0)},
{"success", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath,
Data: sampleData,
}, nil, nil, []kexpect{
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil},
{"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil},
{"bindMount", expectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil},
{"remove", expectArgs{"tmp.32768"}, nil, nil},
}, nil, nil, []stub.Call{
call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil),
call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil),
call("bindMount", stub.ExpectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil),
call("remove", stub.ExpectArgs{"tmp.32768"}, nil, nil),
}, nil},
})
@@ -79,18 +83,8 @@ func TestTmpfileOp(t *testing.T) {
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"noref", new(Ops).Place(samplePath, sampleData), Ops{
&TmpfileOp{
Path: samplePath,
Data: sampleData,
},
}},
{"ref", new(Ops).PlaceP(samplePath, new(*[]byte)), Ops{
&TmpfileOp{
Path: samplePath,
Data: []byte{},
},
{"full", new(Ops).Place(samplePath, sampleData), Ops{
&TmpfileOp{Path: samplePath, Data: sampleData},
}},
})
@@ -98,7 +92,7 @@ func TestTmpfileOp(t *testing.T) {
{"zero", new(TmpfileOp), new(TmpfileOp), false},
{"differs path", &TmpfileOp{
Path: MustAbs("/etc/group"),
Path: check.MustAbs("/etc/group"),
Data: sampleData,
}, &TmpfileOp{
Path: samplePath,

View File

@@ -4,34 +4,36 @@ import (
"encoding/gob"
"fmt"
. "syscall"
"hakurei.app/container/check"
)
func init() { gob.Register(new(MountProcOp)) }
// Proc appends an [Op] that mounts a private instance of proc.
func (f *Ops) Proc(target *Absolute) *Ops {
func (f *Ops) Proc(target *check.Absolute) *Ops {
*f = append(*f, &MountProcOp{target})
return f
}
// MountProcOp mounts a new instance of [FstypeProc] on container path Target.
type MountProcOp struct{ Target *Absolute }
type MountProcOp struct{ Target *check.Absolute }
func (p *MountProcOp) Valid() bool { return p != nil && p.Target != nil }
func (p *MountProcOp) early(*setupState, syscallDispatcher) error { return nil }
func (p *MountProcOp) apply(state *setupState, k syscallDispatcher) error {
target := toSysroot(p.Target.String())
if err := k.mkdirAll(target, state.ParentPerm); err != nil {
return wrapErrSelf(err)
return err
}
return wrapErrSuffix(k.mount(SourceProc, target, FstypeProc, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString),
fmt.Sprintf("cannot mount proc on %q:", p.Target.String()))
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 {
vp, ok := op.(*MountProcOp)
return ok && p.Valid() && vp.Valid() &&
p.Target.Is(vp.Target)
}
func (*MountProcOp) prefix() string { return "mounting" }
func (p *MountProcOp) String() string { return fmt.Sprintf("proc on %q", p.Target) }
func (*MountProcOp) prefix() (string, bool) { return "mounting", true }
func (p *MountProcOp) String() string { return fmt.Sprintf("proc on %q", p.Target) }

View File

@@ -3,35 +3,40 @@ package container
import (
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
)
func TestMountProcOp(t *testing.T) {
t.Parallel()
checkOpBehaviour(t, []opBehaviourTestCase{
{"mkdir", &Params{ParentPerm: 0755},
&MountProcOp{
Target: MustAbs("/proc/"),
}, nil, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, errUnique},
}, wrapErrSelf(errUnique)},
Target: check.MustAbs("/proc/"),
}, nil, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, stub.UniqueError(0)),
}, stub.UniqueError(0)},
{"success", &Params{ParentPerm: 0700},
&MountProcOp{
Target: MustAbs("/proc/"),
}, nil, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/proc", os.FileMode(0700)}, nil, nil},
{"mount", expectArgs{"proc", "/sysroot/proc", "proc", uintptr(0xe), ""}, nil, nil},
Target: check.MustAbs("/proc/"),
}, nil, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0700)}, nil, nil),
call("mount", stub.ExpectArgs{"proc", "/sysroot/proc", "proc", uintptr(0xe), ""}, nil, nil),
}, nil},
})
checkOpsValid(t, []opValidTestCase{
{"nil", (*MountProcOp)(nil), false},
{"zero", new(MountProcOp), false},
{"valid", &MountProcOp{Target: MustAbs("/proc/")}, true},
{"valid", &MountProcOp{Target: check.MustAbs("/proc/")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"proc", new(Ops).Proc(MustAbs("/proc/")), Ops{
&MountProcOp{Target: MustAbs("/proc/")},
{"proc", new(Ops).Proc(check.MustAbs("/proc/")), Ops{
&MountProcOp{Target: check.MustAbs("/proc/")},
}},
})
@@ -39,20 +44,20 @@ func TestMountProcOp(t *testing.T) {
{"zero", new(MountProcOp), new(MountProcOp), false},
{"target differs", &MountProcOp{
Target: MustAbs("/proc/nonexistent"),
Target: check.MustAbs("/proc/nonexistent"),
}, &MountProcOp{
Target: MustAbs("/proc/"),
Target: check.MustAbs("/proc/"),
}, false},
{"equals", &MountProcOp{
Target: MustAbs("/proc/"),
Target: check.MustAbs("/proc/"),
}, &MountProcOp{
Target: MustAbs("/proc/"),
Target: check.MustAbs("/proc/"),
}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"proc", &MountProcOp{Target: MustAbs("/proc/")},
{"proc", &MountProcOp{Target: check.MustAbs("/proc/")},
"mounting", `proc on "/proc/"`},
})
}

View File

@@ -3,28 +3,30 @@ package container
import (
"encoding/gob"
"fmt"
"hakurei.app/container/check"
)
func init() { gob.Register(new(RemountOp)) }
// Remount appends an [Op] that applies [RemountOp.Flags] on container path [RemountOp.Target].
func (f *Ops) Remount(target *Absolute, flags uintptr) *Ops {
func (f *Ops) Remount(target *check.Absolute, flags uintptr) *Ops {
*f = append(*f, &RemountOp{target, flags})
return f
}
// RemountOp remounts Target with Flags.
type RemountOp struct {
Target *Absolute
Target *check.Absolute
Flags uintptr
}
func (r *RemountOp) Valid() bool { return r != nil && r.Target != nil }
func (*RemountOp) early(*setupState, syscallDispatcher) error { return nil }
func (r *RemountOp) apply(_ *setupState, k syscallDispatcher) error {
return wrapErrSuffix(k.remount(toSysroot(r.Target.String()), r.Flags),
fmt.Sprintf("cannot remount %q:", r.Target))
func (r *RemountOp) apply(state *setupState, k syscallDispatcher) error {
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 {
vr, ok := op.(*RemountOp)
@@ -32,5 +34,5 @@ func (r *RemountOp) Is(op Op) bool {
r.Target.Is(vr.Target) &&
r.Flags == vr.Flags
}
func (*RemountOp) prefix() string { return "remounting" }
func (r *RemountOp) String() string { return fmt.Sprintf("%q flags %#x", r.Target, r.Flags) }
func (*RemountOp) prefix() (string, bool) { return "remounting", true }
func (r *RemountOp) String() string { return fmt.Sprintf("%q flags %#x", r.Target, r.Flags) }

View File

@@ -3,28 +3,33 @@ package container
import (
"syscall"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
)
func TestRemountOp(t *testing.T) {
t.Parallel()
checkOpBehaviour(t, []opBehaviourTestCase{
{"success", new(Params), &RemountOp{
Target: MustAbs("/"),
Target: check.MustAbs("/"),
Flags: syscall.MS_RDONLY,
}, nil, nil, []kexpect{
{"remount", expectArgs{"/sysroot", uintptr(1)}, nil, nil},
}, nil, nil, []stub.Call{
call("remount", stub.ExpectArgs{"/sysroot", uintptr(1)}, nil, nil),
}, nil},
})
checkOpsValid(t, []opValidTestCase{
{"nil", (*RemountOp)(nil), false},
{"zero", new(RemountOp), false},
{"valid", &RemountOp{Target: MustAbs("/"), Flags: syscall.MS_RDONLY}, true},
{"valid", &RemountOp{Target: check.MustAbs("/"), Flags: syscall.MS_RDONLY}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"root", new(Ops).Remount(MustAbs("/"), syscall.MS_RDONLY), Ops{
{"root", new(Ops).Remount(check.MustAbs("/"), syscall.MS_RDONLY), Ops{
&RemountOp{
Target: MustAbs("/"),
Target: check.MustAbs("/"),
Flags: syscall.MS_RDONLY,
},
}},
@@ -34,33 +39,33 @@ func TestRemountOp(t *testing.T) {
{"zero", new(RemountOp), new(RemountOp), false},
{"target differs", &RemountOp{
Target: MustAbs("/dev/"),
Target: check.MustAbs("/dev/"),
Flags: syscall.MS_RDONLY,
}, &RemountOp{
Target: MustAbs("/"),
Target: check.MustAbs("/"),
Flags: syscall.MS_RDONLY,
}, false},
{"flags differs", &RemountOp{
Target: MustAbs("/"),
Target: check.MustAbs("/"),
Flags: syscall.MS_RDONLY | syscall.MS_NODEV,
}, &RemountOp{
Target: MustAbs("/"),
Target: check.MustAbs("/"),
Flags: syscall.MS_RDONLY,
}, false},
{"equals", &RemountOp{
Target: MustAbs("/"),
Target: check.MustAbs("/"),
Flags: syscall.MS_RDONLY,
}, &RemountOp{
Target: MustAbs("/"),
Target: check.MustAbs("/"),
Flags: syscall.MS_RDONLY,
}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"root", &RemountOp{
Target: MustAbs("/"),
Target: check.MustAbs("/"),
Flags: syscall.MS_RDONLY,
}, "remounting", `"/" flags 0x1`},
})

View File

@@ -3,21 +3,22 @@ package container
import (
"encoding/gob"
"fmt"
"io/fs"
"path"
"hakurei.app/container/check"
)
func init() { gob.Register(new(SymlinkOp)) }
// Link appends an [Op] that creates a symlink in the container filesystem.
func (f *Ops) Link(target *Absolute, linkName string, dereference bool) *Ops {
func (f *Ops) Link(target *check.Absolute, linkName string, dereference bool) *Ops {
*f = append(*f, &SymlinkOp{target, linkName, dereference})
return f
}
// SymlinkOp optionally dereferences LinkName and creates a symlink at container path Target.
type SymlinkOp struct {
Target *Absolute
Target *check.Absolute
// LinkName is an arbitrary uninterpreted pathname.
LinkName string
@@ -29,11 +30,11 @@ func (l *SymlinkOp) Valid() bool { return l != nil && l.Target != nil && l.LinkN
func (l *SymlinkOp) early(_ *setupState, k syscallDispatcher) error {
if l.Dereference {
if !isAbs(l.LinkName) {
return msg.WrapErr(fs.ErrInvalid, fmt.Sprintf("path %q is not absolute", l.LinkName))
if !path.IsAbs(l.LinkName) {
return &check.AbsoluteError{Pathname: l.LinkName}
}
if name, err := k.readlink(l.LinkName); err != nil {
return wrapErrSelf(err)
return err
} else {
l.LinkName = name
}
@@ -44,11 +45,13 @@ func (l *SymlinkOp) early(_ *setupState, k syscallDispatcher) error {
func (l *SymlinkOp) apply(state *setupState, k syscallDispatcher) error {
target := toSysroot(l.Target.String())
if err := k.mkdirAll(path.Dir(target), state.ParentPerm); err != nil {
return wrapErrSelf(err)
return err
}
return wrapErrSelf(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 {
vl, ok := op.(*SymlinkOp)
return ok && l.Valid() && vl.Valid() &&
@@ -56,7 +59,7 @@ func (l *SymlinkOp) Is(op Op) bool {
l.LinkName == vl.LinkName &&
l.Dereference == vl.Dereference
}
func (*SymlinkOp) prefix() string { return "creating" }
func (*SymlinkOp) prefix() (string, bool) { return "creating", true }
func (l *SymlinkOp) String() string {
return fmt.Sprintf("symlink on %q linkname %q", l.Target, l.LinkName)
}

View File

@@ -1,51 +1,55 @@
package container
import (
"io/fs"
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
)
func TestSymlinkOp(t *testing.T) {
t.Parallel()
checkOpBehaviour(t, []opBehaviourTestCase{
{"mkdir", &Params{ParentPerm: 0700}, &SymlinkOp{
Target: MustAbs("/etc/nixos"),
Target: check.MustAbs("/etc/nixos"),
LinkName: "/etc/static/nixos",
}, nil, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, errUnique},
}, wrapErrSelf(errUnique)},
}, nil, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, stub.UniqueError(1)),
}, stub.UniqueError(1)},
{"abs", &Params{ParentPerm: 0755}, &SymlinkOp{
Target: MustAbs("/etc/mtab"),
Target: check.MustAbs("/etc/mtab"),
LinkName: "etc/mtab",
Dereference: true,
}, nil, msg.WrapErr(fs.ErrInvalid, `path "etc/mtab" is not absolute`), nil, nil},
}, nil, &check.AbsoluteError{Pathname: "etc/mtab"}, nil, nil},
{"readlink", &Params{ParentPerm: 0755}, &SymlinkOp{
Target: MustAbs("/etc/mtab"),
Target: check.MustAbs("/etc/mtab"),
LinkName: "/etc/mtab",
Dereference: true,
}, []kexpect{
{"readlink", expectArgs{"/etc/mtab"}, "/proc/mounts", errUnique},
}, wrapErrSelf(errUnique), nil, nil},
}, []stub.Call{
call("readlink", stub.ExpectArgs{"/etc/mtab"}, "/proc/mounts", stub.UniqueError(0)),
}, stub.UniqueError(0), nil, nil},
{"success noderef", &Params{ParentPerm: 0700}, &SymlinkOp{
Target: MustAbs("/etc/nixos"),
Target: check.MustAbs("/etc/nixos"),
LinkName: "/etc/static/nixos",
}, nil, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, nil},
{"symlink", expectArgs{"/etc/static/nixos", "/sysroot/etc/nixos"}, nil, nil},
}, nil, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, nil),
call("symlink", stub.ExpectArgs{"/etc/static/nixos", "/sysroot/etc/nixos"}, nil, nil),
}, nil},
{"success", &Params{ParentPerm: 0755}, &SymlinkOp{
Target: MustAbs("/etc/mtab"),
Target: check.MustAbs("/etc/mtab"),
LinkName: "/etc/mtab",
Dereference: true,
}, []kexpect{
{"readlink", expectArgs{"/etc/mtab"}, "/proc/mounts", nil},
}, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/etc", os.FileMode(0755)}, nil, nil},
{"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil},
}, []stub.Call{
call("readlink", stub.ExpectArgs{"/etc/mtab"}, "/proc/mounts", nil),
}, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc", os.FileMode(0755)}, nil, nil),
call("symlink", stub.ExpectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil),
}, nil},
})
@@ -53,18 +57,18 @@ func TestSymlinkOp(t *testing.T) {
{"nil", (*SymlinkOp)(nil), false},
{"zero", new(SymlinkOp), false},
{"nil target", &SymlinkOp{LinkName: "/run/current-system"}, false},
{"zero linkname", &SymlinkOp{Target: MustAbs("/run/current-system")}, false},
{"valid", &SymlinkOp{Target: MustAbs("/run/current-system"), LinkName: "/run/current-system", Dereference: true}, true},
{"zero linkname", &SymlinkOp{Target: check.MustAbs("/run/current-system")}, false},
{"valid", &SymlinkOp{Target: check.MustAbs("/run/current-system"), LinkName: "/run/current-system", Dereference: true}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"current-system", new(Ops).Link(
MustAbs("/run/current-system"),
check.MustAbs("/run/current-system"),
"/run/current-system",
true,
), Ops{
&SymlinkOp{
Target: MustAbs("/run/current-system"),
Target: check.MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
},
@@ -75,40 +79,40 @@ func TestSymlinkOp(t *testing.T) {
{"zero", new(SymlinkOp), new(SymlinkOp), false},
{"target differs", &SymlinkOp{
Target: MustAbs("/run/current-system/differs"),
Target: check.MustAbs("/run/current-system/differs"),
LinkName: "/run/current-system",
Dereference: true,
}, &SymlinkOp{
Target: MustAbs("/run/current-system"),
Target: check.MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
}, false},
{"linkname differs", &SymlinkOp{
Target: MustAbs("/run/current-system"),
Target: check.MustAbs("/run/current-system"),
LinkName: "/run/current-system/differs",
Dereference: true,
}, &SymlinkOp{
Target: MustAbs("/run/current-system"),
Target: check.MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
}, false},
{"dereference differs", &SymlinkOp{
Target: MustAbs("/run/current-system"),
Target: check.MustAbs("/run/current-system"),
LinkName: "/run/current-system",
}, &SymlinkOp{
Target: MustAbs("/run/current-system"),
Target: check.MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
}, false},
{"equals", &SymlinkOp{
Target: MustAbs("/run/current-system"),
Target: check.MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
}, &SymlinkOp{
Target: MustAbs("/run/current-system"),
Target: check.MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
}, true},
@@ -116,7 +120,7 @@ func TestSymlinkOp(t *testing.T) {
checkOpMeta(t, []opMetaTestCase{
{"current-system", &SymlinkOp{
Target: MustAbs("/run/current-system"),
Target: check.MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
}, "creating", `symlink on "/run/current-system" linkname "/run/current-system"`},

View File

@@ -3,22 +3,30 @@ package container
import (
"encoding/gob"
"fmt"
"io/fs"
"math"
"os"
"strconv"
. "syscall"
"hakurei.app/container/check"
)
func init() { gob.Register(new(MountTmpfsOp)) }
type TmpfsSizeError int
func (e TmpfsSizeError) Error() string {
return "tmpfs size " + strconv.Itoa(int(e)) + " out of bounds"
}
// Tmpfs appends an [Op] that mounts tmpfs on container path [MountTmpfsOp.Path].
func (f *Ops) Tmpfs(target *Absolute, size int, perm os.FileMode) *Ops {
func (f *Ops) Tmpfs(target *check.Absolute, size int, perm os.FileMode) *Ops {
*f = append(*f, &MountTmpfsOp{SourceTmpfsEphemeral, target, MS_NOSUID | MS_NODEV, size, perm})
return f
}
// Readonly appends an [Op] that mounts read-only tmpfs on container path [MountTmpfsOp.Path].
func (f *Ops) Readonly(target *Absolute, perm os.FileMode) *Ops {
func (f *Ops) Readonly(target *check.Absolute, perm os.FileMode) *Ops {
*f = append(*f, &MountTmpfsOp{SourceTmpfsReadonly, target, MS_RDONLY | MS_NOSUID | MS_NODEV, 0, perm})
return f
}
@@ -26,7 +34,7 @@ func (f *Ops) Readonly(target *Absolute, perm os.FileMode) *Ops {
// MountTmpfsOp mounts [FstypeTmpfs] on container Path.
type MountTmpfsOp struct {
FSName string
Path *Absolute
Path *check.Absolute
Flags uintptr
Size int
Perm os.FileMode
@@ -36,10 +44,11 @@ func (t *MountTmpfsOp) Valid() bool { return t !=
func (t *MountTmpfsOp) early(*setupState, syscallDispatcher) error { return nil }
func (t *MountTmpfsOp) apply(_ *setupState, k syscallDispatcher) error {
if t.Size < 0 || t.Size > math.MaxUint>>1 {
return msg.WrapErr(fs.ErrInvalid, fmt.Sprintf("size %d out of bounds", t.Size))
return TmpfsSizeError(t.Size)
}
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 {
vt, ok := op.(*MountTmpfsOp)
@@ -50,5 +59,5 @@ func (t *MountTmpfsOp) Is(op Op) bool {
t.Size == vt.Size &&
t.Perm == vt.Perm
}
func (*MountTmpfsOp) prefix() string { return "mounting" }
func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }
func (*MountTmpfsOp) prefix() (string, bool) { return "mounting", true }
func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }

View File

@@ -1,31 +1,44 @@
package container
import (
"io/fs"
"os"
"syscall"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
)
func TestMountTmpfsOp(t *testing.T) {
t.Parallel()
t.Run("size error", func(t *testing.T) {
t.Parallel()
tmpfsSizeError := TmpfsSizeError(-1)
want := "tmpfs size -1 out of bounds"
if got := tmpfsSizeError.Error(); got != want {
t.Errorf("Error: %q, want %q", got, want)
}
})
checkOpBehaviour(t, []opBehaviourTestCase{
{"size oob", new(Params), &MountTmpfsOp{
Size: -1,
}, nil, nil, nil, msg.WrapErr(fs.ErrInvalid, "size -1 out of bounds")},
}, nil, nil, nil, TmpfsSizeError(-1)},
{"success", new(Params), &MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user/1000/"),
Path: check.MustAbs("/run/user/1000/"),
Size: 1 << 10,
Perm: 0700,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{
"ephemeral", // fsname
"/sysroot/run/user/1000", // target
uintptr(0), // flags
0x400, // size
os.FileMode(0700), // perm
}, nil, nil},
}, nil, nil),
}, nil},
})
@@ -33,19 +46,19 @@ func TestMountTmpfsOp(t *testing.T) {
{"nil", (*MountTmpfsOp)(nil), false},
{"zero", new(MountTmpfsOp), false},
{"nil path", &MountTmpfsOp{FSName: "tmpfs"}, false},
{"zero fsname", &MountTmpfsOp{Path: MustAbs("/tmp/")}, false},
{"valid", &MountTmpfsOp{FSName: "tmpfs", Path: MustAbs("/tmp/")}, true},
{"zero fsname", &MountTmpfsOp{Path: check.MustAbs("/tmp/")}, false},
{"valid", &MountTmpfsOp{FSName: "tmpfs", Path: check.MustAbs("/tmp/")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"runtime", new(Ops).Tmpfs(
MustAbs("/run/user"),
check.MustAbs("/run/user"),
1<<10,
0755,
), Ops{
&MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user"),
Path: check.MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
@@ -53,12 +66,12 @@ func TestMountTmpfsOp(t *testing.T) {
}},
{"nscd", new(Ops).Readonly(
MustAbs("/var/run/nscd"),
check.MustAbs("/var/run/nscd"),
0755,
), Ops{
&MountTmpfsOp{
FSName: "readonly",
Path: MustAbs("/var/run/nscd"),
Path: check.MustAbs("/var/run/nscd"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV | syscall.MS_RDONLY,
Perm: 0755,
},
@@ -70,13 +83,13 @@ func TestMountTmpfsOp(t *testing.T) {
{"fsname differs", &MountTmpfsOp{
FSName: "readonly",
Path: MustAbs("/run/user"),
Path: check.MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
}, &MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user"),
Path: check.MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
@@ -84,13 +97,13 @@ func TestMountTmpfsOp(t *testing.T) {
{"path differs", &MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user/differs"),
Path: check.MustAbs("/run/user/differs"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
}, &MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user"),
Path: check.MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
@@ -98,13 +111,13 @@ func TestMountTmpfsOp(t *testing.T) {
{"flags differs", &MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user"),
Path: check.MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV | syscall.MS_RDONLY,
Size: 1 << 10,
Perm: 0755,
}, &MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user"),
Path: check.MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
@@ -112,13 +125,13 @@ func TestMountTmpfsOp(t *testing.T) {
{"size differs", &MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user"),
Path: check.MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1,
Perm: 0755,
}, &MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user"),
Path: check.MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
@@ -126,13 +139,13 @@ func TestMountTmpfsOp(t *testing.T) {
{"perm differs", &MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user"),
Path: check.MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0700,
}, &MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user"),
Path: check.MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
@@ -140,13 +153,13 @@ func TestMountTmpfsOp(t *testing.T) {
{"equals", &MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user"),
Path: check.MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
}, &MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user"),
Path: check.MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
@@ -156,7 +169,7 @@ func TestMountTmpfsOp(t *testing.T) {
checkOpMeta(t, []opMetaTestCase{
{"runtime", &MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user"),
Path: check.MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,

View File

@@ -5,7 +5,7 @@ import (
"syscall"
"unsafe"
"hakurei.app/container/seccomp"
"hakurei.app/container/std"
)
// include/uapi/linux/landlock.h
@@ -14,7 +14,8 @@ const (
LANDLOCK_CREATE_RULESET_VERSION = 1 << iota
)
type LandlockAccessFS uintptr
// LandlockAccessFS is bitmask of handled filesystem actions.
type LandlockAccessFS uint64
const (
LANDLOCK_ACCESS_FS_EXECUTE LandlockAccessFS = 1 << iota
@@ -105,7 +106,8 @@ func (f LandlockAccessFS) String() string {
}
}
type LandlockAccessNet uintptr
// LandlockAccessNet is bitmask of handled network actions.
type LandlockAccessNet uint64
const (
LANDLOCK_ACCESS_NET_BIND_TCP LandlockAccessNet = 1 << iota
@@ -140,7 +142,8 @@ func (f LandlockAccessNet) String() string {
}
}
type LandlockScope uintptr
// LandlockScope is bitmask of scopes restricting a Landlock domain from accessing outside resources.
type LandlockScope uint64
const (
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET LandlockScope = 1 << iota
@@ -175,6 +178,7 @@ func (f LandlockScope) String() string {
}
}
// RulesetAttr is equivalent to struct landlock_ruleset_attr.
type RulesetAttr struct {
// Bitmask of handled filesystem actions.
HandledAccessFS LandlockAccessFS
@@ -212,7 +216,7 @@ func (rulesetAttr *RulesetAttr) Create(flags uintptr) (fd int, err error) {
size = unsafe.Sizeof(*rulesetAttr)
}
rulesetFd, _, errno := syscall.Syscall(seccomp.SYS_LANDLOCK_CREATE_RULESET, pointer, size, flags)
rulesetFd, _, errno := syscall.Syscall(std.SYS_LANDLOCK_CREATE_RULESET, pointer, size, flags)
fd = int(rulesetFd)
err = errno
@@ -231,7 +235,7 @@ func LandlockGetABI() (int, error) {
}
func LandlockRestrictSelf(rulesetFd int, flags uintptr) error {
r, _, errno := syscall.Syscall(seccomp.SYS_LANDLOCK_RESTRICT_SELF, uintptr(rulesetFd), flags, 0)
r, _, errno := syscall.Syscall(std.SYS_LANDLOCK_RESTRICT_SELF, uintptr(rulesetFd), flags, 0)
if r != 0 {
return errno
}

View File

@@ -8,6 +8,8 @@ import (
)
func TestLandlockString(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
rulesetAttr *container.RulesetAttr
@@ -46,6 +48,7 @@ func TestLandlockString(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.rulesetAttr.String(); got != tc.want {
t.Errorf("String: %s, want %s", got, tc.want)
}
@@ -54,6 +57,7 @@ func TestLandlockString(t *testing.T) {
}
func TestLandlockAttrSize(t *testing.T) {
t.Parallel()
want := 24
if got := unsafe.Sizeof(container.RulesetAttr{}); got != uintptr(want) {
t.Errorf("Sizeof: %d, want %d", got, want)

View File

@@ -4,10 +4,10 @@ import (
"errors"
"fmt"
"os"
"strings"
. "syscall"
"hakurei.app/container/vfs"
"hakurei.app/message"
)
/*
@@ -43,6 +43,8 @@ const (
// Note that any source value is allowed when fstype is [FstypeOverlay].
SourceOverlay = "overlay"
// SourceTmpfs is used when mounting tmpfs.
SourceTmpfs = "tmpfs"
// SourceTmpfsRootfs is used when mounting the tmpfs instance backing the intermediate root.
SourceTmpfsRootfs = "rootfs"
// SourceTmpfsDevtmpfs is used when mounting tmpfs representing a subset of host devtmpfs.
@@ -57,7 +59,6 @@ const (
FstypeNULL = zeroString
// FstypeProc represents the proc pseudo-filesystem.
// A fully visible instance of proc must be available in the mount namespace for proc to be mounted.
// This filesystem type is usually mounted on [FHSProc].
FstypeProc = "proc"
// FstypeDevpts represents the devpts pseudo-filesystem.
// This type of filesystem is usually mounted on /dev/pts.
@@ -84,44 +85,29 @@ const (
// OptionOverlayUserxattr represents the userxattr option of the overlay pseudo-filesystem.
// Use the "user.overlay." xattr namespace instead of "trusted.overlay.".
OptionOverlayUserxattr = "userxattr"
// SpecialOverlayEscape is the escape string for overlay mount options.
SpecialOverlayEscape = `\`
// SpecialOverlayOption is the separator string between overlay mount options.
SpecialOverlayOption = ","
// SpecialOverlayPath is the separator string between overlay paths.
SpecialOverlayPath = ":"
)
// bindMount mounts source on target and recursively applies flags if MS_REC is set.
func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) error {
func (p *procPaths) bindMount(msg message.Msg, source, target string, flags uintptr) error {
// syscallDispatcher.bindMount and procPaths.remount must not be called from this function
if eq {
p.k.verbosef("resolved %q flags %#x", target, flags)
} else {
p.k.verbosef("resolved %q on %q flags %#x", source, target, flags)
}
if err := p.k.mount(source, target, FstypeNULL, MS_SILENT|MS_BIND|flags&MS_REC, zeroString); err != nil {
return wrapErrSuffix(err,
fmt.Sprintf("cannot mount %q on %q:", source, target))
return err
}
return p.k.remount(target, flags)
return p.k.remount(msg, target, flags)
}
// remount applies flags on target, recursively if MS_REC is set.
func (p *procPaths) remount(target string, flags uintptr) error {
func (p *procPaths) remount(msg message.Msg, target string, flags uintptr) error {
// syscallDispatcher methods bindMount, remount must not be called from this function
var targetFinal string
if v, err := p.k.evalSymlinks(target); err != nil {
return wrapErrSelf(err)
return err
} else {
targetFinal = v
if targetFinal != target {
p.k.verbosef("target resolves to %q", targetFinal)
msg.Verbosef("target resolves to %q", targetFinal)
}
}
@@ -133,14 +119,12 @@ func (p *procPaths) remount(target string, flags uintptr) error {
destFd, err = p.k.open(targetFinal, O_PATH|O_CLOEXEC, 0)
return
}); err != nil {
return wrapErrSuffix(err,
fmt.Sprintf("cannot open %q:", targetFinal))
return &os.PathError{Op: "open", Path: targetFinal, Err: err}
}
if v, err := p.k.readlink(p.fd(destFd)); err != nil {
return wrapErrSelf(err)
return err
} else if err = p.k.close(destFd); err != nil {
return wrapErrSuffix(err,
fmt.Sprintf("cannot close %q:", targetFinal))
return &os.PathError{Op: "close", Path: targetFinal, Err: err}
} else {
targetKFinal = v
}
@@ -150,17 +134,11 @@ func (p *procPaths) remount(target string, flags uintptr) error {
return p.mountinfo(func(d *vfs.MountInfoDecoder) error {
n, err := d.Unfold(targetKFinal)
if err != nil {
if errors.Is(err, ESTALE) {
return msg.WrapErr(err,
fmt.Sprintf("mount point %q never appeared in mountinfo", targetKFinal))
}
return wrapErrSuffix(err,
"cannot unfold mount hierarchy:")
return err
}
if err = remountWithFlags(p.k, n, mf); err != nil {
return wrapErrSuffix(err,
fmt.Sprintf("cannot remount %q:", n.Clean))
if err = remountWithFlags(p.k, msg, n, mf); err != nil {
return err
}
if flags&MS_REC == 0 {
return nil
@@ -172,11 +150,8 @@ func (p *procPaths) remount(target string, flags uintptr) error {
continue
}
err = remountWithFlags(p.k, cur, mf)
if err != nil && !errors.Is(err, EACCES) {
return wrapErrSuffix(err,
fmt.Sprintf("cannot propagate flags to %q:", cur.Clean))
if err = remountWithFlags(p.k, msg, cur, mf); err != nil && !errors.Is(err, EACCES) {
return err
}
}
@@ -185,12 +160,12 @@ func (p *procPaths) remount(target string, flags uintptr) error {
}
// remountWithFlags remounts mount point described by [vfs.MountInfoNode].
func remountWithFlags(k syscallDispatcher, n *vfs.MountInfoNode, mf uintptr) error {
func remountWithFlags(k syscallDispatcher, msg message.Msg, n *vfs.MountInfoNode, mf uintptr) error {
// syscallDispatcher methods bindMount, remount must not be called from this function
kf, unmatched := n.Flags()
if len(unmatched) != 0 {
k.verbosef("unmatched vfs options: %q", unmatched)
msg.Verbosef("unmatched vfs options: %q", unmatched)
}
if kf&mf != mf {
@@ -205,15 +180,13 @@ func mountTmpfs(k syscallDispatcher, fsname, target string, flags uintptr, size
// syscallDispatcher.mountTmpfs must not be called from this function
if err := k.mkdirAll(target, parentPerm(perm)); err != nil {
return wrapErrSelf(err)
return err
}
opt := fmt.Sprintf("mode=%#o", perm)
if size > 0 {
opt += fmt.Sprintf(",size=%d", size)
}
return wrapErrSuffix(
k.mount(fsname, target, FstypeTmpfs, flags, opt),
fmt.Sprintf("cannot mount tmpfs on %q:", target))
return k.mount(fsname, target, FstypeTmpfs, flags, opt)
}
func parentPerm(perm os.FileMode) os.FileMode {
@@ -226,20 +199,3 @@ func parentPerm(perm os.FileMode) os.FileMode {
}
return os.FileMode(pperm)
}
// EscapeOverlayDataSegment escapes a string for formatting into the data argument of an overlay mount call.
func EscapeOverlayDataSegment(s string) string {
if s == zeroString {
return zeroString
}
if f := strings.SplitN(s, "\x00", 2); len(f) > 0 {
s = f[0]
}
return strings.NewReplacer(
SpecialOverlayEscape, SpecialOverlayEscape+SpecialOverlayEscape,
SpecialOverlayOption, SpecialOverlayEscape+SpecialOverlayOption,
SpecialOverlayPath, SpecialOverlayEscape+SpecialOverlayPath,
).Replace(s)
}

View File

@@ -5,37 +5,39 @@ import (
"syscall"
"testing"
"hakurei.app/container/stub"
"hakurei.app/container/vfs"
)
func TestBindMount(t *testing.T) {
checkSimple(t, "bindMount", []simpleTestCase{
{"mount", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY, true)
}, [][]kexpect{{
{"verbosef", expectArgs{"resolved %q flags %#x", []any{"/sysroot/nix", uintptr(1)}}, nil, nil},
{"mount", expectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, errUnique},
}}, wrapErrSuffix(errUnique, `cannot mount "/host/nix" on "/sysroot/nix":`)},
t.Parallel()
{"success ne", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/.host-nix", syscall.MS_RDONLY, false)
}, [][]kexpect{{
{"verbosef", expectArgs{"resolved %q on %q flags %#x", []any{"/host/nix", "/sysroot/.host-nix", uintptr(1)}}, nil, nil},
{"mount", expectArgs{"/host/nix", "/sysroot/.host-nix", "", uintptr(0x9000), ""}, nil, nil},
{"remount", expectArgs{"/sysroot/.host-nix", uintptr(1)}, nil, nil},
checkSimple(t, "bindMount", []simpleTestCase{
{"mount", func(k *kstub) error {
return newProcPaths(k, hostPath).bindMount(nil, "/host/nix", "/sysroot/nix", syscall.MS_RDONLY)
}, stub.Expect{Calls: []stub.Call{
call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, stub.UniqueError(0xbad)),
}}, stub.UniqueError(0xbad)},
{"success ne", func(k *kstub) error {
return newProcPaths(k, hostPath).bindMount(k, "/host/nix", "/sysroot/.host-nix", syscall.MS_RDONLY)
}, stub.Expect{Calls: []stub.Call{
call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/.host-nix", "", uintptr(0x9000), ""}, nil, nil),
call("remount", stub.ExpectArgs{"/sysroot/.host-nix", uintptr(1)}, nil, nil),
}}, nil},
{"success", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY, true)
}, [][]kexpect{{
{"verbosef", expectArgs{"resolved %q flags %#x", []any{"/sysroot/nix", uintptr(1)}}, nil, nil},
{"mount", expectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, nil},
{"remount", expectArgs{"/sysroot/nix", uintptr(1)}, nil, nil},
{"success", func(k *kstub) error {
return newProcPaths(k, hostPath).bindMount(k, "/host/nix", "/sysroot/nix", syscall.MS_RDONLY)
}, stub.Expect{Calls: []stub.Call{
call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, nil),
call("remount", stub.ExpectArgs{"/sysroot/nix", uintptr(1)}, nil, nil),
}}, nil},
})
}
func TestRemount(t *testing.T) {
t.Parallel()
const sampleMountinfoNix = `254 407 253:0 / /host rw,relatime master:1 - ext4 /dev/disk/by-label/nixos rw
255 254 0:28 / /host/mnt/.ro-cwd ro,noatime master:2 - 9p cwd ro,access=client,msize=16384,trans=virtio
256 254 0:29 / /host/nix/.ro-store rw,relatime master:3 - 9p nix-store rw,cache=f,access=client,msize=16384,trans=virtio
@@ -67,8 +69,8 @@ func TestRemount(t *testing.T) {
403 397 0:63 / /host/run/user/1000 rw,nosuid,nodev,relatime master:295 - tmpfs tmpfs rw,size=401060k,nr_inodes=100265,mode=700,uid=1000,gid=100
404 254 0:46 / /host/mnt/cwd rw,relatime master:96 - overlay overlay rw,lowerdir=/mnt/.ro-cwd,upperdir=/tmp/.cwd/upper,workdir=/tmp/.cwd/work
405 254 0:47 / /host/mnt/src rw,relatime master:99 - overlay overlay rw,lowerdir=/nix/store/ihcrl3zwvp2002xyylri2wz0drwajx4z-ns0pa7q2b1jpx9pbf1l9352x6rniwxjn-source,upperdir=/tmp/.src/upper,workdir=/tmp/.src/work
407 253 0:65 / / rw,nosuid,nodev,relatime - tmpfs rootfs rw,uid=1000000,gid=1000000
408 407 0:65 /sysroot /sysroot rw,nosuid,nodev,relatime - tmpfs rootfs rw,uid=1000000,gid=1000000
407 253 0:65 / / rw,nosuid,nodev,relatime - tmpfs rootfs rw,uid=10000,gid=10000
408 407 0:65 /sysroot /sysroot rw,nosuid,nodev,relatime - tmpfs rootfs rw,uid=10000,gid=10000
409 408 253:0 /bin /sysroot/bin rw,nosuid,nodev,relatime master:1 - ext4 /dev/disk/by-label/nixos rw
410 408 253:0 /home /sysroot/home rw,nosuid,nodev,relatime master:1 - ext4 /dev/disk/by-label/nixos rw
411 408 253:0 /lib64 /sysroot/lib64 rw,nosuid,nodev,relatime master:1 - ext4 /dev/disk/by-label/nixos rw
@@ -79,189 +81,195 @@ func TestRemount(t *testing.T) {
416 415 0:30 / /sysroot/nix/store ro,relatime master:5 - overlay overlay rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work`
checkSimple(t, "remount", []simpleTestCase{
{"evalSymlinks", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", errUnique},
}}, wrapErrSelf(errUnique)},
{"evalSymlinks", func(k *kstub) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", stub.UniqueError(6)),
}}, stub.UniqueError(6)},
{"open", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, errUnique},
}}, wrapErrSuffix(errUnique, `cannot open "/sysroot/nix":`)},
{"open", func(k *kstub) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdead, stub.UniqueError(5)),
}}, &os.PathError{Op: "open", Path: "/sysroot/nix", Err: stub.UniqueError(5)}},
{"readlink", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", errUnique},
}}, wrapErrSelf(errUnique)},
{"readlink", func(k *kstub) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdead, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", stub.UniqueError(4)),
}}, stub.UniqueError(4)},
{"close", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
{"close", expectArgs{0xdeadbeef}, nil, errUnique},
}}, wrapErrSuffix(errUnique, `cannot close "/sysroot/nix":`)},
{"close", func(k *kstub) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdead, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdead}, nil, stub.UniqueError(3)),
}}, &os.PathError{Op: "close", Path: "/sysroot/nix", Err: stub.UniqueError(3)}},
{"mountinfo stale", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/.hakurei", nil},
{"verbosef", expectArgs{"target resolves to %q", []any{"/sysroot/.hakurei"}}, nil, nil},
{"open", expectArgs{"/sysroot/.hakurei", 0x280000, uint32(0)}, 0xdeadbeef, nil},
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/.hakurei", nil},
{"close", expectArgs{0xdeadbeef}, nil, nil},
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
}}, msg.WrapErr(syscall.ESTALE, `mount point "/sysroot/.hakurei" never appeared in mountinfo`)},
{"mountinfo no match", func(k *kstub) error {
return newProcPaths(k, hostPath).remount(k, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/.hakurei", nil),
call("verbosef", stub.ExpectArgs{"target resolves to %q", []any{"/sysroot/.hakurei"}}, nil, nil),
call("open", stub.ExpectArgs{"/sysroot/.hakurei", 0x280000, uint32(0)}, 0xdead, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/.hakurei", nil),
call("close", stub.ExpectArgs{0xdead}, nil, nil),
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
}}, &vfs.DecoderError{Op: "unfold", Line: -1, Err: vfs.UnfoldTargetError("/sysroot/.hakurei")}},
{"mountinfo", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
{"close", expectArgs{0xdeadbeef}, nil, nil},
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile("\x00"), nil},
}}, wrapErrSuffix(vfs.ErrMountInfoFields, `cannot parse mountinfo:`)},
{"mountinfo", func(k *kstub) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdead, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdead}, nil, nil),
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile("\x00"), nil),
}}, &vfs.DecoderError{Op: "parse", Line: 0, Err: vfs.ErrMountInfoFields}},
{"mount", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
{"close", expectArgs{0xdeadbeef}, nil, nil},
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, errUnique},
}}, wrapErrSuffix(errUnique, `cannot remount "/sysroot/nix":`)},
{"mount", func(k *kstub) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdead, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdead}, nil, nil),
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, stub.UniqueError(2)),
}}, stub.UniqueError(2)},
{"mount propagate", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
{"close", expectArgs{0xdeadbeef}, nil, nil},
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil},
{"mount", expectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, errUnique},
}}, wrapErrSuffix(errUnique, `cannot propagate flags to "/sysroot/nix/.ro-store":`)},
{"mount propagate", func(k *kstub) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdead, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdead}, nil, nil),
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, stub.UniqueError(1)),
}}, stub.UniqueError(1)},
{"success toplevel", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/bin", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/bin"}, "/sysroot/bin", nil},
{"open", expectArgs{"/sysroot/bin", 0x280000, uint32(0)}, 0xbabe, nil},
{"readlink", expectArgs{"/host/proc/self/fd/47806"}, "/sysroot/bin", nil},
{"close", expectArgs{0xbabe}, nil, nil},
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
{"mount", expectArgs{"none", "/sysroot/bin", "", uintptr(0x209027), ""}, nil, nil},
{"success toplevel", func(k *kstub) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/bin", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/bin"}, "/sysroot/bin", nil),
call("open", stub.ExpectArgs{"/sysroot/bin", 0x280000, uint32(0)}, 0xbabe, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/47806"}, "/sysroot/bin", nil),
call("close", stub.ExpectArgs{0xbabe}, nil, nil),
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/bin", "", uintptr(0x209027), ""}, nil, nil),
}}, nil},
{"success EACCES", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
{"close", expectArgs{0xdeadbeef}, nil, nil},
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil},
{"mount", expectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, syscall.EACCES},
{"mount", expectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil},
{"success EACCES", func(k *kstub) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdead, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdead}, nil, nil),
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, syscall.EACCES),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil),
}}, nil},
{"success no propagate", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
{"close", expectArgs{0xdeadbeef}, nil, nil},
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil},
{"success no propagate", func(k *kstub) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdead, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdead}, nil, nil),
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil),
}}, nil},
{"success case sensitive", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
{"close", expectArgs{0xdeadbeef}, nil, nil},
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil},
{"mount", expectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, nil},
{"mount", expectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil},
{"success case sensitive", func(k *kstub) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdead, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdead}, nil, nil),
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil),
}}, nil},
{"success", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/.nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/.nix"}, "/sysroot/NIX", nil},
{"verbosef", expectArgs{"target resolves to %q", []any{"/sysroot/NIX"}}, nil, nil},
{"open", expectArgs{"/sysroot/NIX", 0x280000, uint32(0)}, 0xdeadbeef, nil},
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
{"close", expectArgs{0xdeadbeef}, nil, nil},
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil},
{"mount", expectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, nil},
{"mount", expectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil},
{"success", func(k *kstub) error {
return newProcPaths(k, hostPath).remount(k, "/sysroot/.nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/.nix"}, "/sysroot/NIX", nil),
call("verbosef", stub.ExpectArgs{"target resolves to %q", []any{"/sysroot/NIX"}}, nil, nil),
call("open", stub.ExpectArgs{"/sysroot/NIX", 0x280000, uint32(0)}, 0xdead, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdead}, nil, nil),
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil),
}}, nil},
})
}
func TestRemountWithFlags(t *testing.T) {
t.Parallel()
checkSimple(t, "remountWithFlags", []simpleTestCase{
{"noop unmatched", func(k syscallDispatcher) error {
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime,cat"}}, 0)
}, [][]kexpect{{
{"verbosef", expectArgs{"unmatched vfs options: %q", []any{[]string{"cat"}}}, nil, nil},
{"noop unmatched", func(k *kstub) error {
return remountWithFlags(k, k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime,cat"}}, 0)
}, stub.Expect{Calls: []stub.Call{
call("verbosef", stub.ExpectArgs{"unmatched vfs options: %q", []any{[]string{"cat"}}}, nil, nil),
}}, nil},
{"noop", func(k syscallDispatcher) error {
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, 0)
}, nil, nil},
{"noop", func(k *kstub) error {
return remountWithFlags(k, nil, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, 0)
}, stub.Expect{}, nil},
{"success", func(k syscallDispatcher) error {
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, syscall.MS_RDONLY)
}, [][]kexpect{{
{"mount", expectArgs{"none", "", "", uintptr(0x209021), ""}, nil, nil},
{"success", func(k *kstub) error {
return remountWithFlags(k, nil, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, syscall.MS_RDONLY)
}, stub.Expect{Calls: []stub.Call{
call("mount", stub.ExpectArgs{"none", "", "", uintptr(0x209021), ""}, nil, nil),
}}, nil},
})
}
func TestMountTmpfs(t *testing.T) {
checkSimple(t, "mountTmpfs", []simpleTestCase{
{"mkdirAll", func(k syscallDispatcher) error {
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700)
}, [][]kexpect{{
{"mkdirAll", expectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, errUnique},
}}, wrapErrSelf(errUnique)},
t.Parallel()
{"success no size", func(k syscallDispatcher) error {
checkSimple(t, "mountTmpfs", []simpleTestCase{
{"mkdirAll", func(k *kstub) error {
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700)
}, stub.Expect{Calls: []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, stub.UniqueError(0)),
}}, stub.UniqueError(0)},
{"success no size", func(k *kstub) error {
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 0, 0710)
}, [][]kexpect{{
{"mkdirAll", expectArgs{"/sysroot/run/user/1000", os.FileMode(0750)}, nil, nil},
{"mount", expectArgs{"ephemeral", "/sysroot/run/user/1000", "tmpfs", uintptr(0), "mode=0710"}, nil, nil},
}, stub.Expect{Calls: []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0750)}, nil, nil),
call("mount", stub.ExpectArgs{"ephemeral", "/sysroot/run/user/1000", "tmpfs", uintptr(0), "mode=0710"}, nil, nil),
}}, nil},
{"success", func(k syscallDispatcher) error {
{"success", func(k *kstub) error {
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700)
}, [][]kexpect{{
{"mkdirAll", expectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, nil},
{"mount", expectArgs{"ephemeral", "/sysroot/run/user/1000", "tmpfs", uintptr(0), "mode=0700,size=1024"}, nil, nil},
}, stub.Expect{Calls: []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, nil),
call("mount", stub.ExpectArgs{"ephemeral", "/sysroot/run/user/1000", "tmpfs", uintptr(0), "mode=0700,size=1024"}, nil, nil),
}}, nil},
})
}
func TestParentPerm(t *testing.T) {
t.Parallel()
testCases := []struct {
perm os.FileMode
want os.FileMode
@@ -277,29 +285,10 @@ func TestParentPerm(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.perm.String(), func(t *testing.T) {
t.Parallel()
if got := parentPerm(tc.perm); got != tc.want {
t.Errorf("parentPerm: %#o, want %#o", got, tc.want)
}
})
}
}
func TestEscapeOverlayDataSegment(t *testing.T) {
testCases := []struct {
name string
s string
want string
}{
{"zero", zeroString, zeroString},
{"multi", `\\\:,:,\\\`, `\\\\\\\:\,\:\,\\\\\\`},
{"bwrap", `/path :,\`, `/path \:\,\\`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := EscapeOverlayDataSegment(tc.s); got != tc.want {
t.Errorf("escapeOverlayDataSegment: %s, want %s", got, tc.want)
}
})
}
}

View File

@@ -1,68 +0,0 @@
package container
import (
"errors"
"fmt"
"log"
"os"
"reflect"
"sync/atomic"
"testing"
)
type Msg interface {
IsVerbose() bool
Verbose(v ...any)
Verbosef(format string, v ...any)
WrapErr(err error, a ...any) error
PrintBaseErr(err error, fallback string)
Suspend()
Resume() bool
BeforeExit()
}
type DefaultMsg struct{ inactive atomic.Bool }
func (msg *DefaultMsg) IsVerbose() bool { return true }
func (msg *DefaultMsg) Verbose(v ...any) {
if !msg.inactive.Load() {
log.Println(v...)
}
}
func (msg *DefaultMsg) Verbosef(format string, v ...any) {
if !msg.inactive.Load() {
log.Printf(format, v...)
}
}
// checkedWrappedErr implements error with strict checks for wrapped values.
type checkedWrappedErr struct {
err error
a []any
}
func (c *checkedWrappedErr) Error() string { return fmt.Sprintf("%v, a = %s", c.err, c.a) }
func (c *checkedWrappedErr) Is(err error) bool {
var concreteErr *checkedWrappedErr
if !errors.As(err, &concreteErr) {
return false
}
return reflect.DeepEqual(c, concreteErr)
}
func (msg *DefaultMsg) WrapErr(err error, a ...any) error {
// provide a mostly bulletproof path to bypass this behaviour in tests
if testing.Testing() && os.Getenv("GOPATH") != Nonexistent {
return &checkedWrappedErr{err, a}
}
log.Println(a...)
return err
}
func (msg *DefaultMsg) PrintBaseErr(err error, fallback string) { log.Println(fallback, err) }
func (msg *DefaultMsg) Suspend() { msg.inactive.Store(true) }
func (msg *DefaultMsg) Resume() bool { return msg.inactive.CompareAndSwap(true, false) }
func (msg *DefaultMsg) BeforeExit() {}

View File

@@ -1,162 +0,0 @@
package container_test
import (
"errors"
"log"
"strings"
"sync/atomic"
"syscall"
"testing"
"hakurei.app/container"
"hakurei.app/internal/hlog"
)
func TestDefaultMsg(t *testing.T) {
// bypass WrapErr testing behaviour
t.Setenv("GOPATH", container.Nonexistent)
{
w := log.Writer()
f := log.Flags()
t.Cleanup(func() { log.SetOutput(w); log.SetFlags(f) })
}
msg := new(container.DefaultMsg)
t.Run("is verbose", func(t *testing.T) {
if !msg.IsVerbose() {
t.Error("IsVerbose unexpected outcome")
}
})
t.Run("verbose", func(t *testing.T) {
log.SetOutput(panicWriter{})
msg.Suspend()
msg.Verbose()
msg.Verbosef("\x00")
msg.Resume()
buf := new(strings.Builder)
log.SetOutput(buf)
log.SetFlags(0)
msg.Verbose()
msg.Verbosef("\x00")
want := "\n\x00\n"
if buf.String() != want {
t.Errorf("Verbose: %q, want %q", buf.String(), want)
}
})
t.Run("wrapErr", func(t *testing.T) {
buf := new(strings.Builder)
log.SetOutput(buf)
log.SetFlags(0)
if err := msg.WrapErr(syscall.EBADE, "\x00", "\x00"); err != syscall.EBADE {
t.Errorf("WrapErr: %v", err)
}
msg.PrintBaseErr(syscall.ENOTRECOVERABLE, "cannot cuddle cat:")
want := "\x00 \x00\ncannot cuddle cat: state not recoverable\n"
if buf.String() != want {
t.Errorf("WrapErr: %q, want %q", buf.String(), want)
}
})
t.Run("inactive", func(t *testing.T) {
{
inactive := msg.Resume()
if inactive {
t.Cleanup(func() { msg.Suspend() })
}
}
if msg.Resume() {
t.Error("Resume unexpected outcome")
}
msg.Suspend()
if !msg.Resume() {
t.Error("Resume unexpected outcome")
}
})
// the function is a noop
t.Run("beforeExit", func(t *testing.T) { msg.BeforeExit() })
t.Run("checkedWrappedErr", func(t *testing.T) {
// temporarily re-enable testing behaviour
t.Setenv("GOPATH", "")
wrappedErr := msg.WrapErr(syscall.ENOTRECOVERABLE, "cannot cuddle cat:", syscall.ENOTRECOVERABLE)
t.Run("string", func(t *testing.T) {
want := "state not recoverable, a = [cannot cuddle cat: state not recoverable]"
if got := wrappedErr.Error(); got != want {
t.Errorf("Error: %q, want %q", got, want)
}
})
t.Run("bad concrete type", func(t *testing.T) {
if errors.Is(wrappedErr, syscall.ENOTRECOVERABLE) {
t.Error("incorrect type assertion")
}
})
})
}
type panicWriter struct{}
func (panicWriter) Write([]byte) (int, error) { panic("unreachable") }
func saveRestoreOutput(t *testing.T) {
out := container.GetOutput()
t.Cleanup(func() { container.SetOutput(out) })
}
func replaceOutput(t *testing.T) {
saveRestoreOutput(t)
container.SetOutput(&testOutput{t: t})
}
type testOutput struct {
t *testing.T
suspended atomic.Bool
}
func (out *testOutput) IsVerbose() bool { return testing.Verbose() }
func (out *testOutput) Verbose(v ...any) {
if !out.IsVerbose() {
return
}
out.t.Log(v...)
}
func (out *testOutput) Verbosef(format string, v ...any) {
if !out.IsVerbose() {
return
}
out.t.Logf(format, v...)
}
func (out *testOutput) WrapErr(err error, a ...any) error { return hlog.WrapErr(err, a...) }
func (out *testOutput) PrintBaseErr(err error, fallback string) { hlog.PrintBaseError(err, fallback) }
func (out *testOutput) Suspend() {
if out.suspended.CompareAndSwap(false, true) {
out.Verbose("suspend called")
return
}
out.Verbose("suspend called on suspended output")
}
func (out *testOutput) Resume() bool {
if out.suspended.CompareAndSwap(true, false) {
out.Verbose("resume called")
return true
}
out.Verbose("resume called on unsuspended output")
return false
}
func (out *testOutput) BeforeExit() { out.Verbose("beforeExit called") }

View File

@@ -1,26 +0,0 @@
package container
var msg Msg = new(DefaultMsg)
func GetOutput() Msg { return msg }
func SetOutput(v Msg) {
if v == nil {
msg = new(DefaultMsg)
} else {
msg = v
}
}
func wrapErrSuffix(err error, a ...any) error {
if err == nil {
return nil
}
return msg.WrapErr(err, append(a, err)...)
}
func wrapErrSelf(err error) error {
if err == nil {
return nil
}
return msg.WrapErr(err, err.Error())
}

View File

@@ -1,110 +0,0 @@
package container
import (
"reflect"
"syscall"
"testing"
)
func TestGetSetOutput(t *testing.T) {
{
out := GetOutput()
t.Cleanup(func() { SetOutput(out) })
}
t.Run("default", func(t *testing.T) {
SetOutput(new(stubOutput))
if v, ok := GetOutput().(*DefaultMsg); ok {
t.Fatalf("SetOutput: got unexpected output %#v", v)
}
SetOutput(nil)
if _, ok := GetOutput().(*DefaultMsg); !ok {
t.Fatalf("SetOutput: got unexpected output %#v", GetOutput())
}
})
t.Run("stub", func(t *testing.T) {
SetOutput(new(stubOutput))
if _, ok := GetOutput().(*stubOutput); !ok {
t.Fatalf("SetOutput: got unexpected output %#v", GetOutput())
}
})
}
func TestWrapErr(t *testing.T) {
{
out := GetOutput()
t.Cleanup(func() { SetOutput(out) })
}
var wrapFp *func(error, ...any) error
s := new(stubOutput)
SetOutput(s)
wrapFp = &s.wrapF
testCases := []struct {
name string
f func(t *testing.T)
wantErr error
wantA []any
}{
{"suffix nil", func(t *testing.T) {
if err := wrapErrSuffix(nil, "\x00"); err != nil {
t.Errorf("wrapErrSuffix: %v", err)
}
}, nil, nil},
{"suffix val", func(t *testing.T) {
if err := wrapErrSuffix(syscall.ENOTRECOVERABLE, "\x00\x00"); err != syscall.ENOTRECOVERABLE {
t.Errorf("wrapErrSuffix: %v", err)
}
}, syscall.ENOTRECOVERABLE, []any{"\x00\x00", syscall.ENOTRECOVERABLE}},
{"self nil", func(t *testing.T) {
if err := wrapErrSelf(nil); err != nil {
t.Errorf("wrapErrSelf: %v", err)
}
}, nil, nil},
{"self val", func(t *testing.T) {
if err := wrapErrSelf(syscall.ENOTRECOVERABLE); err != syscall.ENOTRECOVERABLE {
t.Errorf("wrapErrSelf: %v", err)
}
}, syscall.ENOTRECOVERABLE, []any{"state not recoverable"}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var (
gotErr error
gotA []any
)
*wrapFp = func(err error, a ...any) error { gotErr = err; gotA = a; return err }
tc.f(t)
if gotErr != tc.wantErr {
t.Errorf("WrapErr: err = %v, want %v", gotErr, tc.wantErr)
}
if !reflect.DeepEqual(gotA, tc.wantA) {
t.Errorf("WrapErr: a = %v, want %v", gotA, tc.wantA)
}
})
}
}
type stubOutput struct {
wrapF func(error, ...any) error
}
func (*stubOutput) IsVerbose() bool { panic("unreachable") }
func (*stubOutput) Verbose(...any) { panic("unreachable") }
func (*stubOutput) Verbosef(string, ...any) { panic("unreachable") }
func (*stubOutput) PrintBaseErr(error, string) { panic("unreachable") }
func (*stubOutput) Suspend() { panic("unreachable") }
func (*stubOutput) Resume() bool { panic("unreachable") }
func (*stubOutput) BeforeExit() { panic("unreachable") }
func (s *stubOutput) WrapErr(err error, v ...any) error {
if s.wrapF == nil {
panic("unreachable")
}
return s.wrapF(err, v...)
}

View File

@@ -8,35 +8,34 @@ import (
"syscall"
)
var (
ErrNotSet = errors.New("environment variable not set")
ErrFdFormat = errors.New("bad file descriptor representation")
)
// Setup appends the read end of a pipe for setup params transmission and returns its fd.
func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) {
func Setup(extraFiles *[]*os.File) (int, *os.File, error) {
if r, w, err := os.Pipe(); err != nil {
return -1, nil, err
} else {
fd := 3 + len(*extraFiles)
*extraFiles = append(*extraFiles, r)
return fd, gob.NewEncoder(w), nil
return fd, w, nil
}
}
var (
ErrReceiveEnv = errors.New("environment variable not set")
)
// Receive retrieves setup fd from the environment and receives params.
func Receive(key string, e any, fdp *uintptr) (func() error, error) {
var setup *os.File
if s, ok := os.LookupEnv(key); !ok {
return nil, ErrNotSet
return nil, ErrReceiveEnv
} else {
if fd, err := strconv.Atoi(s); err != nil {
return nil, ErrFdFormat
return nil, optionalErrorUnwrap(err)
} else {
setup = os.NewFile(uintptr(fd), "setup")
if setup == nil {
return nil, syscall.EBADF
return nil, syscall.EDOM
}
if fdp != nil {
*fdp = setup.Fd()

View File

@@ -1,6 +1,7 @@
package container_test
import (
"encoding/gob"
"errors"
"os"
"slices"
@@ -29,8 +30,8 @@ func TestSetupReceive(t *testing.T) {
})
}
if _, err := container.Receive(key, nil, nil); !errors.Is(err, container.ErrNotSet) {
t.Errorf("Receive: error = %v, want %v", err, container.ErrNotSet)
if _, err := container.Receive(key, nil, nil); !errors.Is(err, container.ErrReceiveEnv) {
t.Errorf("Receive: error = %v, want %v", err, container.ErrReceiveEnv)
}
})
@@ -38,8 +39,8 @@ func TestSetupReceive(t *testing.T) {
const key = "TEST_ENV_FORMAT"
t.Setenv(key, "")
if _, err := container.Receive(key, nil, nil); !errors.Is(err, container.ErrFdFormat) {
t.Errorf("Receive: error = %v, want %v", err, container.ErrFdFormat)
if _, err := container.Receive(key, nil, nil); !errors.Is(err, strconv.ErrSyntax) {
t.Errorf("Receive: error = %v, want %v", err, strconv.ErrSyntax)
}
})
@@ -47,24 +48,28 @@ func TestSetupReceive(t *testing.T) {
const key = "TEST_ENV_RANGE"
t.Setenv(key, "-1")
if _, err := container.Receive(key, nil, nil); !errors.Is(err, syscall.EBADF) {
t.Errorf("Receive: error = %v, want %v", err, syscall.EBADF)
if _, err := container.Receive(key, nil, nil); !errors.Is(err, syscall.EDOM) {
t.Errorf("Receive: error = %v, want %v", err, syscall.EDOM)
}
})
t.Run("setup receive", func(t *testing.T) {
check := func(t *testing.T, useNilFdp bool) {
const key = "TEST_SETUP_RECEIVE"
payload := []int{syscall.MS_MGC_VAL, syscall.MS_MGC_MSK, syscall.MS_ASYNC, syscall.MS_ACTIVE}
payload := []uint64{syscall.MS_MGC_VAL, syscall.MS_MGC_MSK, syscall.MS_ASYNC, syscall.MS_ACTIVE}
encoderDone := make(chan error, 1)
extraFiles := make([]*os.File, 0, 1)
if fd, encoder, err := container.Setup(&extraFiles); err != nil {
deadline, _ := t.Deadline()
if fd, f, err := container.Setup(&extraFiles); err != nil {
t.Fatalf("Setup: error = %v", err)
} else if fd != 3 {
t.Fatalf("Setup: fd = %d, want 3", fd)
} else {
go func() { encoderDone <- encoder.Encode(payload) }()
if err = f.SetDeadline(deadline); err != nil {
t.Fatal(err.Error())
}
go func() { encoderDone <- gob.NewEncoder(f).Encode(payload) }()
}
if len(extraFiles) != 1 {
@@ -81,7 +86,7 @@ func TestSetupReceive(t *testing.T) {
}
var (
gotPayload []int
gotPayload []uint64
fdp *uintptr
)
if !useNilFdp {

View File

@@ -2,7 +2,6 @@ package container
import (
"errors"
"fmt"
"io/fs"
"os"
"path"
@@ -10,84 +9,18 @@ import (
"strings"
"syscall"
"hakurei.app/container/fhs"
"hakurei.app/container/vfs"
)
/* constants in this file bypass abs check, be extremely careful when changing them! */
const (
// FHSRoot points to the file system root.
FHSRoot = "/"
// FHSEtc points to the directory for system-specific configuration.
FHSEtc = "/etc/"
// FHSTmp points to the place for small temporary files.
FHSTmp = "/tmp/"
// FHSRun points to a "tmpfs" file system for system packages to place runtime data, socket files, and similar.
FHSRun = "/run/"
// FHSRunUser points to a directory containing per-user runtime directories,
// each usually individually mounted "tmpfs" instances.
FHSRunUser = FHSRun + "user/"
// FHSUsr points to vendor-supplied operating system resources.
FHSUsr = "/usr/"
// FHSUsrBin points to binaries and executables for user commands that shall appear in the $PATH search path.
FHSUsrBin = FHSUsr + "bin/"
// FHSVar points to persistent, variable system data. Writable during normal system operation.
FHSVar = "/var/"
// FHSVarLib points to persistent system data.
FHSVarLib = FHSVar + "lib/"
// FHSVarEmpty points to a nonstandard directory that is usually empty.
FHSVarEmpty = FHSVar + "empty/"
// FHSDev points to the root directory for device nodes.
FHSDev = "/dev/"
// FHSProc points to a virtual kernel file system exposing the process list and other functionality.
FHSProc = "/proc/"
// FHSProcSys points to a hierarchy below /proc/ that exposes a number of kernel tunables.
FHSProcSys = FHSProc + "sys/"
// FHSSys points to a virtual kernel file system exposing discovered devices and other functionality.
FHSSys = "/sys/"
)
var (
// AbsFHSRoot is [FHSRoot] as [Absolute].
AbsFHSRoot = &Absolute{FHSRoot}
// AbsFHSEtc is [FHSEtc] as [Absolute].
AbsFHSEtc = &Absolute{FHSEtc}
// AbsFHSTmp is [FHSTmp] as [Absolute].
AbsFHSTmp = &Absolute{FHSTmp}
// AbsFHSRun is [FHSRun] as [Absolute].
AbsFHSRun = &Absolute{FHSRun}
// AbsFHSRunUser is [FHSRunUser] as [Absolute].
AbsFHSRunUser = &Absolute{FHSRunUser}
// AbsFHSUsrBin is [FHSUsrBin] as [Absolute].
AbsFHSUsrBin = &Absolute{FHSUsrBin}
// AbsFHSVar is [FHSVar] as [Absolute].
AbsFHSVar = &Absolute{FHSVar}
// AbsFHSVarLib is [FHSVarLib] as [Absolute].
AbsFHSVarLib = &Absolute{FHSVarLib}
// AbsFHSDev is [FHSDev] as [Absolute].
AbsFHSDev = &Absolute{FHSDev}
// AbsFHSProc is [FHSProc] as [Absolute].
AbsFHSProc = &Absolute{FHSProc}
// AbsFHSSys is [FHSSys] as [Absolute].
AbsFHSSys = &Absolute{FHSSys}
)
const (
// Nonexistent is a path that cannot exist.
// /proc is chosen because a system with covered /proc is unsupported by this package.
Nonexistent = FHSProc + "nonexistent"
Nonexistent = fhs.Proc + "nonexistent"
hostPath = FHSRoot + hostDir
hostPath = fhs.Root + hostDir
hostDir = "host"
sysrootPath = FHSRoot + sysrootDir
sysrootPath = fhs.Root + sysrootDir
sysrootDir = "sysroot"
)
@@ -103,30 +36,29 @@ func toHost(name string) string {
func createFile(name string, perm, pperm os.FileMode, content []byte) error {
if err := os.MkdirAll(path.Dir(name), pperm); err != nil {
return wrapErrSelf(err)
return err
}
f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm)
if err != nil {
return wrapErrSelf(err)
return err
}
if content != nil {
_, err = f.Write(content)
}
return errors.Join(f.Close(), wrapErrSelf(err))
return errors.Join(f.Close(), err)
}
func ensureFile(name string, perm, pperm os.FileMode) error {
fi, err := os.Stat(name)
if err != nil {
if !os.IsNotExist(err) {
return wrapErrSelf(err)
return err
}
return createFile(name, perm, pperm, nil)
}
if mode := fi.Mode(); mode&fs.ModeDir != 0 || mode&fs.ModeSymlink != 0 {
err = msg.WrapErr(syscall.EISDIR,
fmt.Sprintf("path %q is a directory", name))
err = &os.PathError{Op: "ensure", Path: name, Err: syscall.EISDIR}
}
return err
}
@@ -147,15 +79,14 @@ func (p *procPaths) stdout() string { return p.self + "/fd/1" }
func (p *procPaths) fd(fd int) string { return p.self + "/fd/" + strconv.Itoa(fd) }
func (p *procPaths) mountinfo(f func(d *vfs.MountInfoDecoder) error) error {
if r, err := p.k.openNew(p.self + "/mountinfo"); err != nil {
return wrapErrSelf(err)
return err
} else {
d := vfs.NewMountInfoDecoder(r)
err0 := f(d)
if err = r.Close(); err != nil {
return wrapErrSelf(err)
return err
} else if err = d.Err(); err != nil {
return wrapErrSuffix(err,
"cannot parse mountinfo:")
return err
}
return err0
}

View File

@@ -1,8 +1,6 @@
package container
import (
"errors"
"fmt"
"io"
"math"
"os"
@@ -12,6 +10,7 @@ import (
"testing"
"unsafe"
"hakurei.app/container/check"
"hakurei.app/container/vfs"
)
@@ -51,25 +50,32 @@ func TestToHost(t *testing.T) {
}
}
// InternalToHostOvlEscape exports toHost passed to EscapeOverlayDataSegment.
func InternalToHostOvlEscape(s string) string { return EscapeOverlayDataSegment(toHost(s)) }
// InternalToHostOvlEscape exports toHost passed to [check.EscapeOverlayDataSegment].
func InternalToHostOvlEscape(s string) string { return check.EscapeOverlayDataSegment(toHost(s)) }
func TestCreateFile(t *testing.T) {
t.Run("nonexistent", func(t *testing.T) {
if err := createFile(path.Join(Nonexistent, ":3"), 0644, 0755, nil); !errors.Is(err, wrapErrSelf(&os.PathError{
Op: "mkdir",
Path: "/proc/nonexistent",
Err: syscall.ENOENT,
})) {
t.Errorf("createFile: error = %v", err)
}
if err := createFile(path.Join(Nonexistent), 0644, 0755, nil); !errors.Is(err, wrapErrSelf(&os.PathError{
Op: "open",
Path: "/proc/nonexistent",
Err: syscall.ENOENT,
})) {
t.Errorf("createFile: error = %v", err)
}
t.Run("mkdir", func(t *testing.T) {
wantErr := &os.PathError{
Op: "mkdir",
Path: "/proc/nonexistent",
Err: syscall.ENOENT,
}
if err := createFile(path.Join(Nonexistent, ":3"), 0644, 0755, nil); !reflect.DeepEqual(err, wantErr) {
t.Errorf("createFile: error = %#v, want %#v", err, wantErr)
}
})
t.Run("open", func(t *testing.T) {
wantErr := &os.PathError{
Op: "open",
Path: "/proc/nonexistent",
Err: syscall.ENOENT,
}
if err := createFile(path.Join(Nonexistent), 0644, 0755, nil); !reflect.DeepEqual(err, wantErr) {
t.Errorf("createFile: error = %#v, want %#v", err, wantErr)
}
})
})
t.Run("touch", func(t *testing.T) {
@@ -120,13 +126,13 @@ func TestEnsureFile(t *testing.T) {
t.Fatalf("Chmod: error = %v", err)
}
wantErr := wrapErrSelf(&os.PathError{
wantErr := &os.PathError{
Op: "stat",
Path: pathname,
Err: syscall.EACCES,
})
if err := ensureFile(pathname, 0644, 0755); !errors.Is(err, wantErr) {
t.Errorf("ensureFile: error = %v, want %v", err, wantErr)
}
if err := ensureFile(pathname, 0644, 0755); !reflect.DeepEqual(err, wantErr) {
t.Errorf("ensureFile: error = %#v, want %#v", err, wantErr)
}
if err := os.Chmod(tempDir, 0755); err != nil {
@@ -136,9 +142,9 @@ func TestEnsureFile(t *testing.T) {
t.Run("directory", func(t *testing.T) {
pathname := t.TempDir()
wantErr := msg.WrapErr(syscall.EISDIR, fmt.Sprintf("path %q is a directory", pathname))
if err := ensureFile(pathname, 0644, 0755); !errors.Is(err, wantErr) {
t.Errorf("ensureFile: error = %v, want %v", err, wantErr)
wantErr := &os.PathError{Op: "ensure", Path: pathname, Err: syscall.EISDIR}
if err := ensureFile(pathname, 0644, 0755); !reflect.DeepEqual(err, wantErr) {
t.Errorf("ensureFile: error = %#v, want %#v", err, wantErr)
}
})
@@ -167,8 +173,8 @@ func TestProcPaths(t *testing.T) {
}
})
t.Run("fd", func(t *testing.T) {
want := "/host/proc/self/fd/9223372036854775807"
if got := hostProc.fd(math.MaxInt64); got != want {
want := "/host/proc/self/fd/2147483647"
if got := hostProc.fd(math.MaxInt32); got != want {
t.Errorf("stdout: %q, want %q", got, want)
}
})
@@ -177,12 +183,12 @@ func TestProcPaths(t *testing.T) {
t.Run("mountinfo", func(t *testing.T) {
t.Run("nonexistent", func(t *testing.T) {
nonexistentProc := newProcPaths(direct{}, t.TempDir())
wantErr := wrapErrSelf(&os.PathError{
wantErr := &os.PathError{
Op: "open",
Path: nonexistentProc.self + "/mountinfo",
Err: syscall.ENOENT,
})
if err := nonexistentProc.mountinfo(func(*vfs.MountInfoDecoder) error { return syscall.EINVAL }); !errors.Is(err, wantErr) {
}
if err := nonexistentProc.mountinfo(func(*vfs.MountInfoDecoder) error { return syscall.EINVAL }); !reflect.DeepEqual(err, wantErr) {
t.Errorf("mountinfo: error = %v, want %v", err, wantErr)
}
})
@@ -217,11 +223,11 @@ func TestProcPaths(t *testing.T) {
t.Run("closed", func(t *testing.T) {
p := newProcPaths(direct{}, tempDir)
wantErr := wrapErrSelf(&os.PathError{
wantErr := &os.PathError{
Op: "close",
Path: p.self + "/mountinfo",
Err: os.ErrClosed,
})
}
if err := p.mountinfo(func(d *vfs.MountInfoDecoder) error {
v := reflect.ValueOf(d).Elem().FieldByName("s").Elem().FieldByName("r")
v = reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr()))
@@ -231,8 +237,8 @@ func TestProcPaths(t *testing.T) {
} else {
return f.Close()
}
}); !errors.Is(err, wantErr) {
t.Errorf("mountinfo: error = %v, want %v", err, wantErr)
}); !reflect.DeepEqual(err, wantErr) {
t.Errorf("mountinfo: error = %#v, want %#v", err, wantErr)
}
})
@@ -242,8 +248,8 @@ func TestProcPaths(t *testing.T) {
t.Fatalf("WriteFile: error = %v", err)
}
wantErr := wrapErrSuffix(vfs.ErrMountInfoFields, "cannot parse mountinfo:")
if err := newProcPaths(direct{}, tempDir).mountinfo(func(d *vfs.MountInfoDecoder) error { return d.Decode(new(*vfs.MountInfo)) }); !errors.Is(err, wantErr) {
wantErr := &vfs.DecoderError{Op: "parse", Line: 0, Err: vfs.ErrMountInfoFields}
if err := newProcPaths(direct{}, tempDir).mountinfo(func(d *vfs.MountInfoDecoder) error { return d.Decode(new(*vfs.MountInfo)) }); !reflect.DeepEqual(err, wantErr) {
t.Fatalf("mountinfo: error = %v, want %v", err, wantErr)
}
})

View File

@@ -9,122 +9,130 @@
#define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch,
uint32_t multiarch,
struct hakurei_syscall_rule *rules,
size_t rules_sz, hakurei_export_flag flags) {
int i;
int last_allowed_family;
int disallowed;
struct hakurei_syscall_rule *rule;
int32_t hakurei_scmp_make_filter(
int *ret_p, uintptr_t allocate_p,
uint32_t arch, uint32_t multiarch,
struct hakurei_syscall_rule *rules,
size_t rules_sz, hakurei_export_flag flags) {
int i;
int last_allowed_family;
int disallowed;
struct hakurei_syscall_rule *rule;
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 */
struct {
int family;
hakurei_export_flag flags_mask;
} socket_family_allowlist[] = {
/* NOTE: Keep in numerical order */
{AF_UNSPEC, 0},
{AF_LOCAL, 0},
{AF_INET, 0},
{AF_INET6, 0},
{AF_NETLINK, 0},
{AF_CAN, HAKUREI_EXPORT_CAN},
{AF_BLUETOOTH, HAKUREI_EXPORT_BLUETOOTH},
};
/* Blocklist all but unix, inet, inet6 and netlink */
struct {
int family;
hakurei_export_flag flags_mask;
} socket_family_allowlist[] = {
/* NOTE: Keep in numerical order */
{AF_UNSPEC, 0},
{AF_LOCAL, 0},
{AF_INET, 0},
{AF_INET6, 0},
{AF_NETLINK, 0},
{AF_CAN, HAKUREI_EXPORT_CAN},
{AF_BLUETOOTH, HAKUREI_EXPORT_BLUETOOTH},
};
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
if (ctx == NULL) {
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;
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
if (ctx == NULL) {
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;
}
}
}
}
for (i = 0; i < rules_sz; i++) {
rule = &rules[i];
assert(rule->m_errno == EPERM || rule->m_errno == ENOSYS);
for (i = 0; i < rules_sz; i++) {
rule = &rules[i];
assert(rule->m_errno == EPERM || rule->m_errno == ENOSYS);
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 (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;
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;
/* 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));
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;
}
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));
/* 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 (fd < 0) {
*ret_p = seccomp_load(ctx);
if (*ret_p != 0) {
res = 7;
goto out;
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;
}
}
} else {
*ret_p = seccomp_export_bpf(ctx, fd);
if (*ret_p != 0) {
res = 6;
goto out;
}
}
out:
if (ctx)
seccomp_release(ctx);
if (ctx)
seccomp_release(ctx);
return res;
return res;
}

View File

@@ -1,24 +1,26 @@
#include <seccomp.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)
#error This package requires libseccomp >= v2.5.1
#endif
typedef enum {
HAKUREI_EXPORT_MULTIARCH = 1 << 0,
HAKUREI_EXPORT_CAN = 1 << 1,
HAKUREI_EXPORT_BLUETOOTH = 1 << 2,
HAKUREI_EXPORT_MULTIARCH = 1 << 0,
HAKUREI_EXPORT_CAN = 1 << 1,
HAKUREI_EXPORT_BLUETOOTH = 1 << 2,
} hakurei_export_flag;
struct hakurei_syscall_rule {
int syscall;
int m_errno;
struct scmp_arg_cmp *arg;
int syscall;
int m_errno;
struct scmp_arg_cmp *arg;
};
int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch,
uint32_t multiarch,
struct hakurei_syscall_rule *rules,
size_t rules_sz, hakurei_export_flag flags);
extern void *hakurei_scmp_allocate(uintptr_t f, size_t len);
int32_t hakurei_scmp_make_filter(
int *ret_p, uintptr_t allocate_p,
uint32_t arch, uint32_t multiarch,
struct hakurei_syscall_rule *rules,
size_t rules_sz, hakurei_export_flag flags);

View File

@@ -3,7 +3,7 @@ package seccomp
/*
#cgo linux pkg-config: --static libseccomp
#include <libseccomp-helper.h>
#include "libseccomp-helper.h"
#include <sys/personality.h>
*/
import "C"
@@ -11,24 +11,24 @@ import (
"errors"
"fmt"
"runtime"
"runtime/cgo"
"syscall"
"unsafe"
"hakurei.app/container/std"
)
const (
PER_LINUX = C.PER_LINUX
PER_LINUX32 = C.PER_LINUX32
)
var (
ErrInvalidRules = errors.New("invalid native rules slice")
)
// ErrInvalidRules is returned for a zero-length rules slice.
var ErrInvalidRules = errors.New("invalid native rules slice")
// LibraryError represents a libseccomp error.
type LibraryError struct {
Prefix string
// User facing description of the libseccomp function returning the error.
Prefix string
// Negated errno value returned by libseccomp.
Seccomp syscall.Errno
Errno error
// Global errno value on return.
Errno error
}
func (e *LibraryError) Error() string {
@@ -56,20 +56,16 @@ func (e *LibraryError) Is(err error) bool {
}
type (
ScmpSyscall = C.int
ScmpErrno = C.int
// scmpUint is equivalent to [std.ScmpUint].
scmpUint = C.uint
// scmpInt is equivalent to [std.ScmpInt].
scmpInt = C.int
// syscallRule is equivalent to [std.NativeRule].
syscallRule = C.struct_hakurei_syscall_rule
)
// A NativeRule specifies an arch-specific action taken by seccomp under certain conditions.
type NativeRule struct {
// Syscall is the arch-dependent syscall number to act against.
Syscall ScmpSyscall
// Errno is the errno value to return when the condition is satisfied.
Errno ScmpErrno
// Arg is the optional struct scmp_arg_cmp passed to libseccomp.
Arg *ScmpArgCmp
}
// ExportFlag configures filter behaviour that are not implemented as rules.
type ExportFlag = C.hakurei_export_flag
const (
@@ -88,12 +84,23 @@ var resPrefix = [...]string{
3: "seccomp_arch_add failed (multiarch)",
4: "internal libseccomp failure",
5: "seccomp_rule_add failed",
6: "seccomp_export_bpf failed",
6: "seccomp_export_bpf_mem failed",
7: "seccomp_load failed",
}
// Export streams filter contents to fd, or installs it to the current process if fd < 0.
func Export(fd int, rules []NativeRule, flags ExportFlag) error {
// cbAllocateBuffer is the function signature for the function handle passed to hakurei_export_filter
// which allocates the buffer that the resulting bpf program is copied into, and writes its slice header
// to a value held by the caller.
type cbAllocateBuffer = func(len C.size_t) (buf unsafe.Pointer)
//export hakurei_scmp_allocate
func hakurei_scmp_allocate(f C.uintptr_t, len C.size_t) (buf unsafe.Pointer) {
return cgo.Handle(f).Value().(cbAllocateBuffer)(len)
}
// makeFilter generates a bpf program from a slice of [std.NativeRule] and writes the resulting byte slice to p.
// The filter is installed to the current process if p is nil.
func makeFilter(rules []std.NativeRule, flags ExportFlag, p *[]byte) error {
if len(rules) == 0 {
return ErrInvalidRules
}
@@ -117,36 +124,66 @@ func Export(fd int, rules []NativeRule, flags ExportFlag) error {
var ret C.int
rulesPinner := new(runtime.Pinner)
var scmpPinner runtime.Pinner
for i := range rules {
rule := &rules[i]
rulesPinner.Pin(rule)
scmpPinner.Pin(rule)
if rule.Arg != nil {
rulesPinner.Pin(rule.Arg)
scmpPinner.Pin(rule.Arg)
}
}
res, err := C.hakurei_export_filter(
&ret, C.int(fd),
var allocateP cgo.Handle
if p != nil {
allocateP = cgo.NewHandle(func(len C.size_t) (buf unsafe.Pointer) {
// this is so the slice header gets a Go pointer
*p = make([]byte, len)
buf = unsafe.Pointer(unsafe.SliceData(*p))
scmpPinner.Pin(buf)
return
})
}
res, err := C.hakurei_scmp_make_filter(
&ret, C.uintptr_t(allocateP),
arch, multiarch,
(*C.struct_hakurei_syscall_rule)(unsafe.Pointer(&rules[0])),
(*syscallRule)(unsafe.Pointer(&rules[0])),
C.size_t(len(rules)),
flags,
)
rulesPinner.Unpin()
scmpPinner.Unpin()
if p != nil {
allocateP.Delete()
}
if prefix := resPrefix[res]; prefix != "" {
return &LibraryError{
prefix,
-syscall.Errno(ret),
err,
}
return &LibraryError{prefix, syscall.Errno(-ret), err}
}
return err
}
// ScmpCompare is the equivalent of scmp_compare;
// Comparison operators
type ScmpCompare = C.enum_scmp_compare
// Export generates a bpf program from a slice of [std.NativeRule].
// Errors returned by libseccomp is wrapped in [LibraryError].
func Export(rules []std.NativeRule, flags ExportFlag) (data []byte, err error) {
err = makeFilter(rules, flags, &data)
return
}
// Load generates a bpf program from a slice of [std.NativeRule] and enforces it on the current process.
// Errors returned by libseccomp is wrapped in [LibraryError].
func Load(rules []std.NativeRule, flags ExportFlag) error { return makeFilter(rules, flags, nil) }
type (
// Comparison operators.
scmpCompare = C.enum_scmp_compare
// Argument datum.
scmpDatum = C.scmp_datum_t
// Argument / Value comparison definition.
scmpArgCmp = C.struct_scmp_arg_cmp
)
const (
_SCMP_CMP_MIN = C._SCMP_CMP_MIN
@@ -169,26 +206,19 @@ const (
_SCMP_CMP_MAX = C._SCMP_CMP_MAX
)
// ScmpDatum is the equivalent of scmp_datum_t;
// Argument datum
type ScmpDatum uint64
const (
// PersonaLinux is passed in a [std.ScmpDatum] for filtering calls to syscall.SYS_PERSONALITY.
PersonaLinux = C.PER_LINUX
// PersonaLinux32 is passed in a [std.ScmpDatum] for filtering calls to syscall.SYS_PERSONALITY.
PersonaLinux32 = C.PER_LINUX32
)
// ScmpArgCmp is the equivalent of struct scmp_arg_cmp;
// Argument / Value comparison definition
type ScmpArgCmp struct {
// argument number, starting at 0
Arg C.uint
// the comparison op, e.g. SCMP_CMP_*
Op ScmpCompare
DatumA, DatumB ScmpDatum
}
// only used for testing
func syscallResolveName(s string) (trap int) {
// syscallResolveName resolves a syscall number by name via seccomp_syscall_resolve_name.
// This function is only for testing the lookup tables and included here for convenience.
func syscallResolveName(s string) (num std.ScmpSyscall, ok bool) {
v := C.CString(s)
trap = int(C.seccomp_syscall_resolve_name(v))
num = std.ScmpSyscall(C.seccomp_syscall_resolve_name(v))
C.free(unsafe.Pointer(v))
ok = num != C.__NR_SCMP_ERROR
return
}

View File

@@ -3,15 +3,77 @@ package seccomp_test
import (
"crypto/sha512"
"errors"
"io"
"slices"
"syscall"
"testing"
. "hakurei.app/container/seccomp"
. "hakurei.app/container/std"
)
func TestLibraryError(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
sample *LibraryError
want string
wantIs bool
compare error
}{
{
"full",
&LibraryError{Prefix: "seccomp_export_bpf failed", Seccomp: syscall.ECANCELED, Errno: syscall.EBADF},
"seccomp_export_bpf failed: operation canceled (bad file descriptor)",
true,
&LibraryError{Prefix: "seccomp_export_bpf failed", Seccomp: syscall.ECANCELED, Errno: syscall.EBADF},
},
{
"errno only",
&LibraryError{Prefix: "seccomp_init failed", Errno: syscall.ENOMEM},
"seccomp_init failed: cannot allocate memory",
false,
nil,
},
{
"seccomp only",
&LibraryError{Prefix: "internal libseccomp failure", Seccomp: syscall.EFAULT},
"internal libseccomp failure: bad address",
true,
syscall.EFAULT,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if errors.Is(tc.sample, tc.compare) != tc.wantIs {
t.Errorf("errors.Is(%#v, %#v) did not return %v",
tc.sample, tc.compare, tc.wantIs)
}
if got := tc.sample.Error(); got != tc.want {
t.Errorf("Error: %q, want %q",
got, tc.want)
}
})
}
t.Run("invalid", func(t *testing.T) {
t.Parallel()
wantPanic := "invalid libseccomp error"
defer func() {
if r := recover(); r != wantPanic {
t.Errorf("panic: %q, want %q", r, wantPanic)
}
}()
_ = new(LibraryError).Error()
})
}
func TestExport(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
flags ExportFlag
@@ -31,64 +93,38 @@ func TestExport(t *testing.T) {
{"hakurei tty", 0, PresetExt | PresetDenyNS | PresetDenyDevel, false},
}
buf := make([]byte, 8)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
e := New(Preset(tc.presets, tc.flags), tc.flags)
want := bpfExpected[bpfPreset{tc.flags, tc.presets}]
digest := sha512.New()
t.Parallel()
if _, err := io.CopyBuffer(digest, e, buf); (err != nil) != tc.wantErr {
t.Errorf("Exporter: error = %v, wantErr %v", err, tc.wantErr)
want := bpfExpected[bpfPreset{tc.flags, tc.presets}]
if data, err := Export(Preset(tc.presets, tc.flags), tc.flags); (err != nil) != tc.wantErr {
t.Errorf("Export: error = %v, wantErr %v", err, tc.wantErr)
return
}
if err := e.Close(); err != nil {
t.Errorf("Close: error = %v", err)
}
if got := digest.Sum(nil); !slices.Equal(got, want) {
t.Fatalf("Export() hash = %x, want %x",
got, want)
} else if got := sha512.Sum512(data); got != want {
t.Fatalf("Export: hash = %x, want %x", got, want)
return
}
})
}
t.Run("close without use", func(t *testing.T) {
e := New(Preset(0, 0), 0)
if err := e.Close(); !errors.Is(err, syscall.EINVAL) {
t.Errorf("Close: error = %v", err)
return
}
})
t.Run("close partial read", func(t *testing.T) {
e := New(Preset(0, 0), 0)
if _, err := e.Read(nil); err != nil {
t.Errorf("Read: error = %v", err)
return
}
// the underlying implementation uses buffered io, so the outcome of this is nondeterministic;
// that is not harmful however, so both outcomes are checked for here
if err := e.Close(); err != nil &&
(!errors.Is(err, syscall.ECANCELED) || !errors.Is(err, syscall.EBADF)) {
t.Errorf("Close: error = %v", err)
return
}
})
}
func BenchmarkExport(b *testing.B) {
buf := make([]byte, 8)
const exportFlags = AllowMultiarch | AllowCAN | AllowBluetooth
const presetFlags = PresetExt | PresetDenyNS | PresetDenyTTY | PresetDenyDevel | PresetLinux32
var want = bpfExpected[bpfPreset{exportFlags, presetFlags}]
for b.Loop() {
e := New(
Preset(PresetExt|PresetDenyNS|PresetDenyTTY|PresetDenyDevel|PresetLinux32,
AllowMultiarch|AllowCAN|AllowBluetooth),
AllowMultiarch|AllowCAN|AllowBluetooth)
if _, err := io.CopyBuffer(io.Discard, e, buf); err != nil {
b.Fatalf("cannot export: %v", err)
data, err := Export(Preset(presetFlags, exportFlags), exportFlags)
b.StopTimer()
if err != nil {
b.Fatalf("Export: error = %v", err)
}
if err := e.Close(); err != nil {
b.Fatalf("cannot close exporter: %v", err)
if got := sha512.Sum512(data); got != want {
b.Fatalf("Export: hash = %x, want %x", got, want)
return
}
b.StartTimer()
}
}

View File

@@ -4,27 +4,14 @@ package seccomp
import (
. "syscall"
)
type FilterPreset int
const (
// PresetExt are project-specific extensions.
PresetExt FilterPreset = 1 << iota
// PresetDenyNS denies namespace setup syscalls.
PresetDenyNS
// PresetDenyTTY denies faking input.
PresetDenyTTY
// PresetDenyDevel denies development-related syscalls.
PresetDenyDevel
// PresetLinux32 sets PER_LINUX32.
PresetLinux32
. "hakurei.app/container/std"
)
func Preset(presets FilterPreset, flags ExportFlag) (rules []NativeRule) {
allowedPersonality := PER_LINUX
allowedPersonality := PersonaLinux
if presets&PresetLinux32 != 0 {
allowedPersonality = PER_LINUX32
allowedPersonality = PersonaLinux32
}
presetDevelFinal := presetDevel(ScmpDatum(allowedPersonality))
@@ -81,121 +68,121 @@ func Preset(presets FilterPreset, flags ExportFlag) (rules []NativeRule) {
var (
presetCommon = []NativeRule{
/* Block dmesg */
{ScmpSyscall(SYS_SYSLOG), ScmpErrno(EPERM), nil},
{Syscall: SNR_SYSLOG, Errno: ScmpErrno(EPERM), Arg: nil},
/* Useless old syscall */
{ScmpSyscall(SYS_USELIB), ScmpErrno(EPERM), nil},
{Syscall: SNR_USELIB, Errno: ScmpErrno(EPERM), Arg: nil},
/* Don't allow disabling accounting */
{ScmpSyscall(SYS_ACCT), ScmpErrno(EPERM), nil},
{Syscall: SNR_ACCT, Errno: ScmpErrno(EPERM), Arg: nil},
/* Don't allow reading current quota use */
{ScmpSyscall(SYS_QUOTACTL), ScmpErrno(EPERM), nil},
{Syscall: SNR_QUOTACTL, Errno: ScmpErrno(EPERM), Arg: nil},
/* Don't allow access to the kernel keyring */
{ScmpSyscall(SYS_ADD_KEY), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_KEYCTL), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_REQUEST_KEY), ScmpErrno(EPERM), nil},
{Syscall: SNR_ADD_KEY, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_KEYCTL, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_REQUEST_KEY, Errno: ScmpErrno(EPERM), Arg: nil},
/* Scary VM/NUMA ops */
{ScmpSyscall(SYS_MOVE_PAGES), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_MBIND), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_GET_MEMPOLICY), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SET_MEMPOLICY), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_MIGRATE_PAGES), ScmpErrno(EPERM), nil},
{Syscall: SNR_MOVE_PAGES, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_MBIND, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_GET_MEMPOLICY, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_SET_MEMPOLICY, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_MIGRATE_PAGES, Errno: ScmpErrno(EPERM), Arg: nil},
}
/* hakurei: project-specific extensions */
presetCommonExt = []NativeRule{
/* system calls for changing the system clock */
{ScmpSyscall(SYS_ADJTIMEX), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_CLOCK_ADJTIME), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_CLOCK_ADJTIME64), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_CLOCK_SETTIME), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_CLOCK_SETTIME64), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETTIMEOFDAY), ScmpErrno(EPERM), nil},
{Syscall: SNR_ADJTIMEX, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_CLOCK_ADJTIME, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_CLOCK_ADJTIME64, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_CLOCK_SETTIME, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_CLOCK_SETTIME64, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_SETTIMEOFDAY, Errno: ScmpErrno(EPERM), Arg: nil},
/* loading and unloading of kernel modules */
{ScmpSyscall(SYS_DELETE_MODULE), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_FINIT_MODULE), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_INIT_MODULE), ScmpErrno(EPERM), nil},
{Syscall: SNR_DELETE_MODULE, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_FINIT_MODULE, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_INIT_MODULE, Errno: ScmpErrno(EPERM), Arg: nil},
/* system calls for rebooting and reboot preparation */
{ScmpSyscall(SYS_KEXEC_FILE_LOAD), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_KEXEC_LOAD), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_REBOOT), ScmpErrno(EPERM), nil},
{Syscall: SNR_KEXEC_FILE_LOAD, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_KEXEC_LOAD, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_REBOOT, Errno: ScmpErrno(EPERM), Arg: nil},
/* system calls for enabling/disabling swap devices */
{ScmpSyscall(SYS_SWAPOFF), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SWAPON), ScmpErrno(EPERM), nil},
{Syscall: SNR_SWAPOFF, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_SWAPON, Errno: ScmpErrno(EPERM), Arg: nil},
}
presetNamespace = []NativeRule{
/* Don't allow subnamespace setups: */
{ScmpSyscall(SYS_UNSHARE), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETNS), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_MOUNT), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_UMOUNT), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_UMOUNT2), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_PIVOT_ROOT), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_CHROOT), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_CLONE), ScmpErrno(EPERM),
&ScmpArgCmp{cloneArg, SCMP_CMP_MASKED_EQ, CLONE_NEWUSER, CLONE_NEWUSER}},
{Syscall: SNR_UNSHARE, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_SETNS, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_MOUNT, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_UMOUNT, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_UMOUNT2, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_PIVOT_ROOT, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_CHROOT, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_CLONE, Errno: ScmpErrno(EPERM),
Arg: &ScmpArgCmp{Arg: cloneArg, Op: SCMP_CMP_MASKED_EQ, DatumA: CLONE_NEWUSER, DatumB: CLONE_NEWUSER}},
/* seccomp can't look into clone3()'s struct clone_args to check whether
* the flags are OK, so we have no choice but to block clone3().
* Return ENOSYS so user-space will fall back to clone().
* (CVE-2021-41133; see also https://github.com/moby/moby/commit/9f6b562d)
*/
{ScmpSyscall(SYS_CLONE3), ScmpErrno(ENOSYS), nil},
{Syscall: SNR_CLONE3, Errno: ScmpErrno(ENOSYS), Arg: nil},
/* New mount manipulation APIs can also change our VFS. There's no
* legitimate reason to do these in the sandbox, so block all of them
* rather than thinking about which ones might be dangerous.
* (CVE-2021-41133) */
{ScmpSyscall(SYS_OPEN_TREE), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_MOVE_MOUNT), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_FSOPEN), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_FSCONFIG), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_FSMOUNT), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_FSPICK), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_MOUNT_SETATTR), ScmpErrno(ENOSYS), nil},
{Syscall: SNR_OPEN_TREE, Errno: ScmpErrno(ENOSYS), Arg: nil},
{Syscall: SNR_MOVE_MOUNT, Errno: ScmpErrno(ENOSYS), Arg: nil},
{Syscall: SNR_FSOPEN, Errno: ScmpErrno(ENOSYS), Arg: nil},
{Syscall: SNR_FSCONFIG, Errno: ScmpErrno(ENOSYS), Arg: nil},
{Syscall: SNR_FSMOUNT, Errno: ScmpErrno(ENOSYS), Arg: nil},
{Syscall: SNR_FSPICK, Errno: ScmpErrno(ENOSYS), Arg: nil},
{Syscall: SNR_MOUNT_SETATTR, Errno: ScmpErrno(ENOSYS), Arg: nil},
}
/* hakurei: project-specific extensions */
presetNamespaceExt = []NativeRule{
/* changing file ownership */
{ScmpSyscall(SYS_CHOWN), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_CHOWN32), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_FCHOWN), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_FCHOWN32), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_FCHOWNAT), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_LCHOWN), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_LCHOWN32), ScmpErrno(EPERM), nil},
{Syscall: SNR_CHOWN, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_CHOWN32, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_FCHOWN, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_FCHOWN32, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_FCHOWNAT, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_LCHOWN, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_LCHOWN32, Errno: ScmpErrno(EPERM), Arg: nil},
/* system calls for changing user ID and group ID credentials */
{ScmpSyscall(SYS_SETGID), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETGID32), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETGROUPS), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETGROUPS32), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETREGID), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETREGID32), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETRESGID), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETRESGID32), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETRESUID), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETRESUID32), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETREUID), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETREUID32), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETUID), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETUID32), ScmpErrno(EPERM), nil},
{Syscall: SNR_SETGID, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_SETGID32, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_SETGROUPS, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_SETGROUPS32, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_SETREGID, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_SETREGID32, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_SETRESGID, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_SETRESGID32, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_SETRESUID, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_SETRESUID32, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_SETREUID, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_SETREUID32, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_SETUID, Errno: ScmpErrno(EPERM), Arg: nil},
{Syscall: SNR_SETUID32, Errno: ScmpErrno(EPERM), Arg: nil},
}
presetTTY = []NativeRule{
/* Don't allow faking input to the controlling tty (CVE-2017-5226) */
{ScmpSyscall(SYS_IOCTL), ScmpErrno(EPERM),
&ScmpArgCmp{1, SCMP_CMP_MASKED_EQ, 0xFFFFFFFF, TIOCSTI}},
{Syscall: SNR_IOCTL, Errno: ScmpErrno(EPERM),
Arg: &ScmpArgCmp{Arg: 1, Op: SCMP_CMP_MASKED_EQ, DatumA: 0xFFFFFFFF, DatumB: TIOCSTI}},
/* In the unlikely event that the controlling tty is a Linux virtual
* console (/dev/tty2 or similar), copy/paste operations have an effect
* similar to TIOCSTI (CVE-2023-28100) */
{ScmpSyscall(SYS_IOCTL), ScmpErrno(EPERM),
&ScmpArgCmp{1, SCMP_CMP_MASKED_EQ, 0xFFFFFFFF, TIOCLINUX}},
{Syscall: SNR_IOCTL, Errno: ScmpErrno(EPERM),
Arg: &ScmpArgCmp{Arg: 1, Op: SCMP_CMP_MASKED_EQ, DatumA: 0xFFFFFFFF, DatumB: TIOCLINUX}},
}
presetEmu = []NativeRule{
@@ -203,15 +190,15 @@ var (
* so it's disabled as a hardening measure.
* However, it is required to run old 16-bit applications
* as well as some Wine patches, so it's allowed in multiarch. */
{ScmpSyscall(SYS_MODIFY_LDT), ScmpErrno(EPERM), nil},
{Syscall: SNR_MODIFY_LDT, Errno: ScmpErrno(EPERM), Arg: nil},
}
/* hakurei: project-specific extensions */
presetEmuExt = []NativeRule{
{ScmpSyscall(SYS_SUBPAGE_PROT), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_SWITCH_ENDIAN), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_VM86), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_VM86OLD), ScmpErrno(ENOSYS), nil},
{Syscall: SNR_SUBPAGE_PROT, Errno: ScmpErrno(ENOSYS), Arg: nil},
{Syscall: SNR_SWITCH_ENDIAN, Errno: ScmpErrno(ENOSYS), Arg: nil},
{Syscall: SNR_VM86, Errno: ScmpErrno(ENOSYS), Arg: nil},
{Syscall: SNR_VM86OLD, Errno: ScmpErrno(ENOSYS), Arg: nil},
}
)
@@ -219,11 +206,11 @@ func presetDevel(allowedPersonality ScmpDatum) []NativeRule {
return []NativeRule{
/* Profiling operations; we expect these to be done by tools from outside
* the sandbox. In particular perf has been the source of many CVEs. */
{ScmpSyscall(SYS_PERF_EVENT_OPEN), ScmpErrno(EPERM), nil},
{Syscall: SNR_PERF_EVENT_OPEN, Errno: ScmpErrno(EPERM), Arg: nil},
/* Don't allow you to switch to bsd emulation or whatnot */
{ScmpSyscall(SYS_PERSONALITY), ScmpErrno(EPERM),
&ScmpArgCmp{0, SCMP_CMP_NE, allowedPersonality, 0}},
{Syscall: SNR_PERSONALITY, Errno: ScmpErrno(EPERM),
Arg: &ScmpArgCmp{Arg: 0, Op: SCMP_CMP_NE, DatumA: allowedPersonality}},
{ScmpSyscall(SYS_PTRACE), ScmpErrno(EPERM), nil},
{Syscall: SNR_PTRACE, Errno: ScmpErrno(EPERM), Arg: nil},
}
}

View File

@@ -0,0 +1,27 @@
package seccomp_test
import (
. "hakurei.app/container/seccomp"
. "hakurei.app/container/std"
)
var bpfExpected = bpfLookup{
{AllowMultiarch | AllowCAN |
AllowBluetooth, PresetExt |
PresetDenyNS | PresetDenyTTY | PresetDenyDevel |
PresetLinux32}: toHash(
"e67735d24caba42b6801e829ea4393727a36c5e37b8a51e5648e7886047e8454484ff06872aaef810799c29cbd0c1b361f423ad0ef518e33f68436372cc90eb1"),
{0, 0}: toHash(
"5dbcc08a4a1ccd8c12dd0cf6d9817ea6d4f40246e1db7a60e71a50111c4897d69f6fb6d710382d70c18910c2e4fa2d2aeb2daed835dd2fabe3f71def628ade59"),
{0, PresetExt}: toHash(
"d6c0f130dbb5c793d1c10f730455701875778138bd2d03ca009d674842fd97a10815a8c539b76b7801a73de19463938701216b756c053ec91cfe304cba04a0ed"),
{0, PresetStrict}: toHash(
"af7d7b66f2e83f9a850472170c1b83d1371426faa9d0dee4e85b179d3ec75ca92828cb8529eb3012b559497494b2eab4d4b140605e3a26c70dfdbe5efe33c105"),
{0, PresetDenyNS | PresetDenyTTY | PresetDenyDevel}: toHash(
"adfb4397e6eeae8c477d315d58204aae854d60071687b8df4c758e297780e02deee1af48328cef80e16e4d6ab1a66ef13e42247c3475cf447923f15cbc17a6a6"),
{0, PresetExt | PresetDenyDevel}: toHash(
"5d641321460cf54a7036a40a08e845082e1f6d65b9dee75db85ef179f2732f321b16aee2258b74273b04e0d24562e8b1e727930a7e787f41eb5c8aaa0bc22793"),
{0, PresetExt | PresetDenyNS | PresetDenyDevel}: toHash(
"b1f802d39de5897b1e4cb0e82a199f53df0a803ea88e2fd19491fb8c90387c9e2eaa7e323f565fecaa0202a579eb050531f22e6748e04cfd935b8faac35983ec"),
}

View File

@@ -1,6 +1,9 @@
package seccomp_test
import . "hakurei.app/container/seccomp"
import (
. "hakurei.app/container/seccomp"
. "hakurei.app/container/std"
)
var bpfExpected = bpfLookup{
{AllowMultiarch | AllowCAN |

View File

@@ -1,6 +1,9 @@
package seccomp_test
import . "hakurei.app/container/seccomp"
import (
. "hakurei.app/container/seccomp"
. "hakurei.app/container/std"
)
var bpfExpected = bpfLookup{
{AllowMultiarch | AllowCAN |

View File

@@ -1,28 +1,30 @@
package seccomp_test
import (
"crypto/sha512"
"encoding/hex"
"hakurei.app/container/seccomp"
"hakurei.app/container/std"
)
type (
bpfPreset = struct {
seccomp.ExportFlag
seccomp.FilterPreset
std.FilterPreset
}
bpfLookup map[bpfPreset][]byte
bpfLookup map[bpfPreset][sha512.Size]byte
)
func toHash(s string) []byte {
if len(s) != 128 {
func toHash(s string) [sha512.Size]byte {
if len(s) != sha512.Size*2 {
panic("bad sha512 string length")
}
if v, err := hex.DecodeString(s); err != nil {
panic(err.Error())
} else if len(v) != 64 {
} else if len(v) != sha512.Size {
panic("unreachable")
} else {
return v
return ([sha512.Size]byte)(v)
}
}

View File

@@ -1,78 +0,0 @@
package seccomp
import (
"context"
"errors"
"syscall"
"hakurei.app/helper/proc"
)
const (
PresetStrict = PresetExt | PresetDenyNS | PresetDenyTTY | PresetDenyDevel
)
// New returns an inactive Encoder instance.
func New(rules []NativeRule, flags ExportFlag) *Encoder { return &Encoder{newExporter(rules, flags)} }
// Load loads a filter into the kernel.
func Load(rules []NativeRule, flags ExportFlag) error { return Export(-1, rules, flags) }
/*
An Encoder writes a BPF program to an output stream.
Methods of Encoder are not safe for concurrent use.
An Encoder must not be copied after first use.
*/
type Encoder struct {
*exporter
}
func (e *Encoder) Read(p []byte) (n int, err error) {
if err = e.prepare(); err != nil {
return
}
return e.r.Read(p)
}
func (e *Encoder) Close() error {
if e.r == nil {
return syscall.EINVAL
}
// this hangs if the cgo thread fails to exit
return errors.Join(e.closeWrite(), <-e.exportErr)
}
// NewFile returns an instance of exporter implementing [proc.File].
func NewFile(rules []NativeRule, flags ExportFlag) proc.File {
return &File{rules: rules, flags: flags}
}
// File implements [proc.File] and provides access to the read end of exporter pipe.
type File struct {
rules []NativeRule
flags ExportFlag
proc.BaseFile
}
func (f *File) ErrCount() int { return 2 }
func (f *File) Fulfill(ctx context.Context, dispatchErr func(error)) error {
e := newExporter(f.rules, f.flags)
if err := e.prepare(); err != nil {
return err
}
f.Set(e.r)
go func() {
select {
case err := <-e.exportErr:
dispatchErr(nil)
dispatchErr(err)
case <-ctx.Done():
dispatchErr(e.closeWrite())
dispatchErr(<-e.exportErr)
}
}()
return nil
}

View File

@@ -1,60 +0,0 @@
// Package seccomp provides high level wrappers around libseccomp.
package seccomp
import (
"os"
"runtime"
"sync"
)
type exporter struct {
rules []NativeRule
flags ExportFlag
r, w *os.File
prepareOnce sync.Once
prepareErr error
closeOnce sync.Once
closeErr error
exportErr <-chan error
}
func (e *exporter) prepare() error {
e.prepareOnce.Do(func() {
if r, w, err := os.Pipe(); err != nil {
e.prepareErr = err
return
} else {
e.r, e.w = r, w
}
ec := make(chan error, 1)
go func(fd uintptr) {
ec <- Export(int(fd), e.rules, e.flags)
close(ec)
_ = e.closeWrite()
runtime.KeepAlive(e.w)
}(e.w.Fd())
e.exportErr = ec
runtime.SetFinalizer(e, (*exporter).closeWrite)
})
return e.prepareErr
}
func (e *exporter) closeWrite() error {
e.closeOnce.Do(func() {
if e.w == nil {
panic("closeWrite called on invalid exporter")
}
e.closeErr = e.w.Close()
// no need for a finalizer anymore
runtime.SetFinalizer(e, nil)
})
return e.closeErr
}
func newExporter(rules []NativeRule, flags ExportFlag) *exporter {
return &exporter{rules: rules, flags: flags}
}

View File

@@ -1,65 +0,0 @@
package seccomp_test
import (
"errors"
"runtime"
"syscall"
"testing"
"hakurei.app/container/seccomp"
)
func TestLibraryError(t *testing.T) {
testCases := []struct {
name string
sample *seccomp.LibraryError
want string
wantIs bool
compare error
}{
{
"full",
&seccomp.LibraryError{Prefix: "seccomp_export_bpf failed", Seccomp: syscall.ECANCELED, Errno: syscall.EBADF},
"seccomp_export_bpf failed: operation canceled (bad file descriptor)",
true,
&seccomp.LibraryError{Prefix: "seccomp_export_bpf failed", Seccomp: syscall.ECANCELED, Errno: syscall.EBADF},
},
{
"errno only",
&seccomp.LibraryError{Prefix: "seccomp_init failed", Errno: syscall.ENOMEM},
"seccomp_init failed: cannot allocate memory",
false,
nil,
},
{
"seccomp only",
&seccomp.LibraryError{Prefix: "internal libseccomp failure", Seccomp: syscall.EFAULT},
"internal libseccomp failure: bad address",
true,
syscall.EFAULT,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if errors.Is(tc.sample, tc.compare) != tc.wantIs {
t.Errorf("errors.Is(%#v, %#v) did not return %v",
tc.sample, tc.compare, tc.wantIs)
}
if got := tc.sample.Error(); got != tc.want {
t.Errorf("Error: %q, want %q",
got, tc.want)
}
})
}
t.Run("invalid", func(t *testing.T) {
wantPanic := "invalid libseccomp error"
defer func() {
if r := recover(); r != wantPanic {
t.Errorf("panic: %q, want %q", r, wantPanic)
}
}()
runtime.KeepAlive(new(seccomp.LibraryError).Error())
})
}

View File

@@ -0,0 +1,63 @@
package seccomp
import (
"reflect"
"testing"
"unsafe"
"hakurei.app/container/std"
)
func TestSyscallResolveName(t *testing.T) {
t.Parallel()
for name, want := range std.Syscalls() {
t.Run(name, func(t *testing.T) {
t.Parallel()
// this checks the std implementation against libseccomp.
if got, ok := syscallResolveName(name); !ok || got != want {
t.Errorf("syscallResolveName(%q) = %d, want %d", name, got, want)
}
})
}
}
func TestRuleType(t *testing.T) {
assertKind[std.ScmpUint, scmpUint](t)
assertKind[std.ScmpInt, scmpInt](t)
assertSize[std.NativeRule, syscallRule](t)
assertKind[std.ScmpDatum, scmpDatum](t)
assertKind[std.ScmpCompare, scmpCompare](t)
assertSize[std.ScmpArgCmp, scmpArgCmp](t)
}
// assertSize asserts that native and equivalent are of the same size.
func assertSize[native, equivalent any](t *testing.T) {
t.Helper()
got, want := unsafe.Sizeof(*new(native)), unsafe.Sizeof(*new(equivalent))
if got != want {
t.Fatalf("%s: %d, want %d", reflect.TypeFor[native]().Name(), got, want)
}
}
// assertKind asserts that native and equivalent are of the same kind.
func assertKind[native, equivalent any](t *testing.T) {
t.Helper()
assertSize[native, equivalent](t)
nativeType, equivalentType := reflect.TypeFor[native](), reflect.TypeFor[equivalent]()
got, want := nativeType.Kind(), equivalentType.Kind()
if got == reflect.Invalid || want == reflect.Invalid {
t.Fatalf("%s: invalid call to assertKind", nativeType.Name())
}
if got == reflect.Struct {
t.Fatalf("%s: struct is unsupported by assertKind", nativeType.Name())
}
if got != want {
t.Fatalf("%s: %s, want %s", nativeType.Name(), nativeType.Kind(), equivalentType.Kind())
}
}

View File

@@ -1,48 +0,0 @@
package seccomp
/*
#cgo linux pkg-config: --static libseccomp
#include <seccomp.h>
*/
import "C"
var syscallNumExtra = map[string]int{
"umount": SYS_UMOUNT,
"subpage_prot": SYS_SUBPAGE_PROT,
"switch_endian": SYS_SWITCH_ENDIAN,
"vm86": SYS_VM86,
"vm86old": SYS_VM86OLD,
"clock_adjtime64": SYS_CLOCK_ADJTIME64,
"clock_settime64": SYS_CLOCK_SETTIME64,
"chown32": SYS_CHOWN32,
"fchown32": SYS_FCHOWN32,
"lchown32": SYS_LCHOWN32,
"setgid32": SYS_SETGID32,
"setgroups32": SYS_SETGROUPS32,
"setregid32": SYS_SETREGID32,
"setresgid32": SYS_SETRESGID32,
"setresuid32": SYS_SETRESUID32,
"setreuid32": SYS_SETREUID32,
"setuid32": SYS_SETUID32,
}
const (
SYS_UMOUNT = C.__SNR_umount
SYS_SUBPAGE_PROT = C.__SNR_subpage_prot
SYS_SWITCH_ENDIAN = C.__SNR_switch_endian
SYS_VM86 = C.__SNR_vm86
SYS_VM86OLD = C.__SNR_vm86old
SYS_CLOCK_ADJTIME64 = C.__SNR_clock_adjtime64
SYS_CLOCK_SETTIME64 = C.__SNR_clock_settime64
SYS_CHOWN32 = C.__SNR_chown32
SYS_FCHOWN32 = C.__SNR_fchown32
SYS_LCHOWN32 = C.__SNR_lchown32
SYS_SETGID32 = C.__SNR_setgid32
SYS_SETGROUPS32 = C.__SNR_setgroups32
SYS_SETREGID32 = C.__SNR_setregid32
SYS_SETRESGID32 = C.__SNR_setresgid32
SYS_SETRESUID32 = C.__SNR_setresuid32
SYS_SETREUID32 = C.__SNR_setreuid32
SYS_SETUID32 = C.__SNR_setuid32
)

View File

@@ -1,61 +0,0 @@
package seccomp
/*
#cgo linux pkg-config: --static libseccomp
#include <seccomp.h>
*/
import "C"
import "syscall"
const (
SYS_NEWFSTATAT = syscall.SYS_FSTATAT
)
var syscallNumExtra = map[string]int{
"uselib": SYS_USELIB,
"clock_adjtime64": SYS_CLOCK_ADJTIME64,
"clock_settime64": SYS_CLOCK_SETTIME64,
"umount": SYS_UMOUNT,
"chown": SYS_CHOWN,
"chown32": SYS_CHOWN32,
"fchown32": SYS_FCHOWN32,
"lchown": SYS_LCHOWN,
"lchown32": SYS_LCHOWN32,
"setgid32": SYS_SETGID32,
"setgroups32": SYS_SETGROUPS32,
"setregid32": SYS_SETREGID32,
"setresgid32": SYS_SETRESGID32,
"setresuid32": SYS_SETRESUID32,
"setreuid32": SYS_SETREUID32,
"setuid32": SYS_SETUID32,
"modify_ldt": SYS_MODIFY_LDT,
"subpage_prot": SYS_SUBPAGE_PROT,
"switch_endian": SYS_SWITCH_ENDIAN,
"vm86": SYS_VM86,
"vm86old": SYS_VM86OLD,
}
const (
SYS_USELIB = C.__SNR_uselib
SYS_CLOCK_ADJTIME64 = C.__SNR_clock_adjtime64
SYS_CLOCK_SETTIME64 = C.__SNR_clock_settime64
SYS_UMOUNT = C.__SNR_umount
SYS_CHOWN = C.__SNR_chown
SYS_CHOWN32 = C.__SNR_chown32
SYS_FCHOWN32 = C.__SNR_fchown32
SYS_LCHOWN = C.__SNR_lchown
SYS_LCHOWN32 = C.__SNR_lchown32
SYS_SETGID32 = C.__SNR_setgid32
SYS_SETGROUPS32 = C.__SNR_setgroups32
SYS_SETREGID32 = C.__SNR_setregid32
SYS_SETRESGID32 = C.__SNR_setresgid32
SYS_SETRESUID32 = C.__SNR_setresuid32
SYS_SETREUID32 = C.__SNR_setreuid32
SYS_SETUID32 = C.__SNR_setuid32
SYS_MODIFY_LDT = C.__SNR_modify_ldt
SYS_SUBPAGE_PROT = C.__SNR_subpage_prot
SYS_SWITCH_ENDIAN = C.__SNR_switch_endian
SYS_VM86 = C.__SNR_vm86
SYS_VM86OLD = C.__SNR_vm86old
)

View File

@@ -1,459 +0,0 @@
// mksysnum_linux.pl /usr/include/asm/unistd_64.h
// Code generated by the command above; DO NOT EDIT.
package seccomp
import . "syscall"
var syscallNum = map[string]int{
"read": SYS_READ,
"write": SYS_WRITE,
"open": SYS_OPEN,
"close": SYS_CLOSE,
"stat": SYS_STAT,
"fstat": SYS_FSTAT,
"lstat": SYS_LSTAT,
"poll": SYS_POLL,
"lseek": SYS_LSEEK,
"mmap": SYS_MMAP,
"mprotect": SYS_MPROTECT,
"munmap": SYS_MUNMAP,
"brk": SYS_BRK,
"rt_sigaction": SYS_RT_SIGACTION,
"rt_sigprocmask": SYS_RT_SIGPROCMASK,
"rt_sigreturn": SYS_RT_SIGRETURN,
"ioctl": SYS_IOCTL,
"pread64": SYS_PREAD64,
"pwrite64": SYS_PWRITE64,
"readv": SYS_READV,
"writev": SYS_WRITEV,
"access": SYS_ACCESS,
"pipe": SYS_PIPE,
"select": SYS_SELECT,
"sched_yield": SYS_SCHED_YIELD,
"mremap": SYS_MREMAP,
"msync": SYS_MSYNC,
"mincore": SYS_MINCORE,
"madvise": SYS_MADVISE,
"shmget": SYS_SHMGET,
"shmat": SYS_SHMAT,
"shmctl": SYS_SHMCTL,
"dup": SYS_DUP,
"dup2": SYS_DUP2,
"pause": SYS_PAUSE,
"nanosleep": SYS_NANOSLEEP,
"getitimer": SYS_GETITIMER,
"alarm": SYS_ALARM,
"setitimer": SYS_SETITIMER,
"getpid": SYS_GETPID,
"sendfile": SYS_SENDFILE,
"socket": SYS_SOCKET,
"connect": SYS_CONNECT,
"accept": SYS_ACCEPT,
"sendto": SYS_SENDTO,
"recvfrom": SYS_RECVFROM,
"sendmsg": SYS_SENDMSG,
"recvmsg": SYS_RECVMSG,
"shutdown": SYS_SHUTDOWN,
"bind": SYS_BIND,
"listen": SYS_LISTEN,
"getsockname": SYS_GETSOCKNAME,
"getpeername": SYS_GETPEERNAME,
"socketpair": SYS_SOCKETPAIR,
"setsockopt": SYS_SETSOCKOPT,
"getsockopt": SYS_GETSOCKOPT,
"clone": SYS_CLONE,
"fork": SYS_FORK,
"vfork": SYS_VFORK,
"execve": SYS_EXECVE,
"exit": SYS_EXIT,
"wait4": SYS_WAIT4,
"kill": SYS_KILL,
"uname": SYS_UNAME,
"semget": SYS_SEMGET,
"semop": SYS_SEMOP,
"semctl": SYS_SEMCTL,
"shmdt": SYS_SHMDT,
"msgget": SYS_MSGGET,
"msgsnd": SYS_MSGSND,
"msgrcv": SYS_MSGRCV,
"msgctl": SYS_MSGCTL,
"fcntl": SYS_FCNTL,
"flock": SYS_FLOCK,
"fsync": SYS_FSYNC,
"fdatasync": SYS_FDATASYNC,
"truncate": SYS_TRUNCATE,
"ftruncate": SYS_FTRUNCATE,
"getdents": SYS_GETDENTS,
"getcwd": SYS_GETCWD,
"chdir": SYS_CHDIR,
"fchdir": SYS_FCHDIR,
"rename": SYS_RENAME,
"mkdir": SYS_MKDIR,
"rmdir": SYS_RMDIR,
"creat": SYS_CREAT,
"link": SYS_LINK,
"unlink": SYS_UNLINK,
"symlink": SYS_SYMLINK,
"readlink": SYS_READLINK,
"chmod": SYS_CHMOD,
"fchmod": SYS_FCHMOD,
"chown": SYS_CHOWN,
"fchown": SYS_FCHOWN,
"lchown": SYS_LCHOWN,
"umask": SYS_UMASK,
"gettimeofday": SYS_GETTIMEOFDAY,
"getrlimit": SYS_GETRLIMIT,
"getrusage": SYS_GETRUSAGE,
"sysinfo": SYS_SYSINFO,
"times": SYS_TIMES,
"ptrace": SYS_PTRACE,
"getuid": SYS_GETUID,
"syslog": SYS_SYSLOG,
"getgid": SYS_GETGID,
"setuid": SYS_SETUID,
"setgid": SYS_SETGID,
"geteuid": SYS_GETEUID,
"getegid": SYS_GETEGID,
"setpgid": SYS_SETPGID,
"getppid": SYS_GETPPID,
"getpgrp": SYS_GETPGRP,
"setsid": SYS_SETSID,
"setreuid": SYS_SETREUID,
"setregid": SYS_SETREGID,
"getgroups": SYS_GETGROUPS,
"setgroups": SYS_SETGROUPS,
"setresuid": SYS_SETRESUID,
"getresuid": SYS_GETRESUID,
"setresgid": SYS_SETRESGID,
"getresgid": SYS_GETRESGID,
"getpgid": SYS_GETPGID,
"setfsuid": SYS_SETFSUID,
"setfsgid": SYS_SETFSGID,
"getsid": SYS_GETSID,
"capget": SYS_CAPGET,
"capset": SYS_CAPSET,
"rt_sigpending": SYS_RT_SIGPENDING,
"rt_sigtimedwait": SYS_RT_SIGTIMEDWAIT,
"rt_sigqueueinfo": SYS_RT_SIGQUEUEINFO,
"rt_sigsuspend": SYS_RT_SIGSUSPEND,
"sigaltstack": SYS_SIGALTSTACK,
"utime": SYS_UTIME,
"mknod": SYS_MKNOD,
"uselib": SYS_USELIB,
"personality": SYS_PERSONALITY,
"ustat": SYS_USTAT,
"statfs": SYS_STATFS,
"fstatfs": SYS_FSTATFS,
"sysfs": SYS_SYSFS,
"getpriority": SYS_GETPRIORITY,
"setpriority": SYS_SETPRIORITY,
"sched_setparam": SYS_SCHED_SETPARAM,
"sched_getparam": SYS_SCHED_GETPARAM,
"sched_setscheduler": SYS_SCHED_SETSCHEDULER,
"sched_getscheduler": SYS_SCHED_GETSCHEDULER,
"sched_get_priority_max": SYS_SCHED_GET_PRIORITY_MAX,
"sched_get_priority_min": SYS_SCHED_GET_PRIORITY_MIN,
"sched_rr_get_interval": SYS_SCHED_RR_GET_INTERVAL,
"mlock": SYS_MLOCK,
"munlock": SYS_MUNLOCK,
"mlockall": SYS_MLOCKALL,
"munlockall": SYS_MUNLOCKALL,
"vhangup": SYS_VHANGUP,
"modify_ldt": SYS_MODIFY_LDT,
"pivot_root": SYS_PIVOT_ROOT,
"_sysctl": SYS__SYSCTL,
"prctl": SYS_PRCTL,
"arch_prctl": SYS_ARCH_PRCTL,
"adjtimex": SYS_ADJTIMEX,
"setrlimit": SYS_SETRLIMIT,
"chroot": SYS_CHROOT,
"sync": SYS_SYNC,
"acct": SYS_ACCT,
"settimeofday": SYS_SETTIMEOFDAY,
"mount": SYS_MOUNT,
"umount2": SYS_UMOUNT2,
"swapon": SYS_SWAPON,
"swapoff": SYS_SWAPOFF,
"reboot": SYS_REBOOT,
"sethostname": SYS_SETHOSTNAME,
"setdomainname": SYS_SETDOMAINNAME,
"iopl": SYS_IOPL,
"ioperm": SYS_IOPERM,
"create_module": SYS_CREATE_MODULE,
"init_module": SYS_INIT_MODULE,
"delete_module": SYS_DELETE_MODULE,
"get_kernel_syms": SYS_GET_KERNEL_SYMS,
"query_module": SYS_QUERY_MODULE,
"quotactl": SYS_QUOTACTL,
"nfsservctl": SYS_NFSSERVCTL,
"getpmsg": SYS_GETPMSG,
"putpmsg": SYS_PUTPMSG,
"afs_syscall": SYS_AFS_SYSCALL,
"tuxcall": SYS_TUXCALL,
"security": SYS_SECURITY,
"gettid": SYS_GETTID,
"readahead": SYS_READAHEAD,
"setxattr": SYS_SETXATTR,
"lsetxattr": SYS_LSETXATTR,
"fsetxattr": SYS_FSETXATTR,
"getxattr": SYS_GETXATTR,
"lgetxattr": SYS_LGETXATTR,
"fgetxattr": SYS_FGETXATTR,
"listxattr": SYS_LISTXATTR,
"llistxattr": SYS_LLISTXATTR,
"flistxattr": SYS_FLISTXATTR,
"removexattr": SYS_REMOVEXATTR,
"lremovexattr": SYS_LREMOVEXATTR,
"fremovexattr": SYS_FREMOVEXATTR,
"tkill": SYS_TKILL,
"time": SYS_TIME,
"futex": SYS_FUTEX,
"sched_setaffinity": SYS_SCHED_SETAFFINITY,
"sched_getaffinity": SYS_SCHED_GETAFFINITY,
"set_thread_area": SYS_SET_THREAD_AREA,
"io_setup": SYS_IO_SETUP,
"io_destroy": SYS_IO_DESTROY,
"io_getevents": SYS_IO_GETEVENTS,
"io_submit": SYS_IO_SUBMIT,
"io_cancel": SYS_IO_CANCEL,
"get_thread_area": SYS_GET_THREAD_AREA,
"lookup_dcookie": SYS_LOOKUP_DCOOKIE,
"epoll_create": SYS_EPOLL_CREATE,
"epoll_ctl_old": SYS_EPOLL_CTL_OLD,
"epoll_wait_old": SYS_EPOLL_WAIT_OLD,
"remap_file_pages": SYS_REMAP_FILE_PAGES,
"getdents64": SYS_GETDENTS64,
"set_tid_address": SYS_SET_TID_ADDRESS,
"restart_syscall": SYS_RESTART_SYSCALL,
"semtimedop": SYS_SEMTIMEDOP,
"fadvise64": SYS_FADVISE64,
"timer_create": SYS_TIMER_CREATE,
"timer_settime": SYS_TIMER_SETTIME,
"timer_gettime": SYS_TIMER_GETTIME,
"timer_getoverrun": SYS_TIMER_GETOVERRUN,
"timer_delete": SYS_TIMER_DELETE,
"clock_settime": SYS_CLOCK_SETTIME,
"clock_gettime": SYS_CLOCK_GETTIME,
"clock_getres": SYS_CLOCK_GETRES,
"clock_nanosleep": SYS_CLOCK_NANOSLEEP,
"exit_group": SYS_EXIT_GROUP,
"epoll_wait": SYS_EPOLL_WAIT,
"epoll_ctl": SYS_EPOLL_CTL,
"tgkill": SYS_TGKILL,
"utimes": SYS_UTIMES,
"vserver": SYS_VSERVER,
"mbind": SYS_MBIND,
"set_mempolicy": SYS_SET_MEMPOLICY,
"get_mempolicy": SYS_GET_MEMPOLICY,
"mq_open": SYS_MQ_OPEN,
"mq_unlink": SYS_MQ_UNLINK,
"mq_timedsend": SYS_MQ_TIMEDSEND,
"mq_timedreceive": SYS_MQ_TIMEDRECEIVE,
"mq_notify": SYS_MQ_NOTIFY,
"mq_getsetattr": SYS_MQ_GETSETATTR,
"kexec_load": SYS_KEXEC_LOAD,
"waitid": SYS_WAITID,
"add_key": SYS_ADD_KEY,
"request_key": SYS_REQUEST_KEY,
"keyctl": SYS_KEYCTL,
"ioprio_set": SYS_IOPRIO_SET,
"ioprio_get": SYS_IOPRIO_GET,
"inotify_init": SYS_INOTIFY_INIT,
"inotify_add_watch": SYS_INOTIFY_ADD_WATCH,
"inotify_rm_watch": SYS_INOTIFY_RM_WATCH,
"migrate_pages": SYS_MIGRATE_PAGES,
"openat": SYS_OPENAT,
"mkdirat": SYS_MKDIRAT,
"mknodat": SYS_MKNODAT,
"fchownat": SYS_FCHOWNAT,
"futimesat": SYS_FUTIMESAT,
"newfstatat": SYS_NEWFSTATAT,
"unlinkat": SYS_UNLINKAT,
"renameat": SYS_RENAMEAT,
"linkat": SYS_LINKAT,
"symlinkat": SYS_SYMLINKAT,
"readlinkat": SYS_READLINKAT,
"fchmodat": SYS_FCHMODAT,
"faccessat": SYS_FACCESSAT,
"pselect6": SYS_PSELECT6,
"ppoll": SYS_PPOLL,
"unshare": SYS_UNSHARE,
"set_robust_list": SYS_SET_ROBUST_LIST,
"get_robust_list": SYS_GET_ROBUST_LIST,
"splice": SYS_SPLICE,
"tee": SYS_TEE,
"sync_file_range": SYS_SYNC_FILE_RANGE,
"vmsplice": SYS_VMSPLICE,
"move_pages": SYS_MOVE_PAGES,
"utimensat": SYS_UTIMENSAT,
"epoll_pwait": SYS_EPOLL_PWAIT,
"signalfd": SYS_SIGNALFD,
"timerfd_create": SYS_TIMERFD_CREATE,
"eventfd": SYS_EVENTFD,
"fallocate": SYS_FALLOCATE,
"timerfd_settime": SYS_TIMERFD_SETTIME,
"timerfd_gettime": SYS_TIMERFD_GETTIME,
"accept4": SYS_ACCEPT4,
"signalfd4": SYS_SIGNALFD4,
"eventfd2": SYS_EVENTFD2,
"epoll_create1": SYS_EPOLL_CREATE1,
"dup3": SYS_DUP3,
"pipe2": SYS_PIPE2,
"inotify_init1": SYS_INOTIFY_INIT1,
"preadv": SYS_PREADV,
"pwritev": SYS_PWRITEV,
"rt_tgsigqueueinfo": SYS_RT_TGSIGQUEUEINFO,
"perf_event_open": SYS_PERF_EVENT_OPEN,
"recvmmsg": SYS_RECVMMSG,
"fanotify_init": SYS_FANOTIFY_INIT,
"fanotify_mark": SYS_FANOTIFY_MARK,
"prlimit64": SYS_PRLIMIT64,
"name_to_handle_at": SYS_NAME_TO_HANDLE_AT,
"open_by_handle_at": SYS_OPEN_BY_HANDLE_AT,
"clock_adjtime": SYS_CLOCK_ADJTIME,
"syncfs": SYS_SYNCFS,
"sendmmsg": SYS_SENDMMSG,
"setns": SYS_SETNS,
"getcpu": SYS_GETCPU,
"process_vm_readv": SYS_PROCESS_VM_READV,
"process_vm_writev": SYS_PROCESS_VM_WRITEV,
"kcmp": SYS_KCMP,
"finit_module": SYS_FINIT_MODULE,
"sched_setattr": SYS_SCHED_SETATTR,
"sched_getattr": SYS_SCHED_GETATTR,
"renameat2": SYS_RENAMEAT2,
"seccomp": SYS_SECCOMP,
"getrandom": SYS_GETRANDOM,
"memfd_create": SYS_MEMFD_CREATE,
"kexec_file_load": SYS_KEXEC_FILE_LOAD,
"bpf": SYS_BPF,
"execveat": SYS_EXECVEAT,
"userfaultfd": SYS_USERFAULTFD,
"membarrier": SYS_MEMBARRIER,
"mlock2": SYS_MLOCK2,
"copy_file_range": SYS_COPY_FILE_RANGE,
"preadv2": SYS_PREADV2,
"pwritev2": SYS_PWRITEV2,
"pkey_mprotect": SYS_PKEY_MPROTECT,
"pkey_alloc": SYS_PKEY_ALLOC,
"pkey_free": SYS_PKEY_FREE,
"statx": SYS_STATX,
"io_pgetevents": SYS_IO_PGETEVENTS,
"rseq": SYS_RSEQ,
"uretprobe": SYS_URETPROBE,
"pidfd_send_signal": SYS_PIDFD_SEND_SIGNAL,
"io_uring_setup": SYS_IO_URING_SETUP,
"io_uring_enter": SYS_IO_URING_ENTER,
"io_uring_register": SYS_IO_URING_REGISTER,
"open_tree": SYS_OPEN_TREE,
"move_mount": SYS_MOVE_MOUNT,
"fsopen": SYS_FSOPEN,
"fsconfig": SYS_FSCONFIG,
"fsmount": SYS_FSMOUNT,
"fspick": SYS_FSPICK,
"pidfd_open": SYS_PIDFD_OPEN,
"clone3": SYS_CLONE3,
"close_range": SYS_CLOSE_RANGE,
"openat2": SYS_OPENAT2,
"pidfd_getfd": SYS_PIDFD_GETFD,
"faccessat2": SYS_FACCESSAT2,
"process_madvise": SYS_PROCESS_MADVISE,
"epoll_pwait2": SYS_EPOLL_PWAIT2,
"mount_setattr": SYS_MOUNT_SETATTR,
"quotactl_fd": SYS_QUOTACTL_FD,
"landlock_create_ruleset": SYS_LANDLOCK_CREATE_RULESET,
"landlock_add_rule": SYS_LANDLOCK_ADD_RULE,
"landlock_restrict_self": SYS_LANDLOCK_RESTRICT_SELF,
"memfd_secret": SYS_MEMFD_SECRET,
"process_mrelease": SYS_PROCESS_MRELEASE,
"futex_waitv": SYS_FUTEX_WAITV,
"set_mempolicy_home_node": SYS_SET_MEMPOLICY_HOME_NODE,
"cachestat": SYS_CACHESTAT,
"fchmodat2": SYS_FCHMODAT2,
"map_shadow_stack": SYS_MAP_SHADOW_STACK,
"futex_wake": SYS_FUTEX_WAKE,
"futex_wait": SYS_FUTEX_WAIT,
"futex_requeue": SYS_FUTEX_REQUEUE,
"statmount": SYS_STATMOUNT,
"listmount": SYS_LISTMOUNT,
"lsm_get_self_attr": SYS_LSM_GET_SELF_ATTR,
"lsm_set_self_attr": SYS_LSM_SET_SELF_ATTR,
"lsm_list_modules": SYS_LSM_LIST_MODULES,
"mseal": SYS_MSEAL,
}
const (
SYS_NAME_TO_HANDLE_AT = 303
SYS_OPEN_BY_HANDLE_AT = 304
SYS_CLOCK_ADJTIME = 305
SYS_SYNCFS = 306
SYS_SENDMMSG = 307
SYS_SETNS = 308
SYS_GETCPU = 309
SYS_PROCESS_VM_READV = 310
SYS_PROCESS_VM_WRITEV = 311
SYS_KCMP = 312
SYS_FINIT_MODULE = 313
SYS_SCHED_SETATTR = 314
SYS_SCHED_GETATTR = 315
SYS_RENAMEAT2 = 316
SYS_SECCOMP = 317
SYS_GETRANDOM = 318
SYS_MEMFD_CREATE = 319
SYS_KEXEC_FILE_LOAD = 320
SYS_BPF = 321
SYS_EXECVEAT = 322
SYS_USERFAULTFD = 323
SYS_MEMBARRIER = 324
SYS_MLOCK2 = 325
SYS_COPY_FILE_RANGE = 326
SYS_PREADV2 = 327
SYS_PWRITEV2 = 328
SYS_PKEY_MPROTECT = 329
SYS_PKEY_ALLOC = 330
SYS_PKEY_FREE = 331
SYS_STATX = 332
SYS_IO_PGETEVENTS = 333
SYS_RSEQ = 334
SYS_URETPROBE = 335
SYS_PIDFD_SEND_SIGNAL = 424
SYS_IO_URING_SETUP = 425
SYS_IO_URING_ENTER = 426
SYS_IO_URING_REGISTER = 427
SYS_OPEN_TREE = 428
SYS_MOVE_MOUNT = 429
SYS_FSOPEN = 430
SYS_FSCONFIG = 431
SYS_FSMOUNT = 432
SYS_FSPICK = 433
SYS_PIDFD_OPEN = 434
SYS_CLONE3 = 435
SYS_CLOSE_RANGE = 436
SYS_OPENAT2 = 437
SYS_PIDFD_GETFD = 438
SYS_FACCESSAT2 = 439
SYS_PROCESS_MADVISE = 440
SYS_EPOLL_PWAIT2 = 441
SYS_MOUNT_SETATTR = 442
SYS_QUOTACTL_FD = 443
SYS_LANDLOCK_CREATE_RULESET = 444
SYS_LANDLOCK_ADD_RULE = 445
SYS_LANDLOCK_RESTRICT_SELF = 446
SYS_MEMFD_SECRET = 447
SYS_PROCESS_MRELEASE = 448
SYS_FUTEX_WAITV = 449
SYS_SET_MEMPOLICY_HOME_NODE = 450
SYS_CACHESTAT = 451
SYS_FCHMODAT2 = 452
SYS_MAP_SHADOW_STACK = 453
SYS_FUTEX_WAKE = 454
SYS_FUTEX_WAIT = 455
SYS_FUTEX_REQUEUE = 456
SYS_STATMOUNT = 457
SYS_LISTMOUNT = 458
SYS_LSM_GET_SELF_ATTR = 459
SYS_LSM_SET_SELF_ATTR = 460
SYS_LSM_LIST_MODULES = 461
SYS_MSEAL = 462
)

View File

@@ -1,382 +0,0 @@
// mksysnum_linux.pl /usr/include/asm/unistd_64.h
// Code generated by the command above; DO NOT EDIT.
package seccomp
import . "syscall"
var syscallNum = map[string]int{
"io_setup": SYS_IO_SETUP,
"io_destroy": SYS_IO_DESTROY,
"io_submit": SYS_IO_SUBMIT,
"io_cancel": SYS_IO_CANCEL,
"io_getevents": SYS_IO_GETEVENTS,
"setxattr": SYS_SETXATTR,
"lsetxattr": SYS_LSETXATTR,
"fsetxattr": SYS_FSETXATTR,
"getxattr": SYS_GETXATTR,
"lgetxattr": SYS_LGETXATTR,
"fgetxattr": SYS_FGETXATTR,
"listxattr": SYS_LISTXATTR,
"llistxattr": SYS_LLISTXATTR,
"flistxattr": SYS_FLISTXATTR,
"removexattr": SYS_REMOVEXATTR,
"lremovexattr": SYS_LREMOVEXATTR,
"fremovexattr": SYS_FREMOVEXATTR,
"getcwd": SYS_GETCWD,
"lookup_dcookie": SYS_LOOKUP_DCOOKIE,
"eventfd2": SYS_EVENTFD2,
"epoll_create1": SYS_EPOLL_CREATE1,
"epoll_ctl": SYS_EPOLL_CTL,
"epoll_pwait": SYS_EPOLL_PWAIT,
"dup": SYS_DUP,
"dup3": SYS_DUP3,
"fcntl": SYS_FCNTL,
"inotify_init1": SYS_INOTIFY_INIT1,
"inotify_add_watch": SYS_INOTIFY_ADD_WATCH,
"inotify_rm_watch": SYS_INOTIFY_RM_WATCH,
"ioctl": SYS_IOCTL,
"ioprio_set": SYS_IOPRIO_SET,
"ioprio_get": SYS_IOPRIO_GET,
"flock": SYS_FLOCK,
"mknodat": SYS_MKNODAT,
"mkdirat": SYS_MKDIRAT,
"unlinkat": SYS_UNLINKAT,
"symlinkat": SYS_SYMLINKAT,
"linkat": SYS_LINKAT,
"renameat": SYS_RENAMEAT,
"umount2": SYS_UMOUNT2,
"mount": SYS_MOUNT,
"pivot_root": SYS_PIVOT_ROOT,
"nfsservctl": SYS_NFSSERVCTL,
"statfs": SYS_STATFS,
"fstatfs": SYS_FSTATFS,
"truncate": SYS_TRUNCATE,
"ftruncate": SYS_FTRUNCATE,
"fallocate": SYS_FALLOCATE,
"faccessat": SYS_FACCESSAT,
"chdir": SYS_CHDIR,
"fchdir": SYS_FCHDIR,
"chroot": SYS_CHROOT,
"fchmod": SYS_FCHMOD,
"fchmodat": SYS_FCHMODAT,
"fchownat": SYS_FCHOWNAT,
"fchown": SYS_FCHOWN,
"openat": SYS_OPENAT,
"close": SYS_CLOSE,
"vhangup": SYS_VHANGUP,
"pipe2": SYS_PIPE2,
"quotactl": SYS_QUOTACTL,
"getdents64": SYS_GETDENTS64,
"lseek": SYS_LSEEK,
"read": SYS_READ,
"write": SYS_WRITE,
"readv": SYS_READV,
"writev": SYS_WRITEV,
"pread64": SYS_PREAD64,
"pwrite64": SYS_PWRITE64,
"preadv": SYS_PREADV,
"pwritev": SYS_PWRITEV,
"sendfile": SYS_SENDFILE,
"pselect6": SYS_PSELECT6,
"ppoll": SYS_PPOLL,
"signalfd4": SYS_SIGNALFD4,
"vmsplice": SYS_VMSPLICE,
"splice": SYS_SPLICE,
"tee": SYS_TEE,
"readlinkat": SYS_READLINKAT,
"newfstatat": SYS_NEWFSTATAT,
"fstat": SYS_FSTAT,
"sync": SYS_SYNC,
"fsync": SYS_FSYNC,
"fdatasync": SYS_FDATASYNC,
"sync_file_range": SYS_SYNC_FILE_RANGE,
"timerfd_create": SYS_TIMERFD_CREATE,
"timerfd_settime": SYS_TIMERFD_SETTIME,
"timerfd_gettime": SYS_TIMERFD_GETTIME,
"utimensat": SYS_UTIMENSAT,
"acct": SYS_ACCT,
"capget": SYS_CAPGET,
"capset": SYS_CAPSET,
"personality": SYS_PERSONALITY,
"exit": SYS_EXIT,
"exit_group": SYS_EXIT_GROUP,
"waitid": SYS_WAITID,
"set_tid_address": SYS_SET_TID_ADDRESS,
"unshare": SYS_UNSHARE,
"futex": SYS_FUTEX,
"set_robust_list": SYS_SET_ROBUST_LIST,
"get_robust_list": SYS_GET_ROBUST_LIST,
"nanosleep": SYS_NANOSLEEP,
"getitimer": SYS_GETITIMER,
"setitimer": SYS_SETITIMER,
"kexec_load": SYS_KEXEC_LOAD,
"init_module": SYS_INIT_MODULE,
"delete_module": SYS_DELETE_MODULE,
"timer_create": SYS_TIMER_CREATE,
"timer_gettime": SYS_TIMER_GETTIME,
"timer_getoverrun": SYS_TIMER_GETOVERRUN,
"timer_settime": SYS_TIMER_SETTIME,
"timer_delete": SYS_TIMER_DELETE,
"clock_settime": SYS_CLOCK_SETTIME,
"clock_gettime": SYS_CLOCK_GETTIME,
"clock_getres": SYS_CLOCK_GETRES,
"clock_nanosleep": SYS_CLOCK_NANOSLEEP,
"syslog": SYS_SYSLOG,
"ptrace": SYS_PTRACE,
"sched_setparam": SYS_SCHED_SETPARAM,
"sched_setscheduler": SYS_SCHED_SETSCHEDULER,
"sched_getscheduler": SYS_SCHED_GETSCHEDULER,
"sched_getparam": SYS_SCHED_GETPARAM,
"sched_setaffinity": SYS_SCHED_SETAFFINITY,
"sched_getaffinity": SYS_SCHED_GETAFFINITY,
"sched_yield": SYS_SCHED_YIELD,
"sched_get_priority_max": SYS_SCHED_GET_PRIORITY_MAX,
"sched_get_priority_min": SYS_SCHED_GET_PRIORITY_MIN,
"sched_rr_get_interval": SYS_SCHED_RR_GET_INTERVAL,
"restart_syscall": SYS_RESTART_SYSCALL,
"kill": SYS_KILL,
"tkill": SYS_TKILL,
"tgkill": SYS_TGKILL,
"sigaltstack": SYS_SIGALTSTACK,
"rt_sigsuspend": SYS_RT_SIGSUSPEND,
"rt_sigaction": SYS_RT_SIGACTION,
"rt_sigprocmask": SYS_RT_SIGPROCMASK,
"rt_sigpending": SYS_RT_SIGPENDING,
"rt_sigtimedwait": SYS_RT_SIGTIMEDWAIT,
"rt_sigqueueinfo": SYS_RT_SIGQUEUEINFO,
"rt_sigreturn": SYS_RT_SIGRETURN,
"setpriority": SYS_SETPRIORITY,
"getpriority": SYS_GETPRIORITY,
"reboot": SYS_REBOOT,
"setregid": SYS_SETREGID,
"setgid": SYS_SETGID,
"setreuid": SYS_SETREUID,
"setuid": SYS_SETUID,
"setresuid": SYS_SETRESUID,
"getresuid": SYS_GETRESUID,
"setresgid": SYS_SETRESGID,
"getresgid": SYS_GETRESGID,
"setfsuid": SYS_SETFSUID,
"setfsgid": SYS_SETFSGID,
"times": SYS_TIMES,
"setpgid": SYS_SETPGID,
"getpgid": SYS_GETPGID,
"getsid": SYS_GETSID,
"setsid": SYS_SETSID,
"getgroups": SYS_GETGROUPS,
"setgroups": SYS_SETGROUPS,
"uname": SYS_UNAME,
"sethostname": SYS_SETHOSTNAME,
"setdomainname": SYS_SETDOMAINNAME,
"getrlimit": SYS_GETRLIMIT,
"setrlimit": SYS_SETRLIMIT,
"getrusage": SYS_GETRUSAGE,
"umask": SYS_UMASK,
"prctl": SYS_PRCTL,
"getcpu": SYS_GETCPU,
"gettimeofday": SYS_GETTIMEOFDAY,
"settimeofday": SYS_SETTIMEOFDAY,
"adjtimex": SYS_ADJTIMEX,
"getpid": SYS_GETPID,
"getppid": SYS_GETPPID,
"getuid": SYS_GETUID,
"geteuid": SYS_GETEUID,
"getgid": SYS_GETGID,
"getegid": SYS_GETEGID,
"gettid": SYS_GETTID,
"sysinfo": SYS_SYSINFO,
"mq_open": SYS_MQ_OPEN,
"mq_unlink": SYS_MQ_UNLINK,
"mq_timedsend": SYS_MQ_TIMEDSEND,
"mq_timedreceive": SYS_MQ_TIMEDRECEIVE,
"mq_notify": SYS_MQ_NOTIFY,
"mq_getsetattr": SYS_MQ_GETSETATTR,
"msgget": SYS_MSGGET,
"msgctl": SYS_MSGCTL,
"msgrcv": SYS_MSGRCV,
"msgsnd": SYS_MSGSND,
"semget": SYS_SEMGET,
"semctl": SYS_SEMCTL,
"semtimedop": SYS_SEMTIMEDOP,
"semop": SYS_SEMOP,
"shmget": SYS_SHMGET,
"shmctl": SYS_SHMCTL,
"shmat": SYS_SHMAT,
"shmdt": SYS_SHMDT,
"socket": SYS_SOCKET,
"socketpair": SYS_SOCKETPAIR,
"bind": SYS_BIND,
"listen": SYS_LISTEN,
"accept": SYS_ACCEPT,
"connect": SYS_CONNECT,
"getsockname": SYS_GETSOCKNAME,
"getpeername": SYS_GETPEERNAME,
"sendto": SYS_SENDTO,
"recvfrom": SYS_RECVFROM,
"setsockopt": SYS_SETSOCKOPT,
"getsockopt": SYS_GETSOCKOPT,
"shutdown": SYS_SHUTDOWN,
"sendmsg": SYS_SENDMSG,
"recvmsg": SYS_RECVMSG,
"readahead": SYS_READAHEAD,
"brk": SYS_BRK,
"munmap": SYS_MUNMAP,
"mremap": SYS_MREMAP,
"add_key": SYS_ADD_KEY,
"request_key": SYS_REQUEST_KEY,
"keyctl": SYS_KEYCTL,
"clone": SYS_CLONE,
"execve": SYS_EXECVE,
"mmap": SYS_MMAP,
"fadvise64": SYS_FADVISE64,
"swapon": SYS_SWAPON,
"swapoff": SYS_SWAPOFF,
"mprotect": SYS_MPROTECT,
"msync": SYS_MSYNC,
"mlock": SYS_MLOCK,
"munlock": SYS_MUNLOCK,
"mlockall": SYS_MLOCKALL,
"munlockall": SYS_MUNLOCKALL,
"mincore": SYS_MINCORE,
"madvise": SYS_MADVISE,
"remap_file_pages": SYS_REMAP_FILE_PAGES,
"mbind": SYS_MBIND,
"get_mempolicy": SYS_GET_MEMPOLICY,
"set_mempolicy": SYS_SET_MEMPOLICY,
"migrate_pages": SYS_MIGRATE_PAGES,
"move_pages": SYS_MOVE_PAGES,
"rt_tgsigqueueinfo": SYS_RT_TGSIGQUEUEINFO,
"perf_event_open": SYS_PERF_EVENT_OPEN,
"accept4": SYS_ACCEPT4,
"recvmmsg": SYS_RECVMMSG,
"wait4": SYS_WAIT4,
"prlimit64": SYS_PRLIMIT64,
"fanotify_init": SYS_FANOTIFY_INIT,
"fanotify_mark": SYS_FANOTIFY_MARK,
"name_to_handle_at": SYS_NAME_TO_HANDLE_AT,
"open_by_handle_at": SYS_OPEN_BY_HANDLE_AT,
"clock_adjtime": SYS_CLOCK_ADJTIME,
"syncfs": SYS_SYNCFS,
"setns": SYS_SETNS,
"sendmmsg": SYS_SENDMMSG,
"process_vm_readv": SYS_PROCESS_VM_READV,
"process_vm_writev": SYS_PROCESS_VM_WRITEV,
"kcmp": SYS_KCMP,
"finit_module": SYS_FINIT_MODULE,
"sched_setattr": SYS_SCHED_SETATTR,
"sched_getattr": SYS_SCHED_GETATTR,
"renameat2": SYS_RENAMEAT2,
"seccomp": SYS_SECCOMP,
"getrandom": SYS_GETRANDOM,
"memfd_create": SYS_MEMFD_CREATE,
"bpf": SYS_BPF,
"execveat": SYS_EXECVEAT,
"userfaultfd": SYS_USERFAULTFD,
"membarrier": SYS_MEMBARRIER,
"mlock2": SYS_MLOCK2,
"copy_file_range": SYS_COPY_FILE_RANGE,
"preadv2": SYS_PREADV2,
"pwritev2": SYS_PWRITEV2,
"pkey_mprotect": SYS_PKEY_MPROTECT,
"pkey_alloc": SYS_PKEY_ALLOC,
"pkey_free": SYS_PKEY_FREE,
"statx": SYS_STATX,
"io_pgetevents": SYS_IO_PGETEVENTS,
"rseq": SYS_RSEQ,
"kexec_file_load": SYS_KEXEC_FILE_LOAD,
"pidfd_send_signal": SYS_PIDFD_SEND_SIGNAL,
"io_uring_setup": SYS_IO_URING_SETUP,
"io_uring_enter": SYS_IO_URING_ENTER,
"io_uring_register": SYS_IO_URING_REGISTER,
"open_tree": SYS_OPEN_TREE,
"move_mount": SYS_MOVE_MOUNT,
"fsopen": SYS_FSOPEN,
"fsconfig": SYS_FSCONFIG,
"fsmount": SYS_FSMOUNT,
"fspick": SYS_FSPICK,
"pidfd_open": SYS_PIDFD_OPEN,
"clone3": SYS_CLONE3,
"close_range": SYS_CLOSE_RANGE,
"openat2": SYS_OPENAT2,
"pidfd_getfd": SYS_PIDFD_GETFD,
"faccessat2": SYS_FACCESSAT2,
"process_madvise": SYS_PROCESS_MADVISE,
"epoll_pwait2": SYS_EPOLL_PWAIT2,
"mount_setattr": SYS_MOUNT_SETATTR,
"quotactl_fd": SYS_QUOTACTL_FD,
"landlock_create_ruleset": SYS_LANDLOCK_CREATE_RULESET,
"landlock_add_rule": SYS_LANDLOCK_ADD_RULE,
"landlock_restrict_self": SYS_LANDLOCK_RESTRICT_SELF,
"memfd_secret": SYS_MEMFD_SECRET,
"process_mrelease": SYS_PROCESS_MRELEASE,
"futex_waitv": SYS_FUTEX_WAITV,
"set_mempolicy_home_node": SYS_SET_MEMPOLICY_HOME_NODE,
"cachestat": SYS_CACHESTAT,
"fchmodat2": SYS_FCHMODAT2,
"map_shadow_stack": SYS_MAP_SHADOW_STACK,
"futex_wake": SYS_FUTEX_WAKE,
"futex_wait": SYS_FUTEX_WAIT,
"futex_requeue": SYS_FUTEX_REQUEUE,
"statmount": SYS_STATMOUNT,
"listmount": SYS_LISTMOUNT,
"lsm_get_self_attr": SYS_LSM_GET_SELF_ATTR,
"lsm_set_self_attr": SYS_LSM_SET_SELF_ATTR,
"lsm_list_modules": SYS_LSM_LIST_MODULES,
"mseal": SYS_MSEAL,
}
const (
SYS_USERFAULTFD = 282
SYS_MEMBARRIER = 283
SYS_MLOCK2 = 284
SYS_COPY_FILE_RANGE = 285
SYS_PREADV2 = 286
SYS_PWRITEV2 = 287
SYS_PKEY_MPROTECT = 288
SYS_PKEY_ALLOC = 289
SYS_PKEY_FREE = 290
SYS_STATX = 291
SYS_IO_PGETEVENTS = 292
SYS_RSEQ = 293
SYS_KEXEC_FILE_LOAD = 294
SYS_PIDFD_SEND_SIGNAL = 424
SYS_IO_URING_SETUP = 425
SYS_IO_URING_ENTER = 426
SYS_IO_URING_REGISTER = 427
SYS_OPEN_TREE = 428
SYS_MOVE_MOUNT = 429
SYS_FSOPEN = 430
SYS_FSCONFIG = 431
SYS_FSMOUNT = 432
SYS_FSPICK = 433
SYS_PIDFD_OPEN = 434
SYS_CLONE3 = 435
SYS_CLOSE_RANGE = 436
SYS_OPENAT2 = 437
SYS_PIDFD_GETFD = 438
SYS_FACCESSAT2 = 439
SYS_PROCESS_MADVISE = 440
SYS_EPOLL_PWAIT2 = 441
SYS_MOUNT_SETATTR = 442
SYS_QUOTACTL_FD = 443
SYS_LANDLOCK_CREATE_RULESET = 444
SYS_LANDLOCK_ADD_RULE = 445
SYS_LANDLOCK_RESTRICT_SELF = 446
SYS_MEMFD_SECRET = 447
SYS_PROCESS_MRELEASE = 448
SYS_FUTEX_WAITV = 449
SYS_SET_MEMPOLICY_HOME_NODE = 450
SYS_CACHESTAT = 451
SYS_FCHMODAT2 = 452
SYS_MAP_SHADOW_STACK = 453
SYS_FUTEX_WAKE = 454
SYS_FUTEX_WAIT = 455
SYS_FUTEX_REQUEUE = 456
SYS_STATMOUNT = 457
SYS_LISTMOUNT = 458
SYS_LSM_GET_SELF_ATTR = 459
SYS_LSM_SET_SELF_ATTR = 460
SYS_LSM_LIST_MODULES = 461
SYS_MSEAL = 462
)

View File

@@ -1,20 +0,0 @@
package seccomp
import (
"testing"
)
func TestSyscallResolveName(t *testing.T) {
for name, want := range Syscalls() {
t.Run(name, func(t *testing.T) {
if got := syscallResolveName(name); got != want {
t.Errorf("syscallResolveName(%q) = %d, want %d",
name, got, want)
}
if got, ok := SyscallResolveName(name); !ok || got != want {
t.Errorf("SyscallResolveName(%q) = %d, want %d",
name, got, want)
}
})
}
}

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