36 Commits

Author SHA1 Message Date
210d36242e system/dbus: create context in subtest
Some checks failed
Test / Create distribution (push) Successful in 25s
Test / Hakurei (push) Successful in 42s
Test / Sandbox (push) Successful in 41s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Sandbox (race detector) (push) Successful in 40s
Test / Hpkg (push) Successful in 41s
Test / Flake checks (push) Has been cancelled
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:25:45 +09:00
ea10c4d5a5 system/dbus: remove redundant proxy pairs
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m10s
Test / Hpkg (push) Successful in 4m4s
Test / Sandbox (race detector) (push) Successful in 4m25s
Test / Hakurei (race detector) (push) Successful in 5m3s
Test / Hakurei (push) Successful in 2m10s
Test / Flake checks (push) Successful in 1m26s
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
9c6439d7be container/dispatcher: optional stub wait4 signal association
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m11s
Test / Hakurei (push) Successful in 3m4s
Test / Hpkg (push) Successful in 3m58s
Test / Sandbox (race detector) (push) Successful in 4m29s
Test / Hakurei (race detector) (push) Successful in 5m6s
Test / Flake checks (push) Successful in 1m28s
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:34:43 +09:00
11bd0797f5 container/stub: remove function call in handleExit
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m5s
Test / Hpkg (push) Successful in 3m59s
Test / Sandbox (race detector) (push) Successful in 4m27s
Test / Hakurei (race detector) (push) Successful in 5m7s
Test / Flake checks (push) Successful in 1m28s
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
25db474cf6 container/dispatcher: remove wait4 test log
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m6s
Test / Hakurei (push) Successful in 3m4s
Test / Hpkg (push) Successful in 3m57s
Test / Sandbox (race detector) (push) Successful in 4m35s
Test / Hakurei (race detector) (push) Successful in 5m7s
Test / Flake checks (push) Successful in 1m21s
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
21f50aa2f0 container/stub: override goexit methods
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m11s
Test / Hakurei (push) Successful in 3m7s
Test / Hpkg (push) Successful in 3m51s
Test / Sandbox (race detector) (push) Successful in 5m31s
Test / Hakurei (race detector) (push) Successful in 2m57s
Test / Flake checks (push) Successful in 1m35s
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 05:20:09 +09:00
031b0ed270 system/dispatcher: wrap syscall helper functions
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m7s
Test / Hakurei (push) Successful in 3m3s
Test / Hpkg (push) Successful in 3m56s
Test / Sandbox (race detector) (push) Successful in 4m23s
Test / Hakurei (race detector) (push) Successful in 5m2s
Test / Flake checks (push) Successful in 1m26s
This allows tests to stub all kernel behaviour, like in the container package.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-04 04:17:55 +09:00
a837557505 system: improve tests of the I struct
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m4s
Test / Hpkg (push) Successful in 3m51s
Test / Sandbox (race detector) (push) Successful in 5m30s
Test / Hakurei (race detector) (push) Successful in 2m41s
Test / Flake checks (push) Successful in 1m29s
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
aab806a6f9 system: update doc commands and remove mutex
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m8s
Test / Hakurei (push) Successful in 2m59s
Test / Hpkg (push) Successful in 3m56s
Test / Sandbox (race detector) (push) Successful in 4m28s
Test / Hakurei (race detector) (push) Successful in 5m4s
Test / Flake checks (push) Successful in 1m21s
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
3acf49f979 container/dispatcher: stub.Call initialisation helper function
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m3s
Test / Hakurei (push) Successful in 3m5s
Test / Hpkg (push) Successful in 3m46s
Test / Sandbox (race detector) (push) Successful in 4m25s
Test / Hakurei (race detector) (push) Successful in 5m4s
Test / Flake checks (push) Successful in 1m25s
This keeps composites analysis happy without making the test cases (too) bloated.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-02 04:44:29 +09:00
3057964cc9 container/stub: export stub helpers
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m5s
Test / Hakurei (push) Successful in 3m3s
Test / Hpkg (push) Successful in 4m6s
Test / Sandbox (race detector) (push) Successful in 4m32s
Test / Hakurei (race detector) (push) Successful in 5m6s
Test / Flake checks (push) Successful in 1m26s
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-09-01 01:39:11 +09:00
a5ce8dd979 system/output: implement MessageError
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m6s
Test / Hakurei (push) Successful in 3m2s
Test / Hpkg (push) Successful in 3m52s
Test / Sandbox (race detector) (push) Successful in 4m23s
Test / Hakurei (race detector) (push) Successful in 5m2s
Test / Flake checks (push) Successful in 1m29s
This error is also formatted differently based on state.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-31 13:51:21 +09:00
fe51d5f78c container/msg: optionally provide error messages
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m18s
Test / Hakurei (push) Successful in 3m0s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hakurei (race detector) (push) Successful in 4m59s
Test / Flake checks (push) Successful in 1m26s
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
1be5a34569 container: wrap container init start errors
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 2m59s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m20s
Test / Hakurei (race detector) (push) Successful in 4m59s
Test / Flake checks (push) Successful in 1m22s
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
57ade7aeaa system: wrap op errors
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Hakurei (race detector) (push) Successful in 42s
Test / Hakurei (push) Successful in 42s
Test / Sandbox (race detector) (push) Successful in 41s
Test / Sandbox (push) Successful in 41s
Test / Hpkg (push) Successful in 40s
Test / Flake checks (push) Successful in 1m24s
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:51:30 +09:00
d13dfeca22 system/internal/xcb: refactor and clean up
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m1s
Test / Hpkg (push) Successful in 3m56s
Test / Sandbox (race detector) (push) Successful in 4m20s
Test / Hakurei (race detector) (push) Successful in 5m1s
Test / Flake checks (push) Successful in 1m25s
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
56c5a7399b system/wayland: improve error descriptions
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 2m59s
Test / Hpkg (push) Successful in 3m57s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hakurei (race detector) (push) Successful in 5m2s
Test / Flake checks (push) Successful in 1m25s
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
e1f07be0b2 system/acl: wrap libacl errors in PathError
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 5m22s
Test / Hakurei (race detector) (push) Successful in 2m53s
Test / Flake checks (push) Successful in 1m18s
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
062bb57b27 system/acl: update test log messages
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m2s
Test / Hpkg (push) Successful in 3m55s
Test / Sandbox (race detector) (push) Successful in 4m28s
Test / Hakurei (race detector) (push) Successful in 5m1s
Test / Flake checks (push) Successful in 1m29s
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
03355fb209 container/mount: unwrap vfs decoder errors
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m6s
Test / Hpkg (push) Successful in 4m7s
Test / Sandbox (race detector) (push) Successful in 4m29s
Test / Hakurei (race detector) (push) Successful in 5m3s
Test / Flake checks (push) Successful in 1m22s
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
289dea3f85 container/dispatcher: check simple test errors via reflect
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m7s
Test / Sandbox (race detector) (push) Successful in 2m15s
Test / Hakurei (push) Successful in 2m20s
Test / Hakurei (race detector) (push) Successful in 2m53s
Test / Hpkg (push) Successful in 3m15s
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
2a9e404c30 container/vfs: wrap decoder errors
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m5s
Test / Hpkg (push) Successful in 4m4s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 5m0s
Test / Hakurei (push) Successful in 2m6s
Test / Flake checks (push) Successful in 1m25s
This passes line information and handles strconv errors so it reads better.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-29 22:01:14 +09:00
85a757101e container/initoverlay: invalid argument type
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m4s
Test / Hakurei (push) Successful in 3m1s
Test / Hpkg (push) Successful in 3m53s
Test / Sandbox (race detector) (push) Successful in 4m31s
Test / Hakurei (race detector) (push) Successful in 5m1s
Test / Flake checks (push) Successful in 1m24s
This eliminates generic WrapErr from overlay.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-29 02:59:36 +09:00
358e94d3c1 container/dispatcher: check test errors via reflect
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m1s
Test / Hpkg (push) Successful in 3m59s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hakurei (race detector) (push) Successful in 5m0s
Test / Flake checks (push) Successful in 1m26s
Using the errors package might conceal some incorrect behaviour.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-29 02:38:27 +09:00
202efb7c7c container/inittmpfs: unwrap out of bounds error
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m6s
Test / Hakurei (push) Successful in 3m1s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 4m59s
Test / Flake checks (push) Successful in 1m26s
This eliminates generic WrapErr from tmpfs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-29 02:28:22 +09:00
d137a70621 container/init: unwrap path errors
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m5s
Test / Hakurei (push) Successful in 3m3s
Test / Hpkg (push) Successful in 3m59s
Test / Sandbox (race detector) (push) Successful in 4m23s
Test / Hakurei (race detector) (push) Successful in 5m2s
Test / Flake checks (push) Successful in 1m26s
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:06:07 +09:00
5b3932ab65 container/initsymlink: unwrap mount errors
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 2m58s
Test / Hpkg (push) Successful in 3m57s
Test / Sandbox (race detector) (push) Successful in 4m21s
Test / Hakurei (race detector) (push) Successful in 4m59s
Test / Flake checks (push) Successful in 1m25s
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:55:01 +09:00
ea8a7a341f container/initsymlink: unwrap absolute error
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m10s
Test / Hpkg (push) Successful in 3m56s
Test / Sandbox (race detector) (push) Successful in 4m23s
Test / Hakurei (race detector) (push) Successful in 5m0s
Test / Hakurei (push) Successful in 2m10s
Test / Flake checks (push) Successful in 1m27s
This is now handled properly by the init.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-29 01:43:11 +09:00
5b6546b541 container/init: handle unwrapped errors
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m3s
Test / Hakurei (push) Successful in 3m3s
Test / Hpkg (push) Successful in 3m54s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 4m54s
Test / Flake checks (push) Successful in 1m35s
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
f60b854ea1 container: repeat and impossible state types
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m3s
Test / Hpkg (push) Successful in 3m56s
Test / Sandbox (race detector) (push) Successful in 4m20s
Test / Hakurei (race detector) (push) Successful in 4m58s
Test / Flake checks (push) Successful in 1m28s
This moves repeated Op errors and impossible internal state errors off of msg.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-29 01:14:52 +09:00
58999599d8 container: wrap mount syscall errno
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m4s
Test / Hakurei (push) Successful in 3m0s
Test / Hpkg (push) Successful in 3m48s
Test / Sandbox (race detector) (push) Successful in 4m21s
Test / Hakurei (race detector) (push) Successful in 4m57s
Test / Flake checks (push) Successful in 1m26s
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
08937cf3a0 internal/app: remove seal interface
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m8s
Test / Hakurei (push) Successful in 3m4s
Test / Hpkg (push) Successful in 3m57s
Test / Sandbox (race detector) (push) Successful in 4m23s
Test / Hakurei (race detector) (push) Successful in 5m1s
Test / Flake checks (push) Successful in 1m25s
This further cleans up the package for the restructure.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-28 01:07:51 +09:00
68c50b3ebc internal/app: remove app interface
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m17s
Test / Hpkg (push) Successful in 3m56s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 5m0s
Test / Hakurei (push) Successful in 2m7s
Test / Flake checks (push) Successful in 1m24s
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
49cc97f67c internal/app: update doc comments
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m8s
Test / Hakurei (push) Successful in 3m7s
Test / Hpkg (push) Successful in 4m0s
Test / Sandbox (race detector) (push) Successful in 4m31s
Test / Hakurei (race detector) (push) Successful in 2m44s
Test / Flake checks (push) Successful in 1m26s
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
56d2029316 internal/app: less strict username regex
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 1m34s
Test / Hakurei (push) Successful in 1m56s
Test / Hpkg (push) Successful in 3m27s
Test / Sandbox (race detector) (push) Successful in 4m28s
Test / Hakurei (race detector) (push) Successful in 5m0s
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:44:54 +09:00
22b33e0375 internal: move sysconf wrapper to app
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m3s
Test / Hakurei (push) Successful in 2m59s
Test / Hpkg (push) Successful in 3m57s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 5m1s
Test / Flake checks (push) Successful in 1m26s
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
484 changed files with 11456 additions and 46845 deletions

View File

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

View File

@@ -2,6 +2,7 @@ name: Test
on:
- push
- pull_request
jobs:
hakurei:
@@ -72,23 +73,6 @@ jobs:
path: result/*
retention-days: 1
sharefs:
name: ShareFS
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run NixOS test
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.sharefs
- name: Upload test output
uses: actions/upload-artifact@v3
with:
name: "sharefs-vm-output"
path: result/*
retention-days: 1
hpkg:
name: Hpkg
runs-on: nix
@@ -113,7 +97,6 @@ jobs:
- race
- sandbox
- sandbox-race
- sharefs
- hpkg
runs-on: nix
steps:

1
.gitignore vendored
View File

@@ -27,7 +27,6 @@ go.work.sum
# go generate
/cmd/hakurei/LICENSE
/internal/pkg/testdata/testtool
# release
/dist/hakurei-*

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,55 +2,22 @@ package main
import (
"context"
"os"
"strings"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/hst"
"hakurei.app/message"
"hakurei.app/internal"
)
func withNixDaemon(
ctx context.Context,
msg message.Msg,
action string, command []string, net bool, updateConfig func(config *hst.Config) *hst.Config,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
) {
flags := hst.FMultiarch | hst.FUserns // nix sandbox requires userns
if net {
flags |= hst.FHostNet
}
if dropShell {
flags |= hst.FTty
}
mustRunAppDropShell(ctx, msg, updateConfig(&hst.Config{
mustRunAppDropShell(ctx, updateConfig(&hst.Config{
ID: app.ID,
ExtraPerms: []hst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
},
Identity: app.Identity,
Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action,
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: fhs.AbsEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath, Target: pathNix, Write: true}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: fhs.AbsUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID), Source: pathSet.homeDir, Write: true, Ensure: true}},
},
Username: "hakurei",
Shell: pathShell,
Home: pathDataData.Append(app.ID),
Path: pathShell,
Args: []string{bash, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
// start nix-daemon
@@ -64,26 +31,48 @@ func withNixDaemon(
" && pkill nix-daemon",
},
Flags: flags,
Username: "hakurei",
Shell: pathShell,
Home: pathDataData.Append(app.ID),
ExtraPerms: []*hst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
},
Identity: app.Identity,
Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action,
Userns: true, // nix sandbox requires userns
HostNet: net,
SeccompFlags: seccomp.AllowMultiarch,
Tty: dropShell,
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath, Target: pathNix, Write: true}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID), Source: pathSet.homeDir, Write: true, Ensure: true}},
},
},
}), dropShell, beforeFail)
}
func withCacheDir(
ctx context.Context,
msg message.Msg,
action string, command []string, workDir *check.Absolute,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
) {
flags := hst.FMultiarch
if dropShell {
flags |= hst.FTty
}
mustRunAppDropShell(ctx, msg, &hst.Config{
action string, command []string, workDir *container.Absolute,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
mustRunAppDropShell(ctx, &hst.Config{
ID: app.ID,
ExtraPerms: []hst.ExtraPermConfig{
Path: pathShell,
Args: []string{bash, "-lc", strings.Join(command, " && ")},
Username: "nixos",
Shell: pathShell,
Home: pathDataData.Append(app.ID, "cache"),
ExtraPerms: []*hst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
{Path: workDir, Execute: true},
@@ -93,38 +82,27 @@ func withCacheDir(
Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action,
SeccompFlags: seccomp.AllowMultiarch,
Tty: dropShell,
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: fhs.AbsEtc, Source: workDir.Append(fhs.Etc), Special: true}},
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: workDir.Append(container.FHSEtc), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: workDir.Append("nix"), Target: pathNix}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: fhs.AbsUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Source: workDir, Target: hst.AbsPrivateTmp.Append("bundle")}},
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Source: workDir, Target: hst.AbsTmp.Append("bundle")}},
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID, "cache"), Source: pathSet.cacheDir, Write: true, Ensure: true}},
},
Username: "nixos",
Shell: pathShell,
Home: pathDataData.Append(app.ID, "cache"),
Path: pathShell,
Args: []string{bash, "-lc", strings.Join(command, " && ")},
Flags: flags,
},
}, dropShell, beforeFail)
}
func mustRunAppDropShell(ctx context.Context, msg message.Msg, config *hst.Config, dropShell bool, beforeFail func()) {
func mustRunAppDropShell(ctx context.Context, config *hst.Config, dropShell bool, beforeFail func()) {
if dropShell {
if config.Container != nil {
config.Container.Args = []string{bash, "-l"}
}
mustRunApp(ctx, msg, config, beforeFail)
config.Args = []string{bash, "-l"}
mustRunApp(ctx, config, beforeFail)
beforeFail()
msg.BeforeExit()
os.Exit(0)
internal.Exit(0)
}
mustRunApp(ctx, msg, config, beforeFail)
mustRunApp(ctx, config, beforeFail)
}

View File

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

View File

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

View File

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

View File

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

View File

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

20
cmd/hsu/path.go Normal file
View File

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

View File

@@ -1,204 +0,0 @@
package main
import (
"context"
"errors"
"fmt"
"log"
"os"
"os/signal"
"path/filepath"
"runtime"
"syscall"
"unique"
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/internal/pkg"
"hakurei.app/internal/rosa"
"hakurei.app/message"
)
func main() {
container.TryArgv0(nil)
log.SetFlags(0)
log.SetPrefix("mbf: ")
msg := message.New(log.Default())
if os.Geteuid() == 0 {
log.Fatal("this program must not run as root")
}
var cache *pkg.Cache
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
defer stop()
defer func() {
if cache != nil {
cache.Close()
}
if r := recover(); r != nil {
fmt.Println(r)
log.Fatal("consider scrubbing the on-disk cache")
}
}()
var (
flagQuiet bool
flagCures int
flagBase string
flagTShift int
)
c := command.New(os.Stderr, log.Printf, "mbf", func([]string) (err error) {
msg.SwapVerbose(!flagQuiet)
var base *check.Absolute
if flagBase, err = filepath.Abs(flagBase); err != nil {
return
} else if base, err = check.NewAbs(flagBase); err != nil {
return
}
if cache, err = pkg.Open(ctx, msg, flagCures, base); err == nil {
if flagTShift < 0 {
cache.SetThreshold(0)
} else if flagTShift > 31 {
cache.SetThreshold(1 << 31)
} else {
cache.SetThreshold(1 << flagTShift)
}
}
return
}).Flag(
&flagQuiet,
"q", command.BoolFlag(false),
"Do not print cure messages",
).Flag(
&flagCures,
"cures", command.IntFlag(0),
"Maximum number of dependencies to cure at any given time",
).Flag(
&flagBase,
"d", command.StringFlag("cache"),
"Directory to store cured artifacts",
).Flag(
&flagTShift,
"tshift", command.IntFlag(-1),
"Dependency graph size exponent, to the power of 2",
)
{
var flagShifts int
c.NewCommand(
"scrub", "Examine the on-disk cache for errors",
func(args []string) error {
if len(args) > 0 {
return errors.New("scrub expects no arguments")
}
if flagShifts < 0 || flagShifts > 31 {
flagShifts = 12
}
return cache.Scrub(runtime.NumCPU() << flagShifts)
},
).Flag(
&flagShifts,
"shift", command.IntFlag(12),
"Scrub parallelism size exponent, to the power of 2",
)
}
c.NewCommand(
"stage3",
"Check for toolchain 3-stage non-determinism",
func(args []string) (err error) {
_, _, _, stage1 := (rosa.Std - 2).NewLLVM()
_, _, _, stage2 := (rosa.Std - 1).NewLLVM()
_, _, _, stage3 := rosa.Std.NewLLVM()
var (
pathname *check.Absolute
checksum [2]unique.Handle[pkg.Checksum]
)
if pathname, _, err = cache.Cure(stage1); err != nil {
return err
}
log.Println("stage1:", pathname)
if pathname, checksum[0], err = cache.Cure(stage2); err != nil {
return err
}
log.Println("stage2:", pathname)
if pathname, checksum[1], err = cache.Cure(stage3); err != nil {
return err
}
log.Println("stage3:", pathname)
if checksum[0] != checksum[1] {
err = &pkg.ChecksumMismatchError{
Got: checksum[0].Value(),
Want: checksum[1].Value(),
}
} else {
log.Println(
"stage2 is identical to stage3",
"("+pkg.Encode(checksum[0].Value())+")",
)
}
return
},
)
{
var (
flagDump string
)
c.NewCommand(
"cure",
"Cure the named artifact and show its path",
func(args []string) error {
if len(args) != 1 {
return errors.New("cure requires 1 argument")
}
if p, ok := rosa.ResolveName(args[0]); !ok {
return fmt.Errorf("unsupported artifact %q", args[0])
} else if flagDump == "" {
pathname, _, err := cache.Cure(rosa.Std.Load(p))
if err == nil {
log.Println(pathname)
}
return err
} else {
f, err := os.OpenFile(
flagDump,
os.O_WRONLY|os.O_CREATE|os.O_EXCL,
0644,
)
if err != nil {
return err
}
if err = cache.EncodeAll(f, rosa.Std.Load(p)); err != nil {
_ = f.Close()
return err
}
return f.Close()
}
},
).
Flag(
&flagDump,
"dump", command.StringFlag(""),
"Write IR to specified pathname and terminate",
)
}
c.MustParse(os.Args[1:], func(err error) {
if cache != nil {
cache.Close()
}
log.Fatal(err)
})
}

View File

@@ -1,282 +0,0 @@
#ifndef _GNU_SOURCE
#define _GNU_SOURCE /* O_DIRECT */
#endif
#include <dirent.h>
#include <errno.h>
#include <unistd.h>
/* TODO(ophestra): remove after 05ce67fea99ca09cd4b6625cff7aec9cc222dd5a reaches a release */
#include <sys/syscall.h>
#include "fuse-operations.h"
/* MUST_TRANSLATE_PATHNAME translates a userspace pathname to a relative pathname;
* the resulting address points to a constant string or part of pathname, it is never heap allocated. */
#define MUST_TRANSLATE_PATHNAME(pathname) \
do { \
if (pathname == NULL) \
return -EINVAL; \
while (*pathname == '/') \
pathname++; \
if (*pathname == '\0') \
pathname = "."; \
} while (0)
/* GET_CONTEXT_PRIV obtains fuse context and private data for the calling thread. */
#define GET_CONTEXT_PRIV(ctx, priv) \
do { \
ctx = fuse_get_context(); \
priv = ctx->private_data; \
} while (0)
/* impl_getattr modifies a struct stat from the kernel to present to userspace;
* impl_getattr returns a negative errno style error code. */
static int impl_getattr(struct fuse_context *ctx, struct stat *statbuf) {
/* allowlist of permitted types */
if (!S_ISDIR(statbuf->st_mode) && !S_ISREG(statbuf->st_mode) && !S_ISLNK(statbuf->st_mode)) {
return -ENOTRECOVERABLE; /* returning an errno causes all operations on the file to return EIO */
}
#define OVERRIDE_PERM(v) (statbuf->st_mode & ~0777) | (v & 0777)
if (S_ISDIR(statbuf->st_mode))
statbuf->st_mode = OVERRIDE_PERM(SHAREFS_PERM_DIR);
else if (S_ISREG(statbuf->st_mode))
statbuf->st_mode = OVERRIDE_PERM(SHAREFS_PERM_REG);
else
statbuf->st_mode = 0; /* should always be symlink in this case */
statbuf->st_uid = ctx->uid;
statbuf->st_gid = SHAREFS_MEDIA_RW_ID;
statbuf->st_ctim = statbuf->st_mtim;
statbuf->st_nlink = 1;
return 0;
}
/* fuse_operations implementation */
int sharefs_getattr(const char *pathname, struct stat *statbuf, struct fuse_file_info *fi) {
struct fuse_context *ctx;
struct sharefs_private *priv;
GET_CONTEXT_PRIV(ctx, priv);
MUST_TRANSLATE_PATHNAME(pathname);
(void)fi;
if (fstatat(priv->dirfd, pathname, statbuf, AT_SYMLINK_NOFOLLOW) == -1)
return -errno;
return impl_getattr(ctx, statbuf);
}
int sharefs_readdir(const char *pathname, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags) {
int fd;
DIR *dp;
struct stat st;
int ret = 0;
struct dirent *de;
struct fuse_context *ctx;
struct sharefs_private *priv;
GET_CONTEXT_PRIV(ctx, priv);
MUST_TRANSLATE_PATHNAME(pathname);
(void)offset;
(void)fi;
if ((fd = openat(priv->dirfd, pathname, O_RDONLY | O_DIRECTORY | O_CLOEXEC)) == -1)
return -errno;
if ((dp = fdopendir(fd)) == NULL) {
close(fd);
return -errno;
}
errno = 0; /* for the next readdir call */
while ((de = readdir(dp)) != NULL) {
if (flags & FUSE_READDIR_PLUS) {
if (fstatat(dirfd(dp), de->d_name, &st, AT_SYMLINK_NOFOLLOW) == -1) {
ret = -errno;
break;
}
if ((ret = impl_getattr(ctx, &st)) < 0)
break;
errno = 0;
ret = filler(buf, de->d_name, &st, 0, FUSE_FILL_DIR_PLUS);
} else
ret = filler(buf, de->d_name, NULL, 0, 0);
if (ret != 0) {
ret = errno != 0 ? -errno : -EIO; /* filler */
break;
}
errno = 0; /* for the next readdir call */
}
if (ret == 0 && errno != 0)
ret = -errno; /* readdir */
closedir(dp);
return ret;
}
int sharefs_mkdir(const char *pathname, mode_t mode) {
struct fuse_context *ctx;
struct sharefs_private *priv;
GET_CONTEXT_PRIV(ctx, priv);
MUST_TRANSLATE_PATHNAME(pathname);
(void)mode;
if (mkdirat(priv->dirfd, pathname, SHAREFS_PERM_DIR) == -1)
return -errno;
return 0;
}
int sharefs_unlink(const char *pathname) {
struct fuse_context *ctx;
struct sharefs_private *priv;
GET_CONTEXT_PRIV(ctx, priv);
MUST_TRANSLATE_PATHNAME(pathname);
if (unlinkat(priv->dirfd, pathname, 0) == -1)
return -errno;
return 0;
}
int sharefs_rmdir(const char *pathname) {
struct fuse_context *ctx;
struct sharefs_private *priv;
GET_CONTEXT_PRIV(ctx, priv);
MUST_TRANSLATE_PATHNAME(pathname);
if (unlinkat(priv->dirfd, pathname, AT_REMOVEDIR) == -1)
return -errno;
return 0;
}
int sharefs_rename(const char *oldpath, const char *newpath, unsigned int flags) {
struct fuse_context *ctx;
struct sharefs_private *priv;
GET_CONTEXT_PRIV(ctx, priv);
MUST_TRANSLATE_PATHNAME(oldpath);
MUST_TRANSLATE_PATHNAME(newpath);
/* TODO(ophestra): replace with wrapper after 05ce67fea99ca09cd4b6625cff7aec9cc222dd5a reaches a release */
if (syscall(__NR_renameat2, priv->dirfd, oldpath, priv->dirfd, newpath, flags) == -1)
return -errno;
return 0;
}
int sharefs_truncate(const char *pathname, off_t length, struct fuse_file_info *fi) {
int fd;
int ret;
struct fuse_context *ctx;
struct sharefs_private *priv;
GET_CONTEXT_PRIV(ctx, priv);
MUST_TRANSLATE_PATHNAME(pathname);
(void)fi;
if ((fd = openat(priv->dirfd, pathname, O_WRONLY | O_CLOEXEC)) == -1)
return -errno;
if ((ret = ftruncate(fd, length)) == -1)
ret = -errno;
close(fd);
return ret;
}
int sharefs_utimens(const char *pathname, const struct timespec times[2], struct fuse_file_info *fi) {
struct fuse_context *ctx;
struct sharefs_private *priv;
GET_CONTEXT_PRIV(ctx, priv);
MUST_TRANSLATE_PATHNAME(pathname);
(void)fi;
if (utimensat(priv->dirfd, pathname, times, AT_SYMLINK_NOFOLLOW) == -1)
return -errno;
return 0;
}
int sharefs_create(const char *pathname, mode_t mode, struct fuse_file_info *fi) {
int fd;
struct fuse_context *ctx;
struct sharefs_private *priv;
GET_CONTEXT_PRIV(ctx, priv);
MUST_TRANSLATE_PATHNAME(pathname);
(void)mode;
if ((fd = openat(priv->dirfd, pathname, fi->flags & ~SHAREFS_FORBIDDEN_FLAGS, SHAREFS_PERM_REG)) == -1)
return -errno;
fi->fh = fd;
return 0;
}
int sharefs_open(const char *pathname, struct fuse_file_info *fi) {
int fd;
struct fuse_context *ctx;
struct sharefs_private *priv;
GET_CONTEXT_PRIV(ctx, priv);
MUST_TRANSLATE_PATHNAME(pathname);
if ((fd = openat(priv->dirfd, pathname, fi->flags & ~SHAREFS_FORBIDDEN_FLAGS)) == -1)
return -errno;
fi->fh = fd;
return 0;
}
int sharefs_read(const char *pathname, char *buf, size_t count, off_t offset, struct fuse_file_info *fi) {
int ret;
(void)pathname;
if ((ret = pread(fi->fh, buf, count, offset)) == -1)
return -errno;
return ret;
}
int sharefs_write(const char *pathname, const char *buf, size_t count, off_t offset, struct fuse_file_info *fi) {
int ret;
(void)pathname;
if ((ret = pwrite(fi->fh, buf, count, offset)) == -1)
return -errno;
return ret;
}
int sharefs_statfs(const char *pathname, struct statvfs *statbuf) {
int fd;
int ret;
struct fuse_context *ctx;
struct sharefs_private *priv;
GET_CONTEXT_PRIV(ctx, priv);
MUST_TRANSLATE_PATHNAME(pathname);
if ((fd = openat(priv->dirfd, pathname, O_RDONLY | O_CLOEXEC)) == -1)
return -errno;
if ((ret = fstatvfs(fd, statbuf)) == -1)
ret = -errno;
close(fd);
return ret;
}
int sharefs_release(const char *pathname, struct fuse_file_info *fi) {
(void)pathname;
return close(fi->fh);
}
int sharefs_fsync(const char *pathname, int datasync, struct fuse_file_info *fi) {
(void)pathname;
if (datasync ? fdatasync(fi->fh) : fsync(fi->fh) == -1)
return -errno;
return 0;
}

