67 Commits

Author SHA1 Message Date
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
136 changed files with 9430 additions and 5779 deletions

View File

@@ -6,11 +6,9 @@ import (
"io"
"log"
"os"
"os/signal"
"os/user"
"strconv"
"sync"
"syscall"
"time"
"hakurei.app/command"
@@ -20,11 +18,12 @@ import (
"hakurei.app/internal/app"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/internal/sys"
"hakurei.app/system"
"hakurei.app/system/dbus"
)
func buildCommand(out io.Writer) command.Command {
func buildCommand(ctx context.Context, out io.Writer) command.Command {
var (
flagVerbose bool
flagJSON bool
@@ -44,35 +43,35 @@ func buildCommand(out io.Writer) command.Command {
config := tryPath(args[0])
config.Args = append(config.Args, args[1:]...)
runApp(config)
app.Main(ctx, std, config)
panic("unreachable")
})
{
var (
dbusConfigSession string
dbusConfigSystem string
mpris bool
dbusVerbose bool
flagDBusConfigSession string
flagDBusConfigSystem string
flagDBusMpris bool
flagDBusVerbose bool
fid string
aid int
groups command.RepeatableFlag
homeDir string
userName string
flagID string
flagIdentity int
flagGroups command.RepeatableFlag
flagHomeDir string
flagUserName string
wayland, x11, dBus, pulse bool
flagWayland, flagX11, flagDBus, flagPulse bool
)
c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error {
// initialise config from flags
config := &hst.Config{
ID: fid,
ID: flagID,
Args: args,
}
if aid < 0 || aid > 9999 {
log.Fatalf("aid %d out of range", aid)
if flagIdentity < 0 || flagIdentity > 9999 {
log.Fatalf("identity %d out of range", flagIdentity)
}
// resolve home/username from os when flag is unset
@@ -80,14 +79,7 @@ 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(sys.MustUid(std, flagIdentity))
if u, err := user.LookupId(us); err != nil {
hlog.Verbosef("cannot look up uid %s", us)
passwd = &user.User{
@@ -103,21 +95,21 @@ func buildCommand(out io.Writer) command.Command {
}
)
if homeDir == "os" {
if flagHomeDir == "os" {
passwdOnce.Do(passwdFunc)
homeDir = passwd.HomeDir
flagHomeDir = passwd.HomeDir
}
if userName == "chronos" {
if flagUserName == "chronos" {
passwdOnce.Do(passwdFunc)
userName = passwd.Username
flagUserName = passwd.Username
}
config.Identity = aid
config.Groups = groups
config.Username = userName
config.Identity = flagIdentity
config.Groups = flagGroups
config.Username = flagUserName
if a, err := container.NewAbs(homeDir); err != nil {
if a, err := container.NewAbs(flagHomeDir); err != nil {
log.Fatal(err.Error())
return err
} else {
@@ -125,105 +117,112 @@ func buildCommand(out io.Writer) command.Command {
}
var e system.Enablement
if wayland {
if flagWayland {
e |= system.EWayland
}
if x11 {
if flagX11 {
e |= system.EX11
}
if dBus {
if flagDBus {
e |= system.EDBus
}
if pulse {
if flagPulse {
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 conf, err := dbus.NewConfigFromFile(flagDBusConfigSession); err != nil {
log.Fatalf("cannot load session bus proxy config from %q: %s", flagDBusConfigSession, err)
} else {
config.SessionBus = conf
}
}
// 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 conf, err := dbus.NewConfigFromFile(flagDBusConfigSystem); err != nil {
log.Fatalf("cannot load system bus proxy config from %q: %s", flagDBusConfigSystem, err)
} else {
config.SystemBus = conf
}
}
// 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)
app.Main(ctx, std, config)
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(&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),
Flag(&flagPulse, "pulse", command.BoolFlag(false),
"Enable direct connection to PulseAudio")
}
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
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]
config, entry := tryShort(name)
if config == nil {
config = tryPath(name)
}
printShowInstance(os.Stdout, time.Now().UTC(), entry, config, flagShort, flagJSON)
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")
}
default:
log.Fatal("show requires 1 argument")
}
return errSuccess
}).Flag(&showFlagShort, "short", command.BoolFlag(false), "Omit filesystem information")
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")
{
var flagShort bool
c.NewCommand("ps", "List active instances", func(args []string) error {
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(std.Paths().RunDirPath.String()), flagShort, flagJSON)
return errSuccess
}).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id")
}
c.Command("version", "Display version information", func(args []string) error {
fmt.Println(internal.Version())
@@ -247,20 +246,3 @@ func buildCommand(out io.Writer) command.Command {
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

@@ -68,7 +68,7 @@ Flags:
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
out := new(bytes.Buffer)
c := buildCommand(out)
c := buildCommand(t.Context(), 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)

View File

@@ -4,10 +4,13 @@ package main
//go:generate cp ../../LICENSE .
import (
"context"
_ "embed"
"errors"
"log"
"os"
"os/signal"
"syscall"
"hakurei.app/container"
"hakurei.app/internal"
@@ -44,7 +47,11 @@ func main() {
log.Fatal("this program must not run as root")
}
buildCommand(os.Stderr).MustParse(os.Args[1:], func(err error) {
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer stop() // unreachable
buildCommand(ctx, os.Stderr).MustParse(os.Args[1:], func(err error) {
hlog.Verbosef("command returned %v", err)
if errors.Is(err, errSuccess) {
hlog.BeforeExit()

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"io"
"log"
"os"
"slices"
"strconv"
"strings"
@@ -14,7 +13,7 @@ import (
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/internal/sys"
"hakurei.app/system/dbus"
)
@@ -22,14 +21,9 @@ 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
info := &hst.Info{
Paths: std.Paths(),
User: sys.MustGetUserID(std),
}
if flagJSON {

View File

@@ -3,7 +3,6 @@ package container
import (
"encoding/gob"
"fmt"
"io/fs"
)
func init() { gob.Register(new(AutoEtcOp)) }
@@ -24,7 +23,7 @@ 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
@@ -32,10 +31,10 @@ func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
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()
@@ -44,12 +43,12 @@ func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
case "mtab":
if err = k.symlink(FHSProc+"mounts", target+n); err != nil {
return wrapErrSelf(err)
return err
}
default:
if err = k.symlink(rel+n, target+n); err != nil {
return wrapErrSelf(err)
return err
}
}
}

View File

@@ -2,14 +2,15 @@ package container
import (
"errors"
"io/fs"
"os"
"testing"
"hakurei.app/container/stub"
)
func TestAutoEtcOp(t *testing.T) {
t.Run("nonrepeatable", func(t *testing.T) {
wantErr := msg.WrapErr(fs.ErrInvalid, "autoetc is not repeatable")
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 +19,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 +42,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 +58,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 +100,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 +180,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},
})

View File

@@ -3,7 +3,6 @@ package container
import (
"encoding/gob"
"fmt"
"io/fs"
)
func init() { gob.Register(new(AutoRootOp)) }
@@ -30,7 +29,7 @@ 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))
for _, ent := range d {
@@ -53,7 +52,7 @@ 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

View File

@@ -2,14 +2,15 @@ package container
import (
"errors"
"io/fs"
"os"
"testing"
"hakurei.app/container/stub"
)
func TestAutoRootOp(t *testing.T) {
t.Run("nonrepeatable", func(t *testing.T) {
wantErr := msg.WrapErr(fs.ErrInvalid, "autoroot is not repeatable")
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)
}
@@ -19,100 +20,100 @@ func TestAutoRootOp(t *testing.T) {
{"readdir", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable,
}, []kexpect{
{"readdir", expectArgs{"/"}, stubDir(), errUnique},
}, wrapErrSelf(errUnique), nil, nil},
}, []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},
}, []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)},
}, []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("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr/bin"), MustAbs("/bin"), MustAbs("/bin"), BindWritable}}}, nil, nil),
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},
}, []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("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr/bin"), MustAbs("/bin"), MustAbs("/bin"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4004), false}, nil, nil),
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/home"), MustAbs("/home"), MustAbs("/home"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/home"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/home", "/sysroot/home", uintptr(0x4004), false}, nil, nil),
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/lib64"), MustAbs("/lib64"), MustAbs("/lib64"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/lib64"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/lib64", "/sysroot/lib64", uintptr(0x4004), false}, nil, nil),
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/lost+found"), MustAbs("/lost+found"), MustAbs("/lost+found"), BindWritable}}}, 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("bindMount", stub.ExpectArgs{"/host/lost+found", "/sysroot/lost+found", uintptr(0x4004), false}, nil, nil),
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/nix"), MustAbs("/nix"), MustAbs("/nix"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/nix"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", uintptr(0x4004), false}, nil, nil),
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/root"), MustAbs("/root"), MustAbs("/root"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/root"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/root", "/sysroot/root", uintptr(0x4004), false}, nil, nil),
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/run"), MustAbs("/run"), MustAbs("/run"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/run"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/run", "/sysroot/run", uintptr(0x4004), false}, nil, nil),
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/srv"), MustAbs("/srv"), MustAbs("/srv"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/srv"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/srv", "/sysroot/srv", uintptr(0x4004), false}, nil, nil),
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/sys"), MustAbs("/sys"), MustAbs("/sys"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/sys"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/sys", "/sysroot/sys", uintptr(0x4004), false}, nil, nil),
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr"), MustAbs("/usr"), MustAbs("/usr"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/usr"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/usr", "/sysroot/usr", uintptr(0x4004), false}, nil, nil),
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var"), MustAbs("/var"), MustAbs("/var"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/var"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/var", os.FileMode(0700)}, 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},
}, []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("verbosef", stub.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), 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("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
call("verbosef", stub.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), 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("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home", "/sysroot/home", uintptr(0x4005), false}, nil, nil),
call("verbosef", stub.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), 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("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64", "/sysroot/lib64", uintptr(0x4005), false}, nil, nil),
call("verbosef", stub.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), 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("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found", "/sysroot/lost+found", uintptr(0x4005), false}, nil, nil),
call("verbosef", stub.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), 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("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix", "/sysroot/nix", uintptr(0x4005), false}, nil, nil),
call("verbosef", stub.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), 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("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root", "/sysroot/root", uintptr(0x4005), false}, nil, nil),
call("verbosef", stub.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), 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("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run", "/sysroot/run", uintptr(0x4005), false}, nil, nil),
call("verbosef", stub.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), 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("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv", "/sysroot/srv", uintptr(0x4005), false}, nil, nil),
call("verbosef", stub.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), 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("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys", "/sysroot/sys", uintptr(0x4005), false}, nil, nil),
call("verbosef", stub.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), 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("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr", "/sysroot/usr", uintptr(0x4005), false}, nil, nil),
call("verbosef", stub.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), 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("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var", "/sysroot/var", uintptr(0x4005), false}, nil, nil),
}, nil},
})

View File

@@ -64,7 +64,7 @@ type (
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.
@@ -99,17 +99,66 @@ 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 {
switch {
case errors.As(e.Err, new(*os.PathError)),
errors.As(e.Err, new(*os.SyscallError)):
return "cannot " + e.Err.Error()
default:
return e.Err.Error()
}
}
if e.Origin {
return e.Step
}
return "cannot " + e.Error()
}
// 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")
}
// map to overflow id to work around ownership checks
if p.Uid < 1 {
@@ -131,9 +180,17 @@ 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) }
@@ -167,8 +224,7 @@ 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:")
return &StartError{true, "set up params stream", err, false, false}
} else {
p.setup = e
p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
@@ -183,8 +239,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,28 +255,24 @@ 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)
}
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)
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)
@@ -234,7 +285,7 @@ func (p *Container) Start() error {
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
}()
@@ -257,7 +308,7 @@ func (p *Container) Serve() error {
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
@@ -285,7 +336,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 +348,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))
@@ -312,7 +393,11 @@ 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)}}
p := &Container{ctx: ctx, Params: Params{Ops: new(Ops)}}
c, cancel := context.WithCancel(ctx)
p.cancel = cancel
p.cmd = exec.CommandContext(c, MustExecutable())
return p
}
// NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields.

View File

@@ -7,9 +7,11 @@ import (
"errors"
"fmt"
"log"
"net"
"os"
"os/exec"
"os/signal"
"reflect"
"strconv"
"strings"
"syscall"
@@ -26,6 +28,143 @@ import (
"hakurei.app/ldd"
)
func TestStartError(t *testing.T) {
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 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.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 := container.GetErrorMessage(tc.err); !ok {
if tc.msg != "" {
t.Errorf("GetErrorMessage: err does not implement MessageError")
}
return
} else if got != tc.msg {
t.Errorf("GetErrorMessage: %q, want %q", got, tc.msg)
}
})
})
}
}
const (
ignore = "\x00"
ignoreV = -1
@@ -75,7 +214,7 @@ var containerTestCases = []struct {
1000, 100, nil, 0, seccomp.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, []seccomp.NativeRule{{Syscall: seccomp.ScmpSyscall(syscall.SYS_SETUID), Errno: seccomp.ScmpErrno(syscall.EPERM)}}, 0, seccomp.PresetExt},
{"tmpfs", true, false, false, true,
earlyOps(new(container.Ops).
@@ -217,9 +356,11 @@ func TestContainer(t *testing.T) {
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")
@@ -233,7 +374,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 {
@@ -313,17 +456,26 @@ func TestContainer(t *testing.T) {
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)
}
}
})
}
@@ -376,11 +528,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()

View File

@@ -138,8 +138,6 @@ type syscallDispatcher interface {
resume() bool
// beforeExit provides [Msg.BeforeExit].
beforeExit()
// printBaseErr provides [Msg.PrintBaseErr].
printBaseErr(err error, fallback string)
}
// direct implements syscallDispatcher on the current kernel.
@@ -225,7 +223,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 +232,11 @@ 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(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() }

View File

