Compare commits

...

37 Commits

Author SHA1 Message Date
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
101 changed files with 6643 additions and 4574 deletions

View File

@ -162,10 +162,14 @@ func buildCommand(out io.Writer) command.Command {
// override log from configuration // override log from configuration
if dbusVerbose { if dbusVerbose {
if config.SessionBus != nil {
config.SessionBus.Log = true config.SessionBus.Log = true
}
if config.SystemBus != nil {
config.SystemBus.Log = true config.SystemBus.Log = true
} }
} }
}
// invoke app // invoke app
runApp(config) runApp(config)

View File

@ -3,7 +3,6 @@ package container
import ( import (
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"io/fs"
) )
func init() { gob.Register(new(AutoEtcOp)) } 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) early(*setupState, syscallDispatcher) error { return nil }
func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error { func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
if state.nonrepeatable&nrAutoEtc != 0 { if state.nonrepeatable&nrAutoEtc != 0 {
return msg.WrapErr(fs.ErrInvalid, "autoetc is not repeatable") return OpRepeatError("autoetc")
} }
state.nonrepeatable |= nrAutoEtc state.nonrepeatable |= nrAutoEtc
@ -32,10 +31,10 @@ func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
rel := e.hostRel() + "/" rel := e.hostRel() + "/"
if err := k.mkdirAll(target, 0755); err != nil { if err := k.mkdirAll(target, 0755); err != nil {
return wrapErrSelf(err) return err
} }
if d, err := k.readdir(toSysroot(e.hostPath().String())); err != nil { if d, err := k.readdir(toSysroot(e.hostPath().String())); err != nil {
return wrapErrSelf(err) return err
} else { } else {
for _, ent := range d { for _, ent := range d {
n := ent.Name() n := ent.Name()
@ -44,12 +43,12 @@ func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
case "mtab": case "mtab":
if err = k.symlink(FHSProc+"mounts", target+n); err != nil { if err = k.symlink(FHSProc+"mounts", target+n); err != nil {
return wrapErrSelf(err) return err
} }
default: default:
if err = k.symlink(rel+n, target+n); err != nil { if err = k.symlink(rel+n, target+n); err != nil {
return wrapErrSelf(err) return err
} }
} }
} }

View File

@ -2,14 +2,15 @@ package container
import ( import (
"errors" "errors"
"io/fs"
"os" "os"
"testing" "testing"
"hakurei.app/container/stub"
) )
func TestAutoEtcOp(t *testing.T) { func TestAutoEtcOp(t *testing.T) {
t.Run("nonrepeatable", func(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) { if err := (&AutoEtcOp{Prefix: "81ceabb30d37bbdb3868004629cb84e9"}).apply(&setupState{nonrepeatable: nrAutoEtc}, nil); !errors.Is(err, wantErr) {
t.Errorf("apply: error = %v, want %v", err, wantErr) t.Errorf("apply: error = %v, want %v", err, wantErr)
} }
@ -18,22 +19,22 @@ func TestAutoEtcOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{ checkOpBehaviour(t, []opBehaviourTestCase{
{"mkdirAll", new(Params), &AutoEtcOp{ {"mkdirAll", new(Params), &AutoEtcOp{
Prefix: "81ceabb30d37bbdb3868004629cb84e9", Prefix: "81ceabb30d37bbdb3868004629cb84e9",
}, nil, nil, []kexpect{ }, nil, nil, []stub.Call{
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, errUnique}, call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, stub.UniqueError(3)),
}, wrapErrSelf(errUnique)}, }, stub.UniqueError(3)},
{"readdir", new(Params), &AutoEtcOp{ {"readdir", new(Params), &AutoEtcOp{
Prefix: "81ceabb30d37bbdb3868004629cb84e9", Prefix: "81ceabb30d37bbdb3868004629cb84e9",
}, nil, nil, []kexpect{ }, nil, nil, []stub.Call{
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil}, call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil),
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(), errUnique}, call("readdir", stub.ExpectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(), stub.UniqueError(2)),
}, wrapErrSelf(errUnique)}, }, stub.UniqueError(2)},
{"symlink", new(Params), &AutoEtcOp{ {"symlink", new(Params), &AutoEtcOp{
Prefix: "81ceabb30d37bbdb3868004629cb84e9", Prefix: "81ceabb30d37bbdb3868004629cb84e9",
}, nil, nil, []kexpect{ }, nil, nil, []stub.Call{
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil}, call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil),
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host", call("readdir", stub.ExpectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts", "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", "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", "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", "nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells", "protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo", "ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil}, "tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, errUnique}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, stub.UniqueError(1)),
}, wrapErrSelf(errUnique)}, }, stub.UniqueError(1)},
{"symlink mtab", new(Params), &AutoEtcOp{ {"symlink mtab", new(Params), &AutoEtcOp{
Prefix: "81ceabb30d37bbdb3868004629cb84e9", Prefix: "81ceabb30d37bbdb3868004629cb84e9",
}, nil, nil, []kexpect{ }, nil, nil, []stub.Call{
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil}, call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil),
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host", call("readdir", stub.ExpectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts", "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", "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", "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", "nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells", "protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo", "ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil}, "tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil}, call("symlink", stub.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}, call("symlink", stub.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}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil}, call("symlink", stub.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}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil),
{"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, errUnique}, call("symlink", stub.ExpectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, stub.UniqueError(0)),
}, wrapErrSelf(errUnique)}, }, stub.UniqueError(0)},
{"success nested", new(Params), &AutoEtcOp{ {"success nested", new(Params), &AutoEtcOp{
Prefix: "81ceabb30d37bbdb3868004629cb84e9", Prefix: "81ceabb30d37bbdb3868004629cb84e9",
}, nil, nil, []kexpect{ }, nil, nil, []stub.Call{
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil}, call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil),
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host", call("readdir", stub.ExpectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts", "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", "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", "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", "nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells", "protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo", "ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil}, "tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil}, call("symlink", stub.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}, call("symlink", stub.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}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil}, call("symlink", stub.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}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil),
{"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil}, call("symlink", stub.ExpectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil),
}, nil}, }, nil},
{"success", new(Params), &AutoEtcOp{ {"success", new(Params), &AutoEtcOp{
Prefix: "81ceabb30d37bbdb3868004629cb84e9", Prefix: "81ceabb30d37bbdb3868004629cb84e9",
}, nil, nil, []kexpect{ }, nil, nil, []stub.Call{
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil}, call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil),
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir( call("readdir", stub.ExpectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts", "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", "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", "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", "nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells", "protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo", "ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil}, "tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil}, call("symlink", stub.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}, call("symlink", stub.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}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil}, call("symlink", stub.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}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil),
{"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil}, call("symlink", stub.ExpectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil),
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil}, call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil),
}, nil}, }, nil},
}) })

View File