View File

@@ -1,34 +0,0 @@
#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 12)
#include <fuse.h>
#include <fuse_lowlevel.h> /* for fuse_cmdline_help */
#if (FUSE_VERSION < FUSE_MAKE_VERSION(3, 12))
#error This package requires libfuse >= v3.12
#endif
#define SHAREFS_MEDIA_RW_ID (1 << 10) - 1 /* owning gid presented to userspace */
#define SHAREFS_PERM_DIR 0700 /* permission bits for directories presented to userspace */
#define SHAREFS_PERM_REG 0600 /* permission bits for regular files presented to userspace */
#define SHAREFS_FORBIDDEN_FLAGS O_DIRECT /* these open flags are cleared unconditionally */
/* sharefs_private is populated by sharefs_init and contains process-wide context */
struct sharefs_private {
int dirfd; /* source dirfd opened during sharefs_init */
uintptr_t setup; /* cgo handle of opaque setup state */
};
int sharefs_getattr(const char *pathname, struct stat *statbuf, struct fuse_file_info *fi);
int sharefs_readdir(const char *pathname, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags);
int sharefs_mkdir(const char *pathname, mode_t mode);
int sharefs_unlink(const char *pathname);
int sharefs_rmdir(const char *pathname);
int sharefs_rename(const char *oldpath, const char *newpath, unsigned int flags);
int sharefs_truncate(const char *pathname, off_t length, struct fuse_file_info *fi);
int sharefs_utimens(const char *pathname, const struct timespec times[2], struct fuse_file_info *fi);
int sharefs_create(const char *pathname, mode_t mode, struct fuse_file_info *fi);
int sharefs_open(const char *pathname, struct fuse_file_info *fi);
int sharefs_read(const char *pathname, char *buf, size_t count, off_t offset, struct fuse_file_info *fi);
int sharefs_write(const char *pathname, const char *buf, size_t count, off_t offset, struct fuse_file_info *fi);
int sharefs_statfs(const char *pathname, struct statvfs *statbuf);
int sharefs_release(const char *pathname, struct fuse_file_info *fi);
int sharefs_fsync(const char *pathname, int datasync, struct fuse_file_info *fi);

View File