@@ -2,7 +2,6 @@ package container
import (
"bytes"
"errors"
"io"
"io/fs"
"os"
@@ -11,16 +10,14 @@ import (
"runtime"
"slices"
"strings"
"sync"
"syscall"
"testing"
"time"
"hakurei.app/container/seccomp"
"hakurei.app/container/stub"
)
var errUnique = errors.New("unique error injected by the test suite")
type opValidTestCase struct {
name string
op Op
@@ -28,9 +25,15 @@ type opValidTestCase struct {
}
func checkOpsValid(t *testing.T, testCases []opValidTestCase) {
t.Helper()
t.Run("valid", func(t *testing.T) {
t.Helper()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Helper()
if got := tc.op.Valid(); got != tc.want {
t.Errorf("Valid: %v, want %v", got, tc.want)
}
@@ -46,9 +49,15 @@ type opsBuilderTestCase struct {
}
func checkOpsBuilder(t *testing.T, testCases []opsBuilderTestCase) {
t.Helper()
t.Run("build", func(t *testing.T) {
t.Helper()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Helper()
if !slices.EqualFunc(*tc.ops, tc.want, func(op Op, v Op) bool { return op.Is(v) }) {
t.Errorf("Ops: %#v, want %#v", tc.ops, tc.want)
}
@@ -64,9 +73,15 @@ type opIsTestCase struct {
}
func checkOpIs(t *testing.T, testCases []opIsTestCase) {
t.Helper()
t.Run("is", func(t *testing.T) {
t.Helper()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Helper()
if got := tc.op.Is(tc.v); got != tc.want {
t.Errorf("Is: %v, want %v", got, tc.want)
}
@@ -84,16 +99,26 @@ type opMetaTestCase struct {
}
func checkOpMeta(t *testing.T, testCases []opMetaTestCase) {
t.Helper()
t.Run("meta", func(t *testing.T) {
t.Helper()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Helper()
t.Run("prefix", func(t *testing.T) {
t.Helper()
if got := tc.op.prefix(); got != tc.wantPrefix {
t.Errorf("prefix: %q, want %q", got, tc.wantPrefix)
}
})
t.Run("string", func(t *testing.T) {
t.Helper()
if got := tc.op.String(); got != tc.wantString {
t.Errorf("String: %s, want %s", got, tc.wantString)
}
@@ -103,23 +128,36 @@ func checkOpMeta(t *testing.T, testCases []opMetaTestCase) {
})
}
// call initialises a [stub.Call].
// This keeps composites analysis happy without making the test cases too bloated.
func call(name string, args stub.ExpectArgs, ret any, err error) stub.Call {
return stub.Call{Name: name, Args: args, Ret: ret, Err: err}
}
type simpleTestCase struct {
name string
f func(k syscallDispatcher) error
want [][]kexpect
want stub.Expect
wantErr error
}
func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) {
t.Helper()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
defer handleExitStub()
k := &kstub{t: t, want: tc.want, wg: new(sync.WaitGroup)}
if err := tc.f(k); !errors.Is(err, tc.wantErr) {
t.Helper()
wait4signal := make(chan struct{})
k := &kstub{wait4signal, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, s} }, tc.want)}
defer stub.HandleExit(t)
if err := tc.f(k); !reflect.DeepEqual(err, tc.wantErr) {
t.Errorf("%s: error = %v, want %v", fname, err, tc.wantErr)
}
k.handleIncomplete(func(k *kstub) {
t.Errorf("%s: %d calls, want %d (track %d)", fname, k.pos, len(k.want[k.track]), k.track)
k.VisitIncomplete(func(s *stub.Stub[syscallDispatcher]) {
t.Helper()
t.Errorf("%s: %d calls, want %d", fname, s.Pos(), s.Len())
})
})
}
@@ -130,36 +168,45 @@ type opBehaviourTestCase struct {
params *Params
op Op
early []kexpect
early []stub.Call
wantErrEarly error
apply []kexpect
apply []stub.Call
wantErrApply error
}
func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
t.Helper()
t.Run("behaviour", func(t *testing.T) {
t.Helper()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
defer handleExitStub()
t.Helper()
state := &setupState{Params: tc.params}
k := &kstub{t: t, want: [][]kexpect{slices.Concat(tc.early, []kexpect{{name: "\x00"}}, tc.apply)}, wg: new(sync.WaitGroup)}
k := &kstub{nil, stub.New(t,
func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, s} },
stub.Expect{Calls: slices.Concat(tc.early, []stub.Call{{Name: stub.CallSeparator}}, tc.apply)},
)}
defer stub.HandleExit(t)
errEarly := tc.op.early(state, k)
k.expect("\x00")
if !errors.Is(errEarly, tc.wantErrEarly) {
k.Expects(stub.CallSeparator)
if !reflect.DeepEqual(errEarly, tc.wantErrEarly) {
t.Errorf("early: error = %v, want %v", errEarly, tc.wantErrEarly)
}
if errEarly != nil {
goto out
}
if err := tc.op.apply(state, k); !errors.Is(err, tc.wantErrApply) {
if err := tc.op.apply(state, k); !reflect.DeepEqual(err, tc.wantErrApply) {
t.Errorf("apply: error = %v, want %v", err, tc.wantErrApply)
}
out:
k.handleIncomplete(func(k *kstub) {
count := k.pos - 1 // separator
k.VisitIncomplete(func(s *stub.Stub[syscallDispatcher]) {
count := k.Pos() - 1 // separator
if count < len(tc.early) {
t.Errorf("early: %d calls, want %d", count, len(tc.early))
} else {
@@ -226,8 +273,6 @@ func (writeErrOsFile) Stat() (fs.FileInfo, error) { panic("unreachable") }
func (writeErrOsFile) Read([]byte) (int, error) { panic("unreachable") }
func (writeErrOsFile) Close() error { panic("unreachable") }
type expectArgs = [5]any
type isDirFi bool
func (isDirFi) Name() string { panic("unreachable") }
@@ -252,184 +297,94 @@ func (nameDentry) IsDir() bool { panic("unreachable") }
func (nameDentry) Type() fs.FileMode { panic("unreachable") }
func (nameDentry) Info() (fs.FileInfo, error) { panic("unreachable") }
type kexpect struct {
name string
args expectArgs
ret any
err error
}
func (k *kexpect) error(ok ...bool) error {
if !slices.Contains(ok, false) {
return k.err
}
return syscall.ENOTRECOVERABLE
}
func handleExitStub() {
r := recover()
if r == 0xdeadbeef {
return
}
if r != nil {
panic(r)
}
}
const (
// magicWait4Signal must be used in a single pair of signal and wait4 calls across two goroutines
// originating from the same toplevel kstub.
// To enable this behaviour this value must be the last element of the args field in the wait4 call
// and the ret value of the signal call.
magicWait4Signal = 0xdef
)
type kstub struct {
t *testing.T
want [][]kexpect
// pos is the current position in want[track].
pos int
// track is the current active want.
track int
// sub stores addresses of kstub created by new.
sub []*kstub
// wg waits for all descendants to complete.
wg *sync.WaitGroup
wait4signal chan struct{}
*stub.Stub[syscallDispatcher]
}
// handleIncomplete calls f on an incomplete k and all its descendants.
func (k *kstub) handleIncomplete(f func(k *kstub)) {
k.wg.Wait()
func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) }
if k.want != nil && len(k.want[k.track]) != k.pos {
f(k)
}
for _, sk := range k.sub {
sk.handleIncomplete(f)
}
}
// expect checks name and returns the current kexpect and advances pos.
func (k *kstub) expect(name string) (expect *kexpect) {
if len(k.want[k.track]) == k.pos {
k.t.Fatal("expect: want too short")
}
expect = &k.want[k.track][k.pos]
if name != expect.name {
if expect.name == "\x00" {
k.t.Fatalf("expect: func = %s, separator overrun", name)
}
if name == "\x00" {
k.t.Fatalf("expect: separator, want %s", expect.name)
}
k.t.Fatalf("expect: func = %s, want %s", name, expect.name)
}
k.pos++
return
}
// checkArg checks an argument comparable with the == operator. Avoid using this with pointers.
func checkArg[T comparable](k *kstub, arg string, got T, n int) bool {
if k.pos == 0 {
panic("invalid call to checkArg")
}
expect := k.want[k.track][k.pos-1]
want, ok := expect.args[n].(T)
if !ok || got != want {
k.t.Errorf("%s: %s = %#v, want %#v (%d)", expect.name, arg, got, want, k.pos-1)
return false
}
return true
}
// checkArgReflect checks an argument of any type.
func checkArgReflect(k *kstub, arg string, got any, n int) bool {
if k.pos == 0 {
panic("invalid call to checkArgReflect")
}
expect := k.want[k.track][k.pos-1]
want := expect.args[n]
if !reflect.DeepEqual(got, want) {
k.t.Errorf("%s: %s = %#v, want %#v (%d)", expect.name, arg, got, want, k.pos-1)
return false
}
return true
}
func (k *kstub) new(f func(k syscallDispatcher)) {
k.expect("new")
if len(k.want) <= k.track+1 {
k.t.Fatalf("new: track overrun")
}
sk := &kstub{t: k.t, want: k.want, track: len(k.sub) + 1, wg: k.wg}
k.sub = append(k.sub, sk)
k.wg.Add(1)
go func() {
defer k.wg.Done()
defer handleExitStub()
f(sk)
}()
}
func (k *kstub) lockOSThread() { k.expect("lockOSThread") }
func (k *kstub) lockOSThread() { k.Helper(); k.Expects("lockOSThread") }
func (k *kstub) setPtracer(pid uintptr) error {
return k.expect("setPtracer").error(
checkArg(k, "pid", pid, 0))
k.Helper()
return k.Expects("setPtracer").Error(
stub.CheckArg(k.Stub, "pid", pid, 0))
}
func (k *kstub) setDumpable(dumpable uintptr) error {
return k.expect("setDumpable").error(
checkArg(k, "dumpable", dumpable, 0))
k.Helper()
return k.Expects("setDumpable").Error(
stub.CheckArg(k.Stub, "dumpable", dumpable, 0))
}
func (k *kstub) setNoNewPrivs() error { return k.expect("setNoNewPrivs").err }
func (k *kstub) lastcap() uintptr { return k.expect("lastcap").ret.(uintptr) }
func (k *kstub) setNoNewPrivs() error { k.Helper(); return k.Expects("setNoNewPrivs").Err }
func (k *kstub) lastcap() uintptr { k.Helper(); return k.Expects("lastcap").Ret.(uintptr) }
func (k *kstub) capset(hdrp *capHeader, datap *[2]capData) error {
return k.expect("capset").error(
checkArgReflect(k, "hdrp", hdrp, 0),
checkArgReflect(k, "datap", datap, 1))
k.Helper()
return k.Expects("capset").Error(
stub.CheckArgReflect(k.Stub, "hdrp", hdrp, 0),
stub.CheckArgReflect(k.Stub, "datap", datap, 1))
}
func (k *kstub) capBoundingSetDrop(cap uintptr) error {
return k.expect("capBoundingSetDrop").error(
checkArg(k, "cap", cap, 0))
k.Helper()
return k.Expects("capBoundingSetDrop").Error(
stub.CheckArg(k.Stub, "cap", cap, 0))
}
func (k *kstub) capAmbientClearAll() error { return k.expect("capAmbientClearAll").err }
func (k *kstub) capAmbientClearAll() error { k.Helper(); return k.Expects("capAmbientClearAll").Err }
func (k *kstub) capAmbientRaise(cap uintptr) error {
return k.expect("capAmbientRaise").error(
checkArg(k, "cap", cap, 0))
k.Helper()
return k.Expects("capAmbientRaise").Error(
stub.CheckArg(k.Stub, "cap", cap, 0))
}
func (k *kstub) isatty(fd int) bool {
expect := k.expect("isatty")
if !checkArg(k, "fd", fd, 0) {
k.t.FailNow()
k.Helper()
expect := k.Expects("isatty")
if !stub.CheckArg(k.Stub, "fd", fd, 0) {
k.FailNow()
}
return expect.ret.(bool)
return expect.Ret.(bool)
}
func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) {
expect := k.expect("receive")
k.Helper()
expect := k.Expects("receive")
var closed bool
closeFunc = func() error {
if closed {
k.t.Error("closeFunc called more than once")
k.Error("closeFunc called more than once")
return os.ErrClosed
}
closed = true
if expect.ret != nil {
if expect.Ret != nil {
// use return stored in kexpect for closeFunc instead
return expect.ret.(error)
return expect.Ret.(error)
}
return nil
}
err = expect.error(
checkArg(k, "key", key, 0),
checkArgReflect(k, "e", e, 1),
checkArgReflect(k, "fdp", fdp, 2))
err = expect.Error(
stub.CheckArg(k.Stub, "key", key, 0),
stub.CheckArgReflect(k.Stub, "e", e, 1),
stub.CheckArgReflect(k.Stub, "fdp", fdp, 2))
// 3 is unused so stores params
if expect.args[3] != nil {
if v, ok := expect.args[3].(*initParams); ok && v != nil {
if expect.Args[3] != nil {
if v, ok := expect.Args[3].(*initParams); ok && v != nil {
if p, ok0 := e.(*initParams); ok0 && p != nil {
*p = *v
}
@@ -437,8 +392,8 @@ func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error
}
// 4 is unused so stores fd
if expect.args[4] != nil {
if v, ok := expect.args[4].(uintptr); ok && v >= 3 {
if expect.Args[4] != nil {
if v, ok := expect.Args[4].(uintptr); ok && v >= 3 {
if fdp != nil {
*fdp = v
}
@@ -449,246 +404,291 @@ func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error
}
func (k *kstub) bindMount(source, target string, flags uintptr, eq bool) error {
return k.expect("bindMount").error(
checkArg(k, "source", source, 0),
checkArg(k, "target", target, 1),
checkArg(k, "flags", flags, 2),
checkArg(k, "eq", eq, 3))
k.Helper()
return k.Expects("bindMount").Error(
stub.CheckArg(k.Stub, "source", source, 0),
stub.CheckArg(k.Stub, "target", target, 1),
stub.CheckArg(k.Stub, "flags", flags, 2),
stub.CheckArg(k.Stub, "eq", eq, 3))
}
func (k *kstub) remount(target string, flags uintptr) error {
return k.expect("remount").error(
checkArg(k, "target", target, 0),
checkArg(k, "flags", flags, 1))
k.Helper()
return k.Expects("remount").Error(
stub.CheckArg(k.Stub, "target", target, 0),
stub.CheckArg(k.Stub, "flags", flags, 1))
}
func (k *kstub) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
return k.expect("mountTmpfs").error(
checkArg(k, "fsname", fsname, 0),
checkArg(k, "target", target, 1),
checkArg(k, "flags", flags, 2),
checkArg(k, "size", size, 3),
checkArg(k, "perm", perm, 4))
k.Helper()
return k.Expects("mountTmpfs").Error(
stub.CheckArg(k.Stub, "fsname", fsname, 0),
stub.CheckArg(k.Stub, "target", target, 1),
stub.CheckArg(k.Stub, "flags", flags, 2),
stub.CheckArg(k.Stub, "size", size, 3),
stub.CheckArg(k.Stub, "perm", perm, 4))
}
func (k *kstub) ensureFile(name string, perm, pperm os.FileMode) error {
return k.expect("ensureFile").error(
checkArg(k, "name", name, 0),
checkArg(k, "perm", perm, 1),
checkArg(k, "pperm", pperm, 2))
k.Helper()
return k.Expects("ensureFile").Error(
stub.CheckArg(k.Stub, "name", name, 0),
stub.CheckArg(k.Stub, "perm", perm, 1),
stub.CheckArg(k.Stub, "pperm", pperm, 2))
}
func (k *kstub) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error {
return k.expect("seccompLoad").error(
checkArgReflect(k, "rules", rules, 0),
checkArg(k, "flags", flags, 1))
k.Helper()
return k.Expects("seccompLoad").Error(
stub.CheckArgReflect(k.Stub, "rules", rules, 0),
stub.CheckArg(k.Stub, "flags", flags, 1))
}
func (k *kstub) notify(c chan<- os.Signal, sig ...os.Signal) {
expect := k.expect("notify")
if c == nil || expect.error(
checkArgReflect(k, "sig", sig, 1)) != nil {
k.t.FailNow()
k.Helper()
expect := k.Expects("notify")
if c == nil || expect.Error(
stub.CheckArgReflect(k.Stub, "sig", sig, 1)) != nil {
k.FailNow()
}
// export channel for external instrumentation
if chanf, ok := expect.args[0].(func(c chan<- os.Signal)); ok && chanf != nil {
if chanf, ok := expect.Args[0].(func(c chan<- os.Signal)); ok && chanf != nil {
chanf(c)
}
}
func (k *kstub) start(c *exec.Cmd) error {
expect := k.expect("start")
err := expect.error(
checkArg(k, "c.Path", c.Path, 0),
checkArgReflect(k, "c.Args", c.Args, 1),
checkArgReflect(k, "c.Env", c.Env, 2),
checkArg(k, "c.Dir", c.Dir, 3))
k.Helper()
expect := k.Expects("start")
err := expect.Error(
stub.CheckArg(k.Stub, "c.Path", c.Path, 0),
stub.CheckArgReflect(k.Stub, "c.Args", c.Args, 1),
stub.CheckArgReflect(k.Stub, "c.Env", c.Env, 2),
stub.CheckArg(k.Stub, "c.Dir", c.Dir, 3))
if process, ok := expect.ret.(*os.Process); ok && process != nil {
if process, ok := expect.Ret.(*os.Process); ok && process != nil {
c.Process = process
}
return err
}
func (k *kstub) signal(c *exec.Cmd, sig os.Signal) error {
return k.expect("signal").error(
checkArg(k, "c.Path", c.Path, 0),
checkArgReflect(k, "c.Args", c.Args, 1),
checkArgReflect(k, "c.Env", c.Env, 2),
checkArg(k, "c.Dir", c.Dir, 3),
checkArg(k, "sig", sig, 4))
k.Helper()
expect := k.Expects("signal")
if v, ok := expect.Ret.(int); ok && v == magicWait4Signal {
if k.wait4signal == nil {
panic("kstub not initialised for wait4 simulation")
}
defer func() { close(k.wait4signal) }()
}
return expect.Error(
stub.CheckArg(k.Stub, "c.Path", c.Path, 0),
stub.CheckArgReflect(k.Stub, "c.Args", c.Args, 1),
stub.CheckArgReflect(k.Stub, "c.Env", c.Env, 2),
stub.CheckArg(k.Stub, "c.Dir", c.Dir, 3),
stub.CheckArg(k.Stub, "sig", sig, 4))
}
func (k *kstub) evalSymlinks(path string) (string, error) {
expect := k.expect("evalSymlinks")
return expect.ret.(string), expect.error(
checkArg(k, "path", path, 0))
k.Helper()
expect := k.Expects("evalSymlinks")
return expect.Ret.(string), expect.Error(
stub.CheckArg(k.Stub, "path", path, 0))
}
func (k *kstub) exit(code int) {
k.expect("exit")
if !checkArg(k, "code", code, 0) {
k.t.FailNow()
k.Helper()
k.Expects("exit")
if !stub.CheckArg(k.Stub, "code", code, 0) {
k.FailNow()
}
panic(0xdeadbeef)
panic(stub.PanicExit)
}
func (k *kstub) getpid() int { return k.expect("getpid").ret.(int) }
func (k *kstub) getpid() int { k.Helper(); return k.Expects("getpid").Ret.(int) }
func (k *kstub) stat(name string) (os.FileInfo, error) {
expect := k.expect("stat")
return expect.ret.(os.FileInfo), expect.error(
checkArg(k, "name", name, 0))
k.Helper()
expect := k.Expects("stat")
return expect.Ret.(os.FileInfo), expect.Error(
stub.CheckArg(k.Stub, "name", name, 0))
}
func (k *kstub) mkdir(name string, perm os.FileMode) error {
return k.expect("mkdir").error(
checkArg(k, "name", name, 0),
checkArg(k, "perm", perm, 1))
k.Helper()
return k.Expects("mkdir").Error(
stub.CheckArg(k.Stub, "name", name, 0),
stub.CheckArg(k.Stub, "perm", perm, 1))
}
func (k *kstub) mkdirTemp(dir, pattern string) (string, error) {
expect := k.expect("mkdirTemp")
return expect.ret.(string), expect.error(
checkArg(k, "dir", dir, 0),
checkArg(k, "pattern", pattern, 1))
k.Helper()
expect := k.Expects("mkdirTemp")
return expect.Ret.(string), expect.Error(
stub.CheckArg(k.Stub, "dir", dir, 0),
stub.CheckArg(k.Stub, "pattern", pattern, 1))
}
func (k *kstub) mkdirAll(path string, perm os.FileMode) error {
return k.expect("mkdirAll").error(
checkArg(k, "path", path, 0),
checkArg(k, "perm", perm, 1))
k.Helper()
return k.Expects("mkdirAll").Error(
stub.CheckArg(k.Stub, "path", path, 0),
stub.CheckArg(k.Stub, "perm", perm, 1))
}
func (k *kstub) readdir(name string) ([]os.DirEntry, error) {
expect := k.expect("readdir")
return expect.ret.([]os.DirEntry), expect.error(
checkArg(k, "name", name, 0))
k.Helper()
expect := k.Expects("readdir")
return expect.Ret.([]os.DirEntry), expect.Error(
stub.CheckArg(k.Stub, "name", name, 0))
}
func (k *kstub) openNew(name string) (osFile, error) {
expect := k.expect("openNew")
return expect.ret.(osFile), expect.error(
checkArg(k, "name", name, 0))
k.Helper()
expect := k.Expects("openNew")
return expect.Ret.(osFile), expect.Error(
stub.CheckArg(k.Stub, "name", name, 0))
}
func (k *kstub) writeFile(name string, data []byte, perm os.FileMode) error {
return k.expect("writeFile").error(
checkArg(k, "name", name, 0),
checkArgReflect(k, "data", data, 1),
checkArg(k, "perm", perm, 2))
k.Helper()
return k.Expects("writeFile").Error(
stub.CheckArg(k.Stub, "name", name, 0),
stub.CheckArgReflect(k.Stub, "data", data, 1),
stub.CheckArg(k.Stub, "perm", perm, 2))
}
func (k *kstub) createTemp(dir, pattern string) (osFile, error) {
expect := k.expect("createTemp")
return expect.ret.(osFile), expect.error(
checkArg(k, "dir", dir, 0),
checkArg(k, "pattern", pattern, 1))
k.Helper()
expect := k.Expects("createTemp")
return expect.Ret.(osFile), expect.Error(
stub.CheckArg(k.Stub, "dir", dir, 0),
stub.CheckArg(k.Stub, "pattern", pattern, 1))
}
func (k *kstub) remove(name string) error {
return k.expect("remove").error(
checkArg(k, "name", name, 0))
k.Helper()
return k.Expects("remove").Error(
stub.CheckArg(k.Stub, "name", name, 0))
}
func (k *kstub) newFile(fd uintptr, name string) *os.File {
expect := k.expect("newFile")
if expect.error(
checkArg(k, "fd", fd, 0),
checkArg(k, "name", name, 1)) != nil {
k.t.FailNow()
k.Helper()
expect := k.Expects("newFile")
if expect.Error(
stub.CheckArg(k.Stub, "fd", fd, 0),
stub.CheckArg(k.Stub, "name", name, 1)) != nil {
k.FailNow()
}
return expect.ret.(*os.File)
return expect.Ret.(*os.File)
}
func (k *kstub) symlink(oldname, newname string) error {
return k.expect("symlink").error(
checkArg(k, "oldname", oldname, 0),
checkArg(k, "newname", newname, 1))
k.Helper()
return k.Expects("symlink").Error(
stub.CheckArg(k.Stub, "oldname", oldname, 0),
stub.CheckArg(k.Stub, "newname", newname, 1))
}
func (k *kstub) readlink(name string) (string, error) {
expect := k.expect("readlink")
return expect.ret.(string), expect.error(
checkArg(k, "name", name, 0))
k.Helper()
expect := k.Expects("readlink")
return expect.Ret.(string), expect.Error(
stub.CheckArg(k.Stub, "name", name, 0))
}
func (k *kstub) umask(mask int) (oldmask int) {
expect := k.expect("umask")
if !checkArg(k, "mask", mask, 0) {
k.t.FailNow()
k.Helper()
expect := k.Expects("umask")
if !stub.CheckArg(k.Stub, "mask", mask, 0) {
k.FailNow()
}
return expect.ret.(int)
return expect.Ret.(int)
}
func (k *kstub) sethostname(p []byte) (err error) {
return k.expect("sethostname").error(
checkArgReflect(k, "p", p, 0))
k.Helper()
return k.Expects("sethostname").Error(
stub.CheckArgReflect(k.Stub, "p", p, 0))
}
func (k *kstub) chdir(path string) (err error) {
return k.expect("chdir").error(
checkArg(k, "path", path, 0))
k.Helper()
return k.Expects("chdir").Error(
stub.CheckArg(k.Stub, "path", path, 0))
}
func (k *kstub) fchdir(fd int) (err error) {
return k.expect("fchdir").error(
checkArg(k, "fd", fd, 0))
k.Helper()
return k.Expects("fchdir").Error(
stub.CheckArg(k.Stub, "fd", fd, 0))
}
func (k *kstub) open(path string, mode int, perm uint32) (fd int, err error) {
expect := k.expect("open")
return expect.ret.(int), expect.error(
checkArg(k, "path", path, 0),
checkArg(k, "mode", mode, 1),
checkArg(k, "perm", perm, 2))
k.Helper()
expect := k.Expects("open")
return expect.Ret.(int), expect.Error(
stub.CheckArg(k.Stub, "path", path, 0),
stub.CheckArg(k.Stub, "mode", mode, 1),
stub.CheckArg(k.Stub, "perm", perm, 2))
}
func (k *kstub) close(fd int) (err error) {
return k.expect("close").error(
checkArg(k, "fd", fd, 0))
k.Helper()
return k.Expects("close").Error(
stub.CheckArg(k.Stub, "fd", fd, 0))
}
func (k *kstub) pivotRoot(newroot, putold string) (err error) {
return k.expect("pivotRoot").error(
checkArg(k, "newroot", newroot, 0),
checkArg(k, "putold", putold, 1))
k.Helper()
return k.Expects("pivotRoot").Error(
stub.CheckArg(k.Stub, "newroot", newroot, 0),
stub.CheckArg(k.Stub, "putold", putold, 1))
}
func (k *kstub) mount(source, target, fstype string, flags uintptr, data string) (err error) {
return k.expect("mount").error(
checkArg(k, "source", source, 0),
checkArg(k, "target", target, 1),
checkArg(k, "fstype", fstype, 2),
checkArg(k, "flags", flags, 3),
checkArg(k, "data", data, 4))
k.Helper()
return k.Expects("mount").Error(
stub.CheckArg(k.Stub, "source", source, 0),
stub.CheckArg(k.Stub, "target", target, 1),
stub.CheckArg(k.Stub, "fstype", fstype, 2),
stub.CheckArg(k.Stub, "flags", flags, 3),
stub.CheckArg(k.Stub, "data", data, 4))
}
func (k *kstub) unmount(target string, flags int) (err error) {
return k.expect("unmount").error(
checkArg(k, "target", target, 0),
checkArg(k, "flags", flags, 1))
k.Helper()
return k.Expects("unmount").Error(
stub.CheckArg(k.Stub, "target", target, 0),
stub.CheckArg(k.Stub, "flags", flags, 1))
}
func (k *kstub) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error) {
expect := k.expect("wait4")
// special case to prevent leaking the wait4 goroutine when testing initEntrypoint
if v, ok := expect.args[4].(int); ok && v == 0xdeadbeef {
k.t.Log("terminating current goroutine as requested by kexpect")
panic(0xdeadbeef)
k.Helper()
expect := k.Expects("wait4")
if v, ok := expect.Args[4].(int); ok {
switch v {
case stub.PanicExit: // special case to prevent leaking the wait4 goroutine while testing initEntrypoint
panic(stub.PanicExit)
case magicWait4Signal: // block until corresponding signal call
if k.wait4signal == nil {
panic("kstub not initialised for wait4 simulation")
}
<-k.wait4signal
}
}
wpid = expect.ret.(int)
err = expect.error(
checkArg(k, "pid", pid, 0),
checkArg(k, "options", options, 2))
wpid = expect.Ret.(int)
err = expect.Error(
stub.CheckArg(k.Stub, "pid", pid, 0),
stub.CheckArg(k.Stub, "options", options, 2))
if wstatusV, ok := expect.args[1].(syscall.WaitStatus); wstatus != nil && ok {
if wstatusV, ok := expect.Args[1].(syscall.WaitStatus); wstatus != nil && ok {
*wstatus = wstatusV
}
if rusageV, ok := expect.args[3].(syscall.Rusage); rusage != nil && ok {
if rusageV, ok := expect.Args[3].(syscall.Rusage); rusage != nil && ok {
*rusage = rusageV
}
@@ -696,53 +696,50 @@ func (k *kstub) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage
}
func (k *kstub) printf(format string, v ...any) {
if k.expect("printf").error(
checkArg(k, "format", format, 0),
checkArgReflect(k, "v", v, 1)) != nil {
k.t.FailNow()
k.Helper()
if k.Expects("printf").Error(
stub.CheckArg(k.Stub, "format", format, 0),
stub.CheckArgReflect(k.Stub, "v", v, 1)) != nil {
k.FailNow()
}
}
func (k *kstub) fatal(v ...any) {
if k.expect("fatal").error(
checkArgReflect(k, "v", v, 0)) != nil {
k.t.FailNow()
k.Helper()
if k.Expects("fatal").Error(
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil {
k.FailNow()
}
panic(0xdeadbeef)
panic(stub.PanicExit)
}
func (k *kstub) fatalf(format string, v ...any) {
if k.expect("fatalf").error(
checkArg(k, "format", format, 0),
checkArgReflect(k, "v", v, 1)) != nil {
k.t.FailNow()
k.Helper()
if k.Expects("fatalf").Error(
stub.CheckArg(k.Stub, "format", format, 0),
stub.CheckArgReflect(k.Stub, "v", v, 1)) != nil {
k.FailNow()
}
panic(0xdeadbeef)
panic(stub.PanicExit)
}
func (k *kstub) verbose(v ...any) {
if k.expect("verbose").error(
checkArgReflect(k, "v", v, 0)) != nil {
k.t.FailNow()
k.Helper()
if k.Expects("verbose").Error(
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil {
k.FailNow()
}
}
func (k *kstub) verbosef(format string, v ...any) {
if k.expect("verbosef").error(
checkArg(k, "format", format, 0),
checkArgReflect(k, "v", v, 1)) != nil {
k.t.FailNow()
k.Helper()
if k.Expects("verbosef").Error(
stub.CheckArg(k.Stub, "format", format, 0),
stub.CheckArgReflect(k.Stub, "v", v, 1)) != nil {
k.FailNow()
}
}
func (k *kstub) suspend() { k.expect("suspend") }
func (k *kstub) resume() bool { return k.expect("resume").ret.(bool) }
func (k *kstub) beforeExit() { k.expect("beforeExit") }
func (k *kstub) printBaseErr(err error, fallback string) {
if k.expect("printBaseErr").error(
checkArgReflect(k, "err", err, 0),
checkArg(k, "fallback", fallback, 1)) != nil {
k.t.FailNow()
}
}
func (k *kstub) suspend() { k.Helper(); k.Expects("suspend") }
func (k *kstub) resume() bool { k.Helper(); return k.Expects("resume").Ret.(bool) }
func (k *kstub) beforeExit() { k.Helper(); k.Expects("beforeExit") }

112
container/errors.go Normal file
View File

@@ -0,0 +1,112 @@
package container
import (
"errors"
"os"
"syscall"
"hakurei.app/container/vfs"
)
// messageFromError returns a printable error message for a supported concrete type.
func messageFromError(err error) (string, bool) {
if m, ok := messagePrefixP[MountError]("cannot ", err); ok {
return m, ok
}
if m, ok := messagePrefixP[os.PathError]("cannot ", err); ok {
return m, ok
}
if m, ok := messagePrefixP[AbsoluteError]("", err); ok {
return m, ok
}
if m, ok := messagePrefix[OpRepeatError]("", err); ok {
return m, ok
}
if m, ok := messagePrefix[OpStateError]("", err); ok {
return m, ok
}
if m, ok := messagePrefixP[vfs.DecoderError]("cannot ", err); ok {
return m, ok
}
if m, ok := messagePrefix[TmpfsSizeError]("", err); ok {
return m, ok
}
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
}
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) 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()
}
// 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}
}
}

168
container/errors_test.go Normal file
View File

@@ -0,0 +1,168 @@
package container
import (
"errors"
"os"
"reflect"
"strconv"
"syscall"
"testing"
"hakurei.app/container/stub"
"hakurei.app/container/vfs"
)
func TestMessageFromError(t *testing.T) {
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", &AbsoluteError{"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: 0xdeadbeef, Err: &strconv.NumError{Func: "Atoi", Num: "meow", Err: strconv.ErrSyntax}},
`cannot parse mountinfo at line 3735928559: 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) {
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) {
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.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) {
if errors.Is(new(MountError), syscall.Errno(0)) {
t.Errorf("Is: zero MountError unexpected true")
}
})
}
func TestErrnoFallback(t *testing.T) {
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) {
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

@@ -68,6 +68,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
@@ -106,7 +116,7 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
if errors.Is(err, EBADF) {
k.fatal("invalid setup descriptor")
}
if errors.Is(err, ErrNotSet) {
if errors.Is(err, ErrReceiveEnv) {
k.fatal("HAKUREI_SETUP not set")
}
@@ -174,10 +184,11 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
}
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(m)
} else {
k.fatalf("cannot prepare op at index %d: %v", i, err)
}
}
}
@@ -214,10 +225,11 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
// ops already checked during early setup
k.verbosef("%s %s", op.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(m)
} else {
k.fatalf("cannot apply op at index %d: %v", i, err)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -43,7 +43,7 @@ func (b *BindMountOp) Valid() bool {
func (b *BindMountOp) early(_ *setupState, k syscallDispatcher) error {
if b.Flags&BindEnsure != 0 {
if err := k.mkdirAll(b.Source.String(), 0700); err != nil {
return wrapErrSelf(err)
return err
}
}
@@ -52,7 +52,7 @@ func (b *BindMountOp) early(_ *setupState, k syscallDispatcher) error {
// leave sourceFinal as nil
return nil
}
return wrapErrSelf(err)
return err
} else {
b.sourceFinal, err = NewAbs(pathname)
return err
@@ -63,7 +63,7 @@ func (b *BindMountOp) apply(_ *setupState, k syscallDispatcher) error {
if b.sourceFinal == nil {
if b.Flags&BindOptional == 0 {
// unreachable
return msg.WrapErr(os.ErrClosed, "impossible bind state reached")
return OpStateError("bind")
}
return nil
}
@@ -74,10 +74,10 @@ 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

View File

@@ -5,6 +5,8 @@ import (
"os"
"syscall"
"testing"
"hakurei.app/container/stub"
)
func TestBindMountOp(t *testing.T) {
@@ -12,138 +14,138 @@ func TestBindMountOp(t *testing.T) {
{"ENOENT not optional", new(Params), &BindMountOp{
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/bin/"}, "", syscall.ENOENT},
}, wrapErrSelf(syscall.ENOENT), nil, nil},
}, []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},
}, []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},
}, []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("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},
}, []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},
}, []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},
}, []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("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},
}, []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("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},
}, []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("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},
}, []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)},
}, []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)},
}, []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},
}, []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("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, stub.UniqueError(0)),
}, stub.UniqueError(0)},
{"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},
}, []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("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
}, nil},
})
t.Run("unreachable", func(t *testing.T) {
t.Run("nil sourceFinal not optional", func(t *testing.T) {
wantErr := msg.WrapErr(os.ErrClosed, "impossible bind state reached")
wantErr := OpStateError("bind")
if err := new(BindMountOp).apply(nil, nil); !errors.Is(err, wantErr) {
t.Errorf("apply: error = %v, want %v", err, wantErr)
}

View File

@@ -59,7 +59,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
FHSProc+"self/fd/"+string(rune(i+'0')),
path.Join(target, name),
); err != nil {
return wrapErrSelf(err)
return err
}
}
for _, pair := range [][2]string{
@@ -68,7 +68,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
{"pts/ptmx", "ptmx"},
} {
if err := k.symlink(pair[0], path.Join(target, pair[1])); err != nil {
return wrapErrSelf(err)
return err
}
}
@@ -76,14 +76,13 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
devPtsPath := path.Join(target, "pts")
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 {
@@ -93,7 +92,7 @@ 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(
toHost(name),
consolePath,
@@ -108,10 +107,10 @@ 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
}
}
@@ -120,8 +119,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
}
if err := k.remount(target, MS_RDONLY); err != nil {
return wrapErrSuffix(k.remount(target, MS_RDONLY),
fmt.Sprintf("cannot remount %q:", target))
return err
}
return k.mountTmpfs(SourceTmpfs, devShmPath, MS_NOSUID|MS_NODEV, 0, 01777)
}

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@ type MkdirOp struct {
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) Is(op Op) bool {

View File

@@ -3,6 +3,8 @@ package container
import (
"os"
"testing"
"hakurei.app/container/stub"
)
func TestMkdirOp(t *testing.T) {
@@ -10,8 +12,8 @@ func TestMkdirOp(t *testing.T) {
{"success", new(Params), &MkdirOp{
Path: 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},
})

View File

@@ -3,7 +3,6 @@ package container
import (
"encoding/gob"
"fmt"
"io/fs"
"slices"
"strings"
)
@@ -19,6 +18,39 @@ 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 {
*f = append(*f, &MountOverlayOp{
@@ -89,7 +121,7 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
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,12 +129,12 @@ 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))
}
@@ -110,7 +142,7 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
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))
}
@@ -120,7 +152,7 @@ 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))
}
@@ -134,17 +166,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)
return err
}
if o.work, err = k.mkdirTemp(FHSRoot, intermediatePatternOverlayWork); err != nil {
return wrapErrSelf(err)
return err
}
}
@@ -152,12 +184,12 @@ 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,
@@ -167,8 +199,7 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
OptionOverlayLowerdir+"="+strings.Join(o.lower, 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, SpecialOverlayOption))
}
func (o *MountOverlayOp) Is(op Op) bool {

View File

@@ -2,12 +2,40 @@ package container
import (
"errors"
"io/fs"
"os"
"testing"
"hakurei.app/container/stub"
)
func TestMountOverlayOp(t *testing.T) {
t.Run("argument error", func(t *testing.T) {
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) {
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("/"),
@@ -16,7 +44,7 @@ func TestMountOverlayOp(t *testing.T) {
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},
}, nil, &OverlayArgumentError{OverlayEphemeralUnexpectedUpper, "/proc/"}, nil, nil},
{"mkdirTemp upper ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
Target: MustAbs("/"),
@@ -25,13 +53,13 @@ func TestMountOverlayOp(t *testing.T) {
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)},
}, []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("/"),
@@ -40,14 +68,14 @@ func TestMountOverlayOp(t *testing.T) {
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)},
}, []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("/"),
@@ -56,20 +84,20 @@ func TestMountOverlayOp(t *testing.T) {
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), "" +
}, []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{
@@ -77,11 +105,11 @@ func TestMountOverlayOp(t *testing.T) {
Lower: []*Absolute{
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"),
@@ -90,16 +118,16 @@ func TestMountOverlayOp(t *testing.T) {
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{
@@ -108,102 +136,102 @@ func TestMountOverlayOp(t *testing.T) {
MustAbs("/mnt-root/nix/.ro-store"),
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")},
}, []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},
}, []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},
}, []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},
}, []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)},
}, []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":`)},
}, []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), "" +
}, []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{
@@ -217,17 +245,17 @@ func TestMountOverlayOp(t *testing.T) {
},
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), "" +
}, []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,13 +264,13 @@ 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.Run("nil Upper non-nil Work not ephemeral", func(t *testing.T) {
wantErr := msg.WrapErr(fs.ErrClosed, "impossible overlay state reached")
wantErr := OpStateError("overlay")
if err := (&MountOverlayOp{
Work: MustAbs("/"),
}).early(nil, nil); !errors.Is(err, wantErr) {

View File

@@ -39,13 +39,11 @@ 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)
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()
}
@@ -61,7 +59,7 @@ func (t *TmpfileOp) apply(state *setupState, k syscallDispatcher) error {
); err != nil {
return err
} else if err = k.remove(tmpPath); err != nil {
return wrapErrSelf(err)
return err
}
return nil
}

View File

@@ -3,6 +3,8 @@ package container
import (
"os"
"testing"
"hakurei.app/container/stub"
)
func TestTmpfileOp(t *testing.T) {
@@ -16,59 +18,59 @@ func TestTmpfileOp(t *testing.T) {
{"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},
})

View File

@@ -22,10 +22,9 @@ 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) Is(op Op) bool {

View File

@@ -3,6 +3,8 @@ package container
import (
"os"
"testing"
"hakurei.app/container/stub"
)
func TestMountProcOp(t *testing.T) {
@@ -10,16 +12,16 @@ func TestMountProcOp(t *testing.T) {
{"mkdir", &Params{ParentPerm: 0755},
&MountProcOp{
Target: MustAbs("/proc/"),
}, nil, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, errUnique},
}, wrapErrSelf(errUnique)},
}, 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},
}, 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},
})

View File

@@ -22,8 +22,7 @@ type RemountOp struct {
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))
return k.remount(toSysroot(r.Target.String()), r.Flags)
}
func (r *RemountOp) Is(op Op) bool {

View File

@@ -3,6 +3,8 @@ package container
import (
"syscall"
"testing"
"hakurei.app/container/stub"
)
func TestRemountOp(t *testing.T) {
@@ -10,8 +12,8 @@ func TestRemountOp(t *testing.T) {
{"success", new(Params), &RemountOp{
Target: 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},
})

View File

@@ -3,7 +3,6 @@ package container
import (
"encoding/gob"
"fmt"
"io/fs"
"path"
)
@@ -30,10 +29,10 @@ 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))
return &AbsoluteError{l.LinkName}
}
if name, err := k.readlink(l.LinkName); err != nil {
return wrapErrSelf(err)
return err
} else {
l.LinkName = name
}
@@ -44,9 +43,9 @@ 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) Is(op Op) bool {

View File

@@ -1,9 +1,10 @@
package container
import (
"io/fs"
"os"
"testing"
"hakurei.app/container/stub"
)
func TestSymlinkOp(t *testing.T) {
@@ -11,41 +12,41 @@ func TestSymlinkOp(t *testing.T) {
{"mkdir", &Params{ParentPerm: 0700}, &SymlinkOp{
Target: 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"),
LinkName: "etc/mtab",
Dereference: true,
}, nil, msg.WrapErr(fs.ErrInvalid, `path "etc/mtab" is not absolute`), nil, nil},
}, nil, &AbsoluteError{"etc/mtab"}, nil, nil},
{"readlink", &Params{ParentPerm: 0755}, &SymlinkOp{
Target: 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"),
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"),
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},
})

View File

@@ -3,14 +3,20 @@ package container
import (
"encoding/gob"
"fmt"
"io/fs"
"math"
"os"
"strconv"
. "syscall"
)
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 {
*f = append(*f, &MountTmpfsOp{SourceTmpfsEphemeral, target, MS_NOSUID | MS_NODEV, size, perm})
@@ -36,7 +42,7 @@ 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)
}

View File

@@ -1,31 +1,40 @@
package container
import (
"io/fs"
"os"
"syscall"
"testing"
"hakurei.app/container/stub"
)
func TestMountTmpfsOp(t *testing.T) {
t.Run("size error", func(t *testing.T) {
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/"),
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},
})

View File

@@ -106,8 +106,7 @@ func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) err
}
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)
@@ -119,7 +118,7 @@ func (p *procPaths) remount(target string, flags uintptr) error {
var targetFinal string
if v, err := p.k.evalSymlinks(target); err != nil {
return wrapErrSelf(err)
return err
} else {
targetFinal = v
if targetFinal != target {
@@ -135,14 +134,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
}
@@ -152,17 +149,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))
return err
}
if flags&MS_REC == 0 {
return nil
@@ -174,11 +165,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, cur, mf); err != nil && !errors.Is(err, EACCES) {
return err
}
}
@@ -207,15 +195,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 {

View File

@@ -5,6 +5,7 @@ import (
"syscall"
"testing"
"hakurei.app/container/stub"
"hakurei.app/container/vfs"
)
@@ -12,25 +13,25 @@ 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":`)},
}, stub.Expect{Calls: []stub.Call{
call("verbosef", stub.ExpectArgs{"resolved %q flags %#x", []any{"/sysroot/nix", uintptr(1)}}, nil, nil),
call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, stub.UniqueError(0xbad)),
}}, stub.UniqueError(0xbad)},
{"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},
}, stub.Expect{Calls: []stub.Call{
call("verbosef", stub.ExpectArgs{"resolved %q on %q flags %#x", []any{"/host/nix", "/sysroot/.host-nix", uintptr(1)}}, nil, nil),
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},
}, stub.Expect{Calls: []stub.Call{
call("verbosef", stub.ExpectArgs{"resolved %q flags %#x", []any{"/sysroot/nix", uintptr(1)}}, nil, nil),
call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, nil),
call("remount", stub.ExpectArgs{"/sysroot/nix", uintptr(1)}, nil, nil),
}}, nil},
})
}
@@ -81,138 +82,138 @@ func TestRemount(t *testing.T) {
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)},
}, 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":`)},
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, 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)},
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/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":`)},
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdeadbeef}, nil, stub.UniqueError(3)),
}}, &os.PathError{Op: "close", Path: "/sysroot/nix", Err: stub.UniqueError(3)}},
{"mountinfo stale", func(k syscallDispatcher) error {
{"mountinfo no match", 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`)},
}, 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)}, 0xdeadbeef, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/.hakurei", nil),
call("close", stub.ExpectArgs{0xdeadbeef}, 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:`)},
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdeadbeef}, 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":`)},
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdeadbeef}, 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":`)},
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdeadbeef}, 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},
}, 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},
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdeadbeef}, 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},
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdeadbeef}, 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},
}, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdeadbeef}, 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},
}, 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)}, 0xdeadbeef, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdeadbeef}, 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},
})
}
@@ -221,18 +222,18 @@ func TestRemountWithFlags(t *testing.T) {
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},
}, 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},
}, 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},
}, stub.Expect{Calls: []stub.Call{
call("mount", stub.ExpectArgs{"none", "", "", uintptr(0x209021), ""}, nil, nil),
}}, nil},
})
}
@@ -241,22 +242,22 @@ 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)},
}, 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 syscallDispatcher) 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 {
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},
})
}