@ -3,7 +3,6 @@ package container
import ( import (
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"io/fs"
) )
func init() { gob.Register(new(AutoRootOp)) } 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 { func (r *AutoRootOp) early(state *setupState, k syscallDispatcher) error {
if d, err := k.readdir(r.Host.String()); err != nil { if d, err := k.readdir(r.Host.String()); err != nil {
return wrapErrSelf(err) return err
} else { } else {
r.resolved = make([]Op, 0, len(d)) r.resolved = make([]Op, 0, len(d))
for _, ent := range 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 { func (r *AutoRootOp) apply(state *setupState, k syscallDispatcher) error {
if state.nonrepeatable&nrAutoRoot != 0 { if state.nonrepeatable&nrAutoRoot != 0 {
return msg.WrapErr(fs.ErrInvalid, "autoroot is not repeatable") return OpRepeatError("autoroot")
} }
state.nonrepeatable |= nrAutoRoot state.nonrepeatable |= nrAutoRoot

View File

@ -2,14 +2,15 @@ package container
import ( import (
"errors" "errors"
"io/fs"
"os" "os"
"testing" "testing"
"hakurei.app/container/stub"
) )
func TestAutoRootOp(t *testing.T) { func TestAutoRootOp(t *testing.T) {
t.Run("nonrepeatable", func(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) { if err := new(AutoRootOp).apply(&setupState{nonrepeatable: nrAutoRoot}, nil); !errors.Is(err, wantErr) {
t.Errorf("apply: error = %v, want %v", 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{ {"readdir", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: MustAbs("/"), Host: MustAbs("/"),
Flags: BindWritable, Flags: BindWritable,
}, []kexpect{ }, []stub.Call{
{"readdir", expectArgs{"/"}, stubDir(), errUnique}, call("readdir", stub.ExpectArgs{"/"}, stubDir(), stub.UniqueError(2)),
}, wrapErrSelf(errUnique), nil, nil}, }, stub.UniqueError(2), nil, nil},
{"early", &Params{ParentPerm: 0750}, &AutoRootOp{ {"early", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: MustAbs("/"), Host: MustAbs("/"),
Flags: BindWritable, Flags: BindWritable,
}, []kexpect{ }, []stub.Call{
{"readdir", expectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64", call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil}, "lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
{"evalSymlinks", expectArgs{"/bin"}, "", errUnique}, call("evalSymlinks", stub.ExpectArgs{"/bin"}, "", stub.UniqueError(1)),
}, wrapErrSelf(errUnique), nil, nil}, }, stub.UniqueError(1), nil, nil},
{"apply", &Params{ParentPerm: 0750}, &AutoRootOp{ {"apply", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: MustAbs("/"), Host: MustAbs("/"),
Flags: BindWritable, Flags: BindWritable,
}, []kexpect{ }, []stub.Call{
{"readdir", expectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64", call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil}, "lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
{"evalSymlinks", expectArgs{"/bin"}, "/usr/bin", nil}, call("evalSymlinks", stub.ExpectArgs{"/bin"}, "/usr/bin", nil),
{"evalSymlinks", expectArgs{"/home"}, "/home", nil}, call("evalSymlinks", stub.ExpectArgs{"/home"}, "/home", nil),
{"evalSymlinks", expectArgs{"/lib64"}, "/lib64", nil}, call("evalSymlinks", stub.ExpectArgs{"/lib64"}, "/lib64", nil),
{"evalSymlinks", expectArgs{"/lost+found"}, "/lost+found", nil}, call("evalSymlinks", stub.ExpectArgs{"/lost+found"}, "/lost+found", nil),
{"evalSymlinks", expectArgs{"/nix"}, "/nix", nil}, call("evalSymlinks", stub.ExpectArgs{"/nix"}, "/nix", nil),
{"evalSymlinks", expectArgs{"/root"}, "/root", nil}, call("evalSymlinks", stub.ExpectArgs{"/root"}, "/root", nil),
{"evalSymlinks", expectArgs{"/run"}, "/run", nil}, call("evalSymlinks", stub.ExpectArgs{"/run"}, "/run", nil),
{"evalSymlinks", expectArgs{"/srv"}, "/srv", nil}, call("evalSymlinks", stub.ExpectArgs{"/srv"}, "/srv", nil),
{"evalSymlinks", expectArgs{"/sys"}, "/sys", nil}, call("evalSymlinks", stub.ExpectArgs{"/sys"}, "/sys", nil),
{"evalSymlinks", expectArgs{"/usr"}, "/usr", nil}, call("evalSymlinks", stub.ExpectArgs{"/usr"}, "/usr", nil),
{"evalSymlinks", expectArgs{"/var"}, "/var", nil}, call("evalSymlinks", stub.ExpectArgs{"/var"}, "/var", nil),
}, nil, []kexpect{ }, nil, []stub.Call{
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr/bin"), MustAbs("/bin"), MustAbs("/bin"), BindWritable}}}, nil, nil}, call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr/bin"), MustAbs("/bin"), MustAbs("/bin"), BindWritable}}}, nil, nil),
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(false), errUnique}, call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(false), stub.UniqueError(0)),
}, wrapErrSelf(errUnique)}, }, stub.UniqueError(0)},
{"success pd", &Params{ParentPerm: 0750}, &AutoRootOp{ {"success pd", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: MustAbs("/"), Host: MustAbs("/"),
Flags: BindWritable, Flags: BindWritable,
}, []kexpect{ }, []stub.Call{
{"readdir", expectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64", call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil}, "lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
{"evalSymlinks", expectArgs{"/bin"}, "/usr/bin", nil}, call("evalSymlinks", stub.ExpectArgs{"/bin"}, "/usr/bin", nil),
{"evalSymlinks", expectArgs{"/home"}, "/home", nil}, call("evalSymlinks", stub.ExpectArgs{"/home"}, "/home", nil),
{"evalSymlinks", expectArgs{"/lib64"}, "/lib64", nil}, call("evalSymlinks", stub.ExpectArgs{"/lib64"}, "/lib64", nil),
{"evalSymlinks", expectArgs{"/lost+found"}, "/lost+found", nil}, call("evalSymlinks", stub.ExpectArgs{"/lost+found"}, "/lost+found", nil),
{"evalSymlinks", expectArgs{"/nix"}, "/nix", nil}, call("evalSymlinks", stub.ExpectArgs{"/nix"}, "/nix", nil),
{"evalSymlinks", expectArgs{"/root"}, "/root", nil}, call("evalSymlinks", stub.ExpectArgs{"/root"}, "/root", nil),
{"evalSymlinks", expectArgs{"/run"}, "/run", nil}, call("evalSymlinks", stub.ExpectArgs{"/run"}, "/run", nil),
{"evalSymlinks", expectArgs{"/srv"}, "/srv", nil}, call("evalSymlinks", stub.ExpectArgs{"/srv"}, "/srv", nil),
{"evalSymlinks", expectArgs{"/sys"}, "/sys", nil}, call("evalSymlinks", stub.ExpectArgs{"/sys"}, "/sys", nil),
{"evalSymlinks", expectArgs{"/usr"}, "/usr", nil}, call("evalSymlinks", stub.ExpectArgs{"/usr"}, "/usr", nil),
{"evalSymlinks", expectArgs{"/var"}, "/var", nil}, call("evalSymlinks", stub.ExpectArgs{"/var"}, "/var", nil),
}, nil, []kexpect{ }, nil, []stub.Call{
{"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}, 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),
{"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}, 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),
{"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}, 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),
{"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}, 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),
{"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}, 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),
{"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}, 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),
{"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}, 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),
{"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}, 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),
{"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}, 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),
{"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}, 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),
{"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}, 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}, }, nil},
{"success", &Params{ParentPerm: 0750}, &AutoRootOp{ {"success", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: MustAbs("/var/lib/planterette/base/debian:f92c9052"), Host: MustAbs("/var/lib/planterette/base/debian:f92c9052"),
}, []kexpect{ }, []stub.Call{
{"readdir", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, stubDir("bin", "dev", "etc", "home", "lib64", 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}, "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}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/var"}, "/var/lib/planterette/base/debian:f92c9052/var", nil),
}, nil, []kexpect{ }, nil, []stub.Call{
{"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}, 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),
{"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}, 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),
{"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}, 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),
{"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}, 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),
{"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}, 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),
{"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}, 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),
{"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}, 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),
{"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}, 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),
{"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}, 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),
{"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}, 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),
{"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}, 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}, }, nil},
}) })

View File

@ -99,6 +99,57 @@ type (
} }
) )
// 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 e.Origin {
return e.Step
}
{
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. // Start starts the container init. The init process blocks until Serve is called.
func (p *Container) Start() error { func (p *Container) Start() error {
if p.cmd != nil { if p.cmd != nil {
@ -167,8 +218,7 @@ func (p *Container) Start() error {
// place setup pipe before user supplied extra files, this is later restored by init // place setup pipe before user supplied extra files, this is later restored by init
if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil { if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil {
return wrapErrSuffix(err, return &StartError{true, "set up params stream", err, false, false}
"cannot create shim setup pipe:")
} else { } else {
p.setup = e p.setup = e
p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)} p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
@ -183,8 +233,7 @@ func (p *Container) Start() error {
done <- func() error { // setup depending on per-thread state must happen here 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 // PR_SET_NO_NEW_PRIVS: depends on per-thread state but acts on all processes created from that thread
if err := SetNoNewPrivs(); err != nil { if err := SetNoNewPrivs(); err != nil {
return wrapErrSuffix(err, return &StartError{true, "prctl(PR_SET_NO_NEW_PRIVS)", err, false, false}
"prctl(PR_SET_NO_NEW_PRIVS):")
} }
// landlock: depends on per-thread state but acts on a process group // landlock: depends on per-thread state but acts on a process group
@ -200,28 +249,24 @@ func (p *Container) Start() error {
// already covered by namespaces (pid) // already covered by namespaces (pid)
goto landlockOut goto landlockOut
} }
return wrapErrSuffix(err, return &StartError{false, "get landlock ABI", err, false, false}
"landlock does not appear to be enabled:")
} else if abi < 6 { } else if abi < 6 {
if p.HostAbstract { if p.HostAbstract {
// see above comment // see above comment
goto landlockOut goto landlockOut
} }
return msg.WrapErr(ENOSYS, return &StartError{false, "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", ENOSYS, true, false}
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET")
} else { } else {
msg.Verbosef("landlock abi version %d", abi) msg.Verbosef("landlock abi version %d", abi)
} }
if rulesetFd, err := rulesetAttr.Create(0); err != nil { if rulesetFd, err := rulesetAttr.Create(0); err != nil {
return wrapErrSuffix(err, return &StartError{true, "create landlock ruleset", err, false, false}
"cannot create landlock ruleset:")
} else { } else {
msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr) msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
if err = LandlockRestrictSelf(rulesetFd, 0); err != nil { if err = LandlockRestrictSelf(rulesetFd, 0); err != nil {
_ = Close(rulesetFd) _ = Close(rulesetFd)
return wrapErrSuffix(err, return &StartError{true, "enforce landlock ruleset", err, false, false}
"cannot enforce landlock ruleset:")
} }
if err = Close(rulesetFd); err != nil { if err = Close(rulesetFd); err != nil {
msg.Verbosef("cannot close landlock ruleset: %v", err) msg.Verbosef("cannot close landlock ruleset: %v", err)
@ -234,7 +279,7 @@ func (p *Container) Start() error {
msg.Verbose("starting container init") msg.Verbose("starting container init")
if err := p.cmd.Start(); err != nil { if err := p.cmd.Start(); err != nil {
return msg.WrapErr(err, err.Error()) return &StartError{false, "start container init", err, false, true}
} }
return nil return nil
}() }()
@ -257,7 +302,7 @@ func (p *Container) Serve() error {
if p.Path == nil { if p.Path == nil {
p.cancel() p.cancel()
return msg.WrapErr(EINVAL, "invalid executable pathname") return &StartError{false, "invalid executable pathname", EINVAL, true, false}
} }
// do not transmit nil // do not transmit nil

View File

@ -7,9 +7,11 @@ import (
"errors" "errors"
"fmt" "fmt"
"log" "log"
"net"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"reflect"
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
@ -26,6 +28,143 @@ import (
"hakurei.app/ldd" "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 ( const (
ignore = "\x00" ignore = "\x00"
ignoreV = -1 ignoreV = -1
@ -75,7 +214,7 @@ var containerTestCases = []struct {
1000, 100, nil, 0, seccomp.PresetExt}, 1000, 100, nil, 0, seccomp.PresetExt},
{"custom rules", true, true, true, false, {"custom rules", true, true, true, false,
emptyOps, emptyMnt, 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, {"tmpfs", true, false, false, true,
earlyOps(new(container.Ops). 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) { t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) {
wantErr := context.Canceled wantErr := context.Canceled
wantExitCode := 0 wantExitCode := 0
if err := c.Wait(); !errors.Is(err, wantErr) { if err := c.Wait(); !reflect.DeepEqual(err, wantErr) {
container.GetOutput().PrintBaseErr(err, "wait:") if m, ok := container.InternalMessageFromError(err); ok {
t.Errorf("Wait: error = %v, want %v", err, wantErr) t.Error(m)
}
t.Errorf("Wait: error = %#v, want %#v", err, wantErr)
} }
if ps := c.ProcessState(); ps == nil { if ps := c.ProcessState(); ps == nil {
t.Errorf("ProcessState unexpectedly returned nil") t.Errorf("ProcessState unexpectedly returned nil")
@ -233,7 +374,9 @@ func TestContainer(t *testing.T) {
}, func(t *testing.T, c *container.Container) { }, func(t *testing.T, c *container.Container) {
var exitError *exec.ExitError var exitError *exec.ExitError
if err := c.Wait(); !errors.As(err, &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) t.Errorf("Wait: error = %v", err)
} }
if code := exitError.ExitCode(); code != blockExitCodeInterrupt { if code := exitError.ExitCode(); code != blockExitCodeInterrupt {
@ -313,18 +456,27 @@ func TestContainer(t *testing.T) {
if err := c.Start(); err != nil { if err := c.Start(); err != nil {
_, _ = output.WriteTo(os.Stdout) _, _ = output.WriteTo(os.Stdout)
container.GetOutput().PrintBaseErr(err, "start:") if m, ok := container.InternalMessageFromError(err); ok {
t.Fatal(m)
} else {
t.Fatalf("cannot start container: %v", err) t.Fatalf("cannot start container: %v", err)
}
} else if err = c.Serve(); err != nil { } else if err = c.Serve(); err != nil {
_, _ = output.WriteTo(os.Stdout) _, _ = output.WriteTo(os.Stdout)
container.GetOutput().PrintBaseErr(err, "serve:") if m, ok := container.InternalMessageFromError(err); ok {
t.Error(m)
} else {
t.Errorf("cannot serve setup params: %v", err) t.Errorf("cannot serve setup params: %v", err)
} }
}
if err := c.Wait(); err != nil { if err := c.Wait(); err != nil {
_, _ = output.WriteTo(os.Stdout) _, _ = output.WriteTo(os.Stdout)
container.GetOutput().PrintBaseErr(err, "wait:") if m, ok := container.InternalMessageFromError(err); ok {
t.Fatal(m)
} else {
t.Fatalf("wait: %v", err) t.Fatalf("wait: %v", err)
} }
}
}) })
} }
} }
@ -376,12 +528,18 @@ func testContainerCancel(
} }
if err := c.Start(); err != nil { if err := c.Start(); err != nil {
container.GetOutput().PrintBaseErr(err, "start:") if m, ok := container.InternalMessageFromError(err); ok {
t.Fatal(m)
} else {
t.Fatalf("cannot start container: %v", err) t.Fatalf("cannot start container: %v", err)
}
} else if err = c.Serve(); err != nil { } else if err = c.Serve(); err != nil {
container.GetOutput().PrintBaseErr(err, "serve:") if m, ok := container.InternalMessageFromError(err); ok {
t.Error(m)
} else {
t.Errorf("cannot serve setup params: %v", err) t.Errorf("cannot serve setup params: %v", err)
} }
}
<-ready <-ready
cancel() cancel()
waitCheck(t, c) waitCheck(t, c)

View File

@ -138,8 +138,6 @@ type syscallDispatcher interface {
resume() bool resume() bool
// beforeExit provides [Msg.BeforeExit]. // beforeExit provides [Msg.BeforeExit].
beforeExit() beforeExit()
// printBaseErr provides [Msg.PrintBaseErr].
printBaseErr(err error, fallback string)
} }
// direct implements syscallDispatcher on the current kernel. // direct implements syscallDispatcher on the current kernel.
@ -225,7 +223,7 @@ func (direct) pivotRoot(newroot, putold string) (err error) {
return syscall.PivotRoot(newroot, putold) return syscall.PivotRoot(newroot, putold)
} }
func (direct) mount(source, target, fstype string, flags uintptr, data string) (err error) { 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) { func (direct) unmount(target string, flags int) (err error) {
return syscall.Unmount(target, flags) return syscall.Unmount(target, flags)
@ -242,4 +240,3 @@ func (direct) verbosef(format string, v ...any) { msg.Verbosef(format, v.
func (direct) suspend() { msg.Suspend() } func (direct) suspend() { msg.Suspend() }
func (direct) resume() bool { return msg.Resume() } func (direct) resume() bool { return msg.Resume() }
func (direct) beforeExit() { msg.BeforeExit() } func (direct) beforeExit() { msg.BeforeExit() }
func (direct) printBaseErr(err error, fallback string) { msg.PrintBaseErr(err, fallback) }

View File

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

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 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. // initParams are params passed from parent.
type initParams struct { type initParams struct {
Params Params
@ -106,7 +116,7 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
if errors.Is(err, EBADF) { if errors.Is(err, EBADF) {
k.fatal("invalid setup descriptor") k.fatal("invalid setup descriptor")
} }
if errors.Is(err, ErrNotSet) { if errors.Is(err, ErrReceiveEnv) {
k.fatal("HAKUREI_SETUP not set") 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 { if err := op.early(state, k); err != nil {
k.printBaseErr(err, if m, ok := messageFromError(err); ok {
fmt.Sprintf("cannot prepare op at index %d:", i)) k.fatal(m)
k.beforeExit() } else {
k.exit(1) 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 // ops already checked during early setup
k.verbosef("%s %s", op.prefix(), op) k.verbosef("%s %s", op.prefix(), op)
if err := op.apply(state, k); err != nil { if err := op.apply(state, k); err != nil {
k.printBaseErr(err, if m, ok := messageFromError(err); ok {
fmt.Sprintf("cannot apply op at index %d:", i)) k.fatal(m)
k.beforeExit() } else {
k.exit(1) 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 { func (b *BindMountOp) early(_ *setupState, k syscallDispatcher) error {
if b.Flags&BindEnsure != 0 { if b.Flags&BindEnsure != 0 {
if err := k.mkdirAll(b.Source.String(), 0700); err != nil { 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 // leave sourceFinal as nil
return nil return nil
} }
return wrapErrSelf(err) return err
} else { } else {
b.sourceFinal, err = NewAbs(pathname) b.sourceFinal, err = NewAbs(pathname)
return err return err
@ -63,7 +63,7 @@ func (b *BindMountOp) apply(_ *setupState, k syscallDispatcher) error {
if b.sourceFinal == nil { if b.sourceFinal == nil {
if b.Flags&BindOptional == 0 { if b.Flags&BindOptional == 0 {
// unreachable // unreachable
return msg.WrapErr(os.ErrClosed, "impossible bind state reached") return OpStateError("bind")
} }
return nil 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 // 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 // op->perms which is never set for any bind setup op so always results in 0700
if fi, err := k.stat(source); err != nil { if fi, err := k.stat(source); err != nil {
return wrapErrSelf(err) return err
} else if fi.IsDir() { } else if fi.IsDir() {
if err = k.mkdirAll(target, 0700); err != nil { if err = k.mkdirAll(target, 0700); err != nil {
return wrapErrSelf(err) return err
} }
} else if err = k.ensureFile(target, 0444, 0700); err != nil { } else if err = k.ensureFile(target, 0444, 0700); err != nil {
return err return err

View File

@ -5,6 +5,8 @@ import (
"os" "os"
"syscall" "syscall"
"testing" "testing"
"hakurei.app/container/stub"
) )
func TestBindMountOp(t *testing.T) { func TestBindMountOp(t *testing.T) {
@ -12,138 +14,138 @@ func TestBindMountOp(t *testing.T) {
{"ENOENT not optional", new(Params), &BindMountOp{ {"ENOENT not optional", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: MustAbs("/bin/"),
}, []kexpect{ }, []stub.Call{
{"evalSymlinks", expectArgs{"/bin/"}, "", syscall.ENOENT}, call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "", syscall.ENOENT),
}, wrapErrSelf(syscall.ENOENT), nil, nil}, }, syscall.ENOENT, nil, nil},
{"skip optional", new(Params), &BindMountOp{ {"skip optional", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: MustAbs("/bin/"),
Flags: BindOptional, Flags: BindOptional,
}, []kexpect{ }, []stub.Call{
{"evalSymlinks", expectArgs{"/bin/"}, "", syscall.ENOENT}, call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "", syscall.ENOENT),
}, nil, nil, nil}, }, nil, nil, nil},
{"success optional", new(Params), &BindMountOp{ {"success optional", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: MustAbs("/bin/"),
Flags: BindOptional, Flags: BindOptional,
}, []kexpect{ }, []stub.Call{
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil}, call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []kexpect{ }, nil, []stub.Call{
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil}, call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
{"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil}, call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil),
{"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil}, call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
}, nil}, }, nil},
{"ensureFile device", new(Params), &BindMountOp{ {"ensureFile device", new(Params), &BindMountOp{
Source: MustAbs("/dev/null"), Source: MustAbs("/dev/null"),
Target: MustAbs("/dev/null"), Target: MustAbs("/dev/null"),
Flags: BindWritable | BindDevice, Flags: BindWritable | BindDevice,
}, []kexpect{ }, []stub.Call{
{"evalSymlinks", expectArgs{"/dev/null"}, "/dev/null", nil}, call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
}, nil, []kexpect{ }, nil, []stub.Call{
{"stat", expectArgs{"/host/dev/null"}, isDirFi(false), nil}, call("stat", stub.ExpectArgs{"/host/dev/null"}, isDirFi(false), nil),
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, errUnique}, call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, stub.UniqueError(5)),
}, errUnique}, }, stub.UniqueError(5)},
{"mkdirAll ensure", new(Params), &BindMountOp{ {"mkdirAll ensure", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: MustAbs("/bin/"),
Flags: BindEnsure, Flags: BindEnsure,
}, []kexpect{ }, []stub.Call{
{"mkdirAll", expectArgs{"/bin/", os.FileMode(0700)}, nil, errUnique}, call("mkdirAll", stub.ExpectArgs{"/bin/", os.FileMode(0700)}, nil, stub.UniqueError(4)),
}, wrapErrSelf(errUnique), nil, nil}, }, stub.UniqueError(4), nil, nil},
{"success ensure", new(Params), &BindMountOp{ {"success ensure", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: MustAbs("/usr/bin/"), Target: MustAbs("/usr/bin/"),
Flags: BindEnsure, Flags: BindEnsure,
}, []kexpect{ }, []stub.Call{
{"mkdirAll", expectArgs{"/bin/", os.FileMode(0700)}, nil, nil}, call("mkdirAll", stub.ExpectArgs{"/bin/", os.FileMode(0700)}, nil, nil),
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil}, call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []kexpect{ }, nil, []stub.Call{
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil}, call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
{"mkdirAll", expectArgs{"/sysroot/usr/bin", os.FileMode(0700)}, nil, nil}, call("mkdirAll", stub.ExpectArgs{"/sysroot/usr/bin", os.FileMode(0700)}, nil, nil),
{"bindMount", expectArgs{"/host/usr/bin", "/sysroot/usr/bin", uintptr(0x4005), false}, nil, nil}, call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/usr/bin", uintptr(0x4005), false}, nil, nil),
}, nil}, }, nil},
{"success device ro", new(Params), &BindMountOp{ {"success device ro", new(Params), &BindMountOp{
Source: MustAbs("/dev/null"), Source: MustAbs("/dev/null"),
Target: MustAbs("/dev/null"), Target: MustAbs("/dev/null"),
Flags: BindDevice, Flags: BindDevice,
}, []kexpect{ }, []stub.Call{
{"evalSymlinks", expectArgs{"/dev/null"}, "/dev/null", nil}, call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
}, nil, []kexpect{ }, nil, []stub.Call{
{"stat", expectArgs{"/host/dev/null"}, isDirFi(false), nil}, call("stat", stub.ExpectArgs{"/host/dev/null"}, isDirFi(false), nil),
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, nil}, call("ensureFile", stub.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}, call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4001), false}, nil, nil),
}, nil}, }, nil},
{"success device", new(Params), &BindMountOp{ {"success device", new(Params), &BindMountOp{
Source: MustAbs("/dev/null"), Source: MustAbs("/dev/null"),
Target: MustAbs("/dev/null"), Target: MustAbs("/dev/null"),
Flags: BindWritable | BindDevice, Flags: BindWritable | BindDevice,
}, []kexpect{ }, []stub.Call{
{"evalSymlinks", expectArgs{"/dev/null"}, "/dev/null", nil}, call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
}, nil, []kexpect{ }, nil, []stub.Call{
{"stat", expectArgs{"/host/dev/null"}, isDirFi(false), nil}, call("stat", stub.ExpectArgs{"/host/dev/null"}, isDirFi(false), nil),
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, nil}, call("ensureFile", stub.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}, call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4000), false}, nil, nil),
}, nil}, }, nil},
{"evalSymlinks", new(Params), &BindMountOp{ {"evalSymlinks", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: MustAbs("/bin/"),
}, []kexpect{ }, []stub.Call{
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", errUnique}, call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", stub.UniqueError(3)),
}, wrapErrSelf(errUnique), nil, nil}, }, stub.UniqueError(3), nil, nil},
{"stat", new(Params), &BindMountOp{ {"stat", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: MustAbs("/bin/"),
}, []kexpect{ }, []stub.Call{
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil}, call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []kexpect{ }, nil, []stub.Call{
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), errUnique}, call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), stub.UniqueError(2)),
}, wrapErrSelf(errUnique)}, }, stub.UniqueError(2)},
{"mkdirAll", new(Params), &BindMountOp{ {"mkdirAll", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: MustAbs("/bin/"),
}, []kexpect{ }, []stub.Call{
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil}, call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []kexpect{ }, nil, []stub.Call{
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil}, call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
{"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, errUnique}, call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, stub.UniqueError(1)),
}, wrapErrSelf(errUnique)}, }, stub.UniqueError(1)},
{"bindMount", new(Params), &BindMountOp{ {"bindMount", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: MustAbs("/bin/"),
}, []kexpect{ }, []stub.Call{
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil}, call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []kexpect{ }, nil, []stub.Call{
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil}, call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
{"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil}, call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil),
{"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, errUnique}, call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, stub.UniqueError(0)),
}, errUnique}, }, stub.UniqueError(0)},
{"success", new(Params), &BindMountOp{ {"success", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: MustAbs("/bin/"),
}, []kexpect{ }, []stub.Call{
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil}, call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []kexpect{ }, nil, []stub.Call{
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil}, call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
{"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil}, call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil),
{"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil}, call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
}, nil}, }, nil},
}) })
t.Run("unreachable", func(t *testing.T) { t.Run("unreachable", func(t *testing.T) {
t.Run("nil sourceFinal not optional", 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) { if err := new(BindMountOp).apply(nil, nil); !errors.Is(err, wantErr) {
t.Errorf("apply: error = %v, want %v", 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')), FHSProc+"self/fd/"+string(rune(i+'0')),
path.Join(target, name), path.Join(target, name),
); err != nil { ); err != nil {
return wrapErrSelf(err) return err
} }
} }
for _, pair := range [][2]string{ for _, pair := range [][2]string{
@ -68,7 +68,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
{"pts/ptmx", "ptmx"}, {"pts/ptmx", "ptmx"},
} { } {
if err := k.symlink(pair[0], path.Join(target, pair[1])); err != nil { 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") devPtsPath := path.Join(target, "pts")
for _, name := range []string{devShmPath, devPtsPath} { for _, name := range []string{devShmPath, devPtsPath} {
if err := k.mkdir(name, state.ParentPerm); err != nil { 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, if err := k.mount(SourceDevpts, devPtsPath, FstypeDevpts, MS_NOSUID|MS_NOEXEC,
"newinstance,ptmxmode=0666,mode=620"); err != nil { "newinstance,ptmxmode=0666,mode=620"); err != nil {
return wrapErrSuffix(err, return err
fmt.Sprintf("cannot mount devpts on %q:", devPtsPath))
} }
if state.RetainSession { if state.RetainSession {
@ -93,7 +92,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
return err return err
} }
if name, err := k.readlink(hostProc.stdout()); err != nil { if name, err := k.readlink(hostProc.stdout()); err != nil {
return wrapErrSelf(err) return err
} else if err = k.bindMount( } else if err = k.bindMount(
toHost(name), toHost(name),
consolePath, consolePath,
@ -108,10 +107,10 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
if d.Mqueue { if d.Mqueue {
mqueueTarget := path.Join(target, "mqueue") mqueueTarget := path.Join(target, "mqueue")
if err := k.mkdir(mqueueTarget, state.ParentPerm); err != nil { 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 { 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 { if err := k.remount(target, MS_RDONLY); err != nil {
return wrapErrSuffix(k.remount(target, MS_RDONLY), return err
fmt.Sprintf("cannot remount %q:", target))
} }
return k.mountTmpfs(SourceTmpfs, devShmPath, MS_NOSUID|MS_NODEV, 0, 01777) 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) Valid() bool { return m != nil && m.Path != nil }
func (m *MkdirOp) early(*setupState, syscallDispatcher) error { return nil } func (m *MkdirOp) early(*setupState, syscallDispatcher) error { return nil }
func (m *MkdirOp) apply(_ *setupState, k syscallDispatcher) error { func (m *MkdirOp) apply(_ *setupState, k syscallDispatcher) error {
return 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 { func (m *MkdirOp) Is(op Op) bool {

View File

@ -3,6 +3,8 @@ package container
import ( import (
"os" "os"
"testing" "testing"
"hakurei.app/container/stub"
) )
func TestMkdirOp(t *testing.T) { func TestMkdirOp(t *testing.T) {
@ -10,8 +12,8 @@ func TestMkdirOp(t *testing.T) {
{"success", new(Params), &MkdirOp{ {"success", new(Params), &MkdirOp{
Path: MustAbs("/.hakurei"), Path: MustAbs("/.hakurei"),
Perm: 0500, Perm: 0500,
}, nil, nil, []kexpect{ }, nil, nil, []stub.Call{
{"mkdirAll", expectArgs{"/sysroot/.hakurei", os.FileMode(0500)}, nil, nil}, call("mkdirAll", stub.ExpectArgs{"/sysroot/.hakurei", os.FileMode(0500)}, nil, nil),
}, nil}, }, nil},
}) })

View File

@ -3,7 +3,6 @@ package container
import ( import (
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"io/fs"
"slices" "slices"
"strings" "strings"
) )
@ -19,6 +18,39 @@ const (
func init() { gob.Register(new(MountOverlayOp)) } 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]. // Overlay appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target].
func (f *Ops) Overlay(target, state, work *Absolute, layers ...*Absolute) *Ops { func (f *Ops) Overlay(target, state, work *Absolute, layers ...*Absolute) *Ops {
*f = append(*f, &MountOverlayOp{ *f = append(*f, &MountOverlayOp{
@ -89,7 +121,7 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
o.ephemeral = true // intermediate root not yet available o.ephemeral = true // intermediate root not yet available
default: 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 // readonly handled in apply
@ -97,12 +129,12 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
if !o.ephemeral { if !o.ephemeral {
if o.Upper != o.Work && (o.Upper == nil || o.Work == nil) { if o.Upper != o.Work && (o.Upper == nil || o.Work == nil) {
// unreachable // unreachable
return msg.WrapErr(fs.ErrClosed, "impossible overlay state reached") return OpStateError("overlay")
} }
if o.Upper != nil { if o.Upper != nil {
if v, err := k.evalSymlinks(o.Upper.String()); err != nil { if v, err := k.evalSymlinks(o.Upper.String()); err != nil {
return wrapErrSelf(err) return err
} else { } else {
o.upper = EscapeOverlayDataSegment(toHost(v)) o.upper = EscapeOverlayDataSegment(toHost(v))
} }
@ -110,7 +142,7 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
if o.Work != nil { if o.Work != nil {
if v, err := k.evalSymlinks(o.Work.String()); err != nil { if v, err := k.evalSymlinks(o.Work.String()); err != nil {
return wrapErrSelf(err) return err
} else { } else {
o.work = EscapeOverlayDataSegment(toHost(v)) o.work = EscapeOverlayDataSegment(toHost(v))
} }
@ -120,7 +152,7 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
o.lower = make([]string, len(o.Lower)) o.lower = make([]string, len(o.Lower))
for i, a := range o.Lower { // nil checked in Valid for i, a := range o.Lower { // nil checked in Valid
if v, err := k.evalSymlinks(a.String()); err != nil { if v, err := k.evalSymlinks(a.String()); err != nil {
return wrapErrSelf(err) return err
} else { } else {
o.lower[i] = EscapeOverlayDataSegment(toHost(v)) o.lower[i] = EscapeOverlayDataSegment(toHost(v))
} }
@ -134,17 +166,17 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
target = toSysroot(target) target = toSysroot(target)
} }
if err := k.mkdirAll(target, state.ParentPerm); err != nil { if err := k.mkdirAll(target, state.ParentPerm); err != nil {
return wrapErrSelf(err) return err
} }
if o.ephemeral { if o.ephemeral {
var err error var err error
// these directories are created internally, therefore early (absolute, symlink, prefix, escape) is bypassed // these directories are created internally, therefore early (absolute, symlink, prefix, escape) is bypassed
if o.upper, err = k.mkdirTemp(FHSRoot, intermediatePatternOverlayUpper); err != nil { 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 { 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 o.upper == zeroString && o.work == zeroString { // readonly
if len(o.Lower) < 2 { 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 // "upperdir=" and "workdir=" may be omitted. In that case the overlay will be read-only
} else { } else {
if len(o.Lower) == 0 { if len(o.Lower) == 0 {
return msg.WrapErr(fs.ErrInvalid, "overlay requires at least one lowerdir") return &OverlayArgumentError{OverlayEmptyLower, zeroString}
} }
options = append(options, options = append(options,
OptionOverlayUpperdir+"="+o.upper, OptionOverlayUpperdir+"="+o.upper,
@ -167,8 +199,7 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
OptionOverlayLowerdir+"="+strings.Join(o.lower, SpecialOverlayPath), OptionOverlayLowerdir+"="+strings.Join(o.lower, SpecialOverlayPath),
OptionOverlayUserxattr) OptionOverlayUserxattr)
return wrapErrSuffix(k.mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, SpecialOverlayOption)), return k.mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, SpecialOverlayOption))
fmt.Sprintf("cannot mount overlay on %q:", o.Target))
} }
func (o *MountOverlayOp) Is(op Op) bool { func (o *MountOverlayOp) Is(op Op) bool {

View File

@ -2,12 +2,40 @@ package container
import ( import (
"errors" "errors"
"io/fs"
"os" "os"
"testing" "testing"
"hakurei.app/container/stub"
) )
func TestMountOverlayOp(t *testing.T) { 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{ checkOpBehaviour(t, []opBehaviourTestCase{
{"mkdirTemp invalid ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{ {"mkdirTemp invalid ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
Target: MustAbs("/"), Target: MustAbs("/"),
@ -16,7 +44,7 @@ func TestMountOverlayOp(t *testing.T) {
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"), MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
}, },
Upper: MustAbs("/proc/"), 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{ {"mkdirTemp upper ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
Target: MustAbs("/"), Target: MustAbs("/"),
@ -25,13 +53,13 @@ func TestMountOverlayOp(t *testing.T) {
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"), MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
}, },
Upper: MustAbs("/"), Upper: MustAbs("/"),
}, []kexpect{ }, []stub.Call{
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil),
}, nil, []kexpect{ }, nil, []stub.Call{
{"mkdirAll", expectArgs{"/sysroot", os.FileMode(0705)}, nil, nil}, call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0705)}, nil, nil),
{"mkdirTemp", expectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", errUnique}, call("mkdirTemp", stub.ExpectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", stub.UniqueError(6)),
}, wrapErrSelf(errUnique)}, }, stub.UniqueError(6)},
{"mkdirTemp work ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{ {"mkdirTemp work ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
Target: MustAbs("/"), Target: MustAbs("/"),
@ -40,14 +68,14 @@ func TestMountOverlayOp(t *testing.T) {
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"), MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
}, },
Upper: MustAbs("/"), Upper: MustAbs("/"),
}, []kexpect{ }, []stub.Call{
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil),
}, nil, []kexpect{ }, nil, []stub.Call{
{"mkdirAll", expectArgs{"/sysroot", os.FileMode(0705)}, nil, nil}, call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0705)}, nil, nil),
{"mkdirTemp", expectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil}, call("mkdirTemp", stub.ExpectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil),
{"mkdirTemp", expectArgs{"/", "overlay.work.*"}, "overlay.work.32768", errUnique}, call("mkdirTemp", stub.ExpectArgs{"/", "overlay.work.*"}, "overlay.work.32768", stub.UniqueError(5)),
}, wrapErrSelf(errUnique)}, }, stub.UniqueError(5)},
{"success ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{ {"success ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
Target: MustAbs("/"), Target: MustAbs("/"),
@ -56,20 +84,20 @@ func TestMountOverlayOp(t *testing.T) {
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"), MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
}, },
Upper: MustAbs("/"), Upper: MustAbs("/"),
}, []kexpect{ }, []stub.Call{
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil),
}, nil, []kexpect{ }, nil, []stub.Call{
{"mkdirAll", expectArgs{"/sysroot", os.FileMode(0705)}, nil, nil}, call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0705)}, nil, nil),
{"mkdirTemp", expectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil}, call("mkdirTemp", stub.ExpectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil),
{"mkdirTemp", expectArgs{"/", "overlay.work.*"}, "overlay.work.32768", nil}, call("mkdirTemp", stub.ExpectArgs{"/", "overlay.work.*"}, "overlay.work.32768", nil),
{"mount", expectArgs{"overlay", "/sysroot", "overlay", uintptr(0), "" + call("mount", stub.ExpectArgs{"overlay", "/sysroot", "overlay", uintptr(0), "" +
"upperdir=overlay.upper.32768," + "upperdir=overlay.upper.32768," +
"workdir=overlay.work.32768," + "workdir=overlay.work.32768," +
"lowerdir=" + "lowerdir=" +
`/host/var/lib/planterette/base/debian\:f92c9052:` + `/host/var/lib/planterette/base/debian\:f92c9052:` +
`/host/var/lib/planterette/app/org.chromium.Chromium@debian\:f92c9052,` + `/host/var/lib/planterette/app/org.chromium.Chromium@debian\:f92c9052,` +
"userxattr"}, nil, nil}, "userxattr"}, nil, nil),
}, nil}, }, nil},
{"short lower ro", &Params{ParentPerm: 0755}, &MountOverlayOp{ {"short lower ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
@ -77,11 +105,11 @@ func TestMountOverlayOp(t *testing.T) {
Lower: []*Absolute{ Lower: []*Absolute{
MustAbs("/mnt-root/nix/.ro-store"), MustAbs("/mnt-root/nix/.ro-store"),
}, },
}, []kexpect{ }, []stub.Call{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil}, call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil),
}, nil, []kexpect{ }, nil, []stub.Call{
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil}, call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil),
}, msg.WrapErr(fs.ErrInvalid, "readonly overlay requires at least two lowerdir")}, }, &OverlayArgumentError{OverlayReadonlyLower, zeroString}},
{"success ro noPrefix", &Params{ParentPerm: 0755}, &MountOverlayOp{ {"success ro noPrefix", &Params{ParentPerm: 0755}, &MountOverlayOp{
Target: MustAbs("/nix/store"), Target: MustAbs("/nix/store"),
@ -90,16 +118,16 @@ func TestMountOverlayOp(t *testing.T) {
MustAbs("/mnt-root/nix/.ro-store0"), MustAbs("/mnt-root/nix/.ro-store0"),
}, },
noPrefix: true, noPrefix: true,
}, []kexpect{ }, []stub.Call{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil),
}, nil, []kexpect{ }, nil, []stub.Call{
{"mkdirAll", expectArgs{"/nix/store", os.FileMode(0755)}, nil, nil}, call("mkdirAll", stub.ExpectArgs{"/nix/store", os.FileMode(0755)}, nil, nil),
{"mount", expectArgs{"overlay", "/nix/store", "overlay", uintptr(0), "" + call("mount", stub.ExpectArgs{"overlay", "/nix/store", "overlay", uintptr(0), "" +
"lowerdir=" + "lowerdir=" +
"/host/mnt-root/nix/.ro-store:" + "/host/mnt-root/nix/.ro-store:" +
"/host/mnt-root/nix/.ro-store0," + "/host/mnt-root/nix/.ro-store0," +
"userxattr"}, nil, nil}, "userxattr"}, nil, nil),
}, nil}, }, nil},
{"success ro", &Params{ParentPerm: 0755}, &MountOverlayOp{ {"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-store"),
MustAbs("/mnt-root/nix/.ro-store0"), MustAbs("/mnt-root/nix/.ro-store0"),
}, },
}, []kexpect{ }, []stub.Call{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil),
}, nil, []kexpect{ }, nil, []stub.Call{
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil}, call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil),
{"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" + call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
"lowerdir=" + "lowerdir=" +
"/host/mnt-root/nix/.ro-store:" + "/host/mnt-root/nix/.ro-store:" +
"/host/mnt-root/nix/.ro-store0," + "/host/mnt-root/nix/.ro-store0," +
"userxattr"}, nil, nil}, "userxattr"}, nil, nil),
}, nil}, }, nil},
{"nil lower", &Params{ParentPerm: 0700}, &MountOverlayOp{ {"nil lower", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"), Target: MustAbs("/nix/store"),
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"), Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"), Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []kexpect{ }, []stub.Call{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
}, nil, []kexpect{ }, nil, []stub.Call{
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil}, call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
}, msg.WrapErr(fs.ErrInvalid, "overlay requires at least one lowerdir")}, }, &OverlayArgumentError{OverlayEmptyLower, zeroString}},
{"evalSymlinks upper", &Params{ParentPerm: 0700}, &MountOverlayOp{ {"evalSymlinks upper", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"), Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")}, Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"), Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"), Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []kexpect{ }, []stub.Call{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", errUnique}, call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", stub.UniqueError(4)),
}, wrapErrSelf(errUnique), nil, nil}, }, stub.UniqueError(4), nil, nil},
{"evalSymlinks work", &Params{ParentPerm: 0700}, &MountOverlayOp{ {"evalSymlinks work", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"), Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")}, Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"), Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"), Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []kexpect{ }, []stub.Call{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", stub.UniqueError(3)),
}, wrapErrSelf(errUnique), nil, nil}, }, stub.UniqueError(3), nil, nil},
{"evalSymlinks lower", &Params{ParentPerm: 0700}, &MountOverlayOp{ {"evalSymlinks lower", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"), Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")}, Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"), Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"), Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []kexpect{ }, []stub.Call{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", stub.UniqueError(2)),
}, wrapErrSelf(errUnique), nil, nil}, }, stub.UniqueError(2), nil, nil},
{"mkdirAll", &Params{ParentPerm: 0700}, &MountOverlayOp{ {"mkdirAll", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"), Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")}, Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"), Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"), Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []kexpect{ }, []stub.Call{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil),
}, nil, []kexpect{ }, nil, []stub.Call{
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, errUnique}, call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, stub.UniqueError(1)),
}, wrapErrSelf(errUnique)}, }, stub.UniqueError(1)},
{"mount", &Params{ParentPerm: 0700}, &MountOverlayOp{ {"mount", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"), Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")}, Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"), Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"), Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []kexpect{ }, []stub.Call{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil),
}, nil, []kexpect{ }, nil, []stub.Call{
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil}, call("mkdirAll", stub.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}, 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)),
}, wrapErrSuffix(errUnique, `cannot mount overlay on "/nix/store":`)}, }, stub.UniqueError(0)},
{"success single layer", &Params{ParentPerm: 0700}, &MountOverlayOp{ {"success single layer", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"), Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")}, Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"), Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"), Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []kexpect{ }, []stub.Call{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil),
}, nil, []kexpect{ }, nil, []stub.Call{
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil}, call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
{"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" + call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
"upperdir=/host/mnt-root/nix/.rw-store/.upper," + "upperdir=/host/mnt-root/nix/.rw-store/.upper," +
"workdir=/host/mnt-root/nix/.rw-store/.work," + "workdir=/host/mnt-root/nix/.rw-store/.work," +
"lowerdir=/host/mnt-root/nix/ro-store," + "lowerdir=/host/mnt-root/nix/ro-store," +
"userxattr"}, nil, nil}, "userxattr"}, nil, nil),
}, nil}, }, nil},
{"success", &Params{ParentPerm: 0700}, &MountOverlayOp{ {"success", &Params{ParentPerm: 0700}, &MountOverlayOp{
@ -217,17 +245,17 @@ func TestMountOverlayOp(t *testing.T) {
}, },
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"), Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"), Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []kexpect{ }, []stub.Call{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.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}, call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store3"}, "/mnt-root/nix/ro-store3", nil),
}, nil, []kexpect{ }, nil, []stub.Call{
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil}, call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
{"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" + call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
"upperdir=/host/mnt-root/nix/.rw-store/.upper," + "upperdir=/host/mnt-root/nix/.rw-store/.upper," +
"workdir=/host/mnt-root/nix/.rw-store/.work," + "workdir=/host/mnt-root/nix/.rw-store/.work," +
"lowerdir=" + "lowerdir=" +
@ -236,13 +264,13 @@ func TestMountOverlayOp(t *testing.T) {
"/host/mnt-root/nix/ro-store1:" + "/host/mnt-root/nix/ro-store1:" +
"/host/mnt-root/nix/ro-store2:" + "/host/mnt-root/nix/ro-store2:" +
"/host/mnt-root/nix/ro-store3," + "/host/mnt-root/nix/ro-store3," +
"userxattr"}, nil, nil}, "userxattr"}, nil, nil),
}, nil}, }, nil},
}) })
t.Run("unreachable", func(t *testing.T) { t.Run("unreachable", func(t *testing.T) {
t.Run("nil Upper non-nil Work not ephemeral", 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{ if err := (&MountOverlayOp{
Work: MustAbs("/"), Work: MustAbs("/"),
}).early(nil, nil); !errors.Is(err, wantErr) { }).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 { func (t *TmpfileOp) apply(state *setupState, k syscallDispatcher) error {
var tmpPath string var tmpPath string
if f, err := k.createTemp(FHSRoot, intermediatePatternTmpfile); err != nil { if f, err := k.createTemp(FHSRoot, intermediatePatternTmpfile); err != nil {
return wrapErrSelf(err) return err
} else if _, err = f.Write(t.Data); err != nil { } else if _, err = f.Write(t.Data); err != nil {
return wrapErrSuffix(err, return err
"cannot write to intermediate file:")
} else if err = f.Close(); err != nil { } else if err = f.Close(); err != nil {
return wrapErrSuffix(err, return err
"cannot close intermediate file:")
} else { } else {
tmpPath = f.Name() tmpPath = f.Name()
} }
@ -61,7 +59,7 @@ func (t *TmpfileOp) apply(state *setupState, k syscallDispatcher) error {
); err != nil { ); err != nil {
return err return err
} else if err = k.remove(tmpPath); err != nil { } else if err = k.remove(tmpPath); err != nil {
return wrapErrSelf(err) return err
} }
return nil return nil
} }

View File

@ -3,6 +3,8 @@ package container
import ( import (
"os" "os"
"testing" "testing"
"hakurei.app/container/stub"
) )
func TestTmpfileOp(t *testing.T) { func TestTmpfileOp(t *testing.T) {
@ -16,59 +18,59 @@ func TestTmpfileOp(t *testing.T) {
{"createTemp", &Params{ParentPerm: 0700}, &TmpfileOp{ {"createTemp", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath, Path: samplePath,
Data: sampleData, Data: sampleData,
}, nil, nil, []kexpect{ }, nil, nil, []stub.Call{
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), errUnique}, call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), stub.UniqueError(5)),
}, wrapErrSelf(errUnique)}, }, stub.UniqueError(5)},
{"Write", &Params{ParentPerm: 0700}, &TmpfileOp{ {"Write", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath, Path: samplePath,
Data: sampleData, Data: sampleData,
}, nil, nil, []kexpect{ }, nil, nil, []stub.Call{
{"createTemp", expectArgs{"/", "tmp.*"}, writeErrOsFile{errUnique}, nil}, call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, writeErrOsFile{stub.UniqueError(4)}, nil),
}, wrapErrSuffix(errUnique, "cannot write to intermediate file:")}, }, stub.UniqueError(4)},
{"Close", &Params{ParentPerm: 0700}, &TmpfileOp{ {"Close", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath, Path: samplePath,
Data: sampleData, Data: sampleData,
}, nil, nil, []kexpect{ }, nil, nil, []stub.Call{
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, errUnique), nil}, call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, stub.UniqueError(3)), nil),
}, wrapErrSuffix(errUnique, "cannot close intermediate file:")}, }, stub.UniqueError(3)},
{"ensureFile", &Params{ParentPerm: 0700}, &TmpfileOp{ {"ensureFile", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath, Path: samplePath,
Data: sampleData, Data: sampleData,
}, nil, nil, []kexpect{ }, nil, nil, []stub.Call{
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil}, call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil),
{"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, errUnique}, call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, stub.UniqueError(2)),
}, errUnique}, }, stub.UniqueError(2)},
{"bindMount", &Params{ParentPerm: 0700}, &TmpfileOp{ {"bindMount", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath, Path: samplePath,
Data: sampleData, Data: sampleData,
}, nil, nil, []kexpect{ }, nil, nil, []stub.Call{
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil}, call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil),
{"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil}, call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil),
{"bindMount", expectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, errUnique}, call("bindMount", stub.ExpectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, stub.UniqueError(1)),
}, errUnique}, }, stub.UniqueError(1)},
{"remove", &Params{ParentPerm: 0700}, &TmpfileOp{ {"remove", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath, Path: samplePath,
Data: sampleData, Data: sampleData,
}, nil, nil, []kexpect{ }, nil, nil, []stub.Call{
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil}, call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil),
{"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil}, call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil),
{"bindMount", expectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil}, call("bindMount", stub.ExpectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil),
{"remove", expectArgs{"tmp.32768"}, nil, errUnique}, call("remove", stub.ExpectArgs{"tmp.32768"}, nil, stub.UniqueError(0)),
}, wrapErrSelf(errUnique)}, }, stub.UniqueError(0)},
{"success", &Params{ParentPerm: 0700}, &TmpfileOp{ {"success", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath, Path: samplePath,
Data: sampleData, Data: sampleData,
}, nil, nil, []kexpect{ }, nil, nil, []stub.Call{
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil}, call("createTemp", stub.ExpectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil),
{"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil}, call("ensureFile", stub.ExpectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil),
{"bindMount", expectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil}, call("bindMount", stub.ExpectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil),
{"remove", expectArgs{"tmp.32768"}, nil, nil}, call("remove", stub.ExpectArgs{"tmp.32768"}, nil, 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 { func (p *MountProcOp) apply(state *setupState, k syscallDispatcher) error {
target := toSysroot(p.Target.String()) target := toSysroot(p.Target.String())
if err := k.mkdirAll(target, state.ParentPerm); err != nil { 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), return k.mount(SourceProc, target, FstypeProc, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString)
fmt.Sprintf("cannot mount proc on %q:", p.Target.String()))
} }
func (p *MountProcOp) Is(op Op) bool { func (p *MountProcOp) Is(op Op) bool {

View File

@ -3,6 +3,8 @@ package container
import ( import (
"os" "os"
"testing" "testing"
"hakurei.app/container/stub"
) )
func TestMountProcOp(t *testing.T) { func TestMountProcOp(t *testing.T) {
@ -10,16 +12,16 @@ func TestMountProcOp(t *testing.T) {
{"mkdir", &Params{ParentPerm: 0755}, {"mkdir", &Params{ParentPerm: 0755},
&MountProcOp{ &MountProcOp{
Target: MustAbs("/proc/"), Target: MustAbs("/proc/"),
}, nil, nil, []kexpect{ }, nil, nil, []stub.Call{
{"mkdirAll", expectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, errUnique}, call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, stub.UniqueError(0)),
}, wrapErrSelf(errUnique)}, }, stub.UniqueError(0)},
{"success", &Params{ParentPerm: 0700}, {"success", &Params{ParentPerm: 0700},
&MountProcOp{ &MountProcOp{
Target: MustAbs("/proc/"), Target: MustAbs("/proc/"),
}, nil, nil, []kexpect{ }, nil, nil, []stub.Call{
{"mkdirAll", expectArgs{"/sysroot/proc", os.FileMode(0700)}, nil, nil}, call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0700)}, nil, nil),
{"mount", expectArgs{"proc", "/sysroot/proc", "proc", uintptr(0xe), ""}, nil, nil}, call("mount", stub.ExpectArgs{"proc", "/sysroot/proc", "proc", uintptr(0xe), ""}, nil, nil),
}, nil}, }, nil},
}) })