@@ -1,556 +0,0 @@
package main
/*
#cgo pkg-config: --static fuse3
#include "fuse-operations.h"
#include <stdlib.h>
#include <string.h>
extern void *sharefs_init(struct fuse_conn_info *conn, struct fuse_config *cfg);
extern void sharefs_destroy(void *private_data);
typedef void (*closure)();
static inline struct fuse_opt _FUSE_OPT_END() { return (struct fuse_opt)FUSE_OPT_END; };
*/
import "C"
import (
"context"
"encoding/gob"
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"os/signal"
"path"
"runtime"
"runtime/cgo"
"strconv"
"syscall"
"unsafe"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/std"
"hakurei.app/hst"
"hakurei.app/internal/helper/proc"
"hakurei.app/internal/info"
"hakurei.app/message"
)
type (
// closure represents a C function pointer.
closure = C.closure
// fuseArgs represents the fuse_args structure.
fuseArgs = C.struct_fuse_args
// setupState holds state used for setup. Its cgo handle is included in
// sharefs_private and considered opaque to non-setup callbacks.
setupState struct {
// Whether sharefs_init failed.
initFailed bool
// Whether to create source directory as root.
mkdir bool
// Open file descriptor to fuse.
Fuse int
// Pathname to open for dirfd.
Source *check.Absolute
// New uid and gid to set by sharefs_init when starting as root.
Setuid, Setgid int
}
)
func init() { gob.Register(new(setupState)) }
// destroySetup invalidates the setup [cgo.Handle] in a sharefs_private structure.
func destroySetup(private_data unsafe.Pointer) (ok bool) {
if private_data == nil {
return false
}
priv := (*C.struct_sharefs_private)(private_data)
if h := cgo.Handle(priv.setup); h != 0 {
priv.setup = 0
h.Delete()
ok = true
}
return
}
//export sharefs_init
func sharefs_init(_ *C.struct_fuse_conn_info, cfg *C.struct_fuse_config) unsafe.Pointer {
ctx := C.fuse_get_context()
priv := (*C.struct_sharefs_private)(ctx.private_data)
setup := cgo.Handle(priv.setup).Value().(*setupState)
if os.Geteuid() == 0 {
log.Println("filesystem daemon must not run as root")
goto fail
}
cfg.use_ino = C.true
cfg.direct_io = C.false
// getattr is context-dependent
cfg.attr_timeout = 0
cfg.entry_timeout = 0
cfg.negative_timeout = 0
// all future filesystem operations happen through this dirfd
if fd, err := syscall.Open(setup.Source.String(), syscall.O_DIRECTORY|syscall.O_RDONLY|syscall.O_CLOEXEC, 0); err != nil {
log.Printf("cannot open %q: %v", setup.Source, err)
goto fail
} else if err = syscall.Fchdir(fd); err != nil {
_ = syscall.Close(fd)
log.Printf("cannot enter %q: %s", setup.Source, err)
goto fail
} else {
priv.dirfd = C.int(fd)
}
return ctx.private_data
fail:
setup.initFailed = true
C.fuse_exit(ctx.fuse)
return nil
}
//export sharefs_destroy
func sharefs_destroy(private_data unsafe.Pointer) {
if private_data != nil {
destroySetup(private_data)
priv := (*C.struct_sharefs_private)(private_data)
if err := syscall.Close(int(priv.dirfd)); err != nil {
log.Printf("cannot close source directory: %v", err)
}
}
}
// showHelp prints the help message.
func showHelp(args *fuseArgs) {
executableName := sharefsName
if args.argc > 0 {
executableName = path.Base(C.GoString(*args.argv))
} else if name, err := os.Executable(); err == nil {
executableName = path.Base(name)
}
fmt.Printf("usage: %s [options] <mountpoint>\n\n", executableName)
fmt.Println("Filesystem options:")
fmt.Println(" -o source=/data/media source directory to be mounted")
fmt.Println(" -o setuid=1023 uid to run as when starting as root")
fmt.Println(" -o setgid=1023 gid to run as when starting as root")
fmt.Println("\nFUSE options:")
C.fuse_cmdline_help()
C.fuse_lib_help(args)
}
// parseOpts parses fuse options via fuse_opt_parse.
func parseOpts(args *fuseArgs, setup *setupState, log *log.Logger) (ok bool) {
var unsafeOpts struct {
// Pathname to writable source directory.
source *C.char
// Whether to create source directory as root.
mkdir C.int
// Decimal string representation of uid to set when running as root.
setuid *C.char
// Decimal string representation of gid to set when running as root.
setgid *C.char
// Decimal string representation of open file descriptor to read setupState from.
// This is an internal detail for containerisation and must not be specified directly.
setup *C.char
}
if C.fuse_opt_parse(args, unsafe.Pointer(&unsafeOpts), &[]C.struct_fuse_opt{
{templ: C.CString("source=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.source)), value: 0},
{templ: C.CString("mkdir"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.mkdir)), value: 1},
{templ: C.CString("setuid=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setuid)), value: 0},
{templ: C.CString("setgid=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setgid)), value: 0},
{templ: C.CString("setup=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setup)), value: 0},
C._FUSE_OPT_END(),
}[0], nil) == -1 {
return false
}
if unsafeOpts.source != nil {
defer C.free(unsafe.Pointer(unsafeOpts.source))
}
if unsafeOpts.setuid != nil {
defer C.free(unsafe.Pointer(unsafeOpts.setuid))
}
if unsafeOpts.setgid != nil {
defer C.free(unsafe.Pointer(unsafeOpts.setgid))
}
if unsafeOpts.setup != nil {
defer C.free(unsafe.Pointer(unsafeOpts.setup))
if v, err := strconv.Atoi(C.GoString(unsafeOpts.setup)); err != nil || v < 3 {
log.Println("invalid value for option setup")
return false
} else {
r := os.NewFile(uintptr(v), "setup")
defer func() {
if err = r.Close(); err != nil {
log.Println(err)
}
}()
if err = gob.NewDecoder(r).Decode(setup); err != nil {
log.Println(err)
return false
}
}
if setup.Fuse < 3 {
log.Println("invalid file descriptor", setup.Fuse)
return false
}
return true
}
if unsafeOpts.source == nil {
showHelp(args)
return false
} else if a, err := check.NewAbs(C.GoString(unsafeOpts.source)); err != nil {
log.Println(err)
return false
} else {
setup.Source = a
}
setup.mkdir = unsafeOpts.mkdir != 0
if unsafeOpts.setuid == nil {
setup.Setuid = -1
} else if v, err := strconv.Atoi(C.GoString(unsafeOpts.setuid)); err != nil || v <= 0 {
log.Println("invalid value for option setuid")
return false
} else {
setup.Setuid = v
}
if unsafeOpts.setgid == nil {
setup.Setgid = -1
} else if v, err := strconv.Atoi(C.GoString(unsafeOpts.setgid)); err != nil || v <= 0 {
log.Println("invalid value for option setgid")
return false
} else {
setup.Setgid = v
}
return true
}
// copyArgs returns a heap allocated copy of an argument slice in fuse_args representation.
func copyArgs(s ...string) fuseArgs {
if len(s) == 0 {
return fuseArgs{argc: 0, argv: nil, allocated: 0}
}
args := unsafe.Slice((**C.char)(C.malloc(C.size_t(uintptr(len(s))*unsafe.Sizeof(s[0])))), len(s))
for i, arg := range s {
args[i] = C.CString(arg)
}
return fuseArgs{argc: C.int(len(s)), argv: &args[0], allocated: 1}
}
// freeArgs frees the contents of argument list.
func freeArgs(args *fuseArgs) { C.fuse_opt_free_args(args) }
// unsafeAddArgument adds an argument to fuseArgs via fuse_opt_add_arg.
// The last byte of arg must be 0.
func unsafeAddArgument(args *fuseArgs, arg string) {
C.fuse_opt_add_arg(args, (*C.char)(unsafe.Pointer(unsafe.StringData(arg))))
}
func _main(s ...string) (exitCode int) {
msg := message.New(log.Default())
container.TryArgv0(msg)
runtime.LockOSThread()
// don't mask creation mode, kernel already did that
syscall.Umask(0)
var pinner runtime.Pinner
defer pinner.Unpin()
args := copyArgs(s...)
defer freeArgs(&args)
// this causes the kernel to enforce access control based on
// struct stat populated by sharefs_getattr
unsafeAddArgument(&args, "-odefault_permissions\x00")
var priv C.struct_sharefs_private
pinner.Pin(&priv)
var setup setupState
priv.setup = C.uintptr_t(cgo.NewHandle(&setup))
defer destroySetup(unsafe.Pointer(&priv))
var opts C.struct_fuse_cmdline_opts
if C.fuse_parse_cmdline(&args, &opts) != 0 {
return 1
}
if opts.mountpoint != nil {
defer C.free(unsafe.Pointer(opts.mountpoint))
}
if opts.show_version != 0 {
fmt.Println("hakurei version", info.Version())
fmt.Println("FUSE library version", C.GoString(C.fuse_pkgversion()))
C.fuse_lowlevel_version()
return 0
}
if opts.show_help != 0 {
showHelp(&args)
return 0
} else if opts.mountpoint == nil {
log.Println("no mountpoint specified")
return 2
} else {
// hack to keep fuse_parse_cmdline happy in the container
mountpoint := C.GoString(opts.mountpoint)
pathnameArg := -1
for i, arg := range s {
if arg == mountpoint {
pathnameArg = i
break
}
}
if pathnameArg < 0 {
log.Println("mountpoint must be absolute")
return 2
}
s[pathnameArg] = container.Nonexistent
}
if !parseOpts(&args, &setup, msg.GetLogger()) {
return 1
}
asRoot := os.Geteuid() == 0
if asRoot {
if setup.Setuid <= 0 || setup.Setgid <= 0 {
log.Println("setuid and setgid must not be 0")
return 1
}
if setup.Fuse >= 3 {
log.Println("filesystem daemon must not run as root")
return 1
}
if setup.mkdir {
if err := os.MkdirAll(setup.Source.String(), 0700); err != nil {
if !errors.Is(err, os.ErrExist) {
log.Println(err)
return 1
}
// skip setup for existing source directory
} else if err = os.Chown(setup.Source.String(), setup.Setuid, setup.Setgid); err != nil {
log.Println(err)
return 1
}
}
} else if setup.Fuse < 3 && (setup.Setuid > 0 || setup.Setgid > 0) {
log.Println("setuid and setgid has no effect when not starting as root")
return 1
} else if setup.mkdir {
log.Println("mkdir has no effect when not starting as root")
return 1
}
op := C.struct_fuse_operations{
init: closure(C.sharefs_init),
destroy: closure(C.sharefs_destroy),
// implemented in fuse-helper.c
getattr: closure(C.sharefs_getattr),
readdir: closure(C.sharefs_readdir),
mkdir: closure(C.sharefs_mkdir),
unlink: closure(C.sharefs_unlink),
rmdir: closure(C.sharefs_rmdir),
rename: closure(C.sharefs_rename),
truncate: closure(C.sharefs_truncate),
utimens: closure(C.sharefs_utimens),
create: closure(C.sharefs_create),
open: closure(C.sharefs_open),
read: closure(C.sharefs_read),
write: closure(C.sharefs_write),
statfs: closure(C.sharefs_statfs),
release: closure(C.sharefs_release),
fsync: closure(C.sharefs_fsync),
}
fuse := C.fuse_new_fn(&args, &op, C.size_t(unsafe.Sizeof(op)), unsafe.Pointer(&priv))
if fuse == nil {
return 3
}
defer C.fuse_destroy(fuse)
se := C.fuse_get_session(fuse)
if setup.Fuse < 3 {
// unconfined, set up mount point and container
if C.fuse_mount(fuse, opts.mountpoint) != 0 {
return 4
}
// unmounted by initial process
defer func() {
if exitCode == 5 {
C.fuse_unmount(fuse)
}
}()
if asRoot {
if err := syscall.Setresgid(setup.Setgid, setup.Setgid, setup.Setgid); err != nil {
log.Printf("cannot set gid: %v", err)
return 5
}
if err := syscall.Setgroups(nil); err != nil {
log.Printf("cannot set supplementary groups: %v", err)
return 5
}
if err := syscall.Setresuid(setup.Setuid, setup.Setuid, setup.Setuid); err != nil {
log.Printf("cannot set uid: %v", err)
return 5
}
}
msg.SwapVerbose(opts.debug != 0)
ctx := context.Background()
if opts.foreground != 0 {
c, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
defer cancel()
ctx = c
}
z := container.New(ctx, msg)
z.AllowOrphan = opts.foreground == 0
z.Env = os.Environ()
// keep fuse_parse_cmdline happy in the container
z.Tmpfs(check.MustAbs(container.Nonexistent), 1<<10, 0755)
if a, err := check.NewAbs(container.MustExecutable(msg)); err != nil {
log.Println(err)
return 5
} else {
z.Path = a
}
z.Args = s
z.ForwardCancel = true
z.SeccompPresets |= std.PresetStrict
z.ParentPerm = 0700
z.Bind(setup.Source, setup.Source, std.BindWritable)
if !z.AllowOrphan {
z.WaitDelay = hst.WaitDelayMax
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
}
z.Bind(z.Path, z.Path, 0)
setup.Fuse = int(proc.ExtraFileSlice(&z.ExtraFiles, os.NewFile(uintptr(C.fuse_session_fd(se)), "fuse")))
var setupWriter io.WriteCloser
if fd, w, err := container.Setup(&z.ExtraFiles); err != nil {
log.Println(err)
return 5
} else {
z.Args = append(z.Args, "-osetup="+strconv.Itoa(fd))
setupWriter = w
}
if err := z.Start(); err != nil {
if m, ok := message.GetMessage(err); ok {
log.Println(m)
} else {
log.Println(err)
}
return 5
}
if err := z.Serve(); err != nil {
if m, ok := message.GetMessage(err); ok {
log.Println(m)
} else {
log.Println(err)
}
return 5
}
if err := gob.NewEncoder(setupWriter).Encode(&setup); err != nil {
log.Println(err)
return 5
} else if err = setupWriter.Close(); err != nil {
log.Println(err)
}
if !z.AllowOrphan {
if err := z.Wait(); err != nil {
var exitError *exec.ExitError
if !errors.As(err, &exitError) || exitError == nil {
log.Println(err)
return 5
}
switch code := exitError.ExitCode(); syscall.Signal(code & 0x7f) {
case syscall.SIGINT:
case syscall.SIGTERM:
default:
return code
}
}
}
return 0
} else { // confined
C.free(unsafe.Pointer(opts.mountpoint))
// must be heap allocated
opts.mountpoint = C.CString("/dev/fd/" + strconv.Itoa(setup.Fuse))
if err := os.Chdir("/"); err != nil {
log.Println(err)
}
}
if C.fuse_mount(fuse, opts.mountpoint) != 0 {
return 4
}
defer C.fuse_unmount(fuse)
if C.fuse_set_signal_handlers(se) != 0 {
return 6
}
defer C.fuse_remove_signal_handlers(se)
if opts.singlethread != 0 {
if C.fuse_loop(fuse) != 0 {
return 8
}
} else {
loopConfig := C.fuse_loop_cfg_create()
if loopConfig == nil {
return 7
}
defer C.fuse_loop_cfg_destroy(loopConfig)
C.fuse_loop_cfg_set_clone_fd(loopConfig, C.uint(opts.clone_fd))
C.fuse_loop_cfg_set_idle_threads(loopConfig, opts.max_idle_threads)
C.fuse_loop_cfg_set_max_threads(loopConfig, opts.max_threads)
if C.fuse_loop_mt(fuse, loopConfig) != 0 {
return 8
}
}
if setup.initFailed {
return 1
}
return 0
}

View File

@@ -1,113 +0,0 @@
package main
import (
"bytes"
"log"
"reflect"
"testing"
"hakurei.app/container/check"
)
func TestParseOpts(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
args []string
want setupState
wantLog string
wantOk bool
}{
{"zero length", []string{}, setupState{}, "", false},
{"not absolute", []string{"sharefs",
"-o", "source=nonexistent",
"-o", "setuid=1023",
"-o", "setgid=1023",
}, setupState{}, "sharefs: path \"nonexistent\" is not absolute\n", false},
{"not specified", []string{"sharefs",
"-o", "setuid=1023",
"-o", "setgid=1023",
}, setupState{}, "", false},
{"invalid setuid", []string{"sharefs",
"-o", "source=/proc/nonexistent",
"-o", "setuid=ff",
"-o", "setgid=1023",
}, setupState{
Source: check.MustAbs("/proc/nonexistent"),
}, "sharefs: invalid value for option setuid\n", false},
{"invalid setgid", []string{"sharefs",
"-o", "source=/proc/nonexistent",
"-o", "setuid=1023",
"-o", "setgid=ff",
}, setupState{
Source: check.MustAbs("/proc/nonexistent"),
Setuid: 1023,
}, "sharefs: invalid value for option setgid\n", false},
{"simple", []string{"sharefs",
"-o", "source=/proc/nonexistent",
}, setupState{
Source: check.MustAbs("/proc/nonexistent"),
Setuid: -1,
Setgid: -1,
}, "", true},
{"root", []string{"sharefs",
"-o", "source=/proc/nonexistent",
"-o", "setuid=1023",
"-o", "setgid=1023",
}, setupState{
Source: check.MustAbs("/proc/nonexistent"),
Setuid: 1023,
Setgid: 1023,
}, "", true},
{"setuid", []string{"sharefs",
"-o", "source=/proc/nonexistent",
"-o", "setuid=1023",
}, setupState{
Source: check.MustAbs("/proc/nonexistent"),
Setuid: 1023,
Setgid: -1,
}, "", true},
{"setgid", []string{"sharefs",
"-o", "source=/proc/nonexistent",
"-o", "setgid=1023",
}, setupState{
Source: check.MustAbs("/proc/nonexistent"),
Setuid: -1,
Setgid: 1023,
}, "", true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var (
got setupState
buf bytes.Buffer
)
args := copyArgs(tc.args...)
defer freeArgs(&args)
unsafeAddArgument(&args, "-odefault_permissions\x00")
if ok := parseOpts(&args, &got, log.New(&buf, "sharefs: ", 0)); ok != tc.wantOk {
t.Errorf("parseOpts: ok = %v, want %v", ok, tc.wantOk)
}
if !reflect.DeepEqual(&got, &tc.want) {
t.Errorf("parseOpts: setup = %#v, want %#v", got, tc.want)
}
if buf.String() != tc.wantLog {
t.Errorf("parseOpts: log =\n%s\nwant\n%s", buf.String(), tc.wantLog)
}
})
}
}

View File

@@ -1,31 +0,0 @@
package main
import (
"log"
"os"
"slices"
)
// sharefsName is the prefix used by log.std in the sharefs process.
const sharefsName = "sharefs"
// handleMountArgs returns an alternative, libfuse-compatible args slice for
// args passed by mount -t fuse.sharefs [options] sharefs <mountpoint>.
//
// In this case, args always has a length of 5 with index 0 being what comes
// after "fuse." in the filesystem type, 1 is the uninterpreted string passed
// to mount (sharefsName is used as the magic string to enable this hack),
// 2 is passed through to libfuse as mountpoint, and 3 is always "-o".
func handleMountArgs(args []string) []string {
if len(args) == 5 && args[1] == sharefsName && args[3] == "-o" {
return []string{sharefsName, args[2], "-o", args[4]}
}
return slices.Clone(args)
}
func main() {
log.SetFlags(0)
log.SetPrefix(sharefsName + ": ")
os.Exit(_main(handleMountArgs(os.Args)...))
}

View File

@@ -1,29 +0,0 @@
package main
import (
"slices"
"testing"
)
func TestHandleMountArgs(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
args []string
want []string
}{
{"nil", nil, nil},
{"passthrough", []string{"sharefs", "-V"}, []string{"sharefs", "-V"}},
{"replace", []string{"/sbin/sharefs", "sharefs", "/sdcard", "-o", "rw"}, []string{"sharefs", "/sdcard", "-o", "rw"}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := handleMountArgs(tc.args); !slices.Equal(got, tc.want) {
t.Errorf("handleMountArgs: %q, want %q", got, tc.want)
}
})
}
}

View File

@@ -1,41 +0,0 @@
{ pkgs, ... }:
{
users.users = {
alice = {
isNormalUser = true;
description = "Alice Foobar";
password = "foobar";
uid = 1000;
};
};
home-manager.users.alice.home.stateVersion = "24.11";
# Automatically login on tty1 as a normal user:
services.getty.autologinUser = "alice";
environment = {
# For benchmarking sharefs:
systemPackages = [ pkgs.fsmark ];
};
virtualisation = {
diskSize = 6 * 1024;
qemu.options = [
# Increase test performance:
"-smp 8"
];
};
environment.hakurei = rec {
enable = true;
stateDir = "/var/lib/hakurei";
sharefs.source = "${stateDir}/sdcard";
users.alice = 0;
extraHomeConfig = {
home.stateVersion = "23.05";
};
};
}

View File

@@ -1,44 +0,0 @@
{
testers,
system,
self,
}:
testers.nixosTest {
name = "sharefs";
nodes.machine =
{ options, pkgs, ... }:
let
fhs =
let
hakurei = options.environment.hakurei.package.default;
in
pkgs.buildFHSEnv {
pname = "hakurei-fhs";
inherit (hakurei) version;
targetPkgs = _: hakurei.targetPkgs;
extraOutputsToInstall = [ "dev" ];
profile = ''
export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH"
'';
};
in
{
environment.systemPackages = [
# For go tests:
(pkgs.writeShellScriptBin "sharefs-workload-hakurei-tests" ''
cp -r "${self.packages.${system}.hakurei.src}" "/sdcard/hakurei" && cd "/sdcard/hakurei"
${fhs}/bin/hakurei-fhs -c 'CC="clang -O3 -Werror" go test ./...'
'')
];
imports = [
./configuration.nix
self.nixosModules.hakurei
self.inputs.home-manager.nixosModules.home-manager
];
};
testScript = builtins.readFile ./test.py;
}

View File

@@ -1,60 +0,0 @@
start_all()
machine.wait_for_unit("multi-user.target")
# To check sharefs version:
print(machine.succeed("sharefs -V"))
# Make sure sharefs started:
machine.wait_for_unit("sdcard.mount")
machine.succeed("mkdir /mnt")
def check_bad_opts_output(opts, want, source="/etc", privileged=False):
output = machine.fail(("" if privileged else "sudo -u alice -i ") + f"sharefs -f -o source={source},{opts} /mnt 2>&1")
if output != want:
raise Exception(f"unexpected output: {output}")
# Malformed setuid/setgid representation:
check_bad_opts_output("setuid=ff", "sharefs: invalid value for option setuid\n")
check_bad_opts_output("setgid=ff", "sharefs: invalid value for option setgid\n")
# Bounds check for setuid/setgid:
check_bad_opts_output("setuid=0", "sharefs: invalid value for option setuid\n")
check_bad_opts_output("setgid=0", "sharefs: invalid value for option setgid\n")
check_bad_opts_output("setuid=-1", "sharefs: invalid value for option setuid\n")
check_bad_opts_output("setgid=-1", "sharefs: invalid value for option setgid\n")
# Non-root setuid/setgid:
check_bad_opts_output("setuid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
check_bad_opts_output("setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
check_bad_opts_output("setuid=1023,setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
check_bad_opts_output("mkdir", "sharefs: mkdir has no effect when not starting as root\n")
# Starting as root without setuid/setgid:
check_bad_opts_output("allow_other", "sharefs: setuid and setgid must not be 0\n", privileged=True)
check_bad_opts_output("setuid=1023", "sharefs: setuid and setgid must not be 0\n", privileged=True)
check_bad_opts_output("setgid=1023", "sharefs: setuid and setgid must not be 0\n", privileged=True)
# Make sure nothing actually got mounted:
machine.fail("umount /mnt")
machine.succeed("rmdir /mnt")
# Unprivileged mount/unmount:
machine.succeed("sudo -u alice -i mkdir /home/alice/{sdcard,persistent}")
machine.succeed("sudo -u alice -i sharefs -o source=/home/alice/persistent /home/alice/sdcard")
machine.succeed("sudo -u alice -i touch /home/alice/sdcard/check")
machine.succeed("sudo -u alice -i umount /home/alice/sdcard")
machine.succeed("sudo -u alice -i rm /home/alice/persistent/check")
machine.succeed("sudo -u alice -i rmdir /home/alice/{sdcard,persistent}")
# Benchmark sharefs:
machine.succeed("fs_mark -v -d /sdcard/fs_mark -l /tmp/fs_log.txt")
machine.copy_from_vm("/tmp/fs_log.txt", "")
# Check permissions:
machine.succeed("sudo -u sharefs touch /var/lib/hakurei/sdcard/fs_mark/.check")
machine.succeed("sudo -u sharefs rm /var/lib/hakurei/sdcard/fs_mark/.check")
machine.succeed("sudo -u alice rm -rf /sdcard/fs_mark")
machine.fail("ls /var/lib/hakurei/sdcard/fs_mark")
# Run hakurei tests on sharefs:
machine.succeed("sudo -u alice -i sharefs-workload-hakurei-tests")

View File

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

View File

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

View File

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

107
container/absolute.go Normal file
View File

@@ -0,0 +1,107 @@
package container
import (
"encoding/json"
"errors"
"fmt"
"path"
"slices"
"strings"
"syscall"
)
// AbsoluteError is returned by [NewAbs] and holds the invalid pathname.
type AbsoluteError struct {
Pathname string
}
func (e *AbsoluteError) Error() string { return fmt.Sprintf("path %q is not absolute", e.Pathname) }
func (e *AbsoluteError) Is(target error) bool {
var ce *AbsoluteError
if !errors.As(target, &ce) {
return errors.Is(target, syscall.EINVAL)
}
return *e == *ce
}
// Absolute holds a pathname checked to be absolute.
type Absolute struct {
pathname string
}
// isAbs wraps [path.IsAbs] in case additional checks are added in the future.
func isAbs(pathname string) bool { return path.IsAbs(pathname) }
func (a *Absolute) String() string {
if a.pathname == zeroString {
panic("attempted use of zero Absolute")
}
return a.pathname
}
func (a *Absolute) Is(v *Absolute) bool {
if a == nil && v == nil {
return true
}
return a != nil && v != nil &&
a.pathname != zeroString && v.pathname != zeroString &&
a.pathname == v.pathname
}
// NewAbs checks pathname and returns a new [Absolute] if pathname is absolute.
func NewAbs(pathname string) (*Absolute, error) {
if !isAbs(pathname) {
return nil, &AbsoluteError{pathname}
}
return &Absolute{pathname}, nil
}
// MustAbs calls [NewAbs] and panics on error.
func MustAbs(pathname string) *Absolute {
if a, err := NewAbs(pathname); err != nil {
panic(err.Error())
} else {
return a
}
}
// Append calls [path.Join] with [Absolute] as the first element.
func (a *Absolute) Append(elem ...string) *Absolute {
return &Absolute{path.Join(append([]string{a.String()}, elem...)...)}
}
// Dir calls [path.Dir] with [Absolute] as its argument.
func (a *Absolute) Dir() *Absolute { return &Absolute{path.Dir(a.String())} }
func (a *Absolute) GobEncode() ([]byte, error) { return []byte(a.String()), nil }
func (a *Absolute) GobDecode(data []byte) error {
pathname := string(data)
if !isAbs(pathname) {
return &AbsoluteError{pathname}
}
a.pathname = pathname
return nil
}
func (a *Absolute) MarshalJSON() ([]byte, error) { return json.Marshal(a.String()) }
func (a *Absolute) UnmarshalJSON(data []byte) error {
var pathname string
if err := json.Unmarshal(data, &pathname); err != nil {
return err
}
if !isAbs(pathname) {
return &AbsoluteError{pathname}
}
a.pathname = pathname
return nil
}
// SortAbs calls [slices.SortFunc] for a slice of [Absolute].
func SortAbs(x []*Absolute) {
slices.SortFunc(x, func(a, b *Absolute) int { return strings.Compare(a.String(), b.String()) })
}
// CompactAbs calls [slices.CompactFunc] for a slice of [Absolute].
func CompactAbs(s []*Absolute) []*Absolute {
return slices.CompactFunc(s, func(a *Absolute, b *Absolute) bool { return a.String() == b.String() })
}

View File

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

View File

@@ -3,18 +3,15 @@ package container
import (
"encoding/gob"
"fmt"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
)
func init() { gob.Register(new(AutoEtcOp)) }
// Etc appends an [Op] that expands host /etc into a toplevel symlink mirror with /etc semantics.
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
func (f *Ops) Etc(host *check.Absolute, prefix string) *Ops {
func (f *Ops) Etc(host *Absolute, prefix string) *Ops {
e := &AutoEtcOp{prefix}
f.Mkdir(fhs.AbsEtc, 0755)
f.Mkdir(AbsFHSEtc, 0755)
f.Bind(host, e.hostPath(), 0)
*f = append(*f, e)
return f
@@ -30,7 +27,7 @@ func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
}
state.nonrepeatable |= nrAutoEtc
const target = sysrootPath + fhs.Etc
const target = sysrootPath + FHSEtc
rel := e.hostRel() + "/"
if err := k.mkdirAll(target, 0755); err != nil {
@@ -45,7 +42,7 @@ func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
case ".host", "passwd", "group":
case "mtab":
if err = k.symlink(fhs.Proc+"mounts", target+n); err != nil {
if err = k.symlink(FHSProc+"mounts", target+n); err != nil {
return err
}
@@ -59,14 +56,13 @@ func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
return nil
}
func (e *AutoEtcOp) late(*setupState, syscallDispatcher) error { return nil }
func (e *AutoEtcOp) hostPath() *check.Absolute { return fhs.AbsEtc.Append(e.hostRel()) }
func (e *AutoEtcOp) hostPath() *Absolute { return AbsFHSEtc.Append(e.hostRel()) }
func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix }
func (e *AutoEtcOp) Is(op Op) bool {
ve, ok := op.(*AutoEtcOp)
return ok && e.Valid() && ve.Valid() && *e == *ve
}
func (*AutoEtcOp) prefix() (string, bool) { return "setting up", true }
func (*AutoEtcOp) prefix() string { return "setting up" }
func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }

View File

@@ -5,15 +5,11 @@ import (
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
)
func TestAutoEtcOp(t *testing.T) {
t.Parallel()
t.Run("nonrepeatable", func(t *testing.T) {
t.Parallel()
wantErr := OpRepeatError("autoetc")
if err := (&AutoEtcOp{Prefix: "81ceabb30d37bbdb3868004629cb84e9"}).apply(&setupState{nonrepeatable: nrAutoEtc}, nil); !errors.Is(err, wantErr) {
t.Errorf("apply: error = %v, want %v", err, wantErr)
@@ -260,11 +256,11 @@ func TestAutoEtcOp(t *testing.T) {
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"pd", new(Ops).Etc(check.MustAbs("/etc/"), "048090b6ed8f9ebb10e275ff5d8c0659"), Ops{
&MkdirOp{Path: check.MustAbs("/etc/"), Perm: 0755},
{"pd", new(Ops).Etc(MustAbs("/etc/"), "048090b6ed8f9ebb10e275ff5d8c0659"), Ops{
&MkdirOp{Path: MustAbs("/etc/"), Perm: 0755},
&BindMountOp{
Source: check.MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
},
&AutoEtcOp{Prefix: "048090b6ed8f9ebb10e275ff5d8c0659"},
}},
@@ -283,7 +279,6 @@ func TestAutoEtcOp(t *testing.T) {
})
t.Run("host path rel", func(t *testing.T) {
t.Parallel()
op := &AutoEtcOp{Prefix: "048090b6ed8f9ebb10e275ff5d8c0659"}
wantHostPath := "/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"
wantHostRel := ".host/048090b6ed8f9ebb10e275ff5d8c0659"

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,132 +0,0 @@
// Package check provides types yielding values checked to meet a condition.
package check
import (
"encoding/json"
"errors"
"fmt"
"path"
"slices"
"strings"
"syscall"
"unique"
)
// AbsoluteError is returned by [NewAbs] and holds the invalid pathname.
type AbsoluteError string
func (e AbsoluteError) Error() string {
return fmt.Sprintf("path %q is not absolute", string(e))
}
func (e AbsoluteError) Is(target error) bool {
var ce AbsoluteError
if !errors.As(target, &ce) {
return errors.Is(target, syscall.EINVAL)
}
return e == ce
}
// Absolute holds a pathname checked to be absolute.
type Absolute struct{ pathname unique.Handle[string] }
// ok returns whether [Absolute] is not the zero value.
func (a *Absolute) ok() bool { return a != nil && *a != (Absolute{}) }
// unsafeAbs returns [check.Absolute] on any string value.
func unsafeAbs(pathname string) *Absolute {
return &Absolute{unique.Make(pathname)}
}
// String returns the checked pathname.
func (a *Absolute) String() string {
if !a.ok() {
panic("attempted use of zero Absolute")
}
return a.pathname.Value()
}
// Handle returns the underlying [unique.Handle].
func (a *Absolute) Handle() unique.Handle[string] {
return a.pathname
}
// Is efficiently compares the underlying pathname.
func (a *Absolute) Is(v *Absolute) bool {
if a == nil && v == nil {
return true
}
return a.ok() && v.ok() && a.pathname == v.pathname
}
// NewAbs checks pathname and returns a new [Absolute] if pathname is absolute.
func NewAbs(pathname string) (*Absolute, error) {
if !path.IsAbs(pathname) {
return nil, AbsoluteError(pathname)
}
return unsafeAbs(pathname), nil
}
// MustAbs calls [NewAbs] and panics on error.
func MustAbs(pathname string) *Absolute {
if a, err := NewAbs(pathname); err != nil {
panic(err)
} else {
return a
}
}
// Append calls [path.Join] with [Absolute] as the first element.
func (a *Absolute) Append(elem ...string) *Absolute {
return unsafeAbs(path.Join(append([]string{a.String()}, elem...)...))
}
// Dir calls [path.Dir] with [Absolute] as its argument.
func (a *Absolute) Dir() *Absolute { return unsafeAbs(path.Dir(a.String())) }
// GobEncode returns the checked pathname.
func (a *Absolute) GobEncode() ([]byte, error) {
return []byte(a.String()), nil
}
// GobDecode stores data if it represents an absolute pathname.
func (a *Absolute) GobDecode(data []byte) error {
pathname := string(data)
if !path.IsAbs(pathname) {
return AbsoluteError(pathname)
}
a.pathname = unique.Make(pathname)
return nil
}
// MarshalJSON returns a JSON representation of the checked pathname.
func (a *Absolute) MarshalJSON() ([]byte, error) {
return json.Marshal(a.String())
}
// UnmarshalJSON stores data if it represents an absolute pathname.
func (a *Absolute) UnmarshalJSON(data []byte) error {
var pathname string
if err := json.Unmarshal(data, &pathname); err != nil {
return err
}
if !path.IsAbs(pathname) {
return AbsoluteError(pathname)
}
a.pathname = unique.Make(pathname)
return nil
}
// SortAbs calls [slices.SortFunc] for a slice of [Absolute].
func SortAbs(x []*Absolute) {
slices.SortFunc(x, func(a, b *Absolute) int {
return strings.Compare(a.String(), b.String())
})
}
// CompactAbs calls [slices.CompactFunc] for a slice of [Absolute].
func CompactAbs(s []*Absolute) []*Absolute {
return slices.CompactFunc(s, func(a *Absolute, b *Absolute) bool {
return a.Is(b)
})
}

View File

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

View File

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

View File

@@ -11,40 +11,30 @@ import (
"os/exec"
"runtime"
"strconv"
"sync"
. "syscall"
"time"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/message"
)
const (
// CancelSignal is the signal expected by container init on context cancel.
// A custom [Container.Cancel] function must eventually deliver this signal.
CancelSignal = SIGUSR2
// Timeout for writing initParams to Container.setup.
initSetupTimeout = 5 * time.Second
CancelSignal = SIGTERM
)
type (
// Container represents a container environment being prepared or run.
// None of [Container] methods are safe for concurrent use.
Container struct {
// Whether the container init should stay alive after its parent terminates.
AllowOrphan bool
// Cgroup fd, nil to disable.
Cgroup *int
// ExtraFiles passed through to initial process in the container,
// with behaviour identical to its [exec.Cmd] counterpart.
ExtraFiles []*os.File
// param pipe for shim and init
setup *os.File
// param encoder for shim and init
setup *gob.Encoder
// cancels cmd
cancel context.CancelFunc
// closed after Wait returns
@@ -59,23 +49,22 @@ type (
cmd *exec.Cmd
ctx context.Context
msg message.Msg
Params
}
// Params holds container configuration and is safe to serialise.
Params struct {
// Working directory in the container.
Dir *check.Absolute
Dir *Absolute
// Initial process environment.
Env []string
// Pathname of initial process in the container.
Path *check.Absolute
Path *Absolute
// Initial process argv.
Args []string
// Deliver SIGINT to the initial process on context cancellation.
ForwardCancel bool
// Time to wait for processes lingering after the initial process terminates.
// time to wait for linger processes after death of initial process
AdoptWaitDelay time.Duration
// Mapped Uid in user namespace.
@@ -88,11 +77,11 @@ type (
*Ops
// Seccomp system call filter rules.
SeccompRules []std.NativeRule
SeccompRules []seccomp.NativeRule
// Extra seccomp flags.
SeccompFlags seccomp.ExportFlag
// Seccomp presets. Has no effect unless SeccompRules is zero-length.
SeccompPresets std.FilterPreset
SeccompPresets seccomp.FilterPreset
// Do not load seccomp program.
SeccompDisable bool
@@ -146,18 +135,11 @@ func (e *StartError) Error() string {
// Message returns a user-facing error message.
func (e *StartError) Message() string {
if e.Passthrough {
var (
numError *strconv.NumError
)
switch {
case errors.As(e.Err, new(*os.PathError)),
errors.As(e.Err, new(*os.SyscallError)):
return "cannot " + e.Err.Error()
case errors.As(e.Err, &numError) && numError != nil:
return "cannot parse " + strconv.Quote(numError.Num) + ": " + numError.Err.Error()
default:
return e.Err.Error()
}
@@ -168,63 +150,28 @@ func (e *StartError) Message() string {
return "cannot " + e.Error()
}
// for ensureCloseOnExec
var (
closeOnExecOnce sync.Once
closeOnExecErr error
)
// ensureCloseOnExec ensures all currently open file descriptors have the syscall.FD_CLOEXEC flag set.
// This is only ran once as it is intended to handle files left open by the parent, and any file opened
// on this side should already have syscall.FD_CLOEXEC set.
func ensureCloseOnExec() error {
closeOnExecOnce.Do(func() {
const fdPrefixPath = "/proc/self/fd/"
var entries []os.DirEntry
if entries, closeOnExecErr = os.ReadDir(fdPrefixPath); closeOnExecErr != nil {
return
}
var fd int
for _, ent := range entries {
if fd, closeOnExecErr = strconv.Atoi(ent.Name()); closeOnExecErr != nil {
break // not reached
}
CloseOnExec(fd)
}
})
if closeOnExecErr == nil {
return nil
}
return &StartError{Fatal: true, Step: "set FD_CLOEXEC on all open files", Err: closeOnExecErr, Passthrough: true}
}
// Start starts the container init. The init process blocks until Serve is called.
func (p *Container) Start() error {
if p == nil || p.cmd == nil ||
p.Ops == nil || len(*p.Ops) == 0 {
return errors.New("container: starting an invalid container")
}
if p.cmd.Process != nil {
if p.cmd != nil {
return errors.New("container: already started")
}
if err := ensureCloseOnExec(); err != nil {
return err
if p.Ops == nil || len(*p.Ops) == 0 {
return errors.New("container: starting an empty container")
}
ctx, cancel := context.WithCancel(p.ctx)
p.cancel = cancel
// map to overflow id to work around ownership checks
if p.Uid < 1 {
p.Uid = OverflowUid(p.msg)
p.Uid = OverflowUid()
}
if p.Gid < 1 {
p.Gid = OverflowGid(p.msg)
p.Gid = OverflowGid()
}
if !p.RetainSession {
p.SeccompPresets |= std.PresetDenyTTY
p.SeccompPresets |= seccomp.PresetDenyTTY
}
if p.AdoptWaitDelay == 0 {
@@ -235,26 +182,19 @@ func (p *Container) Start() error {
p.AdoptWaitDelay = 0
}
if p.cmd.Stdin == nil {
p.cmd.Stdin = p.Stdin
}
if p.cmd.Stdout == nil {
p.cmd.Stdout = p.Stdout
}
if p.cmd.Stderr == nil {
p.cmd.Stderr = p.Stderr
}
p.cmd = exec.CommandContext(ctx, MustExecutable())
p.cmd.Args = []string{initName}
p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
p.cmd.WaitDelay = p.WaitDelay
if p.Cancel != nil {
p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
} else {
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(CancelSignal) }
}
p.cmd.Dir = fhs.Root
p.cmd.Dir = FHSRoot
p.cmd.SysProcAttr = &SysProcAttr{
Setsid: !p.RetainSession,
Pdeathsig: SIGKILL,
Cloneflags: CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS |
CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP,
@@ -263,17 +203,12 @@ func (p *Container) Start() error {
CAP_SYS_ADMIN,
// drop capabilities
CAP_SETPCAP,
// bring up loopback interface
CAP_NET_ADMIN,
// overlay access to upperdir and workdir
CAP_DAC_OVERRIDE,
},
UseCgroupFD: p.Cgroup != nil,
}
if !p.AllowOrphan {
p.cmd.SysProcAttr.Pdeathsig = SIGKILL
}
if p.cmd.SysProcAttr.UseCgroupFD {
p.cmd.SysProcAttr.CgroupFD = *p.Cgroup
}
@@ -282,10 +217,10 @@ func (p *Container) Start() error {
}
// place setup pipe before user supplied extra files, this is later restored by init
if fd, f, err := Setup(&p.cmd.ExtraFiles); err != nil {
if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil {
return &StartError{true, "set up params stream", err, false, false}
} else {
p.setup = f
p.setup = e
p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
}
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...)
@@ -322,19 +257,19 @@ func (p *Container) Start() error {
}
return &StartError{false, "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", ENOSYS, true, false}
} else {
p.msg.Verbosef("landlock abi version %d", abi)
msg.Verbosef("landlock abi version %d", abi)
}
if rulesetFd, err := rulesetAttr.Create(0); err != nil {
return &StartError{true, "create landlock ruleset", err, false, false}
} else {
p.msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
if err = LandlockRestrictSelf(rulesetFd, 0); err != nil {
_ = Close(rulesetFd)
return &StartError{true, "enforce landlock ruleset", err, false, false}
}
if err = Close(rulesetFd); err != nil {
p.msg.Verbosef("cannot close landlock ruleset: %v", err)
msg.Verbosef("cannot close landlock ruleset: %v", err)
// not fatal
}
}
@@ -342,7 +277,7 @@ func (p *Container) Start() error {
landlockOut:
}
p.msg.Verbose("starting container init")
msg.Verbose("starting container init")
if err := p.cmd.Start(); err != nil {
return &StartError{false, "start container init", err, false, true}
}
@@ -364,9 +299,6 @@ func (p *Container) Serve() error {
setup := p.setup
p.setup = nil
if err := setup.SetDeadline(time.Now().Add(initSetupTimeout)); err != nil {
return &StartError{true, "set init pipe deadline", err, false, true}
}
if p.Path == nil {
p.cancel()
@@ -375,20 +307,21 @@ func (p *Container) Serve() error {
// do not transmit nil
if p.Dir == nil {
p.Dir = fhs.AbsRoot
p.Dir = AbsFHSRoot
}
if p.SeccompRules == nil {
p.SeccompRules = make([]std.NativeRule, 0)
p.SeccompRules = make([]seccomp.NativeRule, 0)
}
err := gob.NewEncoder(setup).Encode(&initParams{
err := setup.Encode(
&initParams{
p.Params,
Getuid(),
Getgid(),
len(p.ExtraFiles),
p.msg.IsVerbose(),
})
_ = setup.Close()
msg.IsVerbose(),
},
)
if err != nil {
p.cancel()
}
@@ -397,7 +330,7 @@ func (p *Container) Serve() error {
// Wait waits for the container init process to exit and releases any resources associated with the [Container].
func (p *Container) Wait() error {
if p.cmd == nil || p.cmd.Process == nil {
if p.cmd == nil {
return EINVAL
}
@@ -409,36 +342,6 @@ func (p *Container) Wait() error {
return err
}
// StdinPipe calls the [exec.Cmd] method with the same name.
func (p *Container) StdinPipe() (w io.WriteCloser, err error) {
if p.Stdin != nil {
return nil, errors.New("container: Stdin already set")
}
w, err = p.cmd.StdinPipe()
p.Stdin = p.cmd.Stdin
return
}
// StdoutPipe calls the [exec.Cmd] method with the same name.
func (p *Container) StdoutPipe() (r io.ReadCloser, err error) {
if p.Stdout != nil {
return nil, errors.New("container: Stdout already set")
}
r, err = p.cmd.StdoutPipe()
p.Stdout = p.cmd.Stdout
return
}
// StderrPipe calls the [exec.Cmd] method with the same name.
func (p *Container) StderrPipe() (r io.ReadCloser, err error) {
if p.Stderr != nil {
return nil, errors.New("container: Stderr already set")
}
r, err = p.cmd.StderrPipe()
p.Stderr = p.cmd.Stderr
return
}
func (p *Container) String() string {
return fmt.Sprintf("argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x",
p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets))
@@ -453,21 +356,13 @@ func (p *Container) ProcessState() *os.ProcessState {
}
// New returns the address to a new instance of [Container] that requires further initialisation before use.
func New(ctx context.Context, msg message.Msg) *Container {
if msg == nil {
msg = message.New(nil)
}
p := &Container{ctx: ctx, msg: msg, Params: Params{Ops: new(Ops)}}
c, cancel := context.WithCancel(ctx)
p.cancel = cancel
p.cmd = exec.CommandContext(c, MustExecutable(msg))
return p
func New(ctx context.Context) *Container {
return &Container{ctx: ctx, Params: Params{Ops: new(Ops)}}
}
// NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields.
func NewCommand(ctx context.Context, msg message.Msg, pathname *check.Absolute, name string, args ...string) *Container {
z := New(ctx, msg)
func NewCommand(ctx context.Context, pathname *Absolute, name string, args ...string) *Container {
z := New(ctx)
z.Path = pathname
z.Args = append([]string{name}, args...)
return z

View File

@@ -20,58 +20,15 @@ import (
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/container/vfs"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/ldd"
"hakurei.app/message"
)
// Note: this package requires cgo, which is unavailable in the Go playground.
func Example() {
// Must be called early if the current process starts containers.
container.TryArgv0(nil)
// Configure the container.
z := container.New(context.Background(), nil)
z.Hostname = "hakurei-example"
z.Proc(fhs.AbsProc).Dev(fhs.AbsDev, true)
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
// Bind / for demonstration.
z.Bind(fhs.AbsRoot, fhs.AbsRoot, 0)
if name, err := exec.LookPath("hostname"); err != nil {
panic(err)
} else {
z.Path = check.MustAbs(name)
}
// This completes the first stage of container setup and starts the container init process.
// The new process blocks until the Serve method is called.
if err := z.Start(); err != nil {
panic(err)
}
// This serves the setup payload to the container init process,
// starting the second stage of container setup.
if err := z.Serve(); err != nil {
panic(err)
}
// Must be called if the Start method succeeds.
if err := z.Wait(); err != nil {
panic(err)
}
// Output: hakurei-example
}
func TestStartError(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
err error
@@ -84,7 +41,8 @@ func TestStartError(t *testing.T) {
Fatal: true,
Step: "set up params stream",
Err: container.ErrReceiveEnv,
}, "set up params stream: environment variable not set",
},
"set up params stream: environment variable not set",
container.ErrReceiveEnv, syscall.EBADF,
"cannot set up params stream: environment variable not set"},
@@ -92,7 +50,8 @@ func TestStartError(t *testing.T) {
Fatal: true,
Step: "set up params stream",
Err: &os.SyscallError{Syscall: "pipe2", Err: syscall.EBADF},
}, "set up params stream pipe2: bad file descriptor",
},
"set up params stream pipe2: bad file descriptor",
syscall.EBADF, os.ErrInvalid,
"cannot set up params stream pipe2: bad file descriptor"},
@@ -100,14 +59,16 @@ func TestStartError(t *testing.T) {
Fatal: true,
Step: "prctl(PR_SET_NO_NEW_PRIVS)",
Err: syscall.EPERM,
}, "prctl(PR_SET_NO_NEW_PRIVS): operation not permitted",
},
"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",
},
"get landlock ABI: function not implemented",
syscall.ENOSYS, syscall.ENOEXEC,
"cannot get landlock ABI: function not implemented"},
@@ -115,7 +76,8 @@ func TestStartError(t *testing.T) {
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",
},
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET",
syscall.ENOSYS, syscall.ENOSPC,
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET"},
@@ -123,7 +85,8 @@ func TestStartError(t *testing.T) {
Fatal: true,
Step: "create landlock ruleset",
Err: syscall.EBADFD,
}, "create landlock ruleset: file descriptor in bad state",
},
"create landlock ruleset: file descriptor in bad state",
syscall.EBADFD, syscall.EBADF,
"cannot create landlock ruleset: file descriptor in bad state"},
@@ -131,7 +94,8 @@ func TestStartError(t *testing.T) {
Fatal: true,
Step: "enforce landlock ruleset",
Err: syscall.ENOTRECOVERABLE,
}, "enforce landlock ruleset: state not recoverable",
},
"enforce landlock ruleset: state not recoverable",
syscall.ENOTRECOVERABLE, syscall.ETIMEDOUT,
"cannot enforce landlock ruleset: state not recoverable"},
@@ -142,7 +106,8 @@ func TestStartError(t *testing.T) {
Path: "/proc/nonexistent",
Err: syscall.ENOENT,
}, Passthrough: true,
}, "fork/exec /proc/nonexistent: no such file or directory",
},
"fork/exec /proc/nonexistent: no such file or directory",
syscall.ENOENT, syscall.ENOSYS,
"cannot fork/exec /proc/nonexistent: no such file or directory"},
@@ -152,19 +117,11 @@ func TestStartError(t *testing.T) {
Syscall: "open",
Err: syscall.ENOSYS,
}, Passthrough: true,
}, "open: function not implemented",
},
"open: function not implemented",
syscall.ENOSYS, syscall.ENOENT,
"cannot open: function not implemented"},
{"start FD_CLOEXEC", &container.StartError{
Fatal: true,
Step: "set FD_CLOEXEC on all open files",
Err: func() error { _, err := strconv.Atoi("invalid"); return err }(),
Passthrough: true,
}, `strconv.Atoi: parsing "invalid": invalid syntax`,
strconv.ErrSyntax, os.ErrInvalid,
`cannot parse "invalid": invalid syntax`},
{"start other", &container.StartError{
Step: "start container init",
Err: &net.OpError{
@@ -172,14 +129,13 @@ func TestStartError(t *testing.T) {
Net: "unix",
Err: syscall.ECONNREFUSED,
}, Passthrough: true,
}, "dial unix: connection refused",
},
"dial unix: connection refused",
syscall.ECONNREFUSED, syscall.ECONNABORTED,
"dial unix: connection refused"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("error", func(t *testing.T) {
if got := tc.err.Error(); got != tc.s {
t.Errorf("Error: %q, want %q", got, tc.s)
@@ -196,13 +152,13 @@ func TestStartError(t *testing.T) {
})
t.Run("msg", func(t *testing.T) {
if got, ok := message.GetMessage(tc.err); !ok {
if got, ok := container.GetErrorMessage(tc.err); !ok {
if tc.msg != "" {
t.Errorf("GetMessage: err does not implement MessageError")
t.Errorf("GetErrorMessage: err does not implement MessageError")
}
return
} else if got != tc.msg {
t.Errorf("GetMessage: %q, want %q", got, tc.msg)
t.Errorf("GetErrorMessage: %q, want %q", got, tc.msg)
}
})
})
@@ -243,83 +199,83 @@ var containerTestCases = []struct {
uid int
gid int
rules []std.NativeRule
rules []seccomp.NativeRule
flags seccomp.ExportFlag
presets std.FilterPreset
presets seccomp.FilterPreset
}{
{"minimal", true, false, false, true,
emptyOps, emptyMnt,
1000, 100, nil, 0, std.PresetStrict},
1000, 100, nil, 0, seccomp.PresetStrict},
{"allow", true, true, true, false,
emptyOps, emptyMnt,
1000, 100, nil, 0, std.PresetExt | std.PresetDenyDevel},
1000, 100, nil, 0, seccomp.PresetExt | seccomp.PresetDenyDevel},
{"no filter", false, true, true, true,
emptyOps, emptyMnt,
1000, 100, nil, 0, std.PresetExt},
1000, 100, nil, 0, seccomp.PresetExt},
{"custom rules", true, true, true, false,
emptyOps, emptyMnt,
1, 31, []std.NativeRule{{Syscall: std.ScmpSyscall(syscall.SYS_SETUID), Errno: std.ScmpErrno(syscall.EPERM)}}, 0, std.PresetExt},
1, 31, []seccomp.NativeRule{{Syscall: seccomp.ScmpSyscall(syscall.SYS_SETUID), Errno: seccomp.ScmpErrno(syscall.EPERM)}}, 0, seccomp.PresetExt},
{"tmpfs", true, false, false, true,
earlyOps(new(container.Ops).
Tmpfs(hst.AbsPrivateTmp, 0, 0755),
Tmpfs(hst.AbsTmp, 0, 0755),
),
earlyMnt(
ent("/", hst.PrivateTmp, "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", ignore),
ent("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", ignore),
),
9, 9, nil, 0, std.PresetStrict},
9, 9, nil, 0, seccomp.PresetStrict},
{"dev", true, true /* go test output is not a tty */, false, false,
earlyOps(new(container.Ops).
Dev(check.MustAbs("/dev"), true),
Dev(container.MustAbs("/dev"), true),
),
earlyMnt(
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", ignore, ignore),
ent("/null", "/dev/null", ignore, "devtmpfs", ignore, ignore),
ent("/zero", "/dev/zero", ignore, "devtmpfs", ignore, ignore),
ent("/full", "/dev/full", ignore, "devtmpfs", ignore, ignore),
ent("/random", "/dev/random", ignore, "devtmpfs", ignore, ignore),
ent("/urandom", "/dev/urandom", ignore, "devtmpfs", ignore, ignore),
ent("/tty", "/dev/tty", ignore, "devtmpfs", ignore, ignore),
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
ent("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
ent("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
),
1971, 100, nil, 0, std.PresetStrict},
1971, 100, nil, 0, seccomp.PresetStrict},
{"dev no mqueue", true, true /* go test output is not a tty */, false, false,
earlyOps(new(container.Ops).
Dev(check.MustAbs("/dev"), false),
Dev(container.MustAbs("/dev"), false),
),
earlyMnt(
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", ignore, ignore),
ent("/null", "/dev/null", ignore, "devtmpfs", ignore, ignore),
ent("/zero", "/dev/zero", ignore, "devtmpfs", ignore, ignore),
ent("/full", "/dev/full", ignore, "devtmpfs", ignore, ignore),
ent("/random", "/dev/random", ignore, "devtmpfs", ignore, ignore),
ent("/urandom", "/dev/urandom", ignore, "devtmpfs", ignore, ignore),
ent("/tty", "/dev/tty", ignore, "devtmpfs", ignore, ignore),
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
ent("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
),
1971, 100, nil, 0, std.PresetStrict},
1971, 100, nil, 0, seccomp.PresetStrict},
{"overlay", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) {
tempDir := check.MustAbs(t.TempDir())
tempDir := container.MustAbs(t.TempDir())
lower0, lower1, upper, work :=
tempDir.Append("lower0"),
tempDir.Append("lower1"),
tempDir.Append("upper"),
tempDir.Append("work")
for _, a := range []*check.Absolute{lower0, lower1, upper, work} {
for _, a := range []*container.Absolute{lower0, lower1, upper, work} {
if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err)
}
}
return new(container.Ops).
Overlay(hst.AbsPrivateTmp, upper, work, lower0, lower1),
Overlay(hst.AbsTmp, upper, work, lower0, lower1),
context.WithValue(context.WithValue(context.WithValue(context.WithValue(t.Context(),
testVal("lower1"), lower1),
testVal("lower0"), lower0),
@@ -328,74 +284,74 @@ var containerTestCases = []struct {
},
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay",
ent("/", hst.Tmp, "rw", "overlay", "overlay",
"rw,lowerdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*check.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+
",upperdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(*check.Absolute).String())+
container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(*container.Absolute).String())+
",workdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("work")).(*check.Absolute).String())+
container.InternalToHostOvlEscape(ctx.Value(testVal("work")).(*container.Absolute).String())+
",redirect_dir=nofollow,uuid=on,userxattr"),
}
},
1 << 3, 1 << 14, nil, 0, std.PresetStrict},
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
{"overlay ephemeral", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) {
tempDir := check.MustAbs(t.TempDir())
tempDir := container.MustAbs(t.TempDir())
lower0, lower1 :=
tempDir.Append("lower0"),
tempDir.Append("lower1")
for _, a := range []*check.Absolute{lower0, lower1} {
for _, a := range []*container.Absolute{lower0, lower1} {
if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err)
}
}
return new(container.Ops).
OverlayEphemeral(hst.AbsPrivateTmp, lower0, lower1),
OverlayEphemeral(hst.AbsTmp, lower0, lower1),
t.Context()
},
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{
// contains random suffix
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay", ignore),
ent("/", hst.Tmp, "rw", "overlay", "overlay", ignore),
}
},
1 << 3, 1 << 14, nil, 0, std.PresetStrict},
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
{"overlay readonly", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) {
tempDir := check.MustAbs(t.TempDir())
tempDir := container.MustAbs(t.TempDir())
lower0, lower1 :=
tempDir.Append("lower0"),
tempDir.Append("lower1")
for _, a := range []*check.Absolute{lower0, lower1} {
for _, a := range []*container.Absolute{lower0, lower1} {
if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err)
}
}
return new(container.Ops).
OverlayReadonly(hst.AbsPrivateTmp, lower0, lower1),
OverlayReadonly(hst.AbsTmp, lower0, lower1),
context.WithValue(context.WithValue(t.Context(),
testVal("lower1"), lower1),
testVal("lower0"), lower0)
},
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay",
ent("/", hst.Tmp, "rw", "overlay", "overlay",
"ro,lowerdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*check.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+
",redirect_dir=nofollow,userxattr"),
}
},
1 << 3, 1 << 14, nil, 0, std.PresetStrict},
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
}
func TestContainer(t *testing.T) {
t.Parallel()
replaceOutput(t)
t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) {
wantErr := context.Canceled
@@ -430,15 +386,13 @@ func TestContainer(t *testing.T) {
for i, tc := range containerTestCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
wantOps, wantOpsCtx := tc.ops(t)
wantMnt := tc.mnt(t, wantOpsCtx)
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
defer cancel()
var libPaths []*check.Absolute
var libPaths []*container.Absolute
c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i))
c.Uid = tc.uid
c.Gid = tc.gid
@@ -459,11 +413,11 @@ func TestContainer(t *testing.T) {
c.HostNet = tc.net
c.
Readonly(check.MustAbs(pathReadonly), 0755).
Tmpfs(check.MustAbs("/tmp"), 0, 0755).
Place(check.MustAbs("/etc/hostname"), []byte(c.Hostname))
Readonly(container.MustAbs(pathReadonly), 0755).
Tmpfs(container.MustAbs("/tmp"), 0, 0755).
Place(container.MustAbs("/etc/hostname"), []byte(c.Hostname))
// needs /proc to check mountinfo
c.Proc(check.MustAbs("/proc"))
c.Proc(container.MustAbs("/proc"))
// mountinfo cannot be resolved directly by helper due to libPaths nondeterminism
mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
@@ -494,10 +448,10 @@ func TestContainer(t *testing.T) {
_, _ = output.WriteTo(os.Stdout)
t.Fatalf("cannot serialise expected mount points: %v", err)
}
c.Place(check.MustAbs(pathWantMnt), want.Bytes())
c.Place(container.MustAbs(pathWantMnt), want.Bytes())
if tc.ro {
c.Remount(check.MustAbs("/"), syscall.MS_RDONLY)
c.Remount(container.MustAbs("/"), syscall.MS_RDONLY)
}
if err := c.Start(); err != nil {
@@ -551,7 +505,6 @@ func testContainerCancel(
waitCheck func(t *testing.T, c *container.Container),
) func(t *testing.T) {
return func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
c := helperNewContainer(ctx, "block")
@@ -594,14 +547,12 @@ func testContainerCancel(
}
func TestContainerString(t *testing.T) {
t.Parallel()
msg := message.New(nil)
c := container.NewCommand(t.Context(), msg, check.MustAbs("/run/current-system/sw/bin/ldd"), "ldd", "/usr/bin/env")
c := container.NewCommand(t.Context(), container.MustAbs("/run/current-system/sw/bin/ldd"), "ldd", "/usr/bin/env")
c.SeccompFlags |= seccomp.AllowMultiarch
c.SeccompRules = seccomp.Preset(
std.PresetExt|std.PresetDenyNS|std.PresetDenyTTY,
seccomp.PresetExt|seccomp.PresetDenyNS|seccomp.PresetDenyTTY,
c.SeccompFlags)
c.SeccompPresets = std.PresetStrict
c.SeccompPresets = seccomp.PresetStrict
want := `argv: ["ldd" "/usr/bin/env"], filter: true, rules: 65, flags: 0x1, presets: 0xf`
if got := c.String(); got != want {
t.Errorf("String: %s, want %s", got, want)
@@ -615,12 +566,13 @@ const (
func init() {
helperCommands = append(helperCommands, func(c command.Command) {
c.Command("block", command.UsageInternal, func(args []string) error {
if _, err := os.NewFile(3, "sync").Write([]byte{0}); err != nil {
return fmt.Errorf("write to sync pipe: %v", err)
}
{
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt)
go func() { <-sig; os.Exit(blockExitCodeInterrupt) }()
if _, err := os.NewFile(3, "sync").Write([]byte{0}); err != nil {
return fmt.Errorf("write to sync pipe: %v", err)
}
select {}
})
@@ -690,22 +642,11 @@ func init() {
return fmt.Errorf("got more than %d entries", len(mnt))
}
// ugly hack but should be reliable and is less likely to
//false negative than comparing by parsed flags
for _, s := range []string{
"relatime",
"noatime",
} {
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ","+s)
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ","+s)
}
for _, s := range []string{
"seclabel",
"inode64",
} {
cur.FsOptstr = strings.Replace(cur.FsOptstr, ","+s, "", 1)
mnt[i].FsOptstr = strings.Replace(mnt[i].FsOptstr, ","+s, "", 1)
}
// ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",relatime")
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",noatime")
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",relatime")
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime")
if !cur.EqualWithIgnore(mnt[i], "\x00") {
fail = true
@@ -742,13 +683,13 @@ const (
)
var (
absHelperInnerPath = check.MustAbs(helperInnerPath)
absHelperInnerPath = container.MustAbs(helperInnerPath)
)
var helperCommands []func(c command.Command)
func TestMain(m *testing.M) {
container.TryArgv0(nil)
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
if os.Getenv(envDoCheck) == "1" {
c := command.New(os.Stderr, log.Printf, "helper", func(args []string) error {
@@ -770,17 +711,13 @@ func TestMain(m *testing.M) {
os.Exit(m.Run())
}
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*check.Absolute, args ...string) (c *container.Container) {
msg := message.New(nil)
msg.SwapVerbose(testing.Verbose())
executable := check.MustAbs(container.MustExecutable(msg))
c = container.NewCommand(ctx, msg, absHelperInnerPath, "helper", args...)
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*container.Absolute, args ...string) (c *container.Container) {
c = container.NewCommand(ctx, absHelperInnerPath, "helper", args...)
c.Env = append(c.Env, envDoCheck+"=1")
c.Bind(executable, absHelperInnerPath, 0)
c.Bind(container.MustAbs(os.Args[0]), absHelperInnerPath, 0)
// in case test has cgo enabled
if entries, err := ldd.Resolve(ctx, msg, executable); err != nil {
if entries, err := ldd.Exec(ctx, os.Args[0]); err != nil {
log.Fatalf("ldd: %v", err)
} else {
*libPaths = ldd.Path(entries)
@@ -793,5 +730,5 @@ func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*check.Absolute
}
func helperNewContainer(ctx context.Context, args ...string) (c *container.Container) {
return helperNewContainerLibPaths(ctx, new([]*check.Absolute), args...)
return helperNewContainerLibPaths(ctx, new([]*container.Absolute), args...)
}

View File

@@ -3,6 +3,7 @@ package container
import (
"io"
"io/fs"
"log"
"os"
"os/exec"
"os/signal"
@@ -11,8 +12,6 @@ import (
"syscall"
"hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/message"
)
type osFile interface {
@@ -39,7 +38,7 @@ type syscallDispatcher interface {
setNoNewPrivs() error
// lastcap provides [LastCap].
lastcap(msg message.Msg) uintptr
lastcap() uintptr
// capset provides capset.
capset(hdrp *capHeader, datap *[2]capData) error
// capBoundingSetDrop provides capBoundingSetDrop.
@@ -54,18 +53,16 @@ type syscallDispatcher interface {
receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
// bindMount provides procPaths.bindMount.
bindMount(msg message.Msg, source, target string, flags uintptr) error
bindMount(source, target string, flags uintptr, eq bool) error
// remount provides procPaths.remount.
remount(msg message.Msg, target string, flags uintptr) error
remount(target string, flags uintptr) error
// mountTmpfs provides mountTmpfs.
mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error
// ensureFile provides ensureFile.
ensureFile(name string, perm, pperm os.FileMode) error
// mustLoopback provides mustLoopback.
mustLoopback(msg message.Msg)
// seccompLoad provides [seccomp.Load].
seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error
seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error
// notify provides [signal.Notify].
notify(c chan<- os.Signal, sig ...os.Signal)
// start starts [os/exec.Cmd].
@@ -125,12 +122,22 @@ type syscallDispatcher interface {
// wait4 provides syscall.Wait4
wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error)
// printf provides the Printf method of [log.Logger].
printf(msg message.Msg, format string, v ...any)
// fatal provides the Fatal method of [log.Logger]
fatal(msg message.Msg, v ...any)
// fatalf provides the Fatalf method of [log.Logger]
fatalf(msg message.Msg, format string, v ...any)
// printf provides [log.Printf].
printf(format string, v ...any)
// fatal provides [log.Fatal]
fatal(v ...any)
// fatalf provides [log.Fatalf]
fatalf(format string, v ...any)
// verbose provides [Msg.Verbose].
verbose(v ...any)
// verbosef provides [Msg.Verbosef].
verbosef(format string, v ...any)
// suspend provides [Msg.Suspend].
suspend()
// resume provides [Msg.Resume].
resume() bool
// beforeExit provides [Msg.BeforeExit].
beforeExit()
}
// direct implements syscallDispatcher on the current kernel.
@@ -144,7 +151,7 @@ func (direct) setPtracer(pid uintptr) error { return SetPtracer(pid) }
func (direct) setDumpable(dumpable uintptr) error { return SetDumpable(dumpable) }
func (direct) setNoNewPrivs() error { return SetNoNewPrivs() }
func (direct) lastcap(msg message.Msg) uintptr { return LastCap(msg) }
func (direct) lastcap() uintptr { return LastCap() }
func (direct) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) }
func (direct) capBoundingSetDrop(cap uintptr) error { return capBoundingSetDrop(cap) }
func (direct) capAmbientClearAll() error { return capAmbientClearAll() }
@@ -154,11 +161,11 @@ func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
return Receive(key, e, fdp)
}
func (direct) bindMount(msg message.Msg, source, target string, flags uintptr) error {
return hostProc.bindMount(msg, source, target, flags)
func (direct) bindMount(source, target string, flags uintptr, eq bool) error {
return hostProc.bindMount(source, target, flags, eq)
}
func (direct) remount(msg message.Msg, target string, flags uintptr) error {
return hostProc.remount(msg, target, flags)
func (direct) remount(target string, flags uintptr) error {
return hostProc.remount(target, flags)
}
func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
return mountTmpfs(k, fsname, target, flags, size, perm)
@@ -166,9 +173,8 @@ func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm
func (direct) ensureFile(name string, perm, pperm os.FileMode) error {
return ensureFile(name, perm, pperm)
}
func (direct) mustLoopback(msg message.Msg) { mustLoopback(msg) }
func (direct) seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error {
func (direct) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error {
return seccomp.Load(rules, flags)
}
func (direct) notify(c chan<- os.Signal, sig ...os.Signal) { signal.Notify(c, sig...) }
@@ -226,6 +232,11 @@ func (direct) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *s
return syscall.Wait4(pid, wstatus, options, rusage)
}
func (direct) printf(msg message.Msg, format string, v ...any) { msg.GetLogger().Printf(format, v...) }
func (direct) fatal(msg message.Msg, v ...any) { msg.GetLogger().Fatal(v...) }
func (direct) fatalf(msg message.Msg, format string, v ...any) { msg.GetLogger().Fatalf(format, v...) }
func (direct) printf(format string, v ...any) { log.Printf(format, v...) }
func (direct) fatal(v ...any) { log.Fatal(v...) }
func (direct) fatalf(format string, v ...any) { log.Fatalf(format, v...) }
func (direct) verbose(v ...any) { msg.Verbose(v...) }
func (direct) verbosef(format string, v ...any) { msg.Verbosef(format, v...) }
func (direct) suspend() { msg.Suspend() }
func (direct) resume() bool { return msg.Resume() }
func (direct) beforeExit() { msg.BeforeExit() }

View File

@@ -2,10 +2,8 @@ package container
import (
"bytes"
"fmt"
"io"
"io/fs"
"log"
"os"
"os/exec"
"reflect"
@@ -17,9 +15,7 @@ import (
"time"
"hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/container/stub"
"hakurei.app/message"
)
type opValidTestCase struct {
@@ -33,12 +29,10 @@ func checkOpsValid(t *testing.T, testCases []opValidTestCase) {
t.Run("valid", func(t *testing.T) {
t.Helper()
t.Parallel()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Helper()
t.Parallel()
if got := tc.op.Valid(); got != tc.want {
t.Errorf("Valid: %v, want %v", got, tc.want)
@@ -59,12 +53,10 @@ func checkOpsBuilder(t *testing.T, testCases []opsBuilderTestCase) {
t.Run("build", func(t *testing.T) {
t.Helper()
t.Parallel()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Helper()
t.Parallel()
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)
@@ -85,12 +77,10 @@ func checkOpIs(t *testing.T, testCases []opIsTestCase) {
t.Run("is", func(t *testing.T) {
t.Helper()
t.Parallel()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Helper()
t.Parallel()
if got := tc.op.Is(tc.v); got != tc.want {
t.Errorf("Is: %v, want %v", got, tc.want)
@@ -113,17 +103,15 @@ func checkOpMeta(t *testing.T, testCases []opMetaTestCase) {
t.Run("meta", func(t *testing.T) {
t.Helper()
t.Parallel()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Helper()
t.Parallel()
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)
}
})
@@ -148,7 +136,7 @@ func call(name string, args stub.ExpectArgs, ret any, err error) stub.Call {
type simpleTestCase struct {
name string
f func(k *kstub) error
f func(k syscallDispatcher) error
want stub.Expect
wantErr error
}
@@ -159,11 +147,9 @@ func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Helper()
t.Parallel()
wait4signal := make(chan struct{})
lockNotify := make(chan struct{})
k := &kstub{wait4signal, lockNotify, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, lockNotify, s} }, tc.want)}
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)
@@ -194,18 +180,16 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
t.Run("behaviour", func(t *testing.T) {
t.Helper()
t.Parallel()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Helper()
t.Parallel()
k := &kstub{nil, nil, stub.New(t,
func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, nil, s} },
state := &setupState{Params: tc.params}
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)},
)}
state := &setupState{Params: tc.params, Msg: k}
defer stub.HandleExit(t)
errEarly := tc.op.early(state, k)
k.Expects(stub.CallSeparator)
@@ -323,19 +307,12 @@ const (
type kstub struct {
wait4signal chan struct{}
lockNotify chan struct{}
*stub.Stub[syscallDispatcher]
}
func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) }
func (k *kstub) lockOSThread() {
k.Helper()
expect := k.Expects("lockOSThread")
if k.lockNotify != nil && expect.Ret == magicWait4Signal {
<-k.lockNotify
}
}
func (k *kstub) lockOSThread() { k.Helper(); k.Expects("lockOSThread") }
func (k *kstub) setPtracer(pid uintptr) error {
k.Helper()
@@ -350,11 +327,7 @@ func (k *kstub) setDumpable(dumpable uintptr) error {
}
func (k *kstub) setNoNewPrivs() error { k.Helper(); return k.Expects("setNoNewPrivs").Err }
func (k *kstub) lastcap(msg message.Msg) uintptr {
k.Helper()
k.checkMsg(msg)
return k.Expects("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 {
k.Helper()
@@ -430,18 +403,17 @@ func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error
return
}
func (k *kstub) bindMount(msg message.Msg, source, target string, flags uintptr) error {
func (k *kstub) bindMount(source, target string, flags uintptr, eq bool) error {
k.Helper()
k.checkMsg(msg)
return k.Expects("bindMount").Error(
stub.CheckArg(k.Stub, "source", source, 0),
stub.CheckArg(k.Stub, "target", target, 1),
stub.CheckArg(k.Stub, "flags", flags, 2))
stub.CheckArg(k.Stub, "flags", flags, 2),
stub.CheckArg(k.Stub, "eq", eq, 3))
}
func (k *kstub) remount(msg message.Msg, target string, flags uintptr) error {
func (k *kstub) remount(target string, flags uintptr) error {
k.Helper()
k.checkMsg(msg)
return k.Expects("remount").Error(
stub.CheckArg(k.Stub, "target", target, 0),
stub.CheckArg(k.Stub, "flags", flags, 1))
@@ -465,9 +437,7 @@ func (k *kstub) ensureFile(name string, perm, pperm os.FileMode) error {
stub.CheckArg(k.Stub, "pperm", pperm, 2))
}
func (*kstub) mustLoopback(message.Msg) { /* noop */ }
func (k *kstub) seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error {
func (k *kstub) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error {
k.Helper()
return k.Expects("seccompLoad").Error(
stub.CheckArgReflect(k.Stub, "rules", rules, 0),
@@ -482,10 +452,6 @@ func (k *kstub) notify(c chan<- os.Signal, sig ...os.Signal) {
k.FailNow()
}
if k.lockNotify != nil && expect.Ret == magicWait4Signal {
defer close(k.lockNotify)
}
// export channel for external instrumentation
if chanf, ok := expect.Args[0].(func(c chan<- os.Signal)); ok && chanf != nil {
chanf(c)
@@ -729,7 +695,7 @@ func (k *kstub) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage
return
}
func (k *kstub) printf(_ message.Msg, format string, v ...any) {
func (k *kstub) printf(format string, v ...any) {
k.Helper()
if k.Expects("printf").Error(
stub.CheckArg(k.Stub, "format", format, 0),
@@ -738,7 +704,7 @@ func (k *kstub) printf(_ message.Msg, format string, v ...any) {
}
}
func (k *kstub) fatal(_ message.Msg, v ...any) {
func (k *kstub) fatal(v ...any) {
k.Helper()
if k.Expects("fatal").Error(
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil {
@@ -747,7 +713,7 @@ func (k *kstub) fatal(_ message.Msg, v ...any) {
panic(stub.PanicExit)
}
func (k *kstub) fatalf(_ message.Msg, format string, v ...any) {
func (k *kstub) fatalf(format string, v ...any) {
k.Helper()
if k.Expects("fatalf").Error(
stub.CheckArg(k.Stub, "format", format, 0),
@@ -757,36 +723,7 @@ func (k *kstub) fatalf(_ message.Msg, format string, v ...any) {
panic(stub.PanicExit)
}
func (k *kstub) checkMsg(msg message.Msg) {
k.Helper()
var target *kstub
if state, ok := msg.(*setupState); ok {
target = state.Msg.(*kstub)
} else {
target = msg.(*kstub)
}
if k != target {
panic(fmt.Sprintf("unexpected Msg: %#v", msg))
}
}
func (k *kstub) GetLogger() *log.Logger { panic("unreachable") }
func (k *kstub) IsVerbose() bool { k.Helper(); return k.Expects("isVerbose").Ret.(bool) }
func (k *kstub) SwapVerbose(verbose bool) bool {
k.Helper()
expect := k.Expects("swapVerbose")
if expect.Error(
stub.CheckArg(k.Stub, "verbose", verbose, 0)) != nil {
k.FailNow()
}
return expect.Ret.(bool)
}
func (k *kstub) Verbose(v ...any) {
func (k *kstub) verbose(v ...any) {
k.Helper()
if k.Expects("verbose").Error(
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil {
@@ -794,7 +731,7 @@ func (k *kstub) Verbose(v ...any) {
}
}
func (k *kstub) Verbosef(format string, v ...any) {
func (k *kstub) verbosef(format string, v ...any) {
k.Helper()
if k.Expects("verbosef").Error(
stub.CheckArg(k.Stub, "format", format, 0),
@@ -803,6 +740,6 @@ func (k *kstub) Verbosef(format string, v ...any) {
}
}
func (k *kstub) Suspend() bool { k.Helper(); return k.Expects("suspend").Ret.(bool) }
func (k *kstub) Resume() bool { k.Helper(); return k.Expects("resume").Ret.(bool) }
func (k *kstub) BeforeExit() { k.Helper(); k.Expects("beforeExit") }
func (k *kstub) suspend() { k.Helper(); k.Expects("suspend") }
func (k *kstub) resume() bool { k.Helper(); return k.Expects("resume").Ret.(bool) }
func (k *kstub) beforeExit() { k.Helper(); k.Expects("beforeExit") }

View File

@@ -5,38 +5,32 @@ import (
"os"
"syscall"
"hakurei.app/container/check"
"hakurei.app/container/vfs"
"hakurei.app/message"
)
// messageFromError returns a printable error message for a supported concrete type.
func messageFromError(err error) (m string, ok bool) {
if m, ok = messagePrefixP[MountError]("cannot ", err); ok {
return
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
if m, ok := messagePrefixP[os.PathError]("cannot ", err); ok {
return m, ok
}
if m, ok = messagePrefix[check.AbsoluteError](zeroString, err); ok {
return
if m, ok := messagePrefixP[AbsoluteError]("", err); ok {
return m, ok
}
if m, ok = messagePrefix[OpRepeatError](zeroString, err); ok {
return
if m, ok := messagePrefix[OpRepeatError]("", err); ok {
return m, ok
}
if m, ok = messagePrefix[OpStateError](zeroString, err); ok {
return
if m, ok := messagePrefix[OpStateError]("", err); ok {
return m, ok
}
if m, ok = messagePrefixP[vfs.DecoderError]("cannot ", err); ok {
return
if m, ok := messagePrefixP[vfs.DecoderError]("cannot ", err); ok {
return m, ok
}
if m, ok = messagePrefix[TmpfsSizeError](zeroString, err); ok {
return
}
if m, ok = message.GetMessage(err); ok {
return
if m, ok := messagePrefix[TmpfsSizeError]("", err); ok {
return m, ok
}
return zeroString, false
@@ -64,7 +58,6 @@ func messagePrefixP[V any, T interface {
return zeroString, false
}
// MountError wraps errors returned by syscall.Mount.
type MountError struct {
Source, Target, Fstype string
@@ -80,7 +73,6 @@ func (e *MountError) Unwrap() error {
return e.Errno
}
func (e *MountError) Message() string { return "cannot " + e.Error() }
func (e *MountError) Error() string {
if e.Flags&syscall.MS_BIND != 0 {
if e.Flags&syscall.MS_REMOUNT != 0 {
@@ -97,15 +89,6 @@ func (e *MountError) Error() string {
return "mount " + e.Target + ": " + e.Errno.Error()
}
// optionalErrorUnwrap calls [errors.Unwrap] and returns the resulting value
// if it is not nil, or the original value if it is.
func optionalErrorUnwrap(err error) error {
if underlyingErr := errors.Unwrap(err); underlyingErr != nil {
return underlyingErr
}
return err
}
// errnoFallback returns the concrete errno from an error, or a [os.PathError] fallback.
func errnoFallback(op, path string, err error) (syscall.Errno, *os.PathError) {
var errno syscall.Errno

View File

@@ -8,14 +8,11 @@ import (
"syscall"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
"hakurei.app/container/vfs"
)
func TestMessageFromError(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
err error
@@ -37,7 +34,7 @@ func TestMessageFromError(t *testing.T) {
Err: stub.UniqueError(0xdeadbeef),
}, "cannot mount /sysroot: unique error 3735928559 injected by the test suite", true},
{"absolute", check.AbsoluteError("etc/mtab"),
{"absolute", &AbsoluteError{"etc/mtab"},
`path "etc/mtab" is not absolute`, true},
{"repeat", OpRepeatError("autoetc"),
@@ -46,8 +43,8 @@ func TestMessageFromError(t *testing.T) {
{"state", OpStateError("overlay"),
"impossible overlay state reached", true},
{"vfs parse", &vfs.DecoderError{Op: "parse", Line: 0xdead, Err: &strconv.NumError{Func: "Atoi", Num: "meow", Err: strconv.ErrSyntax}},
`cannot parse mountinfo at line 57005: numeric field "meow" invalid syntax`, true},
{"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},
@@ -56,7 +53,6 @@ func TestMessageFromError(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, ok := messageFromError(tc.err)
if got != tc.want {
t.Errorf("messageFromError: %q, want %q", got, tc.want)
@@ -69,8 +65,6 @@ func TestMessageFromError(t *testing.T) {
}
func TestMountError(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
err error
@@ -116,7 +110,6 @@ func TestMountError(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("is", func(t *testing.T) {
if !errors.Is(tc.err, tc.errno) {
t.Errorf("Is: %#v is not %v", tc.err, tc.errno)
@@ -131,7 +124,6 @@ func TestMountError(t *testing.T) {
}
t.Run("zero", func(t *testing.T) {
t.Parallel()
if errors.Is(new(MountError), syscall.Errno(0)) {
t.Errorf("Is: zero MountError unexpected true")
}
@@ -139,8 +131,6 @@ func TestMountError(t *testing.T) {
}
func TestErrnoFallback(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
err error
@@ -163,7 +153,6 @@ func TestErrnoFallback(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
errno, err := errnoFallback(tc.name, Nonexistent, tc.err)
if errno != tc.wantErrno {
t.Errorf("errnoFallback: errno = %v, want %v", errno, tc.wantErrno)

View File

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

View File

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

View File

@@ -1,47 +0,0 @@
package fhs
import (
_ "unsafe" // for go:linkname
"hakurei.app/container/check"
)
/* constants in this file bypass abs check, be extremely careful when changing them! */
// unsafeAbs returns check.Absolute on any string value.
//
//go:linkname unsafeAbs hakurei.app/container/check.unsafeAbs
func unsafeAbs(pathname string) *check.Absolute
var (
// AbsRoot is [Root] as [check.Absolute].
AbsRoot = unsafeAbs(Root)
// AbsEtc is [Etc] as [check.Absolute].
AbsEtc = unsafeAbs(Etc)
// AbsTmp is [Tmp] as [check.Absolute].
AbsTmp = unsafeAbs(Tmp)
// AbsRun is [Run] as [check.Absolute].
AbsRun = unsafeAbs(Run)
// AbsRunUser is [RunUser] as [check.Absolute].
AbsRunUser = unsafeAbs(RunUser)
// AbsUsr is [Usr] as [check.Absolute].
AbsUsr = unsafeAbs(Usr)
// AbsUsrBin is [UsrBin] as [check.Absolute].
AbsUsrBin = unsafeAbs(UsrBin)
// AbsVar is [Var] as [check.Absolute].
AbsVar = unsafeAbs(Var)
// AbsVarLib is [VarLib] as [check.Absolute].
AbsVarLib = unsafeAbs(VarLib)
// AbsDev is [Dev] as [check.Absolute].
AbsDev = unsafeAbs(Dev)
// AbsDevShm is [DevShm] as [check.Absolute].
AbsDevShm = unsafeAbs(DevShm)
// AbsProc is [Proc] as [check.Absolute].
AbsProc = unsafeAbs(Proc)
// AbsSys is [Sys] as [check.Absolute].
AbsSys = unsafeAbs(Sys)
)

View File

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

View File

@@ -1,48 +1,38 @@
package container
import (
"context"
"errors"
"fmt"
"log"
"os"
"os/exec"
"path"
"slices"
"strconv"
"sync"
"sync/atomic"
. "syscall"
"time"
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp"
"hakurei.app/message"
)
const (
/* intermediateHostPath is the pathname of the intermediate tmpfs mount point.
/* intermediate tmpfs mount point
This path might seem like a weird choice, however there are many good reasons to use it:
- The contents of this path is never exposed to the container:
The tmpfs root established here effectively becomes anonymous after pivot_root.
- It is safe to assume this path exists and is a directory:
This program will not work correctly without a proper /proc and neither will most others.
- This path belongs to the container init:
The container init is not any more privileged or trusted than the rest of the container.
- This path is only accessible by init and root:
The container init sets SUID_DUMP_DISABLE and terminates if that fails.
this path might seem like a weird choice, however there are many good reasons to use it:
- the contents of this path is never exposed to the container:
the tmpfs root established here effectively becomes anonymous after pivot_root
- it is safe to assume this path exists and is a directory:
this program will not work correctly without a proper /proc and neither will most others
- this path belongs to the container init:
the container init is not any more privileged or trusted than the rest of the container
- this path is only accessible by init and root:
the container init sets SUID_DUMP_DISABLE and terminates if that fails;
It should be noted that none of this should become relevant at any point since the resulting
intermediate root tmpfs should be effectively anonymous. */
intermediateHostPath = fhs.Proc + "self/fd"
it should be noted that none of this should become relevant at any point since the resulting
intermediate root tmpfs should be effectively anonymous */
intermediateHostPath = FHSProc + "self/fd"
// setupEnv is the name of the environment variable holding the string representation of
// the read end file descriptor of the setup params pipe.
// setup params file descriptor
setupEnv = "HAKUREI_SETUP"
// exitUnexpectedWait4 is the exit code if wait4 returns an unexpected errno.
exitUnexpectedWait4 = 2
)
type (
@@ -56,12 +46,8 @@ type (
early(state *setupState, k syscallDispatcher) error
// apply is called in intermediate root.
apply(state *setupState, k syscallDispatcher) error
// late is called right before starting the initial process.
late(state *setupState, k syscallDispatcher) error
// prefix returns a log message prefix, and whether this Op prints no identifying message on its own.
prefix() (string, bool)
prefix() string
Is(op Op) bool
Valid() bool
fmt.Stringer
@@ -70,29 +56,10 @@ type (
// setupState persists context between Ops.
setupState struct {
nonrepeatable uintptr
// Whether early reaping has concluded. Must only be accessed in the wait4 loop.
processConcluded bool
// Process to syscall.WaitStatus populated in the wait4 loop. Freed after early reaping concludes.
process map[int]WaitStatus
// Synchronises access to process.
processMu sync.RWMutex
*Params
context.Context
message.Msg
}
)
// terminated returns whether the specified pid has been reaped, and its
// syscall.WaitStatus if it had. This is only usable by [Op].
func (state *setupState) terminated(pid int) (wstatus WaitStatus, ok bool) {
state.processMu.RLock()
wstatus, ok = state.process[pid]
state.processMu.RUnlock()
return
}
// Grow grows the slice Ops points to using [slices.Grow].
func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) }
@@ -122,22 +89,20 @@ type initParams struct {
Verbose bool
}
// Init is called by [TryArgv0] if the current process is the container init.
func Init(msg message.Msg) { initEntrypoint(direct{}, msg) }
func initEntrypoint(k syscallDispatcher, msg message.Msg) {
k.lockOSThread()
if msg == nil {
panic("attempting to call initEntrypoint with nil msg")
func Init(prepareLogger func(prefix string), setVerbose func(verbose bool)) {
initEntrypoint(direct{}, prepareLogger, setVerbose)
}
func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setVerbose func(verbose bool)) {
k.lockOSThread()
prepareLogger("init")
if k.getpid() != 1 {
k.fatal(msg, "this process must run as pid 1")
k.fatal("this process must run as pid 1")
}
if err := k.setPtracer(0); err != nil {
msg.Verbosef("cannot enable ptrace protection via Yama LSM: %v", err)
k.verbosef("cannot enable ptrace protection via Yama LSM: %v", err)
// not fatal: this program has no additional privileges at initial program start
}
@@ -149,71 +114,65 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
)
if f, err := k.receive(setupEnv, &params, &setupFd); err != nil {
if errors.Is(err, EBADF) {
k.fatal(msg, "invalid setup descriptor")
k.fatal("invalid setup descriptor")
}
if errors.Is(err, ErrReceiveEnv) {
k.fatal(msg, setupEnv+" not set")
k.fatal("HAKUREI_SETUP not set")
}
k.fatalf(msg, "cannot decode init setup payload: %v", err)
k.fatalf("cannot decode init setup payload: %v", err)
} else {
if params.Ops == nil {
k.fatal(msg, "invalid setup parameters")
k.fatal("invalid setup parameters")
}
if params.ParentPerm == 0 {
params.ParentPerm = 0755
}
msg.SwapVerbose(params.Verbose)
msg.Verbose("received setup parameters")
setVerbose(params.Verbose)
k.verbose("received setup parameters")
closeSetup = f
offsetSetup = int(setupFd + 1)
}
if !params.HostNet {
k.mustLoopback(msg)
}
// write uid/gid map here so parent does not need to set dumpable
if err := k.setDumpable(SUID_DUMP_USER); err != nil {
k.fatalf(msg, "cannot set SUID_DUMP_USER: %v", err)
k.fatalf("cannot set SUID_DUMP_USER: %v", err)
}
if err := k.writeFile(fhs.Proc+"self/uid_map",
if err := k.writeFile(FHSProc+"self/uid_map",
append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...),
0); err != nil {
k.fatalf(msg, "%v", err)
k.fatalf("%v", err)
}
if err := k.writeFile(fhs.Proc+"self/setgroups",
if err := k.writeFile(FHSProc+"self/setgroups",
[]byte("deny\n"),
0); err != nil && !os.IsNotExist(err) {
k.fatalf(msg, "%v", err)
k.fatalf("%v", err)
}
if err := k.writeFile(fhs.Proc+"self/gid_map",
if err := k.writeFile(FHSProc+"self/gid_map",
append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...),
0); err != nil {
k.fatalf(msg, "%v", err)
k.fatalf("%v", err)
}
if err := k.setDumpable(SUID_DUMP_DISABLE); err != nil {
k.fatalf(msg, "cannot set SUID_DUMP_DISABLE: %v", err)
k.fatalf("cannot set SUID_DUMP_DISABLE: %v", err)
}
oldmask := k.umask(0)
if params.Hostname != "" {
if err := k.sethostname([]byte(params.Hostname)); err != nil {
k.fatalf(msg, "cannot set hostname: %v", err)
k.fatalf("cannot set hostname: %v", err)
}
}
// cache sysctl before pivot_root
lastcap := k.lastcap(msg)
lastcap := k.lastcap()
if err := k.mount(zeroString, fhs.Root, zeroString, MS_SILENT|MS_SLAVE|MS_REC, zeroString); err != nil {
k.fatalf(msg, "cannot make / rslave: %v", optionalErrorUnwrap(err))
if err := k.mount(zeroString, FHSRoot, zeroString, MS_SILENT|MS_SLAVE|MS_REC, zeroString); err != nil {
k.fatalf("cannot make / rslave: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
state := &setupState{process: make(map[int]WaitStatus), Params: &params.Params, Msg: msg, Context: ctx}
defer cancel()
state := &setupState{Params: &params.Params}
/* early is called right before pivot_root into intermediate root;
this step is mostly for gathering information that would otherwise be difficult to obtain
@@ -221,41 +180,41 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
the state of the mount namespace */
for i, op := range *params.Ops {
if op == nil || !op.Valid() {
k.fatalf(msg, "invalid op at index %d", i)
k.fatalf("invalid op at index %d", i)
}
if err := op.early(state, k); err != nil {
if m, ok := messageFromError(err); ok {
k.fatal(msg, m)
k.fatal(m)
} else {
k.fatalf(msg, "cannot prepare op at index %d: %v", i, err)
k.fatalf("cannot prepare op at index %d: %v", i, err)
}
}
}
if err := k.mount(SourceTmpfsRootfs, intermediateHostPath, FstypeTmpfs, MS_NODEV|MS_NOSUID, zeroString); err != nil {
k.fatalf(msg, "cannot mount intermediate root: %v", optionalErrorUnwrap(err))
k.fatalf("cannot mount intermediate root: %v", err)
}
if err := k.chdir(intermediateHostPath); err != nil {
k.fatalf(msg, "cannot enter intermediate host path: %v", err)
k.fatalf("cannot enter intermediate host path: %v", err)
}
if err := k.mkdir(sysrootDir, 0755); err != nil {
k.fatalf(msg, "%v", err)
k.fatalf("%v", err)
}
if err := k.mount(sysrootDir, sysrootDir, zeroString, MS_SILENT|MS_BIND|MS_REC, zeroString); err != nil {
k.fatalf(msg, "cannot bind sysroot: %v", optionalErrorUnwrap(err))
k.fatalf("cannot bind sysroot: %v", err)
}
if err := k.mkdir(hostDir, 0755); err != nil {
k.fatalf(msg, "%v", err)
k.fatalf("%v", err)
}
// pivot_root uncovers intermediateHostPath in hostDir
if err := k.pivotRoot(intermediateHostPath, hostDir); err != nil {
k.fatalf(msg, "cannot pivot into intermediate root: %v", err)
k.fatalf("cannot pivot into intermediate root: %v", err)
}
if err := k.chdir(fhs.Root); err != nil {
k.fatalf(msg, "cannot enter intermediate root: %v", err)
if err := k.chdir(FHSRoot); err != nil {
k.fatalf("cannot enter intermediate root: %v", err)
}
/* apply is called right after pivot_root and entering the new root;
@@ -264,65 +223,63 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
chdir is allowed but discouraged */
for i, op := range *params.Ops {
// ops already checked during early setup
if prefix, ok := op.prefix(); ok {
msg.Verbosef("%s %s", prefix, op)
}
k.verbosef("%s %s", op.prefix(), op)
if err := op.apply(state, k); err != nil {
if m, ok := messageFromError(err); ok {
k.fatal(msg, m)
k.fatal(m)
} else {
k.fatalf(msg, "cannot apply op at index %d: %v", i, err)
k.fatalf("cannot apply op at index %d: %v", i, err)
}
}
}
// setup requiring host root complete at this point
if err := k.mount(hostDir, hostDir, zeroString, MS_SILENT|MS_REC|MS_PRIVATE, zeroString); err != nil {
k.fatalf(msg, "cannot make host root rprivate: %v", optionalErrorUnwrap(err))
k.fatalf("cannot make host root rprivate: %v", err)
}
if err := k.unmount(hostDir, MNT_DETACH); err != nil {
k.fatalf(msg, "cannot unmount host root: %v", err)
k.fatalf("cannot unmount host root: %v", err)
}
{
var fd int
if err := IgnoringEINTR(func() (err error) {
fd, err = k.open(fhs.Root, O_DIRECTORY|O_RDONLY, 0)
fd, err = k.open(FHSRoot, O_DIRECTORY|O_RDONLY, 0)
return
}); err != nil {
k.fatalf(msg, "cannot open intermediate root: %v", err)
k.fatalf("cannot open intermediate root: %v", err)
}
if err := k.chdir(sysrootPath); err != nil {
k.fatalf(msg, "cannot enter sysroot: %v", err)
k.fatalf("cannot enter sysroot: %v", err)
}
if err := k.pivotRoot(".", "."); err != nil {
k.fatalf(msg, "cannot pivot into sysroot: %v", err)
k.fatalf("cannot pivot into sysroot: %v", err)
}
if err := k.fchdir(fd); err != nil {
k.fatalf(msg, "cannot re-enter intermediate root: %v", err)
k.fatalf("cannot re-enter intermediate root: %v", err)
}
if err := k.unmount(".", MNT_DETACH); err != nil {
k.fatalf(msg, "cannot unmount intermediate root: %v", err)
k.fatalf("cannot unmount intermediate root: %v", err)
}
if err := k.chdir(fhs.Root); err != nil {
k.fatalf(msg, "cannot enter root: %v", err)
if err := k.chdir(FHSRoot); err != nil {
k.fatalf("cannot enter root: %v", err)
}
if err := k.close(fd); err != nil {
k.fatalf(msg, "cannot close intermediate root: %v", err)
k.fatalf("cannot close intermediate root: %v", err)
}
}
if err := k.capAmbientClearAll(); err != nil {
k.fatalf(msg, "cannot clear the ambient capability set: %v", err)
k.fatalf("cannot clear the ambient capability set: %v", err)
}
for i := uintptr(0); i <= lastcap; i++ {
if params.Privileged && i == CAP_SYS_ADMIN {
continue
}
if err := k.capBoundingSetDrop(i); err != nil {
k.fatalf(msg, "cannot drop capability from bounding set: %v", err)
k.fatalf("cannot drop capability from bounding set: %v", err)
}
}
@@ -331,29 +288,29 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN)
if err := k.capAmbientRaise(CAP_SYS_ADMIN); err != nil {
k.fatalf(msg, "cannot raise CAP_SYS_ADMIN: %v", err)
k.fatalf("cannot raise CAP_SYS_ADMIN: %v", err)
}
}
if err := k.capset(
&capHeader{_LINUX_CAPABILITY_VERSION_3, 0},
&[2]capData{{0, keep[0], keep[0]}, {0, keep[1], keep[1]}},
); err != nil {
k.fatalf(msg, "cannot capset: %v", err)
k.fatalf("cannot capset: %v", err)
}
if !params.SeccompDisable {
rules := params.SeccompRules
if len(rules) == 0 { // non-empty rules slice always overrides presets
msg.Verbosef("resolving presets %#x", params.SeccompPresets)
k.verbosef("resolving presets %#x", params.SeccompPresets)
rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags)
}
if err := k.seccompLoad(rules, params.SeccompFlags); err != nil {
// this also indirectly asserts PR_SET_NO_NEW_PRIVS
k.fatalf(msg, "cannot load syscall filter: %v", err)
k.fatalf("cannot load syscall filter: %v", err)
}
msg.Verbosef("%d filter rules loaded", len(rules))
k.verbosef("%d filter rules loaded", len(rules))
} else {
msg.Verbose("syscall filter not configured")
k.verbose("syscall filter not configured")
}
extraFiles := make([]*os.File, params.Count)
@@ -363,30 +320,36 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
}
k.umask(oldmask)
// winfo represents an exited process from wait4.
cmd := exec.Command(params.Path.String())
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Args = params.Args
cmd.Env = params.Env
cmd.ExtraFiles = extraFiles
cmd.Dir = params.Dir.String()
k.verbosef("starting initial program %s", params.Path)
if err := k.start(cmd); err != nil {
k.fatalf("%v", err)
}
k.suspend()
if err := closeSetup(); err != nil {
k.printf("cannot close setup pipe: %v", err)
// not fatal
}
type winfo struct {
wpid int
wstatus WaitStatus
}
// info is closed as the wait4 thread terminates
// when there are no longer any processes left to reap
info := make(chan winfo, 1)
// whether initial process has started
var initialProcessStarted atomic.Bool
done := make(chan struct{})
k.new(func(k syscallDispatcher) {
k.lockOSThread()
wait4:
var (
err error
wpid = -2
wstatus WaitStatus
// whether initial process has started
started bool
)
// keep going until no child process is left
@@ -396,25 +359,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
}
if wpid != -2 {
if !state.processConcluded {
state.processMu.Lock()
if state.process == nil {
// early reaping has already concluded at this point
state.processConcluded = true
info <- winfo{wpid, wstatus}
} else {
// initial process has not yet been created, and the
// info channel is not yet being received from
state.process[wpid] = wstatus
}
state.processMu.Unlock()
} else {
info <- winfo{wpid, wstatus}
}
}
if !started {
started = initialProcessStarted.Load()
}
err = EINTR
@@ -422,145 +367,80 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
wpid, err = k.wait4(-1, &wstatus, 0, nil)
}
}
if !errors.Is(err, ECHILD) {
k.printf(msg, "unexpected wait4 response: %v", err)
} else if !started {
// initial process has not yet been reached and all daemons
// terminated or none were started in the first place
time.Sleep(500 * time.Microsecond)
goto wait4
k.printf("unexpected wait4 response: %v", err)
}
close(info)
close(done)
})
// called right before startup of initial process, all state changes to the
// current process is prohibited during late
for i, op := range *params.Ops {
// ops already checked during early setup
if err := op.late(state, k); err != nil {
if m, ok := messageFromError(err); ok {
k.fatal(msg, m)
} else if errors.Is(err, context.DeadlineExceeded) {
k.fatalf(msg, "%s deadline exceeded", op.String())
} else {
k.fatalf(msg, "cannot complete op at index %d: %v", i, err)
}
}
}
// early reaping has concluded, this must happen before initial process is created
state.processMu.Lock()
state.process = nil
state.processMu.Unlock()
if err := closeSetup(); err != nil {
k.fatalf(msg, "cannot close setup pipe: %v", err)
}
cmd := exec.Command(params.Path.String())
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Args = params.Args
cmd.Env = params.Env
cmd.ExtraFiles = extraFiles
cmd.Dir = params.Dir.String()
msg.Verbosef("starting initial process %s", params.Path)
if err := k.start(cmd); err != nil {
k.fatalf(msg, "%v", err)
}
initialProcessStarted.Store(true)
// handle signals to dump withheld messages
sig := make(chan os.Signal, 2)
k.notify(sig, CancelSignal,
os.Interrupt, SIGTERM, SIGQUIT)
k.notify(sig, os.Interrupt, CancelSignal)
// closed after residualProcessTimeout has elapsed after initial process death
timeout := make(chan struct{})
r := exitUnexpectedWait4
r := 2
for {
select {
case s := <-sig:
if k.resume() {
k.verbosef("%s after process start", s.String())
} else {
k.verbosef("got %s", s.String())
}
if s == CancelSignal && params.ForwardCancel && cmd.Process != nil {
msg.Verbose("forwarding context cancellation")
k.verbose("forwarding context cancellation")
if err := k.signal(cmd, os.Interrupt); err != nil {
k.printf(msg, "cannot forward cancellation: %v", err)
k.printf("cannot forward cancellation: %v", err)
}
continue
}
if s == SIGTERM || s == SIGQUIT {
msg.Verbosef("got %s, forwarding to initial process", s.String())
if err := k.signal(cmd, s); err != nil {
k.printf(msg, "cannot forward signal: %v", err)
}
continue
}
msg.Verbosef("got %s", s.String())
msg.BeforeExit()
k.beforeExit()
k.exit(0)
case w, ok := <-info:
if !ok {
msg.BeforeExit()
k.exit(r)
continue // unreachable
}
case w := <-info:
if w.wpid == cmd.Process.Pid {
// cancel Op context early
cancel()
// start timeout early
go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
// close initial process files; this also keeps them alive
for _, f := range extraFiles {
if err := f.Close(); err != nil {
msg.Verbose(err.Error())
}
}
// initial process exited, output is most likely available again
k.resume()
switch {
case w.wstatus.Exited():
r = w.wstatus.ExitStatus()
msg.Verbosef("initial process exited with code %d", w.wstatus.ExitStatus())
k.verbosef("initial process exited with code %d", w.wstatus.ExitStatus())
case w.wstatus.Signaled():
r = 128 + int(w.wstatus.Signal())
msg.Verbosef("initial process exited with signal %s", w.wstatus.Signal())
k.verbosef("initial process exited with signal %s", w.wstatus.Signal())
default:
r = 255
msg.Verbosef("initial process exited with status %#x", w.wstatus)
}
k.verbosef("initial process exited with status %#x", w.wstatus)
}
go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
}
case <-done:
k.beforeExit()
k.exit(r)
case <-timeout:
k.printf(msg, "timeout exceeded waiting for lingering processes")
msg.BeforeExit()
k.printf("timeout exceeded waiting for lingering processes")
k.beforeExit()
k.exit(r)
}
}
}
// initName is the prefix used by log.std in the init process.
const initName = "init"
// TryArgv0 calls [Init] if the last element of argv0 is "init".
// If a nil msg is passed, the system logger is used instead.
func TryArgv0(msg message.Msg) {
if msg == nil {
log.SetPrefix(initName + ": ")
log.SetFlags(0)
msg = message.New(log.Default())
}
func TryArgv0(v Msg, prepare func(prefix string), setVerbose func(verbose bool)) {
if len(os.Args) > 0 && path.Base(os.Args[0]) == initName {
Init(msg)
msg = v
Init(prepare, setVerbose)
msg.BeforeExit()
os.Exit(0)
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,15 +5,12 @@ import (
"fmt"
"os"
"syscall"
"hakurei.app/container/check"
"hakurei.app/container/std"
)
func init() { gob.Register(new(BindMountOp)) }
// Bind appends an [Op] that bind mounts host path [BindMountOp.Source] on container path [BindMountOp.Target].
func (f *Ops) Bind(source, target *check.Absolute, flags int) *Ops {
func (f *Ops) Bind(source, target *Absolute, flags int) *Ops {
*f = append(*f, &BindMountOp{nil, source, target, flags})
return f
}
@@ -21,39 +18,50 @@ func (f *Ops) Bind(source, target *check.Absolute, flags int) *Ops {
// BindMountOp bind mounts host path Source on container path Target.
// Note that Flags uses bits declared in this package and should not be set with constants in [syscall].
type BindMountOp struct {
sourceFinal, Source, Target *check.Absolute
sourceFinal, Source, Target *Absolute
Flags int
}
const (
// BindOptional skips nonexistent host paths.
BindOptional = 1 << iota
// BindWritable mounts filesystem read-write.
BindWritable
// BindDevice allows access to devices (special files) on this filesystem.
BindDevice
// BindEnsure attempts to create the host path if it does not exist.
BindEnsure
)
func (b *BindMountOp) Valid() bool {
return b != nil &&
b.Source != nil && b.Target != nil &&
b.Flags&(std.BindOptional|std.BindEnsure) != (std.BindOptional|std.BindEnsure)
b.Flags&(BindOptional|BindEnsure) != (BindOptional|BindEnsure)
}
func (b *BindMountOp) early(_ *setupState, k syscallDispatcher) error {
if b.Flags&std.BindEnsure != 0 {
if b.Flags&BindEnsure != 0 {
if err := k.mkdirAll(b.Source.String(), 0700); err != nil {
return err
}
}
if pathname, err := k.evalSymlinks(b.Source.String()); err != nil {
if os.IsNotExist(err) && b.Flags&std.BindOptional != 0 {
if os.IsNotExist(err) && b.Flags&BindOptional != 0 {
// leave sourceFinal as nil
return nil
}
return err
} else {
b.sourceFinal, err = check.NewAbs(pathname)
b.sourceFinal, err = NewAbs(pathname)
return err
}
}
func (b *BindMountOp) apply(state *setupState, k syscallDispatcher) error {
func (b *BindMountOp) apply(_ *setupState, k syscallDispatcher) error {
if b.sourceFinal == nil {
if b.Flags&std.BindOptional == 0 {
if b.Flags&BindOptional == 0 {
// unreachable
return OpStateError("bind")
}
@@ -76,21 +84,15 @@ func (b *BindMountOp) apply(state *setupState, k syscallDispatcher) error {
}
var flags uintptr = syscall.MS_REC
if b.Flags&std.BindWritable == 0 {
if b.Flags&BindWritable == 0 {
flags |= syscall.MS_RDONLY
}
if b.Flags&std.BindDevice == 0 {
if b.Flags&BindDevice == 0 {
flags |= syscall.MS_NODEV
}
if b.sourceFinal.String() == b.Target.String() {
state.Verbosef("mounting %q flags %#x", target, flags)
} else {
state.Verbosef("mounting %q on %q flags %#x", source, target, flags)
return k.bindMount(source, target, flags, b.sourceFinal == b.Target)
}
return k.bindMount(state, source, target, flags)
}
func (b *BindMountOp) late(*setupState, syscallDispatcher) error { return nil }
func (b *BindMountOp) Is(op Op) bool {
vb, ok := op.(*BindMountOp)
@@ -99,7 +101,7 @@ func (b *BindMountOp) Is(op Op) bool {
b.Target.Is(vb.Target) &&
b.Flags == vb.Flags
}
func (*BindMountOp) prefix() (string, bool) { return "mounting", false }
func (*BindMountOp) prefix() string { return "mounting" }
func (b *BindMountOp) String() string {
if b.Source == nil || b.Target == nil {
return "<invalid>"

View File

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

View File

@@ -1,134 +0,0 @@
package container
import (
"context"
"encoding/gob"
"errors"
"fmt"
"os"
"os/exec"
"slices"
"strconv"
"syscall"
"time"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
)
func init() { gob.Register(new(DaemonOp)) }
const (
// daemonTimeout is the duration a [DaemonOp] is allowed to block before the
// [DaemonOp.Target] marker becomes available.
daemonTimeout = 5 * time.Second
)
// Daemon appends an [Op] that starts a daemon in the container and blocks until
// [DaemonOp.Target] appears.
func (f *Ops) Daemon(target, path *check.Absolute, args ...string) *Ops {
*f = append(*f, &DaemonOp{target, path, args})
return f
}
// DaemonOp starts a daemon in the container and blocks until Target appears.
type DaemonOp struct {
// Pathname indicating readiness of daemon.
Target *check.Absolute
// Absolute pathname passed to [exec.Cmd].
Path *check.Absolute
// Arguments (excl. first) passed to [exec.Cmd].
Args []string
}
// earlyTerminationError is returned by [DaemonOp] when a daemon terminates
// before [DaemonOp.Target] appears.
type earlyTerminationError struct {
// Returned by [DaemonOp.String].
op string
// Copied from wait4 loop.
wstatus syscall.WaitStatus
}
func (e *earlyTerminationError) Error() string {
res := ""
switch {
case e.wstatus.Exited():
res = "exit status " + strconv.Itoa(e.wstatus.ExitStatus())
case e.wstatus.Signaled():
res = "signal: " + e.wstatus.Signal().String()
case e.wstatus.Stopped():
res = "stop signal: " + e.wstatus.StopSignal().String()
if e.wstatus.StopSignal() == syscall.SIGTRAP && e.wstatus.TrapCause() != 0 {
res += " (trap " + strconv.Itoa(e.wstatus.TrapCause()) + ")"
}
case e.wstatus.Continued():
res = "continued"
}
if e.wstatus.CoreDump() {
res += " (core dumped)"
}
return res
}
func (e *earlyTerminationError) Message() string { return e.op + " " + e.Error() }
func (d *DaemonOp) Valid() bool { return d != nil && d.Target != nil && d.Path != nil }
func (d *DaemonOp) early(*setupState, syscallDispatcher) error { return nil }
func (d *DaemonOp) apply(*setupState, syscallDispatcher) error { return nil }
func (d *DaemonOp) late(state *setupState, k syscallDispatcher) error {
cmd := exec.CommandContext(state.Context, d.Path.String(), d.Args...)
cmd.Env = state.Env
cmd.Dir = fhs.Root
if state.IsVerbose() {
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
}
// WaitDelay: left unset because lifetime is bound by AdoptWaitDelay on cancellation
cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGTERM) }
state.Verbosef("starting %s", d.String())
if err := k.start(cmd); err != nil {
return err
}
deadline := time.Now().Add(daemonTimeout)
var wstatusErr error
for {
if _, err := k.stat(d.Target.String()); err != nil {
if !errors.Is(err, os.ErrNotExist) {
_ = k.signal(cmd, os.Kill)
return err
}
if time.Now().After(deadline) {
_ = k.signal(cmd, os.Kill)
return context.DeadlineExceeded
}
if wstatusErr != nil {
return wstatusErr
}
if wstatus, ok := state.terminated(cmd.Process.Pid); ok {
// check once again: process could have satisfied Target between stat and the lookup
wstatusErr = &earlyTerminationError{d.String(), wstatus}
continue
}
time.Sleep(500 * time.Microsecond)
continue
}
state.Verbosef("daemon process %d ready", cmd.Process.Pid)
return nil
}
}
func (d *DaemonOp) Is(op Op) bool {
vd, ok := op.(*DaemonOp)
return ok && d.Valid() && vd.Valid() &&
d.Target.Is(vd.Target) && d.Path.Is(vd.Path) &&
slices.Equal(d.Args, vd.Args)
}
func (*DaemonOp) prefix() (string, bool) { return zeroString, false }
func (d *DaemonOp) String() string { return fmt.Sprintf("daemon providing %q", d.Target) }

View File

@@ -1,127 +0,0 @@
package container
import (
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
"hakurei.app/message"
)
func TestEarlyTerminationError(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
err error
want string
msg string
}{
{"exited", &earlyTerminationError{
`daemon providing "/run/user/1971/pulse/native"`, 127 << 8,
}, "exit status 127", `daemon providing "/run/user/1971/pulse/native" exit status 127`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.err.Error(); got != tc.want {
t.Errorf("Error: %q, want %q", got, tc.want)
}
if got := tc.err.(message.Error).Message(); got != tc.msg {
t.Errorf("Message: %s, want %s", got, tc.msg)
}
})
}
}
func TestDaemonOp(t *testing.T) {
t.Parallel()
checkSimple(t, "DaemonOp.late", []simpleTestCase{
{"success", func(k *kstub) error {
state := setupState{Params: &Params{Env: []string{"\x00"}}, Context: t.Context(), Msg: k}
return (&DaemonOp{
Target: check.MustAbs("/run/user/1971/pulse/native"),
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
Args: []string{"-v"},
}).late(&state, k)
}, stub.Expect{Calls: []stub.Call{
call("isVerbose", stub.ExpectArgs{}, true, nil),
call("verbosef", stub.ExpectArgs{"starting %s", []any{`daemon providing "/run/user/1971/pulse/native"`}}, nil, nil),
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/pipewire-pulse", []string{"/run/current-system/sw/bin/pipewire-pulse", "-v"}, []string{"\x00"}, "/"}, &os.Process{Pid: 0xcafe}, nil),
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), os.ErrNotExist),
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), os.ErrNotExist),
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), os.ErrNotExist),
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), nil),
call("verbosef", stub.ExpectArgs{"daemon process %d ready", []any{0xcafe}}, nil, nil),
}}, nil},
})
checkOpsValid(t, []opValidTestCase{
{"nil", (*DaemonOp)(nil), false},
{"zero", new(DaemonOp), false},
{"valid", &DaemonOp{
Target: check.MustAbs("/run/user/1971/pulse/native"),
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
Args: []string{"-v"},
}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"pipewire-pulse", new(Ops).Daemon(
check.MustAbs("/run/user/1971/pulse/native"),
check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"), "-v",
), Ops{
&DaemonOp{
Target: check.MustAbs("/run/user/1971/pulse/native"),
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
Args: []string{"-v"},
},
}},
})
checkOpIs(t, []opIsTestCase{
{"zero", new(DaemonOp), new(DaemonOp), false},
{"args differs", &DaemonOp{
Target: check.MustAbs("/run/user/1971/pulse/native"),
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
Args: []string{"-v"},
}, &DaemonOp{
Target: check.MustAbs("/run/user/1971/pulse/native"),
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
}, false},
{"path differs", &DaemonOp{
Target: check.MustAbs("/run/user/1971/pulse/native"),
Path: check.MustAbs("/run/current-system/sw/bin/pipewire"),
}, &DaemonOp{
Target: check.MustAbs("/run/user/1971/pulse/native"),
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
}, false},
{"target differs", &DaemonOp{
Target: check.MustAbs("/run/user/65534/pulse/native"),
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
}, &DaemonOp{
Target: check.MustAbs("/run/user/1971/pulse/native"),
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
}, false},
{"equals", &DaemonOp{
Target: check.MustAbs("/run/user/1971/pulse/native"),
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
}, &DaemonOp{
Target: check.MustAbs("/run/user/1971/pulse/native"),
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"pipewire-pulse", &DaemonOp{
Target: check.MustAbs("/run/user/1971/pulse/native"),
}, zeroString, `daemon providing "/run/user/1971/pulse/native"`},
})
}

View File

@@ -5,22 +5,19 @@ import (
"fmt"
"path"
. "syscall"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
)
func init() { gob.Register(new(MountDevOp)) }
// Dev appends an [Op] that mounts a subset of host /dev.
func (f *Ops) Dev(target *check.Absolute, mqueue bool) *Ops {
func (f *Ops) Dev(target *Absolute, mqueue bool) *Ops {
*f = append(*f, &MountDevOp{target, mqueue, false})
return f
}
// DevWritable appends an [Op] that mounts a writable subset of host /dev.
// There is usually no good reason to write to /dev, so this should always be followed by a [RemountOp].
func (f *Ops) DevWritable(target *check.Absolute, mqueue bool) *Ops {
func (f *Ops) DevWritable(target *Absolute, mqueue bool) *Ops {
*f = append(*f, &MountDevOp{target, mqueue, true})
return f
}
@@ -29,7 +26,7 @@ func (f *Ops) DevWritable(target *check.Absolute, mqueue bool) *Ops {
// If Mqueue is true, a private instance of [FstypeMqueue] is mounted.
// If Write is true, the resulting mount point is left writable.
type MountDevOp struct {
Target *check.Absolute
Target *Absolute
Mqueue bool
Write bool
}
@@ -49,25 +46,25 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
return err
}
if err := k.bindMount(
state,
toHost(fhs.Dev+name),
toHost(FHSDev+name),
targetPath,
0,
true,
); err != nil {
return err
}
}
for i, name := range []string{"stdin", "stdout", "stderr"} {
if err := k.symlink(
fhs.Proc+"self/fd/"+string(rune(i+'0')),
FHSProc+"self/fd/"+string(rune(i+'0')),
path.Join(target, name),
); err != nil {
return err
}
}
for _, pair := range [][2]string{
{fhs.Proc + "self/fd", "fd"},
{fhs.Proc + "kcore", "core"},
{FHSProc + "self/fd", "fd"},
{FHSProc + "kcore", "core"},
{"pts/ptmx", "ptmx"},
} {
if err := k.symlink(pair[0], path.Join(target, pair[1])); err != nil {
@@ -97,10 +94,10 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
if name, err := k.readlink(hostProc.stdout()); err != nil {
return err
} else if err = k.bindMount(
state,
toHost(name),
consolePath,
0,
false,
); err != nil {
return err
}
@@ -121,12 +118,11 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
return nil
}
if err := k.remount(state, target, MS_RDONLY); err != nil {
if err := k.remount(target, MS_RDONLY); err != nil {
return err
}
return k.mountTmpfs(SourceTmpfs, devShmPath, MS_NOSUID|MS_NODEV, 0, 01777)
}
func (d *MountDevOp) late(*setupState, syscallDispatcher) error { return nil }
func (d *MountDevOp) Is(op Op) bool {
vd, ok := op.(*MountDevOp)
@@ -135,7 +131,7 @@ func (d *MountDevOp) Is(op Op) bool {
d.Mqueue == vd.Mqueue &&
d.Write == vd.Write
}
func (*MountDevOp) prefix() (string, bool) { return "mounting", true }
func (*MountDevOp) prefix() string { return "mounting" }
func (d *MountDevOp) String() string {
if d.Mqueue {
return fmt.Sprintf("dev on %q with mqueue", d.Target)

View File

@@ -4,23 +4,20 @@ import (
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
)
func TestMountDevOp(t *testing.T) {
t.Parallel()
checkOpBehaviour(t, []opBehaviourTestCase{
{"mountTmpfs", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, stub.UniqueError(27)),
}, stub.UniqueError(27)},
{"ensureFile null", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -28,7 +25,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(26)},
{"bindMount null", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -37,7 +34,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(25)},
{"ensureFile zero", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -47,7 +44,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(24)},
{"bindMount zero", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -58,7 +55,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(23)},
{"ensureFile full", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -70,7 +67,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(22)},
{"bindMount full", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -83,7 +80,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(21)},
{"ensureFile random", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -97,7 +94,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(20)},
{"bindMount random", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -112,7 +109,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(19)},
{"ensureFile urandom", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -128,7 +125,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(18)},
{"bindMount urandom", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -145,7 +142,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(17)},
{"ensureFile tty", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -163,7 +160,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(16)},
{"bindMount tty", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -182,7 +179,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(15)},
{"symlink stdin", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -202,7 +199,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(14)},
{"symlink stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -223,7 +220,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(13)},
{"symlink stderr", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -245,7 +242,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(12)},
{"symlink fd", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -268,7 +265,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(11)},
{"symlink kcore", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -292,7 +289,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(10)},
{"symlink ptmx", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -317,7 +314,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(9)},
{"mkdir shm", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -343,7 +340,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(8)},
{"mkdir devpts", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -370,7 +367,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(7)},
{"mount devpts", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -398,7 +395,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(6)},
{"ensureFile stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -428,7 +425,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(5)},
{"readlink stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -459,7 +456,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(4)},
{"bindMount stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -491,7 +488,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(3)},
{"mkdir mqueue", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -524,7 +521,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(2)},
{"mount mqueue", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -558,7 +555,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(1)},
{"success no session", &Params{ParentPerm: 0755}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
Write: true,
}, nil, nil, []stub.Call{
@@ -589,7 +586,7 @@ func TestMountDevOp(t *testing.T) {
}, nil},
{"success no tty", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
Write: true,
}, nil, nil, []stub.Call{
@@ -621,7 +618,7 @@ func TestMountDevOp(t *testing.T) {
}, nil},
{"remount", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
@@ -653,7 +650,7 @@ func TestMountDevOp(t *testing.T) {
}, stub.UniqueError(0)},
{"success no mqueue", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil),
@@ -686,7 +683,7 @@ func TestMountDevOp(t *testing.T) {
}, nil},
{"success rw", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
Write: true,
}, nil, nil, []stub.Call{
@@ -721,7 +718,7 @@ func TestMountDevOp(t *testing.T) {
}, nil},
{"success", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil),
@@ -760,20 +757,20 @@ func TestMountDevOp(t *testing.T) {
checkOpsValid(t, []opValidTestCase{
{"nil", (*MountDevOp)(nil), false},
{"zero", new(MountDevOp), false},
{"valid", &MountDevOp{Target: check.MustAbs("/dev/")}, true},
{"valid", &MountDevOp{Target: MustAbs("/dev/")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"dev", new(Ops).Dev(check.MustAbs("/dev/"), true), Ops{
{"dev", new(Ops).Dev(MustAbs("/dev/"), true), Ops{
&MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
},
}},
{"dev writable", new(Ops).DevWritable(check.MustAbs("/.hakurei/dev/"), false), Ops{
{"dev writable", new(Ops).DevWritable(MustAbs("/.hakurei/dev/"), false), Ops{
&MountDevOp{
Target: check.MustAbs("/.hakurei/dev/"),
Target: MustAbs("/.hakurei/dev/"),
Write: true,
},
}},
@@ -783,46 +780,46 @@ func TestMountDevOp(t *testing.T) {
{"zero", new(MountDevOp), new(MountDevOp), false},
{"write differs", &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
Write: true,
}, false},
{"mqueue differs", &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, false},
{"target differs", &MountDevOp{
Target: check.MustAbs("/"),
Target: MustAbs("/"),
Mqueue: true,
}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, false},
{"equals", &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"mqueue", &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Mqueue: true,
}, "mounting", `dev on "/dev/" with mqueue`},
{"dev", &MountDevOp{
Target: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"),
}, "mounting", `dev on "/dev/"`},
})
}

View File

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

View File

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

View File

@@ -5,9 +5,6 @@ import (
"fmt"
"slices"
"strings"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
)
const (
@@ -55,7 +52,7 @@ func (e *OverlayArgumentError) Error() string {
}
// Overlay appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target].
func (f *Ops) Overlay(target, state, work *check.Absolute, layers ...*check.Absolute) *Ops {
func (f *Ops) Overlay(target, state, work *Absolute, layers ...*Absolute) *Ops {
*f = append(*f, &MountOverlayOp{
Target: target,
Lower: layers,
@@ -67,34 +64,34 @@ func (f *Ops) Overlay(target, state, work *check.Absolute, layers ...*check.Abso
// OverlayEphemeral appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target]
// with an ephemeral upperdir and workdir.
func (f *Ops) OverlayEphemeral(target *check.Absolute, layers ...*check.Absolute) *Ops {
return f.Overlay(target, fhs.AbsRoot, nil, layers...)
func (f *Ops) OverlayEphemeral(target *Absolute, layers ...*Absolute) *Ops {
return f.Overlay(target, AbsFHSRoot, nil, layers...)
}
// OverlayReadonly appends an [Op] that mounts the overlay pseudo filesystem readonly on [MountOverlayOp.Target]
func (f *Ops) OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) *Ops {
func (f *Ops) OverlayReadonly(target *Absolute, layers ...*Absolute) *Ops {
return f.Overlay(target, nil, nil, layers...)
}
// MountOverlayOp mounts [FstypeOverlay] on container path Target.
type MountOverlayOp struct {
Target *check.Absolute
Target *Absolute
// Any filesystem, does not need to be on a writable filesystem.
Lower []*check.Absolute
Lower []*Absolute
// formatted for [OptionOverlayLowerdir], resolved, prefixed and escaped during early
lower []string
// The upperdir is normally on a writable filesystem.
//
// If Work is nil and Upper holds the special value [fhs.AbsRoot],
// If Work is nil and Upper holds the special value [AbsFHSRoot],
// an ephemeral upperdir and workdir will be set up.
//
// If both Work and Upper are nil, upperdir and workdir is omitted and the overlay is mounted readonly.
Upper *check.Absolute
Upper *Absolute
// formatted for [OptionOverlayUpperdir], resolved, prefixed and escaped during early
upper string
// The workdir needs to be an empty directory on the same filesystem as upperdir.
Work *check.Absolute
Work *Absolute
// formatted for [OptionOverlayWorkdir], resolved, prefixed and escaped during early
work string
@@ -120,7 +117,7 @@ func (o *MountOverlayOp) Valid() bool {
func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
if o.Work == nil && o.Upper != nil {
switch o.Upper.String() {
case fhs.Root: // ephemeral
case FHSRoot: // ephemeral
o.ephemeral = true // intermediate root not yet available
default:
@@ -139,7 +136,7 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
if v, err := k.evalSymlinks(o.Upper.String()); err != nil {
return err
} else {
o.upper = check.EscapeOverlayDataSegment(toHost(v))
o.upper = EscapeOverlayDataSegment(toHost(v))
}
}
@@ -147,7 +144,7 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
if v, err := k.evalSymlinks(o.Work.String()); err != nil {
return err
} else {
o.work = check.EscapeOverlayDataSegment(toHost(v))
o.work = EscapeOverlayDataSegment(toHost(v))
}
}
}
@@ -157,7 +154,7 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
if v, err := k.evalSymlinks(a.String()); err != nil {
return err
} else {
o.lower[i] = check.EscapeOverlayDataSegment(toHost(v))
o.lower[i] = EscapeOverlayDataSegment(toHost(v))
}
}
return nil
@@ -175,10 +172,10 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
if o.ephemeral {
var err error
// these directories are created internally, therefore early (absolute, symlink, prefix, escape) is bypassed
if o.upper, err = k.mkdirTemp(fhs.Root, intermediatePatternOverlayUpper); err != nil {
if o.upper, err = k.mkdirTemp(FHSRoot, intermediatePatternOverlayUpper); err != nil {
return err
}
if o.work, err = k.mkdirTemp(fhs.Root, intermediatePatternOverlayWork); err != nil {
if o.work, err = k.mkdirTemp(FHSRoot, intermediatePatternOverlayWork); err != nil {
return err
}
}
@@ -199,22 +196,20 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
OptionOverlayWorkdir+"="+o.work)
}
options = append(options,
OptionOverlayLowerdir+"="+strings.Join(o.lower, check.SpecialOverlayPath),
OptionOverlayLowerdir+"="+strings.Join(o.lower, SpecialOverlayPath),
OptionOverlayUserxattr)
return k.mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, check.SpecialOverlayOption))
return k.mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, SpecialOverlayOption))
}
func (o *MountOverlayOp) late(*setupState, syscallDispatcher) error { return nil }
func (o *MountOverlayOp) Is(op Op) bool {
vo, ok := op.(*MountOverlayOp)
return ok && o.Valid() && vo.Valid() &&
o.Target.Is(vo.Target) &&
slices.EqualFunc(o.Lower, vo.Lower, func(a, v *check.Absolute) bool { return a.Is(v) }) &&
slices.EqualFunc(o.Lower, vo.Lower, func(a *Absolute, v *Absolute) bool { return a.Is(v) }) &&
o.Upper.Is(vo.Upper) && o.Work.Is(vo.Work)
}
func (*MountOverlayOp) prefix() (string, bool) { return "mounting", true }
func (*MountOverlayOp) prefix() string { return "mounting" }
func (o *MountOverlayOp) String() string {
return fmt.Sprintf("overlay on %q with %d layers", o.Target, len(o.Lower))
}

View File

@@ -5,16 +5,11 @@ import (
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
)
func TestMountOverlayOp(t *testing.T) {
t.Parallel()
t.Run("argument error", func(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
err *OverlayArgumentError
@@ -34,7 +29,6 @@ func TestMountOverlayOp(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.err.Error(); got != tc.want {
t.Errorf("Error: %q, want %q", got, tc.want)
}
@@ -44,21 +38,21 @@ func TestMountOverlayOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{
{"mkdirTemp invalid ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
Target: check.MustAbs("/"),
Lower: []*check.Absolute{
check.MustAbs("/var/lib/planterette/base/debian:f92c9052"),
check.MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
Target: MustAbs("/"),
Lower: []*Absolute{
MustAbs("/var/lib/planterette/base/debian:f92c9052"),
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
},
Upper: check.MustAbs("/proc/"),
Upper: MustAbs("/proc/"),
}, nil, &OverlayArgumentError{OverlayEphemeralUnexpectedUpper, "/proc/"}, nil, nil},
{"mkdirTemp upper ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
Target: check.MustAbs("/"),
Lower: []*check.Absolute{
check.MustAbs("/var/lib/planterette/base/debian:f92c9052"),
check.MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
Target: MustAbs("/"),
Lower: []*Absolute{
MustAbs("/var/lib/planterette/base/debian:f92c9052"),
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
},
Upper: check.MustAbs("/"),
Upper: MustAbs("/"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil),
@@ -68,12 +62,12 @@ func TestMountOverlayOp(t *testing.T) {
}, stub.UniqueError(6)},
{"mkdirTemp work ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
Target: check.MustAbs("/"),
Lower: []*check.Absolute{
check.MustAbs("/var/lib/planterette/base/debian:f92c9052"),
check.MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
Target: MustAbs("/"),
Lower: []*Absolute{
MustAbs("/var/lib/planterette/base/debian:f92c9052"),
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
},
Upper: check.MustAbs("/"),
Upper: MustAbs("/"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil),
@@ -84,12 +78,12 @@ func TestMountOverlayOp(t *testing.T) {
}, stub.UniqueError(5)},
{"success ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
Target: check.MustAbs("/"),
Lower: []*check.Absolute{
check.MustAbs("/var/lib/planterette/base/debian:f92c9052"),
check.MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
Target: MustAbs("/"),
Lower: []*Absolute{
MustAbs("/var/lib/planterette/base/debian:f92c9052"),
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
},
Upper: check.MustAbs("/"),
Upper: MustAbs("/"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil),
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil),
@@ -107,9 +101,9 @@ func TestMountOverlayOp(t *testing.T) {
}, nil},
{"short lower ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{
check.MustAbs("/mnt-root/nix/.ro-store"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{
MustAbs("/mnt-root/nix/.ro-store"),
},
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil),
@@ -118,10 +112,10 @@ func TestMountOverlayOp(t *testing.T) {
}, &OverlayArgumentError{OverlayReadonlyLower, zeroString}},
{"success ro noPrefix", &Params{ParentPerm: 0755}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{
check.MustAbs("/mnt-root/nix/.ro-store"),
check.MustAbs("/mnt-root/nix/.ro-store0"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{
MustAbs("/mnt-root/nix/.ro-store"),
MustAbs("/mnt-root/nix/.ro-store0"),
},
noPrefix: true,
}, []stub.Call{
@@ -137,10 +131,10 @@ func TestMountOverlayOp(t *testing.T) {
}, nil},
{"success ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{
check.MustAbs("/mnt-root/nix/.ro-store"),
check.MustAbs("/mnt-root/nix/.ro-store0"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{
MustAbs("/mnt-root/nix/.ro-store"),
MustAbs("/mnt-root/nix/.ro-store0"),
},
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil),
@@ -155,9 +149,9 @@ func TestMountOverlayOp(t *testing.T) {
}, nil},
{"nil lower", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: MustAbs("/nix/store"),
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
@@ -166,29 +160,29 @@ func TestMountOverlayOp(t *testing.T) {
}, &OverlayArgumentError{OverlayEmptyLower, zeroString}},
{"evalSymlinks upper", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", stub.UniqueError(4)),
}, stub.UniqueError(4), nil, nil},
{"evalSymlinks work", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", stub.UniqueError(3)),
}, stub.UniqueError(3), nil, nil},
{"evalSymlinks lower", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
@@ -196,10 +190,10 @@ func TestMountOverlayOp(t *testing.T) {
}, stub.UniqueError(2), nil, nil},
{"mkdirAll", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
@@ -209,10 +203,10 @@ func TestMountOverlayOp(t *testing.T) {
}, stub.UniqueError(1)},
{"mount", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
@@ -223,10 +217,10 @@ func TestMountOverlayOp(t *testing.T) {
}, stub.UniqueError(0)},
{"success single layer", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
@@ -241,16 +235,16 @@ func TestMountOverlayOp(t *testing.T) {
}, nil},
{"success", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{
check.MustAbs("/mnt-root/nix/.ro-store"),
check.MustAbs("/mnt-root/nix/.ro-store0"),
check.MustAbs("/mnt-root/nix/.ro-store1"),
check.MustAbs("/mnt-root/nix/.ro-store2"),
check.MustAbs("/mnt-root/nix/.ro-store3"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{
MustAbs("/mnt-root/nix/.ro-store"),
MustAbs("/mnt-root/nix/.ro-store0"),
MustAbs("/mnt-root/nix/.ro-store1"),
MustAbs("/mnt-root/nix/.ro-store2"),
MustAbs("/mnt-root/nix/.ro-store3"),
},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil),
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil),
@@ -275,13 +269,10 @@ func TestMountOverlayOp(t *testing.T) {
})
t.Run("unreachable", func(t *testing.T) {
t.Parallel()
t.Run("nil Upper non-nil Work not ephemeral", func(t *testing.T) {
t.Parallel()
wantErr := OpStateError("overlay")
if err := (&MountOverlayOp{
Work: check.MustAbs("/"),
Work: MustAbs("/"),
}).early(nil, nil); !errors.Is(err, wantErr) {
t.Errorf("apply: error = %v, want %v", err, wantErr)
}
@@ -291,45 +282,39 @@ func TestMountOverlayOp(t *testing.T) {
checkOpsValid(t, []opValidTestCase{
{"nil", (*MountOverlayOp)(nil), false},
{"zero", new(MountOverlayOp), false},
{"nil lower", &MountOverlayOp{Target: check.MustAbs("/"), Lower: []*check.Absolute{nil}}, false},
{"ro", &MountOverlayOp{Target: check.MustAbs("/"), Lower: []*check.Absolute{check.MustAbs("/")}}, true},
{"ro work", &MountOverlayOp{Target: check.MustAbs("/"), Work: check.MustAbs("/tmp/")}, false},
{"rw", &MountOverlayOp{Target: check.MustAbs("/"), Lower: []*check.Absolute{check.MustAbs("/")}, Upper: check.MustAbs("/"), Work: check.MustAbs("/")}, true},
{"nil lower", &MountOverlayOp{Target: MustAbs("/"), Lower: []*Absolute{nil}}, false},
{"ro", &MountOverlayOp{Target: MustAbs("/"), Lower: []*Absolute{MustAbs("/")}}, true},
{"ro work", &MountOverlayOp{Target: MustAbs("/"), Work: MustAbs("/tmp/")}, false},
{"rw", &MountOverlayOp{Target: MustAbs("/"), Lower: []*Absolute{MustAbs("/")}, Upper: MustAbs("/"), Work: MustAbs("/")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"full", new(Ops).Overlay(
check.MustAbs("/nix/store"),
check.MustAbs("/mnt-root/nix/.rw-store/upper"),
check.MustAbs("/mnt-root/nix/.rw-store/work"),
check.MustAbs("/mnt-root/nix/.ro-store"),
MustAbs("/nix/store"),
MustAbs("/mnt-root/nix/.rw-store/upper"),
MustAbs("/mnt-root/nix/.rw-store/work"),
MustAbs("/mnt-root/nix/.ro-store"),
), Ops{
&MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
},
}},
{"ephemeral", new(Ops).OverlayEphemeral(
check.MustAbs("/nix/store"),
check.MustAbs("/mnt-root/nix/.ro-store"),
), Ops{
{"ephemeral", new(Ops).OverlayEphemeral(MustAbs("/nix/store"), MustAbs("/mnt-root/nix/.ro-store")), Ops{
&MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/"),
},
}},
{"readonly", new(Ops).OverlayReadonly(
check.MustAbs("/nix/store"),
check.MustAbs("/mnt-root/nix/.ro-store"),
), Ops{
{"readonly", new(Ops).OverlayReadonly(MustAbs("/nix/store"), MustAbs("/mnt-root/nix/.ro-store")), Ops{
&MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
},
}},
})
@@ -338,74 +323,74 @@ func TestMountOverlayOp(t *testing.T) {
{"zero", new(MountOverlayOp), new(MountOverlayOp), false},
{"differs target", &MountOverlayOp{
Target: check.MustAbs("/nix/store/differs"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: MustAbs("/nix/store/differs"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work")}, false},
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work")}, false},
{"differs lower", &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store/differs")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store/differs")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work")}, false},
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work")}, false},
{"differs upper", &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper/differs"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper/differs"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work")}, false},
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work")}, false},
{"differs work", &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work/differs"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work/differs"),
}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work")}, false},
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work")}, false},
{"equals ro", &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")}}, true},
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")}}, true},
{"equals", &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work")}, true},
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work")}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"nix", &MountOverlayOp{
Target: check.MustAbs("/nix/store"),
Lower: []*check.Absolute{check.MustAbs("/mnt-root/nix/.ro-store")},
Upper: check.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: check.MustAbs("/mnt-root/nix/.rw-store/work"),
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, "mounting", `overlay on "/nix/store" with 1 layers`},
})
}

View File

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

View File

@@ -4,17 +4,15 @@ import (
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
)
func TestTmpfileOp(t *testing.T) {
const sampleDataString = `chronos:x:65534:65534:Hakurei:/var/empty:/bin/zsh`
var (
samplePath = check.MustAbs("/etc/passwd")
samplePath = MustAbs("/etc/passwd")
sampleData = []byte(sampleDataString)
)
t.Parallel()
checkOpBehaviour(t, []opBehaviourTestCase{
{"createTemp", &Params{ParentPerm: 0700}, &TmpfileOp{
@@ -83,8 +81,18 @@ func TestTmpfileOp(t *testing.T) {
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"full", new(Ops).Place(samplePath, sampleData), Ops{
&TmpfileOp{Path: samplePath, Data: sampleData},
{"noref", new(Ops).Place(samplePath, sampleData), Ops{
&TmpfileOp{
Path: samplePath,
Data: sampleData,
},
}},
{"ref", new(Ops).PlaceP(samplePath, new(*[]byte)), Ops{
&TmpfileOp{
Path: samplePath,
Data: []byte{},
},
}},
})
@@ -92,7 +100,7 @@ func TestTmpfileOp(t *testing.T) {
{"zero", new(TmpfileOp), new(TmpfileOp), false},
{"differs path", &TmpfileOp{
Path: check.MustAbs("/etc/group"),
Path: MustAbs("/etc/group"),
Data: sampleData,
}, &TmpfileOp{
Path: samplePath,

View File

@@ -4,20 +4,18 @@ import (
"encoding/gob"
"fmt"
. "syscall"
"hakurei.app/container/check"
)
func init() { gob.Register(new(MountProcOp)) }
// Proc appends an [Op] that mounts a private instance of proc.
func (f *Ops) Proc(target *check.Absolute) *Ops {
func (f *Ops) Proc(target *Absolute) *Ops {
*f = append(*f, &MountProcOp{target})
return f
}
// MountProcOp mounts a new instance of [FstypeProc] on container path Target.
type MountProcOp struct{ Target *check.Absolute }
type MountProcOp struct{ Target *Absolute }
func (p *MountProcOp) Valid() bool { return p != nil && p.Target != nil }
func (p *MountProcOp) early(*setupState, syscallDispatcher) error { return nil }
@@ -28,12 +26,11 @@ func (p *MountProcOp) apply(state *setupState, k syscallDispatcher) error {
}
return k.mount(SourceProc, target, FstypeProc, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString)
}
func (p *MountProcOp) late(*setupState, syscallDispatcher) error { return nil }
func (p *MountProcOp) Is(op Op) bool {
vp, ok := op.(*MountProcOp)
return ok && p.Valid() && vp.Valid() &&
p.Target.Is(vp.Target)
}
func (*MountProcOp) prefix() (string, bool) { return "mounting", true }
func (*MountProcOp) prefix() string { return "mounting" }
func (p *MountProcOp) String() string { return fmt.Sprintf("proc on %q", p.Target) }

View File

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

View File

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

View File

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

View File

@@ -4,21 +4,19 @@ import (
"encoding/gob"
"fmt"
"path"
"hakurei.app/container/check"
)
func init() { gob.Register(new(SymlinkOp)) }
// Link appends an [Op] that creates a symlink in the container filesystem.
func (f *Ops) Link(target *check.Absolute, linkName string, dereference bool) *Ops {
func (f *Ops) Link(target *Absolute, linkName string, dereference bool) *Ops {
*f = append(*f, &SymlinkOp{target, linkName, dereference})
return f
}
// SymlinkOp optionally dereferences LinkName and creates a symlink at container path Target.
type SymlinkOp struct {
Target *check.Absolute
Target *Absolute
// LinkName is an arbitrary uninterpreted pathname.
LinkName string
@@ -30,8 +28,8 @@ func (l *SymlinkOp) Valid() bool { return l != nil && l.Target != nil && l.LinkN
func (l *SymlinkOp) early(_ *setupState, k syscallDispatcher) error {
if l.Dereference {
if !path.IsAbs(l.LinkName) {
return check.AbsoluteError(l.LinkName)
if !isAbs(l.LinkName) {
return &AbsoluteError{l.LinkName}
}
if name, err := k.readlink(l.LinkName); err != nil {
return err
@@ -50,8 +48,6 @@ func (l *SymlinkOp) apply(state *setupState, k syscallDispatcher) error {
return k.symlink(l.LinkName, target)
}
func (l *SymlinkOp) late(*setupState, syscallDispatcher) error { return nil }
func (l *SymlinkOp) Is(op Op) bool {
vl, ok := op.(*SymlinkOp)
return ok && l.Valid() && vl.Valid() &&
@@ -59,7 +55,7 @@ func (l *SymlinkOp) Is(op Op) bool {
l.LinkName == vl.LinkName &&
l.Dereference == vl.Dereference
}
func (*SymlinkOp) prefix() (string, bool) { return "creating", true }
func (*SymlinkOp) prefix() string { return "creating" }
func (l *SymlinkOp) String() string {
return fmt.Sprintf("symlink on %q linkname %q", l.Target, l.LinkName)
}

View File

@@ -4,29 +4,26 @@ import (
"os"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
)
func TestSymlinkOp(t *testing.T) {
t.Parallel()
checkOpBehaviour(t, []opBehaviourTestCase{
{"mkdir", &Params{ParentPerm: 0700}, &SymlinkOp{
Target: check.MustAbs("/etc/nixos"),
Target: MustAbs("/etc/nixos"),
LinkName: "/etc/static/nixos",
}, nil, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, stub.UniqueError(1)),
}, stub.UniqueError(1)},
{"abs", &Params{ParentPerm: 0755}, &SymlinkOp{
Target: check.MustAbs("/etc/mtab"),
Target: MustAbs("/etc/mtab"),
LinkName: "etc/mtab",
Dereference: true,
}, nil, check.AbsoluteError("etc/mtab"), nil, nil},
}, nil, &AbsoluteError{"etc/mtab"}, nil, nil},
{"readlink", &Params{ParentPerm: 0755}, &SymlinkOp{
Target: check.MustAbs("/etc/mtab"),
Target: MustAbs("/etc/mtab"),
LinkName: "/etc/mtab",
Dereference: true,
}, []stub.Call{
@@ -34,7 +31,7 @@ func TestSymlinkOp(t *testing.T) {
}, stub.UniqueError(0), nil, nil},
{"success noderef", &Params{ParentPerm: 0700}, &SymlinkOp{
Target: check.MustAbs("/etc/nixos"),
Target: MustAbs("/etc/nixos"),
LinkName: "/etc/static/nixos",
}, nil, nil, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, nil),
@@ -42,7 +39,7 @@ func TestSymlinkOp(t *testing.T) {
}, nil},
{"success", &Params{ParentPerm: 0755}, &SymlinkOp{
Target: check.MustAbs("/etc/mtab"),
Target: MustAbs("/etc/mtab"),
LinkName: "/etc/mtab",
Dereference: true,
}, []stub.Call{
@@ -57,18 +54,18 @@ func TestSymlinkOp(t *testing.T) {
{"nil", (*SymlinkOp)(nil), false},
{"zero", new(SymlinkOp), false},
{"nil target", &SymlinkOp{LinkName: "/run/current-system"}, false},
{"zero linkname", &SymlinkOp{Target: check.MustAbs("/run/current-system")}, false},
{"valid", &SymlinkOp{Target: check.MustAbs("/run/current-system"), LinkName: "/run/current-system", Dereference: true}, true},
{"zero linkname", &SymlinkOp{Target: MustAbs("/run/current-system")}, false},
{"valid", &SymlinkOp{Target: MustAbs("/run/current-system"), LinkName: "/run/current-system", Dereference: true}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"current-system", new(Ops).Link(
check.MustAbs("/run/current-system"),
MustAbs("/run/current-system"),
"/run/current-system",
true,
), Ops{
&SymlinkOp{
Target: check.MustAbs("/run/current-system"),
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
},
@@ -79,40 +76,40 @@ func TestSymlinkOp(t *testing.T) {
{"zero", new(SymlinkOp), new(SymlinkOp), false},
{"target differs", &SymlinkOp{
Target: check.MustAbs("/run/current-system/differs"),
Target: MustAbs("/run/current-system/differs"),
LinkName: "/run/current-system",
Dereference: true,
}, &SymlinkOp{
Target: check.MustAbs("/run/current-system"),
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
}, false},
{"linkname differs", &SymlinkOp{
Target: check.MustAbs("/run/current-system"),
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system/differs",
Dereference: true,
}, &SymlinkOp{
Target: check.MustAbs("/run/current-system"),
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
}, false},
{"dereference differs", &SymlinkOp{
Target: check.MustAbs("/run/current-system"),
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system",
}, &SymlinkOp{
Target: check.MustAbs("/run/current-system"),
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
}, false},
{"equals", &SymlinkOp{
Target: check.MustAbs("/run/current-system"),
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
}, &SymlinkOp{
Target: check.MustAbs("/run/current-system"),
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
}, true},
@@ -120,7 +117,7 @@ func TestSymlinkOp(t *testing.T) {
checkOpMeta(t, []opMetaTestCase{
{"current-system", &SymlinkOp{
Target: check.MustAbs("/run/current-system"),
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
}, "creating", `symlink on "/run/current-system" linkname "/run/current-system"`},

View File

@@ -7,8 +7,6 @@ import (
"os"
"strconv"
. "syscall"
"hakurei.app/container/check"
)
func init() { gob.Register(new(MountTmpfsOp)) }
@@ -20,13 +18,13 @@ func (e TmpfsSizeError) Error() string {
}
// Tmpfs appends an [Op] that mounts tmpfs on container path [MountTmpfsOp.Path].
func (f *Ops) Tmpfs(target *check.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})
return f
}
// Readonly appends an [Op] that mounts read-only tmpfs on container path [MountTmpfsOp.Path].
func (f *Ops) Readonly(target *check.Absolute, perm os.FileMode) *Ops {
func (f *Ops) Readonly(target *Absolute, perm os.FileMode) *Ops {
*f = append(*f, &MountTmpfsOp{SourceTmpfsReadonly, target, MS_RDONLY | MS_NOSUID | MS_NODEV, 0, perm})
return f
}
@@ -34,7 +32,7 @@ func (f *Ops) Readonly(target *check.Absolute, perm os.FileMode) *Ops {
// MountTmpfsOp mounts [FstypeTmpfs] on container Path.
type MountTmpfsOp struct {
FSName string
Path *check.Absolute
Path *Absolute
Flags uintptr
Size int
Perm os.FileMode
@@ -48,7 +46,6 @@ func (t *MountTmpfsOp) apply(_ *setupState, k syscallDispatcher) error {
}
return k.mountTmpfs(t.FSName, toSysroot(t.Path.String()), t.Flags, t.Size, t.Perm)
}
func (t *MountTmpfsOp) late(*setupState, syscallDispatcher) error { return nil }
func (t *MountTmpfsOp) Is(op Op) bool {
vt, ok := op.(*MountTmpfsOp)
@@ -59,5 +56,5 @@ func (t *MountTmpfsOp) Is(op Op) bool {
t.Size == vt.Size &&
t.Perm == vt.Perm
}
func (*MountTmpfsOp) prefix() (string, bool) { return "mounting", true }
func (*MountTmpfsOp) prefix() string { return "mounting" }
func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }

View File

@@ -5,15 +5,11 @@ import (
"syscall"
"testing"
"hakurei.app/container/check"
"hakurei.app/container/stub"
)
func TestMountTmpfsOp(t *testing.T) {
t.Parallel()
t.Run("size error", func(t *testing.T) {
t.Parallel()
tmpfsSizeError := TmpfsSizeError(-1)
want := "tmpfs size -1 out of bounds"
if got := tmpfsSizeError.Error(); got != want {
@@ -28,7 +24,7 @@ func TestMountTmpfsOp(t *testing.T) {
{"success", new(Params), &MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user/1000/"),
Path: MustAbs("/run/user/1000/"),
Size: 1 << 10,
Perm: 0700,
}, nil, nil, []stub.Call{
@@ -46,19 +42,19 @@ func TestMountTmpfsOp(t *testing.T) {
{"nil", (*MountTmpfsOp)(nil), false},
{"zero", new(MountTmpfsOp), false},
{"nil path", &MountTmpfsOp{FSName: "tmpfs"}, false},
{"zero fsname", &MountTmpfsOp{Path: check.MustAbs("/tmp/")}, false},
{"valid", &MountTmpfsOp{FSName: "tmpfs", Path: check.MustAbs("/tmp/")}, true},
{"zero fsname", &MountTmpfsOp{Path: MustAbs("/tmp/")}, false},
{"valid", &MountTmpfsOp{FSName: "tmpfs", Path: MustAbs("/tmp/")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"runtime", new(Ops).Tmpfs(
check.MustAbs("/run/user"),
MustAbs("/run/user"),
1<<10,
0755,
), Ops{
&MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user"),
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
@@ -66,12 +62,12 @@ func TestMountTmpfsOp(t *testing.T) {
}},
{"nscd", new(Ops).Readonly(
check.MustAbs("/var/run/nscd"),
MustAbs("/var/run/nscd"),
0755,
), Ops{
&MountTmpfsOp{
FSName: "readonly",
Path: check.MustAbs("/var/run/nscd"),
Path: MustAbs("/var/run/nscd"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV | syscall.MS_RDONLY,
Perm: 0755,
},
@@ -83,13 +79,13 @@ func TestMountTmpfsOp(t *testing.T) {
{"fsname differs", &MountTmpfsOp{
FSName: "readonly",
Path: check.MustAbs("/run/user"),
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
}, &MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user"),
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
@@ -97,13 +93,13 @@ func TestMountTmpfsOp(t *testing.T) {
{"path differs", &MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user/differs"),
Path: MustAbs("/run/user/differs"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
}, &MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user"),
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
@@ -111,13 +107,13 @@ func TestMountTmpfsOp(t *testing.T) {
{"flags differs", &MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user"),
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV | syscall.MS_RDONLY,
Size: 1 << 10,
Perm: 0755,
}, &MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user"),
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
@@ -125,13 +121,13 @@ func TestMountTmpfsOp(t *testing.T) {
{"size differs", &MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user"),
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1,
Perm: 0755,
}, &MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user"),
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
@@ -139,13 +135,13 @@ func TestMountTmpfsOp(t *testing.T) {
{"perm differs", &MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user"),
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0700,
}, &MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user"),
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
@@ -153,13 +149,13 @@ func TestMountTmpfsOp(t *testing.T) {
{"equals", &MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user"),
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
}, &MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user"),
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
@@ -169,7 +165,7 @@ func TestMountTmpfsOp(t *testing.T) {
checkOpMeta(t, []opMetaTestCase{
{"runtime", &MountTmpfsOp{
FSName: "ephemeral",
Path: check.MustAbs("/run/user"),
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,

View File

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

View File

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

View File

@@ -4,10 +4,10 @@ import (
"errors"
"fmt"
"os"
"strings"
. "syscall"
"hakurei.app/container/vfs"
"hakurei.app/message"
)
/*
@@ -59,6 +59,7 @@ const (
FstypeNULL = zeroString
// FstypeProc represents the proc pseudo-filesystem.
// A fully visible instance of proc must be available in the mount namespace for proc to be mounted.
// This filesystem type is usually mounted on [FHSProc].
FstypeProc = "proc"
// FstypeDevpts represents the devpts pseudo-filesystem.
// This type of filesystem is usually mounted on /dev/pts.
@@ -85,20 +86,34 @@ const (
// OptionOverlayUserxattr represents the userxattr option of the overlay pseudo-filesystem.
// Use the "user.overlay." xattr namespace instead of "trusted.overlay.".
OptionOverlayUserxattr = "userxattr"
// SpecialOverlayEscape is the escape string for overlay mount options.
SpecialOverlayEscape = `\`
// SpecialOverlayOption is the separator string between overlay mount options.
SpecialOverlayOption = ","
// SpecialOverlayPath is the separator string between overlay paths.
SpecialOverlayPath = ":"
)
// bindMount mounts source on target and recursively applies flags if MS_REC is set.
func (p *procPaths) bindMount(msg message.Msg, source, target string, flags uintptr) error {
func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) error {
// syscallDispatcher.bindMount and procPaths.remount must not be called from this function
if eq {
p.k.verbosef("resolved %q flags %#x", target, flags)
} else {
p.k.verbosef("resolved %q on %q flags %#x", source, target, flags)
}
if err := p.k.mount(source, target, FstypeNULL, MS_SILENT|MS_BIND|flags&MS_REC, zeroString); err != nil {
return err
}
return p.k.remount(msg, target, flags)
return p.k.remount(target, flags)
}
// remount applies flags on target, recursively if MS_REC is set.
func (p *procPaths) remount(msg message.Msg, target string, flags uintptr) error {
func (p *procPaths) remount(target string, flags uintptr) error {
// syscallDispatcher methods bindMount, remount must not be called from this function
var targetFinal string
@@ -107,7 +122,7 @@ func (p *procPaths) remount(msg message.Msg, target string, flags uintptr) error
} else {
targetFinal = v
if targetFinal != target {
msg.Verbosef("target resolves to %q", targetFinal)
p.k.verbosef("target resolves to %q", targetFinal)
}
}
@@ -137,7 +152,7 @@ func (p *procPaths) remount(msg message.Msg, target string, flags uintptr) error
return err
}
if err = remountWithFlags(p.k, msg, n, mf); err != nil {
if err = remountWithFlags(p.k, n, mf); err != nil {
return err
}
if flags&MS_REC == 0 {
@@ -150,7 +165,7 @@ func (p *procPaths) remount(msg message.Msg, target string, flags uintptr) error
continue
}
if err = remountWithFlags(p.k, msg, cur, mf); err != nil && !errors.Is(err, EACCES) {
if err = remountWithFlags(p.k, cur, mf); err != nil && !errors.Is(err, EACCES) {
return err
}
}
@@ -160,12 +175,12 @@ func (p *procPaths) remount(msg message.Msg, target string, flags uintptr) error
}
// remountWithFlags remounts mount point described by [vfs.MountInfoNode].
func remountWithFlags(k syscallDispatcher, msg message.Msg, n *vfs.MountInfoNode, mf uintptr) error {
func remountWithFlags(k syscallDispatcher, n *vfs.MountInfoNode, mf uintptr) error {
// syscallDispatcher methods bindMount, remount must not be called from this function
kf, unmatched := n.Flags()
if len(unmatched) != 0 {
msg.Verbosef("unmatched vfs options: %q", unmatched)
k.verbosef("unmatched vfs options: %q", unmatched)
}
if kf&mf != mf {
@@ -199,3 +214,20 @@ func parentPerm(perm os.FileMode) os.FileMode {
}
return os.FileMode(pperm)
}
// EscapeOverlayDataSegment escapes a string for formatting into the data argument of an overlay mount call.
func EscapeOverlayDataSegment(s string) string {
if s == zeroString {
return zeroString
}
if f := strings.SplitN(s, "\x00", 2); len(f) > 0 {
s = f[0]
}
return strings.NewReplacer(
SpecialOverlayEscape, SpecialOverlayEscape+SpecialOverlayEscape,
SpecialOverlayOption, SpecialOverlayEscape+SpecialOverlayOption,
SpecialOverlayPath, SpecialOverlayEscape+SpecialOverlayPath,
).Replace(s)
}

View File

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

52
container/msg.go Normal file
View File

@@ -0,0 +1,52 @@
package container
import (
"errors"
"log"
"sync/atomic"
)
// MessageError is an error with a user-facing message.
type MessageError interface {
// Message returns a user-facing error message.
Message() string
error
}
// GetErrorMessage returns whether an error implements [MessageError], and the message if it does.
func GetErrorMessage(err error) (string, bool) {
var e MessageError
if !errors.As(err, &e) || e == nil {
return zeroString, false
}
return e.Message(), true
}
type Msg interface {
IsVerbose() bool
Verbose(v ...any)
Verbosef(format string, v ...any)
Suspend()
Resume() bool
BeforeExit()
}
type DefaultMsg struct{ inactive atomic.Bool }
func (msg *DefaultMsg) IsVerbose() bool { return true }
func (msg *DefaultMsg) Verbose(v ...any) {
if !msg.inactive.Load() {
log.Println(v...)
}
}
func (msg *DefaultMsg) Verbosef(format string, v ...any) {
if !msg.inactive.Load() {
log.Printf(format, v...)
}
}
func (msg *DefaultMsg) Suspend() { msg.inactive.Store(true) }
func (msg *DefaultMsg) Resume() bool { return msg.inactive.CompareAndSwap(true, false) }
func (msg *DefaultMsg) BeforeExit() {}

148
container/msg_test.go Normal file
View File

@@ -0,0 +1,148 @@
package container_test
import (
"errors"
"log"
"strings"
"sync/atomic"
"syscall"
"testing"
"hakurei.app/container"
)
func TestMessageError(t *testing.T) {
testCases := []struct {
name string
err error
want string
wantOk bool
}{
{"nil", nil, "", false},
{"new", errors.New(":3"), "", false},
{"start", &container.StartError{
Step: "meow",
Err: syscall.ENOTRECOVERABLE,
}, "cannot meow: state not recoverable", true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got, ok := container.GetErrorMessage(tc.err)
if got != tc.want {
t.Errorf("GetErrorMessage: %q, want %q", got, tc.want)
}
if ok != tc.wantOk {
t.Errorf("GetErrorMessage: ok = %v, want %v", ok, tc.wantOk)
}
})
}
}
func TestDefaultMsg(t *testing.T) {
{
w := log.Writer()
f := log.Flags()
t.Cleanup(func() { log.SetOutput(w); log.SetFlags(f) })
}
msg := new(container.DefaultMsg)
t.Run("is verbose", func(t *testing.T) {
if !msg.IsVerbose() {
t.Error("IsVerbose unexpected outcome")
}
})
t.Run("verbose", func(t *testing.T) {
log.SetOutput(panicWriter{})
msg.Suspend()
msg.Verbose()
msg.Verbosef("\x00")
msg.Resume()
buf := new(strings.Builder)
log.SetOutput(buf)
log.SetFlags(0)
msg.Verbose()
msg.Verbosef("\x00")
want := "\n\x00\n"
if buf.String() != want {
t.Errorf("Verbose: %q, want %q", buf.String(), want)
}
})
t.Run("inactive", func(t *testing.T) {
{
inactive := msg.Resume()
if inactive {
t.Cleanup(func() { msg.Suspend() })
}
}
if msg.Resume() {
t.Error("Resume unexpected outcome")
}
msg.Suspend()
if !msg.Resume() {
t.Error("Resume unexpected outcome")
}
})
// the function is a noop
t.Run("beforeExit", func(t *testing.T) { msg.BeforeExit() })
}
type panicWriter struct{}
func (panicWriter) Write([]byte) (int, error) { panic("unreachable") }
func saveRestoreOutput(t *testing.T) {
out := container.GetOutput()
t.Cleanup(func() { container.SetOutput(out) })
}
func replaceOutput(t *testing.T) {
saveRestoreOutput(t)
container.SetOutput(&testOutput{t: t})
}
type testOutput struct {
t *testing.T
suspended atomic.Bool
}
func (out *testOutput) IsVerbose() bool { return testing.Verbose() }
func (out *testOutput) Verbose(v ...any) {
if !out.IsVerbose() {
return
}
out.t.Log(v...)
}
func (out *testOutput) Verbosef(format string, v ...any) {
if !out.IsVerbose() {
return
}
out.t.Logf(format, v...)
}
func (out *testOutput) Suspend() {
if out.suspended.CompareAndSwap(false, true) {
out.Verbose("suspend called")
return
}
out.Verbose("suspend called on suspended output")
}
func (out *testOutput) Resume() bool {
if out.suspended.CompareAndSwap(true, false) {
out.Verbose("resume called")
return true
}
out.Verbose("resume called on unsuspended output")
return false
}
func (out *testOutput) BeforeExit() { out.Verbose("beforeExit called") }

View File

@@ -1,269 +0,0 @@
package container
import (
"encoding/binary"
"errors"
"net"
"os"
. "syscall"
"unsafe"
"hakurei.app/container/std"
"hakurei.app/message"
)
// rtnetlink represents a NETLINK_ROUTE socket.
type rtnetlink struct {
// Sent as part of rtnetlink messages.
pid uint32
// AF_NETLINK socket.
fd int
// Whether the socket is open.
ok bool
// Message sequence number.
seq uint32
}
// open creates the underlying NETLINK_ROUTE socket.
func (s *rtnetlink) open() (err error) {
if s.ok || s.fd < 0 {
return os.ErrInvalid
}
s.pid = uint32(Getpid())
if s.fd, err = Socket(
AF_NETLINK,
SOCK_RAW|SOCK_CLOEXEC,
NETLINK_ROUTE,
); err != nil {
return os.NewSyscallError("socket", err)
} else if err = Bind(s.fd, &SockaddrNetlink{
Family: AF_NETLINK,
Pid: s.pid,
}); err != nil {
_ = s.close()
return os.NewSyscallError("bind", err)
} else {
s.ok = true
return nil
}
}
// close closes the underlying NETLINK_ROUTE socket.
func (s *rtnetlink) close() error {
if !s.ok {
return os.ErrInvalid
}
s.ok = false
err := Close(s.fd)
s.fd = -1
return err
}
// roundtrip sends a netlink message and handles the reply.
func (s *rtnetlink) roundtrip(data []byte) error {
if !s.ok {
return os.ErrInvalid
}
defer func() { s.seq++ }()
if err := Sendto(s.fd, data, 0, &SockaddrNetlink{
Family: AF_NETLINK,
}); err != nil {
return os.NewSyscallError("sendto", err)
}
buf := make([]byte, Getpagesize())
done:
for {
p := buf
if n, _, err := Recvfrom(s.fd, p, 0); err != nil {
return os.NewSyscallError("recvfrom", err)
} else if n < NLMSG_HDRLEN {
return errors.ErrUnsupported
} else {
p = p[:n]
}
if msgs, err := ParseNetlinkMessage(p); err != nil {
return err
} else {
for _, m := range msgs {
if m.Header.Seq != s.seq || m.Header.Pid != s.pid {
return errors.ErrUnsupported
}
if m.Header.Type == NLMSG_DONE {
break done
}
if m.Header.Type == NLMSG_ERROR {
if len(m.Data) >= 4 {
errno := Errno(-std.ScmpInt(binary.NativeEndian.Uint32(m.Data)))
if errno == 0 {
return nil
}
return errno
}
return errors.ErrUnsupported
}
}
}
}
return nil
}
// mustRoundtrip calls roundtrip and terminates via msg for a non-nil error.
func (s *rtnetlink) mustRoundtrip(msg message.Msg, data []byte) {
err := s.roundtrip(data)
if err == nil {
return
}
if closeErr := Close(s.fd); closeErr != nil {
msg.Verbosef("cannot close: %v", err)
}
switch err.(type) {
case *os.SyscallError:
msg.GetLogger().Fatalf("cannot %v", err)
case Errno:
msg.GetLogger().Fatalf("RTNETLINK answers: %v", err)
default:
msg.GetLogger().Fatalln("RTNETLINK answers with unexpected message")
}
}
// newaddrLo represents a RTM_NEWADDR message with two addresses.
type newaddrLo struct {
header NlMsghdr
data IfAddrmsg
r0 RtAttr
a0 [4]byte // in_addr
r1 RtAttr
a1 [4]byte // in_addr
}
// sizeofNewaddrLo is the expected size of newaddrLo.
const sizeofNewaddrLo = NLMSG_HDRLEN + SizeofIfAddrmsg + (SizeofRtAttr+4)*2
// newaddrLo returns the address of a populated newaddrLo.
func (s *rtnetlink) newaddrLo(lo int) *newaddrLo {
return &newaddrLo{NlMsghdr{
Len: sizeofNewaddrLo,
Type: RTM_NEWADDR,
Flags: NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL,
Seq: s.seq,
Pid: s.pid,
}, IfAddrmsg{
Family: AF_INET,
Prefixlen: 8,
Flags: IFA_F_PERMANENT,
Scope: RT_SCOPE_HOST,
Index: uint32(lo),
}, RtAttr{
Len: uint16(SizeofRtAttr + len(newaddrLo{}.a0)),
Type: IFA_LOCAL,
}, [4]byte{127, 0, 0, 1}, RtAttr{
Len: uint16(SizeofRtAttr + len(newaddrLo{}.a1)),
Type: IFA_ADDRESS,
}, [4]byte{127, 0, 0, 1}}
}
func (msg *newaddrLo) toWireFormat() []byte {
var buf [sizeofNewaddrLo]byte
*(*uint32)(unsafe.Pointer(&buf[0:4][0])) = msg.header.Len
*(*uint16)(unsafe.Pointer(&buf[4:6][0])) = msg.header.Type
*(*uint16)(unsafe.Pointer(&buf[6:8][0])) = msg.header.Flags
*(*uint32)(unsafe.Pointer(&buf[8:12][0])) = msg.header.Seq
*(*uint32)(unsafe.Pointer(&buf[12:16][0])) = msg.header.Pid
buf[16] = msg.data.Family
buf[17] = msg.data.Prefixlen
buf[18] = msg.data.Flags
buf[19] = msg.data.Scope
*(*uint32)(unsafe.Pointer(&buf[20:24][0])) = msg.data.Index
*(*uint16)(unsafe.Pointer(&buf[24:26][0])) = msg.r0.Len
*(*uint16)(unsafe.Pointer(&buf[26:28][0])) = msg.r0.Type
copy(buf[28:32], msg.a0[:])
*(*uint16)(unsafe.Pointer(&buf[32:34][0])) = msg.r1.Len
*(*uint16)(unsafe.Pointer(&buf[34:36][0])) = msg.r1.Type
copy(buf[36:40], msg.a1[:])
return buf[:]
}
// newlinkLo represents a RTM_NEWLINK message.
type newlinkLo struct {
header NlMsghdr
data IfInfomsg
}
// sizeofNewlinkLo is the expected size of newlinkLo.
const sizeofNewlinkLo = NLMSG_HDRLEN + SizeofIfInfomsg
// newlinkLo returns the address of a populated newlinkLo.
func (s *rtnetlink) newlinkLo(lo int) *newlinkLo {
return &newlinkLo{NlMsghdr{
Len: sizeofNewlinkLo,
Type: RTM_NEWLINK,
Flags: NLM_F_REQUEST | NLM_F_ACK,
Seq: s.seq,
Pid: s.pid,
}, IfInfomsg{
Family: AF_UNSPEC,
Index: int32(lo),
Flags: IFF_UP,
Change: IFF_UP,
}}
}
func (msg *newlinkLo) toWireFormat() []byte {
var buf [sizeofNewlinkLo]byte
*(*uint32)(unsafe.Pointer(&buf[0:4][0])) = msg.header.Len
*(*uint16)(unsafe.Pointer(&buf[4:6][0])) = msg.header.Type
*(*uint16)(unsafe.Pointer(&buf[6:8][0])) = msg.header.Flags
*(*uint32)(unsafe.Pointer(&buf[8:12][0])) = msg.header.Seq
*(*uint32)(unsafe.Pointer(&buf[12:16][0])) = msg.header.Pid
buf[16] = msg.data.Family
*(*uint16)(unsafe.Pointer(&buf[18:20][0])) = msg.data.Type
*(*int32)(unsafe.Pointer(&buf[20:24][0])) = msg.data.Index
*(*uint32)(unsafe.Pointer(&buf[24:28][0])) = msg.data.Flags
*(*uint32)(unsafe.Pointer(&buf[28:32][0])) = msg.data.Change
return buf[:]
}
// mustLoopback creates the loopback address and brings the lo interface up.
// mustLoopback calls a fatal method of the underlying [log.Logger] of m with a
// user-facing error message if RTNETLINK behaves unexpectedly.
func mustLoopback(msg message.Msg) {
log := msg.GetLogger()
var lo int
if ifi, err := net.InterfaceByName("lo"); err != nil {
log.Fatalln(err)
} else {
lo = ifi.Index
}
var s rtnetlink
if err := s.open(); err != nil {
log.Fatalln(err)
}
defer func() {
if err := s.close(); err != nil {
msg.Verbosef("cannot close netlink: %v", err)
}
}()
s.mustRoundtrip(msg, s.newaddrLo(lo).toWireFormat())
s.mustRoundtrip(msg, s.newlinkLo(lo).toWireFormat())
}

View File

@@ -1,72 +0,0 @@
package container
import (
"testing"
"unsafe"
)
func TestSizeof(t *testing.T) {
if got := unsafe.Sizeof(newaddrLo{}); got != sizeofNewaddrLo {
t.Fatalf("newaddrLo: sizeof = %#x, want %#x", got, sizeofNewaddrLo)
}
if got := unsafe.Sizeof(newlinkLo{}); got != sizeofNewlinkLo {
t.Fatalf("newlinkLo: sizeof = %#x, want %#x", got, sizeofNewlinkLo)
}
}
func TestRtnetlinkMessage(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
msg interface{ toWireFormat() []byte }
want []byte
}{
{"newaddrLo", (&rtnetlink{pid: 1, seq: 0}).newaddrLo(1), []byte{
/* Len */ 0x28, 0, 0, 0,
/* Type */ 0x14, 0,
/* Flags */ 5, 6,
/* Seq */ 0, 0, 0, 0,
/* Pid */ 1, 0, 0, 0,
/* Family */ 2,
/* Prefixlen */ 8,
/* Flags */ 0x80,
/* Scope */ 0xfe,
/* Index */ 1, 0, 0, 0,
/* Len */ 8, 0,
/* Type */ 2, 0,
/* in_addr */ 127, 0, 0, 1,
/* Len */ 8, 0,
/* Type */ 1, 0,
/* in_addr */ 127, 0, 0, 1,
}},
{"newlinkLo", (&rtnetlink{pid: 1, seq: 1}).newlinkLo(1), []byte{
/* Len */ 0x20, 0, 0, 0,
/* Type */ 0x10, 0,
/* Flags */ 5, 0,
/* Seq */ 1, 0, 0, 0,
/* Pid */ 1, 0, 0, 0,
/* Family */ 0,
/* pad */ 0,
/* Type */ 0, 0,
/* Index */ 1, 0, 0, 0,
/* Flags */ 1, 0, 0, 0,
/* Change */ 1, 0, 0, 0,
}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.msg.toWireFormat(); string(got) != string(tc.want) {
t.Fatalf("toWireFormat: %#v, want %#v", got, tc.want)
}
})
}
}

12
container/output.go Normal file
View File

@@ -0,0 +1,12 @@
package container
var msg Msg = new(DefaultMsg)
func GetOutput() Msg { return msg }
func SetOutput(v Msg) {
if v == nil {
msg = new(DefaultMsg)
} else {
msg = v
}
}

41
container/output_test.go Normal file
View File

@@ -0,0 +1,41 @@
package container
import (
"testing"
)
func TestGetSetOutput(t *testing.T) {
{
out := GetOutput()
t.Cleanup(func() { SetOutput(out) })
}
t.Run("default", func(t *testing.T) {
SetOutput(new(stubOutput))
if v, ok := GetOutput().(*DefaultMsg); ok {
t.Fatalf("SetOutput: got unexpected output %#v", v)
}
SetOutput(nil)
if _, ok := GetOutput().(*DefaultMsg); !ok {
t.Fatalf("SetOutput: got unexpected output %#v", GetOutput())
}
})
t.Run("stub", func(t *testing.T) {
SetOutput(new(stubOutput))
if _, ok := GetOutput().(*stubOutput); !ok {
t.Fatalf("SetOutput: got unexpected output %#v", GetOutput())
}
})
}
type stubOutput struct {
wrapF func(error, ...any) error
}
func (*stubOutput) IsVerbose() bool { panic("unreachable") }
func (*stubOutput) Verbose(...any) { panic("unreachable") }
func (*stubOutput) Verbosef(string, ...any) { panic("unreachable") }
func (*stubOutput) Suspend() { panic("unreachable") }
func (*stubOutput) Resume() bool { panic("unreachable") }
func (*stubOutput) BeforeExit() { panic("unreachable") }

View File

@@ -9,13 +9,13 @@ import (
)
// Setup appends the read end of a pipe for setup params transmission and returns its fd.
func Setup(extraFiles *[]*os.File) (int, *os.File, error) {
func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) {
if r, w, err := os.Pipe(); err != nil {
return -1, nil, err
} else {
fd := 3 + len(*extraFiles)
*extraFiles = append(*extraFiles, r)
return fd, w, nil
return fd, gob.NewEncoder(w), nil
}
}
@@ -31,7 +31,7 @@ func Receive(key string, e any, fdp *uintptr) (func() error, error) {
return nil, ErrReceiveEnv
} else {
if fd, err := strconv.Atoi(s); err != nil {
return nil, optionalErrorUnwrap(err)
return nil, errors.Unwrap(err)
} else {
setup = os.NewFile(uintptr(fd), "setup")
if setup == nil {

View File

@@ -1,7 +1,6 @@
package container_test
import (
"encoding/gob"
"errors"
"os"
"slices"
@@ -56,20 +55,16 @@ func TestSetupReceive(t *testing.T) {
t.Run("setup receive", func(t *testing.T) {
check := func(t *testing.T, useNilFdp bool) {
const key = "TEST_SETUP_RECEIVE"
payload := []uint64{syscall.MS_MGC_VAL, syscall.MS_MGC_MSK, syscall.MS_ASYNC, syscall.MS_ACTIVE}
payload := []int{syscall.MS_MGC_VAL, syscall.MS_MGC_MSK, syscall.MS_ASYNC, syscall.MS_ACTIVE}
encoderDone := make(chan error, 1)
extraFiles := make([]*os.File, 0, 1)
deadline, _ := t.Deadline()
if fd, f, err := container.Setup(&extraFiles); err != nil {
if fd, encoder, err := container.Setup(&extraFiles); err != nil {
t.Fatalf("Setup: error = %v", err)
} else if fd != 3 {
t.Fatalf("Setup: fd = %d, want 3", fd)
} else {
if err = f.SetDeadline(deadline); err != nil {
t.Fatal(err.Error())
}
go func() { encoderDone <- gob.NewEncoder(f).Encode(payload) }()
go func() { encoderDone <- encoder.Encode(payload) }()
}
if len(extraFiles) != 1 {
@@ -86,7 +81,7 @@ func TestSetupReceive(t *testing.T) {
}
var (
gotPayload []uint64
gotPayload []int
fdp *uintptr
)
if !useNilFdp {

View File

@@ -9,18 +9,84 @@ import (
"strings"
"syscall"
"hakurei.app/container/fhs"
"hakurei.app/container/vfs"
)
/* constants in this file bypass abs check, be extremely careful when changing them! */
const (
// FHSRoot points to the file system root.
FHSRoot = "/"
// FHSEtc points to the directory for system-specific configuration.
FHSEtc = "/etc/"
// FHSTmp points to the place for small temporary files.
FHSTmp = "/tmp/"
// FHSRun points to a "tmpfs" file system for system packages to place runtime data, socket files, and similar.
FHSRun = "/run/"
// FHSRunUser points to a directory containing per-user runtime directories,
// each usually individually mounted "tmpfs" instances.
FHSRunUser = FHSRun + "user/"
// FHSUsr points to vendor-supplied operating system resources.
FHSUsr = "/usr/"
// FHSUsrBin points to binaries and executables for user commands that shall appear in the $PATH search path.
FHSUsrBin = FHSUsr + "bin/"
// FHSVar points to persistent, variable system data. Writable during normal system operation.
FHSVar = "/var/"
// FHSVarLib points to persistent system data.
FHSVarLib = FHSVar + "lib/"
// FHSVarEmpty points to a nonstandard directory that is usually empty.
FHSVarEmpty = FHSVar + "empty/"
// FHSDev points to the root directory for device nodes.
FHSDev = "/dev/"
// FHSProc points to a virtual kernel file system exposing the process list and other functionality.
FHSProc = "/proc/"
// FHSProcSys points to a hierarchy below /proc/ that exposes a number of kernel tunables.
FHSProcSys = FHSProc + "sys/"
// FHSSys points to a virtual kernel file system exposing discovered devices and other functionality.
FHSSys = "/sys/"
)
var (
// AbsFHSRoot is [FHSRoot] as [Absolute].
AbsFHSRoot = &Absolute{FHSRoot}
// AbsFHSEtc is [FHSEtc] as [Absolute].
AbsFHSEtc = &Absolute{FHSEtc}
// AbsFHSTmp is [FHSTmp] as [Absolute].
AbsFHSTmp = &Absolute{FHSTmp}
// AbsFHSRun is [FHSRun] as [Absolute].
AbsFHSRun = &Absolute{FHSRun}
// AbsFHSRunUser is [FHSRunUser] as [Absolute].
AbsFHSRunUser = &Absolute{FHSRunUser}
// AbsFHSUsrBin is [FHSUsrBin] as [Absolute].
AbsFHSUsrBin = &Absolute{FHSUsrBin}
// AbsFHSVar is [FHSVar] as [Absolute].
AbsFHSVar = &Absolute{FHSVar}
// AbsFHSVarLib is [FHSVarLib] as [Absolute].
AbsFHSVarLib = &Absolute{FHSVarLib}
// AbsFHSDev is [FHSDev] as [Absolute].
AbsFHSDev = &Absolute{FHSDev}
// AbsFHSProc is [FHSProc] as [Absolute].
AbsFHSProc = &Absolute{FHSProc}
// AbsFHSSys is [FHSSys] as [Absolute].
AbsFHSSys = &Absolute{FHSSys}
)
const (
// Nonexistent is a path that cannot exist.
// /proc is chosen because a system with covered /proc is unsupported by this package.
Nonexistent = fhs.Proc + "nonexistent"
Nonexistent = FHSProc + "nonexistent"
hostPath = fhs.Root + hostDir
hostPath = FHSRoot + hostDir
hostDir = "host"
sysrootPath = fhs.Root + sysrootDir
sysrootPath = FHSRoot + sysrootDir
sysrootDir = "sysroot"
)

View File

@@ -10,7 +10,6 @@ import (
"testing"
"unsafe"
"hakurei.app/container/check"
"hakurei.app/container/vfs"
)
@@ -50,8 +49,8 @@ func TestToHost(t *testing.T) {
}
}
// InternalToHostOvlEscape exports toHost passed to [check.EscapeOverlayDataSegment].
func InternalToHostOvlEscape(s string) string { return check.EscapeOverlayDataSegment(toHost(s)) }
// InternalToHostOvlEscape exports toHost passed to EscapeOverlayDataSegment.
func InternalToHostOvlEscape(s string) string { return EscapeOverlayDataSegment(toHost(s)) }
func TestCreateFile(t *testing.T) {
t.Run("nonexistent", func(t *testing.T) {
@@ -173,8 +172,8 @@ func TestProcPaths(t *testing.T) {
}
})
t.Run("fd", func(t *testing.T) {
want := "/host/proc/self/fd/2147483647"
if got := hostProc.fd(math.MaxInt32); got != want {
want := "/host/proc/self/fd/9223372036854775807"
if got := hostProc.fd(math.MaxInt64); got != want {
t.Errorf("stdout: %q, want %q", got, want)
}
})

View File

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

View File

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

View File

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

View File

@@ -9,17 +9,14 @@
#define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
int32_t hakurei_scmp_make_filter(
int *ret_p, uintptr_t allocate_p,
uint32_t arch, uint32_t multiarch,
int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch,
uint32_t multiarch,
struct hakurei_syscall_rule *rules,
size_t rules_sz, hakurei_export_flag flags) {
int i;
int last_allowed_family;
int disallowed;
struct hakurei_syscall_rule *rule;
void *buf;
size_t len = 0;
int32_t res = 0; /* refer to resPrefix for message */
@@ -73,9 +70,11 @@ int32_t hakurei_scmp_make_filter(
assert(rule->m_errno == EPERM || rule->m_errno == ENOSYS);
if (rule->arg)
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno), rule->syscall, 1, *rule->arg);
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno),
rule->syscall, 1, *rule->arg);
else
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno), rule->syscall, 0);
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno),
rule->syscall, 0);
if (*ret_p == -EFAULT) {
res = 4;
@@ -92,38 +91,31 @@ int32_t hakurei_scmp_make_filter(
last_allowed_family = -1;
for (i = 0; i < LEN(socket_family_allowlist); i++) {
if (socket_family_allowlist[i].flags_mask != 0 &&
(socket_family_allowlist[i].flags_mask & flags) != socket_family_allowlist[i].flags_mask)
(socket_family_allowlist[i].flags_mask & flags) !=
socket_family_allowlist[i].flags_mask)
continue;
for (disallowed = last_allowed_family + 1; disallowed < socket_family_allowlist[i].family; disallowed++) {
for (disallowed = last_allowed_family + 1;
disallowed < socket_family_allowlist[i].family; disallowed++) {
/* Blocklist the in-between valid families */
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_EQ, disallowed));
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT),
SCMP_SYS(socket), 1,
SCMP_A0(SCMP_CMP_EQ, disallowed));
}
last_allowed_family = socket_family_allowlist[i].family;
}
/* Blocklist the rest */
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1));
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1,
SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1));
if (allocate_p == 0) {
if (fd < 0) {
*ret_p = seccomp_load(ctx);
if (*ret_p != 0) {
res = 7;
goto out;
}
} else {
*ret_p = seccomp_export_bpf_mem(ctx, NULL, &len);
if (*ret_p != 0) {
res = 6;
goto out;
}
buf = hakurei_scmp_allocate(allocate_p, len);
if (buf == NULL) {
res = 4;
goto out;
}
*ret_p = seccomp_export_bpf_mem(ctx, buf, &len);
*ret_p = seccomp_export_bpf(ctx, fd);
if (*ret_p != 0) {
res = 6;
goto out;

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