View File

@@ -2,24 +2,34 @@ package container
import (
"errors"
"fmt"
"log"
"os"
"reflect"
"sync/atomic"
"testing"
)
// MessageError is an error with a user-facing message.
type MessageError interface {
// Message returns a user-facing error message.
Message() string
error
}
// GetErrorMessage returns whether an error implements [MessageError], and the message if it does.
func GetErrorMessage(err error) (string, bool) {
var e MessageError
if !errors.As(err, &e) || e == nil {
return zeroString, false
}
return e.Message(), true
}
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()
}
@@ -37,32 +47,6 @@ func (msg *DefaultMsg) Verbosef(format string, v ...any) {
}
}
// 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

@@ -9,13 +9,36 @@ import (
"testing"
"hakurei.app/container"
"hakurei.app/internal/hlog"
)
func TestDefaultMsg(t *testing.T) {
// bypass WrapErr testing behaviour
t.Setenv("GOPATH", container.Nonexistent)
func TestMessageError(t *testing.T) {
testCases := []struct {
name string
err error
want string
wantOk bool
}{
{"nil", nil, "", false},
{"new", errors.New(":3"), "", false},
{"start", &container.StartError{
Step: "meow",
Err: syscall.ENOTRECOVERABLE,
}, "cannot meow: state not recoverable", true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got, ok := container.GetErrorMessage(tc.err)
if got != tc.want {
t.Errorf("GetErrorMessage: %q, want %q", got, tc.want)
}
if ok != tc.wantOk {
t.Errorf("GetErrorMessage: ok = %v, want %v", ok, tc.wantOk)
}
})
}
}
func TestDefaultMsg(t *testing.T) {
{
w := log.Writer()
f := log.Flags()
@@ -48,21 +71,6 @@ func TestDefaultMsg(t *testing.T) {
}
})
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()
@@ -83,25 +91,6 @@ func TestDefaultMsg(t *testing.T) {
// 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{}
@@ -139,9 +128,6 @@ func (out *testOutput) Verbosef(format string, v ...any) {
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")

View File

@@ -10,17 +10,3 @@ func SetOutput(v Msg) {
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,8 +1,6 @@
package container
import (
"reflect"
"syscall"
"testing"
)
@@ -31,80 +29,13 @@ func TestGetSetOutput(t *testing.T) {
})
}
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...)
}
func (*stubOutput) IsVerbose() bool { panic("unreachable") }
func (*stubOutput) Verbose(...any) { panic("unreachable") }
func (*stubOutput) Verbosef(string, ...any) { panic("unreachable") }
func (*stubOutput) Suspend() { panic("unreachable") }
func (*stubOutput) Resume() bool { panic("unreachable") }
func (*stubOutput) BeforeExit() { panic("unreachable") }

View File

@@ -8,11 +8,6 @@ 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) {
if r, w, err := os.Pipe(); err != nil {
@@ -24,19 +19,23 @@ func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) {
}
}
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, errors.Unwrap(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

@@ -29,8 +29,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 +38,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,8 +47,8 @@ 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)
}
})

View File

@@ -2,7 +2,6 @@ package container
import (
"errors"
"fmt"
"io/fs"
"os"
"path"
@@ -103,30 +102,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 +145,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"
@@ -56,20 +54,27 @@ func InternalToHostOvlEscape(s string) string { return EscapeOverlayDataSegment(
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 +125,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 +141,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)
}
})
@@ -177,12 +182,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 +222,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 +236,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 +247,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)
}
})

37
container/stub/call.go Normal file
View File

@@ -0,0 +1,37 @@
package stub
import (
"slices"
)
// ExpectArgs is an array primarily for storing expected function arguments.
// Its actual use is defined by the implementation.
type ExpectArgs = [5]any
// An Expect stores expected calls of a goroutine.
type Expect struct {
Calls []Call
// Tracks are handed out to descendant goroutines in order.
Tracks []Expect
}
// A Call holds expected arguments of a function call and its outcome.
type Call struct {
// Name is the function Name of this call. Must be unique.
Name string
// Args are the expected arguments of this Call.
Args ExpectArgs
// Ret is the return value of this Call.
Ret any
// Err is the returned error of this Call.
Err error
}
// Error returns [Call.Err] if all arguments are true, or [ErrCheck] otherwise.
func (k *Call) Error(ok ...bool) error {
if !slices.Contains(ok, false) {
return k.Err
}
return ErrCheck
}

View File

@@ -0,0 +1,23 @@
package stub_test
import (
"reflect"
"testing"
"hakurei.app/container/stub"
)
func TestCallError(t *testing.T) {
t.Run("contains false", func(t *testing.T) {
if err := new(stub.Call).Error(true, false, true); !reflect.DeepEqual(err, stub.ErrCheck) {
t.Errorf("Error: %#v, want %#v", err, stub.ErrCheck)
}
})
t.Run("passthrough", func(t *testing.T) {
wantErr := stub.UniqueError(0xbabe)
if err := (&stub.Call{Err: wantErr}).Error(true); !reflect.DeepEqual(err, wantErr) {
t.Errorf("Error: %#v, want %#v", err, wantErr)
}
})
}

25
container/stub/errors.go Normal file
View File

@@ -0,0 +1,25 @@
package stub
import (
"errors"
"strconv"
)
var (
ErrCheck = errors.New("one or more arguments did not match")
)
// UniqueError is an error that only equivalates to other [UniqueError] with the same magic value.
type UniqueError uintptr
func (e UniqueError) Error() string {
return "unique error " + strconv.Itoa(int(e)) + " injected by the test suite"
}
func (e UniqueError) Is(target error) bool {
var u UniqueError
if !errors.As(target, &u) {
return false
}
return e == u
}

View File

@@ -0,0 +1,35 @@
package stub_test
import (
"errors"
"syscall"
"testing"
"hakurei.app/container/stub"
)
func TestUniqueError(t *testing.T) {
t.Run("format", func(t *testing.T) {
want := "unique error 2989 injected by the test suite"
if got := stub.UniqueError(0xbad).Error(); got != want {
t.Errorf("Error: %q, want %q", got, want)
}
})
t.Run("is", func(t *testing.T) {
t.Run("type", func(t *testing.T) {
if errors.Is(stub.UniqueError(0), syscall.ENOTRECOVERABLE) {
t.Error("Is: unexpected true")
}
})
t.Run("val", func(t *testing.T) {
if errors.Is(stub.UniqueError(0), stub.UniqueError(1)) {
t.Error("Is: unexpected true")
}
if !errors.Is(stub.UniqueError(0xbad), stub.UniqueError(0xbad)) {
t.Error("Is: unexpected false")
}
})
})
}

44
container/stub/exit.go Normal file
View File

@@ -0,0 +1,44 @@
package stub
import "testing"
// PanicExit is a magic panic value treated as a simulated exit.
const PanicExit = 0xdeadbeef
const (
panicFailNow = 0xcafe0000 + iota
panicFatal
panicFatalf
)
// HandleExit must be deferred before calling with the stub.
func HandleExit(t testing.TB) {
switch r := recover(); r {
case PanicExit:
break
case panicFailNow:
t.FailNow()
case panicFatal, panicFatalf, nil:
break
default:
panic(r)
}
}
// handleExitNew handles exits from goroutines created by [Stub.New].
func handleExitNew(t testing.TB) {
switch r := recover(); r {
case PanicExit, panicFatal, panicFatalf, nil:
break
case panicFailNow:
t.Fail()
break
default:
panic(r)
}
}

View File

@@ -0,0 +1,93 @@
package stub_test
import (
"testing"
_ "unsafe"
"hakurei.app/container/stub"
)
//go:linkname handleExitNew hakurei.app/container/stub.handleExitNew
func handleExitNew(_ testing.TB)
// overrideTFailNow overrides the Fail and FailNow method.
type overrideTFailNow struct {
*testing.T
failNow bool
fail bool
}
func (o *overrideTFailNow) FailNow() {
if o.failNow {
o.Errorf("attempted to FailNow twice")
}
o.failNow = true
}
func (o *overrideTFailNow) Fail() {
if o.fail {
o.Errorf("attempted to Fail twice")
}
o.fail = true
}
func TestHandleExit(t *testing.T) {
t.Run("exit", func(t *testing.T) {
defer stub.HandleExit(t)
panic(stub.PanicExit)
})
t.Run("goexit", func(t *testing.T) {
t.Run("FailNow", func(t *testing.T) {
ot := &overrideTFailNow{T: t}
defer func() {
if !ot.failNow {
t.Errorf("FailNow was never called")
}
}()
defer stub.HandleExit(ot)
panic(0xcafe0000)
})
t.Run("Fail", func(t *testing.T) {
ot := &overrideTFailNow{T: t}
defer func() {
if !ot.fail {
t.Errorf("Fail was never called")
}
}()
defer handleExitNew(ot)
panic(0xcafe0000)
})
})
t.Run("nil", func(t *testing.T) {
defer stub.HandleExit(t)
})
t.Run("passthrough", func(t *testing.T) {
t.Run("toplevel", func(t *testing.T) {
defer func() {
want := 0xcafebabe
if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want)
}
}()
defer stub.HandleExit(t)
panic(0xcafebabe)
})
t.Run("new", func(t *testing.T) {
defer func() {
want := 0xcafe
if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want)
}
}()
defer handleExitNew(t)
panic(0xcafe)
})
})
}

148
container/stub/stub.go Normal file
View File

@@ -0,0 +1,148 @@
// Package stub provides function call level stubbing and validation
// for library functions that are impossible to check otherwise.
package stub
import (
"reflect"
"sync"
"testing"
)
// this should prevent stub from being inadvertently imported outside tests
var _ = func() {
if !testing.Testing() {
panic("stub imported while not in a test")
}
}
const (
// A CallSeparator denotes an injected separation between two groups of calls.
CallSeparator = "\x00"
)
// A Stub is a collection of tracks of expected calls.
type Stub[K any] struct {
testing.TB
// makeK creates a new K for a descendant [Stub].
// This function may be called concurrently.
makeK func(s *Stub[K]) K
// want is a hierarchy of expected calls.
want Expect
// pos is the current position in [Expect.Calls].
pos int
// goroutine counts the number of goroutines created by this [Stub].
goroutine int
// sub stores the addresses of descendant [Stub] created by New.
sub []*Stub[K]
// wg waits for all descendants to complete.
wg *sync.WaitGroup
}
// New creates a root [Stub].
func New[K any](tb testing.TB, makeK func(s *Stub[K]) K, want Expect) *Stub[K] {
return &Stub[K]{TB: tb, makeK: makeK, want: want, wg: new(sync.WaitGroup)}
}
func (s *Stub[K]) FailNow() { panic(panicFailNow) }
func (s *Stub[K]) Fatal(args ...any) { s.Error(args...); panic(panicFatal) }
func (s *Stub[K]) Fatalf(format string, args ...any) { s.Errorf(format, args...); panic(panicFatalf) }
func (s *Stub[K]) SkipNow() { panic("invalid call to SkipNow") }
func (s *Stub[K]) Skip(...any) { panic("invalid call to Skip") }
func (s *Stub[K]) Skipf(string, ...any) { panic("invalid call to Skipf") }
// New calls f in a new goroutine
func (s *Stub[K]) New(f func(k K)) {
s.Helper()
s.Expects("New")
if len(s.want.Tracks) <= s.goroutine {
s.Fatal("New: track overrun")
}
ds := &Stub[K]{TB: s.TB, makeK: s.makeK, want: s.want.Tracks[s.goroutine], wg: s.wg}
s.goroutine++
s.sub = append(s.sub, ds)
s.wg.Add(1)
go func() {
s.Helper()
defer s.wg.Done()
defer handleExitNew(s.TB)
f(s.makeK(ds))
}()
}
// Pos returns the current position of [Stub] in its [Expect.Calls]
func (s *Stub[K]) Pos() int { return s.pos }
// Len returns the length of [Expect.Calls].
func (s *Stub[K]) Len() int { return len(s.want.Calls) }
// VisitIncomplete calls f on an incomplete s and all its descendants.
func (s *Stub[K]) VisitIncomplete(f func(s *Stub[K])) {
s.Helper()
s.wg.Wait()
if s.want.Calls != nil && len(s.want.Calls) != s.pos {
f(s)
}
for _, ds := range s.sub {
ds.VisitIncomplete(f)
}
}
// Expects checks the name of and returns the current [Call] and advances pos.
func (s *Stub[K]) Expects(name string) (expect *Call) {
s.Helper()
if len(s.want.Calls) == s.pos {
s.Fatal("Expects: advancing beyond expected calls")
}
expect = &s.want.Calls[s.pos]
if name != expect.Name {
if expect.Name == CallSeparator {
s.Fatalf("Expects: func = %s, separator overrun", name)
}
if name == CallSeparator {
s.Fatalf("Expects: separator, want %s", expect.Name)
}
s.Fatalf("Expects: func = %s, want %s", name, expect.Name)
}
s.pos++
return
}
// CheckArg checks an argument comparable with the == operator. Avoid using this with pointers.
func CheckArg[T comparable, K any](s *Stub[K], arg string, got T, n int) bool {
s.Helper()
pos := s.pos - 1
if pos < 0 || pos >= len(s.want.Calls) {
panic("invalid call to CheckArg")
}
expect := s.want.Calls[pos]
want, ok := expect.Args[n].(T)
if !ok || got != want {
s.Errorf("%s: %s = %#v, want %#v (%d)", expect.Name, arg, got, want, pos)
return false
}
return true
}
// CheckArgReflect checks an argument of any type.
func CheckArgReflect[K any](s *Stub[K], arg string, got any, n int) bool {
s.Helper()
pos := s.pos - 1
if pos < 0 || pos >= len(s.want.Calls) {
panic("invalid call to CheckArgReflect")
}
expect := s.want.Calls[pos]
want := expect.Args[n]
if !reflect.DeepEqual(got, want) {
s.Errorf("%s: %s = %#v, want %#v (%d)", expect.Name, arg, got, want, pos)
return false
}
return true
}

296
container/stub/stub_test.go Normal file
View File

@@ -0,0 +1,296 @@
package stub
import (
"reflect"
"sync/atomic"
"testing"
)
// stubHolder embeds [Stub].
type stubHolder struct{ *Stub[stubHolder] }
// overrideT allows some methods of [testing.T] to be overridden.
type overrideT struct {
*testing.T
error atomic.Pointer[func(args ...any)]
errorf atomic.Pointer[func(format string, args ...any)]
}
func (t *overrideT) Error(args ...any) {
fp := t.error.Load()
if fp == nil || *fp == nil {
t.T.Error(args...)
return
}
(*fp)(args...)
}
func (t *overrideT) Errorf(format string, args ...any) {
fp := t.errorf.Load()
if fp == nil || *fp == nil {
t.T.Errorf(format, args...)
return
}
(*fp)(format, args...)
}
func TestStub(t *testing.T) {
t.Run("goexit", func(t *testing.T) {
t.Run("FailNow", func(t *testing.T) {
defer func() {
if r := recover(); r != panicFailNow {
t.Errorf("recover: %v", r)
}
}()
new(stubHolder).FailNow()
})
t.Run("SkipNow", func(t *testing.T) {
defer func() {
want := "invalid call to SkipNow"
if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want)
}
}()
new(stubHolder).SkipNow()
})
t.Run("Skip", func(t *testing.T) {
defer func() {
want := "invalid call to Skip"
if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want)
}
}()
new(stubHolder).Skip()
})
t.Run("Skipf", func(t *testing.T) {
defer func() {
want := "invalid call to Skipf"
if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want)
}
}()
new(stubHolder).Skipf("")
})
})
t.Run("new", func(t *testing.T) {
t.Run("success", func(t *testing.T) {
s := New(t, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
{"New", ExpectArgs{}, nil, nil},
}, Tracks: []Expect{{Calls: []Call{
{"done", ExpectArgs{0xbabe}, nil, nil},
}}}})
s.New(func(k stubHolder) {
expect := k.Expects("done")
if expect.Name != "done" {
t.Errorf("New: Name = %s, want done", expect.Name)
}
if expect.Args != (ExpectArgs{0xbabe}) {
t.Errorf("New: Args = %#v", expect.Args)
}
if expect.Ret != nil {
t.Errorf("New: Ret = %#v", expect.Ret)
}
if expect.Err != nil {
t.Errorf("New: Err = %#v", expect.Err)
}
})
if pos := s.Pos(); pos != 1 {
t.Errorf("Pos: %d, want 1", pos)
}
if l := s.Len(); l != 1 {
t.Errorf("Len: %d, want 1", l)
}
s.VisitIncomplete(func(s *Stub[stubHolder]) { panic("unreachable") })
})
t.Run("overrun", func(t *testing.T) {
ot := &overrideT{T: t}
ot.error.Store(checkError(t, "New: track overrun"))
s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
{"New", ExpectArgs{}, nil, nil},
{"panic", ExpectArgs{"unreachable"}, nil, nil},
}})
func() { defer HandleExit(t); s.New(func(k stubHolder) { panic("unreachable") }) }()
var visit int
s.VisitIncomplete(func(s *Stub[stubHolder]) {
visit++
if visit > 1 {
panic("unexpected visit count")
}
want := Call{"panic", ExpectArgs{"unreachable"}, nil, nil}
if got := s.want.Calls[s.pos]; !reflect.DeepEqual(got, want) {
t.Errorf("VisitIncomplete: %#v, want %#v", got, want)
}
})
})
t.Run("expects", func(t *testing.T) {
t.Run("overrun", func(t *testing.T) {
ot := &overrideT{T: t}
ot.error.Store(checkError(t, "Expects: advancing beyond expected calls"))
s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{})
func() { defer HandleExit(t); s.Expects("unreachable") }()
})
t.Run("separator", func(t *testing.T) {
t.Run("overrun", func(t *testing.T) {
ot := &overrideT{T: t}
ot.errorf.Store(checkErrorf(t, "Expects: func = %s, separator overrun", "meow"))
s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
{CallSeparator, ExpectArgs{}, nil, nil},
}})
func() { defer HandleExit(t); s.Expects("meow") }()
})
t.Run("mismatch", func(t *testing.T) {
ot := &overrideT{T: t}
ot.errorf.Store(checkErrorf(t, "Expects: separator, want %s", "panic"))
s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
{"panic", ExpectArgs{}, nil, nil},
}})
func() { defer HandleExit(t); s.Expects(CallSeparator) }()
})
})
t.Run("mismatch", func(t *testing.T) {
ot := &overrideT{T: t}
ot.errorf.Store(checkErrorf(t, "Expects: func = %s, want %s", "meow", "nya"))
s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
{"nya", ExpectArgs{}, nil, nil},
}})
func() { defer HandleExit(t); s.Expects("meow") }()
})
})
})
}
func TestCheckArg(t *testing.T) {
t.Run("oob negative", func(t *testing.T) {
defer func() {
want := "invalid call to CheckArg"
if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want)
}
}()
s := New(t, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{})
CheckArg(s, "unreachable", struct{}{}, 0)
})
ot := &overrideT{T: t}
s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
{"panic", ExpectArgs{PanicExit}, nil, nil},
{"meow", ExpectArgs{-1}, nil, nil},
}})
t.Run("match", func(t *testing.T) {
s.Expects("panic")
if !CheckArg(s, "v", PanicExit, 0) {
t.Errorf("CheckArg: unexpected false")
}
})
t.Run("mismatch", func(t *testing.T) {
defer HandleExit(t)
s.Expects("meow")
ot.errorf.Store(checkErrorf(t, "%s: %s = %#v, want %#v (%d)", "meow", "time", 0, -1, 1))
if CheckArg(s, "time", 0, 0) {
t.Errorf("CheckArg: unexpected true")
}
})
t.Run("oob", func(t *testing.T) {
s.pos++
defer func() {
want := "invalid call to CheckArg"
if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want)
}
}()
CheckArg(s, "unreachable", struct{}{}, 0)
})
}
func TestCheckArgReflect(t *testing.T) {
t.Run("oob lower", func(t *testing.T) {
defer func() {
want := "invalid call to CheckArgReflect"
if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want)
}
}()
s := New(t, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{})
CheckArgReflect(s, "unreachable", struct{}{}, 0)
})
ot := &overrideT{T: t}
s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
{"panic", ExpectArgs{PanicExit}, nil, nil},
{"meow", ExpectArgs{-1}, nil, nil},
}})
t.Run("match", func(t *testing.T) {
s.Expects("panic")
if !CheckArgReflect(s, "v", PanicExit, 0) {
t.Errorf("CheckArgReflect: unexpected false")
}
})
t.Run("mismatch", func(t *testing.T) {
defer HandleExit(t)
s.Expects("meow")
ot.errorf.Store(checkErrorf(t, "%s: %s = %#v, want %#v (%d)", "meow", "time", 0, -1, 1))
if CheckArgReflect(s, "time", 0, 0) {
t.Errorf("CheckArgReflect: unexpected true")
}
})
t.Run("oob", func(t *testing.T) {
s.pos++
defer func() {
want := "invalid call to CheckArgReflect"
if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want)
}
}()
CheckArgReflect(s, "unreachable", struct{}{}, 0)
})
}
func checkError(t *testing.T, wantArgs ...any) *func(args ...any) {
var called bool
f := func(args ...any) {
if called {
panic("invalid call to error")
}
called = true
if !reflect.DeepEqual(args, wantArgs) {
t.Errorf("Error: %#v, want %#v", args, wantArgs)
}
panic(PanicExit)
}
return &f
}
func checkErrorf(t *testing.T, wantFormat string, wantArgs ...any) *func(format string, args ...any) {
var called bool
f := func(format string, args ...any) {
if called {
panic("invalid call to errorf")
}
called = true
if format != wantFormat {
t.Errorf("Errorf: format = %q, want %q", format, wantFormat)
}
if !reflect.DeepEqual(args, wantArgs) {
t.Errorf("Errorf: args = %#v, want %#v", args, wantArgs)
}
panic(PanicExit)
}
return &f
}

View File