View File

@ -22,8 +22,7 @@ type RemountOp struct {
func (r *RemountOp) Valid() bool { return r != nil && r.Target != nil } func (r *RemountOp) Valid() bool { return r != nil && r.Target != nil }
func (*RemountOp) early(*setupState, syscallDispatcher) error { return nil } func (*RemountOp) early(*setupState, syscallDispatcher) error { return nil }
func (r *RemountOp) apply(_ *setupState, k syscallDispatcher) error { func (r *RemountOp) apply(_ *setupState, k syscallDispatcher) error {
return wrapErrSuffix(k.remount(toSysroot(r.Target.String()), r.Flags), return k.remount(toSysroot(r.Target.String()), r.Flags)
fmt.Sprintf("cannot remount %q:", r.Target))
} }
func (r *RemountOp) Is(op Op) bool { func (r *RemountOp) Is(op Op) bool {

View File

@ -3,6 +3,8 @@ package container
import ( import (
"syscall" "syscall"
"testing" "testing"
"hakurei.app/container/stub"
) )
func TestRemountOp(t *testing.T) { func TestRemountOp(t *testing.T) {
@ -10,8 +12,8 @@ func TestRemountOp(t *testing.T) {
{"success", new(Params), &RemountOp{ {"success", new(Params), &RemountOp{
Target: MustAbs("/"), Target: MustAbs("/"),
Flags: syscall.MS_RDONLY, Flags: syscall.MS_RDONLY,
}, nil, nil, []kexpect{ }, nil, nil, []stub.Call{
{"remount", expectArgs{"/sysroot", uintptr(1)}, nil, nil}, call("remount", stub.ExpectArgs{"/sysroot", uintptr(1)}, nil, nil),
}, nil}, }, nil},
}) })

View File

@ -3,7 +3,6 @@ package container
import ( import (
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"io/fs"
"path" "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 { func (l *SymlinkOp) early(_ *setupState, k syscallDispatcher) error {
if l.Dereference { if l.Dereference {
if !isAbs(l.LinkName) { 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 { if name, err := k.readlink(l.LinkName); err != nil {
return wrapErrSelf(err) return err
} else { } else {
l.LinkName = name l.LinkName = name
} }
@ -44,9 +43,9 @@ func (l *SymlinkOp) early(_ *setupState, k syscallDispatcher) error {
func (l *SymlinkOp) apply(state *setupState, k syscallDispatcher) error { func (l *SymlinkOp) apply(state *setupState, k syscallDispatcher) error {
target := toSysroot(l.Target.String()) target := toSysroot(l.Target.String())
if err := k.mkdirAll(path.Dir(target), state.ParentPerm); err != nil { 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 { func (l *SymlinkOp) Is(op Op) bool {

View File

@ -1,9 +1,10 @@
package container package container
import ( import (
"io/fs"
"os" "os"
"testing" "testing"
"hakurei.app/container/stub"
) )
func TestSymlinkOp(t *testing.T) { func TestSymlinkOp(t *testing.T) {
@ -11,41 +12,41 @@ func TestSymlinkOp(t *testing.T) {
{"mkdir", &Params{ParentPerm: 0700}, &SymlinkOp{ {"mkdir", &Params{ParentPerm: 0700}, &SymlinkOp{
Target: MustAbs("/etc/nixos"), Target: MustAbs("/etc/nixos"),
LinkName: "/etc/static/nixos", LinkName: "/etc/static/nixos",
}, nil, nil, []kexpect{ }, nil, nil, []stub.Call{
{"mkdirAll", expectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, errUnique}, call("mkdirAll", stub.ExpectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, stub.UniqueError(1)),
}, wrapErrSelf(errUnique)}, }, stub.UniqueError(1)},
{"abs", &Params{ParentPerm: 0755}, &SymlinkOp{ {"abs", &Params{ParentPerm: 0755}, &SymlinkOp{
Target: MustAbs("/etc/mtab"), Target: MustAbs("/etc/mtab"),
LinkName: "etc/mtab", LinkName: "etc/mtab",
Dereference: true, 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{ {"readlink", &Params{ParentPerm: 0755}, &SymlinkOp{
Target: MustAbs("/etc/mtab"), Target: MustAbs("/etc/mtab"),
LinkName: "/etc/mtab", LinkName: "/etc/mtab",
Dereference: true, Dereference: true,
}, []kexpect{ }, []stub.Call{
{"readlink", expectArgs{"/etc/mtab"}, "/proc/mounts", errUnique}, call("readlink", stub.ExpectArgs{"/etc/mtab"}, "/proc/mounts", stub.UniqueError(0)),
}, wrapErrSelf(errUnique), nil, nil}, }, stub.UniqueError(0), nil, nil},
{"success noderef", &Params{ParentPerm: 0700}, &SymlinkOp{ {"success noderef", &Params{ParentPerm: 0700}, &SymlinkOp{
Target: MustAbs("/etc/nixos"), Target: MustAbs("/etc/nixos"),
LinkName: "/etc/static/nixos", LinkName: "/etc/static/nixos",
}, nil, nil, []kexpect{ }, nil, nil, []stub.Call{
{"mkdirAll", expectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, nil}, call("mkdirAll", stub.ExpectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, nil),
{"symlink", expectArgs{"/etc/static/nixos", "/sysroot/etc/nixos"}, nil, nil}, call("symlink", stub.ExpectArgs{"/etc/static/nixos", "/sysroot/etc/nixos"}, nil, nil),
}, nil}, }, nil},
{"success", &Params{ParentPerm: 0755}, &SymlinkOp{ {"success", &Params{ParentPerm: 0755}, &SymlinkOp{
Target: MustAbs("/etc/mtab"), Target: MustAbs("/etc/mtab"),
LinkName: "/etc/mtab", LinkName: "/etc/mtab",
Dereference: true, Dereference: true,
}, []kexpect{ }, []stub.Call{
{"readlink", expectArgs{"/etc/mtab"}, "/proc/mounts", nil}, call("readlink", stub.ExpectArgs{"/etc/mtab"}, "/proc/mounts", nil),
}, nil, []kexpect{ }, nil, []stub.Call{
{"mkdirAll", expectArgs{"/sysroot/etc", os.FileMode(0755)}, nil, nil}, call("mkdirAll", stub.ExpectArgs{"/sysroot/etc", os.FileMode(0755)}, nil, nil),
{"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil}, call("symlink", stub.ExpectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil),
}, nil}, }, nil},
}) })

View File

@ -3,14 +3,20 @@ package container
import ( import (
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"io/fs"
"math" "math"
"os" "os"
"strconv"
. "syscall" . "syscall"
) )
func init() { gob.Register(new(MountTmpfsOp)) } 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]. // Tmpfs appends an [Op] that mounts tmpfs on container path [MountTmpfsOp.Path].
func (f *Ops) Tmpfs(target *Absolute, size int, perm os.FileMode) *Ops { func (f *Ops) Tmpfs(target *Absolute, size int, perm os.FileMode) *Ops {
*f = append(*f, &MountTmpfsOp{SourceTmpfsEphemeral, target, MS_NOSUID | MS_NODEV, size, perm}) *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) early(*setupState, syscallDispatcher) error { return nil }
func (t *MountTmpfsOp) apply(_ *setupState, k syscallDispatcher) error { func (t *MountTmpfsOp) apply(_ *setupState, k syscallDispatcher) error {
if t.Size < 0 || t.Size > math.MaxUint>>1 { 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) return k.mountTmpfs(t.FSName, toSysroot(t.Path.String()), t.Flags, t.Size, t.Perm)
} }

View File

@ -1,31 +1,40 @@
package container package container
import ( import (
"io/fs"
"os" "os"
"syscall" "syscall"
"testing" "testing"
"hakurei.app/container/stub"
) )
func TestMountTmpfsOp(t *testing.T) { 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{ checkOpBehaviour(t, []opBehaviourTestCase{
{"size oob", new(Params), &MountTmpfsOp{ {"size oob", new(Params), &MountTmpfsOp{
Size: -1, Size: -1,
}, nil, nil, nil, msg.WrapErr(fs.ErrInvalid, "size -1 out of bounds")}, }, nil, nil, nil, TmpfsSizeError(-1)},
{"success", new(Params), &MountTmpfsOp{ {"success", new(Params), &MountTmpfsOp{
FSName: "ephemeral", FSName: "ephemeral",
Path: MustAbs("/run/user/1000/"), Path: MustAbs("/run/user/1000/"),
Size: 1 << 10, Size: 1 << 10,
Perm: 0700, Perm: 0700,
}, nil, nil, []kexpect{ }, nil, nil, []stub.Call{
{"mountTmpfs", expectArgs{ call("mountTmpfs", stub.ExpectArgs{
"ephemeral", // fsname "ephemeral", // fsname
"/sysroot/run/user/1000", // target "/sysroot/run/user/1000", // target
uintptr(0), // flags uintptr(0), // flags
0x400, // size 0x400, // size
os.FileMode(0700), // perm os.FileMode(0700), // perm
}, nil, 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 { if err := p.k.mount(source, target, FstypeNULL, MS_SILENT|MS_BIND|flags&MS_REC, zeroString); err != nil {
return wrapErrSuffix(err, return err
fmt.Sprintf("cannot mount %q on %q:", source, target))
} }
return p.k.remount(target, flags) return p.k.remount(target, flags)
@ -119,7 +118,7 @@ func (p *procPaths) remount(target string, flags uintptr) error {
var targetFinal string var targetFinal string
if v, err := p.k.evalSymlinks(target); err != nil { if v, err := p.k.evalSymlinks(target); err != nil {
return wrapErrSelf(err) return err
} else { } else {
targetFinal = v targetFinal = v
if targetFinal != target { 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) destFd, err = p.k.open(targetFinal, O_PATH|O_CLOEXEC, 0)
return return
}); err != nil { }); err != nil {
return wrapErrSuffix(err, return &os.PathError{Op: "open", Path: targetFinal, Err: err}
fmt.Sprintf("cannot open %q:", targetFinal))
} }
if v, err := p.k.readlink(p.fd(destFd)); err != nil { 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 { } else if err = p.k.close(destFd); err != nil {
return wrapErrSuffix(err, return &os.PathError{Op: "close", Path: targetFinal, Err: err}
fmt.Sprintf("cannot close %q:", targetFinal))
} else { } else {
targetKFinal = v targetKFinal = v
} }
@ -152,17 +149,11 @@ func (p *procPaths) remount(target string, flags uintptr) error {
return p.mountinfo(func(d *vfs.MountInfoDecoder) error { return p.mountinfo(func(d *vfs.MountInfoDecoder) error {
n, err := d.Unfold(targetKFinal) n, err := d.Unfold(targetKFinal)
if err != nil { if err != nil {
if errors.Is(err, ESTALE) { return err
return msg.WrapErr(err,
fmt.Sprintf("mount point %q never appeared in mountinfo", targetKFinal))
}
return wrapErrSuffix(err,
"cannot unfold mount hierarchy:")
} }
if err = remountWithFlags(p.k, n, mf); err != nil { if err = remountWithFlags(p.k, n, mf); err != nil {
return wrapErrSuffix(err, return err
fmt.Sprintf("cannot remount %q:", n.Clean))
} }
if flags&MS_REC == 0 { if flags&MS_REC == 0 {
return nil return nil
@ -174,11 +165,8 @@ func (p *procPaths) remount(target string, flags uintptr) error {
continue continue
} }
err = remountWithFlags(p.k, cur, mf) if err = remountWithFlags(p.k, cur, mf); err != nil && !errors.Is(err, EACCES) {
return err
if err != nil && !errors.Is(err, EACCES) {
return wrapErrSuffix(err,
fmt.Sprintf("cannot propagate flags to %q:", cur.Clean))
} }
} }
@ -207,15 +195,13 @@ func mountTmpfs(k syscallDispatcher, fsname, target string, flags uintptr, size
// syscallDispatcher.mountTmpfs must not be called from this function // syscallDispatcher.mountTmpfs must not be called from this function
if err := k.mkdirAll(target, parentPerm(perm)); err != nil { if err := k.mkdirAll(target, parentPerm(perm)); err != nil {
return wrapErrSelf(err) return err
} }
opt := fmt.Sprintf("mode=%#o", perm) opt := fmt.Sprintf("mode=%#o", perm)
if size > 0 { if size > 0 {
opt += fmt.Sprintf(",size=%d", size) opt += fmt.Sprintf(",size=%d", size)
} }
return wrapErrSuffix( return k.mount(fsname, target, FstypeTmpfs, flags, opt)
k.mount(fsname, target, FstypeTmpfs, flags, opt),
fmt.Sprintf("cannot mount tmpfs on %q:", target))
} }
func parentPerm(perm os.FileMode) os.FileMode { func parentPerm(perm os.FileMode) os.FileMode {

View File

@ -5,6 +5,7 @@ import (
"syscall" "syscall"
"testing" "testing"
"hakurei.app/container/stub"
"hakurei.app/container/vfs" "hakurei.app/container/vfs"
) )
@ -12,25 +13,25 @@ func TestBindMount(t *testing.T) {
checkSimple(t, "bindMount", []simpleTestCase{ checkSimple(t, "bindMount", []simpleTestCase{
{"mount", func(k syscallDispatcher) error { {"mount", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY, true) return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY, true)
}, [][]kexpect{{ }, stub.Expect{Calls: []stub.Call{
{"verbosef", expectArgs{"resolved %q flags %#x", []any{"/sysroot/nix", uintptr(1)}}, nil, nil}, call("verbosef", stub.ExpectArgs{"resolved %q flags %#x", []any{"/sysroot/nix", uintptr(1)}}, nil, nil),
{"mount", expectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, errUnique}, call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, stub.UniqueError(0xbad)),
}}, wrapErrSuffix(errUnique, `cannot mount "/host/nix" on "/sysroot/nix":`)}, }}, stub.UniqueError(0xbad)},
{"success ne", func(k syscallDispatcher) error { {"success ne", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/.host-nix", syscall.MS_RDONLY, false) return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/.host-nix", syscall.MS_RDONLY, false)
}, [][]kexpect{{ }, stub.Expect{Calls: []stub.Call{
{"verbosef", expectArgs{"resolved %q on %q flags %#x", []any{"/host/nix", "/sysroot/.host-nix", uintptr(1)}}, nil, nil}, call("verbosef", stub.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}, call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/.host-nix", "", uintptr(0x9000), ""}, nil, nil),
{"remount", expectArgs{"/sysroot/.host-nix", uintptr(1)}, nil, nil}, call("remount", stub.ExpectArgs{"/sysroot/.host-nix", uintptr(1)}, nil, nil),
}}, nil}, }}, nil},
{"success", func(k syscallDispatcher) error { {"success", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY, true) return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY, true)
}, [][]kexpect{{ }, stub.Expect{Calls: []stub.Call{
{"verbosef", expectArgs{"resolved %q flags %#x", []any{"/sysroot/nix", uintptr(1)}}, nil, nil}, call("verbosef", stub.ExpectArgs{"resolved %q flags %#x", []any{"/sysroot/nix", uintptr(1)}}, nil, nil),
{"mount", expectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, nil}, call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, nil),
{"remount", expectArgs{"/sysroot/nix", uintptr(1)}, nil, nil}, call("remount", stub.ExpectArgs{"/sysroot/nix", uintptr(1)}, nil, nil),
}}, nil}, }}, nil},
}) })
} }
@ -81,138 +82,138 @@ func TestRemount(t *testing.T) {
checkSimple(t, "remount", []simpleTestCase{ checkSimple(t, "remount", []simpleTestCase{
{"evalSymlinks", func(k syscallDispatcher) error { {"evalSymlinks", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{ }, stub.Expect{Calls: []stub.Call{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", errUnique}, call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", stub.UniqueError(6)),
}}, wrapErrSelf(errUnique)}, }}, stub.UniqueError(6)},
{"open", func(k syscallDispatcher) error { {"open", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{ }, stub.Expect{Calls: []stub.Call{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil}, call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, errUnique}, call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, stub.UniqueError(5)),
}}, wrapErrSuffix(errUnique, `cannot open "/sysroot/nix":`)}, }}, &os.PathError{Op: "open", Path: "/sysroot/nix", Err: stub.UniqueError(5)}},
{"readlink", func(k syscallDispatcher) error { {"readlink", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{ }, stub.Expect{Calls: []stub.Call{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil}, call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil}, call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", errUnique}, call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", stub.UniqueError(4)),
}}, wrapErrSelf(errUnique)}, }}, stub.UniqueError(4)},
{"close", func(k syscallDispatcher) error { {"close", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{ }, stub.Expect{Calls: []stub.Call{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil}, call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil}, call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil}, call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
{"close", expectArgs{0xdeadbeef}, nil, errUnique}, call("close", stub.ExpectArgs{0xdeadbeef}, nil, stub.UniqueError(3)),
}}, wrapErrSuffix(errUnique, `cannot close "/sysroot/nix":`)}, }}, &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) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{ }, stub.Expect{Calls: []stub.Call{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/.hakurei", nil}, call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/.hakurei", nil),
{"verbosef", expectArgs{"target resolves to %q", []any{"/sysroot/.hakurei"}}, nil, nil}, call("verbosef", stub.ExpectArgs{"target resolves to %q", []any{"/sysroot/.hakurei"}}, nil, nil),
{"open", expectArgs{"/sysroot/.hakurei", 0x280000, uint32(0)}, 0xdeadbeef, nil}, call("open", stub.ExpectArgs{"/sysroot/.hakurei", 0x280000, uint32(0)}, 0xdeadbeef, nil),
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/.hakurei", nil}, call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/.hakurei", nil),
{"close", expectArgs{0xdeadbeef}, nil, nil}, call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil}, call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
}}, msg.WrapErr(syscall.ESTALE, `mount point "/sysroot/.hakurei" never appeared in mountinfo`)}, }}, &vfs.DecoderError{Op: "unfold", Line: -1, Err: vfs.UnfoldTargetError("/sysroot/.hakurei")}},
{"mountinfo", func(k syscallDispatcher) error { {"mountinfo", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{ }, stub.Expect{Calls: []stub.Call{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil}, call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil}, call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil}, call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
{"close", expectArgs{0xdeadbeef}, nil, nil}, call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile("\x00"), nil}, call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile("\x00"), nil),
}}, wrapErrSuffix(vfs.ErrMountInfoFields, `cannot parse mountinfo:`)}, }}, &vfs.DecoderError{Op: "parse", Line: 0, Err: vfs.ErrMountInfoFields}},
{"mount", func(k syscallDispatcher) error { {"mount", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{ }, stub.Expect{Calls: []stub.Call{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil}, call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil}, call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil}, call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
{"close", expectArgs{0xdeadbeef}, nil, nil}, call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil}, call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, errUnique}, call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, stub.UniqueError(2)),
}}, wrapErrSuffix(errUnique, `cannot remount "/sysroot/nix":`)}, }}, stub.UniqueError(2)},
{"mount propagate", func(k syscallDispatcher) error { {"mount propagate", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{ }, stub.Expect{Calls: []stub.Call{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil}, call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil}, call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil}, call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
{"close", expectArgs{0xdeadbeef}, nil, nil}, call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil}, call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil}, call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil),
{"mount", expectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, errUnique}, call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, stub.UniqueError(1)),
}}, wrapErrSuffix(errUnique, `cannot propagate flags to "/sysroot/nix/.ro-store":`)}, }}, stub.UniqueError(1)},
{"success toplevel", func(k syscallDispatcher) error { {"success toplevel", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/bin", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/bin", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{ }, stub.Expect{Calls: []stub.Call{
{"evalSymlinks", expectArgs{"/sysroot/bin"}, "/sysroot/bin", nil}, call("evalSymlinks", stub.ExpectArgs{"/sysroot/bin"}, "/sysroot/bin", nil),
{"open", expectArgs{"/sysroot/bin", 0x280000, uint32(0)}, 0xbabe, nil}, call("open", stub.ExpectArgs{"/sysroot/bin", 0x280000, uint32(0)}, 0xbabe, nil),
{"readlink", expectArgs{"/host/proc/self/fd/47806"}, "/sysroot/bin", nil}, call("readlink", stub.ExpectArgs{"/host/proc/self/fd/47806"}, "/sysroot/bin", nil),
{"close", expectArgs{0xbabe}, nil, nil}, call("close", stub.ExpectArgs{0xbabe}, nil, nil),
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil}, call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
{"mount", expectArgs{"none", "/sysroot/bin", "", uintptr(0x209027), ""}, nil, nil}, call("mount", stub.ExpectArgs{"none", "/sysroot/bin", "", uintptr(0x209027), ""}, nil, nil),
}}, nil}, }}, nil},
{"success EACCES", func(k syscallDispatcher) error { {"success EACCES", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{ }, stub.Expect{Calls: []stub.Call{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil}, call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil}, call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil}, call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
{"close", expectArgs{0xdeadbeef}, nil, nil}, call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil}, call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil}, call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil),
{"mount", expectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, syscall.EACCES}, call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, syscall.EACCES),
{"mount", expectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil}, call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil),
}}, nil}, }}, nil},
{"success no propagate", func(k syscallDispatcher) error { {"success no propagate", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{ }, stub.Expect{Calls: []stub.Call{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil}, call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil}, call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil}, call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
{"close", expectArgs{0xdeadbeef}, nil, nil}, call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil}, call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil}, call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil),
}}, nil}, }}, nil},
{"success case sensitive", func(k syscallDispatcher) error { {"success case sensitive", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{ }, stub.Expect{Calls: []stub.Call{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil}, call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil}, call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil}, call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
{"close", expectArgs{0xdeadbeef}, nil, nil}, call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil}, call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil}, call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil),
{"mount", expectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, nil}, call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, nil),
{"mount", expectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil}, call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil),
}}, nil}, }}, nil},
{"success", func(k syscallDispatcher) error { {"success", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/.nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/.nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{ }, stub.Expect{Calls: []stub.Call{
{"evalSymlinks", expectArgs{"/sysroot/.nix"}, "/sysroot/NIX", nil}, call("evalSymlinks", stub.ExpectArgs{"/sysroot/.nix"}, "/sysroot/NIX", nil),
{"verbosef", expectArgs{"target resolves to %q", []any{"/sysroot/NIX"}}, nil, nil}, call("verbosef", stub.ExpectArgs{"target resolves to %q", []any{"/sysroot/NIX"}}, nil, nil),
{"open", expectArgs{"/sysroot/NIX", 0x280000, uint32(0)}, 0xdeadbeef, nil}, call("open", stub.ExpectArgs{"/sysroot/NIX", 0x280000, uint32(0)}, 0xdeadbeef, nil),
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil}, call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
{"close", expectArgs{0xdeadbeef}, nil, nil}, call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil}, call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil}, call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil),
{"mount", expectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, nil}, call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, nil),
{"mount", expectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil}, call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil),
}}, nil}, }}, nil},
}) })
} }
@ -221,18 +222,18 @@ func TestRemountWithFlags(t *testing.T) {
checkSimple(t, "remountWithFlags", []simpleTestCase{ checkSimple(t, "remountWithFlags", []simpleTestCase{
{"noop unmatched", func(k syscallDispatcher) error { {"noop unmatched", func(k syscallDispatcher) error {
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime,cat"}}, 0) return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime,cat"}}, 0)
}, [][]kexpect{{ }, stub.Expect{Calls: []stub.Call{
{"verbosef", expectArgs{"unmatched vfs options: %q", []any{[]string{"cat"}}}, nil, nil}, call("verbosef", stub.ExpectArgs{"unmatched vfs options: %q", []any{[]string{"cat"}}}, nil, nil),
}}, nil}, }}, nil},
{"noop", func(k syscallDispatcher) error { {"noop", func(k syscallDispatcher) error {
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, 0) return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, 0)
}, nil, nil}, }, stub.Expect{}, nil},
{"success", func(k syscallDispatcher) error { {"success", func(k syscallDispatcher) error {
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, syscall.MS_RDONLY) return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, syscall.MS_RDONLY)
}, [][]kexpect{{ }, stub.Expect{Calls: []stub.Call{
{"mount", expectArgs{"none", "", "", uintptr(0x209021), ""}, nil, nil}, call("mount", stub.ExpectArgs{"none", "", "", uintptr(0x209021), ""}, nil, nil),
}}, nil}, }}, nil},
}) })
} }
@ -241,22 +242,22 @@ func TestMountTmpfs(t *testing.T) {
checkSimple(t, "mountTmpfs", []simpleTestCase{ checkSimple(t, "mountTmpfs", []simpleTestCase{
{"mkdirAll", func(k syscallDispatcher) error { {"mkdirAll", func(k syscallDispatcher) error {
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700) return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700)
}, [][]kexpect{{ }, stub.Expect{Calls: []stub.Call{
{"mkdirAll", expectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, errUnique}, call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, stub.UniqueError(0)),
}}, wrapErrSelf(errUnique)}, }}, stub.UniqueError(0)},
{"success no size", func(k syscallDispatcher) error { {"success no size", func(k syscallDispatcher) error {
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 0, 0710) return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 0, 0710)
}, [][]kexpect{{ }, stub.Expect{Calls: []stub.Call{
{"mkdirAll", expectArgs{"/sysroot/run/user/1000", os.FileMode(0750)}, nil, nil}, call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0750)}, nil, nil),
{"mount", expectArgs{"ephemeral", "/sysroot/run/user/1000", "tmpfs", uintptr(0), "mode=0710"}, nil, nil}, call("mount", stub.ExpectArgs{"ephemeral", "/sysroot/run/user/1000", "tmpfs", uintptr(0), "mode=0710"}, nil, nil),
}}, nil}, }}, nil},
{"success", func(k syscallDispatcher) error { {"success", func(k syscallDispatcher) error {
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700) return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700)
}, [][]kexpect{{ }, stub.Expect{Calls: []stub.Call{
{"mkdirAll", expectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, nil}, call("mkdirAll", stub.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}, call("mount", stub.ExpectArgs{"ephemeral", "/sysroot/run/user/1000", "tmpfs", uintptr(0), "mode=0700,size=1024"}, nil, nil),
}}, nil}, }}, nil},
}) })
} }