@@ -24,6 +24,32 @@ var (
ErrMountInfoSep = errors.New("bad optional fields separator")
)
type DecoderError struct {
Op string
Line int
Err error
}
func (e *DecoderError) Unwrap() error { return e.Err }
func (e *DecoderError) Error() string {
var s string
var numError *strconv.NumError
switch {
case errors.As(e.Err, &numError) && numError != nil:
s = "numeric field " + strconv.Quote(numError.Num) + " " + numError.Err.Error()
default:
s = e.Err.Error()
}
var atLine string
if e.Line >= 0 {
atLine = " at line " + strconv.Itoa(e.Line)
}
return e.Op + " mountinfo" + atLine + ": " + s
}
type (
// A MountInfoDecoder reads and decodes proc_pid_mountinfo(5) entries from an input stream.
MountInfoDecoder struct {
@@ -32,6 +58,7 @@ type (
current *MountInfo
parseErr error
curLine int
complete bool
}
@@ -132,9 +159,12 @@ func (d *MountInfoDecoder) Entries() iter.Seq[*MountInfoEntry] {
func (d *MountInfoDecoder) Err() error {
if err := d.s.Err(); err != nil {
return err
return &DecoderError{"scan", d.curLine, err}
}
return d.parseErr
if d.parseErr != nil {
return &DecoderError{"parse", d.curLine, d.parseErr}
}
return nil
}
func (d *MountInfoDecoder) scan() bool {
@@ -160,6 +190,7 @@ func (d *MountInfoDecoder) scan() bool {
d.current.Next = m
d.current = d.current.Next
}
d.curLine++
return true
}

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"iter"
"os"
"path"
"reflect"
"slices"
@@ -15,62 +16,102 @@ import (
"hakurei.app/container/vfs"
)
func TestDecoderError(t *testing.T) {
testCases := []struct {
name string
err *vfs.DecoderError
want string
target error
targetF error
}{
{"errno", &vfs.DecoderError{Op: "parse", Line: 0xdeadbeef, Err: syscall.ENOTRECOVERABLE},
"parse mountinfo at line 3735928559: state not recoverable", syscall.ENOTRECOVERABLE, syscall.EROFS},
{"strconv", &vfs.DecoderError{Op: "parse", Line: 0xdeadbeef, Err: &strconv.NumError{Func: "Atoi", Num: "meow", Err: strconv.ErrSyntax}},
`parse mountinfo at line 3735928559: numeric field "meow" invalid syntax`, strconv.ErrSyntax, os.ErrInvalid},
{"unfold", &vfs.DecoderError{Op: "unfold", Line: -1, Err: vfs.UnfoldTargetError("/proc/nonexistent")},
"unfold mountinfo: mount point /proc/nonexistent never appeared in mountinfo", vfs.UnfoldTargetError("/proc/nonexistent"), os.ErrNotExist},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("error", func(t *testing.T) {
if got := tc.err.Error(); got != tc.want {
t.Errorf("Error: %s, want %s", got, tc.want)
}
})
t.Run("is", func(t *testing.T) {
if !errors.Is(tc.err, tc.target) {
t.Errorf("Is: unexpected false")
}
if errors.Is(tc.err, tc.targetF) {
t.Errorf("Is: unexpected true")
}
})
})
}
}
func TestMountInfo(t *testing.T) {
testCases := []mountInfoTest{
{"count", sampleMountinfoBase + `
21 20 0:53/ /mnt/test rw,relatime - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoFields, "", nil, nil, nil},
&vfs.DecoderError{Op: "parse", Line: 6, Err: vfs.ErrMountInfoFields},
"", nil, nil, nil},
{"sep", sampleMountinfoBase + `
21 20 0:53 / /mnt/test rw,relatime shared:212 _ tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoSep, "", nil, nil, nil},
&vfs.DecoderError{Op: "parse", Line: 6, Err: vfs.ErrMountInfoSep},
"", nil, nil, nil},
{"id", sampleMountinfoBase + `
id 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
strconv.ErrSyntax, "", nil, nil, nil},
&vfs.DecoderError{Op: "parse", Line: 6, Err: &strconv.NumError{Func: "Atoi", Num: "id", Err: strconv.ErrSyntax}},
"", nil, nil, nil},
{"parent", sampleMountinfoBase + `
21 parent 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
strconv.ErrSyntax, "", nil, nil, nil},
&vfs.DecoderError{Op: "parse", Line: 6, Err: &strconv.NumError{Func: "Atoi", Num: "parent", Err: strconv.ErrSyntax}}, "", nil, nil, nil},
{"devno", sampleMountinfoBase + `
21 20 053 / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
nil, "unexpected EOF", nil, nil, nil},
nil, "parse mountinfo at line 6: unexpected EOF", nil, nil, nil},
{"maj", sampleMountinfoBase + `
21 20 maj:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
nil, "expected integer", nil, nil, nil},
nil, "parse mountinfo at line 6: expected integer", nil, nil, nil},
{"min", sampleMountinfoBase + `
21 20 0:min / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
nil, "expected integer", nil, nil, nil},
nil, "parse mountinfo at line 6: expected integer", nil, nil, nil},
{"mountroot", sampleMountinfoBase + `
21 20 0:53 /mnt/test rw,relatime - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoEmpty, "", nil, nil, nil},
&vfs.DecoderError{Op: "parse", Line: 6, Err: vfs.ErrMountInfoEmpty}, "", nil, nil, nil},
{"target", sampleMountinfoBase + `
21 20 0:53 / rw,relatime - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoEmpty, "", nil, nil, nil},
&vfs.DecoderError{Op: "parse", Line: 6, Err: vfs.ErrMountInfoEmpty}, "", nil, nil, nil},
{"vfs options", sampleMountinfoBase + `
21 20 0:53 / /mnt/test - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoEmpty, "", nil, nil, nil},
&vfs.DecoderError{Op: "parse", Line: 6, Err: vfs.ErrMountInfoEmpty}, "", nil, nil, nil},
{"FS type", sampleMountinfoBase + `
21 20 0:53 / /mnt/test rw,relatime - rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoEmpty, "", nil, nil, nil},
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755
21 20 0:53 / /mnt/test rw,relatime - rw`,
&vfs.DecoderError{Op: "parse", Line: 7, Err: vfs.ErrMountInfoEmpty}, "", nil, nil, nil},
{"base", sampleMountinfoBase, nil, "", []*wantMountInfo{
m(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", syscall.MS_RELATIME, nil),
@@ -266,9 +307,9 @@ func (tc *mountInfoTest) check(t *testing.T, d *vfs.MountInfoDecoder, funcName s
})
} else if tc.wantNode != nil || tc.wantCollectF != nil {
panic("invalid test case")
} else if _, err := d.Unfold("/"); !errors.Is(err, tc.wantErr) {
} else if _, err := d.Unfold("/"); !reflect.DeepEqual(err, tc.wantErr) {
if tc.wantError == "" {
t.Errorf("Unfold: error = %v, wantErr %v",
t.Errorf("Unfold: error = %#v, wantErr %#v",
err, tc.wantErr)
} else if err != nil && err.Error() != tc.wantError {
t.Errorf("Unfold: error = %q, wantError %q",
@@ -276,9 +317,9 @@ func (tc *mountInfoTest) check(t *testing.T, d *vfs.MountInfoDecoder, funcName s
}
}
if err := gotErr(); !errors.Is(err, tc.wantErr) {
if err := gotErr(); !reflect.DeepEqual(err, tc.wantErr) {
if tc.wantError == "" {
t.Errorf("%s: error = %v, wantErr %v",
t.Errorf("%s: error = %#v, wantErr %#v",
funcName, err, tc.wantErr)
} else if err != nil && err.Error() != tc.wantError {
t.Errorf("%s: error = %q, wantError %q",

View File

@@ -4,9 +4,14 @@ import (
"iter"
"path"
"strings"
"syscall"
)
type UnfoldTargetError string
func (e UnfoldTargetError) Error() string {
return "mount point " + string(e) + " never appeared in mountinfo"
}
// MountInfoNode positions a [MountInfoEntry] in its mount hierarchy.
type MountInfoNode struct {
*MountInfoEntry
@@ -65,7 +70,8 @@ func (d *MountInfoDecoder) Unfold(target string) (*MountInfoNode, error) {
}
if targetIndex == -1 {
return nil, syscall.ESTALE
// target does not exist in parsed mountinfo
return nil, &DecoderError{Op: "unfold", Line: -1, Err: UnfoldTargetError(targetClean)}
}
for _, cur := range mountinfo {

View File

@@ -1,11 +1,9 @@
package vfs_test
import (
"errors"
"reflect"
"slices"
"strings"
"syscall"
"testing"
"hakurei.app/container/vfs"
@@ -26,7 +24,7 @@ func TestUnfold(t *testing.T) {
"no match",
sampleMountinfoBase,
"/mnt",
syscall.ESTALE, nil, nil, nil,
&vfs.DecoderError{Op: "unfold", Line: -1, Err: vfs.UnfoldTargetError("/mnt")}, nil, nil, nil,
},
{
"cover",
@@ -55,7 +53,7 @@ func TestUnfold(t *testing.T) {
d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
got, err := d.Unfold(tc.target)
if !errors.Is(err, tc.wantErr) {
if !reflect.DeepEqual(err, tc.wantErr) {
t.Errorf("Unfold: error = %v, wantErr %v",
err, tc.wantErr)
}

12
flake.lock generated
View File

@@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1753479839,
"narHash": "sha256-E/rPVh7vyPMJUFl2NAew+zibNGfVbANr8BP8nLRbLkQ=",
"lastModified": 1756679287,
"narHash": "sha256-Xd1vOeY9ccDf5VtVK12yM0FS6qqvfUop8UQlxEB+gTQ=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "0b9bf983db4d064764084cd6748efb1ab8297d1e",
"rev": "07fc025fe10487dd80f2ec694f1cd790e752d0e8",
"type": "github"
},
"original": {
@@ -23,11 +23,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1753345091,
"narHash": "sha256-CdX2Rtvp5I8HGu9swBmYuq+ILwRxpXdJwlpg8jvN4tU=",
"lastModified": 1757020766,
"narHash": "sha256-PLoSjHRa2bUbi1x9HoXgTx2AiuzNXs54c8omhadyvp0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3ff0e34b1383648053bba8ed03f201d3466f90c9",
"rev": "fe83bbdde2ccdc2cb9573aa846abe8363f79a97a",
"type": "github"
},
"original": {

View File

@@ -11,10 +11,10 @@ import (
)
func TestContainer(t *testing.T) {
t.Run("start empty container", func(t *testing.T) {
t.Run("start invalid container", func(t *testing.T) {
h := helper.New(t.Context(), container.MustAbs(container.Nonexistent), "hakurei", argsWt, false, argF, nil, nil)
wantErr := "container: starting an empty container"
wantErr := "container: starting an invalid container"
if err := h.Start(); err == nil || err.Error() != wantErr {
t.Errorf("Start: error = %v, wantErr %q",
err, wantErr)

View File

@@ -6,8 +6,10 @@ import (
"fmt"
"io"
"os"
"reflect"
"strconv"
"strings"
"syscall"
"testing"
"time"
@@ -47,6 +49,10 @@ func argFChecked(argsFd, statFd int) (args []string) {
return
}
const (
containerTimeout = 30 * time.Second
)
// this function tests an implementation of the helper.Helper interface
func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper) {
oldWaitDelay := helper.WaitDelay
@@ -54,18 +60,15 @@ func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput f
t.Cleanup(func() { helper.WaitDelay = oldWaitDelay })
t.Run("start helper with status channel and wait", func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
ctx, cancel := context.WithTimeout(t.Context(), containerTimeout)
stdout := new(strings.Builder)
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, os.Stderr }, true)
t.Run("wait not yet started helper", func(t *testing.T) {
defer func() {
r := recover()
if r == nil {
t.Fatalf("Wait did not panic")
}
}()
panic(fmt.Sprintf("unreachable: %v", h.Wait()))
if err := h.Wait(); !reflect.DeepEqual(err, syscall.EINVAL) &&
!reflect.DeepEqual(err, errors.New("exec: not started")) {
t.Errorf("Wait: error = %v", err)
}
})
t.Log("starting helper stub")
@@ -108,7 +111,7 @@ func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput f
})
t.Run("start helper and wait", func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
ctx, cancel := context.WithTimeout(t.Context(), containerTimeout)
defer cancel()
stdout := new(strings.Builder)
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, os.Stderr }, false)

View File

@@ -87,7 +87,9 @@ type (
// initial process environment variables
Env map[string]string `json:"env"`
// map target user uid to privileged user uid in the user namespace
// map target user uid to privileged user uid in the user namespace;
// some programs fail to connect to dbus session running as a different uid,
// this option works around it by mapping priv-side caller uid in container
MapRealUID bool `json:"map_real_uid"`
// pass through all devices

View File

@@ -2,12 +2,42 @@
package hst
import (
"errors"
"net"
"os"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/system"
"hakurei.app/system/dbus"
)
// An AppError is returned while starting an app according to [hst.Config].
type AppError struct {
Step string
Err error
Msg string
}
func (e *AppError) Error() string { return e.Err.Error() }
func (e *AppError) Unwrap() error { return e.Err }
func (e *AppError) Message() string {
if e.Msg != "" {
return e.Msg
}
switch {
case errors.As(e.Err, new(*os.PathError)),
errors.As(e.Err, new(*os.LinkError)),
errors.As(e.Err, new(*os.SyscallError)),
errors.As(e.Err, new(*net.OpError)):
return "cannot " + e.Error()
default:
return "cannot " + e.Step + ": " + e.Error()
}
}
// Paths contains environment-dependent paths used by hakurei.
type Paths struct {
// temporary directory returned by [os.TempDir] (usually `/tmp`)

View File

@@ -2,11 +2,93 @@ package hst_test
import (
"encoding/json"
"errors"
"net"
"os"
"syscall"
"testing"
"hakurei.app/container"
"hakurei.app/container/stub"
"hakurei.app/hst"
)
func TestAppError(t *testing.T) {
testCases := []struct {
name string
err error
s string
message string
is, isF error
}{
{"message", &hst.AppError{Step: "obtain uid from hsu", Err: stub.UniqueError(0),
Msg: "the setuid helper is missing: /run/wrappers/bin/hsu"},
"unique error 0 injected by the test suite",
"the setuid helper is missing: /run/wrappers/bin/hsu",
stub.UniqueError(0), os.ErrNotExist},
{"os.PathError", &hst.AppError{Step: "passthrough os.PathError",
Err: &os.PathError{Op: "stat", Path: "/proc/nonexistent", Err: os.ErrNotExist}},
"stat /proc/nonexistent: file does not exist",
"cannot stat /proc/nonexistent: file does not exist",
os.ErrNotExist, stub.UniqueError(0xdeadbeef)},
{"os.LinkError", &hst.AppError{Step: "passthrough os.LinkError",
Err: &os.LinkError{Op: "link", Old: "/proc/self", New: "/proc/nonexistent", Err: os.ErrNotExist}},
"link /proc/self /proc/nonexistent: file does not exist",
"cannot link /proc/self /proc/nonexistent: file does not exist",
os.ErrNotExist, stub.UniqueError(0xdeadbeef)},
{"os.SyscallError", &hst.AppError{Step: "passthrough os.SyscallError",
Err: &os.SyscallError{Syscall: "meow", Err: syscall.ENOSYS}},
"meow: function not implemented",
"cannot meow: function not implemented",
syscall.ENOSYS, syscall.ENOTRECOVERABLE},
{"net.OpError", &hst.AppError{Step: "passthrough net.OpError",
Err: &net.OpError{Op: "dial", Net: "cat", Err: net.UnknownNetworkError("cat")}},
"dial cat: unknown network cat",
"cannot dial cat: unknown network cat",
net.UnknownNetworkError("cat"), syscall.ENOTRECOVERABLE},
{"default", &hst.AppError{Step: "initialise container configuration", Err: stub.UniqueError(1)},
"unique error 1 injected by the test suite",
"cannot initialise container configuration: unique error 1 injected by the test suite",
stub.UniqueError(1), os.ErrInvalid},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("error", func(t *testing.T) {
if got := tc.err.Error(); got != tc.s {
t.Errorf("Error: %s, want %s", got, tc.s)
}
})
t.Run("message", func(t *testing.T) {
gotMessage, gotMessageOk := container.GetErrorMessage(tc.err)
if want := tc.message != "\x00"; gotMessageOk != want {
t.Errorf("GetErrorMessage: ok = %v, want %v", gotMessage, want)
}
if gotMessageOk {
if gotMessage != tc.message {
t.Errorf("GetErrorMessage: %s, want %s", gotMessage, tc.message)
}
}
})
t.Run("is", func(t *testing.T) {
if !errors.Is(tc.err, tc.is) {
t.Errorf("Is: unexpected false for %v", tc.is)
}
if errors.Is(tc.err, tc.isF) {
t.Errorf("Is: unexpected true for %v", tc.isF)
}
})
})
}
}
func TestTemplate(t *testing.T) {
const want = `{
"id": "org.chromium.Chromium",

View File

@@ -1,61 +1,30 @@
// Package app defines the generic [App] interface.
// Package app implements high-level hakurei container behaviour.
package app
import (
"context"
"log"
"syscall"
"time"
"os"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/internal/sys"
)
type App interface {
// ID returns a copy of [ID] held by App.
ID() state.ID
// Seal determines the outcome of config as a [SealedApp].
// The value of config might be overwritten and must not be used again.
Seal(config *hst.Config) (SealedApp, error)
String() string
}
type SealedApp interface {
// Run commits sealed system setup and starts the app process.
Run(rs *RunState) error
}
// RunState stores the outcome of a call to [SealedApp.Run].
type RunState struct {
// Time is the exact point in time where the process was created.
// Location must be set to UTC.
//
// Time is nil if no process was ever created.
Time *time.Time
// RevertErr is stored by the deferred revert call.
RevertErr error
// WaitErr is the generic error value created by the standard library.
WaitErr error
syscall.WaitStatus
}
// SetStart stores the current time in [RunState] once.
func (rs *RunState) SetStart() {
if rs.Time != nil {
panic("attempted to store time twice")
// Main runs an app according to [hst.Config] and terminates. Main does not return.
func Main(ctx context.Context, k sys.State, config *hst.Config) {
var id state.ID
if err := state.NewAppID(&id); err != nil {
log.Fatal(err)
}
now := time.Now().UTC()
rs.Time = &now
}
func MustNew(ctx context.Context, os sys.State) App {
a, err := New(ctx, os)
if err != nil {
log.Fatalf("cannot create app: %v", err)
var seal outcome
seal.id = &stringPair[state.ID]{id, id.String()}
if err := seal.finalise(ctx, k, config); err != nil {
printMessageError("cannot seal app:", err)
os.Exit(1)
}
return a
seal.main()
panic("unreachable")
}

View File

@@ -1,69 +0,0 @@
package app
import (
"context"
"fmt"
"sync"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/internal/sys"
)
func New(ctx context.Context, os sys.State) (App, error) {
a := new(app)
a.sys = os
a.ctx = ctx
id := new(state.ID)
err := state.NewAppID(id)
a.id = newID(id)
return a, err
}
type app struct {
id *stringPair[state.ID]
sys sys.State
ctx context.Context
*outcome
mu sync.RWMutex
}
func (a *app) ID() state.ID { a.mu.RLock(); defer a.mu.RUnlock(); return a.id.unwrap() }
func (a *app) String() string {
if a == nil {
return "(invalid app)"
}
a.mu.RLock()
defer a.mu.RUnlock()
if a.outcome != nil {
if a.outcome.user.uid == nil {
return fmt.Sprintf("(sealed app %s with invalid uid)", a.id)
}
return fmt.Sprintf("(sealed app %s as uid %s)", a.id, a.outcome.user.uid)
}
return fmt.Sprintf("(unsealed app %s)", a.id)
}
func (a *app) Seal(config *hst.Config) (SealedApp, error) {
a.mu.Lock()
defer a.mu.Unlock()
if a.outcome != nil {
panic("app sealed twice")
}
seal := new(outcome)
seal.id = a.id
err := seal.finalise(a.ctx, a.sys, config)
if err == nil {
a.outcome = seal
}
return seal, err
}

View File

@@ -1,6 +1,7 @@
package app_test
import (
"context"
"syscall"
"hakurei.app/container"
@@ -74,7 +75,7 @@ var testCasesNixos = []sealTestCase{
0x4c, 0xf0, 0x73, 0xbd,
0xb4, 0x6e, 0xb5, 0xc1,
},
system.New(1000001).
system.New(context.TODO(), 1000001).
Ensure("/tmp/hakurei.1971", 0711).
Ensure("/tmp/hakurei.1971/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime", acl.Execute).
Ensure("/tmp/hakurei.1971/runtime/1", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/1", acl.Read, acl.Write, acl.Execute).
@@ -134,6 +135,7 @@ var testCasesNixos = []sealTestCase{
Proc(m("/proc/")).
Tmpfs(hst.AbsTmp, 4096, 0755).
DevWritable(m("/dev/"), true).
Tmpfs(m("/dev/shm"), 0, 01777).
Bind(m("/bin"), m("/bin"), 0).
Bind(m("/usr/bin/"), m("/usr/bin/"), 0).
Bind(m("/nix/store"), m("/nix/store"), 0).
@@ -148,7 +150,6 @@ var testCasesNixos = []sealTestCase{
Etc(m("/etc/"), "8e2c76b066dabe574cf073bdb46eb5c1").
Bind(m("/var/lib/persist/module/hakurei/0/1"), m("/var/lib/persist/module/hakurei/0/1"), container.BindWritable|container.BindEnsure).
Remount(m("/dev/"), syscall.MS_RDONLY).
Tmpfs(m("/dev/shm"), 0, 01777).
Tmpfs(m("/run/user/"), 4096, 0755).
Bind(m("/tmp/hakurei.1971/runtime/1"), m("/run/user/1971"), container.BindWritable).
Bind(m("/tmp/hakurei.1971/tmpdir/1"), m("/tmp/"), container.BindWritable).

View File

@@ -1,6 +1,7 @@
package app_test
import (
"context"
"os"
"syscall"
@@ -23,7 +24,7 @@ var testCasesPd = []sealTestCase{
0xbd, 0x01, 0x78, 0x0e,
0xb9, 0xa6, 0x07, 0xac,
},
system.New(1000000).
system.New(context.TODO(), 1000000).
Ensure("/tmp/hakurei.1971", 0711).
Ensure("/tmp/hakurei.1971/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime", acl.Execute).
Ensure("/tmp/hakurei.1971/runtime/0", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/0", acl.Read, acl.Write, acl.Execute).
@@ -47,13 +48,13 @@ var testCasesPd = []sealTestCase{
Proc(m("/proc/")).
Tmpfs(hst.AbsTmp, 4096, 0755).
DevWritable(m("/dev/"), true).
Tmpfs(m("/dev/shm"), 0, 01777).
Bind(m("/dev/kvm"), m("/dev/kvm"), container.BindWritable|container.BindDevice|container.BindOptional).
Readonly(m("/var/run/nscd"), 0755).
Etc(m("/etc/"), "4a450b6596d7bc15bd01780eb9a607ac").
Tmpfs(m("/run/user/1971"), 8192, 0755).
Tmpfs(m("/run/dbus"), 8192, 0755).
Remount(m("/dev/"), syscall.MS_RDONLY).
Tmpfs(m("/dev/shm"), 0, 01777).
Tmpfs(m("/run/user/"), 4096, 0755).
Bind(m("/tmp/hakurei.1971/runtime/0"), m("/run/user/65534"), container.BindWritable).
Bind(m("/tmp/hakurei.1971/tmpdir/0"), m("/tmp/"), container.BindWritable).
@@ -115,7 +116,7 @@ var testCasesPd = []sealTestCase{
0x82, 0xd4, 0x13, 0x36,
0x9b, 0x64, 0xce, 0x7c,
},
system.New(1000009).
system.New(context.TODO(), 1000009).
Ensure("/tmp/hakurei.1971", 0711).
Ensure("/tmp/hakurei.1971/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime", acl.Execute).
Ensure("/tmp/hakurei.1971/runtime/9", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/9", acl.Read, acl.Write, acl.Execute).
@@ -183,6 +184,7 @@ var testCasesPd = []sealTestCase{
Proc(m("/proc/")).
Tmpfs(hst.AbsTmp, 4096, 0755).
DevWritable(m("/dev/"), true).
Tmpfs(m("/dev/shm"), 0, 01777).
Bind(m("/dev/dri"), m("/dev/dri"), container.BindWritable|container.BindDevice|container.BindOptional).
Bind(m("/dev/kvm"), m("/dev/kvm"), container.BindWritable|container.BindDevice|container.BindOptional).
Readonly(m("/var/run/nscd"), 0755).
@@ -190,7 +192,6 @@ var testCasesPd = []sealTestCase{
Tmpfs(m("/run/user/1971"), 8192, 0755).
Tmpfs(m("/run/dbus"), 8192, 0755).
Remount(m("/dev/"), syscall.MS_RDONLY).
Tmpfs(m("/dev/shm"), 0, 01777).
Tmpfs(m("/run/user/"), 4096, 0755).
Bind(m("/tmp/hakurei.1971/runtime/9"), m("/run/user/65534"), container.BindWritable).
Bind(m("/tmp/hakurei.1971/tmpdir/9"), m("/tmp/"), container.BindWritable).

View File

@@ -11,18 +11,17 @@ import (
"hakurei.app/hst"
"hakurei.app/internal/app"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/internal/sys"
"hakurei.app/system"
)
type sealTestCase struct {
name string
os sys.State
config *hst.Config
id state.ID
wantSys *system.I
wantContainer *container.Params
name string
os sys.State
config *hst.Config
id state.ID
wantSys *system.I
wantParams *container.Params
}
func TestApp(t *testing.T) {
@@ -30,35 +29,27 @@ func TestApp(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
a := app.NewWithID(tc.id, tc.os)
var (
gotSys *system.I
gotContainer *container.Params
)
if !t.Run("seal", func(t *testing.T) {
if sa, err := a.Seal(tc.config); err != nil {
hlog.PrintBaseError(err, "got generic error:")
t.Errorf("Seal: error = %v", err)
return
} else {
gotSys, gotContainer = app.AppIParams(a, sa)
t.Run("finalise", func(t *testing.T) {
sys, params, err := app.FinaliseIParams(t.Context(), tc.os, tc.config, &tc.id)
if err != nil {
if s, ok := container.GetErrorMessage(err); !ok {
t.Fatalf("Seal: error = %v", err)
} else {
t.Fatalf("Seal: %s", s)
}
}
}) {
return
}
t.Run("compare sys", func(t *testing.T) {
if !gotSys.Equal(tc.wantSys) {
t.Errorf("Seal: sys = %#v, want %#v",
gotSys, tc.wantSys)
}
})
t.Run("sys", func(t *testing.T) {
if !sys.Equal(tc.wantSys) {
t.Errorf("Seal: sys = %#v, want %#v", sys, tc.wantSys)
}
})
t.Run("compare params", func(t *testing.T) {
if !reflect.DeepEqual(gotContainer, tc.wantContainer) {
t.Errorf("seal: params =\n%s\n, want\n%s",
mustMarshal(gotContainer), mustMarshal(tc.wantContainer))
}
t.Run("params", func(t *testing.T) {
if !reflect.DeepEqual(params, tc.wantParams) {
t.Errorf("seal: params =\n%s\n, want\n%s", mustMarshal(params), mustMarshal(tc.wantParams))
}
})
})
})
}

View File

@@ -11,20 +11,18 @@ import (
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/hst"
"hakurei.app/internal/hlog"
"hakurei.app/internal/sys"
"hakurei.app/system/dbus"
)
// in practice there should be less than 30 entries added by the runtime;
// allocating slightly more as a margin for future expansion
// in practice there should be less than 30 system mount points
const preallocateOpsCount = 1 << 5
// newContainer initialises [container.Params] via [hst.ContainerConfig].
// Note that remaining container setup must be queued by the caller.
func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid *int) (*container.Params, map[string]string, error) {
if s == nil {
return nil, nil, hlog.WrapErr(syscall.EBADE, "invalid container configuration")
return nil, nil, newWithMessage("invalid container configuration")
}
params := &container.Params{
@@ -67,8 +65,6 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
}
if s.MapRealUID {
/* some programs fail to connect to dbus session running as a different uid
so this workaround is introduced to map priv-side caller uid in container */
params.Uid = os.Getuid()
*uid = params.Uid
params.Gid = os.Getgid()
@@ -102,8 +98,11 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
} else {
params.Bind(container.AbsFHSDev, container.AbsFHSDev, container.BindWritable|container.BindDevice)
}
// /dev is mounted readonly later on, this prevents /dev/shm from going readonly with it
params.Tmpfs(container.AbsFHSDev.Append("shm"), 0, 01777)
/* retrieve paths and hide them if they're made available in the sandbox;
this feature tries to improve user experience of permissive defaults, and
to warn about issues in custom configuration; it is NOT a security feature
and should not be treated as such, ALWAYS be careful with what you bind */
@@ -233,9 +232,7 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
// no more ContainerConfig paths beyond this point
if !s.Device {
params.
Remount(container.AbsFHSDev, syscall.MS_RDONLY).
Tmpfs(container.AbsFHSDev.Append("shm"), 0, 01777)
params.Remount(container.AbsFHSDev, syscall.MS_RDONLY)
}
return params, maps.Clone(s.Env), nil

View File

@@ -1,181 +0,0 @@
package app
import (
"errors"
"log"
"hakurei.app/internal/hlog"
)
func PrintRunStateErr(rs *RunState, runErr error) (code int) {
code = rs.ExitStatus()
if runErr != nil {
if rs.Time == nil {
hlog.PrintBaseError(runErr, "cannot start app:")
} else {
var e *hlog.BaseError
if !hlog.AsBaseError(runErr, &e) {
log.Println("wait failed:", runErr)
} else {
// Wait only returns either *app.ProcessError or *app.StateStoreError wrapped in a *app.BaseError
var se *StateStoreError
if !errors.As(runErr, &se) {
// does not need special handling
log.Print(e.Message())
} else {
// inner error are either unwrapped store errors
// or joined errors returned by *appSealTx revert
// wrapped in *app.BaseError
var ej RevertCompoundError
if !errors.As(se.InnerErr, &ej) {
// does not require special handling
log.Print(e.Message())
} else {
errs := ej.Unwrap()
// every error here is wrapped in *app.BaseError
for _, ei := range errs {
var eb *hlog.BaseError
if !errors.As(ei, &eb) {
// unreachable
log.Println("invalid error type returned by revert:", ei)
} else {
// print inner *app.BaseError message
log.Print(eb.Message())
}
}
}
}
}
}
if code == 0 {
code = 126
}
}
if rs.RevertErr != nil {
var stateStoreError *StateStoreError
if !errors.As(rs.RevertErr, &stateStoreError) || stateStoreError == nil {
hlog.PrintBaseError(rs.RevertErr, "generic fault during cleanup:")
goto out
}
if stateStoreError.Err != nil {
if len(stateStoreError.Err) == 2 {
if stateStoreError.Err[0] != nil {
if joinedErrs, ok := stateStoreError.Err[0].(interface{ Unwrap() []error }); !ok {
hlog.PrintBaseError(stateStoreError.Err[0], "generic fault during revert:")
} else {
for _, err := range joinedErrs.Unwrap() {
if err != nil {
hlog.PrintBaseError(err, "fault during revert:")
}
}
}
}
if stateStoreError.Err[1] != nil {
log.Printf("cannot close store: %v", stateStoreError.Err[1])
}
} else {
log.Printf("fault during cleanup: %v",
errors.Join(stateStoreError.Err...))
}
}
if stateStoreError.OpErr != nil {
log.Printf("blind revert due to store fault: %v",
stateStoreError.OpErr)
}
if stateStoreError.DoErr != nil {
hlog.PrintBaseError(stateStoreError.DoErr, "state store operation unsuccessful:")
}
if stateStoreError.Inner && stateStoreError.InnerErr != nil {
hlog.PrintBaseError(stateStoreError.InnerErr, "cannot destroy state entry:")
}
out:
if code == 0 {
code = 128
}
}
if rs.WaitErr != nil {
hlog.Verbosef("wait: %v", rs.WaitErr)
}
return
}
// StateStoreError is returned for a failed state save
type StateStoreError struct {
// whether inner function was called
Inner bool
// returned by the Save/Destroy method of [state.Cursor]
InnerErr error
// returned by the Do method of [state.Store]
DoErr error
// stores an arbitrary store operation error
OpErr error
// stores arbitrary errors
Err []error
}
// save saves arbitrary errors in [StateStoreError] once.
func (e *StateStoreError) save(errs ...error) {
if len(errs) == 0 || e.Err != nil {
panic("invalid call to save")
}
e.Err = errs
}
func (e *StateStoreError) equiv(a ...any) error {
if e.Inner && e.InnerErr == nil && e.DoErr == nil && e.OpErr == nil && errors.Join(e.Err...) == nil {
return nil
} else {
return hlog.WrapErrSuffix(e, a...)
}
}
func (e *StateStoreError) Error() string {
if e.Inner && e.InnerErr != nil {
return e.InnerErr.Error()
}
if e.DoErr != nil {
return e.DoErr.Error()
}
if e.OpErr != nil {
return e.OpErr.Error()
}
if err := errors.Join(e.Err...); err != nil {
return err.Error()
}
// equiv nullifies e for values where this is reached
panic("unreachable")
}
func (e *StateStoreError) Unwrap() (errs []error) {
errs = make([]error, 0, 3)
if e.InnerErr != nil {
errs = append(errs, e.InnerErr)
}
if e.DoErr != nil {
errs = append(errs, e.DoErr)
}
if e.OpErr != nil {
errs = append(errs, e.OpErr)
}
if err := errors.Join(e.Err...); err != nil {
errs = append(errs, err)
}
return
}
// A RevertCompoundError encapsulates errors returned by
// the Revert method of [system.I].
type RevertCompoundError interface {
Error() string
Unwrap() []error
}

View File

@@ -1,24 +0,0 @@
package app
import (
"hakurei.app/container"
"hakurei.app/internal/app/state"
"hakurei.app/internal/sys"
"hakurei.app/system"
)
func NewWithID(id state.ID, os sys.State) App {
a := new(app)
a.id = newID(&id)
a.sys = os
return a
}
func AppIParams(a App, sa SealedApp) (*system.I, *container.Params) {
v := a.(*app)
seal := sa.(*outcome)
if v.outcome != seal || v.id != seal.id {
panic("broken app/outcome link")
}
return seal.sys, seal.container
}

View File

@@ -0,0 +1,16 @@
package app
import (
"context"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/internal/sys"
"hakurei.app/system"
)
func FinaliseIParams(ctx context.Context, k sys.State, config *hst.Config, id *state.ID) (*system.I, *container.Params, error) {
seal := outcome{id: &stringPair[state.ID]{*id, id.String()}}
return seal.sys, seal.container, seal.finalise(ctx, k, config)
}

339
internal/app/process.go Normal file
View File

@@ -0,0 +1,339 @@
package app
import (
"context"
"encoding/gob"
"errors"
"log"
"os"
"os/exec"
"strconv"
"strings"
"syscall"
"time"
"hakurei.app/container"
"hakurei.app/internal"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/system"
)
// duration to wait for shim to exit, after container WaitDelay has elapsed.
const shimWaitTimeout = 5 * time.Second
// mainState holds persistent state bound to outcome.main.
type mainState struct {
// done is whether beforeExit has been called already.
done bool
// Time is the exact point in time where the process was created.
// Location must be set to UTC.
//
// Time is nil if no process was ever created.
Time *time.Time
seal *outcome
store state.Store
cancel context.CancelFunc
cmd *exec.Cmd
cmdWait chan error
uintptr
}
const (
// mainNeedsRevert indicates the call to Commit has succeeded.
mainNeedsRevert uintptr = 1 << iota
// mainNeedsDestroy indicates the instance state entry is present in the store.
mainNeedsDestroy
)
// beforeExit must be called immediately before a call to [os.Exit].
func (ms mainState) beforeExit(isFault bool) {
if ms.done {
panic("attempting to call beforeExit twice")
}
ms.done = true
defer hlog.BeforeExit()
if isFault && ms.cancel != nil {
ms.cancel()
}
var hasErr bool
// updates hasErr but does not terminate
perror := func(err error, message string) {
hasErr = true
printMessageError("cannot "+message+":", err)
}
exitCode := 1
defer func() {
if hasErr {
os.Exit(exitCode)
}
}()
// this also handles wait for a non-fault termination
if ms.cmd != nil && ms.cmdWait != nil {
waitDone := make(chan struct{})
// TODO(ophestra): enforce this limit early so it does not have to be done twice
shimTimeoutCompensated := shimWaitTimeout
if ms.seal.waitDelay > MaxShimWaitDelay {
shimTimeoutCompensated += MaxShimWaitDelay
} else {
shimTimeoutCompensated += ms.seal.waitDelay
}
// this ties waitDone to ctx with the additional compensated timeout duration
go func() { <-ms.seal.ctx.Done(); time.Sleep(shimTimeoutCompensated); close(waitDone) }()
select {
case err := <-ms.cmdWait:
wstatus, ok := ms.cmd.ProcessState.Sys().(syscall.WaitStatus)
if ok {
if v := wstatus.ExitStatus(); v != 0 {
hasErr = true
exitCode = v
}
}
if hlog.Load() {
if !ok {
if err != nil {
hlog.Verbosef("wait: %v", err)
}
} else {
switch {
case wstatus.Exited():
hlog.Verbosef("process %d exited with code %d", ms.cmd.Process.Pid, wstatus.ExitStatus())
case wstatus.CoreDump():
hlog.Verbosef("process %d dumped core", ms.cmd.Process.Pid)
case wstatus.Signaled():
hlog.Verbosef("process %d got %s", ms.cmd.Process.Pid, wstatus.Signal())
default:
hlog.Verbosef("process %d exited with status %#x", ms.cmd.Process.Pid, wstatus)
}
}
}
case <-waitDone:
hlog.Resume()
// this is only reachable when shim did not exit within shimWaitTimeout, after its WaitDelay has elapsed.
// This is different from the container failing to terminate within its timeout period, as that is enforced
// by the shim. This path is instead reached when there is a lockup in shim preventing it from completing.
log.Printf("process %d did not terminate", ms.cmd.Process.Pid)
}
hlog.Resume()
if ms.seal.sync != nil {
if err := ms.seal.sync.Close(); err != nil {
perror(err, "close wayland security context")
}
}
if ms.seal.dbusMsg != nil {
ms.seal.dbusMsg()
}
}
if ms.uintptr&mainNeedsRevert != 0 {
if ok, err := ms.store.Do(ms.seal.user.identity.unwrap(), func(c state.Cursor) {
if ms.uintptr&mainNeedsDestroy != 0 {
if err := c.Destroy(ms.seal.id.unwrap()); err != nil {
perror(err, "destroy state entry")
}
}
var rt system.Enablement
if states, err := c.Load(); err != nil {
// it is impossible to continue from this point;
// revert per-process state here to limit damage
ec := system.Process
if revertErr := ms.seal.sys.Revert((*system.Criteria)(&ec)); revertErr != nil {
var joinError interface {
Unwrap() []error
error
}
if !errors.As(revertErr, &joinError) || joinError == nil {
perror(revertErr, "revert system setup")
} else {
for _, v := range joinError.Unwrap() {
perror(v, "revert system setup step")
}
}
}
perror(err, "load instance states")
} else {
ec := system.Process
if l := len(states); l == 0 {
ec |= system.User
} else {
hlog.Verbosef("found %d instances, cleaning up without user-scoped operations", l)
}
// accumulate enablements of remaining launchers
for i, s := range states {
if s.Config != nil {
rt |= s.Config.Enablements.Unwrap()
} else {
log.Printf("state entry %d does not contain config", i)
}
}
ec |= rt ^ (system.EWayland | system.EX11 | system.EDBus | system.EPulse)
if hlog.Load() {
if ec > 0 {
hlog.Verbose("reverting operations scope", system.TypeString(ec))
}
}
if err = ms.seal.sys.Revert((*system.Criteria)(&ec)); err != nil {
perror(err, "revert system setup")
}
}
}); err != nil {
if ok {
perror(err, "unlock state store")
} else {
perror(err, "open state store")
}
}
} else if ms.uintptr&mainNeedsDestroy != 0 {
panic("unreachable")
}
if ms.store != nil {
if err := ms.store.Close(); err != nil {
perror(err, "close state store")
}
}
}
// fatal calls printMessageError, performs necessary cleanup, followed by a call to [os.Exit](1).
func (ms mainState) fatal(fallback string, ferr error) {
printMessageError(fallback, ferr)
ms.beforeExit(true)
os.Exit(1)
}
// main carries out outcome and terminates. main does not return.
func (seal *outcome) main() {
if !seal.f.CompareAndSwap(false, true) {
panic("outcome: attempted to run twice")
}
// read comp value early for early failure
hsuPath := internal.MustHsuPath()
// ms.beforeExit required beyond this point
ms := &mainState{seal: seal}
if err := seal.sys.Commit(); err != nil {
ms.fatal("cannot commit system setup:", err)
}
ms.uintptr |= mainNeedsRevert
ms.store = state.NewMulti(seal.runDirPath.String())
ctx, cancel := context.WithCancel(seal.ctx)
defer cancel()
ms.cancel = cancel
ms.cmd = exec.CommandContext(ctx, hsuPath)
ms.cmd.Stdin, ms.cmd.Stdout, ms.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
ms.cmd.Dir = container.FHSRoot // container init enters final working directory
// shim runs in the same session as monitor; see shim.go for behaviour
ms.cmd.Cancel = func() error { return ms.cmd.Process.Signal(syscall.SIGCONT) }
var e *gob.Encoder
if fd, encoder, err := container.Setup(&ms.cmd.ExtraFiles); err != nil {
ms.fatal("cannot create shim setup pipe:", err)
} else {
e = encoder
ms.cmd.Env = []string{
// passed through to shim by hsu
shimEnv + "=" + strconv.Itoa(fd),
// interpreted by hsu
"HAKUREI_APP_ID=" + seal.user.identity.String(),
}
}
if len(seal.user.supp) > 0 {
hlog.Verbosef("attaching supplementary group ids %s", seal.user.supp)
// interpreted by hsu
ms.cmd.Env = append(ms.cmd.Env, "HAKUREI_GROUPS="+strings.Join(seal.user.supp, " "))
}
hlog.Verbosef("setuid helper at %s", hsuPath)
hlog.Suspend()
if err := ms.cmd.Start(); err != nil {
ms.fatal("cannot start setuid wrapper:", err)
}
startTime := time.Now().UTC()
ms.cmdWait = make(chan error, 1)
// this ties context back to the life of the process
go func() { ms.cmdWait <- ms.cmd.Wait(); cancel() }()
ms.Time = &startTime
// unfortunately the I/O here cannot be directly canceled;
// the cancellation path leads to fatal in this case so that is fine
select {
case err := <-func() (setupErr chan error) {
setupErr = make(chan error, 1)
go func() {
setupErr <- e.Encode(&shimParams{
os.Getpid(),
seal.waitDelay,
seal.container,
hlog.Load(),
})
}()
return
}():
if err != nil {
hlog.Resume()
ms.fatal("cannot transmit shim config:", err)
}
case <-ctx.Done():
hlog.Resume()
ms.fatal("shim context canceled:", newWithMessageError("shim setup canceled", ctx.Err()))
}
// shim accepted setup payload, create process state
if ok, err := ms.store.Do(seal.user.identity.unwrap(), func(c state.Cursor) {
if err := c.Save(&state.State{
ID: seal.id.unwrap(),
PID: ms.cmd.Process.Pid,
Time: *ms.Time,
}, seal.ct); err != nil {
ms.fatal("cannot save state entry:", err)
}
}); err != nil {
if ok {
ms.uintptr |= mainNeedsDestroy
ms.fatal("cannot unlock state store:", err)
} else {
ms.fatal("cannot open state store:", err)
}
}
// state in store at this point, destroy defunct state entry on termination
ms.uintptr |= mainNeedsDestroy
// beforeExit ties shim process to context
ms.beforeExit(false)
os.Exit(0)
}
// printMessageError prints the error message according to [container.GetErrorMessage],
// or fallback prepended to err if an error message is not available.
func printMessageError(fallback string, err error) {
m, ok := container.GetErrorMessage(err)
if !ok {
log.Println(fallback, err)
return
}
log.Print(m)
}

View File

@@ -1,201 +0,0 @@
package app
import (
"context"
"encoding/gob"
"errors"
"log"
"os"
"os/exec"
"strconv"
"strings"
"syscall"
"time"
"hakurei.app/container"
"hakurei.app/internal"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/system"
)
const shimWaitTimeout = 5 * time.Second
func (seal *outcome) Run(rs *RunState) error {
if !seal.f.CompareAndSwap(false, true) {
// run does much more than just starting a process; calling it twice, even if the first call fails, will result
// in inconsistent state that is impossible to clean up; return here to limit damage and hopefully give the
// other Run a chance to return
return errors.New("outcome: attempted to run twice")
}
if rs == nil {
panic("invalid state")
}
// read comp value early to allow for early failure
hsuPath := internal.MustHsuPath()
if err := seal.sys.Commit(seal.ctx); err != nil {
return err
}
store := state.NewMulti(seal.runDirPath.String())
deferredStoreFunc := func(c state.Cursor) error { return nil } // noop until state in store
defer func() {
var revertErr error
storeErr := new(StateStoreError)
storeErr.Inner, storeErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) {
revertErr = func() error {
storeErr.InnerErr = deferredStoreFunc(c)
var rt system.Enablement
ec := system.Process
if states, err := c.Load(); err != nil {
// revert per-process state here to limit damage
storeErr.OpErr = err
return seal.sys.Revert((*system.Criteria)(&ec))
} else {
if l := len(states); l == 0 {
ec |= system.User
} else {
hlog.Verbosef("found %d instances, cleaning up without user-scoped operations", l)
}
// accumulate enablements of remaining launchers
for i, s := range states {
if s.Config != nil {
rt |= s.Config.Enablements.Unwrap()
} else {
log.Printf("state entry %d does not contain config", i)
}
}
}
ec |= rt ^ (system.EWayland | system.EX11 | system.EDBus | system.EPulse)
if hlog.Load() {
if ec > 0 {
hlog.Verbose("reverting operations scope", system.TypeString(ec))
}
}
return seal.sys.Revert((*system.Criteria)(&ec))
}()
})
storeErr.save(revertErr, store.Close())
rs.RevertErr = storeErr.equiv("error during cleanup:")
}()
ctx, cancel := context.WithCancel(seal.ctx)
defer cancel()
cmd := exec.CommandContext(ctx, hsuPath)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Dir = container.FHSRoot // container init enters final working directory
// shim runs in the same session as monitor; see shim.go for behaviour
cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGCONT) }
var e *gob.Encoder
if fd, encoder, err := container.Setup(&cmd.ExtraFiles); err != nil {
return hlog.WrapErrSuffix(err,
"cannot create shim setup pipe:")
} else {
e = encoder
cmd.Env = []string{
// passed through to shim by hsu
shimEnv + "=" + strconv.Itoa(fd),
// interpreted by hsu
"HAKUREI_APP_ID=" + seal.user.aid.String(),
}
}
if len(seal.user.supp) > 0 {
hlog.Verbosef("attaching supplementary group ids %s", seal.user.supp)
// interpreted by hsu
cmd.Env = append(cmd.Env, "HAKUREI_GROUPS="+strings.Join(seal.user.supp, " "))
}
hlog.Verbosef("setuid helper at %s", hsuPath)
hlog.Suspend()
if err := cmd.Start(); err != nil {
return hlog.WrapErrSuffix(err,
"cannot start setuid wrapper:")
}
rs.SetStart()
// this prevents blocking forever on an early failure
waitErr, setupErr := make(chan error, 1), make(chan error, 1)
go func() { waitErr <- cmd.Wait(); cancel() }()
go func() {
setupErr <- e.Encode(&shimParams{
os.Getpid(),
seal.waitDelay,
seal.container,
hlog.Load(),
})
}()
select {
case err := <-setupErr:
if err != nil {
hlog.Resume()
return hlog.WrapErrSuffix(err,
"cannot transmit shim config:")
}
case <-ctx.Done():
hlog.Resume()
return hlog.WrapErr(syscall.ECANCELED,
"shim setup canceled")
}
// returned after blocking on waitErr
var earlyStoreErr = new(StateStoreError)
{
// shim accepted setup payload, create process state
sd := state.State{
ID: seal.id.unwrap(),
PID: cmd.Process.Pid,
Time: *rs.Time,
}
earlyStoreErr.Inner, earlyStoreErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) {
earlyStoreErr.InnerErr = c.Save(&sd, seal.ct)
})
}
// state in store at this point, destroy defunct state entry on return
deferredStoreFunc = func(c state.Cursor) error { return c.Destroy(seal.id.unwrap()) }
waitTimeout := make(chan struct{})
go func() { <-seal.ctx.Done(); time.Sleep(shimWaitTimeout); close(waitTimeout) }()
select {
case rs.WaitErr = <-waitErr:
rs.WaitStatus = cmd.ProcessState.Sys().(syscall.WaitStatus)
if hlog.Load() {
switch {
case rs.Exited():
hlog.Verbosef("process %d exited with code %d", cmd.Process.Pid, rs.ExitStatus())
case rs.CoreDump():
hlog.Verbosef("process %d dumped core", cmd.Process.Pid)
case rs.Signaled():
hlog.Verbosef("process %d got %s", cmd.Process.Pid, rs.Signal())
default:
hlog.Verbosef("process %d exited with status %#x", cmd.Process.Pid, rs.WaitStatus)
}
}
case <-waitTimeout:
rs.WaitErr = syscall.ETIMEDOUT
hlog.Resume()
log.Printf("process %d did not terminate", cmd.Process.Pid)
}
hlog.Resume()
if seal.sync != nil {
if err := seal.sync.Close(); err != nil {
log.Printf("cannot close wayland security context: %v", err)
}
}
if seal.dbusMsg != nil {
seal.dbusMsg()
}
return earlyStoreErr.equiv("cannot save process state:")
}

View File

@@ -9,8 +9,7 @@ import (
"io"
"io/fs"
"os"
"path"
"regexp"
"os/user"
"slices"
"strconv"
"strings"
@@ -20,7 +19,6 @@ import (
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/internal/sys"
@@ -30,39 +28,12 @@ import (
"hakurei.app/system/wayland"
)
const (
home = "HOME"
shell = "SHELL"
func newWithMessage(msg string) error { return newWithMessageError(msg, os.ErrInvalid) }
func newWithMessageError(msg string, err error) error {
return &hst.AppError{Step: "finalise", Err: err, Msg: msg}
}
xdgConfigHome = "XDG_CONFIG_HOME"
xdgRuntimeDir = "XDG_RUNTIME_DIR"
xdgSessionClass = "XDG_SESSION_CLASS"
xdgSessionType = "XDG_SESSION_TYPE"
term = "TERM"
display = "DISPLAY"
pulseServer = "PULSE_SERVER"
pulseCookie = "PULSE_COOKIE"
dbusSessionBusAddress = "DBUS_SESSION_BUS_ADDRESS"
dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS"
)
var (
ErrIdent = errors.New("invalid identity")
ErrName = errors.New("invalid username")
ErrXDisplay = errors.New(display + " unset")
ErrPulseCookie = errors.New("pulse cookie not present")
ErrPulseSocket = errors.New("pulse socket not present")
ErrPulseMode = errors.New("unexpected pulse socket mode")
)
var posixUsername = regexp.MustCompilePOSIX("^[a-z_]([A-Za-z0-9_-]{0,31}|[A-Za-z0-9_-]{0,30}\\$)$")
// outcome stores copies of various parts of [hst.Config]
// An outcome is the runnable state of a hakurei container via [hst.Config].
type outcome struct {
// copied from initialising [app]
id *stringPair[state.ID]
@@ -136,8 +107,7 @@ func (share *shareHost) runtime() *container.Absolute {
// hsuUser stores post-hsu credentials and metadata
type hsuUser struct {
// identity
aid *stringPair[int]
identity *stringPair[int]
// target uid resolved by hid:aid
uid *stringPair[int]
@@ -150,57 +120,80 @@ type hsuUser struct {
username string
}
func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Config) error {
func (seal *outcome) finalise(ctx context.Context, k sys.State, config *hst.Config) error {
const (
home = "HOME"
shell = "SHELL"
xdgConfigHome = "XDG_CONFIG_HOME"
xdgRuntimeDir = "XDG_RUNTIME_DIR"
xdgSessionClass = "XDG_SESSION_CLASS"
xdgSessionType = "XDG_SESSION_TYPE"
term = "TERM"
display = "DISPLAY"
pulseServer = "PULSE_SERVER"
pulseCookie = "PULSE_COOKIE"
dbusSessionBusAddress = "DBUS_SESSION_BUS_ADDRESS"
dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS"
)
if ctx == nil {
// unreachable
panic("invalid call to finalise")
}
if seal.ctx != nil {
panic("finalise called twice")
// unreachable
panic("attempting to finalise twice")
}
seal.ctx = ctx
if config == nil {
return hlog.WrapErr(syscall.EINVAL, syscall.EINVAL.Error())
// unreachable
return newWithMessage("invalid configuration")
}
if config.Home == nil {
return hlog.WrapErr(os.ErrInvalid, "invalid path to home directory")
return newWithMessage("invalid path to home directory")
}
{
// encode initial configuration for state tracking
ct := new(bytes.Buffer)
if err := gob.NewEncoder(ct).Encode(config); err != nil {
return hlog.WrapErrSuffix(err,
"cannot encode initial config:")
return &hst.AppError{Step: "encode initial config", Err: err}
}
seal.ct = ct
}
// allowed aid range 0 to 9999, this is checked again in hsu
// allowed identity range 0 to 9999, this is checked again in hsu
if config.Identity < 0 || config.Identity > 9999 {
return hlog.WrapErr(ErrIdent,
fmt.Sprintf("identity %d out of range", config.Identity))
return newWithMessage(fmt.Sprintf("identity %d out of range", config.Identity))
}
seal.user = hsuUser{
aid: newInt(config.Identity),
identity: newInt(config.Identity),
home: config.Home,
username: config.Username,
}
if seal.user.username == "" {
seal.user.username = "chronos"
} else if !posixUsername.MatchString(seal.user.username) ||
len(seal.user.username) >= internal.Sysconf(internal.SC_LOGIN_NAME_MAX) {
return hlog.WrapErr(ErrName,
fmt.Sprintf("invalid user name %q", seal.user.username))
}
if u, err := sys.Uid(seal.user.aid.unwrap()); err != nil {
return err
} else {
seal.user.uid = newInt(u)
} else if !isValidUsername(seal.user.username) {
return newWithMessage(fmt.Sprintf("invalid user name %q", seal.user.username))
}
seal.user.uid = newInt(sys.MustUid(k, seal.user.identity.unwrap()))
seal.user.supp = make([]string, len(config.Groups))
for i, name := range config.Groups {
if g, err := sys.LookupGroup(name); err != nil {
return hlog.WrapErr(err,
fmt.Sprintf("unknown group %q", name))
if g, err := k.LookupGroup(name); err != nil {
var unknownGroupError user.UnknownGroupError
if errors.As(err, &unknownGroupError) {
return newWithMessageError(fmt.Sprintf("unknown group %q", name), unknownGroupError)
} else {
return &hst.AppError{Step: "look up group by name", Err: err}
}
} else {
seal.user.supp[i] = g.Gid
}
@@ -212,7 +205,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
if config.Shell == nil {
config.Shell = container.AbsFHSRoot.Append("bin", "sh")
s, _ := sys.LookupEnv(shell)
s, _ := k.LookupEnv(shell)
if a, err := container.NewAbs(s); err == nil {
config.Shell = a
}
@@ -221,10 +214,10 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
// hsu clears the environment so resolve paths early
if config.Path == nil {
if len(config.Args) > 0 {
if p, err := sys.LookPath(config.Args[0]); err != nil {
return hlog.WrapErr(err, err.Error())
if p, err := k.LookPath(config.Args[0]); err != nil {
return &hst.AppError{Step: "look up executable file", Err: err}
} else if config.Path, err = container.NewAbs(p); err != nil {
return hlog.WrapErr(err, err.Error())
return newWithMessageError(err.Error(), err)
}
} else {
config.Path = config.Shell
@@ -239,7 +232,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
Filesystem: []hst.FilesystemConfigJSON{
// autoroot, includes the home directory
{&hst.FSBind{
{FilesystemConfig: &hst.FSBind{
Target: container.AbsFHSRoot,
Source: container.AbsFHSRoot,
Write: true,
@@ -257,7 +250,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
// hide nscd from container if present
nscd := container.AbsFHSVar.Append("run/nscd")
if _, err := sys.Stat(nscd.String()); !errors.Is(err, fs.ErrNotExist) {
if _, err := k.Stat(nscd.String()); !errors.Is(err, fs.ErrNotExist) {
conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSEphemeral{Target: nscd}})
}
@@ -275,21 +268,20 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
// late nil checks for pd behaviour
if config.Shell == nil {
return hlog.WrapErr(syscall.EINVAL, "invalid shell path")
return newWithMessage("invalid shell path")
}
if config.Path == nil {
return hlog.WrapErr(syscall.EINVAL, "invalid program path")
return newWithMessage("invalid program path")
}
var mapuid, mapgid *stringPair[int]
{
var uid, gid int
var err error
seal.container, seal.env, err = newContainer(config.Container, sys, seal.id.String(), &uid, &gid)
seal.container, seal.env, err = newContainer(config.Container, k, seal.id.String(), &uid, &gid)
seal.waitDelay = config.Container.WaitDelay
if err != nil {
return hlog.WrapErrSuffix(err,
"cannot initialise container configuration:")
return &hst.AppError{Step: "initialise container configuration", Err: err}
}
if len(config.Args) == 0 {
config.Args = []string{config.Path.String()}
@@ -310,16 +302,16 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
seal.env[xdgSessionClass] = "user"
seal.env[xdgSessionType] = "tty"
share := &shareHost{seal: seal, sc: sys.Paths()}
share := &shareHost{seal: seal, sc: k.Paths()}
seal.runDirPath = share.sc.RunDirPath
seal.sys = system.New(seal.user.uid.unwrap())
seal.sys = system.New(seal.ctx, seal.user.uid.unwrap())
seal.sys.Ensure(share.sc.SharePath.String(), 0711)
{
runtimeDir := share.sc.SharePath.Append("runtime")
seal.sys.Ensure(runtimeDir.String(), 0700)
seal.sys.UpdatePermType(system.User, runtimeDir.String(), acl.Execute)
runtimeDirInst := runtimeDir.Append(seal.user.aid.String())
runtimeDirInst := runtimeDir.Append(seal.user.identity.String())
seal.sys.Ensure(runtimeDirInst.String(), 0700)
seal.sys.UpdatePermType(system.User, runtimeDirInst.String(), acl.Read, acl.Write, acl.Execute)
seal.container.Tmpfs(container.AbsFHSRunUser, 1<<12, 0755)
@@ -330,7 +322,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
tmpdir := share.sc.SharePath.Append("tmpdir")
seal.sys.Ensure(tmpdir.String(), 0700)
seal.sys.UpdatePermType(system.User, tmpdir.String(), acl.Execute)
tmpdirInst := tmpdir.Append(seal.user.aid.String())
tmpdirInst := tmpdir.Append(seal.user.identity.String())
seal.sys.Ensure(tmpdirInst.String(), 01700)
seal.sys.UpdatePermType(system.User, tmpdirInst.String(), acl.Read, acl.Write, acl.Execute)
// mount inner /tmp from share so it shares persistence and storage behaviour of host /tmp
@@ -354,14 +346,14 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
}
// pass TERM for proper terminal I/O in initial process
if t, ok := sys.LookupEnv(term); ok {
if t, ok := k.LookupEnv(term); ok {
seal.env[term] = t
}
if config.Enablements.Unwrap()&system.EWayland != 0 {
// outer wayland socket (usually `/run/user/%d/wayland-%d`)
var socketPath *container.Absolute
if name, ok := sys.LookupEnv(wayland.WaylandDisplay); !ok {
if name, ok := k.LookupEnv(wayland.WaylandDisplay); !ok {
hlog.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName)
socketPath = share.sc.RuntimePath.Append(wayland.FallbackName)
} else if a, err := container.NewAbs(name); err != nil {
@@ -392,9 +384,8 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
}
if config.Enablements.Unwrap()&system.EX11 != 0 {
if d, ok := sys.LookupEnv(display); !ok {
return hlog.WrapErr(ErrXDisplay,
"DISPLAY is not set")
if d, ok := k.LookupEnv(display); !ok {
return newWithMessage("DISPLAY is not set")
} else {
socketDir := container.AbsFHSTmp.Append(".X11-unix")
@@ -411,14 +402,15 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
}
}
if socketPath != nil {
if _, err := sys.Stat(socketPath.String()); err != nil {
if _, err := k.Stat(socketPath.String()); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return hlog.WrapErrSuffix(err,
fmt.Sprintf("cannot access X11 socket %q:", socketPath))
return &hst.AppError{Step: fmt.Sprintf("access X11 socket %q", socketPath), Err: err}
}
} else {
seal.sys.UpdatePermType(system.EX11, socketPath.String(), acl.Read, acl.Write, acl.Execute)
d = "unix:" + socketPath.String()
if !config.Container.HostAbstract {
d = "unix:" + socketPath.String()
}
}
}
@@ -434,26 +426,21 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
// PulseAudio socket (usually `/run/user/%d/pulse/native`)
pulseSocket := pulseRuntimeDir.Append("native")
if _, err := sys.Stat(pulseRuntimeDir.String()); err != nil {
if _, err := k.Stat(pulseRuntimeDir.String()); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return hlog.WrapErrSuffix(err,
fmt.Sprintf("cannot access PulseAudio directory %q:", pulseRuntimeDir))
return &hst.AppError{Step: fmt.Sprintf("access PulseAudio directory %q", pulseRuntimeDir), Err: err}
}
return hlog.WrapErr(ErrPulseSocket,
fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir))
return newWithMessage(fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir))
}
if s, err := sys.Stat(pulseSocket.String()); err != nil {
if s, err := k.Stat(pulseSocket.String()); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return hlog.WrapErrSuffix(err,
fmt.Sprintf("cannot access PulseAudio socket %q:", pulseSocket))
return &hst.AppError{Step: fmt.Sprintf("access PulseAudio socket %q", pulseSocket), Err: err}
}
return hlog.WrapErr(ErrPulseSocket,
fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pulseRuntimeDir))
return newWithMessage(fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pulseRuntimeDir))
} else {
if m := s.Mode(); m&0o006 != 0o006 {
return hlog.WrapErr(ErrPulseMode,
fmt.Sprintf("unexpected permissions on %q:", pulseSocket), m)
return newWithMessage(fmt.Sprintf("unexpected permissions on %q: %s", pulseSocket, m))
}
}
@@ -465,15 +452,75 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
seal.env[pulseServer] = "unix:" + innerPulseSocket.String()
// publish current user's pulse cookie for target user
if src, err := discoverPulseCookie(sys); err != nil {
// not fatal
hlog.Verbose(strings.TrimSpace(err.(*hlog.BaseError).Message()))
} else {
var paCookiePath *container.Absolute
{
const paLocateStep = "locate PulseAudio cookie"
// from environment
if p, ok := k.LookupEnv(pulseCookie); ok {
if a, err := container.NewAbs(p); err != nil {
return &hst.AppError{Step: paLocateStep, Err: err}
} else {
// this takes precedence, do not verify whether the file is accessible
paCookiePath = a
goto out
}
}
// $HOME/.pulse-cookie
if p, ok := k.LookupEnv(home); ok {
if a, err := container.NewAbs(p); err != nil {
return &hst.AppError{Step: paLocateStep, Err: err}
} else {
paCookiePath = a.Append(".pulse-cookie")
}
if s, err := k.Stat(paCookiePath.String()); err != nil {
paCookiePath = nil
if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: "access PulseAudio cookie", Err: err}
}
// fallthrough
} else if s.IsDir() {
paCookiePath = nil
} else {
goto out
}
}
// $XDG_CONFIG_HOME/pulse/cookie
if p, ok := k.LookupEnv(xdgConfigHome); ok {
if a, err := container.NewAbs(p); err != nil {
return &hst.AppError{Step: paLocateStep, Err: err}
} else {
paCookiePath = a.Append("pulse", "cookie")
}
if s, err := k.Stat(paCookiePath.String()); err != nil {
paCookiePath = nil
if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: "access PulseAudio cookie", Err: err}
}
// fallthrough
} else if s.IsDir() {
paCookiePath = nil
} else {
goto out
}
}
out:
}
if paCookiePath != nil {
innerDst := hst.AbsTmp.Append("/pulse-cookie")
seal.env[pulseCookie] = innerDst.String()
var payload *[]byte
seal.container.PlaceP(innerDst, &payload)
seal.sys.CopyFile(payload, src, 256, 256)
seal.sys.CopyFile(payload, paCookiePath.String(), 256, 256)
} else {
hlog.Verbose("cannot locate PulseAudio cookie (tried " +
"$PULSE_COOKIE, " +
"$XDG_CONFIG_HOME/pulse/cookie, " +
"$HOME/.pulse-cookie)")
}
}
@@ -539,8 +586,8 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
seal.container.Env = make([]string, 0, len(seal.env))
for k, v := range seal.env {
if strings.IndexByte(k, '=') != -1 {
return hlog.WrapErr(syscall.EINVAL,
fmt.Sprintf("invalid environment variable %s", k))
return &hst.AppError{Step: "flatten environment", Err: syscall.EINVAL,
Msg: fmt.Sprintf("invalid environment variable %s", k)}
}
seal.container.Env = append(seal.container.Env, k+"="+v)
}
@@ -553,42 +600,3 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
return nil
}
// discoverPulseCookie attempts various standard methods to discover the current user's PulseAudio authentication cookie
func discoverPulseCookie(sys sys.State) (string, error) {
if p, ok := sys.LookupEnv(pulseCookie); ok {
return p, nil
}
// dotfile $HOME/.pulse-cookie
if p, ok := sys.LookupEnv(home); ok {
p = path.Join(p, ".pulse-cookie")
if s, err := sys.Stat(p); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return p, hlog.WrapErrSuffix(err,
fmt.Sprintf("cannot access PulseAudio cookie %q:", p))
}
// not found, try next method
} else if !s.IsDir() {
return p, nil
}
}
// $XDG_CONFIG_HOME/pulse/cookie
if p, ok := sys.LookupEnv(xdgConfigHome); ok {
p = path.Join(p, "pulse", "cookie")
if s, err := sys.Stat(p); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return p, hlog.WrapErrSuffix(err,
fmt.Sprintf("cannot access PulseAudio cookie %q:", p))
}
// not found, try next method
} else if !s.IsDir() {
return p, nil
}
}
return "", hlog.WrapErr(ErrPulseCookie,
fmt.Sprintf("cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)",
pulseCookie, xdgConfigHome, home))
}

View File

@@ -45,8 +45,10 @@ const (
// ShimExitOrphan is returned when the shim is orphaned before monitor delivers a signal.
ShimExitOrphan = 3
// DefaultShimWaitDelay is used when WaitDelay has its zero value.
DefaultShimWaitDelay = 5 * time.Second
MaxShimWaitDelay = 30 * time.Second
// MaxShimWaitDelay is used instead if WaitDelay exceeds its value.
MaxShimWaitDelay = 30 * time.Second
)
// ShimMain is the main function of the shim process and runs as the unconstrained target user.
@@ -65,7 +67,7 @@ func ShimMain() {
if errors.Is(err, syscall.EBADF) {
log.Fatal("invalid config descriptor")
}
if errors.Is(err, container.ErrNotSet) {
if errors.Is(err, container.ErrReceiveEnv) {
log.Fatal("HAKUREI_SHIM not set")
}
@@ -155,11 +157,11 @@ func ShimMain() {
}
if err := z.Start(); err != nil {
hlog.PrintBaseError(err, "cannot start container:")
printMessageError("cannot start container:", err)
os.Exit(1)
}
if err := z.Serve(); err != nil {
hlog.PrintBaseError(err, "cannot configure container:")
printMessageError("cannot configure container:", err)
}
if err := seccomp.Load(

View File

@@ -27,27 +27,27 @@ type multiStore struct {
lock sync.RWMutex
}
func (s *multiStore) Do(aid int, f func(c Cursor)) (bool, error) {
func (s *multiStore) Do(identity int, f func(c Cursor)) (bool, error) {
s.lock.RLock()
defer s.lock.RUnlock()
// load or initialise new backend
b := new(multiBackend)
b.lock.Lock()
if v, ok := s.backends.LoadOrStore(aid, b); ok {
if v, ok := s.backends.LoadOrStore(identity, b); ok {
b = v.(*multiBackend)
} else {
b.path = path.Join(s.base, strconv.Itoa(aid))
b.path = path.Join(s.base, strconv.Itoa(identity))
// ensure directory
if err := os.MkdirAll(b.path, 0700); err != nil && !errors.Is(err, fs.ErrExist) {
s.backends.CompareAndDelete(aid, b)
s.backends.CompareAndDelete(identity, b)
return false, err
}
// open locker file
if l, err := os.OpenFile(b.path+".lock", os.O_RDWR|os.O_CREATE, 0600); err != nil {
s.backends.CompareAndDelete(aid, b)
s.backends.CompareAndDelete(identity, b)
return false, err
} else {
b.lockfile = l

View File

@@ -17,7 +17,7 @@ type Store interface {
// Do calls f exactly once and ensures store exclusivity until f returns.
// Returns whether f is called and any errors during the locking process.
// Cursor provided to f becomes invalid as soon as f returns.
Do(aid int, f func(c Cursor)) (ok bool, err error)
Do(identity int, f func(c Cursor)) (ok bool, err error)
// List queries the store and returns a list of aids known to the store.
// Note that some or all returned aids might not have any active apps.

View File

@@ -2,12 +2,9 @@ package app
import (
"strconv"
"hakurei.app/internal/app/state"
)
func newInt(v int) *stringPair[int] { return &stringPair[int]{v, strconv.Itoa(v)} }
func newID(id *state.ID) *stringPair[state.ID] { return &stringPair[state.ID]{*id, id.String()} }
func newInt(v int) *stringPair[int] { return &stringPair[int]{v, strconv.Itoa(v)} }
// stringPair stores a value and its string representation.
type stringPair[T comparable] struct {

8
internal/app/sysconf.go Normal file
View File

@@ -0,0 +1,8 @@
package app
//#include <unistd.h>
import "C"
const _SC_LOGIN_NAME_MAX = C._SC_LOGIN_NAME_MAX
func sysconf(name C.int) int { return int(C.sysconf(name)) }

12
internal/app/username.go Normal file
View File

@@ -0,0 +1,12 @@
package app
import "regexp"
// nameRegex is the default NAME_REGEX value from adduser.
var nameRegex = regexp.MustCompilePOSIX(`^[a-zA-Z][a-zA-Z0-9_-]*\$?$`)
// isValidUsername returns whether the argument is a valid username
func isValidUsername(username string) bool {
return len(username) < sysconf(_SC_LOGIN_NAME_MAX) &&
nameRegex.MatchString(username)
}

View File

@@ -1,81 +0,0 @@
package hlog
import (
"fmt"
"log"
"reflect"
"strings"
)
// baseError implements a basic error container
type baseError struct {
Err error
}
func (e *baseError) Error() string { return e.Err.Error() }
func (e *baseError) Unwrap() error { return e.Err }
// BaseError implements an error container with a user-facing message
type BaseError struct {
message string
baseError
}
// Message returns a user-facing error message
func (e *BaseError) Message() string { return e.message }
// WrapErr wraps an error with a corresponding message.
func WrapErr(err error, a ...any) error {
if err == nil {
return nil
}
return wrapErr(err, fmt.Sprintln(a...))
}
// WrapErrSuffix wraps an error with a corresponding message with err at the end of the message.
func WrapErrSuffix(err error, a ...any) error {
if err == nil {
return nil
}
return wrapErr(err, fmt.Sprintln(append(a, err)...))
}
// WrapErrFunc wraps an error with a corresponding message returned by f.
func WrapErrFunc(err error, f func(err error) string) error {
if err == nil {
return nil
}
return wrapErr(err, f(err))
}
func wrapErr(err error, message string) *BaseError {
return &BaseError{message, baseError{err}}
}
var (
baseErrorType = reflect.TypeFor[*BaseError]()
)
func AsBaseError(err error, target **BaseError) bool {
v := reflect.ValueOf(err)
if !v.CanConvert(baseErrorType) {
return false
}
*target = v.Convert(baseErrorType).Interface().(*BaseError)
return true
}
func PrintBaseError(err error, fallback string) {
var e *BaseError
if AsBaseError(err, &e) {
if msg := e.Message(); strings.TrimSpace(msg) != "" {
log.Print(msg)
return
}
Verbose("*"+fallback, err)
return
}
log.Println(fallback, err)
}

View File

@@ -2,11 +2,9 @@ package hlog
type Output struct{}
func (Output) IsVerbose() bool { return Load() }
func (Output) Verbose(v ...any) { Verbose(v...) }
func (Output) Verbosef(format string, v ...any) { Verbosef(format, v...) }
func (Output) WrapErr(err error, a ...any) error { return WrapErr(err, a...) }
func (Output) PrintBaseErr(err error, fallback string) { PrintBaseError(err, fallback) }
func (Output) Suspend() { Suspend() }
func (Output) Resume() bool { return Resume() }
func (Output) BeforeExit() { BeforeExit() }
func (Output) IsVerbose() bool { return Load() }
func (Output) Verbose(v ...any) { Verbose(v...) }
func (Output) Verbosef(format string, v ...any) { Verbosef(format, v...) }
func (Output) Suspend() { Suspend() }
func (Output) Resume() bool { return Resume() }
func (Output) BeforeExit() { BeforeExit() }

104
internal/sys/hsu.go Normal file
View File

@@ -0,0 +1,104 @@
package sys
import (
"errors"
"fmt"
"log"
"os"
"os/exec"
"strconv"
"sync"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
)
// Hsu caches responses from cmd/hsu.
type Hsu struct {
uidOnce sync.Once
uidCopy map[int]struct {
uid int
err error
}
uidMu sync.RWMutex
}
var ErrHsuAccess = errors.New("current user is not in the hsurc file")
func (h *Hsu) Uid(identity int) (int, error) {
h.uidOnce.Do(func() {
h.uidCopy = make(map[int]struct {
uid int
err error
})
})
{
h.uidMu.RLock()
u, ok := h.uidCopy[identity]
h.uidMu.RUnlock()
if ok {
return u.uid, u.err
}
}
h.uidMu.Lock()
defer h.uidMu.Unlock()
u := struct {
uid int
err error
}{}
defer func() { h.uidCopy[identity] = u }()
u.uid = -1
hsuPath := internal.MustHsuPath()
cmd := exec.Command(hsuPath)
cmd.Path = hsuPath
cmd.Stderr = os.Stderr // pass through fatal messages
cmd.Env = []string{"HAKUREI_APP_ID=" + strconv.Itoa(identity)}
cmd.Dir = container.FHSRoot
var (
p []byte
exitError *exec.ExitError
)
const step = "obtain uid from hsu"
if p, u.err = cmd.Output(); u.err == nil {
u.uid, u.err = strconv.Atoi(string(p))
if u.err != nil {
u.err = &hst.AppError{Step: step, Err: u.err, Msg: "invalid uid string from hsu"}
}
} else if errors.As(u.err, &exitError) && exitError != nil && exitError.ExitCode() == 1 {
// hsu prints an error message in this case
u.err = &hst.AppError{Step: step, Err: ErrHsuAccess}
} else if os.IsNotExist(u.err) {
u.err = &hst.AppError{Step: step, Err: os.ErrNotExist,
Msg: fmt.Sprintf("the setuid helper is missing: %s", hsuPath)}
}
return u.uid, u.err
}
// MustUid calls [State.Uid] and terminates on error.
func MustUid(s State, identity int) int {
uid, err := s.Uid(identity)
if err == nil {
return uid
}
const fallback = "cannot obtain uid from setuid wrapper:"
if errors.Is(err, ErrHsuAccess) {
hlog.Verbose("*"+fallback, err)
os.Exit(1)
return -0xdeadbeef
} else if m, ok := container.GetErrorMessage(err); ok {
log.Fatal(m)
return -0xdeadbeef
} else {
log.Fatalln(fallback, err)
return -0xdeadbeef
}
}

View File

@@ -49,14 +49,8 @@ type State interface {
Uid(identity int) (int, error)
}
// GetUserID obtains user id from hsu by querying uid of identity 0.
func GetUserID(os State) (int, error) {
if uid, err := os.Uid(0); err != nil {
return -1, err
} else {
return (uid / 10000) - 100, nil
}
}
// MustGetUserID obtains user id from hsu by querying uid of identity 0.
func MustGetUserID(os State) int { return (MustUid(os, 0) / 10000) - 100 }
// CopyPaths is a generic implementation of [hst.Paths].
func CopyPaths(os State, v *hst.Paths, userid int) {

View File

@@ -1,16 +1,12 @@
package sys
import (
"errors"
"fmt"
"io/fs"
"os"
"os/exec"
"os/user"
"path/filepath"
"strconv"
"sync"
"syscall"
"hakurei.app/container"
"hakurei.app/hst"
@@ -22,13 +18,7 @@ import (
type Std struct {
paths hst.Paths
pathsOnce sync.Once
uidOnce sync.Once
uidCopy map[int]struct {
uid int
err error
}
uidMu sync.RWMutex
Hsu
}
func (s *Std) Getuid() int { return os.Getuid() }
@@ -49,66 +39,6 @@ func (s *Std) Printf(format string, v ...any) { hlog.Verbosef(form
const xdgRuntimeDir = "XDG_RUNTIME_DIR"
func (s *Std) Paths() hst.Paths {
s.pathsOnce.Do(func() {
if userid, err := GetUserID(s); err != nil {
hlog.PrintBaseError(err, "cannot obtain user id from hsu:")
hlog.BeforeExit()
s.Exit(1)
} else {
CopyPaths(s, &s.paths, userid)
}
})
s.pathsOnce.Do(func() { CopyPaths(s, &s.paths, MustGetUserID(s)) })
return s.paths
}
func (s *Std) Uid(identity int) (int, error) {
s.uidOnce.Do(func() {
s.uidCopy = make(map[int]struct {
uid int
err error
})
})
{
s.uidMu.RLock()
u, ok := s.uidCopy[identity]
s.uidMu.RUnlock()
if ok {
return u.uid, u.err
}
}
s.uidMu.Lock()
defer s.uidMu.Unlock()
u := struct {
uid int
err error
}{}
defer func() { s.uidCopy[identity] = u }()
u.uid = -1
hsuPath := internal.MustHsuPath()
cmd := exec.Command(hsuPath)
cmd.Path = hsuPath
cmd.Stderr = os.Stderr // pass through fatal messages
cmd.Env = []string{"HAKUREI_APP_ID=" + strconv.Itoa(identity)}
cmd.Dir = container.FHSRoot
var (
p []byte
exitError *exec.ExitError
)
if p, u.err = cmd.Output(); u.err == nil {
u.uid, u.err = strconv.Atoi(string(p))
if u.err != nil {
u.err = hlog.WrapErr(u.err, "invalid uid string from hsu")
}
} else if errors.As(u.err, &exitError) && exitError != nil && exitError.ExitCode() == 1 {
u.err = hlog.WrapErr(syscall.EACCES, "") // hsu prints to stderr in this case
} else if os.IsNotExist(u.err) {
u.err = hlog.WrapErr(os.ErrNotExist, fmt.Sprintf("the setuid helper is missing: %s", hsuPath))
}
return u.uid, u.err
}

View File

@@ -1,8 +0,0 @@
package internal
//#include <unistd.h>
import "C"
const SC_LOGIN_NAME_MAX = C._SC_LOGIN_NAME_MAX
func Sysconf(name C.int) int { return int(C.sysconf(name)) }

View File

@@ -203,7 +203,7 @@ in
};
extraPaths = mkOption {
type = anything;
type = listOf (attrsOf anything);
default = [ ];
description = ''
Extra paths to make available to the container.
@@ -261,7 +261,7 @@ in
};
commonPaths = mkOption {
type = types.anything;
type = types.listOf (types.attrsOf types.anything);
default = [ ];
description = ''
Common extra paths to make available to the container.

View File

@@ -9,65 +9,59 @@ import (
"hakurei.app/system/acl"
)
// UpdatePerm appends an ephemeral acl update Op.
// UpdatePerm calls UpdatePermType with the [Process] criteria.
func (sys *I) UpdatePerm(path string, perms ...acl.Perm) *I {
sys.UpdatePermType(Process, path, perms...)
return sys
}
// UpdatePermType appends an acl update Op.
// UpdatePermType maintains [acl.Perms] on a file until its [Enablement] is no longer satisfied.
func (sys *I) UpdatePermType(et Enablement, path string, perms ...acl.Perm) *I {
sys.lock.Lock()
defer sys.lock.Unlock()
sys.ops = append(sys.ops, &ACL{et, path, perms})
sys.ops = append(sys.ops, &aclUpdateOp{et, path, perms})
return sys
}
type ACL struct {
// aclUpdateOp implements [I.UpdatePermType].
type aclUpdateOp struct {
et Enablement
path string
perms acl.Perms
}
func (a *ACL) Type() Enablement { return a.et }
func (a *aclUpdateOp) Type() Enablement { return a.et }
func (a *ACL) apply(sys *I) error {
msg.Verbose("applying ACL", a)
return wrapErrSuffix(acl.Update(a.path, sys.uid, a.perms...),
fmt.Sprintf("cannot apply ACL entry to %q:", a.path))
func (a *aclUpdateOp) apply(sys *I) error {
sys.verbose("applying ACL", a)
return newOpError("acl", sys.aclUpdate(a.path, sys.uid, a.perms...), false)
}
func (a *ACL) revert(sys *I, ec *Criteria) error {
if ec.hasType(a) {
msg.Verbose("stripping ACL", a)
err := acl.Update(a.path, sys.uid)
func (a *aclUpdateOp) revert(sys *I, ec *Criteria) error {
if ec.hasType(a.Type()) {
sys.verbose("stripping ACL", a)
err := sys.aclUpdate(a.path, sys.uid)
if errors.Is(err, os.ErrNotExist) {
// the ACL is effectively stripped if the file no longer exists
msg.Verbosef("target of ACL %s no longer exists", a)
sys.verbosef("target of ACL %s no longer exists", a)
err = nil
}
return wrapErrSuffix(err,
fmt.Sprintf("cannot strip ACL entry from %q:", a.path))
return newOpError("acl", err, true)
} else {
msg.Verbose("skipping ACL", a)
sys.verbose("skipping ACL", a)
return nil
}
}
func (a *ACL) Is(o Op) bool {
a0, ok := o.(*ACL)
return ok && a0 != nil &&
a.et == a0.et &&
a.path == a0.path &&
slices.Equal(a.perms, a0.perms)
func (a *aclUpdateOp) Is(o Op) bool {
target, ok := o.(*aclUpdateOp)
return ok && a != nil && target != nil &&
a.et == target.et &&
a.path == target.path &&
slices.Equal(a.perms, target.perms)
}
func (a *ACL) Path() string { return a.path }
func (a *aclUpdateOp) Path() string { return a.path }
func (a *ACL) String() string {
func (a *aclUpdateOp) String() string {
return fmt.Sprintf("%s type: %s path: %q",
a.perms, TypeString(a.et), a.path)
}

View File

@@ -29,8 +29,5 @@ func Update(name string, uid int, perms ...Perm) error {
(*C.acl_perm_t)(p),
C.size_t(len(perms)),
)
if r == 0 {
return nil
}
return err
return newAclPathError(name, int(r), err)
}

View File

@@ -1,156 +0,0 @@
package acl_test
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os/exec"
"strconv"
)
type (
getFAclInvocation struct {
cmd *exec.Cmd
val []*getFAclResp
pe []error
}
getFAclResp struct {
typ fAclType
cred int32
val fAclPerm
raw []byte
}
fAclPerm uintptr
fAclType uint8
)
const fAclBufSize = 16
const (
fAclPermRead fAclPerm = 1 << iota
fAclPermWrite
fAclPermExecute
)
const (
fAclTypeUser fAclType = iota
fAclTypeGroup
fAclTypeMask
fAclTypeOther
)
func (c *getFAclInvocation) run(name string) error {
if c.cmd != nil {
panic("attempted to run twice")
}
c.cmd = exec.Command("getfacl", "--omit-header", "--absolute-names", "--numeric", name)
scanErr := make(chan error, 1)
if p, err := c.cmd.StdoutPipe(); err != nil {
return err
} else {
go c.parse(p, scanErr)
}
if err := c.cmd.Start(); err != nil {
return err
}
return errors.Join(<-scanErr, c.cmd.Wait())
}
func (c *getFAclInvocation) parse(pipe io.Reader, scanErr chan error) {
c.val = make([]*getFAclResp, 0, 4+fAclBufSize)
s := bufio.NewScanner(pipe)
for s.Scan() {
fields := bytes.SplitN(s.Bytes(), []byte{':'}, 3)
if len(fields) != 3 {
continue
}
resp := getFAclResp{}
switch string(fields[0]) {
case "user":
resp.typ = fAclTypeUser
case "group":
resp.typ = fAclTypeGroup
case "mask":
resp.typ = fAclTypeMask
case "other":
resp.typ = fAclTypeOther
default:
c.pe = append(c.pe, fmt.Errorf("unknown type %s", string(fields[0])))
continue
}
if len(fields[1]) == 0 {
resp.cred = -1
} else {
if cred, err := strconv.Atoi(string(fields[1])); err != nil {
c.pe = append(c.pe, err)
continue
} else {
resp.cred = int32(cred)
if resp.cred < 0 {
c.pe = append(c.pe, fmt.Errorf("credential %d out of range", resp.cred))
continue
}
}
}
if len(fields[2]) != 3 {
c.pe = append(c.pe, fmt.Errorf("invalid perm length %d", len(fields[2])))
continue
} else {
switch fields[2][0] {
case 'r':
resp.val |= fAclPermRead
case '-':
default:
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][0]))
continue
}
switch fields[2][1] {
case 'w':
resp.val |= fAclPermWrite
case '-':
default:
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][1]))
continue
}
switch fields[2][2] {
case 'x':
resp.val |= fAclPermExecute
case '-':
default:
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][2]))
continue
}
}
resp.raw = make([]byte, len(s.Bytes()))
copy(resp.raw, s.Bytes())
c.val = append(c.val, &resp)
}
scanErr <- s.Err()
}
func (r *getFAclResp) String() string {
if r.raw != nil && len(r.raw) > 0 {
return string(r.raw)
}
return "(user-initialised resp value)"
}
func (r *getFAclResp) equals(typ fAclType, cred int32, val fAclPerm) bool {
return r.typ == typ && r.cred == cred && r.val == val
}

View File

@@ -1,10 +1,16 @@
package acl_test
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path"
"reflect"
"strconv"
"testing"
"hakurei.app/system/acl"
@@ -17,7 +23,7 @@ var (
cred = int32(os.Geteuid())
)
func TestUpdatePerm(t *testing.T) {
func TestUpdate(t *testing.T) {
if os.Getenv("GO_TEST_SKIP_ACL") == "1" {
t.Log("acl test skipped")
t.SkipNow()
@@ -48,19 +54,19 @@ func TestUpdatePerm(t *testing.T) {
t.Run("default clear mask", func(t *testing.T) {
if err := acl.Update(testFilePath, uid); err != nil {
t.Fatalf("UpdatePerm: error = %v", err)
t.Fatalf("Update: error = %v", err)
}
if cur = getfacl(t, testFilePath); len(cur) != 4 {
t.Fatalf("UpdatePerm: %v", cur)
t.Fatalf("Update: %v", cur)
}
})
t.Run("default clear consistency", func(t *testing.T) {
if err := acl.Update(testFilePath, uid); err != nil {
t.Fatalf("UpdatePerm: error = %v", err)
t.Fatalf("Update: error = %v", err)
}
if val := getfacl(t, testFilePath); !reflect.DeepEqual(val, cur) {
t.Fatalf("UpdatePerm: %v, want %v", val, cur)
t.Fatalf("Update: %v, want %v", val, cur)
}
})
@@ -77,26 +83,171 @@ func testUpdate(t *testing.T, testFilePath, name string, cur []*getFAclResp, val
t.Run(name, func(t *testing.T) {
t.Cleanup(func() {
if err := acl.Update(testFilePath, uid); err != nil {
t.Fatalf("UpdatePerm: error = %v", err)
t.Fatalf("Update: error = %v", err)
}
if v := getfacl(t, testFilePath); !reflect.DeepEqual(v, cur) {
t.Fatalf("UpdatePerm: %v, want %v", v, cur)
t.Fatalf("Update: %v, want %v", v, cur)
}
})
if err := acl.Update(testFilePath, uid, perms...); err != nil {
t.Fatalf("UpdatePerm: error = %v", err)
t.Fatalf("Update: error = %v", err)
}
r := respByCred(getfacl(t, testFilePath), fAclTypeUser, cred)
if r == nil {
t.Fatalf("UpdatePerm did not add an ACL entry")
t.Fatalf("Update did not add an ACL entry")
}
if !r.equals(fAclTypeUser, cred, val) {
t.Fatalf("UpdatePerm(%s) = %s", name, r)
t.Fatalf("Update(%s) = %s", name, r)
}
})
}
type (
getFAclInvocation struct {
cmd *exec.Cmd
val []*getFAclResp
pe []error
}
getFAclResp struct {
typ fAclType
cred int32
val fAclPerm
raw []byte
}
fAclPerm uintptr
fAclType uint8
)
const fAclBufSize = 16
const (
fAclPermRead fAclPerm = 1 << iota
fAclPermWrite
fAclPermExecute
)
const (
fAclTypeUser fAclType = iota
fAclTypeGroup
fAclTypeMask
fAclTypeOther
)
func (c *getFAclInvocation) run(name string) error {
if c.cmd != nil {
panic("attempted to run twice")
}
c.cmd = exec.Command("getfacl", "--omit-header", "--absolute-names", "--numeric", name)
scanErr := make(chan error, 1)
if p, err := c.cmd.StdoutPipe(); err != nil {
return err
} else {
go c.parse(p, scanErr)
}
if err := c.cmd.Start(); err != nil {
return err
}
return errors.Join(<-scanErr, c.cmd.Wait())
}
func (c *getFAclInvocation) parse(pipe io.Reader, scanErr chan error) {
c.val = make([]*getFAclResp, 0, 4+fAclBufSize)
s := bufio.NewScanner(pipe)
for s.Scan() {
fields := bytes.SplitN(s.Bytes(), []byte{':'}, 3)
if len(fields) != 3 {
continue
}
resp := getFAclResp{}
switch string(fields[0]) {
case "user":
resp.typ = fAclTypeUser
case "group":
resp.typ = fAclTypeGroup
case "mask":
resp.typ = fAclTypeMask
case "other":
resp.typ = fAclTypeOther
default:
c.pe = append(c.pe, fmt.Errorf("unknown type %s", string(fields[0])))
continue
}
if len(fields[1]) == 0 {
resp.cred = -1
} else {
if cred, err := strconv.Atoi(string(fields[1])); err != nil {
c.pe = append(c.pe, err)
continue
} else {
resp.cred = int32(cred)
if resp.cred < 0 {
c.pe = append(c.pe, fmt.Errorf("credential %d out of range", resp.cred))
continue
}
}
}
if len(fields[2]) != 3 {
c.pe = append(c.pe, fmt.Errorf("invalid perm length %d", len(fields[2])))
continue
} else {
switch fields[2][0] {
case 'r':
resp.val |= fAclPermRead
case '-':
default:
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][0]))
continue
}
switch fields[2][1] {
case 'w':
resp.val |= fAclPermWrite
case '-':
default:
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][1]))
continue
}
switch fields[2][2] {
case 'x':
resp.val |= fAclPermExecute
case '-':
default:
c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][2]))
continue
}
}
resp.raw = make([]byte, len(s.Bytes()))
copy(resp.raw, s.Bytes())
c.val = append(c.val, &resp)
}
scanErr <- s.Err()
}
func (r *getFAclResp) String() string {
if r.raw != nil && len(r.raw) > 0 {
return string(r.raw)
}
return "(user-initialised resp value)"
}
func (r *getFAclResp) equals(typ fAclType, cred int32, val fAclPerm) bool {
return r.typ == typ && r.cred == cred && r.val == val
}
func getfacl(t *testing.T, name string) []*getFAclResp {
c := new(getFAclInvocation)
if err := c.run(name); err != nil {

View File

@@ -6,7 +6,7 @@
int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid,
acl_perm_t *perms, size_t plen) {
int ret = -1;
int ret;
bool v;
int i;
acl_t acl;
@@ -15,51 +15,70 @@ int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid,
void *qualifier_p;
acl_permset_t permset;
ret = -1; /* acl_get_file */
acl = acl_get_file(path_p, ACL_TYPE_ACCESS);
if (acl == NULL)
goto out;
// prune entries by uid
/* prune entries by uid */
for (i = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); i == 1;
i = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) {
ret = -2; /* acl_get_tag_type */
if (acl_get_tag_type(entry, &tag_type) != 0)
return -1;
goto out;
if (tag_type != ACL_USER)
continue;
ret = -3; /* acl_get_qualifier */
qualifier_p = acl_get_qualifier(entry);
if (qualifier_p == NULL)
return -1;
goto out;
v = *(uid_t *)qualifier_p == uid;
acl_free(qualifier_p);
if (!v)
continue;
acl_delete_entry(acl, entry);
ret = -4; /* acl_delete_entry */
if (acl_delete_entry(acl, entry) != 0)
goto out;
}
if (plen == 0)
goto set;
ret = -5; /* acl_create_entry */
if (acl_create_entry(&acl, &entry) != 0)
goto out;
ret = -6; /* acl_get_permset */
if (acl_get_permset(entry, &permset) != 0)
goto out;
ret = -7; /* acl_add_perm */
for (i = 0; i < plen; i++) {
if (acl_add_perm(permset, perms[i]) != 0)
goto out;
}
ret = -8; /* acl_set_tag_type */
if (acl_set_tag_type(entry, ACL_USER) != 0)
goto out;
ret = -9; /* acl_set_qualifier */
if (acl_set_qualifier(entry, (void *)&uid) != 0)
goto out;
set:
ret = -10; /* acl_calc_mask */
if (acl_calc_mask(&acl) != 0)
goto out;
ret = -11; /* acl_valid */
if (acl_valid(acl) != 0)
goto out;
ret = -12; /* acl_set_file */
if (acl_set_file(path_p, ACL_TYPE_ACCESS, acl) == 0)
ret = 0;

View File

@@ -0,0 +1,40 @@
package acl
import "os"
func newAclPathError(name string, r int, err error) error {
pathError := &os.PathError{Path: name, Err: err}
switch r {
case 0:
return nil
case -1:
pathError.Op = "acl_get_file"
case -2:
pathError.Op = "acl_get_tag_type"
case -3:
pathError.Op = "acl_get_qualifier"
case -4:
pathError.Op = "acl_delete_entry"
case -5:
pathError.Op = "acl_create_entry"
case -6:
pathError.Op = "acl_get_permset"
case -7:
pathError.Op = "acl_add_perm"
case -8:
pathError.Op = "acl_set_tag_type"
case -9:
pathError.Op = "acl_set_qualifier"
case -10:
pathError.Op = "acl_calc_mask"
case -11:
pathError.Op = "acl_valid"
case -12:
pathError.Op = "acl_set_file"
default: // unreachable
pathError.Op = "setfacl"
}
return pathError
}

View File

@@ -0,0 +1,60 @@
package acl
import (
"os"
"reflect"
"syscall"
"testing"
"hakurei.app/container"
)
func TestNewAclPathError(t *testing.T) {
testCases := []struct {
name string
path string
r int
err error
want error
}{
{"nil", container.Nonexistent, 0, syscall.ENOTRECOVERABLE, nil},
{"acl_get_file", container.Nonexistent, -1, syscall.ENOTRECOVERABLE,
&os.PathError{Op: "acl_get_file", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
{"acl_get_tag_type", container.Nonexistent, -2, syscall.ENOTRECOVERABLE,
&os.PathError{Op: "acl_get_tag_type", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
{"acl_get_qualifier", container.Nonexistent, -3, syscall.ENOTRECOVERABLE,
&os.PathError{Op: "acl_get_qualifier", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
{"acl_delete_entry", container.Nonexistent, -4, syscall.ENOTRECOVERABLE,
&os.PathError{Op: "acl_delete_entry", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
{"acl_create_entry", container.Nonexistent, -5, syscall.ENOTRECOVERABLE,
&os.PathError{Op: "acl_create_entry", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
{"acl_get_permset", container.Nonexistent, -6, syscall.ENOTRECOVERABLE,
&os.PathError{Op: "acl_get_permset", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
{"acl_add_perm", container.Nonexistent, -7, syscall.ENOTRECOVERABLE,
&os.PathError{Op: "acl_add_perm", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
{"acl_set_tag_type", container.Nonexistent, -8, syscall.ENOTRECOVERABLE,
&os.PathError{Op: "acl_set_tag_type", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
{"acl_set_qualifier", container.Nonexistent, -9, syscall.ENOTRECOVERABLE,
&os.PathError{Op: "acl_set_qualifier", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
{"acl_calc_mask", container.Nonexistent, -10, syscall.ENOTRECOVERABLE,
&os.PathError{Op: "acl_calc_mask", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
{"acl_valid", container.Nonexistent, -11, syscall.ENOTRECOVERABLE,
&os.PathError{Op: "acl_valid", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
{"acl_set_file", container.Nonexistent, -12, syscall.ENOTRECOVERABLE,
&os.PathError{Op: "acl_set_file", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
{"acl", container.Nonexistent, -13, syscall.ENOTRECOVERABLE,
&os.PathError{Op: "setfacl", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}},
{"invalid", container.Nonexistent, -0xdeadbeef, nil,
&os.PathError{Op: "setfacl", Path: container.Nonexistent}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := newAclPathError(tc.path, tc.r, tc.err)
if !reflect.DeepEqual(err, tc.want) {
t.Errorf("newAclPathError: %v, want %v", err, tc.want)
}
})
}
}

30
system/acl/perms_test.go Normal file
View File

@@ -0,0 +1,30 @@
package acl_test
import (
"testing"
"hakurei.app/system/acl"
)
func TestPerms(t *testing.T) {
testCases := []struct {
name string
perms acl.Perms
}{
{"---", acl.Perms{}},
{"r--", acl.Perms{acl.Read}},
{"-w-", acl.Perms{acl.Write}},
{"--x", acl.Perms{acl.Execute}},
{"rw-", acl.Perms{acl.Read, acl.Read, acl.Write}},
{"r-x", acl.Perms{acl.Read, acl.Execute, acl.Execute}},
{"-wx", acl.Perms{acl.Write, acl.Write, acl.Execute, acl.Execute}},
{"rwx", acl.Perms{acl.Read, acl.Write, acl.Execute}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := tc.perms.String(); got != tc.name {
t.Errorf("String: %q, want %q", got, tc.name)
}
})
}
}

View File

@@ -1,91 +1,182 @@
package system
import (
"os"
"syscall"
"testing"
"hakurei.app/container"
"hakurei.app/container/stub"
"hakurei.app/system/acl"
)
func TestUpdatePerm(t *testing.T) {
testCases := []struct {
path string
perms []acl.Perm
}{
{"/run/user/1971/hakurei", []acl.Perm{acl.Execute}},
{"/tmp/hakurei.1971/tmpdir/150", []acl.Perm{acl.Read, acl.Write, acl.Execute}},
}
func TestACLUpdateOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{
{"apply aclUpdate", 0xdeadbeef, 0xff,
&aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xdeadbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, stub.UniqueError(1)),
}, &OpError{Op: "acl", Err: stub.UniqueError(1)}, nil, nil},
for _, tc := range testCases {
t.Run(tc.path+permSubTestSuffix(tc.perms), func(t *testing.T) {
sys := New(150)
sys.UpdatePerm(tc.path, tc.perms...)
(&tcOp{Process, tc.path}).test(t, sys.ops, []Op{&ACL{Process, tc.path, tc.perms}}, "UpdatePerm")
})
}
}
func TestUpdatePermType(t *testing.T) {
testCases := []struct {
perms []acl.Perm
tcOp
}{
{[]acl.Perm{acl.Execute}, tcOp{User, "/tmp/hakurei.1971/tmpdir"}},
{[]acl.Perm{acl.Read, acl.Write, acl.Execute}, tcOp{User, "/tmp/hakurei.1971/tmpdir/150"}},
{[]acl.Perm{acl.Execute}, tcOp{Process, "/run/user/1971/hakurei/fcb8a12f7c482d183ade8288c3de78b5"}},
{[]acl.Perm{acl.Read}, tcOp{Process, "/tmp/hakurei.1971/fcb8a12f7c482d183ade8288c3de78b5/passwd"}},
{[]acl.Perm{acl.Read}, tcOp{Process, "/tmp/hakurei.1971/fcb8a12f7c482d183ade8288c3de78b5/group"}},
{[]acl.Perm{acl.Read, acl.Write, acl.Execute}, tcOp{EWayland, "/run/user/1971/wayland-0"}},
}
for _, tc := range testCases {
t.Run(tc.path+"_"+TypeString(tc.et)+permSubTestSuffix(tc.perms), func(t *testing.T) {
sys := New(150)
sys.UpdatePermType(tc.et, tc.path, tc.perms...)
tc.test(t, sys.ops, []Op{&ACL{tc.et, tc.path, tc.perms}}, "UpdatePermType")
})
}
}
func TestACLString(t *testing.T) {
testCases := []struct {
want string
et Enablement
perms []acl.Perm
}{
{`--- type: process path: "/proc/nonexistent"`, Process, []acl.Perm{}},
{`r-- type: user path: "/proc/nonexistent"`, User, []acl.Perm{acl.Read}},
{`-w- type: wayland path: "/proc/nonexistent"`, EWayland, []acl.Perm{acl.Write}},
{`--x type: x11 path: "/proc/nonexistent"`, EX11, []acl.Perm{acl.Execute}},
{`rw- type: dbus path: "/proc/nonexistent"`, EDBus, []acl.Perm{acl.Read, acl.Write}},
{`r-x type: pulseaudio path: "/proc/nonexistent"`, EPulse, []acl.Perm{acl.Read, acl.Execute}},
{`rwx type: user path: "/proc/nonexistent"`, User, []acl.Perm{acl.Read, acl.Write, acl.Execute}},
{`rwx type: process path: "/proc/nonexistent"`, Process, []acl.Perm{acl.Read, acl.Write, acl.Write, acl.Execute}},
}
for _, tc := range testCases {
t.Run(tc.want, func(t *testing.T) {
a := &ACL{et: tc.et, perms: tc.perms, path: container.Nonexistent}
if got := a.String(); got != tc.want {
t.Errorf("String() = %v, want %v",
got, tc.want)
}
})
}
}
func permSubTestSuffix(perms []acl.Perm) (suffix string) {
for _, perm := range perms {
switch perm {
case acl.Read:
suffix += "_read"
case acl.Write:
suffix += "_write"
case acl.Execute:
suffix += "_execute"
default:
panic("unreachable")
}
}
return
{"revert aclUpdate", 0xdeadbeef, 0xff,
&aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xdeadbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
}, nil, []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"stripping ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xdeadbeef, ([]acl.Perm)(nil)}, nil, stub.UniqueError(0)),
}, &OpError{Op: "acl", Err: stub.UniqueError(0), Revert: true}},
{"success revert skip", 0xdeadbeef, Process,
&aclUpdateOp{User, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{User, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xdeadbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
}, nil, []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"skipping ACL", &aclUpdateOp{User, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
}, nil},
{"success revert aclUpdate ENOENT", 0xdeadbeef, 0xff,
&aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xdeadbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
}, nil, []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"stripping ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xdeadbeef, ([]acl.Perm)(nil)}, nil, &os.PathError{Op: "acl_get_file", Path: "/proc/nonexistent", Err: syscall.ENOENT}),
call("verbosef", stub.ExpectArgs{"target of ACL %s no longer exists", []any{&aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
}, nil},
{"success", 0xdeadbeef, 0xff,
&aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xdeadbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
}, nil, []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"stripping ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xdeadbeef, ([]acl.Perm)(nil)}, nil, nil),
}, nil},
})
checkOpsBuilder(t, "UpdatePermType", []opsBuilderTestCase{
{"simple",
0xdeadbeef,
func(_ *testing.T, sys *I) {
sys.
UpdatePerm("/run/user/1971/hakurei", acl.Execute).
UpdatePerm("/tmp/hakurei.0/tmpdir/150", acl.Read, acl.Write, acl.Execute)
}, []Op{
&aclUpdateOp{Process, "/run/user/1971/hakurei", []acl.Perm{acl.Execute}},
&aclUpdateOp{Process, "/tmp/hakurei.0/tmpdir/150", []acl.Perm{acl.Read, acl.Write, acl.Execute}},
}, stub.Expect{}},
{"tmpdirp", 0xdeadbeef, func(_ *testing.T, sys *I) {
sys.UpdatePermType(User, "/tmp/hakurei.0/tmpdir", acl.Execute)
}, []Op{
&aclUpdateOp{User, "/tmp/hakurei.0/tmpdir", []acl.Perm{acl.Execute}},
}, stub.Expect{}},
{"tmpdir", 0xdeadbeef, func(_ *testing.T, sys *I) {
sys.UpdatePermType(User, "/tmp/hakurei.0/tmpdir/150", acl.Read, acl.Write, acl.Execute)
}, []Op{
&aclUpdateOp{User, "/tmp/hakurei.0/tmpdir/150", []acl.Perm{acl.Read, acl.Write, acl.Execute}},
}, stub.Expect{}},
{"share", 0xdeadbeef, func(_ *testing.T, sys *I) {
sys.UpdatePermType(Process, "/run/user/1971/hakurei/fcb8a12f7c482d183ade8288c3de78b5", acl.Execute)
}, []Op{
&aclUpdateOp{Process, "/run/user/1971/hakurei/fcb8a12f7c482d183ade8288c3de78b5", []acl.Perm{acl.Execute}},
}, stub.Expect{}},
{"passwd", 0xdeadbeef, func(_ *testing.T, sys *I) {
sys.
UpdatePermType(Process, "/tmp/hakurei.0/fcb8a12f7c482d183ade8288c3de78b5/passwd", acl.Read).
UpdatePermType(Process, "/tmp/hakurei.0/fcb8a12f7c482d183ade8288c3de78b5/group", acl.Read)
}, []Op{
&aclUpdateOp{Process, "/tmp/hakurei.0/fcb8a12f7c482d183ade8288c3de78b5/passwd", []acl.Perm{acl.Read}},
&aclUpdateOp{Process, "/tmp/hakurei.0/fcb8a12f7c482d183ade8288c3de78b5/group", []acl.Perm{acl.Read}},
}, stub.Expect{}},
{"wayland", 0xdeadbeef, func(_ *testing.T, sys *I) {
sys.UpdatePermType(EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute)
}, []Op{
&aclUpdateOp{EWayland, "/run/user/1971/wayland-0", []acl.Perm{acl.Read, acl.Write, acl.Execute}},
}, stub.Expect{}},
})
checkOpIs(t, []opIsTestCase{
{"nil", (*aclUpdateOp)(nil), (*aclUpdateOp)(nil), false},
{"zero", new(aclUpdateOp), new(aclUpdateOp), true},
{"et differs",
&aclUpdateOp{
EWayland, "/run/user/1971/wayland-0",
[]acl.Perm{acl.Read, acl.Write, acl.Execute},
}, &aclUpdateOp{
EX11, "/run/user/1971/wayland-0",
[]acl.Perm{acl.Read, acl.Write, acl.Execute},
}, false},
{"path differs", &aclUpdateOp{
EWayland, "/run/user/1971/wayland-0",
[]acl.Perm{acl.Read, acl.Write, acl.Execute},
}, &aclUpdateOp{
EWayland, "/run/user/1971/wayland-1",
[]acl.Perm{acl.Read, acl.Write, acl.Execute},
}, false},
{"perms differs", &aclUpdateOp{
EWayland, "/run/user/1971/wayland-0",
[]acl.Perm{acl.Read, acl.Write, acl.Execute},
}, &aclUpdateOp{
EWayland, "/run/user/1971/wayland-0",
[]acl.Perm{acl.Read, acl.Write},
}, false},
{"equals", &aclUpdateOp{
EWayland, "/run/user/1971/wayland-0",
[]acl.Perm{acl.Read, acl.Write, acl.Execute},
}, &aclUpdateOp{
EWayland, "/run/user/1971/wayland-0",
[]acl.Perm{acl.Read, acl.Write, acl.Execute},
}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"clear",
&aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{}},
Process, "/proc/nonexistent",
`--- type: process path: "/proc/nonexistent"`},
{"read",
&aclUpdateOp{User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/0", []acl.Perm{acl.Read}},
User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/0",
`r-- type: user path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/0"`},
{"write",
&aclUpdateOp{User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/1", []acl.Perm{acl.Write}},
User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/1",
`-w- type: user path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/1"`},
{"execute",
&aclUpdateOp{User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/2", []acl.Perm{acl.Execute}},
User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/2",
`--x type: user path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/2"`},
{"wayland",
&aclUpdateOp{EWayland, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/wayland", []acl.Perm{acl.Read, acl.Write}},
EWayland, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/wayland",
`rw- type: wayland path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/wayland"`},
{"x11",
&aclUpdateOp{EX11, "/tmp/.X11-unix/X0", []acl.Perm{acl.Read, acl.Execute}},
EX11, "/tmp/.X11-unix/X0",
`r-x type: x11 path: "/tmp/.X11-unix/X0"`},
{"dbus",
&aclUpdateOp{EDBus, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/bus", []acl.Perm{acl.Write, acl.Execute}},
EDBus, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/bus",
`-wx type: dbus path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/bus"`},
{"pulseaudio",
&aclUpdateOp{EPulse, "/run/user/1971/hakurei/27d81d567f8fae7f33278eec45da9446/pulse", []acl.Perm{acl.Read, acl.Write, acl.Execute}},
EPulse, "/run/user/1971/hakurei/27d81d567f8fae7f33278eec45da9446/pulse",
`rwx type: pulseaudio path: "/run/user/1971/hakurei/27d81d567f8fae7f33278eec45da9446/pulse"`},
})
}

View File

@@ -4,11 +4,14 @@ import (
"bytes"
"context"
"errors"
"fmt"
"log"
"reflect"
"strings"
"sync"
"syscall"
"hakurei.app/container"
"hakurei.app/system/dbus"
)
@@ -16,6 +19,7 @@ var (
ErrDBusConfig = errors.New("dbus config not supplied")
)
// MustProxyDBus calls ProxyDBus and panics if an error is returned.
func (sys *I) MustProxyDBus(sessionPath string, session *dbus.Config, systemPath string, system *dbus.Config) *I {
if _, err := sys.ProxyDBus(session, system, sessionPath, systemPath); err != nil {
panic(err.Error())
@@ -24,35 +28,40 @@ func (sys *I) MustProxyDBus(sessionPath string, session *dbus.Config, systemPath
}
}
// ProxyDBus finalises configuration ahead of time and starts xdg-dbus-proxy via [dbus] and terminates it on revert.
// This [Op] is always [Process] scoped.
func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath string) (func(), error) {
d := new(DBus)
d := new(dbusProxyOp)
// session bus is required as otherwise this is effectively a very expensive noop
if session == nil {
return nil, msg.WrapErr(ErrDBusConfig,
"attempted to create message bus proxy args without session bus config")
return nil, newOpErrorMessage("dbus", ErrDBusConfig,
"attempted to create message bus proxy args without session bus config", false)
}
// system bus is optional
d.system = system != nil
d.sessionBus[0], d.systemBus[0] = dbus.Address()
d.sessionBus[1], d.systemBus[1] = sessionPath, systemPath
d.out = &scanToFmsg{msg: new(strings.Builder)}
if final, err := dbus.Finalise(d.sessionBus, d.systemBus, session, system); err != nil {
var sessionBus, systemBus dbus.ProxyPair
sessionBus[0], systemBus[0] = sys.dbusAddress()
sessionBus[1], systemBus[1] = sessionPath, systemPath
d.out = &linePrefixWriter{println: log.Println, prefix: "(dbus) ", buf: new(strings.Builder)}
if final, err := sys.dbusFinalise(sessionBus, systemBus, session, system); err != nil {
if errors.Is(err, syscall.EINVAL) {
return nil, msg.WrapErr(err, "message bus proxy configuration contains NUL byte")
return nil, newOpErrorMessage("dbus", err,
"message bus proxy configuration contains NUL byte", false)
}
return nil, wrapErrSuffix(err, "cannot finalise message bus proxy:")
return nil, newOpErrorMessage("dbus", err,
fmt.Sprintf("cannot finalise message bus proxy: %v", err), false)
} else {
if msg.IsVerbose() {
msg.Verbose("session bus proxy:", session.Args(d.sessionBus))
if sys.isVerbose() {
sys.verbose("session bus proxy:", session.Args(sessionBus))
if system != nil {
msg.Verbose("system bus proxy:", system.Args(d.systemBus))
sys.verbose("system bus proxy:", system.Args(systemBus))
}
// this calls the argsWt String method
msg.Verbose("message bus proxy final args:", final.WriterTo)
sys.verbose("message bus proxy final args:", final.WriterTo)
}
d.final = final
@@ -62,100 +71,130 @@ func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath st
return d.out.Dump, nil
}
type DBus struct {
// dbusProxyOp implements [I.ProxyDBus].
type dbusProxyOp struct {
proxy *dbus.Proxy // populated during apply
final *dbus.Final
out *scanToFmsg
out *linePrefixWriter
// whether system bus proxy is enabled
system bool
sessionBus, systemBus dbus.ProxyPair
}
func (d *DBus) Type() Enablement { return Process }
func (d *dbusProxyOp) Type() Enablement { return Process }
func (d *DBus) apply(sys *I) error {
msg.Verbosef("session bus proxy on %q for upstream %q", d.sessionBus[1], d.sessionBus[0])
func (d *dbusProxyOp) apply(sys *I) error {
sys.verbosef("session bus proxy on %q for upstream %q", d.final.Session[1], d.final.Session[0])
if d.system {
msg.Verbosef("system bus proxy on %q for upstream %q", d.systemBus[1], d.systemBus[0])
sys.verbosef("system bus proxy on %q for upstream %q", d.final.System[1], d.final.System[0])
}
d.proxy = dbus.New(sys.ctx, d.final, d.out)
if err := d.proxy.Start(); err != nil {
if err := sys.dbusProxyStart(d.proxy); err != nil {
d.out.Dump()
return wrapErrSuffix(err,
"cannot start message bus proxy:")
return newOpErrorMessage("dbus", err,
fmt.Sprintf("cannot start message bus proxy: %v", err), false)
}
msg.Verbose("starting message bus proxy", d.proxy)
sys.verbose("starting message bus proxy", d.proxy)
return nil
}
func (d *DBus) revert(*I, *Criteria) error {
func (d *dbusProxyOp) revert(sys *I, _ *Criteria) error {
// criteria ignored here since dbus is always process-scoped
msg.Verbose("terminating message bus proxy")
d.proxy.Close()
defer msg.Verbose("message bus proxy exit")
err := d.proxy.Wait()
sys.verbose("terminating message bus proxy")
sys.dbusProxyClose(d.proxy)
exitMessage := "message bus proxy exit"
defer func() { sys.verbose(exitMessage) }()
err := sys.dbusProxyWait(d.proxy)
if errors.Is(err, context.Canceled) {
msg.Verbose("message bus proxy canceled upstream")
exitMessage = "message bus proxy canceled upstream"
err = nil
}
return wrapErrSuffix(err, "message bus proxy error:")
return newOpErrorMessage("dbus", err,
fmt.Sprintf("message bus proxy error: %v", err), true)
}
func (d *DBus) Is(o Op) bool {
d0, ok := o.(*DBus)
return ok && d0 != nil &&
((d.proxy == nil && d0.proxy == nil) ||
(d.proxy != nil && d0.proxy != nil && d.proxy.String() == d0.proxy.String()))
func (d *dbusProxyOp) Is(o Op) bool {
target, ok := o.(*dbusProxyOp)
return ok && d != nil && target != nil &&
d.system == target.system &&
d.final != nil && target.final != nil &&
d.final.Session == target.final.Session &&
d.final.System == target.final.System &&
dbus.EqualAddrEntries(d.final.SessionUpstream, target.final.SessionUpstream) &&
dbus.EqualAddrEntries(d.final.SystemUpstream, target.final.SystemUpstream) &&
reflect.DeepEqual(d.final.WriterTo, target.final.WriterTo)
}
func (d *DBus) Path() string {
return "(dbus proxy)"
}
func (d *dbusProxyOp) Path() string { return container.Nonexistent }
func (d *dbusProxyOp) String() string { return d.proxy.String() }
func (d *DBus) String() string {
return d.proxy.String()
}
const (
// lpwSizeThreshold is the threshold of bytes written to linePrefixWriter which,
// if reached or exceeded, causes linePrefixWriter to drop all future writes.
lpwSizeThreshold = 1 << 24
)
type scanToFmsg struct {
msg *strings.Builder
msgbuf []string
// linePrefixWriter calls println with a prefix for every line written.
type linePrefixWriter struct {
prefix string
println func(v ...any)
n int
msg []string
buf *strings.Builder
mu sync.RWMutex
}
func (s *scanToFmsg) Write(p []byte) (n int, err error) {
func (s *linePrefixWriter) Write(p []byte) (n int, err error) {
s.mu.Lock()
defer s.mu.Unlock()
return s.write(p, 0)
}
func (s *scanToFmsg) write(p []byte, a int) (int, error) {
func (s *linePrefixWriter) write(p []byte, a int) (int, error) {
if s.n >= lpwSizeThreshold {
if len(p) == 0 {
return a, nil
}
return a, syscall.ENOMEM
}
if i := bytes.IndexByte(p, '\n'); i == -1 {
n, _ := s.msg.Write(p)
n, _ := s.buf.Write(p)
s.n += n
return a + n, nil
} else {
n, _ := s.msg.Write(p[:i])
n, _ := s.buf.Write(p[:i])
s.n += n + 1
// allow container init messages through
v := s.msg.String()
v := s.buf.String()
if strings.HasPrefix(v, "init: ") {
log.Println("(dbus) " + v)
s.n -= len(v) + 1
// pass through container init messages
s.println(s.prefix + v)
} else {
s.msgbuf = append(s.msgbuf, v)
s.msg = append(s.msg, v)
}
s.msg.Reset()
s.buf.Reset()
return s.write(p[i+1:], a+n+1)
}
}
func (s *scanToFmsg) Dump() {
func (s *linePrefixWriter) Dump() {
s.mu.RLock()
for _, msg := range s.msgbuf {
log.Println("(dbus) " + msg)
for _, m := range s.msg {
s.println(s.prefix + m)
}
if s.buf != nil && s.buf.Len() != 0 {
s.println("*" + s.prefix + s.buf.String())
}
if s.n >= lpwSizeThreshold {
s.println("+" + s.prefix + "write threshold reached, output may be incomplete")
}
s.mu.RUnlock()
}

View File

@@ -13,6 +13,13 @@ type AddrEntry struct {
Values [][2]string `json:"values"`
}
// EqualAddrEntries returns whether two slices of [AddrEntry] are equal.
func EqualAddrEntries(entries, target []AddrEntry) bool {
return slices.EqualFunc(entries, target, func(a AddrEntry, b AddrEntry) bool {
return a.Method == b.Method && slices.Equal(a.Values, b.Values)
})
}
// Parse parses D-Bus address according to
// https://dbus.freedesktop.org/doc/dbus-specification.html#addresses
func Parse(addr []byte) ([]AddrEntry, error) {

View File

@@ -63,6 +63,10 @@ func TestProxyStartWaitCloseString(t *testing.T) {
t.Run("direct", func(t *testing.T) { testProxyFinaliseStartWaitCloseString(t, false) })
}
const (
stubProxyTimeout = 30 * time.Second
)
func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) {
{
oldWaitDelay := helper.WaitDelay
@@ -118,33 +122,33 @@ func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) {
}
})
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
output := new(strings.Builder)
if !useSandbox {
p = dbus.NewDirect(ctx, final, output)
} else {
p = dbus.New(ctx, final, output)
}
t.Run("invalid wait", func(t *testing.T) {
wantErr := "dbus: not started"
if err := p.Wait(); err == nil || err.Error() != wantErr {
t.Errorf("Wait: error = %v, wantErr %v",
err, wantErr)
t.Run("run", func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), stubProxyTimeout)
defer cancel()
output := new(strings.Builder)
if !useSandbox {
p = dbus.NewDirect(ctx, final, output)
} else {
p = dbus.New(ctx, final, output)
}
})
t.Run("string", func(t *testing.T) {
want := "(unused dbus proxy)"
if got := p.String(); got != want {
t.Errorf("String: %q, want %q",
got, want)
return
}
})
t.Run("invalid wait", func(t *testing.T) {
wantErr := "dbus: not started"
if err := p.Wait(); err == nil || err.Error() != wantErr {
t.Errorf("Wait: error = %v, wantErr %v",
err, wantErr)
}
})
t.Run("string", func(t *testing.T) {
want := "(unused dbus proxy)"
if got := p.String(); got != want {
t.Errorf("String: %q, want %q",
got, want)
return
}
})
t.Run("start", func(t *testing.T) {
if err := p.Start(); err != nil {
t.Fatalf("Start: error = %v",
err)

659
system/dbus_test.go Normal file
View File

@@ -0,0 +1,659 @@
package system
import (
"context"
"reflect"
"slices"
"strconv"
"strings"
"syscall"
"testing"
"hakurei.app/container/stub"
"hakurei.app/helper"
"hakurei.app/system/dbus"
)
func TestDBusProxyOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{
{"dbusProxyStart", 0xdeadbeef, 0xff, &dbusProxyOp{
final: dbusNewFinalSample(4),
out: new(linePrefixWriter), // panics on write
system: true,
}, []stub.Call{
call("verbosef", stub.ExpectArgs{"session bus proxy on %q for upstream %q", []any{"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus", "unix:path=/run/user/1000/bus"}}, nil, nil),
call("verbosef", stub.ExpectArgs{"system bus proxy on %q for upstream %q", []any{"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket", "unix:path=/run/dbus/system_bus_socket"}}, nil, nil),
call("dbusProxyStart", stub.ExpectArgs{dbusNewFinalSample(4)}, nil, stub.UniqueError(2)),
}, &OpError{
Op: "dbus", Err: stub.UniqueError(2),
Msg: "cannot start message bus proxy: unique error 2 injected by the test suite",
}, nil, nil},
{"dbusProxyWait", 0xdeadbeef, 0xff, &dbusProxyOp{
final: dbusNewFinalSample(3),
}, []stub.Call{
call("verbosef", stub.ExpectArgs{"session bus proxy on %q for upstream %q", []any{"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus", "unix:path=/run/user/1000/bus"}}, nil, nil),
call("dbusProxyStart", stub.ExpectArgs{dbusNewFinalSample(3)}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{"starting message bus proxy", ignoreValue{}}}, nil, nil),
}, nil, []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"terminating message bus proxy"}}, nil, nil),
call("dbusProxyClose", stub.ExpectArgs{dbusNewFinalSample(3)}, nil, nil),
call("dbusProxyWait", stub.ExpectArgs{dbusNewFinalSample(3)}, nil, stub.UniqueError(1)),
call("verbose", stub.ExpectArgs{[]any{"message bus proxy exit"}}, nil, nil),
}, &OpError{
Op: "dbus", Err: stub.UniqueError(1), Revert: true,
Msg: "message bus proxy error: unique error 1 injected by the test suite",
}},
{"success dbusProxyWait cancel", 0xdeadbeef, 0xff, &dbusProxyOp{
final: dbusNewFinalSample(2),
system: true,
}, []stub.Call{
call("verbosef", stub.ExpectArgs{"session bus proxy on %q for upstream %q", []any{"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus", "unix:path=/run/user/1000/bus"}}, nil, nil),
call("verbosef", stub.ExpectArgs{"system bus proxy on %q for upstream %q", []any{"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket", "unix:path=/run/dbus/system_bus_socket"}}, nil, nil),
call("dbusProxyStart", stub.ExpectArgs{dbusNewFinalSample(2)}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{"starting message bus proxy", ignoreValue{}}}, nil, nil),
}, nil, []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"terminating message bus proxy"}}, nil, nil),
call("dbusProxyClose", stub.ExpectArgs{dbusNewFinalSample(2)}, nil, nil),
call("dbusProxyWait", stub.ExpectArgs{dbusNewFinalSample(2)}, nil, context.Canceled),
call("verbose", stub.ExpectArgs{[]any{"message bus proxy canceled upstream"}}, nil, nil),
}, nil},
{"success", 0xdeadbeef, 0xff, &dbusProxyOp{
final: dbusNewFinalSample(1),
system: true,
}, []stub.Call{
call("verbosef", stub.ExpectArgs{"session bus proxy on %q for upstream %q", []any{"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus", "unix:path=/run/user/1000/bus"}}, nil, nil),
call("verbosef", stub.ExpectArgs{"system bus proxy on %q for upstream %q", []any{"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket", "unix:path=/run/dbus/system_bus_socket"}}, nil, nil),
call("dbusProxyStart", stub.ExpectArgs{dbusNewFinalSample(1)}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{"starting message bus proxy", ignoreValue{}}}, nil, nil),
}, nil, []stub.Call{
call("verbose", stub.ExpectArgs{[]any{"terminating message bus proxy"}}, nil, nil),
call("dbusProxyClose", stub.ExpectArgs{dbusNewFinalSample(1)}, nil, nil),
call("dbusProxyWait", stub.ExpectArgs{dbusNewFinalSample(1)}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{"message bus proxy exit"}}, nil, nil),
}, nil},
})
checkOpsBuilder(t, "ProxyDBus", []opsBuilderTestCase{
{"nil session", 0xcafebabe, func(t *testing.T, sys *I) {
wantErr := &OpError{
Op: "dbus", Err: ErrDBusConfig,
Msg: "attempted to create message bus proxy args without session bus config",
}
if f, err := sys.ProxyDBus(nil, new(dbus.Config), "", ""); !reflect.DeepEqual(err, wantErr) {
t.Errorf("ProxyDBus: error = %v, want %v", err, wantErr)
} else if f != nil {
t.Errorf("ProxyDBus: f = %p", f)
}
}, nil, stub.Expect{}},
{"dbusFinalise NUL", 0xcafebabe, func(_ *testing.T, sys *I) {
defer func() {
want := "message bus proxy configuration contains NUL byte"
if r := recover(); r != want {
t.Errorf("MustProxyDBus: panic = %v, want %v", r, want)
}
}()
sys.MustProxyDBus(
"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus", &dbus.Config{
// use impossible value here as an implicit assert that it goes through the stub
Talk: []string{"session\x00"}, Filter: true,
}, "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket", &dbus.Config{
// use impossible value here as an implicit assert that it goes through the stub
Talk: []string{"system\x00"}, Filter: true,
})
}, nil, stub.Expect{Calls: []stub.Call{
call("dbusAddress", stub.ExpectArgs{}, [2]string{"unix:path=/run/user/1000/bus", "unix:path=/run/dbus/system_bus_socket"}, nil),
call("dbusFinalise", stub.ExpectArgs{
dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus"},
dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket"},
&dbus.Config{Talk: []string{"session\x00"}, Filter: true},
&dbus.Config{Talk: []string{"system\x00"}, Filter: true},
}, (*dbus.Final)(nil), syscall.EINVAL),
}}},
{"dbusFinalise", 0xcafebabe, func(_ *testing.T, sys *I) {
wantErr := &OpError{
Op: "dbus", Err: stub.UniqueError(0),
Msg: "cannot finalise message bus proxy: unique error 0 injected by the test suite",
}
if f, err := sys.ProxyDBus(
&dbus.Config{
// use impossible value here as an implicit assert that it goes through the stub
Talk: []string{"session\x00"}, Filter: true,
}, &dbus.Config{
// use impossible value here as an implicit assert that it goes through the stub
Talk: []string{"system\x00"}, Filter: true,
},
"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus",
"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket"); !reflect.DeepEqual(err, wantErr) {
t.Errorf("ProxyDBus: error = %v", err)
} else if f != nil {
t.Errorf("ProxyDBus: f = %p", f)
}
}, nil, stub.Expect{Calls: []stub.Call{
call("dbusAddress", stub.ExpectArgs{}, [2]string{"unix:path=/run/user/1000/bus", "unix:path=/run/dbus/system_bus_socket"}, nil),
call("dbusFinalise", stub.ExpectArgs{
dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus"},
dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket"},
&dbus.Config{Talk: []string{"session\x00"}, Filter: true},
&dbus.Config{Talk: []string{"system\x00"}, Filter: true},
}, (*dbus.Final)(nil), stub.UniqueError(0)),
}}},
{"full", 0xcafebabe, func(_ *testing.T, sys *I) {
sys.MustProxyDBus(
"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus", &dbus.Config{
// use impossible value here as an implicit assert that it goes through the stub
Talk: []string{"session\x00"}, Filter: true,
}, "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket", &dbus.Config{
// use impossible value here as an implicit assert that it goes through the stub
Talk: []string{"system\x00"}, Filter: true,
})
}, []Op{
&dbusProxyOp{
final: dbusNewFinalSample(0),
system: true,
},
}, stub.Expect{Calls: []stub.Call{
call("dbusAddress", stub.ExpectArgs{}, [2]string{"unix:path=/run/user/1000/bus", "unix:path=/run/dbus/system_bus_socket"}, nil),
call("dbusFinalise", stub.ExpectArgs{
dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus"},
dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket"},
&dbus.Config{Talk: []string{"session\x00"}, Filter: true},
&dbus.Config{Talk: []string{"system\x00"}, Filter: true},
}, dbusNewFinalSample(0), nil),
call("isVerbose", stub.ExpectArgs{}, true, nil),
call("verbose", stub.ExpectArgs{[]any{"session bus proxy:", []string{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus", "--filter", "--talk=session\x00"}}}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{"system bus proxy:", []string{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket", "--filter", "--talk=system\x00"}}}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{"message bus proxy final args:", helper.MustNewCheckedArgs([]string{"unique", "value", "0", "injected", "by", "the", "test", "suite"})}}, nil, nil),
}}},
})
checkOpIs(t, []opIsTestCase{
{"nil", (*dbusProxyOp)(nil), (*dbusProxyOp)(nil), false},
{"zero", new(dbusProxyOp), new(dbusProxyOp), false},
{"system differs", &dbusProxyOp{final: &dbus.Final{
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}),
}, system: false,
}, &dbusProxyOp{final: &dbus.Final{
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}),
}, system: true,
}, false},
{"wt differs", &dbusProxyOp{final: &dbus.Final{
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{
"--filter", "unix:path=/run/user/1001/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}),
}, system: true,
}, &dbusProxyOp{final: &dbus.Final{
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}),
}, system: true,
}, false},
{"final system upstream differs", &dbusProxyOp{final: &dbus.Final{
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket\x00"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}),
}, system: true,
}, &dbusProxyOp{final: &dbus.Final{
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}),
}, system: true,
}, false},
{"final session upstream differs", &dbusProxyOp{final: &dbus.Final{
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1001/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}),
}, system: true,
}, &dbusProxyOp{final: &dbus.Final{
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}),
}, system: true,
}, false},
{"final system differs", &dbusProxyOp{final: &dbus.Final{
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.1/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}),
}, system: true,
}, &dbusProxyOp{final: &dbus.Final{
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}),
}, system: true,
}, false},
{"final session differs", &dbusProxyOp{final: &dbus.Final{
Session: dbus.ProxyPair{"unix:path=/run/user/1001/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}),
}, system: true,
}, &dbusProxyOp{final: &dbus.Final{
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}),
}, system: true,
}, false},
{"equals", &dbusProxyOp{final: &dbus.Final{
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}),
}, system: true,
}, &dbusProxyOp{final: &dbus.Final{
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"},
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"},
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{
"--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus",
"--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket",
}),
}, system: true,
}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"dbus", new(dbusProxyOp),
Process, "/proc/nonexistent",
"(invalid dbus proxy)"},
})
}
func dbusNewFinalSample(v int) *dbus.Final {
return &dbus.Final{
Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus"},
System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket"},
SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}},
SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/dbus/system_bus_socket"}}}},
WriterTo: helper.MustNewCheckedArgs([]string{"unique", "value", strconv.Itoa(v), "injected", "by", "the", "test", "suite"}),
}
}
func TestLinePrefixWriter(t *testing.T) {
testCases := []struct {
name string
prefix string
f func(w func(s string))
wantErr []error
wantPt []string
want []string
wantExt []string
wantBuf string
}{
{"nop", "(nop) ", func(func(string)) {}, nil, nil, nil, nil, ""},
{"partial", "(partial) ", func(w func(string)) {
w("C-65533: -> ")
}, nil, nil, nil, []string{
"*(partial) C-65533: -> ",
}, "C-65533: -> "},
{"break", "(break) ", func(w func(string)) {
w("C-65533: -> ")
w("org.freedesktop.DBus fake ListNames\n")
}, nil, nil, []string{
"C-65533: -> org.freedesktop.DBus fake ListNames",
}, nil, ""},
{"break pt", "(break pt) ", func(w func(string)) {
w("init: ")
w("received setup parameters\n")
}, nil, []string{
"init: received setup parameters",
}, nil, nil, ""},
{"threshold", "(threshold) ", func(w func(s string)) {
w(string(make([]byte, lpwSizeThreshold)))
w("\n")
}, []error{nil, syscall.ENOMEM}, nil, nil, []string{
"*(threshold) " + string(make([]byte, lpwSizeThreshold)),
"+(threshold) write threshold reached, output may be incomplete",
}, string(make([]byte, lpwSizeThreshold))},
{"threshold multi", "(threshold multi) ", func(w func(s string)) {
w(":3\n")
w(string(make([]byte, lpwSizeThreshold-3)))
w("\n")
}, []error{nil, nil, syscall.ENOMEM}, nil, []string{
":3",
}, []string{
"*(threshold multi) " + string(make([]byte, lpwSizeThreshold-3)),
"+(threshold multi) write threshold reached, output may be incomplete",
}, string(make([]byte, lpwSizeThreshold-3))},
{"threshold multi partial", "(threshold multi partial) ", func(w func(s string)) {
w(":3\n")
w(string(make([]byte, lpwSizeThreshold-2)))
w("dropped\n")
}, []error{nil, nil, syscall.ENOMEM}, nil, []string{
":3",
}, []string{
"*(threshold multi partial) " + string(make([]byte, lpwSizeThreshold-2)),
"+(threshold multi partial) write threshold reached, output may be incomplete",
}, string(make([]byte, lpwSizeThreshold-2))},
{"threshold exact", "(threshold exact) ", func(w func(s string)) {
w(string(make([]byte, lpwSizeThreshold-1)))
w("\n")
}, nil, nil, []string{
string(make([]byte, lpwSizeThreshold-1)),
}, []string{
"+(threshold exact) write threshold reached, output may be incomplete",
}, ""},
{"sample", "(dbus) ", func(w func(s string)) {
w("init: received setup parameters\n")
w(`init: mounting "/nix/store/5gml2l2cj28yvyfyzblzjy1laqpxmyzd-libselinux-3.8.1/lib" flags 0x0` + "\n")
w(`init: resolved "/host/nix/store/5gml2l2cj28yvyfyzblzjy1laqpxmyzd-libselinux-3.8.1/lib" on "/sysroot/nix/store/5gml2l2cj28yvyfyzblzjy1laqpxmyzd-libselinux-3.8.1/lib" flags 0x4005` + "\n")
w(`init: mounting "/nix/store/bcs094l67dlbqf7idxxbljp293zms9mh-util-linux-minimal-2.41-lib/lib" flags 0x0` + "\n")
w(`init: resolved "/host/nix/store/bcs094l67dlbqf7idxxbljp293zms9mh-util-linux-minimal-2.41-lib/lib" on "/sysroot/nix/store/bcs094l67dlbqf7idxxbljp293zms9mh-util-linux-minimal-2.41-lib/lib" flags 0x4005` + "\n")
w(`init: mounting "/nix/store/jl19fdc7gdxqz9a1s368r9d15vpirnqy-zlib-1.3.1/lib" flags 0x0` + "\n")
w(`init: resolved "/host/nix/store/jl19fdc7gdxqz9a1s368r9d15vpirnqy-zlib-1.3.1/lib" on "/sysroot/nix/store/jl19fdc7gdxqz9a1s368r9d15vpirnqy-zlib-1.3.1/lib" flags 0x4005` + "\n")
w(`init: mounting "/nix/store/rnn29mhynsa4ncmk0fkcrdr29n0j20l4-libffi-3.4.8/lib" flags 0x0` + "\n")
w(`init: resolved "/host/nix/store/rnn29mhynsa4ncmk0fkcrdr29n0j20l4-libffi-3.4.8/lib" on "/sysroot/nix/store/rnn29mhynsa4ncmk0fkcrdr29n0j20l4-libffi-3.4.8/lib" flags 0x4005` + "\n")
w(`init: mounting "/nix/store/vvp8hlss3d5q6hn0cifq04jrpnp6bini-pcre2-10.44/lib" flags 0x0` + "\n")
w(`init: resolved "/host/nix/store/vvp8hlss3d5q6hn0cifq04jrpnp6bini-pcre2-10.44/lib" on "/sysroot/nix/store/vvp8hlss3d5q6hn0cifq04jrpnp6bini-pcre2-10.44/lib" flags 0x4005` + "\n")
w(`init: mounting "/nix/store/y3nxdc2x8hwivppzgx5hkrhacsh87l21-glib-2.84.3/lib" flags 0x0` + "\n")
w(`init: resolved "/host/nix/store/y3nxdc2x8hwivppzgx5hkrhacsh87l21-glib-2.84.3/lib" on "/sysroot/nix/store/y3nxdc2x8hwivppzgx5hkrhacsh87l21-glib-2.84.3/lib" flags 0x4005` + "\n")
w(`init: mounting "/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib" flags 0x0` + "\n")
w(`init: resolved "/host/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib" on "/sysroot/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib" flags 0x4005` + "\n")
w(`init: mounting "/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib64" flags 0x0` + "\n")
w(`init: resolved "/host/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib" on "/sysroot/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib64" flags 0x4005` + "\n")
w(`init: mounting "/run/user/1000" flags 0x0` + "\n")
w(`init: resolved "/host/run/user/1000" on "/sysroot/run/user/1000" flags 0x4005` + "\n")
w(`init: mounting "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f" flags 0x2` + "\n")
w(`init: resolved "/host/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f" on "/sysroot/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f" flags 0x4004` + "\n")
w(`init: mounting "/nix/store/d2divmq2d897amikcwpdx7zrbpddxxcl-xdg-dbus-proxy-0.1.6/bin" flags 0x0` + "\n")
w(`init: resolved "/host/nix/store/d2divmq2d897amikcwpdx7zrbpddxxcl-xdg-dbus-proxy-0.1.6/bin" on "/sysroot/nix/store/d2divmq2d897amikcwpdx7zrbpddxxcl-xdg-dbus-proxy-0.1.6/bin" flags 0x4005` + "\n")
w("init: resolving presets 0xf\n")
w("init: 68 filter rules loaded\n")
w("init: starting initial program /nix/store/d2divmq2d897amikcwpdx7zrbpddxxcl-xdg-dbus-proxy-0.1.6/bin/xdg-dbus-proxy\n")
w("C1: -> org.freedesktop.DBus call org.freedesktop.DBus.Hello at /org/freedesktop/DBus\n")
w("C-65536: -> org.freedesktop.DBus fake wildcarded AddMatch for org.freedesktop.portal\n")
w("C-65535: -> org.freedesktop.DBus fake AddMatch for org.freedesktop.Notifications\n")
w("C-65534: -> org.freedesktop.DBus fake GetNameOwner for org.freedesktop.Notifications\n")
w("C-65533: -> org.freedesktop.DBus fake ListNames\n")
w("B1: <- org.freedesktop.DBus return from C1\n")
w("B2: <- org.freedesktop.DBus signal org.freedesktop.DBus.NameAcquired at /org/freedesktop/DBus\n")
w("B3: <- org.freedesktop.DBus return from C-65536\n")
w("*SKIPPED*\n")
w("B4: <- org.freedesktop.DBus return from C-65535\n")
w("*SKIPPED*\n")
w("B5: <- org.freedesktop.DBus return error org.freedesktop.DBus.Error.NameHasNoOwner from C-65534\n")
w("*SKIPPED*\n")
w("B6: <- org.freedesktop.DBus return from C-65533\n")
w("C-65532: -> org.freedesktop.DBus fake GetNameOwner for org.freedesktop.DBus\n")
w("*SKIPPED*\n")
w("B7: <- org.freedesktop.DBus return from C-65532\n")
w("*SKIPPED*\n")
w("C2: -> org.freedesktop.DBus call org.freedesktop.DBus.AddMatch at /org/freedesktop/DBus\n")
w("C3: -> org.freedesktop.DBus call org.freedesktop.DBus.GetNameOwner at /org/freedesktop/DBus\n")
w("C4: -> org.freedesktop.DBus call org.freedesktop.DBus.AddMatch at /org/freedesktop/DBus\n")
w("C5: -> org.freedesktop.DBus call org.freedesktop.DBus.StartServiceByName at /org/freedesktop/DBus\n")
w("B8: <- org.freedesktop.DBus return from C2\n")
w("B9: <- org.freedesktop.DBus return error org.freedesktop.DBus.Error.NameHasNoOwner from C3\n")
w("B10: <- org.freedesktop.DBus return from C4\n")
w("B12: <- org.freedesktop.DBus signal org.freedesktop.DBus.NameOwnerChanged at /org/freedesktop/DBus\n")
w("B11: <- org.freedesktop.DBus return from C5\n")
w("C6: -> org.freedesktop.DBus call org.freedesktop.DBus.GetNameOwner at /org/freedesktop/DBus\n")
w("B13: <- org.freedesktop.DBus return from C6\n")
w("C7: -> :1.4 call org.freedesktop.Notifications.GetServerInformation at /org/freedesktop/Notifications\n")
w("B4: <- :1.4 return from C7\n")
w("C8: -> :1.4 call org.freedesktop.Notifications.GetServerInformation at /org/freedesktop/Notifications\n")
w("B5: <- :1.4 return from C8\n")
w("C9: -> :1.4 call org.freedesktop.Notifications.Notify at /org/freedesktop/Notifications\n")
w("B6: <- :1.4 return from C9\n")
w("C10: -> org.freedesktop.DBus call org.freedesktop.DBus.RemoveMatch at /org/freedesktop/DBus\n")
w("C11: -> org.freedesktop.DBus call org.freedesktop.DBus.RemoveMatch at /org/freedesktop/DBus\n")
w("B14: <- org.freedesktop.DBus return from C10\n")
w("B15: <- org.freedesktop.DBus return from C11\n")
w("init: initial process exited with code 0\n")
}, nil, []string{
"init: received setup parameters",
`init: mounting "/nix/store/5gml2l2cj28yvyfyzblzjy1laqpxmyzd-libselinux-3.8.1/lib" flags 0x0`,
`init: resolved "/host/nix/store/5gml2l2cj28yvyfyzblzjy1laqpxmyzd-libselinux-3.8.1/lib" on "/sysroot/nix/store/5gml2l2cj28yvyfyzblzjy1laqpxmyzd-libselinux-3.8.1/lib" flags 0x4005`,
`init: mounting "/nix/store/bcs094l67dlbqf7idxxbljp293zms9mh-util-linux-minimal-2.41-lib/lib" flags 0x0`,
`init: resolved "/host/nix/store/bcs094l67dlbqf7idxxbljp293zms9mh-util-linux-minimal-2.41-lib/lib" on "/sysroot/nix/store/bcs094l67dlbqf7idxxbljp293zms9mh-util-linux-minimal-2.41-lib/lib" flags 0x4005`,
`init: mounting "/nix/store/jl19fdc7gdxqz9a1s368r9d15vpirnqy-zlib-1.3.1/lib" flags 0x0`,
`init: resolved "/host/nix/store/jl19fdc7gdxqz9a1s368r9d15vpirnqy-zlib-1.3.1/lib" on "/sysroot/nix/store/jl19fdc7gdxqz9a1s368r9d15vpirnqy-zlib-1.3.1/lib" flags 0x4005`,
`init: mounting "/nix/store/rnn29mhynsa4ncmk0fkcrdr29n0j20l4-libffi-3.4.8/lib" flags 0x0`,
`init: resolved "/host/nix/store/rnn29mhynsa4ncmk0fkcrdr29n0j20l4-libffi-3.4.8/lib" on "/sysroot/nix/store/rnn29mhynsa4ncmk0fkcrdr29n0j20l4-libffi-3.4.8/lib" flags 0x4005`,
`init: mounting "/nix/store/vvp8hlss3d5q6hn0cifq04jrpnp6bini-pcre2-10.44/lib" flags 0x0`,
`init: resolved "/host/nix/store/vvp8hlss3d5q6hn0cifq04jrpnp6bini-pcre2-10.44/lib" on "/sysroot/nix/store/vvp8hlss3d5q6hn0cifq04jrpnp6bini-pcre2-10.44/lib" flags 0x4005`,
`init: mounting "/nix/store/y3nxdc2x8hwivppzgx5hkrhacsh87l21-glib-2.84.3/lib" flags 0x0`,
`init: resolved "/host/nix/store/y3nxdc2x8hwivppzgx5hkrhacsh87l21-glib-2.84.3/lib" on "/sysroot/nix/store/y3nxdc2x8hwivppzgx5hkrhacsh87l21-glib-2.84.3/lib" flags 0x4005`,
`init: mounting "/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib" flags 0x0`,
`init: resolved "/host/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib" on "/sysroot/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib" flags 0x4005`,
`init: mounting "/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib64" flags 0x0`,
`init: resolved "/host/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib" on "/sysroot/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib64" flags 0x4005`,
`init: mounting "/run/user/1000" flags 0x0`,
`init: resolved "/host/run/user/1000" on "/sysroot/run/user/1000" flags 0x4005`,
`init: mounting "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f" flags 0x2`,
`init: resolved "/host/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f" on "/sysroot/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f" flags 0x4004`,
`init: mounting "/nix/store/d2divmq2d897amikcwpdx7zrbpddxxcl-xdg-dbus-proxy-0.1.6/bin" flags 0x0`,
`init: resolved "/host/nix/store/d2divmq2d897amikcwpdx7zrbpddxxcl-xdg-dbus-proxy-0.1.6/bin" on "/sysroot/nix/store/d2divmq2d897amikcwpdx7zrbpddxxcl-xdg-dbus-proxy-0.1.6/bin" flags 0x4005`,
"init: resolving presets 0xf",
"init: 68 filter rules loaded",
"init: starting initial program /nix/store/d2divmq2d897amikcwpdx7zrbpddxxcl-xdg-dbus-proxy-0.1.6/bin/xdg-dbus-proxy",
"init: initial process exited with code 0",
}, []string{
"C1: -> org.freedesktop.DBus call org.freedesktop.DBus.Hello at /org/freedesktop/DBus",
"C-65536: -> org.freedesktop.DBus fake wildcarded AddMatch for org.freedesktop.portal",
"C-65535: -> org.freedesktop.DBus fake AddMatch for org.freedesktop.Notifications",
"C-65534: -> org.freedesktop.DBus fake GetNameOwner for org.freedesktop.Notifications",
"C-65533: -> org.freedesktop.DBus fake ListNames",
"B1: <- org.freedesktop.DBus return from C1",
"B2: <- org.freedesktop.DBus signal org.freedesktop.DBus.NameAcquired at /org/freedesktop/DBus",
"B3: <- org.freedesktop.DBus return from C-65536",
"*SKIPPED*",
"B4: <- org.freedesktop.DBus return from C-65535",
"*SKIPPED*",
"B5: <- org.freedesktop.DBus return error org.freedesktop.DBus.Error.NameHasNoOwner from C-65534",
"*SKIPPED*",
"B6: <- org.freedesktop.DBus return from C-65533",
"C-65532: -> org.freedesktop.DBus fake GetNameOwner for org.freedesktop.DBus",
"*SKIPPED*",
"B7: <- org.freedesktop.DBus return from C-65532",
"*SKIPPED*",
"C2: -> org.freedesktop.DBus call org.freedesktop.DBus.AddMatch at /org/freedesktop/DBus",
"C3: -> org.freedesktop.DBus call org.freedesktop.DBus.GetNameOwner at /org/freedesktop/DBus",
"C4: -> org.freedesktop.DBus call org.freedesktop.DBus.AddMatch at /org/freedesktop/DBus",
"C5: -> org.freedesktop.DBus call org.freedesktop.DBus.StartServiceByName at /org/freedesktop/DBus",
"B8: <- org.freedesktop.DBus return from C2",
"B9: <- org.freedesktop.DBus return error org.freedesktop.DBus.Error.NameHasNoOwner from C3",
"B10: <- org.freedesktop.DBus return from C4",
"B12: <- org.freedesktop.DBus signal org.freedesktop.DBus.NameOwnerChanged at /org/freedesktop/DBus",
"B11: <- org.freedesktop.DBus return from C5",
"C6: -> org.freedesktop.DBus call org.freedesktop.DBus.GetNameOwner at /org/freedesktop/DBus",
"B13: <- org.freedesktop.DBus return from C6",
"C7: -> :1.4 call org.freedesktop.Notifications.GetServerInformation at /org/freedesktop/Notifications",
"B4: <- :1.4 return from C7",
"C8: -> :1.4 call org.freedesktop.Notifications.GetServerInformation at /org/freedesktop/Notifications",
"B5: <- :1.4 return from C8",
"C9: -> :1.4 call org.freedesktop.Notifications.Notify at /org/freedesktop/Notifications",
"B6: <- :1.4 return from C9",
"C10: -> org.freedesktop.DBus call org.freedesktop.DBus.RemoveMatch at /org/freedesktop/DBus",
"C11: -> org.freedesktop.DBus call org.freedesktop.DBus.RemoveMatch at /org/freedesktop/DBus",
"B14: <- org.freedesktop.DBus return from C10",
"B15: <- org.freedesktop.DBus return from C11",
}, nil, ""},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
gotPt := make([]string, 0, len(tc.wantPt))
out := &linePrefixWriter{
prefix: tc.prefix,
println: func(v ...any) {
if len(v) != 1 {
t.Fatalf("invalid call to println: %#v", v)
}
gotPt = append(gotPt, v[0].(string))
},
buf: new(strings.Builder),
}
var pos int
tc.f(func(s string) {
_, err := out.Write([]byte(s))
if tc.wantErr != nil {
if !reflect.DeepEqual(err, tc.wantErr[pos]) {
t.Fatalf("Write: error = %v, want %v", err, tc.wantErr[pos])
}
} else if err != nil {
t.Fatalf("Write: unexpected error: %v", err)
return
}
pos++
})
if !slices.Equal(out.msg, tc.want) {
t.Errorf("msg: %#v, want %#v", out.msg, tc.want)
}
if out.buf.String() != tc.wantBuf {
t.Errorf("buf: %q, want %q", out.buf, tc.wantBuf)
}
wantPt := make([]string, len(tc.wantPt))
for i, m := range tc.wantPt {
wantPt[i] = tc.prefix + m
}
if !slices.Equal(gotPt, wantPt) {
t.Errorf("passthrough: %#v, want %#v", gotPt, wantPt)
}
wantDump := make([]string, len(tc.want)+len(tc.wantExt))
for i, m := range tc.want {
wantDump[i] = tc.prefix + m
}
for i, m := range tc.wantExt {
wantDump[len(tc.want)+i] = m
}
t.Run("dump", func(t *testing.T) {
got := make([]string, 0, len(wantDump))
out.println = func(v ...any) {
if len(v) != 1 {
t.Fatalf("Dump: invalid call to println: %#v", v)
}
got = append(got, v[0].(string))
}
out.Dump()
if !slices.Equal(got, wantDump) {
t.Errorf("Dump: %#v, want %#v", got, wantDump)
}
})
})
}
}

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