View File

@ -2,24 +2,34 @@ package container
import ( import (
"errors" "errors"
"fmt"
"log" "log"
"os"
"reflect"
"sync/atomic" "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 { type Msg interface {
IsVerbose() bool IsVerbose() bool
Verbose(v ...any) Verbose(v ...any)
Verbosef(format string, v ...any) Verbosef(format string, v ...any)
WrapErr(err error, a ...any) error
PrintBaseErr(err error, fallback string)
Suspend() Suspend()
Resume() bool Resume() bool
BeforeExit() 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) Suspend() { msg.inactive.Store(true) }
func (msg *DefaultMsg) Resume() bool { return msg.inactive.CompareAndSwap(true, false) } func (msg *DefaultMsg) Resume() bool { return msg.inactive.CompareAndSwap(true, false) }
func (msg *DefaultMsg) BeforeExit() {} func (msg *DefaultMsg) BeforeExit() {}

View File

@ -9,13 +9,36 @@ import (
"testing" "testing"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/internal/hlog"
) )
func TestDefaultMsg(t *testing.T) { func TestMessageError(t *testing.T) {
// bypass WrapErr testing behaviour testCases := []struct {
t.Setenv("GOPATH", container.Nonexistent) 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() w := log.Writer()
f := log.Flags() 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) { t.Run("inactive", func(t *testing.T) {
{ {
inactive := msg.Resume() inactive := msg.Resume()
@ -83,25 +91,6 @@ func TestDefaultMsg(t *testing.T) {
// the function is a noop // the function is a noop
t.Run("beforeExit", func(t *testing.T) { msg.BeforeExit() }) 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{} type panicWriter struct{}
@ -139,9 +128,6 @@ func (out *testOutput) Verbosef(format string, v ...any) {
out.t.Logf(format, v...) 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() { func (out *testOutput) Suspend() {
if out.suspended.CompareAndSwap(false, true) { if out.suspended.CompareAndSwap(false, true) {
out.Verbose("suspend called") out.Verbose("suspend called")

View File

@ -10,17 +10,3 @@ func SetOutput(v Msg) {
msg = v 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 package container
import ( import (
"reflect"
"syscall"
"testing" "testing"
) )
@ -31,65 +29,6 @@ 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 { type stubOutput struct {
wrapF func(error, ...any) error wrapF func(error, ...any) error
} }
@ -97,14 +36,6 @@ type stubOutput struct {
func (*stubOutput) IsVerbose() bool { panic("unreachable") } func (*stubOutput) IsVerbose() bool { panic("unreachable") }
func (*stubOutput) Verbose(...any) { panic("unreachable") } func (*stubOutput) Verbose(...any) { panic("unreachable") }
func (*stubOutput) Verbosef(string, ...any) { panic("unreachable") } func (*stubOutput) Verbosef(string, ...any) { panic("unreachable") }
func (*stubOutput) PrintBaseErr(error, string) { panic("unreachable") }
func (*stubOutput) Suspend() { panic("unreachable") } func (*stubOutput) Suspend() { panic("unreachable") }
func (*stubOutput) Resume() bool { panic("unreachable") } func (*stubOutput) Resume() bool { panic("unreachable") }
func (*stubOutput) BeforeExit() { panic("unreachable") } func (*stubOutput) BeforeExit() { panic("unreachable") }
func (s *stubOutput) WrapErr(err error, v ...any) error {
if s.wrapF == nil {
panic("unreachable")
}
return s.wrapF(err, v...)
}

View File

@ -8,11 +8,6 @@ import (
"syscall" "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. // Setup appends the read end of a pipe for setup params transmission and returns its fd.
func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) { func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) {
if r, w, err := os.Pipe(); err != nil { 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. // Receive retrieves setup fd from the environment and receives params.
func Receive(key string, e any, fdp *uintptr) (func() error, error) { func Receive(key string, e any, fdp *uintptr) (func() error, error) {
var setup *os.File var setup *os.File
if s, ok := os.LookupEnv(key); !ok { if s, ok := os.LookupEnv(key); !ok {
return nil, ErrNotSet return nil, ErrReceiveEnv
} else { } else {
if fd, err := strconv.Atoi(s); err != nil { if fd, err := strconv.Atoi(s); err != nil {
return nil, ErrFdFormat return nil, errors.Unwrap(err)
} else { } else {
setup = os.NewFile(uintptr(fd), "setup") setup = os.NewFile(uintptr(fd), "setup")
if setup == nil { if setup == nil {
return nil, syscall.EBADF return nil, syscall.EDOM
} }
if fdp != nil { if fdp != nil {
*fdp = setup.Fd() *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) { if _, err := container.Receive(key, nil, nil); !errors.Is(err, container.ErrReceiveEnv) {
t.Errorf("Receive: error = %v, want %v", err, container.ErrNotSet) t.Errorf("Receive: error = %v, want %v", err, container.ErrReceiveEnv)
} }
}) })
@ -38,8 +38,8 @@ func TestSetupReceive(t *testing.T) {
const key = "TEST_ENV_FORMAT" const key = "TEST_ENV_FORMAT"
t.Setenv(key, "") t.Setenv(key, "")
if _, err := container.Receive(key, nil, nil); !errors.Is(err, container.ErrFdFormat) { if _, err := container.Receive(key, nil, nil); !errors.Is(err, strconv.ErrSyntax) {
t.Errorf("Receive: error = %v, want %v", err, container.ErrFdFormat) t.Errorf("Receive: error = %v, want %v", err, strconv.ErrSyntax)
} }
}) })
@ -47,8 +47,8 @@ func TestSetupReceive(t *testing.T) {
const key = "TEST_ENV_RANGE" const key = "TEST_ENV_RANGE"
t.Setenv(key, "-1") t.Setenv(key, "-1")
if _, err := container.Receive(key, nil, nil); !errors.Is(err, syscall.EBADF) { if _, err := container.Receive(key, nil, nil); !errors.Is(err, syscall.EDOM) {
t.Errorf("Receive: error = %v, want %v", err, syscall.EBADF) t.Errorf("Receive: error = %v, want %v", err, syscall.EDOM)
} }
}) })

View File

@ -2,7 +2,6 @@ package container
import ( import (
"errors" "errors"
"fmt"
"io/fs" "io/fs"
"os" "os"
"path" "path"
@ -103,30 +102,29 @@ func toHost(name string) string {
func createFile(name string, perm, pperm os.FileMode, content []byte) error { func createFile(name string, perm, pperm os.FileMode, content []byte) error {
if err := os.MkdirAll(path.Dir(name), pperm); err != nil { 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) f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm)
if err != nil { if err != nil {
return wrapErrSelf(err) return err
} }
if content != nil { if content != nil {
_, err = f.Write(content) _, 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 { func ensureFile(name string, perm, pperm os.FileMode) error {
fi, err := os.Stat(name) fi, err := os.Stat(name)
if err != nil { if err != nil {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
return wrapErrSelf(err) return err
} }
return createFile(name, perm, pperm, nil) return createFile(name, perm, pperm, nil)
} }
if mode := fi.Mode(); mode&fs.ModeDir != 0 || mode&fs.ModeSymlink != 0 { if mode := fi.Mode(); mode&fs.ModeDir != 0 || mode&fs.ModeSymlink != 0 {
err = msg.WrapErr(syscall.EISDIR, err = &os.PathError{Op: "ensure", Path: name, Err: syscall.EISDIR}
fmt.Sprintf("path %q is a directory", name))
} }
return err 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) fd(fd int) string { return p.self + "/fd/" + strconv.Itoa(fd) }
func (p *procPaths) mountinfo(f func(d *vfs.MountInfoDecoder) error) error { func (p *procPaths) mountinfo(f func(d *vfs.MountInfoDecoder) error) error {
if r, err := p.k.openNew(p.self + "/mountinfo"); err != nil { if r, err := p.k.openNew(p.self + "/mountinfo"); err != nil {
return wrapErrSelf(err) return err
} else { } else {
d := vfs.NewMountInfoDecoder(r) d := vfs.NewMountInfoDecoder(r)
err0 := f(d) err0 := f(d)
if err = r.Close(); err != nil { if err = r.Close(); err != nil {
return wrapErrSelf(err) return err
} else if err = d.Err(); err != nil { } else if err = d.Err(); err != nil {
return wrapErrSuffix(err, return err
"cannot parse mountinfo:")
} }
return err0 return err0
} }

View File

@ -1,8 +1,6 @@
package container package container
import ( import (
"errors"
"fmt"
"io" "io"
"math" "math"
"os" "os"
@ -56,20 +54,27 @@ func InternalToHostOvlEscape(s string) string { return EscapeOverlayDataSegment(
func TestCreateFile(t *testing.T) { func TestCreateFile(t *testing.T) {
t.Run("nonexistent", func(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{ t.Run("mkdir", func(t *testing.T) {
wantErr := &os.PathError{
Op: "mkdir", Op: "mkdir",
Path: "/proc/nonexistent", Path: "/proc/nonexistent",
Err: syscall.ENOENT, Err: syscall.ENOENT,
})) {
t.Errorf("createFile: error = %v", err)
} }
if err := createFile(path.Join(Nonexistent), 0644, 0755, nil); !errors.Is(err, wrapErrSelf(&os.PathError{ 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", Op: "open",
Path: "/proc/nonexistent", Path: "/proc/nonexistent",
Err: syscall.ENOENT, Err: syscall.ENOENT,
})) {
t.Errorf("createFile: error = %v", err)
} }
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) { t.Run("touch", func(t *testing.T) {
@ -120,13 +125,13 @@ func TestEnsureFile(t *testing.T) {
t.Fatalf("Chmod: error = %v", err) t.Fatalf("Chmod: error = %v", err)
} }
wantErr := wrapErrSelf(&os.PathError{ wantErr := &os.PathError{
Op: "stat", Op: "stat",
Path: pathname, Path: pathname,
Err: syscall.EACCES, Err: syscall.EACCES,
}) }
if err := ensureFile(pathname, 0644, 0755); !errors.Is(err, wantErr) { if err := ensureFile(pathname, 0644, 0755); !reflect.DeepEqual(err, wantErr) {
t.Errorf("ensureFile: error = %v, want %v", err, wantErr) t.Errorf("ensureFile: error = %#v, want %#v", err, wantErr)
} }
if err := os.Chmod(tempDir, 0755); err != nil { if err := os.Chmod(tempDir, 0755); err != nil {
@ -136,9 +141,9 @@ func TestEnsureFile(t *testing.T) {
t.Run("directory", func(t *testing.T) { t.Run("directory", func(t *testing.T) {
pathname := t.TempDir() pathname := t.TempDir()
wantErr := msg.WrapErr(syscall.EISDIR, fmt.Sprintf("path %q is a directory", pathname)) wantErr := &os.PathError{Op: "ensure", Path: pathname, Err: syscall.EISDIR}
if err := ensureFile(pathname, 0644, 0755); !errors.Is(err, wantErr) { if err := ensureFile(pathname, 0644, 0755); !reflect.DeepEqual(err, wantErr) {
t.Errorf("ensureFile: error = %v, want %v", 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("mountinfo", func(t *testing.T) {
t.Run("nonexistent", func(t *testing.T) { t.Run("nonexistent", func(t *testing.T) {
nonexistentProc := newProcPaths(direct{}, t.TempDir()) nonexistentProc := newProcPaths(direct{}, t.TempDir())
wantErr := wrapErrSelf(&os.PathError{ wantErr := &os.PathError{
Op: "open", Op: "open",
Path: nonexistentProc.self + "/mountinfo", Path: nonexistentProc.self + "/mountinfo",
Err: syscall.ENOENT, 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) 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) { t.Run("closed", func(t *testing.T) {
p := newProcPaths(direct{}, tempDir) p := newProcPaths(direct{}, tempDir)
wantErr := wrapErrSelf(&os.PathError{ wantErr := &os.PathError{
Op: "close", Op: "close",
Path: p.self + "/mountinfo", Path: p.self + "/mountinfo",
Err: os.ErrClosed, Err: os.ErrClosed,
}) }
if err := p.mountinfo(func(d *vfs.MountInfoDecoder) error { if err := p.mountinfo(func(d *vfs.MountInfoDecoder) error {
v := reflect.ValueOf(d).Elem().FieldByName("s").Elem().FieldByName("r") v := reflect.ValueOf(d).Elem().FieldByName("s").Elem().FieldByName("r")
v = reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr())) v = reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr()))
@ -231,8 +236,8 @@ func TestProcPaths(t *testing.T) {
} else { } else {
return f.Close() return f.Close()
} }
}); !errors.Is(err, wantErr) { }); !reflect.DeepEqual(err, wantErr) {
t.Errorf("mountinfo: error = %v, want %v", 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) t.Fatalf("WriteFile: error = %v", err)
} }
wantErr := wrapErrSuffix(vfs.ErrMountInfoFields, "cannot parse mountinfo:") 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)) }); !errors.Is(err, wantErr) { 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) 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") 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 ( type (
// A MountInfoDecoder reads and decodes proc_pid_mountinfo(5) entries from an input stream. // A MountInfoDecoder reads and decodes proc_pid_mountinfo(5) entries from an input stream.
MountInfoDecoder struct { MountInfoDecoder struct {
@ -32,6 +58,7 @@ type (
current *MountInfo current *MountInfo
parseErr error parseErr error
curLine int
complete bool complete bool
} }
@ -132,9 +159,12 @@ func (d *MountInfoDecoder) Entries() iter.Seq[*MountInfoEntry] {
func (d *MountInfoDecoder) Err() error { func (d *MountInfoDecoder) Err() error {
if err := d.s.Err(); err != nil { 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 { func (d *MountInfoDecoder) scan() bool {
@ -160,6 +190,7 @@ func (d *MountInfoDecoder) scan() bool {
d.current.Next = m d.current.Next = m
d.current = d.current.Next d.current = d.current.Next
} }
d.curLine++
return true return true
} }

View File

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"iter" "iter"
"os"
"path" "path"
"reflect" "reflect"
"slices" "slices"
@ -15,62 +16,102 @@ import (
"hakurei.app/container/vfs" "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) { func TestMountInfo(t *testing.T) {
testCases := []mountInfoTest{ testCases := []mountInfoTest{
{"count", sampleMountinfoBase + ` {"count", sampleMountinfoBase + `
21 20 0:53/ /mnt/test rw,relatime - tmpfs rw 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`, 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 + ` {"sep", sampleMountinfoBase + `
21 20 0:53 / /mnt/test rw,relatime shared:212 _ tmpfs rw 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`, 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", sampleMountinfoBase + `
id 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw 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`, 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 + ` {"parent", sampleMountinfoBase + `
21 parent 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw 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`, 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 + ` {"devno", sampleMountinfoBase + `
21 20 053 / /mnt/test rw,relatime shared:212 - tmpfs rw 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`, 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 + ` {"maj", sampleMountinfoBase + `
21 20 maj:53 / /mnt/test rw,relatime shared:212 - tmpfs rw 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`, 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 + ` {"min", sampleMountinfoBase + `
21 20 0:min / /mnt/test rw,relatime shared:212 - tmpfs rw 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`, 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 + ` {"mountroot", sampleMountinfoBase + `
21 20 0:53 /mnt/test rw,relatime - tmpfs rw 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`, 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 + ` {"target", sampleMountinfoBase + `
21 20 0:53 / rw,relatime - tmpfs rw 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`, 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 + ` {"vfs options", sampleMountinfoBase + `
21 20 0:53 / /mnt/test - tmpfs rw 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`, 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 + ` {"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
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.ErrMountInfoEmpty, "", nil, nil, nil}, &vfs.DecoderError{Op: "parse", Line: 7, Err: vfs.ErrMountInfoEmpty}, "", nil, nil, nil},
{"base", sampleMountinfoBase, nil, "", []*wantMountInfo{ {"base", sampleMountinfoBase, nil, "", []*wantMountInfo{
m(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", syscall.MS_RELATIME, nil), 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 { } else if tc.wantNode != nil || tc.wantCollectF != nil {
panic("invalid test case") 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 == "" { if tc.wantError == "" {
t.Errorf("Unfold: error = %v, wantErr %v", t.Errorf("Unfold: error = %#v, wantErr %#v",
err, tc.wantErr) err, tc.wantErr)
} else if err != nil && err.Error() != tc.wantError { } else if err != nil && err.Error() != tc.wantError {
t.Errorf("Unfold: error = %q, wantError %q", 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 == "" { if tc.wantError == "" {
t.Errorf("%s: error = %v, wantErr %v", t.Errorf("%s: error = %#v, wantErr %#v",
funcName, err, tc.wantErr) funcName, err, tc.wantErr)
} else if err != nil && err.Error() != tc.wantError { } else if err != nil && err.Error() != tc.wantError {
t.Errorf("%s: error = %q, wantError %q", t.Errorf("%s: error = %q, wantError %q",

View File

@ -4,9 +4,14 @@ import (
"iter" "iter"
"path" "path"
"strings" "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. // MountInfoNode positions a [MountInfoEntry] in its mount hierarchy.
type MountInfoNode struct { type MountInfoNode struct {
*MountInfoEntry *MountInfoEntry
@ -65,7 +70,8 @@ func (d *MountInfoDecoder) Unfold(target string) (*MountInfoNode, error) {
} }
if targetIndex == -1 { 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 { for _, cur := range mountinfo {

View File

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

View File

@ -87,7 +87,9 @@ type (
// initial process environment variables // initial process environment variables
Env map[string]string `json:"env"` 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"` MapRealUID bool `json:"map_real_uid"`
// pass through all devices // pass through all devices

View File

@ -1,61 +1,82 @@
// Package app defines the generic [App] interface. // Package app implements high-level hakurei container behaviour.
package app package app
import ( import (
"context" "context"
"fmt"
"log" "log"
"syscall" "sync"
"time"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/app/state" "hakurei.app/internal/app/state"
"hakurei.app/internal/sys" "hakurei.app/internal/sys"
) )
type App interface { func New(ctx context.Context, os sys.State) (*App, error) {
// ID returns a copy of [ID] held by App. a := new(App)
ID() state.ID a.sys = os
a.ctx = ctx
// Seal determines the outcome of config as a [SealedApp]. id := new(state.ID)
// The value of config might be overwritten and must not be used again. err := state.NewAppID(id)
Seal(config *hst.Config) (SealedApp, error) a.id = newID(id)
String() string return a, err
} }
type SealedApp interface { func MustNew(ctx context.Context, os sys.State) *App {
// 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")
}
now := time.Now().UTC()
rs.Time = &now
}
func MustNew(ctx context.Context, os sys.State) App {
a, err := New(ctx, os) a, err := New(ctx, os)
if err != nil { if err != nil {
log.Fatalf("cannot create app: %v", err) log.Fatalf("cannot create app: %v", err)
} }
return a return a
} }
type App struct {
outcome *Outcome
id *stringPair[state.ID]
sys sys.State
ctx context.Context
mu sync.RWMutex
}
// ID returns a copy of [state.ID] held by App.
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)
}
// Seal determines the [Outcome] of [hst.Config].
// Values stored in and referred to by [hst.Config] might be overwritten and must not be used again.
func (a *App) Seal(config *hst.Config) (*Outcome, 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,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 package app_test
import ( import (
"context"
"syscall" "syscall"
"hakurei.app/container" "hakurei.app/container"
@ -74,7 +75,7 @@ var testCasesNixos = []sealTestCase{
0x4c, 0xf0, 0x73, 0xbd, 0x4c, 0xf0, 0x73, 0xbd,
0xb4, 0x6e, 0xb5, 0xc1, 0xb4, 0x6e, 0xb5, 0xc1,
}, },
system.New(1000001). system.New(context.TODO(), 1000001).
Ensure("/tmp/hakurei.1971", 0711). 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", 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). Ensure("/tmp/hakurei.1971/runtime/1", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/1", acl.Read, acl.Write, acl.Execute).

View File

@ -1,6 +1,7 @@
package app_test package app_test
import ( import (
"context"
"os" "os"
"syscall" "syscall"
@ -23,7 +24,7 @@ var testCasesPd = []sealTestCase{
0xbd, 0x01, 0x78, 0x0e, 0xbd, 0x01, 0x78, 0x0e,
0xb9, 0xa6, 0x07, 0xac, 0xb9, 0xa6, 0x07, 0xac,
}, },
system.New(1000000). system.New(context.TODO(), 1000000).
Ensure("/tmp/hakurei.1971", 0711). 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", 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). Ensure("/tmp/hakurei.1971/runtime/0", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/0", acl.Read, acl.Write, acl.Execute).
@ -115,7 +116,7 @@ var testCasesPd = []sealTestCase{
0x82, 0xd4, 0x13, 0x36, 0x82, 0xd4, 0x13, 0x36,
0x9b, 0x64, 0xce, 0x7c, 0x9b, 0x64, 0xce, 0x7c,
}, },
system.New(1000009). system.New(context.TODO(), 1000009).
Ensure("/tmp/hakurei.1971", 0711). 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", 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). Ensure("/tmp/hakurei.1971/runtime/9", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/9", acl.Read, acl.Write, acl.Execute).

View File

@ -30,7 +30,7 @@ func TestApp(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
a := app.NewWithID(tc.id, tc.os) a := app.NewWithID(t.Context(), tc.id, tc.os)
var ( var (
gotSys *system.I gotSys *system.I
gotContainer *container.Params gotContainer *container.Params

View File

@ -16,8 +16,7 @@ import (
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
) )
// in practice there should be less than 30 entries added by the runtime; // in practice there should be less than 30 system mount points
// allocating slightly more as a margin for future expansion
const preallocateOpsCount = 1 << 5 const preallocateOpsCount = 1 << 5
// newContainer initialises [container.Params] via [hst.ContainerConfig]. // newContainer initialises [container.Params] via [hst.ContainerConfig].
@ -67,8 +66,6 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
} }
if s.MapRealUID { 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() params.Uid = os.Getuid()
*uid = params.Uid *uid = params.Uid
params.Gid = os.Getgid() params.Gid = os.Getgid()
@ -104,6 +101,7 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
} }
/* retrieve paths and hide them if they're made available in the sandbox; /* retrieve paths and hide them if they're made available in the sandbox;
this feature tries to improve user experience of permissive defaults, and this feature tries to improve user experience of permissive defaults, and
to warn about issues in custom configuration; it is NOT a security feature 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 */ and should not be treated as such, ALWAYS be careful with what you bind */

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,21 @@
package app
import (
"context"
"hakurei.app/container"
"hakurei.app/internal/app/state"
"hakurei.app/internal/sys"
"hakurei.app/system"
)
func NewWithID(ctx context.Context, id state.ID, os sys.State) *App {
return &App{id: newID(&id), sys: os, ctx: ctx}
}
func AppIParams(a *App, seal *Outcome) (*system.I, *container.Params) {
if a.outcome != seal || a.id != seal.id {
panic("broken app/outcome link")
}
return seal.sys, seal.container
}

View File

@ -21,9 +21,34 @@ import (
const shimWaitTimeout = 5 * time.Second const shimWaitTimeout = 5 * time.Second
func (seal *outcome) Run(rs *RunState) error { // RunState stores the outcome of a call to [Outcome.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")
}
now := time.Now().UTC()
rs.Time = &now
}
// Run commits deferred system setup and starts the container.
func (seal *Outcome) Run(rs *RunState) error {
if !seal.f.CompareAndSwap(false, true) { 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 // 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 // in inconsistent state that is impossible to clean up; return here to limit damage and hopefully give the
// other Run a chance to return // other Run a chance to return
return errors.New("outcome: attempted to run twice") return errors.New("outcome: attempted to run twice")
@ -36,7 +61,7 @@ func (seal *outcome) Run(rs *RunState) error {
// read comp value early to allow for early failure // read comp value early to allow for early failure
hsuPath := internal.MustHsuPath() hsuPath := internal.MustHsuPath()
if err := seal.sys.Commit(seal.ctx); err != nil { if err := seal.sys.Commit(); err != nil {
return err return err
} }
store := state.NewMulti(seal.runDirPath.String()) store := state.NewMulti(seal.runDirPath.String())
@ -44,7 +69,7 @@ func (seal *outcome) Run(rs *RunState) error {
defer func() { defer func() {
var revertErr error var revertErr error
storeErr := new(StateStoreError) storeErr := new(StateStoreError)
storeErr.Inner, storeErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) { storeErr.Inner, storeErr.DoErr = store.Do(seal.user.identity.unwrap(), func(c state.Cursor) {
revertErr = func() error { revertErr = func() error {
storeErr.InnerErr = deferredStoreFunc(c) storeErr.InnerErr = deferredStoreFunc(c)
@ -102,7 +127,7 @@ func (seal *outcome) Run(rs *RunState) error {
// passed through to shim by hsu // passed through to shim by hsu
shimEnv + "=" + strconv.Itoa(fd), shimEnv + "=" + strconv.Itoa(fd),
// interpreted by hsu // interpreted by hsu
"HAKUREI_APP_ID=" + seal.user.aid.String(), "HAKUREI_APP_ID=" + seal.user.identity.String(),
} }
} }
@ -118,7 +143,7 @@ func (seal *outcome) Run(rs *RunState) error {
return hlog.WrapErrSuffix(err, return hlog.WrapErrSuffix(err,
"cannot start setuid wrapper:") "cannot start setuid wrapper:")
} }
rs.SetStart() rs.setStart()
// this prevents blocking forever on an early failure // this prevents blocking forever on an early failure
waitErr, setupErr := make(chan error, 1), make(chan error, 1) waitErr, setupErr := make(chan error, 1), make(chan error, 1)
@ -155,7 +180,7 @@ func (seal *outcome) Run(rs *RunState) error {
PID: cmd.Process.Pid, PID: cmd.Process.Pid,
Time: *rs.Time, Time: *rs.Time,
} }
earlyStoreErr.Inner, earlyStoreErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) { earlyStoreErr.Inner, earlyStoreErr.DoErr = store.Do(seal.user.identity.unwrap(), func(c state.Cursor) {
earlyStoreErr.InnerErr = c.Save(&sd, seal.ct) earlyStoreErr.InnerErr = c.Save(&sd, seal.ct)
}) })
} }
@ -173,10 +198,13 @@ func (seal *outcome) Run(rs *RunState) error {
switch { switch {
case rs.Exited(): case rs.Exited():
hlog.Verbosef("process %d exited with code %d", cmd.Process.Pid, rs.ExitStatus()) hlog.Verbosef("process %d exited with code %d", cmd.Process.Pid, rs.ExitStatus())
case rs.CoreDump(): case rs.CoreDump():
hlog.Verbosef("process %d dumped core", cmd.Process.Pid) hlog.Verbosef("process %d dumped core", cmd.Process.Pid)
case rs.Signaled(): case rs.Signaled():
hlog.Verbosef("process %d got %s", cmd.Process.Pid, rs.Signal()) hlog.Verbosef("process %d got %s", cmd.Process.Pid, rs.Signal())
default: default:
hlog.Verbosef("process %d exited with status %#x", cmd.Process.Pid, rs.WaitStatus) hlog.Verbosef("process %d exited with status %#x", cmd.Process.Pid, rs.WaitStatus)
} }

View File

@ -10,7 +10,6 @@ import (
"io/fs" "io/fs"
"os" "os"
"path" "path"
"regexp"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
@ -20,7 +19,6 @@ import (
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/app/state" "hakurei.app/internal/app/state"
"hakurei.app/internal/hlog" "hakurei.app/internal/hlog"
"hakurei.app/internal/sys" "hakurei.app/internal/sys"
@ -60,10 +58,8 @@ var (
ErrPulseMode = errors.New("unexpected pulse socket mode") 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}\\$)$") // An Outcome is the runnable state of a hakurei container via [hst.Config].
type Outcome struct {
// outcome stores copies of various parts of [hst.Config]
type outcome struct {
// copied from initialising [app] // copied from initialising [app]
id *stringPair[state.ID] id *stringPair[state.ID]
// copied from [sys.State] // copied from [sys.State]
@ -96,7 +92,7 @@ type shareHost struct {
// process-specific directory in XDG_RUNTIME_DIR, empty if unused // process-specific directory in XDG_RUNTIME_DIR, empty if unused
runtimeSharePath *container.Absolute runtimeSharePath *container.Absolute
seal *outcome seal *Outcome
sc hst.Paths sc hst.Paths
} }
@ -136,8 +132,7 @@ func (share *shareHost) runtime() *container.Absolute {
// hsuUser stores post-hsu credentials and metadata // hsuUser stores post-hsu credentials and metadata
type hsuUser struct { type hsuUser struct {
// identity identity *stringPair[int]
aid *stringPair[int]
// target uid resolved by hid:aid // target uid resolved by hid:aid
uid *stringPair[int] uid *stringPair[int]
@ -150,9 +145,12 @@ type hsuUser struct {
username string username string
} }
func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Config) error { func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Config) error {
if ctx == nil {
panic("invalid call to finalise")
}
if seal.ctx != nil { if seal.ctx != nil {
panic("finalise called twice") panic("attempting to finalise twice")
} }
seal.ctx = ctx seal.ctx = ctx
@ -173,25 +171,24 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
seal.ct = ct 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 { if config.Identity < 0 || config.Identity > 9999 {
return hlog.WrapErr(ErrIdent, return hlog.WrapErr(ErrIdent,
fmt.Sprintf("identity %d out of range", config.Identity)) fmt.Sprintf("identity %d out of range", config.Identity))
} }
seal.user = hsuUser{ seal.user = hsuUser{
aid: newInt(config.Identity), identity: newInt(config.Identity),
home: config.Home, home: config.Home,
username: config.Username, username: config.Username,
} }
if seal.user.username == "" { if seal.user.username == "" {
seal.user.username = "chronos" seal.user.username = "chronos"
} else if !posixUsername.MatchString(seal.user.username) || } else if !isValidUsername(seal.user.username) {
len(seal.user.username) >= internal.Sysconf(internal.SC_LOGIN_NAME_MAX) {
return hlog.WrapErr(ErrName, return hlog.WrapErr(ErrName,
fmt.Sprintf("invalid user name %q", seal.user.username)) fmt.Sprintf("invalid user name %q", seal.user.username))
} }
if u, err := sys.Uid(seal.user.aid.unwrap()); err != nil { if u, err := sys.Uid(seal.user.identity.unwrap()); err != nil {
return err return err
} else { } else {
seal.user.uid = newInt(u) seal.user.uid = newInt(u)
@ -239,7 +236,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []hst.FilesystemConfigJSON{
// autoroot, includes the home directory // autoroot, includes the home directory
{&hst.FSBind{ {FilesystemConfig: &hst.FSBind{
Target: container.AbsFHSRoot, Target: container.AbsFHSRoot,
Source: container.AbsFHSRoot, Source: container.AbsFHSRoot,
Write: true, Write: true,
@ -312,14 +309,14 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
share := &shareHost{seal: seal, sc: sys.Paths()} share := &shareHost{seal: seal, sc: sys.Paths()}
seal.runDirPath = share.sc.RunDirPath 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) seal.sys.Ensure(share.sc.SharePath.String(), 0711)
{ {
runtimeDir := share.sc.SharePath.Append("runtime") runtimeDir := share.sc.SharePath.Append("runtime")
seal.sys.Ensure(runtimeDir.String(), 0700) seal.sys.Ensure(runtimeDir.String(), 0700)
seal.sys.UpdatePermType(system.User, runtimeDir.String(), acl.Execute) 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.Ensure(runtimeDirInst.String(), 0700)
seal.sys.UpdatePermType(system.User, runtimeDirInst.String(), acl.Read, acl.Write, acl.Execute) seal.sys.UpdatePermType(system.User, runtimeDirInst.String(), acl.Read, acl.Write, acl.Execute)
seal.container.Tmpfs(container.AbsFHSRunUser, 1<<12, 0755) seal.container.Tmpfs(container.AbsFHSRunUser, 1<<12, 0755)
@ -330,7 +327,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
tmpdir := share.sc.SharePath.Append("tmpdir") tmpdir := share.sc.SharePath.Append("tmpdir")
seal.sys.Ensure(tmpdir.String(), 0700) seal.sys.Ensure(tmpdir.String(), 0700)
seal.sys.UpdatePermType(system.User, tmpdir.String(), acl.Execute) 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.Ensure(tmpdirInst.String(), 01700)
seal.sys.UpdatePermType(system.User, tmpdirInst.String(), acl.Read, acl.Write, acl.Execute) 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 // mount inner /tmp from share so it shares persistence and storage behaviour of host /tmp

View File

@ -65,7 +65,7 @@ func ShimMain() {
if errors.Is(err, syscall.EBADF) { if errors.Is(err, syscall.EBADF) {
log.Fatal("invalid config descriptor") log.Fatal("invalid config descriptor")
} }
if errors.Is(err, container.ErrNotSet) { if errors.Is(err, container.ErrReceiveEnv) {
log.Fatal("HAKUREI_SHIM not set") log.Fatal("HAKUREI_SHIM not set")
} }

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,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

@ -9,65 +9,59 @@ import (
"hakurei.app/system/acl" "hakurei.app/system/acl"
) )
// UpdatePerm appends an ephemeral acl update Op. // UpdatePerm appends [ACLUpdateOp] to [I] with the [Process] criteria.
func (sys *I) UpdatePerm(path string, perms ...acl.Perm) *I { func (sys *I) UpdatePerm(path string, perms ...acl.Perm) *I {
sys.UpdatePermType(Process, path, perms...) sys.UpdatePermType(Process, path, perms...)
return sys return sys
} }
// UpdatePermType appends an acl update Op. // UpdatePermType appends [ACLUpdateOp] to [I].
func (sys *I) UpdatePermType(et Enablement, path string, perms ...acl.Perm) *I { func (sys *I) UpdatePermType(et Enablement, path string, perms ...acl.Perm) *I {
sys.lock.Lock() sys.ops = append(sys.ops, &ACLUpdateOp{et, path, perms})
defer sys.lock.Unlock()
sys.ops = append(sys.ops, &ACL{et, path, perms})
return sys return sys
} }
type ACL struct { // ACLUpdateOp maintains [acl.Perms] on a file until its [Enablement] is no longer satisfied.
type ACLUpdateOp struct {
et Enablement et Enablement
path string path string
perms acl.Perms 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 { func (a *ACLUpdateOp) apply(sys *I) error {
msg.Verbose("applying ACL", a) sys.verbose("applying ACL", a)
return wrapErrSuffix(acl.Update(a.path, sys.uid, a.perms...), return newOpError("acl", sys.aclUpdate(a.path, sys.uid, a.perms...), false)
fmt.Sprintf("cannot apply ACL entry to %q:", a.path))
} }
func (a *ACL) revert(sys *I, ec *Criteria) error { func (a *ACLUpdateOp) revert(sys *I, ec *Criteria) error {
if ec.hasType(a) { if ec.hasType(a.Type()) {
msg.Verbose("stripping ACL", a) sys.verbose("stripping ACL", a)
err := acl.Update(a.path, sys.uid) err := sys.aclUpdate(a.path, sys.uid)
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
// the ACL is effectively stripped if the file no longer exists // 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 err = nil
} }
return wrapErrSuffix(err, return newOpError("acl", err, true)
fmt.Sprintf("cannot strip ACL entry from %q:", a.path))
} else { } else {
msg.Verbose("skipping ACL", a) sys.verbose("skipping ACL", a)
return nil return nil
} }
} }
func (a *ACL) Is(o Op) bool { func (a *ACLUpdateOp) Is(o Op) bool {
a0, ok := o.(*ACL) target, ok := o.(*ACLUpdateOp)
return ok && a0 != nil && return ok && a != nil && target != nil &&
a.et == a0.et && a.et == target.et &&
a.path == a0.path && a.path == target.path &&
slices.Equal(a.perms, a0.perms) 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", return fmt.Sprintf("%s type: %s path: %q",
a.perms, TypeString(a.et), a.path) 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.acl_perm_t)(p),
C.size_t(len(perms)), C.size_t(len(perms)),
) )
if r == 0 { return newAclPathError(name, int(r), err)
return nil
}
return 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 package acl_test
import ( import (
"bufio"
"bytes"
"errors" "errors"
"fmt"
"io"
"os" "os"
"os/exec"
"path" "path"
"reflect" "reflect"
"strconv"
"testing" "testing"
"hakurei.app/system/acl" "hakurei.app/system/acl"
@ -17,7 +23,7 @@ var (
cred = int32(os.Geteuid()) cred = int32(os.Geteuid())
) )
func TestUpdatePerm(t *testing.T) { func TestUpdate(t *testing.T) {
if os.Getenv("GO_TEST_SKIP_ACL") == "1" { if os.Getenv("GO_TEST_SKIP_ACL") == "1" {
t.Log("acl test skipped") t.Log("acl test skipped")
t.SkipNow() t.SkipNow()
@ -48,19 +54,19 @@ func TestUpdatePerm(t *testing.T) {
t.Run("default clear mask", func(t *testing.T) { t.Run("default clear mask", func(t *testing.T) {
if err := acl.Update(testFilePath, uid); err != nil { 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 { 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) { t.Run("default clear consistency", func(t *testing.T) {
if err := acl.Update(testFilePath, uid); err != nil { 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) { 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.Run(name, func(t *testing.T) {
t.Cleanup(func() { t.Cleanup(func() {
if err := acl.Update(testFilePath, uid); err != nil { 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) { 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 { 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) r := respByCred(getfacl(t, testFilePath), fAclTypeUser, cred)
if r == nil { 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) { 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 { func getfacl(t *testing.T, name string) []*getFAclResp {
c := new(getFAclInvocation) c := new(getFAclInvocation)
if err := c.run(name); err != nil { 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, int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid,
acl_perm_t *perms, size_t plen) { acl_perm_t *perms, size_t plen) {
int ret = -1; int ret;
bool v; bool v;
int i; int i;
acl_t acl; acl_t acl;
@ -15,51 +15,70 @@ int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid,
void *qualifier_p; void *qualifier_p;
acl_permset_t permset; acl_permset_t permset;
ret = -1; /* acl_get_file */
acl = acl_get_file(path_p, ACL_TYPE_ACCESS); acl = acl_get_file(path_p, ACL_TYPE_ACCESS);
if (acl == NULL) if (acl == NULL)
goto out; goto out;
// prune entries by uid /* prune entries by uid */
for (i = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); i == 1; for (i = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); i == 1;
i = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) { i = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) {
ret = -2; /* acl_get_tag_type */
if (acl_get_tag_type(entry, &tag_type) != 0) if (acl_get_tag_type(entry, &tag_type) != 0)
return -1; goto out;
if (tag_type != ACL_USER) if (tag_type != ACL_USER)
continue; continue;
ret = -3; /* acl_get_qualifier */
qualifier_p = acl_get_qualifier(entry); qualifier_p = acl_get_qualifier(entry);
if (qualifier_p == NULL) if (qualifier_p == NULL)
return -1; goto out;
v = *(uid_t *)qualifier_p == uid; v = *(uid_t *)qualifier_p == uid;
acl_free(qualifier_p); acl_free(qualifier_p);
if (!v) if (!v)
continue; continue;
acl_delete_entry(acl, entry); ret = -4; /* acl_delete_entry */
if (acl_delete_entry(acl, entry) != 0)
goto out;
} }
if (plen == 0) if (plen == 0)
goto set; goto set;
ret = -5; /* acl_create_entry */
if (acl_create_entry(&acl, &entry) != 0) if (acl_create_entry(&acl, &entry) != 0)
goto out; goto out;
ret = -6; /* acl_get_permset */
if (acl_get_permset(entry, &permset) != 0) if (acl_get_permset(entry, &permset) != 0)
goto out; goto out;
ret = -7; /* acl_add_perm */
for (i = 0; i < plen; i++) { for (i = 0; i < plen; i++) {
if (acl_add_perm(permset, perms[i]) != 0) if (acl_add_perm(permset, perms[i]) != 0)
goto out; goto out;
} }
ret = -8; /* acl_set_tag_type */
if (acl_set_tag_type(entry, ACL_USER) != 0) if (acl_set_tag_type(entry, ACL_USER) != 0)
goto out; goto out;
ret = -9; /* acl_set_qualifier */
if (acl_set_qualifier(entry, (void *)&uid) != 0) if (acl_set_qualifier(entry, (void *)&uid) != 0)
goto out; goto out;
set: set:
ret = -10; /* acl_calc_mask */
if (acl_calc_mask(&acl) != 0) if (acl_calc_mask(&acl) != 0)
goto out; goto out;
ret = -11; /* acl_valid */
if (acl_valid(acl) != 0) if (acl_valid(acl) != 0)
goto out; goto out;
ret = -12; /* acl_set_file */
if (acl_set_file(path_p, ACL_TYPE_ACCESS, acl) == 0) if (acl_set_file(path_p, ACL_TYPE_ACCESS, acl) == 0)
ret = 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,183 @@
package system package system
import ( import (
"os"
"syscall"
"testing" "testing"
"hakurei.app/container" "hakurei.app/container/stub"
"hakurei.app/system/acl" "hakurei.app/system/acl"
) )
func TestUpdatePerm(t *testing.T) { func TestACLUpdateOp(t *testing.T) {
testCases := []struct { checkOpBehaviour(t, []opBehaviourTestCase{
path string {"apply aclUpdate", 0xdeadbeef, 0xff,
perms []acl.Perm &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),
{"/run/user/1971/hakurei", []acl.Perm{acl.Execute}}, call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xdeadbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, stub.UniqueError(1)),
{"/tmp/hakurei.1971/tmpdir/150", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, }, &OpError{Op: "acl", Err: stub.UniqueError(1)}, nil, nil},
}
for _, tc := range testCases { {"revert aclUpdate", 0xdeadbeef, 0xff,
t.Run(tc.path+permSubTestSuffix(tc.perms), func(t *testing.T) { &ACLUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{
sys := New(150) call("verbose", stub.ExpectArgs{[]any{"applying ACL", &ACLUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil),
sys.UpdatePerm(tc.path, tc.perms...) call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xdeadbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
(&tcOp{Process, tc.path}).test(t, sys.ops, []Op{&ACL{Process, tc.path, tc.perms}}, "UpdatePerm") }, 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, "UpdatePerm", []opsBuilderTestCase{
{"simple",
0xdeadbeef,
func(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{}},
})
checkOpsBuilder(t, "UpdatePermType", []opsBuilderTestCase{
{"tmpdirp", 0xdeadbeef, func(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(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(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(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(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"`},
}) })
} }
}
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
}

View File

@ -4,11 +4,14 @@ import (
"bytes" "bytes"
"context" "context"
"errors" "errors"
"fmt"
"log" "log"
"reflect"
"strings" "strings"
"sync" "sync"
"syscall" "syscall"
"hakurei.app/container"
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
) )
@ -16,6 +19,7 @@ var (
ErrDBusConfig = errors.New("dbus config not supplied") 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 { 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 { if _, err := sys.ProxyDBus(session, system, sessionPath, systemPath); err != nil {
panic(err.Error()) panic(err.Error())
@ -24,31 +28,35 @@ func (sys *I) MustProxyDBus(sessionPath string, session *dbus.Config, systemPath
} }
} }
// ProxyDBus finalises configuration and appends [DBusProxyOp] to [I].
func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath string) (func(), error) { 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 // session bus is required as otherwise this is effectively a very expensive noop
if session == nil { if session == nil {
return nil, msg.WrapErr(ErrDBusConfig, return nil, newOpErrorMessage("dbus", ErrDBusConfig,
"attempted to create message bus proxy args without session bus config") "attempted to create message bus proxy args without session bus config", false)
} }
// system bus is optional // system bus is optional
d.system = system != nil d.system = system != nil
d.sessionBus[0], d.systemBus[0] = dbus.Address() var sessionBus, systemBus dbus.ProxyPair
d.sessionBus[1], d.systemBus[1] = sessionPath, systemPath sessionBus[0], systemBus[0] = dbus.Address()
d.out = &scanToFmsg{msg: new(strings.Builder)} sessionBus[1], systemBus[1] = sessionPath, systemPath
if final, err := dbus.Finalise(d.sessionBus, d.systemBus, session, system); err != nil { d.out = &linePrefixWriter{println: log.Println, prefix: "(dbus) ", msg: new(strings.Builder)}
if final, err := dbus.Finalise(sessionBus, systemBus, session, system); err != nil {
if errors.Is(err, syscall.EINVAL) { 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 { } else {
if msg.IsVerbose() { if msg.IsVerbose() {
msg.Verbose("session bus proxy:", session.Args(d.sessionBus)) msg.Verbose("session bus proxy:", session.Args(sessionBus))
if system != nil { if system != nil {
msg.Verbose("system bus proxy:", system.Args(d.systemBus)) msg.Verbose("system bus proxy:", system.Args(systemBus))
} }
// this calls the argsWt String method // this calls the argsWt String method
@ -62,36 +70,36 @@ func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath st
return d.out.Dump, nil return d.out.Dump, nil
} }
type DBus struct { // DBusProxyOp starts xdg-dbus-proxy via [dbus] and terminates it on revert.
// This [Op] is always [Process] scoped.
type DBusProxyOp struct {
proxy *dbus.Proxy // populated during apply proxy *dbus.Proxy // populated during apply
final *dbus.Final final *dbus.Final
out *scanToFmsg out *linePrefixWriter
// whether system bus proxy is enabled // whether system bus proxy is enabled
system bool 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 { func (d *DBusProxyOp) apply(sys *I) error {
msg.Verbosef("session bus proxy on %q for upstream %q", d.sessionBus[1], d.sessionBus[0]) msg.Verbosef("session bus proxy on %q for upstream %q", d.final.Session[1], d.final.Session[0])
if d.system { if d.system {
msg.Verbosef("system bus proxy on %q for upstream %q", d.systemBus[1], d.systemBus[0]) msg.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) d.proxy = dbus.New(sys.ctx, d.final, d.out)
if err := d.proxy.Start(); err != nil { if err := d.proxy.Start(); err != nil {
d.out.Dump() d.out.Dump()
return wrapErrSuffix(err, return newOpErrorMessage("dbus", err,
"cannot start message bus proxy:") fmt.Sprintf("cannot start message bus proxy: %v", err), false)
} }
msg.Verbose("starting message bus proxy", d.proxy) msg.Verbose("starting message bus proxy", d.proxy)
return nil return nil
} }
func (d *DBus) revert(*I, *Criteria) error { func (d *DBusProxyOp) revert(*I, *Criteria) error {
// criteria ignored here since dbus is always process-scoped // criteria ignored here since dbus is always process-scoped
msg.Verbose("terminating message bus proxy") msg.Verbose("terminating message bus proxy")
d.proxy.Close() d.proxy.Close()
@ -101,38 +109,42 @@ func (d *DBus) revert(*I, *Criteria) error {
msg.Verbose("message bus proxy canceled upstream") msg.Verbose("message bus proxy canceled upstream")
err = nil 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 { func (d *DBusProxyOp) Is(o Op) bool {
d0, ok := o.(*DBus) target, ok := o.(*DBusProxyOp)
return ok && d0 != nil && return ok && d != nil && target != nil &&
((d.proxy == nil && d0.proxy == nil) || d.system == target.system &&
(d.proxy != nil && d0.proxy != nil && d.proxy.String() == d0.proxy.String())) 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 { func (d *DBusProxyOp) Path() string { return container.Nonexistent }
return "(dbus proxy)" func (d *DBusProxyOp) String() string { return d.proxy.String() }
}
func (d *DBus) String() string { // linePrefixWriter calls println with a prefix for every line written.
return d.proxy.String() type linePrefixWriter struct {
} prefix string
println func(v ...any)
type scanToFmsg struct {
msg *strings.Builder msg *strings.Builder
msgbuf []string msgbuf []string
mu sync.RWMutex 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() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
return s.write(p, 0) 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 i := bytes.IndexByte(p, '\n'); i == -1 { if i := bytes.IndexByte(p, '\n'); i == -1 {
n, _ := s.msg.Write(p) n, _ := s.msg.Write(p)
return a + n, nil return a + n, nil
@ -142,7 +154,7 @@ func (s *scanToFmsg) write(p []byte, a int) (int, error) {
// allow container init messages through // allow container init messages through
v := s.msg.String() v := s.msg.String()
if strings.HasPrefix(v, "init: ") { if strings.HasPrefix(v, "init: ") {
log.Println("(dbus) " + v) s.println(s.prefix + v)
} else { } else {
s.msgbuf = append(s.msgbuf, v) s.msgbuf = append(s.msgbuf, v)
} }
@ -152,10 +164,10 @@ func (s *scanToFmsg) write(p []byte, a int) (int, error) {
} }
} }
func (s *scanToFmsg) Dump() { func (s *linePrefixWriter) Dump() {
s.mu.RLock() s.mu.RLock()
for _, msg := range s.msgbuf { for _, m := range s.msgbuf {
log.Println("(dbus) " + msg) s.println(s.prefix + m)
} }
s.mu.RUnlock() s.mu.RUnlock()
} }

View File

@ -13,6 +13,13 @@ type AddrEntry struct {
Values [][2]string `json:"values"` 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 // Parse parses D-Bus address according to
// https://dbus.freedesktop.org/doc/dbus-specification.html#addresses // https://dbus.freedesktop.org/doc/dbus-specification.html#addresses
func Parse(addr []byte) ([]AddrEntry, error) { 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) }) t.Run("direct", func(t *testing.T) { testProxyFinaliseStartWaitCloseString(t, false) })
} }
const (
stubProxyTimeout = 30 * time.Second
)
func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) { func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) {
{ {
oldWaitDelay := helper.WaitDelay oldWaitDelay := helper.WaitDelay
@ -118,7 +122,8 @@ func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) {
} }
}) })
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) t.Run("run", func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), stubProxyTimeout)
defer cancel() defer cancel()
output := new(strings.Builder) output := new(strings.Builder)
if !useSandbox { if !useSandbox {
@ -144,7 +149,6 @@ func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) {
} }
}) })
t.Run("start", func(t *testing.T) {
if err := p.Start(); err != nil { if err := p.Start(); err != nil {
t.Fatalf("Start: error = %v", t.Fatalf("Start: error = %v",
err) err)

30
system/dispatcher.go Normal file
View File

@ -0,0 +1,30 @@
package system
import "hakurei.app/system/acl"
// syscallDispatcher provides methods that make state-dependent system calls as part of their behaviour.
// syscallDispatcher is embedded in [I], so all methods must be unexported.
type syscallDispatcher interface {
// new starts a goroutine with a new instance of syscallDispatcher.
// A syscallDispatcher must never be used in any goroutine other than the one owning it,
// just synchronising access is not enough, as this is for test instrumentation.
new(f func(k syscallDispatcher))
// aclUpdate provides [acl.Update].
aclUpdate(name string, uid int, perms ...acl.Perm) error
verbose(v ...any)
verbosef(format string, v ...any)
}
// direct implements syscallDispatcher on the current kernel.
type direct struct{}
func (k direct) new(f func(k syscallDispatcher)) { go f(k) }
func (k direct) aclUpdate(name string, uid int, perms ...acl.Perm) error {
return acl.Update(name, uid, perms...)
}
func (direct) verbose(v ...any) { msg.Verbose(v...) }
func (direct) verbosef(format string, v ...any) { msg.Verbosef(format, v...) }

215
system/dispatcher_test.go Normal file
View File

@ -0,0 +1,215 @@
package system
import (
"reflect"
"slices"
"testing"
"hakurei.app/container/stub"
"hakurei.app/system/acl"
)
// 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 opBehaviourTestCase struct {
name string
uid int
ec Enablement
op Op
apply []stub.Call
wantErrApply error
revert []stub.Call
wantErrRevert 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) {
t.Helper()
var ec *Criteria
if tc.ec != 0xff {
ec = (*Criteria)(&tc.ec)
}
sys, s := InternalNew(t, stub.Expect{Calls: slices.Concat(tc.apply, []stub.Call{{Name: stub.CallSeparator}}, tc.revert)}, tc.uid)
defer stub.HandleExit(t)
errApply := tc.op.apply(sys)
s.Expects(stub.CallSeparator)
if !reflect.DeepEqual(errApply, tc.wantErrApply) {
t.Errorf("apply: error = %v, want %v", errApply, tc.wantErrApply)
}
if errApply != nil {
goto out
}
if err := tc.op.revert(sys, ec); !reflect.DeepEqual(err, tc.wantErrRevert) {
t.Errorf("revert: error = %v, want %v", err, tc.wantErrRevert)
}
out:
s.VisitIncomplete(func(s *stub.Stub[syscallDispatcher]) {
count := s.Pos() - 1 // separator
if count < len(tc.apply) {
t.Errorf("apply: %d calls, want %d", count, len(tc.apply))
} else {
t.Errorf("revert: %d calls, want %d", count-len(tc.apply), len(tc.revert))
}
})
})
}
})
}
type opsBuilderTestCase struct {
name string
uid int
f func(sys *I)
want []Op
exp stub.Expect
}
func checkOpsBuilder(t *testing.T, fname string, 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()
sys, s := InternalNew(t, tc.exp, tc.uid)
defer stub.HandleExit(t)
tc.f(sys)
s.VisitIncomplete(func(s *stub.Stub[syscallDispatcher]) {
t.Helper()
t.Errorf("%s: %d calls, want %d", fname, s.Pos(), s.Len())
})
if !slices.EqualFunc(sys.ops, tc.want, func(op Op, v Op) bool { return op.Is(v) }) {
t.Errorf("ops: %#v, want %#v", sys.ops, tc.want)
}
})
}
})
}
type opIsTestCase struct {
name string
op, v Op
want bool
}
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)
}
})
}
})
}
type opMetaTestCase struct {
name string
op Op
wantType Enablement
wantPath string
wantString string
}
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("type", func(t *testing.T) {
t.Helper()
if got := tc.op.Type(); got != tc.wantType {
t.Errorf("Type: %q, want %q", got, tc.wantType)
}
})
t.Run("path", func(t *testing.T) {
t.Helper()
if got := tc.op.Path(); got != tc.wantPath {
t.Errorf("Path: %q, want %q", got, tc.wantPath)
}
})
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)
}
})
})
}
})
}
// InternalNew initialises [I] with a stub syscallDispatcher.
func InternalNew(t *testing.T, want stub.Expect, uid int) (*I, *stub.Stub[syscallDispatcher]) {
k := stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{s} }, want)
sys := New(t.Context(), uid)
sys.syscallDispatcher = &kstub{k}
return sys, k
}
type kstub struct{ *stub.Stub[syscallDispatcher] }
func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) }
func (k *kstub) aclUpdate(name string, uid int, perms ...acl.Perm) error {
k.Helper()
return k.Expects("aclUpdate").Error(
stub.CheckArg(k.Stub, "name", name, 0),
stub.CheckArg(k.Stub, "uid", uid, 1),
stub.CheckArgReflect(k.Stub, "perms", perms, 2))
}
func (k *kstub) verbose(v ...any) {
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) {
k.Helper()
if k.Expects("verbosef").Error(
stub.CheckArg(k.Stub, "format", format, 0),
stub.CheckArgReflect(k.Stub, "v", v, 1)) != nil {
k.FailNow()
}
}

View File

@ -5,7 +5,7 @@ import (
"strings" "strings"
) )
// Enablement represents optional system resources. // Enablement represents an optional host service to export to the target user.
type Enablement byte type Enablement byte
const ( const (

View File

@ -1,21 +1,16 @@
// Package xcb implements X11 ChangeHosts via libxcb.
package xcb package xcb
import ( import "errors"
"errors"
)
var ErrChangeHosts = errors.New("xcb_change_hosts() failed") var ErrChangeHosts = errors.New("xcb_change_hosts() failed")
func ChangeHosts(mode HostMode, family Family, address string) error { func ChangeHosts(mode HostMode, family Family, address string) error {
var conn *connection conn := new(connection)
if err := conn.connect(); err != nil {
if c, err := connect(); err != nil { conn.disconnect()
c.disconnect()
return err return err
} else { } else {
defer c.disconnect() defer conn.disconnect()
conn = c
} }
return conn.changeHostsChecked(mode, family, address) return conn.changeHostsChecked(mode, family, address)

View File

@ -1,3 +1,4 @@
// Package xcb implements X11 ChangeHosts via libxcb.
package xcb package xcb
import ( import (
@ -11,22 +12,28 @@ import (
#include <stdlib.h> #include <stdlib.h>
#include <xcb/xcb.h> #include <xcb/xcb.h>
static int _go_xcb_change_hosts_checked(xcb_connection_t *c, uint8_t mode, uint8_t family, uint16_t address_len, const uint8_t *address) { static int hakurei_xcb_change_hosts_checked(xcb_connection_t *c,
xcb_void_cookie_t cookie = xcb_change_hosts_checked(c, mode, family, address_len, address); uint8_t mode, uint8_t family,
uint16_t address_len, const uint8_t *address) {
int ret;
xcb_generic_error_t *e;
xcb_void_cookie_t cookie;
cookie = xcb_change_hosts_checked(c, mode, family, address_len, address);
free((void *)address); free((void *)address);
int errno = xcb_connection_has_error(c); ret = xcb_connection_has_error(c);
if (errno != 0) if (ret != 0)
return errno; return ret;
xcb_generic_error_t *e = xcb_request_check(c, cookie); e = xcb_request_check(c, cookie);
if (e != NULL) { if (e != NULL) {
// don't want to deal with xcb errors // don't want to deal with xcb errors
free((void *)e); free((void *)e);
return -1; ret = -1;
} }
return 0; return ret;
} }
*/ */
import "C" import "C"
@ -48,49 +55,60 @@ type (
) )
func (conn *connection) changeHostsChecked(mode HostMode, family Family, address string) error { func (conn *connection) changeHostsChecked(mode HostMode, family Family, address string) error {
errno := C._go_xcb_change_hosts_checked( ret := C.hakurei_xcb_change_hosts_checked(
conn.c, conn.c,
C.uint8_t(mode), C.uint8_t(mode),
C.uint8_t(family), C.uint8_t(family),
C.uint16_t(len(address)), C.uint16_t(len(address)),
(*C.uint8_t)(unsafe.Pointer(C.CString(address))), (*C.uint8_t)(unsafe.Pointer(C.CString(address))),
) )
switch errno { switch ret {
case 0: case 0:
return nil return nil
case -1: case -1:
return ErrChangeHosts return ErrChangeHosts
default: default:
return &ConnectionError{errno} return ConnectionError(ret)
} }
} }
type connection struct{ c *C.xcb_connection_t } type connection struct{ c *C.xcb_connection_t }
func connect() (*connection, error) { func (conn *connection) connect() error {
conn := newConnection(C.xcb_connect(nil, nil)) conn.c = C.xcb_connect(nil, nil)
return conn, conn.hasError() runtime.SetFinalizer(conn, (*connection).disconnect)
return conn.hasError()
} }
func newConnection(c *C.xcb_connection_t) *connection { func (conn *connection) hasError() error {
conn := &connection{c} ret := C.xcb_connection_has_error(conn.c)
runtime.SetFinalizer(conn, (*connection).disconnect) if ret == 0 {
return conn return nil
}
return ConnectionError(ret)
}
func (conn *connection) disconnect() {
C.xcb_disconnect(conn.c)
// no need for a finalizer anymore
runtime.SetFinalizer(conn, nil)
} }
const ( const (
ConnError = C.XCB_CONN_ERROR ConnError ConnectionError = C.XCB_CONN_ERROR
ConnClosedExtNotSupported = C.XCB_CONN_CLOSED_EXT_NOTSUPPORTED ConnClosedExtNotSupported ConnectionError = C.XCB_CONN_CLOSED_EXT_NOTSUPPORTED
ConnClosedMemInsufficient = C.XCB_CONN_CLOSED_MEM_INSUFFICIENT ConnClosedMemInsufficient ConnectionError = C.XCB_CONN_CLOSED_MEM_INSUFFICIENT
ConnClosedReqLenExceed = C.XCB_CONN_CLOSED_REQ_LEN_EXCEED ConnClosedReqLenExceed ConnectionError = C.XCB_CONN_CLOSED_REQ_LEN_EXCEED
ConnClosedParseErr = C.XCB_CONN_CLOSED_PARSE_ERR ConnClosedParseErr ConnectionError = C.XCB_CONN_CLOSED_PARSE_ERR
ConnClosedInvalidScreen = C.XCB_CONN_CLOSED_INVALID_SCREEN ConnClosedInvalidScreen ConnectionError = C.XCB_CONN_CLOSED_INVALID_SCREEN
) )
type ConnectionError struct{ errno C.int } // ConnectionError represents an error returned by xcb_connection_has_error.
type ConnectionError int
func (ce *ConnectionError) Error() string { func (ce ConnectionError) Error() string {
switch ce.errno { switch ce {
case ConnError: case ConnError:
return "connection error" return "connection error"
case ConnClosedExtNotSupported: case ConnClosedExtNotSupported:
@ -107,18 +125,3 @@ func (ce *ConnectionError) Error() string {
return "generic X11 failure" return "generic X11 failure"
} }
} }
func (conn *connection) hasError() error {
errno := C.xcb_connection_has_error(conn.c)
if errno == 0 {
return nil
}
return &ConnectionError{errno}
}
func (conn *connection) disconnect() {
C.xcb_disconnect(conn.c)
// no need for a finalizer anymore
runtime.SetFinalizer(conn, nil)
}

View File

@ -5,43 +5,42 @@ import (
"os" "os"
) )
// Link registers an Op that links dst to src. // Link appends [HardlinkOp] to [I] the [Process] criteria.
func (sys *I) Link(oldname, newname string) *I { return sys.LinkFileType(Process, oldname, newname) } func (sys *I) Link(oldname, newname string) *I { return sys.LinkFileType(Process, oldname, newname) }
// LinkFileType registers a file linking Op labelled with type et. // LinkFileType appends [HardlinkOp] to [I].
func (sys *I) LinkFileType(et Enablement, oldname, newname string) *I { func (sys *I) LinkFileType(et Enablement, oldname, newname string) *I {
sys.lock.Lock() sys.ops = append(sys.ops, &HardlinkOp{et, newname, oldname})
defer sys.lock.Unlock()
sys.ops = append(sys.ops, &Hardlink{et, newname, oldname})
return sys return sys
} }
type Hardlink struct { // HardlinkOp maintains a hardlink until its [Enablement] is no longer satisfied.
type HardlinkOp struct {
et Enablement et Enablement
dst, src string dst, src string
} }
func (l *Hardlink) Type() Enablement { return l.et } func (l *HardlinkOp) Type() Enablement { return l.et }
func (l *Hardlink) apply(*I) error { func (l *HardlinkOp) apply(*I) error {
msg.Verbose("linking", l) msg.Verbose("linking", l)
return wrapErrSuffix(os.Link(l.src, l.dst), return newOpError("hardlink", os.Link(l.src, l.dst), false)
fmt.Sprintf("cannot link %q:", l.dst))
} }
func (l *Hardlink) revert(_ *I, ec *Criteria) error { func (l *HardlinkOp) revert(_ *I, ec *Criteria) error {
if ec.hasType(l) { if ec.hasType(l.Type()) {
msg.Verbosef("removing hard link %q", l.dst) msg.Verbosef("removing hard link %q", l.dst)
return wrapErrSuffix(os.Remove(l.dst), return newOpError("hardlink", os.Remove(l.dst), true)
fmt.Sprintf("cannot remove hard link %q:", l.dst))
} else { } else {
msg.Verbosef("skipping hard link %q", l.dst) msg.Verbosef("skipping hard link %q", l.dst)
return nil return nil
} }
} }
func (l *Hardlink) Is(o Op) bool { l0, ok := o.(*Hardlink); return ok && l0 != nil && *l == *l0 } func (l *HardlinkOp) Is(o Op) bool {
func (l *Hardlink) Path() string { return l.src } target, ok := o.(*HardlinkOp)
func (l *Hardlink) String() string { return fmt.Sprintf("%q from %q", l.dst, l.src) } return ok && l != nil && target != nil && *l == *target
}
func (l *HardlinkOp) Path() string { return l.src }
func (l *HardlinkOp) String() string { return fmt.Sprintf("%q from %q", l.dst, l.src) }

View File

@ -6,78 +6,67 @@ import (
"os" "os"
) )
// Ensure the existence and mode of a directory. // Ensure appends [MkdirOp] to [I] with its [Enablement] ignored.
func (sys *I) Ensure(name string, perm os.FileMode) *I { func (sys *I) Ensure(name string, perm os.FileMode) *I {
sys.lock.Lock() sys.ops = append(sys.ops, &MkdirOp{User, name, perm, false})
defer sys.lock.Unlock()
sys.ops = append(sys.ops, &Mkdir{User, name, perm, false})
return sys return sys
} }
// Ephemeral ensures the temporary existence and mode of a directory through the life of et. // Ephemeral appends an ephemeral [MkdirOp] to [I].
func (sys *I) Ephemeral(et Enablement, name string, perm os.FileMode) *I { func (sys *I) Ephemeral(et Enablement, name string, perm os.FileMode) *I {
sys.lock.Lock() sys.ops = append(sys.ops, &MkdirOp{et, name, perm, true})
defer sys.lock.Unlock()
sys.ops = append(sys.ops, &Mkdir{et, name, perm, true})
return sys return sys
} }
type Mkdir struct { // MkdirOp ensures the existence of a directory.
// For ephemeral, the directory is destroyed once [Enablement] is no longer satisfied.
type MkdirOp struct {
et Enablement et Enablement
path string path string
perm os.FileMode perm os.FileMode
ephemeral bool ephemeral bool
} }
func (m *Mkdir) Type() Enablement { func (m *MkdirOp) Type() Enablement { return m.et }
return m.et
}
func (m *Mkdir) apply(*I) error { func (m *MkdirOp) apply(*I) error {
msg.Verbose("ensuring directory", m) msg.Verbose("ensuring directory", m)
// create directory // create directory
err := os.Mkdir(m.path, m.perm) if err := os.Mkdir(m.path, m.perm); err != nil {
if !errors.Is(err, os.ErrExist) { if !errors.Is(err, os.ErrExist) {
return wrapErrSuffix(err, return newOpError("mkdir", err, false)
fmt.Sprintf("cannot create directory %q:", m.path))
} }
// directory exists, ensure mode // directory exists, ensure mode
return wrapErrSuffix(os.Chmod(m.path, m.perm), return newOpError("mkdir", os.Chmod(m.path, m.perm), false)
fmt.Sprintf("cannot change mode of %q to %s:", m.path, m.perm)) } else {
return nil
}
} }
func (m *Mkdir) revert(_ *I, ec *Criteria) error { func (m *MkdirOp) revert(_ *I, ec *Criteria) error {
if !m.ephemeral { if !m.ephemeral {
// skip non-ephemeral dir and do not log anything // skip non-ephemeral dir and do not log anything
return nil return nil
} }
if ec.hasType(m) { if ec.hasType(m.Type()) {
msg.Verbose("destroying ephemeral directory", m) msg.Verbose("destroying ephemeral directory", m)
return wrapErrSuffix(os.Remove(m.path), return newOpError("mkdir", os.Remove(m.path), true)
fmt.Sprintf("cannot remove ephemeral directory %q:", m.path))
} else { } else {
msg.Verbose("skipping ephemeral directory", m) msg.Verbose("skipping ephemeral directory", m)
return nil return nil
} }
} }
func (m *Mkdir) Is(o Op) bool { func (m *MkdirOp) Is(o Op) bool {
m0, ok := o.(*Mkdir) target, ok := o.(*MkdirOp)
return ok && m0 != nil && *m == *m0 return ok && m != nil && target != nil && *m == *target
} }
func (m *Mkdir) Path() string { func (m *MkdirOp) Path() string { return m.path }
return m.path
}
func (m *Mkdir) String() string { func (m *MkdirOp) String() string {
t := "ensure" t := "ensure"
if m.ephemeral { if m.ephemeral {
t = TypeString(m.Type()) t = TypeString(m.Type())

View File

@ -19,9 +19,9 @@ func TestEnsure(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name+"_"+tc.perm.String(), func(t *testing.T) { t.Run(tc.name+"_"+tc.perm.String(), func(t *testing.T) {
sys := New(150) sys := New(t.Context(), 150)
sys.Ensure(tc.name, tc.perm) sys.Ensure(tc.name, tc.perm)
(&tcOp{User, tc.name}).test(t, sys.ops, []Op{&Mkdir{User, tc.name, tc.perm, false}}, "Ensure") (&tcOp{User, tc.name}).test(t, sys.ops, []Op{&MkdirOp{User, tc.name, tc.perm, false}}, "Ensure")
}) })
} }
} }
@ -36,9 +36,9 @@ func TestEphemeral(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.path+"_"+tc.perm.String()+"_"+TypeString(tc.et), func(t *testing.T) { t.Run(tc.path+"_"+tc.perm.String()+"_"+TypeString(tc.et), func(t *testing.T) {
sys := New(150) sys := New(t.Context(), 150)
sys.Ephemeral(tc.et, tc.path, tc.perm) sys.Ephemeral(tc.et, tc.path, tc.perm)
tc.test(t, sys.ops, []Op{&Mkdir{tc.et, tc.path, tc.perm, true}}, "Ephemeral") tc.test(t, sys.ops, []Op{&MkdirOp{tc.et, tc.path, tc.perm, true}}, "Ephemeral")
}) })
} }
} }
@ -60,7 +60,7 @@ func TestMkdirString(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.want, func(t *testing.T) { t.Run(tc.want, func(t *testing.T) {
m := &Mkdir{ m := &MkdirOp{
et: tc.et, et: tc.et,
path: container.Nonexistent, path: container.Nonexistent,
perm: 0701, perm: 0701,

View File

@ -1,68 +0,0 @@
package system
import "testing"
type tcOp struct {
et Enablement
path string
}
// test an instance of the Op interface
func (ptc tcOp) test(t *testing.T, gotOps []Op, wantOps []Op, fn string) {
if len(gotOps) != len(wantOps) {
t.Errorf("%s: inserted %v Ops, want %v", fn,
len(gotOps), len(wantOps))
return
}
t.Run("path", func(t *testing.T) {
if len(gotOps) > 0 {
if got := gotOps[0].Path(); got != ptc.path {
t.Errorf("Path() = %q, want %q",
got, ptc.path)
return
}
}
})
for i := range gotOps {
o := gotOps[i]
t.Run("is", func(t *testing.T) {
if !o.Is(o) {
t.Errorf("Is returned false on self")
return
}
if !o.Is(wantOps[i]) {
t.Errorf("%s: inserted %#v, want %#v",
fn,
o, wantOps[i])
return
}
})
t.Run("criteria", func(t *testing.T) {
testCases := []struct {
name string
ec *Criteria
want bool
}{
{"nil", nil, ptc.et != User},
{"self", newCriteria(ptc.et), true},
{"all", newCriteria(EWayland | EX11 | EDBus | EPulse | User | Process), true},
{"enablements", newCriteria(EWayland | EX11 | EDBus | EPulse), ptc.et != User && ptc.et != Process},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := tc.ec.hasType(o); got != tc.want {
t.Errorf("hasType: got %v, want %v",
got, tc.want)
}
})
}
})
}
}
func newCriteria(e Enablement) *Criteria { return (*Criteria)(&e) }

View File

@ -1,131 +0,0 @@
package system_test
import (
"strconv"
"testing"
"hakurei.app/system"
)
func TestNew(t *testing.T) {
testCases := []struct {
uid int
}{
{150},
{149},
{148},
{147},
}
for _, tc := range testCases {
t.Run("sys initialised with uid "+strconv.Itoa(tc.uid), func(t *testing.T) {
if got := system.New(tc.uid); got.UID() != tc.uid {
t.Errorf("New(%d) uid = %d, want %d",
tc.uid,
got.UID(), tc.uid)
}
})
}
}
func TestTypeString(t *testing.T) {
testCases := []struct {
e system.Enablement
want string
}{
{system.EWayland, system.EWayland.String()},
{system.EX11, system.EX11.String()},
{system.EDBus, system.EDBus.String()},
{system.EPulse, system.EPulse.String()},
{system.User, "user"},
{system.Process, "process"},
{system.User | system.Process, "user, process"},
{system.EWayland | system.User | system.Process, "wayland, user, process"},
{system.EX11 | system.Process, "x11, process"},
}
for _, tc := range testCases {
t.Run("label type string "+tc.want, func(t *testing.T) {
if got := system.TypeString(tc.e); got != tc.want {
t.Errorf("TypeString: %q, want %q",
got, tc.want)
}
})
}
}
func TestI_Equal(t *testing.T) {
testCases := []struct {
name string
sys *system.I
v *system.I
want bool
}{
{
"simple UID",
system.New(150),
system.New(150),
true,
},
{
"simple UID differ",
system.New(150),
system.New(151),
false,
},
{
"simple UID nil",
system.New(150),
nil,
false,
},
{
"op length mismatch",
system.New(150).
ChangeHosts("chronos"),
system.New(150).
ChangeHosts("chronos").
Ensure("/run", 0755),
false,
},
{
"op value mismatch",
system.New(150).
ChangeHosts("chronos").
Ensure("/run", 0644),
system.New(150).
ChangeHosts("chronos").
Ensure("/run", 0755),
false,
},
{
"op type mismatch",
system.New(150).
ChangeHosts("chronos").
CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 0, 256),
system.New(150).
ChangeHosts("chronos").
Ensure("/run", 0755),
false,
},
{
"op equals",
system.New(150).
ChangeHosts("chronos").
Ensure("/run", 0755),
system.New(150).
ChangeHosts("chronos").
Ensure("/run", 0755),
true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.sys.Equal(tc.v) != tc.want {
t.Errorf("Equal: got %v; want %v",
!tc.want, tc.want)
}
})
}
}

View File

@ -1,6 +1,10 @@
package system package system
import ( import (
"errors"
"net"
"os"
"hakurei.app/container" "hakurei.app/container"
) )
@ -14,9 +18,79 @@ func SetOutput(v container.Msg) {
} }
} }
func wrapErrSuffix(err error, a ...any) error { // OpError is returned by [I.Commit] and [I.Revert].
type OpError struct {
Op string
Err error
Msg string
Revert bool
}
func (e *OpError) Unwrap() error { return e.Err }
func (e *OpError) Error() string {
if e.Msg != "" {
return e.Msg
}
switch {
case errors.As(e.Err, new(*os.PathError)),
errors.As(e.Err, new(*net.OpError)),
errors.As(e.Err, new(*container.StartError)):
return e.Err.Error()
default:
if !e.Revert {
return "apply " + e.Op + ": " + e.Err.Error()
} else {
return "revert " + e.Op + ": " + e.Err.Error()
}
}
}
func (e *OpError) Message() string {
switch {
case e.Msg != "":
return e.Error()
default:
return "cannot " + e.Error()
}
}
// newOpError returns an [OpError] without a message string.
func newOpError(op string, err error, revert bool) error {
if err == nil { if err == nil {
return nil return nil
} }
return msg.WrapErr(err, append(a, err)...) return &OpError{op, err, "", revert}
}
// newOpErrorMessage returns an [OpError] with an overriding message string.
func newOpErrorMessage(op string, err error, message string, revert bool) error {
if err == nil {
return nil
}
return &OpError{op, err, message, revert}
}
func printJoinedError(println func(v ...any), fallback string, err error) {
var joinErr interface {
Unwrap() []error
error
}
if !errors.As(err, &joinErr) {
if m, ok := container.GetErrorMessage(err); ok {
println(m)
} else {
println(fallback, err)
}
} else {
for _, err = range joinErr.Unwrap() {
if m, ok := container.GetErrorMessage(err); ok {
println(m)
} else {
println(err.Error())
}
}
}
} }

235
system/output_test.go Normal file
View File

@ -0,0 +1,235 @@
package system
import (
"errors"
"net"
"os"
"reflect"
"syscall"
"testing"
"hakurei.app/container"
"hakurei.app/internal/hlog"
)
func TestOpError(t *testing.T) {
testCases := []struct {
name string
err error
s string
is error
isF error
msg string
}{
{"message", newOpErrorMessage("dbus", ErrDBusConfig,
"attempted to create message bus proxy args without session bus config", false),
"attempted to create message bus proxy args without session bus config",
ErrDBusConfig, syscall.ENOTRECOVERABLE,
"attempted to create message bus proxy args without session bus config"},
{"apply", newOpError("tmpfile", syscall.EBADE, false),
"apply tmpfile: invalid exchange",
syscall.EBADE, syscall.EBADF,
"cannot apply tmpfile: invalid exchange"},
{"revert", newOpError("wayland", syscall.EBADF, true),
"revert wayland: bad file descriptor",
syscall.EBADF, syscall.EBADE,
"cannot revert wayland: bad file descriptor"},
{"path", newOpError("tmpfile", &os.PathError{Op: "stat", Path: "/run/dbus", Err: syscall.EISDIR}, false),
"stat /run/dbus: is a directory",
syscall.EISDIR, syscall.ENOTDIR,
"cannot stat /run/dbus: is a directory"},
{"net", newOpError("wayland", &net.OpError{Op: "dial", Net: "unix", Addr: &net.UnixAddr{Name: "/run/user/1000/wayland-1", Net: "unix"}, Err: syscall.ENOENT}, false),
"dial unix /run/user/1000/wayland-1: no such file or directory",
syscall.ENOENT, syscall.EPERM,
"cannot dial unix /run/user/1000/wayland-1: no such file or directory"},
}
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.Error("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)
}
})
})
}
t.Run("new", func(t *testing.T) {
if err := newOpError("check", nil, false); err != nil {
t.Errorf("newOpError: %v", err)
}
if err := newOpErrorMessage("check", nil, "", false); err != nil {
t.Errorf("newOpErrorMessage: %v", err)
}
})
}
func TestSetOutput(t *testing.T) {
oldmsg := msg
t.Cleanup(func() { msg = oldmsg })
msg = nil
t.Run("nil", func(t *testing.T) {
SetOutput(nil)
if _, ok := msg.(*container.DefaultMsg); !ok {
t.Errorf("SetOutput: %#v", msg)
}
})
t.Run("hlog", func(t *testing.T) {
SetOutput(hlog.Output{})
if _, ok := msg.(hlog.Output); !ok {
t.Errorf("SetOutput: %#v", msg)
}
})
t.Run("reset", func(t *testing.T) {
SetOutput(nil)
if _, ok := msg.(*container.DefaultMsg); !ok {
t.Errorf("SetOutput: %#v", msg)
}
})
}
func TestPrintJoinedError(t *testing.T) {
testCases := []struct {
name string
err error
want [][]any
}{
{"nil", nil, [][]any{{"not a joined error:", nil}}},
{"single", errors.Join(syscall.EINVAL), [][]any{{"invalid argument"}}},
{"unwrapped", syscall.EINVAL, [][]any{{"not a joined error:", syscall.EINVAL}}},
{"unwrapped message", &OpError{
Op: "meow",
Err: syscall.EBADFD,
}, [][]any{
{"cannot apply meow: file descriptor in bad state"},
}},
{"many", errors.Join(syscall.ENOTRECOVERABLE, syscall.ETIMEDOUT, syscall.EBADFD), [][]any{
{"state not recoverable"},
{"connection timed out"},
{"file descriptor in bad state"},
}},
{"many message", errors.Join(
&container.StartError{
Step: "meow",
Err: syscall.ENOMEM,
},
&os.PathError{
Op: "meow",
Path: "/proc/nonexistent",
Err: syscall.ENOSYS,
},
&OpError{
Op: "meow",
Err: syscall.ENODEV,
Revert: true,
}), [][]any{
{"cannot meow: cannot allocate memory"},
{"meow /proc/nonexistent: function not implemented"},
{"cannot revert meow: no such device"},
}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var got [][]any
printJoinedError(func(v ...any) { got = append(got, v) }, "not a joined error:", tc.err)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("printJoinedError: %#v, want %#v", got, tc.want)
}
})
}
}
type tcOp struct {
et Enablement
path string
}
// test an instance of the Op interface
func (ptc tcOp) test(t *testing.T, gotOps []Op, wantOps []Op, fn string) {
if len(gotOps) != len(wantOps) {
t.Errorf("%s: inserted %v Ops, want %v", fn,
len(gotOps), len(wantOps))
return
}
t.Run("path", func(t *testing.T) {
if len(gotOps) > 0 {
if got := gotOps[0].Path(); got != ptc.path {
t.Errorf("Path() = %q, want %q",
got, ptc.path)
return
}
}
})
for i := range gotOps {
o := gotOps[i]
t.Run("is", func(t *testing.T) {
if !o.Is(o) {
t.Errorf("Is returned false on self")
return
}
if !o.Is(wantOps[i]) {
t.Errorf("%s: inserted %#v, want %#v",
fn,
o, wantOps[i])
return
}
})
t.Run("criteria", func(t *testing.T) {
testCases := []struct {
name string
ec Enablement
want bool
}{
{"nil", 0xff, ptc.et != User},
{"self", ptc.et, true},
{"all", EWayland | EX11 | EDBus | EPulse | User | Process, true},
{"enablements", EWayland | EX11 | EDBus | EPulse, ptc.et != User && ptc.et != Process},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var criteria *Criteria
if tc.ec != 0xff {
criteria = (*Criteria)(&tc.ec)
}
if got := criteria.hasType(o.Type()); got != tc.want {
t.Errorf("hasType: got %v, want %v",
got, tc.want)
}
})
}
})
}
}

View File

@ -1,4 +1,4 @@
// Package system provides tools for safely interacting with the operating system. // Package system provides helpers to apply and revert groups of operations to the system.
package system package system
import ( import (
@ -6,11 +6,10 @@ import (
"errors" "errors"
"log" "log"
"strings" "strings"
"sync"
) )
const ( const (
// User type is reverted at final launcher exit. // User type is reverted at final instance exit.
User = EM << iota User = EM << iota
// Process type is unconditionally reverted on exit. // Process type is unconditionally reverted on exit.
Process Process
@ -21,23 +20,21 @@ const (
// Criteria specifies types of Op to revert. // Criteria specifies types of Op to revert.
type Criteria Enablement type Criteria Enablement
func (ec *Criteria) hasType(o Op) bool { func (ec *Criteria) hasType(t Enablement) bool {
// nil criteria: revert everything except User // nil criteria: revert everything except User
if ec == nil { if ec == nil {
return o.Type() != User return t != User
} }
return Enablement(*ec)&o.Type() != 0 return Enablement(*ec)&t != 0
} }
// Op is a reversible system operation. // Op is a reversible system operation.
type Op interface { type Op interface {
// Type returns Op's enablement type. // Type returns [Op]'s enablement type, for matching a revert criteria.
Type() Enablement Type() Enablement
// apply the Op
apply(sys *I) error apply(sys *I) error
// revert reverses the Op if criteria is met
revert(sys *I, ec *Criteria) error revert(sys *I, ec *Criteria) error
Is(o Op) bool Is(o Op) bool
@ -45,7 +42,7 @@ type Op interface {
String() string String() string
} }
// TypeString returns the string representation of a type stored as an [Enablement]. // TypeString extends [Enablement.String] to support [User] and [Process].
func TypeString(e Enablement) string { func TypeString(e Enablement) string {
switch e { switch e {
case User: case User:
@ -68,35 +65,41 @@ func TypeString(e Enablement) string {
} }
} }
// New initialises sys with no-op verbose functions. // New returns the address of a new [I] targeting uid.
func New(uid int) (sys *I) { func New(ctx context.Context, uid int) (sys *I) {
sys = new(I) if ctx == nil || uid < 0 {
sys.uid = uid panic("invalid call to New")
return }
return &I{ctx: ctx, uid: uid, syscallDispatcher: direct{}}
} }
// An I provides indirect bulk operating system interaction. I must not be copied. // An I provides deferred operating system interaction. [I] must not be copied.
// Methods of [I] must not be used concurrently.
type I struct { type I struct {
_ noCopy
uid int uid int
ops []Op ops []Op
ctx context.Context ctx context.Context
// whether sys has been reverted // the behaviour of Commit is only defined for up to one call
state bool committed bool
// the behaviour of Revert is only defined for up to one call
reverted bool
lock sync.Mutex syscallDispatcher
} }
func (sys *I) UID() int { return sys.uid } func (sys *I) UID() int { return sys.uid }
// Equal returns whether all [Op] instances held by v is identical to that of sys. // Equal returns whether all [Op] instances held by sys matches that of target.
func (sys *I) Equal(v *I) bool { func (sys *I) Equal(target *I) bool {
if v == nil || sys.uid != v.uid || len(sys.ops) != len(v.ops) { if sys == nil || target == nil || sys.uid != target.uid || len(sys.ops) != len(target.ops) {
return false return false
} }
for i, o := range sys.ops { for i, o := range sys.ops {
if !o.Is(v.ops[i]) { if !o.Is(target.ops[i]) {
return false return false
} }
} }
@ -104,18 +107,15 @@ func (sys *I) Equal(v *I) bool {
return true return true
} }
// Commit applies all [Op] held by [I] and reverts successful [Op] on first error encountered. // Commit applies all [Op] held by [I] and reverts all successful [Op] on first error encountered.
// Commit must not be called more than once. // Commit must not be called more than once.
func (sys *I) Commit(ctx context.Context) error { func (sys *I) Commit() error {
sys.lock.Lock() if sys.committed {
defer sys.lock.Unlock() panic("attempting to commit twice")
if sys.ctx != nil {
panic("sys instance committed twice")
} }
sys.ctx = ctx sys.committed = true
sp := New(sys.uid) sp := New(sys.ctx, sys.uid)
sp.ops = make([]Op, 0, len(sys.ops)) // prevent copies during commits sp.ops = make([]Op, 0, len(sys.ops)) // prevent copies during commits
defer func() { defer func() {
// sp is set to nil when all ops are applied // sp is set to nil when all ops are applied
@ -123,7 +123,7 @@ func (sys *I) Commit(ctx context.Context) error {
// rollback partial commit // rollback partial commit
msg.Verbosef("commit faulted after %d ops, rolling back partial commit", len(sp.ops)) msg.Verbosef("commit faulted after %d ops, rolling back partial commit", len(sp.ops))
if err := sp.Revert(nil); err != nil { if err := sp.Revert(nil); err != nil {
log.Println("errors returned reverting partial commit:", err) printJoinedError(log.Println, "cannot revert partial commit:", err)
} }
} }
}() }()
@ -144,17 +144,13 @@ func (sys *I) Commit(ctx context.Context) error {
// Revert reverts all [Op] meeting [Criteria] held by [I]. // Revert reverts all [Op] meeting [Criteria] held by [I].
func (sys *I) Revert(ec *Criteria) error { func (sys *I) Revert(ec *Criteria) error {
sys.lock.Lock() if sys.reverted {
defer sys.lock.Unlock() panic("attempting to revert twice")
if sys.state {
panic("sys instance reverted twice")
} }
sys.state = true sys.reverted = true
// collect errors // collect errors
errs := make([]error, len(sys.ops)) errs := make([]error, len(sys.ops))
for i := range sys.ops { for i := range sys.ops {
errs[i] = sys.ops[len(sys.ops)-i-1].revert(sys, ec) errs[i] = sys.ops[len(sys.ops)-i-1].revert(sys, ec)
} }
@ -162,3 +158,16 @@ func (sys *I) Revert(ec *Criteria) error {
// errors.Join filters nils // errors.Join filters nils
return errors.Join(errs...) return errors.Join(errs...)
} }
// noCopy may be added to structs which must not be copied
// after the first use.
//
// See https://golang.org/issues/8005#issuecomment-190753527
// for details.
//
// Note that it must not be embedded, due to the Lock and Unlock methods.
type noCopy struct{}
// Lock is a no-op used by -copylocks checker from `go vet`.
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}

162
system/system_test.go Normal file
View File

@ -0,0 +1,162 @@
package system_test
import (
"reflect"
"strconv"
"testing"
_ "unsafe"
"hakurei.app/system"
)
//go:linkname criteriaHasType hakurei.app/system.(*Criteria).hasType
func criteriaHasType(_ *system.Criteria, _ system.Enablement) bool
func TestCriteria(t *testing.T) {
testCases := []struct {
name string
ec, t system.Enablement
want bool
}{
{"nil", 0xff, system.EWayland, true},
{"nil user", 0xff, system.User, false},
{"all", system.EWayland | system.EX11 | system.EDBus | system.EPulse | system.User | system.Process, system.Process, true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var criteria *system.Criteria
if tc.ec != 0xff {
criteria = (*system.Criteria)(&tc.ec)
}
if got := criteriaHasType(criteria, tc.t); got != tc.want {
t.Errorf("hasType: got %v, want %v",
got, tc.want)
}
})
}
}
func TestTypeString(t *testing.T) {
testCases := []struct {
e system.Enablement
want string
}{
{system.EWayland, system.EWayland.String()},
{system.EX11, system.EX11.String()},
{system.EDBus, system.EDBus.String()},
{system.EPulse, system.EPulse.String()},
{system.User, "user"},
{system.Process, "process"},
{system.User | system.Process, "user, process"},
{system.EWayland | system.User | system.Process, "wayland, user, process"},
{system.EX11 | system.Process, "x11, process"},
}
for _, tc := range testCases {
t.Run("label type string "+strconv.Itoa(int(tc.e)), func(t *testing.T) {
if got := system.TypeString(tc.e); got != tc.want {
t.Errorf("TypeString: %q, want %q", got, tc.want)
}
})
}
}
func TestNew(t *testing.T) {
t.Run("panic", func(t *testing.T) {
t.Run("ctx", func(t *testing.T) {
defer func() {
want := "invalid call to New"
if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want)
}
}()
system.New(nil, 0)
})
t.Run("uid", func(t *testing.T) {
defer func() {
want := "invalid call to New"
if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want)
}
}()
system.New(t.Context(), -1)
})
})
sys := system.New(t.Context(), 0xdeadbeef)
if got := reflect.ValueOf(sys).Elem().FieldByName("ctx"); got.IsNil() {
t.Errorf("New: ctx = %#v", got)
}
if got := sys.UID(); got != 0xdeadbeef {
t.Errorf("UID: %d", got)
}
}
func TestEqual(t *testing.T) {
testCases := []struct {
name string
sys *system.I
v *system.I
want bool
}{
{"simple UID",
system.New(t.Context(), 150),
system.New(t.Context(), 150),
true},
{"simple UID differ",
system.New(t.Context(), 150),
system.New(t.Context(), 151),
false},
{"simple UID nil",
system.New(t.Context(), 150),
nil,
false},
{"op length mismatch",
system.New(t.Context(), 150).
ChangeHosts("chronos"),
system.New(t.Context(), 150).
ChangeHosts("chronos").
Ensure("/run", 0755),
false},
{"op value mismatch",
system.New(t.Context(), 150).
ChangeHosts("chronos").
Ensure("/run", 0644),
system.New(t.Context(), 150).
ChangeHosts("chronos").
Ensure("/run", 0755),
false},
{"op type mismatch",
system.New(t.Context(), 150).
ChangeHosts("chronos").
CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 0, 256),
system.New(t.Context(), 150).
ChangeHosts("chronos").
Ensure("/run", 0755),
false},
{"op equals",
system.New(t.Context(), 150).
ChangeHosts("chronos").
Ensure("/run", 0755),
system.New(t.Context(), 150).
ChangeHosts("chronos").
Ensure("/run", 0755),
true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.sys.Equal(tc.v) != tc.want {
t.Errorf("Equal: %v, want %v", !tc.want, tc.want)
}
})
}
}

View File

@ -9,20 +9,16 @@ import (
"syscall" "syscall"
) )
// CopyFile registers an Op that copies from src. // CopyFile appends [TmpfileOp] to [I].
// A buffer is initialised with size cap and the Op faults if bytes read exceed n.
func (sys *I) CopyFile(payload *[]byte, src string, cap int, n int64) *I { func (sys *I) CopyFile(payload *[]byte, src string, cap int, n int64) *I {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
buf.Grow(cap) buf.Grow(cap)
sys.ops = append(sys.ops, &TmpfileOp{payload, src, n, buf})
sys.lock.Lock()
sys.ops = append(sys.ops, &Tmpfile{payload, src, n, buf})
sys.lock.Unlock()
return sys return sys
} }
type Tmpfile struct { // TmpfileOp reads up to n bytes from src and writes the resulting byte slice to payload.
type TmpfileOp struct {
payload *[]byte payload *[]byte
src string src string
@ -30,8 +26,8 @@ type Tmpfile struct {
buf *bytes.Buffer buf *bytes.Buffer
} }
func (t *Tmpfile) Type() Enablement { return Process } func (t *TmpfileOp) Type() Enablement { return Process }
func (t *Tmpfile) apply(*I) error { func (t *TmpfileOp) apply(*I) error {
msg.Verbose("copying", t) msg.Verbose("copying", t)
if t.payload == nil { if t.payload == nil {
@ -40,37 +36,31 @@ func (t *Tmpfile) apply(*I) error {
} }
if b, err := os.Stat(t.src); err != nil { if b, err := os.Stat(t.src); err != nil {
return wrapErrSuffix(err, return newOpError("tmpfile", err, false)
fmt.Sprintf("cannot stat %q:", t.src))
} else { } else {
if b.IsDir() { if b.IsDir() {
return wrapErrSuffix(syscall.EISDIR, return newOpError("tmpfile", &os.PathError{Op: "stat", Path: t.src, Err: syscall.EISDIR}, false)
fmt.Sprintf("%q is a directory", t.src))
} }
if s := b.Size(); s > t.n { if s := b.Size(); s > t.n {
return wrapErrSuffix(syscall.ENOMEM, return newOpError("tmpfile", &os.PathError{Op: "stat", Path: t.src, Err: syscall.ENOMEM}, false)
fmt.Sprintf("file %q is too long: %d > %d",
t.src, s, t.n))
} }
} }
if f, err := os.Open(t.src); err != nil { if f, err := os.Open(t.src); err != nil {
return wrapErrSuffix(err, return newOpError("tmpfile", err, false)
fmt.Sprintf("cannot open %q:", t.src))
} else if _, err = io.CopyN(t.buf, f, t.n); err != nil { } else if _, err = io.CopyN(t.buf, f, t.n); err != nil {
return wrapErrSuffix(err, return newOpError("tmpfile", err, false)
fmt.Sprintf("cannot read from %q:", t.src))
} }
*t.payload = t.buf.Bytes() *t.payload = t.buf.Bytes()
return nil return nil
} }
func (t *Tmpfile) revert(*I, *Criteria) error { t.buf.Reset(); return nil } func (t *TmpfileOp) revert(*I, *Criteria) error { t.buf.Reset(); return nil }
func (t *Tmpfile) Is(o Op) bool { func (t *TmpfileOp) Is(o Op) bool {
t0, ok := o.(*Tmpfile) target, ok := o.(*TmpfileOp)
return ok && t0 != nil && return ok && t != nil && target != nil &&
t.src == t0.src && t.n == t0.n t.src == target.src && t.n == target.n
} }
func (t *Tmpfile) Path() string { return t.src } func (t *TmpfileOp) Path() string { return t.src }
func (t *Tmpfile) String() string { return fmt.Sprintf("up to %d bytes from %q", t.n, t.src) } func (t *TmpfileOp) String() string { return fmt.Sprintf("up to %d bytes from %q", t.n, t.src) }

View File

@ -15,10 +15,10 @@ func TestCopyFile(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run("copy file "+tc.path+" with cap = "+strconv.Itoa(tc.cap)+" n = "+strconv.Itoa(int(tc.n)), func(t *testing.T) { t.Run("copy file "+tc.path+" with cap = "+strconv.Itoa(tc.cap)+" n = "+strconv.Itoa(int(tc.n)), func(t *testing.T) {
sys := New(150) sys := New(t.Context(), 150)
sys.CopyFile(new([]byte), tc.path, tc.cap, tc.n) sys.CopyFile(new([]byte), tc.path, tc.cap, tc.n)
tc.test(t, sys.ops, []Op{ tc.test(t, sys.ops, []Op{
&Tmpfile{nil, tc.path, tc.n, nil}, &TmpfileOp{nil, tc.path, tc.n, nil},
}, "CopyFile") }, "CopyFile")
}) })
} }
@ -33,10 +33,10 @@ func TestLink(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run("link file "+tc.dst+" from "+tc.src, func(t *testing.T) { t.Run("link file "+tc.dst+" from "+tc.src, func(t *testing.T) {
sys := New(150) sys := New(t.Context(), 150)
sys.Link(tc.src, tc.dst) sys.Link(tc.src, tc.dst)
(&tcOp{Process, tc.src}).test(t, sys.ops, []Op{ (&tcOp{Process, tc.src}).test(t, sys.ops, []Op{
&Hardlink{Process, tc.dst, tc.src}, &HardlinkOp{Process, tc.dst, tc.src},
}, "Link") }, "Link")
}) })
} }
@ -52,10 +52,10 @@ func TestLinkFileType(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run("link file "+tc.dst+" from "+tc.path+" with type "+TypeString(tc.et), func(t *testing.T) { t.Run("link file "+tc.dst+" from "+tc.path+" with type "+TypeString(tc.et), func(t *testing.T) {
sys := New(150) sys := New(t.Context(), 150)
sys.LinkFileType(tc.et, tc.path, tc.dst) sys.LinkFileType(tc.et, tc.path, tc.dst)
tc.test(t, sys.ops, []Op{ tc.test(t, sys.ops, []Op{
&Hardlink{tc.et, tc.dst, tc.path}, &HardlinkOp{tc.et, tc.dst, tc.path},
}, "LinkFileType") }, "LinkFileType")
}) })
} }
@ -73,7 +73,7 @@ func TestTmpfile_String(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.want, func(t *testing.T) { t.Run(tc.want, func(t *testing.T) {
if got := (&Tmpfile{src: tc.src, n: tc.n}).String(); got != tc.want { if got := (&TmpfileOp{src: tc.src, n: tc.n}).String(); got != tc.want {
t.Errorf("String() = %v, want %v", got, tc.want) t.Errorf("String() = %v, want %v", got, tc.want)
} }
}) })

View File

@ -9,17 +9,16 @@ import (
"hakurei.app/system/wayland" "hakurei.app/system/wayland"
) )
// Wayland sets up a wayland socket with a security context attached. // Wayland appends [WaylandOp] to [I].
func (sys *I) Wayland(syncFd **os.File, dst, src, appID, instanceID string) *I { func (sys *I) Wayland(syncFd **os.File, dst, src, appID, instanceID string) *I {
sys.lock.Lock() sys.ops = append(sys.ops, &WaylandOp{syncFd, dst, src, appID, instanceID, wayland.Conn{}})
defer sys.lock.Unlock()
sys.ops = append(sys.ops, &Wayland{syncFd, dst, src, appID, instanceID, wayland.Conn{}})
return sys return sys
} }
type Wayland struct { // WaylandOp maintains a wayland socket with security-context-v1 attached via [wayland].
// The socket stops accepting connections once the pipe referred to by sync is closed.
// The socket is pathname only and is destroyed on revert.
type WaylandOp struct {
sync **os.File sync **os.File
dst, src string dst, src string
appID, instanceID string appID, instanceID string
@ -27,63 +26,59 @@ type Wayland struct {
conn wayland.Conn conn wayland.Conn
} }
func (w *Wayland) Type() Enablement { return Process } func (w *WaylandOp) Type() Enablement { return Process }
func (w *Wayland) apply(sys *I) error { func (w *WaylandOp) apply(sys *I) error {
if w.sync == nil { if w.sync == nil {
// this is a misuse of the API; do not return an error message // this is a misuse of the API; do not return a wrapped error
return errors.New("invalid sync") return errors.New("invalid sync")
} }
// the Wayland op is not repeatable // the Wayland op is not repeatable
if *w.sync != nil { if *w.sync != nil {
// this is a misuse of the API; do not return an error message // this is a misuse of the API; do not return a wrapped error
return errors.New("attempted to attach multiple wayland sockets") return errors.New("attempted to attach multiple wayland sockets")
} }
if err := w.conn.Attach(w.src); err != nil { if err := w.conn.Attach(w.src); err != nil {
// make console output less nasty return newOpError("wayland", err, false)
if errors.Is(err, os.ErrNotExist) {
err = os.ErrNotExist
}
return wrapErrSuffix(err,
fmt.Sprintf("cannot attach to wayland on %q:", w.src))
} else { } else {
msg.Verbosef("wayland attached on %q", w.src) msg.Verbosef("wayland attached on %q", w.src)
} }
if sp, err := w.conn.Bind(w.dst, w.appID, w.instanceID); err != nil { if sp, err := w.conn.Bind(w.dst, w.appID, w.instanceID); err != nil {
return wrapErrSuffix(err, return newOpError("wayland", err, false)
fmt.Sprintf("cannot bind to socket on %q:", w.dst))
} else { } else {
*w.sync = sp *w.sync = sp
msg.Verbosef("wayland listening on %q", w.dst) msg.Verbosef("wayland listening on %q", w.dst)
return wrapErrSuffix(errors.Join(os.Chmod(w.dst, 0), acl.Update(w.dst, sys.uid, acl.Read, acl.Write, acl.Execute)), if err = os.Chmod(w.dst, 0); err != nil {
fmt.Sprintf("cannot chmod socket on %q:", w.dst)) return newOpError("wayland", err, false)
}
return newOpError("wayland", acl.Update(w.dst, sys.uid, acl.Read, acl.Write, acl.Execute), false)
} }
} }
func (w *Wayland) revert(_ *I, ec *Criteria) error { func (w *WaylandOp) revert(_ *I, ec *Criteria) error {
if ec.hasType(w) { if ec.hasType(w.Type()) {
msg.Verbosef("removing wayland socket on %q", w.dst) msg.Verbosef("removing wayland socket on %q", w.dst)
if err := os.Remove(w.dst); err != nil && !errors.Is(err, os.ErrNotExist) { if err := os.Remove(w.dst); err != nil && !errors.Is(err, os.ErrNotExist) {
return err return newOpError("wayland", err, true)
} }
msg.Verbosef("detaching from wayland on %q", w.src) msg.Verbosef("detaching from wayland on %q", w.src)
return wrapErrSuffix(w.conn.Close(), return newOpError("wayland", w.conn.Close(), true)
fmt.Sprintf("cannot detach from wayland on %q:", w.src))
} else { } else {
msg.Verbosef("skipping wayland cleanup on %q", w.dst) msg.Verbosef("skipping wayland cleanup on %q", w.dst)
return nil return nil
} }
} }
func (w *Wayland) Is(o Op) bool { func (w *WaylandOp) Is(o Op) bool {
w0, ok := o.(*Wayland) target, ok := o.(*WaylandOp)
return ok && w.dst == w0.dst && w.src == w0.src && return ok && w != nil && target != nil &&
w.appID == w0.appID && w.instanceID == w0.instanceID w.dst == target.dst && w.src == target.src &&
w.appID == target.appID && w.instanceID == target.instanceID
} }
func (w *Wayland) Path() string { return w.dst } func (w *WaylandOp) Path() string { return w.dst }
func (w *Wayland) String() string { return fmt.Sprintf("wayland socket at %q", w.dst) } func (w *WaylandOp) String() string { return fmt.Sprintf("wayland socket at %q", w.dst) }

View File

@ -10,6 +10,7 @@ import (
"syscall" "syscall"
) )
// Conn represents a connection to the wayland display server.
type Conn struct { type Conn struct {
conn *net.UnixConn conn *net.UnixConn
@ -25,7 +26,7 @@ func (c *Conn) Attach(p string) (err error) {
defer c.mu.Unlock() defer c.mu.Unlock()
if c.conn != nil { if c.conn != nil {
return errors.New("attached") return errors.New("socket already attached")
} }
c.conn, err = net.DialUnix("unix", nil, &net.UnixAddr{Name: p, Net: "unix"}) c.conn, err = net.DialUnix("unix", nil, &net.UnixAddr{Name: p, Net: "unix"})
@ -51,15 +52,16 @@ func (c *Conn) Close() error {
return nil return nil
} }
func (c *Conn) Bind(p, appID, instanceID string) (*os.File, error) { // Bind binds the new socket to pathname.
func (c *Conn) Bind(pathname, appID, instanceID string) (*os.File, error) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
if c.conn == nil { if c.conn == nil {
return nil, errors.New("not attached") return nil, errors.New("socket not attached")
} }
if c.done != nil { if c.done != nil {
return nil, errors.New("bound") return nil, errors.New("socket already bound")
} }
if rc, err := c.conn.SyscallConn(); err != nil { if rc, err := c.conn.SyscallConn(); err != nil {
@ -67,7 +69,7 @@ func (c *Conn) Bind(p, appID, instanceID string) (*os.File, error) {
return nil, err return nil, err
} else { } else {
c.done = make(chan struct{}) c.done = make(chan struct{})
return bindRawConn(c.done, rc, p, appID, instanceID) return bindRawConn(c.done, rc, pathname, appID, instanceID)
} }
} }

View File

@ -1,53 +1,37 @@
package system package system
import ( import (
"fmt"
"hakurei.app/system/internal/xcb" "hakurei.app/system/internal/xcb"
) )
// ChangeHosts appends an X11 ChangeHosts command Op. // ChangeHosts appends [XHostOp] to [I].
func (sys *I) ChangeHosts(username string) *I { func (sys *I) ChangeHosts(username string) *I {
sys.lock.Lock() sys.ops = append(sys.ops, XHostOp(username))
defer sys.lock.Unlock()
sys.ops = append(sys.ops, XHost(username))
return sys return sys
} }
type XHost string // XHostOp inserts the target user into X11 hosts and deletes it once its [Enablement] is no longer satisfied.
type XHostOp string
func (x XHost) Type() Enablement { func (x XHostOp) Type() Enablement { return EX11 }
return EX11
}
func (x XHost) apply(*I) error { func (x XHostOp) apply(*I) error {
msg.Verbosef("inserting entry %s to X11", x) msg.Verbosef("inserting entry %s to X11", x)
return wrapErrSuffix(xcb.ChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)), return newOpError("xhost",
fmt.Sprintf("cannot insert entry %s to X11:", x)) xcb.ChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)), false)
} }
func (x XHost) revert(_ *I, ec *Criteria) error { func (x XHostOp) revert(_ *I, ec *Criteria) error {
if ec.hasType(x) { if ec.hasType(x.Type()) {
msg.Verbosef("deleting entry %s from X11", x) msg.Verbosef("deleting entry %s from X11", x)
return wrapErrSuffix(xcb.ChangeHosts(xcb.HostModeDelete, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)), return newOpError("xhost",
fmt.Sprintf("cannot delete entry %s from X11:", x)) xcb.ChangeHosts(xcb.HostModeDelete, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)), false)
} else { } else {
msg.Verbosef("skipping entry %s in X11", x) msg.Verbosef("skipping entry %s in X11", x)
return nil return nil
} }
} }
func (x XHost) Is(o Op) bool { func (x XHostOp) Is(o Op) bool { target, ok := o.(XHostOp); return ok && x == target }
x0, ok := o.(XHost) func (x XHostOp) Path() string { return string(x) }
return ok && x == x0 func (x XHostOp) String() string { return string("SI:localuser:" + x) }
}
func (x XHost) Path() string {
return string(x)
}
func (x XHost) String() string {
return string("SI:localuser:" + x)
}

View File

@ -8,10 +8,10 @@ func TestChangeHosts(t *testing.T) {
testCases := []string{"chronos", "keyring", "cat", "kbd", "yonah"} testCases := []string{"chronos", "keyring", "cat", "kbd", "yonah"}
for _, tc := range testCases { for _, tc := range testCases {
t.Run("append ChangeHosts operation for "+tc, func(t *testing.T) { t.Run("append ChangeHosts operation for "+tc, func(t *testing.T) {
sys := New(150) sys := New(t.Context(), 150)
sys.ChangeHosts(tc) sys.ChangeHosts(tc)
(&tcOp{EX11, tc}).test(t, sys.ops, []Op{ (&tcOp{EX11, tc}).test(t, sys.ops, []Op{
XHost(tc), XHostOp(tc),
}, "ChangeHosts") }, "ChangeHosts")
}) })
} }
@ -26,7 +26,7 @@ func TestXHost_String(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.want, func(t *testing.T) { t.Run(tc.want, func(t *testing.T) {
if got := XHost(tc.username).String(); got != tc.want { if got := XHostOp(tc.username).String(); got != tc.want {
t.Errorf("String() = %v, want %v", got, tc.want) t.Errorf("String() = %v, want %v", got, tc.want)
} }
}) })

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