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: on:
- push - push
- pull_request
jobs: jobs:
hakurei: hakurei:
@@ -72,23 +73,6 @@ jobs:
path: result/* path: result/*
retention-days: 1 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: hpkg:
name: Hpkg name: Hpkg
runs-on: nix runs-on: nix
@@ -113,7 +97,6 @@ jobs:
- race - race
- sandbox - sandbox
- sandbox-race - sandbox-race
- sharefs
- hpkg - hpkg
runs-on: nix runs-on: nix
steps: steps:

1
.gitignore vendored
View File

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

View File

@@ -6,97 +6,73 @@ import (
"io" "io"
"log" "log"
"os" "os"
"os/exec" "os/signal"
"os/user" "os/user"
"strconv" "strconv"
"sync" "sync"
"syscall"
"time" "time"
_ "unsafe" // for go:linkname
"hakurei.app/command" "hakurei.app/command"
"hakurei.app/container/check" "hakurei.app/container"
"hakurei.app/container/fhs"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/dbus" "hakurei.app/internal"
"hakurei.app/internal/env" "hakurei.app/internal/app"
"hakurei.app/internal/info" "hakurei.app/internal/app/state"
"hakurei.app/internal/outcome" "hakurei.app/internal/hlog"
"hakurei.app/message" "hakurei.app/system"
"hakurei.app/system/dbus"
) )
// optionalErrorUnwrap calls [errors.Unwrap] and returns the resulting value func buildCommand(out io.Writer) command.Command {
// if it is not nil, or the original value if it is.
//
//go:linkname optionalErrorUnwrap hakurei.app/container.optionalErrorUnwrap
func optionalErrorUnwrap(err error) error
func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
var ( var (
flagVerbose bool flagVerbose bool
flagJSON bool flagJSON bool
) )
c := command.New(out, log.Printf, "hakurei", func([]string) error { c := command.New(out, log.Printf, "hakurei", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
msg.SwapVerbose(flagVerbose)
if early.yamaLSM != nil {
msg.Verbosef("cannot enable ptrace protection via Yama LSM: %v", early.yamaLSM)
// not fatal
}
if early.dumpable != nil {
log.Printf("cannot set SUID_DUMP_DISABLE: %s", early.dumpable)
// not fatal
}
return nil
}).
Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity"). Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable") 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 })
c.Command("app", "Load app from configuration file", func(args []string) error {
if len(args) < 1 {
log.Fatal("app requires at least 1 argument")
}
// config extraArgs...
config := tryPath(args[0])
config.Args = append(config.Args, args[1:]...)
runApp(config)
panic("unreachable")
})
{ {
var ( var (
flagIdentifierFile int dbusConfigSession string
) dbusConfigSystem string
c.NewCommand("app", "Load and start container from configuration file", func(args []string) error { mpris bool
if len(args) < 1 { dbusVerbose bool
log.Fatal("app requires at least 1 argument")
}
config := tryPath(msg, args[0]) fid string
if config != nil && config.Container != nil { aid int
config.Container.Args = append(config.Container.Args, args[1:]...) groups command.RepeatableFlag
} homeDir string
userName string
outcome.Main(ctx, msg, config, flagIdentifierFile) wayland, x11, dBus, pulse bool
panic("unreachable")
}).
Flag(&flagIdentifierFile, "identifier-fd", command.IntFlag(-1),
"Write identifier of current instance to fd after successful startup")
}
{
var (
flagDBusConfigSession string
flagDBusConfigSystem string
flagDBusMpris bool
flagDBusVerbose bool
flagID string
flagIdentity int
flagGroups command.RepeatableFlag
flagHomeDir string
flagUserName string
flagPrivateRuntime, flagPrivateTmpdir bool
flagWayland, flagX11, flagDBus, flagPipeWire, flagPulse bool
) )
c.NewCommand("run", "Configure and start a permissive container", func(args []string) error { c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error {
if flagIdentity < hst.IdentityStart || flagIdentity > hst.IdentityEnd { // initialise config from flags
log.Fatalf("identity %d out of range", flagIdentity) 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 // 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 passwd *user.User
passwdOnce sync.Once passwdOnce sync.Once
passwdFunc = func() { 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 { 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{ passwd = &user.User{
Uid: us, Uid: us,
Gid: us, Gid: us,
Username: "chronos", Username: "chronos",
Name: "Hakurei Permissive Default", Name: "Hakurei Permissive Default",
HomeDir: fhs.VarEmpty, HomeDir: container.FHSVarEmpty,
} }
} else { } else {
passwd = u 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 if homeDir == "os" {
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) passwdOnce.Do(passwdFunc)
config.Container.Username = passwd.Username homeDir = passwd.HomeDir
} }
{ if userName == "chronos" {
homeDir := flagHomeDir passwdOnce.Do(passwdFunc)
if homeDir == "os" { userName = passwd.Username
passwdOnce.Do(passwdFunc)
homeDir = passwd.HomeDir
}
if a, err := check.NewAbs(homeDir); err != nil {
log.Fatal(err.Error())
return err
} else {
config.Container.Home = a
}
} }
if !flagPrivateRuntime { config.Identity = aid
config.Container.Flags |= hst.FShareRuntime config.Groups = groups
config.Username = userName
if a, err := container.NewAbs(homeDir); err != nil {
log.Fatal(err.Error())
return err
} else {
config.Home = a
} }
if !flagPrivateTmpdir {
config.Container.Flags |= hst.FShareTmpdir var e system.Enablement
if wayland {
e |= system.EWayland
} }
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 // parse D-Bus config file from flags if applicable
if flagDBus { if dBus {
if flagDBusConfigSession == "builtin" { if dbusConfigSession == "builtin" {
config.SessionBus = dbus.NewConfig(flagID, true, flagDBusMpris) config.SessionBus = dbus.NewConfig(fid, true, mpris)
} else { } else {
if f, err := os.Open(flagDBusConfigSession); err != nil { if conf, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
log.Fatal(err.Error()) log.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err)
} else { } else {
decodeJSON(log.Fatal, "load session bus proxy config", f, &config.SessionBus) config.SessionBus = conf
if err = f.Close(); err != nil {
log.Fatal(err.Error())
}
} }
} }
// system bus proxy is optional // system bus proxy is optional
if flagDBusConfigSystem != "nil" { if dbusConfigSystem != "nil" {
if f, err := os.Open(flagDBusConfigSystem); err != nil { if conf, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil {
log.Fatal(err.Error()) log.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err)
} else { } else {
decodeJSON(log.Fatal, "load system bus proxy config", f, &config.SystemBus) config.SystemBus = conf
if err = f.Close(); err != nil {
log.Fatal(err.Error())
}
} }
} }
// override log from configuration // override log from configuration
if flagDBusVerbose { if dbusVerbose {
if config.SessionBus != nil { config.SessionBus.Log = true
config.SessionBus.Log = true config.SystemBus.Log = true
}
if config.SystemBus != nil {
config.SystemBus.Log = true
}
} }
} }
outcome.Main(ctx, msg, config, -1) // invoke app
runApp(config)
panic("unreachable") 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"). "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"). "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"). "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"). "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"). "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"). "Application identity").
Flag(nil, "g", &flagGroups, Flag(nil, "g", &groups,
"Groups inherited by all container processes"). "Groups inherited by all container processes").
Flag(&flagHomeDir, "d", command.StringFlag("os"), Flag(&homeDir, "d", command.StringFlag("os"),
"Container home directory"). "Container home directory").
Flag(&flagUserName, "u", command.StringFlag("chronos"), Flag(&userName, "u", command.StringFlag("chronos"),
"Passwd user name within sandbox"). "Passwd user name within sandbox").
Flag(&flagPrivateRuntime, "private-runtime", command.BoolFlag(false), Flag(&wayland, "wayland", command.BoolFlag(false),
"Do not share XDG_RUNTIME_DIR between containers under the same identity").
Flag(&flagPrivateTmpdir, "private-tmpdir", command.BoolFlag(false),
"Do not share TMPDIR between containers under the same identity").
Flag(&flagWayland, "wayland", command.BoolFlag(false),
"Enable connection to Wayland via security-context-v1"). "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"). "Enable direct connection to X11").
Flag(&flagDBus, "dbus", command.BoolFlag(false), Flag(&dBus, "dbus", command.BoolFlag(false),
"Enable proxied connection to D-Bus"). "Enable proxied connection to D-Bus").
Flag(&flagPipeWire, "pipewire", command.BoolFlag(false), Flag(&pulse, "pulse", command.BoolFlag(false),
"Enable connection to PipeWire via SecurityContext"). "Enable direct connection to PulseAudio")
Flag(&flagPulse, "pulse", command.BoolFlag(false),
"Enable PulseAudio compatibility daemon")
} }
{ var showFlagShort bool
var ( c.NewCommand("show", "Show live or local app configuration", func(args []string) error {
flagShort bool switch len(args) {
flagNoStore bool case 0: // system
) printShowSystem(os.Stdout, showFlagShort, flagJSON)
c.NewCommand("show", "Show live or local app configuration", func(args []string) error {
switch len(args) {
case 0: // system
printShowSystem(os.Stdout, flagShort, flagJSON)
case 1: // instance case 1: // instance
name := args[0] name := args[0]
config, entry := tryShort(name)
var ( if config == nil {
config *hst.Config config = tryPath(name)
entry *hst.State
)
if !flagNoStore {
var sc hst.Paths
env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil))
entry = tryIdentifier(msg, name, outcome.NewStore(&sc))
}
if entry == nil {
config = tryPath(msg, name)
} else {
config = entry.Config
}
if !printShowInstance(os.Stdout, time.Now().UTC(), entry, config, flagShort, flagJSON) {
os.Exit(1)
}
default:
log.Fatal("show requires 1 argument")
} }
return errSuccess printShowInstance(os.Stdout, time.Now().UTC(), entry, config, showFlagShort, flagJSON)
}).
Flag(&flagShort, "short", command.BoolFlag(false), "Omit filesystem information").
Flag(&flagNoStore, "no-store", command.BoolFlag(false), "Do not attempt to match from active instances")
}
{ default:
var flagShort bool log.Fatal("show requires 1 argument")
c.NewCommand("ps", "List active instances", func(args []string) error { }
var sc hst.Paths return errSuccess
env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil)) }).Flag(&showFlagShort, "short", command.BoolFlag(false), "Omit filesystem information")
printPs(msg, os.Stdout, time.Now().UTC(), outcome.NewStore(&sc), flagShort, flagJSON)
return errSuccess
}).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id")
}
c.Command("version", "Display version information", func(args []string) error { fmt.Println(info.Version()); return errSuccess }) var psFlagShort bool
c.Command("license", "Show full license text", func(args []string) error { fmt.Println(license); return errSuccess }) c.NewCommand("ps", "List active instances", func(args []string) error {
c.Command("template", "Produce a config template", func(args []string) error { encodeJSON(log.Fatal, os.Stdout, false, hst.Template()); return errSuccess }) printPs(os.Stdout, time.Now().UTC(), state.NewMulti(std.Paths().RunDirPath.String()), psFlagShort, flagJSON)
c.Command("help", "Show this help message", func([]string) error { c.PrintHelp(); return errSuccess }) return errSuccess
}).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id")
c.Command("version", "Display version information", func(args []string) error {
fmt.Println(internal.Version())
return errSuccess
})
c.Command("license", "Show full license text", func(args []string) error {
fmt.Println(license)
return errSuccess
})
c.Command("template", "Produce a config template", func(args []string) error {
printJSON(os.Stdout, false, hst.Template())
return errSuccess
})
c.Command("help", "Show this help message", func([]string) error {
c.PrintHelp()
return errSuccess
})
return c 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" "testing"
"hakurei.app/command" "hakurei.app/command"
"hakurei.app/message"
) )
func TestHelp(t *testing.T) { func TestHelp(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
args []string args []string
@@ -23,8 +20,8 @@ func TestHelp(t *testing.T) {
Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS] Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS]
Commands: Commands:
app Load and start container from configuration file app Load app from configuration file
run Configure and start a permissive container run Configure and start a permissive default sandbox
show Show live or local app configuration show Show live or local app configuration
ps List active instances ps List active instances
version Display version information version Display version information
@@ -36,7 +33,7 @@ Commands:
}, },
{ {
"run", []string{"run", "-h"}, ` "run", []string{"run", "-h"}, `
Usage: hakurei run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--private-runtime] [--private-tmpdir] [--wayland] [-X] [--dbus] [--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: Flags:
-X Enable direct connection to X11 -X Enable direct connection to X11
@@ -58,14 +55,8 @@ Flags:
Reverse-DNS style Application identifier, leave empty to inherit instance identifier Reverse-DNS style Application identifier, leave empty to inherit instance identifier
-mpris -mpris
Allow owning MPRIS D-Bus path, has no effect if custom config is available Allow owning MPRIS D-Bus path, has no effect if custom config is available
-pipewire
Enable connection to PipeWire via SecurityContext
-private-runtime
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 -pulse
Enable PulseAudio compatibility daemon Enable direct connection to PulseAudio
-u string -u string
Passwd user name within sandbox (default "chronos") Passwd user name within sandbox (default "chronos")
-wayland -wayland
@@ -76,10 +67,8 @@ Flags:
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
out := new(bytes.Buffer) 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) { if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) {
t.Errorf("Parse: error = %v; want %v", t.Errorf("Parse: error = %v; want %v",
err, command.ErrHelp) 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 . //go:generate cp ../../LICENSE .
import ( import (
"context"
_ "embed" _ "embed"
"errors" "errors"
"log" "log"
"os" "os"
"os/signal"
"syscall"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/message" "hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/internal/sys"
) )
var ( var (
@@ -23,34 +22,32 @@ var (
license string license string
) )
// earlyHardeningErrs are errors collected while setting up early hardening feature. func init() { hlog.Prepare("hakurei") }
type earlyHardeningErrs struct{ yamaLSM, dumpable error }
var std sys.State = new(sys.Std)
func main() { func main() {
// early init path, skips root check and duplicate PR_SET_DUMPABLE // 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: ") if err := container.SetPtracer(0); err != nil {
log.SetFlags(0) hlog.Verbosef("cannot enable ptrace protection via Yama LSM: %v", err)
msg := message.New(log.Default()) // not fatal: this program runs as the privileged user
}
early := earlyHardeningErrs{ if err := container.SetDumpable(container.SUID_DUMP_DISABLE); err != nil {
yamaLSM: container.SetPtracer(0), log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
dumpable: container.SetDumpable(container.SUID_DUMP_DISABLE), // not fatal: this program runs as the privileged user
} }
if os.Geteuid() == 0 { if os.Geteuid() == 0 {
log.Fatal("this program must not run as root") log.Fatal("this program must not run as root")
} }
ctx, stop := signal.NotifyContext(context.Background(), buildCommand(os.Stderr).MustParse(os.Args[1:], func(err error) {
syscall.SIGINT, syscall.SIGTERM) hlog.Verbosef("command returned %v", err)
defer stop() // unreachable
buildCommand(ctx, msg, &early, os.Stderr).MustParse(os.Args[1:], func(err error) {
msg.Verbosef("command returned %v", err)
if errors.Is(err, errSuccess) { if errors.Is(err, errSuccess) {
msg.BeforeExit() hlog.BeforeExit()
os.Exit(0) os.Exit(0)
} }
// this catches faulty command handlers that fail to return before this point // this catches faulty command handlers that fail to return before this point

View File

@@ -1,7 +1,7 @@
package main package main
import ( import (
"encoding/hex" "encoding/json"
"errors" "errors"
"io" "io"
"log" "log"
@@ -11,93 +11,66 @@ import (
"syscall" "syscall"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/outcome" "hakurei.app/internal/app/state"
"hakurei.app/internal/store" "hakurei.app/internal/hlog"
"hakurei.app/message"
) )
// tryPath attempts to read [hst.Config] from multiple sources. func tryPath(name string) (config *hst.Config) {
// tryPath reads from [os.Stdin] if name has value "-". var r io.Reader
// Otherwise, name is passed to tryFd, and if that returns nil, name is passed to [os.Open].
func tryPath(msg message.Msg, name string) (config *hst.Config) {
var r io.ReadCloser
config = new(hst.Config) config = new(hst.Config)
if name != "-" { if name != "-" {
r = tryFd(msg, name) r = tryFd(name)
if r == nil { if r == nil {
msg.Verbose("load configuration from file") hlog.Verbose("load configuration from file")
if f, err := os.Open(name); err != nil { if f, err := os.Open(name); err != nil {
log.Fatal(err.Error()) log.Fatalf("cannot access configuration file %q: %s", name, err)
return
} else { } else {
// finalizer closes f
r = f r = f
} }
} else {
defer func() {
if err := r.(io.ReadCloser).Close(); err != nil {
log.Printf("cannot close config fd: %v", err)
}
}()
} }
} else { } else {
r = os.Stdin r = os.Stdin
} }
decodeJSON(log.Fatal, "load configuration", r, &config) if err := json.NewDecoder(r).Decode(&config); err != nil {
if err := r.Close(); err != nil { log.Fatalf("cannot load configuration: %v", err)
log.Fatal(err.Error())
} }
return return
} }
// tryFd returns a [io.ReadCloser] if name represents an integer corresponding to a valid file descriptor. func tryFd(name string) io.ReadCloser {
func tryFd(msg message.Msg, name string) io.ReadCloser {
if v, err := strconv.Atoi(name); err != nil { if v, err := strconv.Atoi(name); err != nil {
if !errors.Is(err, strconv.ErrSyntax) { 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 return nil
} else { } else {
if v < 3 { // reject standard streams hlog.Verbosef("trying config stream from %d", v)
return nil
}
msg.Verbosef("trying config stream from %d", v)
fd := uintptr(v) fd := uintptr(v)
if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 { 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 return nil
} }
log.Fatalf("cannot get fd %d: %v", fd, errno) 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)) 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. func tryShort(name string) (config *hst.Config, entry *state.State) {
const shortLengthMin = 1 << 3 likePrefix := false
if len(name) <= 32 {
// shortIdentifier returns an eight character short representation of [hst.ID] from its random bytes. likePrefix = true
func shortIdentifier(id *hst.ID) string {
return shortIdentifierString(id.String())
}
// shortIdentifierString implements shortIdentifier on an arbitrary string.
func shortIdentifierString(s string) string {
return s[len(hst.ID{}) : len(hst.ID{})+shortLengthMin]
}
// tryIdentifier attempts to match [hst.State] from a [hex] representation of [hst.ID] or a prefix of its lower half.
func tryIdentifier(msg message.Msg, name string, s *store.Store) *hst.State {
const (
likeShort = 1 << iota
likeFull
)
var likely uintptr
if len(name) >= shortLengthMin && len(name) <= len(hst.ID{}) { // half the hex representation
// cannot safely decode here due to unknown alignment
for _, c := range name { for _, c := range name {
if c >= '0' && c <= '9' { if c >= '0' && c <= '9' {
continue continue
@@ -105,68 +78,33 @@ func tryIdentifier(msg message.Msg, name string, s *store.Store) *hst.State {
if c >= 'a' && c <= 'f' { if c >= 'a' && c <= 'f' {
continue continue
} }
return nil likePrefix = false
break
} }
likely |= likeShort
} else if len(name) == hex.EncodedLen(len(hst.ID{})) {
likely |= likeFull
} }
if likely == 0 { // try to match from state store
return nil if likePrefix && len(name) >= 8 {
} hlog.Verbose("argument looks like prefix")
entries, copyError := s.All() s := state.NewMulti(std.Paths().RunDirPath.String())
defer func() { if entries, err := state.Join(s); err != nil {
if err := copyError(); err != nil { log.Printf("cannot join store: %v", err)
msg.GetLogger().Println(getMessage("cannot iterate over store:", err)) // drop to fetch from file
} } else {
}() for id := range entries {
v := id.String()
switch { if strings.HasPrefix(v, name) {
case likely&likeShort != 0: // match, use config from this state entry
msg.Verbose("argument looks like short identifier") entry = entries[id]
for eh := range entries { config = entry.Config
if eh.DecodeErr != nil { break
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
hlog.Verbosef("instance %s skipped", v)
} }
} }
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 package main
import ( import (
"bytes" "encoding/json"
"fmt" "fmt"
"io" "io"
"log" "log"
"os"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
@@ -12,43 +13,46 @@ import (
"time" "time"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/outcome" "hakurei.app/internal/app/state"
"hakurei.app/internal/store" "hakurei.app/internal/hlog"
"hakurei.app/message" "hakurei.app/system/dbus"
) )
// printShowSystem populates and writes a representation of [hst.Info] to output.
func printShowSystem(output io.Writer, short, flagJSON bool) { func printShowSystem(output io.Writer, short, flagJSON bool) {
t := newPrinter(output) t := newPrinter(output)
defer t.MustFlush() defer t.MustFlush()
hi := outcome.Info()
info := &hst.Info{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 { if flagJSON {
encodeJSON(log.Fatal, output, short, hi) printJSON(output, short, info)
return return
} }
t.Printf("Version:\t%s (libwayland %s)\n", hi.Version, hi.WaylandVersion) t.Printf("User:\t%d\n", info.User)
t.Printf("User:\t%d\n", hi.User) t.Printf("TempDir:\t%s\n", info.TempDir)
t.Printf("TempDir:\t%s\n", hi.TempDir) t.Printf("SharePath:\t%s\n", info.SharePath)
t.Printf("SharePath:\t%s\n", hi.SharePath) t.Printf("RuntimePath:\t%s\n", info.RuntimePath)
t.Printf("RuntimePath:\t%s\n", hi.RuntimePath) t.Printf("RunDirPath:\t%s\n", info.RunDirPath)
t.Printf("RunDirPath:\t%s\n", hi.RunDirPath)
} }
// printShowInstance writes a representation of [hst.State] or [hst.Config] to output.
func printShowInstance( func printShowInstance(
output io.Writer, now time.Time, output io.Writer, now time.Time,
instance *hst.State, config *hst.Config, instance *state.State, config *hst.Config,
short, flagJSON bool, short, flagJSON bool) {
) (valid bool) {
valid = true
if flagJSON { if flagJSON {
if instance != nil { if instance != nil {
encodeJSON(log.Fatal, output, short, instance) printJSON(output, short, instance)
} else { } else {
encodeJSON(log.Fatal, output, short, config) printJSON(output, short, config)
} }
return return
} }
@@ -56,21 +60,13 @@ func printShowInstance(
t := newPrinter(output) t := newPrinter(output)
defer t.MustFlush() defer t.MustFlush()
if err := config.Validate(); err != nil { if config.Container == nil {
valid = false mustPrint(output, "Warning: this configuration uses permissive defaults!\n\n")
if m, ok := message.GetMessage(err); ok {
mustPrint(output, "Error: "+m+"!\n\n")
}
}
if config == nil {
// nothing to print
return
} }
if instance != nil { if instance != nil {
t.Printf("State\n") 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(" Uptime:\t%s\n", now.Sub(instance.Time).Round(time.Second).String())
t.Printf("\n") t.Printf("\n")
} }
@@ -85,34 +81,40 @@ func printShowInstance(
if len(config.Groups) > 0 { if len(config.Groups) > 0 {
t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", ")) 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 { if config.Container != nil {
flags := config.Container.Flags.String() params := config.Container
if params.Hostname != "" {
// this is included in the upper hst.Config struct but is relevant here t.Printf(" Hostname:\t%s\n", params.Hostname)
const flagDirectWayland = "directwl" }
if config.DirectWayland { flags := make([]string, 0, 7)
// hardcoded value when every flag is unset writeFlag := func(name string, value bool) {
if flags == "none" { if value {
flags = flagDirectWayland flags = append(flags, name)
} else {
flags += ", " + flagDirectWayland
} }
} }
t.Printf(" Flags:\t%s\n", flags) 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, " "))
if config.Container.Home != nil { if config.Path != nil {
t.Printf(" Home:\t%s\n", config.Container.Home) t.Printf(" Path:\t%s\n", config.Path)
}
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") t.Printf("\n")
if !short { if !short {
@@ -120,7 +122,6 @@ func printShowInstance(
t.Printf("Filesystem\n") t.Printf("Filesystem\n")
for _, f := range config.Container.Filesystem { for _, f := range config.Container.Filesystem {
if !f.Valid() { if !f.Valid() {
valid = false
t.Println(" <invalid>") t.Println(" <invalid>")
continue continue
} }
@@ -130,14 +131,17 @@ func printShowInstance(
} }
if len(config.ExtraPerms) > 0 { if len(config.ExtraPerms) > 0 {
t.Printf("Extra ACL\n") t.Printf("Extra ACL\n")
for i := range config.ExtraPerms { for _, p := range config.ExtraPerms {
t.Printf(" %s\n", config.ExtraPerms[i].String()) if p == nil {
continue
}
t.Printf(" %s\n", p.String())
} }
t.Printf("\n") t.Printf("\n")
} }
} }
printDBus := func(c *hst.BusConfig) { printDBus := func(c *dbus.Config) {
t.Printf(" Filter:\t%v\n", c.Filter) t.Printf(" Filter:\t%v\n", c.Filter)
if len(c.See) > 0 { if len(c.See) > 0 {
t.Printf(" See:\t%q\n", c.See) t.Printf(" See:\t%q\n", c.See)
@@ -165,57 +169,59 @@ func printShowInstance(
printDBus(config.SystemBus) printDBus(config.SystemBus)
t.Printf("\n") t.Printf("\n")
} }
return
} }
// printPs writes a representation of active instances to output. func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON bool) {
func printPs(msg message.Msg, output io.Writer, now time.Time, s *store.Store, short, flagJSON bool) { var entries state.Entries
f := func(a func(eh *store.EntryHandle)) { if e, err := state.Join(s); err != nil {
entries, copyError := s.All() log.Fatalf("cannot join store: %v", err)
for eh := range entries { } else {
a(eh) entries = e
} }
if err := copyError(); err != nil { if err := s.Close(); err != nil {
msg.GetLogger().Println(getMessage("cannot iterate over store:", err)) log.Printf("cannot close store: %v", err)
}
} }
if short { // short output requires identifier only if !short && flagJSON {
var identifiers []*hst.ID es := make(map[string]*state.State, len(entries))
f(func(eh *store.EntryHandle) { for id, instance := range entries {
if _, err := eh.Load(nil); err != nil { // passes through decode error es[id.String()] = instance
msg.GetLogger().Println(getMessage("cannot validate state entry header:", err))
return
}
identifiers = append(identifiers, &eh.ID)
})
slices.SortFunc(identifiers, func(a, b *hst.ID) int { return bytes.Compare(a[:], b[:]) })
if flagJSON {
encodeJSON(log.Fatal, output, short, identifiers)
} else {
for _, id := range identifiers {
mustPrintln(output, shortIdentifier(id))
}
} }
printJSON(output, short, es)
return return
} }
// long output requires full instance state // sort state entries by id string to ensure consistency between runs
var instances []*hst.State exp := make([]*expandedStateEntry, 0, len(entries))
f(func(eh *store.EntryHandle) { for id, instance := range entries {
var state hst.State // gracefully skip nil states
if _, err := eh.Load(&state); err != nil { // passes through decode error if instance == nil {
msg.GetLogger().Println(getMessage("cannot load state entry:", err)) log.Printf("got invalid state entry %s", id.String())
return continue
} }
instances = append(instances, &state)
})
slices.SortFunc(instances, func(a, b *hst.State) int { return bytes.Compare(a.ID[:], b.ID[:]) })
if flagJSON { // gracefully skip inconsistent states
encodeJSON(log.Fatal, output, short, instances) if id != instance.ID {
log.Printf("possible store corruption: entry %s has id %s",
id.String(), instance.ID.String())
continue
}
exp = append(exp, &expandedStateEntry{s: id.String(), State: instance})
}
slices.SortFunc(exp, func(a, b *expandedStateEntry) int { return a.Time.Compare(b.Time) })
if short {
if flagJSON {
v := make([]string, len(exp))
for i, e := range exp {
v[i] = e.s
}
printJSON(output, short, v)
} else {
for _, e := range exp {
mustPrintln(output, e.s[:8])
}
}
return return
} }
@@ -223,48 +229,61 @@ func printPs(msg message.Msg, output io.Writer, now time.Time, s *store.Store, s
defer t.MustFlush() defer t.MustFlush()
t.Println("\tInstance\tPID\tApplication\tUptime") 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)" as := "(No configuration information)"
if instance.Config != nil { if e.Config != nil {
as = strconv.Itoa(instance.Config.Identity) as = strconv.Itoa(e.Config.Identity)
id := instance.Config.ID id := e.Config.ID
if id == "" { if id == "" {
id = "app.hakurei." + shortIdentifier(&instance.ID) id = "app.hakurei." + e.s[:8]
} }
as += " (" + id + ")" as += " (" + id + ")"
} }
t.Printf("\t%s\t%d\t%s\t%s\n", 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)} } 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 } type tp struct{ *tabwriter.Writer }
// Printf calls [fmt.Fprintf] on the underlying [tabwriter.Writer].
func (p *tp) Printf(format string, a ...any) { func (p *tp) Printf(format string, a ...any) {
if _, err := fmt.Fprintf(p, format, a...); err != nil { if _, err := fmt.Fprintf(p, format, a...); err != nil {
log.Fatalf("cannot write to tabwriter: %v", err) log.Fatalf("cannot write to tabwriter: %v", err)
} }
} }
// Println calls [fmt.Fprintln] on the underlying [tabwriter.Writer].
func (p *tp) Println(a ...any) { func (p *tp) Println(a ...any) {
if _, err := fmt.Fprintln(p, a...); err != nil { if _, err := fmt.Fprintln(p, a...); err != nil {
log.Fatalf("cannot write to tabwriter: %v", err) 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() { func (p *tp) MustFlush() {
if err := p.Writer.Flush(); err != nil { if err := p.Writer.Flush(); err != nil {
log.Fatalf("cannot flush tabwriter: %v", err) log.Fatalf("cannot flush tabwriter: %v", err)
} }
} }
func mustPrint(output io.Writer, a ...any) { func mustPrint(output io.Writer, a ...any) {
if _, err := fmt.Fprint(output, a...); err != nil { if _, err := fmt.Fprint(output, a...); err != nil {
log.Fatalf("cannot print: %v", err) log.Fatalf("cannot print: %v", err)
@@ -275,11 +294,3 @@ func mustPrintln(output io.Writer, a ...any) {
log.Fatalf("cannot print: %v", err) 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" "log"
"os" "os"
"hakurei.app/container/check" "hakurei.app/container"
"hakurei.app/container/fhs" "hakurei.app/container/seccomp"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/system/dbus"
) )
type appInfo struct { type appInfo struct {
@@ -37,9 +38,9 @@ type appInfo struct {
// passed through to [hst.Config] // passed through to [hst.Config]
DirectWayland bool `json:"direct_wayland,omitempty"` DirectWayland bool `json:"direct_wayland,omitempty"`
// passed through to [hst.Config] // passed through to [hst.Config]
SystemBus *hst.BusConfig `json:"system_bus,omitempty"` SystemBus *dbus.Config `json:"system_bus,omitempty"`
// passed through to [hst.Config] // passed through to [hst.Config]
SessionBus *hst.BusConfig `json:"session_bus,omitempty"` SessionBus *dbus.Config `json:"session_bus,omitempty"`
// passed through to [hst.Config] // passed through to [hst.Config]
Enablements *hst.Enablements `json:"enablements,omitempty"` Enablements *hst.Enablements `json:"enablements,omitempty"`
@@ -55,82 +56,69 @@ type appInfo struct {
// store path to nixGL source // store path to nixGL source
NixGL string `json:"nix_gl,omitempty"` NixGL string `json:"nix_gl,omitempty"`
// store path to activate-and-exec script // store path to activate-and-exec script
Launcher *check.Absolute `json:"launcher"` Launcher *container.Absolute `json:"launcher"`
// store path to /run/current-system // 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 // store path to home-manager activation package
ActivationPackage string `json:"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{ config := &hst.Config{
ID: app.ID, ID: app.ID,
Path: pathname,
Args: argv,
Enablements: app.Enablements, Enablements: app.Enablements,
SystemBus: app.SystemBus, SystemBus: app.SystemBus,
SessionBus: app.SessionBus, SessionBus: app.SessionBus,
DirectWayland: app.DirectWayland, DirectWayland: app.DirectWayland,
Username: "hakurei",
Shell: pathShell,
Home: pathDataData.Append(app.ID),
Identity: app.Identity, Identity: app.Identity,
Groups: app.Groups, Groups: app.Groups,
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name), 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{ 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.FSBind{Source: pathSet.nixPath.Append("store"), Target: pathNixStore}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}}, {FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}}, {FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: fhs.AbsUsrBin, Linkname: pathSwBin.String()}}, {FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.metaPath, Target: hst.AbsPrivateTmp.Append("app")}}, {FilesystemConfig: &hst.FSBind{Source: pathSet.metaPath, Target: hst.AbsTmp.Append("app")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsEtc.Append("resolv.conf"), Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSEtc.Append("resolv.conf"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("block"), Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("block"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("bus"), Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("bus"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("class"), Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("class"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("dev"), Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("dev"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("devices"), 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}}, {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}, {Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, 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 { 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 return config
} }

View File

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

View File

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

View File

@@ -7,37 +7,36 @@ import (
"strconv" "strconv"
"sync/atomic" "sync/atomic"
"hakurei.app/container/check" "hakurei.app/container"
"hakurei.app/container/fhs"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/message" "hakurei.app/internal/hlog"
) )
const bash = "bash" const bash = "bash"
var ( var (
dataHome *check.Absolute dataHome *container.Absolute
) )
func init() { func init() {
// dataHome // 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 dataHome = a
} else { } else {
dataHome = fhs.AbsVarLib.Append("hakurei/" + strconv.Itoa(os.Getuid())) dataHome = container.AbsFHSVarLib.Append("hakurei/" + strconv.Itoa(os.Getuid()))
} }
} }
var ( var (
pathBin = fhs.AbsRoot.Append("bin") pathBin = container.AbsFHSRoot.Append("bin")
pathNix = check.MustAbs("/nix/") pathNix = container.MustAbs("/nix/")
pathNixStore = pathNix.Append("store/") pathNixStore = pathNix.Append("store/")
pathCurrentSystem = fhs.AbsRun.Append("current-system") pathCurrentSystem = container.AbsFHSRun.Append("current-system")
pathSwBin = pathCurrentSystem.Append("sw/bin/") pathSwBin = pathCurrentSystem.Append("sw/bin/")
pathShell = pathSwBin.Append(bash) pathShell = pathSwBin.Append(bash)
pathData = check.MustAbs("/data") pathData = container.MustAbs("/data")
pathDataData = pathData.Append("data") pathDataData = pathData.Append("data")
) )
@@ -52,8 +51,8 @@ func lookPath(file string) string {
var beforeRunFail = new(atomic.Pointer[func()]) var beforeRunFail = new(atomic.Pointer[func()])
func mustRun(msg message.Msg, name string, arg ...string) { func mustRun(name string, arg ...string) {
msg.Verbosef("spawning process: %q %q", name, arg) hlog.Verbosef("spawning process: %q %q", name, arg)
cmd := exec.Command(name, arg...) cmd := exec.Command(name, arg...)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
@@ -66,15 +65,15 @@ func mustRun(msg message.Msg, name string, arg ...string) {
type appPathSet struct { type appPathSet struct {
// ${dataHome}/${id} // ${dataHome}/${id}
baseDir *check.Absolute baseDir *container.Absolute
// ${baseDir}/app // ${baseDir}/app
metaPath *check.Absolute metaPath *container.Absolute
// ${baseDir}/files // ${baseDir}/files
homeDir *check.Absolute homeDir *container.Absolute
// ${baseDir}/cache // ${baseDir}/cache
cacheDir *check.Absolute cacheDir *container.Absolute
// ${baseDir}/cache/nix // ${baseDir}/cache/nix
nixPath *check.Absolute nixPath *container.Absolute
} }
func pathSetByApp(id string) *appPathSet { func pathSetByApp(id string) *appPathSet {
@@ -90,28 +89,28 @@ func pathSetByApp(id string) *appPathSet {
func appendGPUFilesystem(config *hst.Config) { func appendGPUFilesystem(config *hst.Config) {
config.Container.Filesystem = append(config.Container.Filesystem, []hst.FilesystemConfigJSON{ config.Container.Filesystem = append(config.Container.Filesystem, []hst.FilesystemConfigJSON{
// flatpak commit 763a686d874dd668f0236f911de00b80766ffe79 // 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 // mali
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("mali"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("mali"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("mali0"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.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("umplock"), Device: true, Optional: true}},
// nvidia // nvidia
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidiactl"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.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("nvidia-modeset"), Device: true, Optional: true}},
// nvidia OpenCL/CUDA // nvidia OpenCL/CUDA
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia-uvm"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.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-tools"), Device: true, Optional: true}},
// flatpak commit d2dff2875bb3b7e2cd92d8204088d743fd07f3ff // 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: container.AbsFHSDev.Append("nvidia0"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.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: container.AbsFHSDev.Append("nvidia2"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.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: container.AbsFHSDev.Append("nvidia4"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.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: container.AbsFHSDev.Append("nvidia6"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.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: container.AbsFHSDev.Append("nvidia8"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.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: container.AbsFHSDev.Append("nvidia10"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.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: container.AbsFHSDev.Append("nvidia12"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.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: container.AbsFHSDev.Append("nvidia14"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.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: container.AbsFHSDev.Append("nvidia16"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.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("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" "os/exec"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/info" "hakurei.app/internal"
"hakurei.app/message" "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 ( var (
cmd *exec.Cmd cmd *exec.Cmd
st io.WriteCloser st io.WriteCloser
@@ -26,10 +26,10 @@ func mustRunApp(ctx context.Context, msg message.Msg, config *hst.Config, before
beforeFail() beforeFail()
log.Fatalf("cannot pipe: %v", err) log.Fatalf("cannot pipe: %v", err)
} else { } else {
if msg.IsVerbose() { if hlog.Load() {
cmd = exec.CommandContext(ctx, hakureiPathVal, "-v", "app", "3") cmd = exec.CommandContext(ctx, hakureiPath, "-v", "app", "3")
} else { } 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.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.ExtraFiles = []*os.File{r} 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 var exitError *exec.ExitError
if errors.As(err, &exitError) { if errors.As(err, &exitError) {
beforeFail() beforeFail()
msg.BeforeExit() internal.Exit(exitError.ExitCode())
os.Exit(exitError.ExitCode())
} else { } else {
beforeFail() beforeFail()
log.Fatalf("cannot wait: %v", err) log.Fatalf("cannot wait: %v", err)

View File

@@ -1,5 +1,5 @@
{ {
testers, nixosTest,
callPackage, callPackage,
system, system,
@@ -8,7 +8,7 @@
let let
buildPackage = self.buildPackage.${system}; buildPackage = self.buildPackage.${system};
in in
testers.nixosTest { nixosTest {
name = "hpkg"; name = "hpkg";
nodes.machine = { nodes.machine = {
environment.etc = { 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")) instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei --json ps"))
if len(instances) != 1: if len(instances) != 1:
raise Exception(f"unexpected state length {len(instances)}") 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]): config = instance['config']
raise Exception(f"unexpected args {instance['container']['args']}")
if instance['enablements'] != 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 enablements {instance['enablements']}") raise Exception(f"unexpected args {instance['config']['args']}")
if config['enablements'] != enablements:
raise Exception(f"unexpected enablements {instance['config']['enablements']}")
start_all() start_all()
@@ -90,21 +92,17 @@ wait_for_window("hakurei@machine-foot")
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n") machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
machine.wait_for_file("/tmp/hakurei.0/tmpdir/2/success-client") machine.wait_for_file("/tmp/hakurei.0/tmpdir/2/success-client")
collect_state_ui("app_wayland") collect_state_ui("app_wayland")
check_state("foot", {"wayland": True, "dbus": True, "pipewire": True}) check_state("foot", {"wayland": True, "dbus": True, "pulse": True})
# Verify acl on XDG_RUNTIME_DIR: # 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.send_chars("exit\n")
machine.wait_until_fails("pgrep foot") machine.wait_until_fails("pgrep foot")
# Verify acl cleanup on XDG_RUNTIME_DIR: # Verify acl cleanup on XDG_RUNTIME_DIR:
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /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: # Exit Sway and verify process exit status 0:
swaymsg("exit", succeed=False) swaymsg("exit", succeed=False)
machine.wait_for_file("/tmp/sway-exit-ok") machine.wait_for_file("/tmp/sway-exit-ok")
# Print hakurei share and rundir contents: # Print hakurei runDir contents:
print(machine.succeed("find /tmp/hakurei.0 " print(machine.succeed("find /run/user/1000/hakurei"))
+ "-path '/tmp/hakurei.0/runtime/*/*' -prune -o "
+ "-path '/tmp/hakurei.0/tmpdir/*/*' -prune -o "
+ "-print"))
print(machine.fail("ls /run/user/1000/hakurei"))

View File

@@ -2,33 +2,39 @@ package main
import ( import (
"context" "context"
"os"
"strings" "strings"
"hakurei.app/container/check" "hakurei.app/container"
"hakurei.app/container/fhs" "hakurei.app/container/seccomp"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/message" "hakurei.app/internal"
) )
func withNixDaemon( func withNixDaemon(
ctx context.Context, ctx context.Context,
msg message.Msg,
action string, command []string, net bool, updateConfig func(config *hst.Config) *hst.Config, action string, command []string, net bool, updateConfig func(config *hst.Config) *hst.Config,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(), app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
) { ) {
flags := hst.FMultiarch | hst.FUserns // nix sandbox requires userns mustRunAppDropShell(ctx, updateConfig(&hst.Config{
if net {
flags |= hst.FHostNet
}
if dropShell {
flags |= hst.FTty
}
mustRunAppDropShell(ctx, msg, updateConfig(&hst.Config{
ID: app.ID, ID: app.ID,
ExtraPerms: []hst.ExtraPermConfig{ Path: pathShell,
Args: []string{bash, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
// start nix-daemon
"nix-daemon --store / & " +
// wait for socket to appear
"(while [ ! -S /nix/var/nix/daemon-socket/socket ]; do sleep 0.01; done) && " +
// create directory so nix stops complaining
"mkdir -p /nix/var/nix/profiles/per-user/root/channels && " +
strings.Join(command, " && ") +
// terminate nix-daemon
" && pkill nix-daemon",
},
Username: "hakurei",
Shell: pathShell,
Home: pathDataData.Append(app.ID),
ExtraPerms: []*hst.ExtraPermConfig{
{Path: dataHome, Execute: true}, {Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true}, {Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
}, },
@@ -36,54 +42,37 @@ func withNixDaemon(
Identity: app.Identity, Identity: app.Identity,
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action, Hostname: formatHostname(app.Name) + "-" + action,
Userns: true, // nix sandbox requires userns
HostNet: net,
SeccompFlags: seccomp.AllowMultiarch,
Tty: dropShell,
Filesystem: []hst.FilesystemConfigJSON{ 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, Target: pathNix, Write: true}}, {FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath, Target: pathNix, Write: true}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}}, {FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}}, {FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: fhs.AbsUsrBin, 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}}, {FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID), Source: pathSet.homeDir, Write: true, Ensure: true}},
}, },
Username: "hakurei",
Shell: pathShell,
Home: pathDataData.Append(app.ID),
Path: pathShell,
Args: []string{bash, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
// start nix-daemon
"nix-daemon --store / & " +
// wait for socket to appear
"(while [ ! -S /nix/var/nix/daemon-socket/socket ]; do sleep 0.01; done) && " +
// create directory so nix stops complaining
"mkdir -p /nix/var/nix/profiles/per-user/root/channels && " +
strings.Join(command, " && ") +
// terminate nix-daemon
" && pkill nix-daemon",
},
Flags: flags,
}, },
}), dropShell, beforeFail) }), dropShell, beforeFail)
} }
func withCacheDir( func withCacheDir(
ctx context.Context, ctx context.Context,
msg message.Msg, action string, command []string, workDir *container.Absolute,
action string, command []string, workDir *check.Absolute, app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(), mustRunAppDropShell(ctx, &hst.Config{
) {
flags := hst.FMultiarch
if dropShell {
flags |= hst.FTty
}
mustRunAppDropShell(ctx, msg, &hst.Config{
ID: app.ID, 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}, {Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true}, {Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
{Path: workDir, Execute: true}, {Path: workDir, Execute: true},
@@ -92,39 +81,28 @@ func withCacheDir(
Identity: app.Identity, Identity: app.Identity,
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action, Hostname: formatHostname(app.Name) + "-" + action,
SeccompFlags: seccomp.AllowMultiarch,
Tty: dropShell,
Filesystem: []hst.FilesystemConfigJSON{ 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.FSBind{Source: workDir.Append("nix"), Target: pathNix}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}}, {FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}}, {FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: fhs.AbsUsrBin, Linkname: pathSwBin.String()}}, {FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Source: workDir, Target: hst.AbsPrivateTmp.Append("bundle")}}, {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}}, {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) }, 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 dropShell {
if config.Container != nil { config.Args = []string{bash, "-l"}
config.Container.Args = []string{bash, "-l"} mustRunApp(ctx, config, beforeFail)
}
mustRunApp(ctx, msg, config, beforeFail)
beforeFail() beforeFail()
msg.BeforeExit() internal.Exit(0)
os.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 package main
// minimise imports to avoid inadvertently calling init or global variable functions
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"log" "log"
"os" "os"
"path" "path"
"runtime"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
@@ -16,23 +13,15 @@ import (
) )
const ( const (
// envIdentity is the name of the environment variable holding a hsuConfFile = "/etc/hsurc"
// single byte representing the shim setup pipe file descriptor. envShim = "HAKUREI_SHIM"
envShim = "HAKUREI_SHIM" envAID = "HAKUREI_APP_ID"
// envGroups holds a ' ' separated list of string representations of envGroups = "HAKUREI_GROUPS"
// supplementary group gid. Membership requirements are enforced.
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() { func main() {
const PR_SET_NO_NEW_PRIVS = 0x26
runtime.LockOSThread()
log.SetFlags(0) log.SetFlags(0)
log.SetPrefix("hsu: ") log.SetPrefix("hsu: ")
log.SetOutput(os.Stderr) log.SetOutput(os.Stderr)
@@ -40,34 +29,31 @@ func main() {
if os.Geteuid() != 0 { if os.Geteuid() != 0 {
log.Fatal("this program must be owned by uid 0 and have the setuid bit set") 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() puid := os.Getuid()
if puid == 0 { if puid == 0 {
log.Fatal("this program must not be started by root") 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 var toolPath string
pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe") pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
if p, err := os.Readlink(pexe); err != nil { if p, err := os.Readlink(pexe); err != nil {
log.Fatalf("cannot read parent executable path: %v", err) log.Fatalf("cannot read parent executable path: %v", err)
} else if strings.HasSuffix(p, " (deleted)") { } else if strings.HasSuffix(p, " (deleted)") {
log.Fatal("hakurei executable has been 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") log.Fatal("this program must be started by hakurei")
} else { } else {
toolPath = p toolPath = p
} }
// uid = 1000000 +
// fid * 10000 +
// aid
uid := 1000000
// refuse to run if hsurc is not protected correctly // 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) log.Fatal(err)
} else if s.Mode().Perm() != 0400 { } else if s.Mode().Perm() != 0400 {
log.Fatal("bad hsurc perm") log.Fatal("bad hsurc perm")
@@ -76,13 +62,29 @@ func main() {
} }
// authenticate before accepting user input // 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 // pass through setup fd to shim
var shimSetupFd string var shimSetupFd string
if s, ok := os.LookupEnv(envShim); !ok { if s, ok := os.LookupEnv(envShim); !ok {
// hakurei requests hsurc user id // hakurei requests target uid
fmt.Print(userid) // print resolved uid and exit
fmt.Print(uid)
os.Exit(0) os.Exit(0)
} else if len(s) != 1 || s[0] > '9' || s[0] < '3' { } else if len(s) != 1 || s[0] > '9' || s[0] < '3' {
log.Fatal("HAKUREI_SHIM holds an invalid value") log.Fatal("HAKUREI_SHIM holds an invalid value")
@@ -90,24 +92,6 @@ func main() {
shimSetupFd = s 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 // supplementary groups
var suppGroups, suppCurrent []int var suppGroups, suppCurrent []int
@@ -135,6 +119,11 @@ func main() {
suppGroups = []int{uid} 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 // careful! users in the allowlist is effectively allowed to drop groups via hsu
if err := syscall.Setresgid(uid, uid, uid); err != nil { if err := syscall.Setresgid(uid, uid, uid); err != nil {

View File

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

View File

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

View File

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

View File

@@ -6,19 +6,15 @@ import (
) )
func TestParseUnreachable(t *testing.T) { func TestParseUnreachable(t *testing.T) {
t.Parallel()
// top level bypasses name matching and recursive calls to Parse // top level bypasses name matching and recursive calls to Parse
// returns when encountering zero-length args // returns when encountering zero-length args
t.Run("zero-length args", func(t *testing.T) { t.Run("zero-length args", func(t *testing.T) {
t.Parallel()
defer checkRecover(t, "Parse", "attempted to parse with zero length args") defer checkRecover(t, "Parse", "attempted to parse with zero length args")
_ = newNode(panicWriter{}, nil, " ", " ").Parse(nil) _ = newNode(panicWriter{}, nil, " ", " ").Parse(nil)
}) })
// top level must not have siblings // top level must not have siblings
t.Run("toplevel siblings", func(t *testing.T) { t.Run("toplevel siblings", func(t *testing.T) {
t.Parallel()
defer checkRecover(t, "Parse", "invalid toplevel state") defer checkRecover(t, "Parse", "invalid toplevel state")
n := newNode(panicWriter{}, nil, " ", "") n := newNode(panicWriter{}, nil, " ", "")
n.append(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 // a node with descendents must not have a direct handler
t.Run("sub handle conflict", func(t *testing.T) { t.Run("sub handle conflict", func(t *testing.T) {
t.Parallel()
defer checkRecover(t, "Parse", "invalid subcommand tree state") defer checkRecover(t, "Parse", "invalid subcommand tree state")
n := newNode(panicWriter{}, nil, " ", " ") n := newNode(panicWriter{}, nil, " ", " ")
n.adopt(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 // this would only happen if a node was matched twice
t.Run("parsed flag set", func(t *testing.T) { t.Run("parsed flag set", func(t *testing.T) {
t.Parallel()
defer checkRecover(t, "Parse", "invalid set state") defer checkRecover(t, "Parse", "invalid set state")
n := newNode(panicWriter{}, nil, " ", "") n := newNode(panicWriter{}, nil, " ", "")
set := flag.NewFlagSet("parsed", flag.ContinueOnError) 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 ( import (
"bytes" "bytes"
@@ -9,19 +9,9 @@ import (
"strings" "strings"
"syscall" "syscall"
"testing" "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) { func TestAbsoluteError(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
@@ -31,8 +21,8 @@ func TestAbsoluteError(t *testing.T) {
}{ }{
{"EINVAL", new(AbsoluteError), syscall.EINVAL, true}, {"EINVAL", new(AbsoluteError), syscall.EINVAL, true},
{"not EINVAL", new(AbsoluteError), syscall.EBADE, false}, {"not EINVAL", new(AbsoluteError), syscall.EBADE, false},
{"ne val", new(AbsoluteError), AbsoluteError("etc"), false}, {"ne val", new(AbsoluteError), &AbsoluteError{"etc"}, false},
{"equals", AbsoluteError("etc"), AbsoluteError("etc"), true}, {"equals", &AbsoluteError{"etc"}, &AbsoluteError{"etc"}, true},
} }
for _, tc := range testCases { for _, tc := range testCases {
@@ -42,18 +32,14 @@ func TestAbsoluteError(t *testing.T) {
} }
t.Run("string", func(t *testing.T) { t.Run("string", func(t *testing.T) {
t.Parallel()
want := `path "etc" is not absolute` 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) t.Errorf("Error: %q, want %q", got, want)
} }
}) })
} }
func TestNewAbs(t *testing.T) { func TestNewAbs(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
@@ -62,14 +48,12 @@ func TestNewAbs(t *testing.T) {
wantErr error wantErr error
}{ }{
{"good", "/etc", MustAbs("/etc"), nil}, {"good", "/etc", MustAbs("/etc"), nil},
{"not absolute", "etc", nil, AbsoluteError("etc")}, {"not absolute", "etc", nil, &AbsoluteError{"etc"}},
{"zero", "", nil, AbsoluteError("")}, {"zero", "", nil, &AbsoluteError{""}},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, err := NewAbs(tc.pathname) got, err := NewAbs(tc.pathname)
if !reflect.DeepEqual(got, tc.want) { if !reflect.DeepEqual(got, tc.want) {
t.Errorf("NewAbs: %#v, want %#v", 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.Run("must", func(t *testing.T) {
t.Parallel()
defer func() { 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) t.Errorf("MustAbs: panic = %v; want %v", r, wantPanic)
} }
}() }()
@@ -97,17 +79,13 @@ func TestNewAbs(t *testing.T) {
func TestAbsoluteString(t *testing.T) { func TestAbsoluteString(t *testing.T) {
t.Run("passthrough", func(t *testing.T) { t.Run("passthrough", func(t *testing.T) {
t.Parallel()
pathname := "/etc" 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.Errorf("String: %q, want %q", got, pathname)
} }
}) })
t.Run("zero", func(t *testing.T) { t.Run("zero", func(t *testing.T) {
t.Parallel()
defer func() { defer func() {
wantPanic := "attempted use of zero Absolute" wantPanic := "attempted use of zero Absolute"
@@ -121,8 +99,6 @@ func TestAbsoluteString(t *testing.T) {
} }
func TestAbsoluteIs(t *testing.T) { func TestAbsoluteIs(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
a, v *Absolute a, v *Absolute
@@ -138,8 +114,6 @@ func TestAbsoluteIs(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.a.Is(tc.v); got != tc.want { if got := tc.a.Is(tc.v); got != tc.want {
t.Errorf("Is: %v, want %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 { type sCheck struct {
Pathname *Absolute `json:"val"` Pathname *Absolute `json:"val"`
Magic uint64 `json:"magic"` Magic int `json:"magic"`
} }
func TestCodecAbsolute(t *testing.T) { func TestCodecAbsolute(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
a *Absolute a *Absolute
@@ -171,36 +143,31 @@ func TestCodecAbsolute(t *testing.T) {
{"good", MustAbs("/etc"), {"good", MustAbs("/etc"),
nil, nil,
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\b\xff\x80\x00\x04/etc", "\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}`}, `"/etc"`, `{"val":"/etc","magic":3236757504}`},
{"not absolute", nil, {"not absolute", nil,
AbsoluteError("etc"), &AbsoluteError{"etc"},
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\a\xff\x80\x00\x03etc", "\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}`}, `"etc"`, `{"val":"etc","magic":3236757504}`},
{"zero", nil, {"zero", nil,
new(AbsoluteError), new(AbsoluteError),
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x04\xff\x80\x00\x00", "\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}`}, `""`, `{"val":"","magic":3236757504}`},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("gob", func(t *testing.T) { t.Run("gob", func(t *testing.T) {
if tc.gob == "\x00" && tc.sGob == "\x00" { if tc.gob == "\x00" && tc.sGob == "\x00" {
// these values mark the current test to skip gob // these values mark the current test to skip gob
return return
} }
t.Parallel()
t.Run("encode", func(t *testing.T) { t.Run("encode", func(t *testing.T) {
t.Parallel()
// encode is unchecked // encode is unchecked
if errors.Is(tc.wantErr, syscall.EINVAL) { if errors.Is(tc.wantErr, syscall.EINVAL) {
return return
@@ -237,8 +204,6 @@ func TestCodecAbsolute(t *testing.T) {
}) })
t.Run("decode", func(t *testing.T) { t.Run("decode", func(t *testing.T) {
t.Parallel()
{ {
var gotA *Absolute var gotA *Absolute
err := gob.NewDecoder(strings.NewReader(tc.gob)).Decode(&gotA) 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.Run("json", func(t *testing.T) {
t.Parallel()
t.Run("marshal", func(t *testing.T) { t.Run("marshal", func(t *testing.T) {
t.Parallel()
// marshal is unchecked // marshal is unchecked
if errors.Is(tc.wantErr, syscall.EINVAL) { if errors.Is(tc.wantErr, syscall.EINVAL) {
return return
@@ -312,8 +273,6 @@ func TestCodecAbsolute(t *testing.T) {
}) })
t.Run("unmarshal", func(t *testing.T) { t.Run("unmarshal", func(t *testing.T) {
t.Parallel()
{ {
var gotA *Absolute var gotA *Absolute
err := json.Unmarshal([]byte(tc.json), &gotA) 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.Run("json passthrough", func(t *testing.T) {
t.Parallel()
wantErr := "invalid character ':' looking for beginning of value" wantErr := "invalid character ':' looking for beginning of value"
if err := new(Absolute).UnmarshalJSON([]byte(":3")); err == nil || err.Error() != wantErr { if err := new(Absolute).UnmarshalJSON([]byte(":3")); err == nil || err.Error() != wantErr {
t.Errorf("UnmarshalJSON: error = %v, want %s", err, wantErr) t.Errorf("UnmarshalJSON: error = %v, want %s", err, wantErr)
@@ -359,11 +316,7 @@ func TestCodecAbsolute(t *testing.T) {
} }
func TestAbsoluteWrap(t *testing.T) { func TestAbsoluteWrap(t *testing.T) {
t.Parallel()
t.Run("join", func(t *testing.T) { t.Run("join", func(t *testing.T) {
t.Parallel()
want := "/etc/nix/nix.conf" want := "/etc/nix/nix.conf"
if got := MustAbs("/etc").Append("nix", "nix.conf"); got.String() != want { if got := MustAbs("/etc").Append("nix", "nix.conf"); got.String() != want {
t.Errorf("Append: %q, want %q", got, 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.Run("dir", func(t *testing.T) {
t.Parallel()
want := "/" want := "/"
if got := MustAbs("/etc").Dir(); got.String() != want { if got := MustAbs("/etc").Dir(); got.String() != want {
t.Errorf("Dir: %q, want %q", got, 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.Run("sort", func(t *testing.T) {
t.Parallel()
want := []*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/sys")} want := []*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/sys")}
got := []*Absolute{MustAbs("/proc"), MustAbs("/sys"), MustAbs("/etc")} got := []*Absolute{MustAbs("/proc"), MustAbs("/sys"), MustAbs("/etc")}
SortAbs(got) SortAbs(got)
@@ -391,8 +340,6 @@ func TestAbsoluteWrap(t *testing.T) {
}) })
t.Run("compact", func(t *testing.T) { t.Run("compact", func(t *testing.T) {
t.Parallel()
want := []*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/sys")} want := []*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/sys")}
if got := CompactAbs([]*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/proc"), MustAbs("/sys")}); !reflect.DeepEqual(got, want) { if got := CompactAbs([]*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/proc"), MustAbs("/sys")}); !reflect.DeepEqual(got, want) {
t.Errorf("CompactAbs: %#v, want %#v", got, want) t.Errorf("CompactAbs: %#v, want %#v", got, want)

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,6 @@ const (
CAP_SYS_ADMIN = 0x15 CAP_SYS_ADMIN = 0x15
CAP_SETPCAP = 0x8 CAP_SETPCAP = 0x8
CAP_NET_ADMIN = 0xc
CAP_DAC_OVERRIDE = 0x1 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. // 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. // 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. // 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" import "testing"
func TestCapToIndex(t *testing.T) { func TestCapToIndex(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
cap uintptr cap uintptr
@@ -16,7 +14,6 @@ func TestCapToIndex(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := capToIndex(tc.cap); got != tc.want { if got := capToIndex(tc.cap); got != tc.want {
t.Errorf("capToIndex: %#x, want %#x", 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) { func TestCapToMask(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
cap uintptr cap uintptr
@@ -38,7 +33,6 @@ func TestCapToMask(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := capToMask(tc.cap); got != tc.want { if got := capToMask(tc.cap); got != tc.want {
t.Errorf("capToMask: %#x, want %#x", 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" "os/exec"
"runtime" "runtime"
"strconv" "strconv"
"sync"
. "syscall" . "syscall"
"time" "time"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/message"
) )
const ( const (
// CancelSignal is the signal expected by container init on context cancel. // CancelSignal is the signal expected by container init on context cancel.
// A custom [Container.Cancel] function must eventually deliver this signal. // A custom [Container.Cancel] function must eventually deliver this signal.
CancelSignal = SIGUSR2 CancelSignal = SIGTERM
// Timeout for writing initParams to Container.setup.
initSetupTimeout = 5 * time.Second
) )
type ( type (
// Container represents a container environment being prepared or run. // Container represents a container environment being prepared or run.
// None of [Container] methods are safe for concurrent use. // None of [Container] methods are safe for concurrent use.
Container struct { Container struct {
// Whether the container init should stay alive after its parent terminates.
AllowOrphan bool
// Cgroup fd, nil to disable. // Cgroup fd, nil to disable.
Cgroup *int Cgroup *int
// ExtraFiles passed through to initial process in the container, // ExtraFiles passed through to initial process in the container,
// with behaviour identical to its [exec.Cmd] counterpart. // with behaviour identical to its [exec.Cmd] counterpart.
ExtraFiles []*os.File ExtraFiles []*os.File
// param pipe for shim and init // param encoder for shim and init
setup *os.File setup *gob.Encoder
// cancels cmd // cancels cmd
cancel context.CancelFunc cancel context.CancelFunc
// closed after Wait returns // closed after Wait returns
@@ -59,23 +49,22 @@ type (
cmd *exec.Cmd cmd *exec.Cmd
ctx context.Context ctx context.Context
msg message.Msg
Params Params
} }
// Params holds container configuration and is safe to serialise. // Params holds container configuration and is safe to serialise.
Params struct { Params struct {
// Working directory in the container. // Working directory in the container.
Dir *check.Absolute Dir *Absolute
// Initial process environment. // Initial process environment.
Env []string Env []string
// Pathname of initial process in the container. // Pathname of initial process in the container.
Path *check.Absolute Path *Absolute
// Initial process argv. // Initial process argv.
Args []string Args []string
// Deliver SIGINT to the initial process on context cancellation. // Deliver SIGINT to the initial process on context cancellation.
ForwardCancel bool 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 AdoptWaitDelay time.Duration
// Mapped Uid in user namespace. // Mapped Uid in user namespace.
@@ -88,11 +77,11 @@ type (
*Ops *Ops
// Seccomp system call filter rules. // Seccomp system call filter rules.
SeccompRules []std.NativeRule SeccompRules []seccomp.NativeRule
// Extra seccomp flags. // Extra seccomp flags.
SeccompFlags seccomp.ExportFlag SeccompFlags seccomp.ExportFlag
// Seccomp presets. Has no effect unless SeccompRules is zero-length. // Seccomp presets. Has no effect unless SeccompRules is zero-length.
SeccompPresets std.FilterPreset SeccompPresets seccomp.FilterPreset
// Do not load seccomp program. // Do not load seccomp program.
SeccompDisable bool SeccompDisable bool
@@ -146,18 +135,11 @@ func (e *StartError) Error() string {
// Message returns a user-facing error message. // Message returns a user-facing error message.
func (e *StartError) Message() string { func (e *StartError) Message() string {
if e.Passthrough { if e.Passthrough {
var (
numError *strconv.NumError
)
switch { switch {
case errors.As(e.Err, new(*os.PathError)), case errors.As(e.Err, new(*os.PathError)),
errors.As(e.Err, new(*os.SyscallError)): errors.As(e.Err, new(*os.SyscallError)):
return "cannot " + e.Err.Error() return "cannot " + e.Err.Error()
case errors.As(e.Err, &numError) && numError != nil:
return "cannot parse " + strconv.Quote(numError.Num) + ": " + numError.Err.Error()
default: default:
return e.Err.Error() return e.Err.Error()
} }
@@ -168,63 +150,28 @@ func (e *StartError) Message() string {
return "cannot " + e.Error() 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. // Start starts the container init. The init process blocks until Serve is called.
func (p *Container) Start() error { func (p *Container) Start() error {
if p == nil || p.cmd == nil || if p.cmd != nil {
p.Ops == nil || len(*p.Ops) == 0 {
return errors.New("container: starting an invalid container")
}
if p.cmd.Process != nil {
return errors.New("container: already started") return errors.New("container: already started")
} }
if p.Ops == nil || len(*p.Ops) == 0 {
if err := ensureCloseOnExec(); err != nil { return errors.New("container: starting an empty container")
return err
} }
ctx, cancel := context.WithCancel(p.ctx)
p.cancel = cancel
// map to overflow id to work around ownership checks // map to overflow id to work around ownership checks
if p.Uid < 1 { if p.Uid < 1 {
p.Uid = OverflowUid(p.msg) p.Uid = OverflowUid()
} }
if p.Gid < 1 { if p.Gid < 1 {
p.Gid = OverflowGid(p.msg) p.Gid = OverflowGid()
} }
if !p.RetainSession { if !p.RetainSession {
p.SeccompPresets |= std.PresetDenyTTY p.SeccompPresets |= seccomp.PresetDenyTTY
} }
if p.AdoptWaitDelay == 0 { if p.AdoptWaitDelay == 0 {
@@ -235,26 +182,19 @@ func (p *Container) Start() error {
p.AdoptWaitDelay = 0 p.AdoptWaitDelay = 0
} }
if p.cmd.Stdin == nil { p.cmd = exec.CommandContext(ctx, MustExecutable())
p.cmd.Stdin = p.Stdin
}
if p.cmd.Stdout == nil {
p.cmd.Stdout = p.Stdout
}
if p.cmd.Stderr == nil {
p.cmd.Stderr = p.Stderr
}
p.cmd.Args = []string{initName} p.cmd.Args = []string{initName}
p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
p.cmd.WaitDelay = p.WaitDelay p.cmd.WaitDelay = p.WaitDelay
if p.Cancel != nil { if p.Cancel != nil {
p.cmd.Cancel = func() error { return p.Cancel(p.cmd) } p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
} else { } else {
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(CancelSignal) } p.cmd.Cancel = func() error { return p.cmd.Process.Signal(CancelSignal) }
} }
p.cmd.Dir = fhs.Root p.cmd.Dir = FHSRoot
p.cmd.SysProcAttr = &SysProcAttr{ p.cmd.SysProcAttr = &SysProcAttr{
Setsid: !p.RetainSession, Setsid: !p.RetainSession,
Pdeathsig: SIGKILL,
Cloneflags: CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS | Cloneflags: CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS |
CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP, CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP,
@@ -263,17 +203,12 @@ func (p *Container) Start() error {
CAP_SYS_ADMIN, CAP_SYS_ADMIN,
// drop capabilities // drop capabilities
CAP_SETPCAP, CAP_SETPCAP,
// bring up loopback interface
CAP_NET_ADMIN,
// overlay access to upperdir and workdir // overlay access to upperdir and workdir
CAP_DAC_OVERRIDE, CAP_DAC_OVERRIDE,
}, },
UseCgroupFD: p.Cgroup != nil, UseCgroupFD: p.Cgroup != nil,
} }
if !p.AllowOrphan {
p.cmd.SysProcAttr.Pdeathsig = SIGKILL
}
if p.cmd.SysProcAttr.UseCgroupFD { if p.cmd.SysProcAttr.UseCgroupFD {
p.cmd.SysProcAttr.CgroupFD = *p.Cgroup 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 // 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} return &StartError{true, "set up params stream", err, false, false}
} else { } else {
p.setup = f p.setup = e
p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)} p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
} }
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...) 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} return &StartError{false, "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", ENOSYS, true, false}
} else { } else {
p.msg.Verbosef("landlock abi version %d", abi) msg.Verbosef("landlock abi version %d", abi)
} }
if rulesetFd, err := rulesetAttr.Create(0); err != nil { if rulesetFd, err := rulesetAttr.Create(0); err != nil {
return &StartError{true, "create landlock ruleset", err, false, false} return &StartError{true, "create landlock ruleset", err, false, false}
} else { } else {
p.msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr) msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
if err = LandlockRestrictSelf(rulesetFd, 0); err != nil { if err = LandlockRestrictSelf(rulesetFd, 0); err != nil {
_ = Close(rulesetFd) _ = Close(rulesetFd)
return &StartError{true, "enforce landlock ruleset", err, false, false} return &StartError{true, "enforce landlock ruleset", err, false, false}
} }
if err = Close(rulesetFd); err != nil { 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 // not fatal
} }
} }
@@ -342,7 +277,7 @@ func (p *Container) Start() error {
landlockOut: landlockOut:
} }
p.msg.Verbose("starting container init") msg.Verbose("starting container init")
if err := p.cmd.Start(); err != nil { if err := p.cmd.Start(); err != nil {
return &StartError{false, "start container init", err, false, true} return &StartError{false, "start container init", err, false, true}
} }
@@ -364,9 +299,6 @@ func (p *Container) Serve() error {
setup := p.setup setup := p.setup
p.setup = nil 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 { if p.Path == nil {
p.cancel() p.cancel()
@@ -375,20 +307,21 @@ func (p *Container) Serve() error {
// do not transmit nil // do not transmit nil
if p.Dir == nil { if p.Dir == nil {
p.Dir = fhs.AbsRoot p.Dir = AbsFHSRoot
} }
if p.SeccompRules == nil { 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(
p.Params, &initParams{
Getuid(), p.Params,
Getgid(), Getuid(),
len(p.ExtraFiles), Getgid(),
p.msg.IsVerbose(), len(p.ExtraFiles),
}) msg.IsVerbose(),
_ = setup.Close() },
)
if err != nil { if err != nil {
p.cancel() 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]. // Wait waits for the container init process to exit and releases any resources associated with the [Container].
func (p *Container) Wait() error { func (p *Container) Wait() error {
if p.cmd == nil || p.cmd.Process == nil { if p.cmd == nil {
return EINVAL return EINVAL
} }
@@ -409,36 +342,6 @@ func (p *Container) Wait() error {
return err 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 { func (p *Container) String() string {
return fmt.Sprintf("argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x", 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)) 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. // 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 { func New(ctx context.Context) *Container {
if msg == nil { return &Container{ctx: ctx, Params: Params{Ops: new(Ops)}}
msg = message.New(nil)
}
p := &Container{ctx: ctx, msg: msg, Params: Params{Ops: new(Ops)}}
c, cancel := context.WithCancel(ctx)
p.cancel = cancel
p.cmd = exec.CommandContext(c, MustExecutable(msg))
return p
} }
// NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields. // 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 { func NewCommand(ctx context.Context, pathname *Absolute, name string, args ...string) *Container {
z := New(ctx, msg) z := New(ctx)
z.Path = pathname z.Path = pathname
z.Args = append([]string{name}, args...) z.Args = append([]string{name}, args...)
return z return z

View File

@@ -20,58 +20,15 @@ import (
"hakurei.app/command" "hakurei.app/command"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/container/vfs" "hakurei.app/container/vfs"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/ldd" "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) { func TestStartError(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
err error err error
@@ -84,7 +41,8 @@ func TestStartError(t *testing.T) {
Fatal: true, Fatal: true,
Step: "set up params stream", Step: "set up params stream",
Err: container.ErrReceiveEnv, Err: container.ErrReceiveEnv,
}, "set up params stream: environment variable not set", },
"set up params stream: environment variable not set",
container.ErrReceiveEnv, syscall.EBADF, container.ErrReceiveEnv, syscall.EBADF,
"cannot set up params stream: environment variable not set"}, "cannot set up params stream: environment variable not set"},
@@ -92,7 +50,8 @@ func TestStartError(t *testing.T) {
Fatal: true, Fatal: true,
Step: "set up params stream", Step: "set up params stream",
Err: &os.SyscallError{Syscall: "pipe2", Err: syscall.EBADF}, 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, syscall.EBADF, os.ErrInvalid,
"cannot set up params stream pipe2: bad file descriptor"}, "cannot set up params stream pipe2: bad file descriptor"},
@@ -100,14 +59,16 @@ func TestStartError(t *testing.T) {
Fatal: true, Fatal: true,
Step: "prctl(PR_SET_NO_NEW_PRIVS)", Step: "prctl(PR_SET_NO_NEW_PRIVS)",
Err: syscall.EPERM, 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, syscall.EPERM, syscall.EACCES,
"cannot prctl(PR_SET_NO_NEW_PRIVS): operation not permitted"}, "cannot prctl(PR_SET_NO_NEW_PRIVS): operation not permitted"},
{"landlock abi", &container.StartError{ {"landlock abi", &container.StartError{
Step: "get landlock ABI", Step: "get landlock ABI",
Err: syscall.ENOSYS, Err: syscall.ENOSYS,
}, "get landlock ABI: function not implemented", },
"get landlock ABI: function not implemented",
syscall.ENOSYS, syscall.ENOEXEC, syscall.ENOSYS, syscall.ENOEXEC,
"cannot get landlock ABI: function not implemented"}, "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", Step: "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET",
Err: syscall.ENOSYS, Err: syscall.ENOSYS,
Origin: true, 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, syscall.ENOSYS, syscall.ENOSPC,
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET"}, "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET"},
@@ -123,7 +85,8 @@ func TestStartError(t *testing.T) {
Fatal: true, Fatal: true,
Step: "create landlock ruleset", Step: "create landlock ruleset",
Err: syscall.EBADFD, Err: syscall.EBADFD,
}, "create landlock ruleset: file descriptor in bad state", },
"create landlock ruleset: file descriptor in bad state",
syscall.EBADFD, syscall.EBADF, syscall.EBADFD, syscall.EBADF,
"cannot create landlock ruleset: file descriptor in bad state"}, "cannot create landlock ruleset: file descriptor in bad state"},
@@ -131,7 +94,8 @@ func TestStartError(t *testing.T) {
Fatal: true, Fatal: true,
Step: "enforce landlock ruleset", Step: "enforce landlock ruleset",
Err: syscall.ENOTRECOVERABLE, Err: syscall.ENOTRECOVERABLE,
}, "enforce landlock ruleset: state not recoverable", },
"enforce landlock ruleset: state not recoverable",
syscall.ENOTRECOVERABLE, syscall.ETIMEDOUT, syscall.ENOTRECOVERABLE, syscall.ETIMEDOUT,
"cannot enforce landlock ruleset: state not recoverable"}, "cannot enforce landlock ruleset: state not recoverable"},
@@ -142,7 +106,8 @@ func TestStartError(t *testing.T) {
Path: "/proc/nonexistent", Path: "/proc/nonexistent",
Err: syscall.ENOENT, Err: syscall.ENOENT,
}, Passthrough: true, }, Passthrough: true,
}, "fork/exec /proc/nonexistent: no such file or directory", },
"fork/exec /proc/nonexistent: no such file or directory",
syscall.ENOENT, syscall.ENOSYS, syscall.ENOENT, syscall.ENOSYS,
"cannot fork/exec /proc/nonexistent: no such file or directory"}, "cannot fork/exec /proc/nonexistent: no such file or directory"},
@@ -152,19 +117,11 @@ func TestStartError(t *testing.T) {
Syscall: "open", Syscall: "open",
Err: syscall.ENOSYS, Err: syscall.ENOSYS,
}, Passthrough: true, }, Passthrough: true,
}, "open: function not implemented", },
"open: function not implemented",
syscall.ENOSYS, syscall.ENOENT, syscall.ENOSYS, syscall.ENOENT,
"cannot open: function not implemented"}, "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{ {"start other", &container.StartError{
Step: "start container init", Step: "start container init",
Err: &net.OpError{ Err: &net.OpError{
@@ -172,14 +129,13 @@ func TestStartError(t *testing.T) {
Net: "unix", Net: "unix",
Err: syscall.ECONNREFUSED, Err: syscall.ECONNREFUSED,
}, Passthrough: true, }, Passthrough: true,
}, "dial unix: connection refused", },
"dial unix: connection refused",
syscall.ECONNREFUSED, syscall.ECONNABORTED, syscall.ECONNREFUSED, syscall.ECONNABORTED,
"dial unix: connection refused"}, "dial unix: connection refused"},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("error", func(t *testing.T) { t.Run("error", func(t *testing.T) {
if got := tc.err.Error(); got != tc.s { if got := tc.err.Error(); got != tc.s {
t.Errorf("Error: %q, want %q", 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) { 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 != "" { if tc.msg != "" {
t.Errorf("GetMessage: err does not implement MessageError") t.Errorf("GetErrorMessage: err does not implement MessageError")
} }
return return
} else if got != tc.msg { } 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 uid int
gid int gid int
rules []std.NativeRule rules []seccomp.NativeRule
flags seccomp.ExportFlag flags seccomp.ExportFlag
presets std.FilterPreset presets seccomp.FilterPreset
}{ }{
{"minimal", true, false, false, true, {"minimal", true, false, false, true,
emptyOps, emptyMnt, emptyOps, emptyMnt,
1000, 100, nil, 0, std.PresetStrict}, 1000, 100, nil, 0, seccomp.PresetStrict},
{"allow", true, true, true, false, {"allow", true, true, true, false,
emptyOps, emptyMnt, emptyOps, emptyMnt,
1000, 100, nil, 0, std.PresetExt | std.PresetDenyDevel}, 1000, 100, nil, 0, seccomp.PresetExt | seccomp.PresetDenyDevel},
{"no filter", false, true, true, true, {"no filter", false, true, true, true,
emptyOps, emptyMnt, emptyOps, emptyMnt,
1000, 100, nil, 0, std.PresetExt}, 1000, 100, nil, 0, seccomp.PresetExt},
{"custom rules", true, true, true, false, {"custom rules", true, true, true, false,
emptyOps, emptyMnt, emptyOps, emptyMnt,
1, 31, []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, {"tmpfs", true, false, false, true,
earlyOps(new(container.Ops). earlyOps(new(container.Ops).
Tmpfs(hst.AbsPrivateTmp, 0, 0755), Tmpfs(hst.AbsTmp, 0, 0755),
), ),
earlyMnt( 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, {"dev", true, true /* go test output is not a tty */, false, false,
earlyOps(new(container.Ops). earlyOps(new(container.Ops).
Dev(check.MustAbs("/dev"), true), Dev(container.MustAbs("/dev"), true),
), ),
earlyMnt( earlyMnt(
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", ignore, ignore), ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
ent("/null", "/dev/null", ignore, "devtmpfs", ignore, ignore), ent("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/zero", "/dev/zero", ignore, "devtmpfs", ignore, ignore), ent("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/full", "/dev/full", ignore, "devtmpfs", ignore, ignore), ent("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/random", "/dev/random", ignore, "devtmpfs", ignore, ignore), ent("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/urandom", "/dev/urandom", ignore, "devtmpfs", ignore, ignore), ent("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/tty", "/dev/tty", ignore, "devtmpfs", ignore, 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/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/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore), 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, {"dev no mqueue", true, true /* go test output is not a tty */, false, false,
earlyOps(new(container.Ops). earlyOps(new(container.Ops).
Dev(check.MustAbs("/dev"), false), Dev(container.MustAbs("/dev"), false),
), ),
earlyMnt( earlyMnt(
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", ignore, ignore), ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
ent("/null", "/dev/null", ignore, "devtmpfs", ignore, ignore), ent("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/zero", "/dev/zero", ignore, "devtmpfs", ignore, ignore), ent("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/full", "/dev/full", ignore, "devtmpfs", ignore, ignore), ent("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/random", "/dev/random", ignore, "devtmpfs", ignore, ignore), ent("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/urandom", "/dev/urandom", ignore, "devtmpfs", ignore, ignore), ent("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/tty", "/dev/tty", ignore, "devtmpfs", ignore, 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/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore), 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, {"overlay", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) { func(t *testing.T) (*container.Ops, context.Context) {
tempDir := check.MustAbs(t.TempDir()) tempDir := container.MustAbs(t.TempDir())
lower0, lower1, upper, work := lower0, lower1, upper, work :=
tempDir.Append("lower0"), tempDir.Append("lower0"),
tempDir.Append("lower1"), tempDir.Append("lower1"),
tempDir.Append("upper"), tempDir.Append("upper"),
tempDir.Append("work") 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 { if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err) t.Fatalf("Mkdir: error = %v", err)
} }
} }
return new(container.Ops). 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(), context.WithValue(context.WithValue(context.WithValue(context.WithValue(t.Context(),
testVal("lower1"), lower1), testVal("lower1"), lower1),
testVal("lower0"), lower0), testVal("lower0"), lower0),
@@ -328,74 +284,74 @@ var containerTestCases = []struct {
}, },
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry { func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{ return []*vfs.MountInfoEntry{
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay", ent("/", hst.Tmp, "rw", "overlay", "overlay",
"rw,lowerdir="+ "rw,lowerdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*check.Absolute).String())+":"+ container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*check.Absolute).String())+ container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+
",upperdir="+ ",upperdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(*check.Absolute).String())+ container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(*container.Absolute).String())+
",workdir="+ ",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"), ",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, {"overlay ephemeral", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) { func(t *testing.T) (*container.Ops, context.Context) {
tempDir := check.MustAbs(t.TempDir()) tempDir := container.MustAbs(t.TempDir())
lower0, lower1 := lower0, lower1 :=
tempDir.Append("lower0"), tempDir.Append("lower0"),
tempDir.Append("lower1") 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 { if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err) t.Fatalf("Mkdir: error = %v", err)
} }
} }
return new(container.Ops). return new(container.Ops).
OverlayEphemeral(hst.AbsPrivateTmp, lower0, lower1), OverlayEphemeral(hst.AbsTmp, lower0, lower1),
t.Context() t.Context()
}, },
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry { func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{ return []*vfs.MountInfoEntry{
// contains random suffix // 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, {"overlay readonly", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) { func(t *testing.T) (*container.Ops, context.Context) {
tempDir := check.MustAbs(t.TempDir()) tempDir := container.MustAbs(t.TempDir())
lower0, lower1 := lower0, lower1 :=
tempDir.Append("lower0"), tempDir.Append("lower0"),
tempDir.Append("lower1") 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 { if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err) t.Fatalf("Mkdir: error = %v", err)
} }
} }
return new(container.Ops). return new(container.Ops).
OverlayReadonly(hst.AbsPrivateTmp, lower0, lower1), OverlayReadonly(hst.AbsTmp, lower0, lower1),
context.WithValue(context.WithValue(t.Context(), context.WithValue(context.WithValue(t.Context(),
testVal("lower1"), lower1), testVal("lower1"), lower1),
testVal("lower0"), lower0) testVal("lower0"), lower0)
}, },
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry { func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{ return []*vfs.MountInfoEntry{
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay", ent("/", hst.Tmp, "rw", "overlay", "overlay",
"ro,lowerdir="+ "ro,lowerdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*check.Absolute).String())+":"+ container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*check.Absolute).String())+ container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+
",redirect_dir=nofollow,userxattr"), ",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) { func TestContainer(t *testing.T) {
t.Parallel() replaceOutput(t)
t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) { t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) {
wantErr := context.Canceled wantErr := context.Canceled
@@ -430,15 +386,13 @@ func TestContainer(t *testing.T) {
for i, tc := range containerTestCases { for i, tc := range containerTestCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
wantOps, wantOpsCtx := tc.ops(t) wantOps, wantOpsCtx := tc.ops(t)
wantMnt := tc.mnt(t, wantOpsCtx) wantMnt := tc.mnt(t, wantOpsCtx)
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout) ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
defer cancel() defer cancel()
var libPaths []*check.Absolute var libPaths []*container.Absolute
c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i)) c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i))
c.Uid = tc.uid c.Uid = tc.uid
c.Gid = tc.gid c.Gid = tc.gid
@@ -459,11 +413,11 @@ func TestContainer(t *testing.T) {
c.HostNet = tc.net c.HostNet = tc.net
c. c.
Readonly(check.MustAbs(pathReadonly), 0755). Readonly(container.MustAbs(pathReadonly), 0755).
Tmpfs(check.MustAbs("/tmp"), 0, 0755). Tmpfs(container.MustAbs("/tmp"), 0, 0755).
Place(check.MustAbs("/etc/hostname"), []byte(c.Hostname)) Place(container.MustAbs("/etc/hostname"), []byte(c.Hostname))
// needs /proc to check mountinfo // 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 // mountinfo cannot be resolved directly by helper due to libPaths nondeterminism
mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths)) mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
@@ -494,10 +448,10 @@ func TestContainer(t *testing.T) {
_, _ = output.WriteTo(os.Stdout) _, _ = output.WriteTo(os.Stdout)
t.Fatalf("cannot serialise expected mount points: %v", err) 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 { if tc.ro {
c.Remount(check.MustAbs("/"), syscall.MS_RDONLY) c.Remount(container.MustAbs("/"), syscall.MS_RDONLY)
} }
if err := c.Start(); err != nil { if err := c.Start(); err != nil {
@@ -551,7 +505,6 @@ func testContainerCancel(
waitCheck func(t *testing.T, c *container.Container), waitCheck func(t *testing.T, c *container.Container),
) func(t *testing.T) { ) func(t *testing.T) {
return func(t *testing.T) { return func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout) ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
c := helperNewContainer(ctx, "block") c := helperNewContainer(ctx, "block")
@@ -594,14 +547,12 @@ func testContainerCancel(
} }
func TestContainerString(t *testing.T) { func TestContainerString(t *testing.T) {
t.Parallel() c := container.NewCommand(t.Context(), container.MustAbs("/run/current-system/sw/bin/ldd"), "ldd", "/usr/bin/env")
msg := message.New(nil)
c := container.NewCommand(t.Context(), msg, check.MustAbs("/run/current-system/sw/bin/ldd"), "ldd", "/usr/bin/env")
c.SeccompFlags |= seccomp.AllowMultiarch c.SeccompFlags |= seccomp.AllowMultiarch
c.SeccompRules = seccomp.Preset( c.SeccompRules = seccomp.Preset(
std.PresetExt|std.PresetDenyNS|std.PresetDenyTTY, seccomp.PresetExt|seccomp.PresetDenyNS|seccomp.PresetDenyTTY,
c.SeccompFlags) c.SeccompFlags)
c.SeccompPresets = std.PresetStrict c.SeccompPresets = seccomp.PresetStrict
want := `argv: ["ldd" "/usr/bin/env"], filter: true, rules: 65, flags: 0x1, presets: 0xf` want := `argv: ["ldd" "/usr/bin/env"], filter: true, rules: 65, flags: 0x1, presets: 0xf`
if got := c.String(); got != want { if got := c.String(); got != want {
t.Errorf("String: %s, want %s", got, want) t.Errorf("String: %s, want %s", got, want)
@@ -615,13 +566,14 @@ const (
func init() { func init() {
helperCommands = append(helperCommands, func(c command.Command) { helperCommands = append(helperCommands, func(c command.Command) {
c.Command("block", command.UsageInternal, func(args []string) error { c.Command("block", command.UsageInternal, func(args []string) error {
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt)
go func() { <-sig; os.Exit(blockExitCodeInterrupt) }()
if _, err := os.NewFile(3, "sync").Write([]byte{0}); err != nil { if _, err := os.NewFile(3, "sync").Write([]byte{0}); err != nil {
return fmt.Errorf("write to sync pipe: %v", err) return fmt.Errorf("write to sync pipe: %v", err)
} }
{
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt)
go func() { <-sig; os.Exit(blockExitCodeInterrupt) }()
}
select {} select {}
}) })
@@ -690,22 +642,11 @@ func init() {
return fmt.Errorf("got more than %d entries", len(mnt)) return fmt.Errorf("got more than %d entries", len(mnt))
} }
// ugly hack but should be reliable and is less likely to // ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags
//false negative than comparing by parsed flags cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",relatime")
for _, s := range []string{ cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",noatime")
"relatime", mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",relatime")
"noatime", mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",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)
}
if !cur.EqualWithIgnore(mnt[i], "\x00") { if !cur.EqualWithIgnore(mnt[i], "\x00") {
fail = true fail = true
@@ -742,13 +683,13 @@ const (
) )
var ( var (
absHelperInnerPath = check.MustAbs(helperInnerPath) absHelperInnerPath = container.MustAbs(helperInnerPath)
) )
var helperCommands []func(c command.Command) var helperCommands []func(c command.Command)
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
container.TryArgv0(nil) container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
if os.Getenv(envDoCheck) == "1" { if os.Getenv(envDoCheck) == "1" {
c := command.New(os.Stderr, log.Printf, "helper", func(args []string) error { 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()) os.Exit(m.Run())
} }
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*check.Absolute, args ...string) (c *container.Container) { func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*container.Absolute, args ...string) (c *container.Container) {
msg := message.New(nil) c = container.NewCommand(ctx, absHelperInnerPath, "helper", args...)
msg.SwapVerbose(testing.Verbose())
executable := check.MustAbs(container.MustExecutable(msg))
c = container.NewCommand(ctx, msg, absHelperInnerPath, "helper", args...)
c.Env = append(c.Env, envDoCheck+"=1") c.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 // 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) log.Fatalf("ldd: %v", err)
} else { } else {
*libPaths = ldd.Path(entries) *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) { 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 ( import (
"io" "io"
"io/fs" "io/fs"
"log"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
@@ -11,8 +12,6 @@ import (
"syscall" "syscall"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/message"
) )
type osFile interface { type osFile interface {
@@ -39,7 +38,7 @@ type syscallDispatcher interface {
setNoNewPrivs() error setNoNewPrivs() error
// lastcap provides [LastCap]. // lastcap provides [LastCap].
lastcap(msg message.Msg) uintptr lastcap() uintptr
// capset provides capset. // capset provides capset.
capset(hdrp *capHeader, datap *[2]capData) error capset(hdrp *capHeader, datap *[2]capData) error
// capBoundingSetDrop provides capBoundingSetDrop. // capBoundingSetDrop provides capBoundingSetDrop.
@@ -54,18 +53,16 @@ type syscallDispatcher interface {
receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
// bindMount provides procPaths.bindMount. // 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 provides procPaths.remount.
remount(msg message.Msg, target string, flags uintptr) error remount(target string, flags uintptr) error
// mountTmpfs provides mountTmpfs. // mountTmpfs provides mountTmpfs.
mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error
// ensureFile provides ensureFile. // ensureFile provides ensureFile.
ensureFile(name string, perm, pperm os.FileMode) error ensureFile(name string, perm, pperm os.FileMode) error
// mustLoopback provides mustLoopback.
mustLoopback(msg message.Msg)
// seccompLoad provides [seccomp.Load]. // 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 provides [signal.Notify].
notify(c chan<- os.Signal, sig ...os.Signal) notify(c chan<- os.Signal, sig ...os.Signal)
// start starts [os/exec.Cmd]. // start starts [os/exec.Cmd].
@@ -125,12 +122,22 @@ type syscallDispatcher interface {
// wait4 provides syscall.Wait4 // wait4 provides syscall.Wait4
wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error)
// printf provides the Printf method of [log.Logger]. // printf provides [log.Printf].
printf(msg message.Msg, format string, v ...any) printf(format string, v ...any)
// fatal provides the Fatal method of [log.Logger] // fatal provides [log.Fatal]
fatal(msg message.Msg, v ...any) fatal(v ...any)
// fatalf provides the Fatalf method of [log.Logger] // fatalf provides [log.Fatalf]
fatalf(msg message.Msg, format string, v ...any) 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. // 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) setDumpable(dumpable uintptr) error { return SetDumpable(dumpable) }
func (direct) setNoNewPrivs() error { return SetNoNewPrivs() } 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) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) }
func (direct) capBoundingSetDrop(cap uintptr) error { return capBoundingSetDrop(cap) } func (direct) capBoundingSetDrop(cap uintptr) error { return capBoundingSetDrop(cap) }
func (direct) capAmbientClearAll() error { return capAmbientClearAll() } 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) return Receive(key, e, fdp)
} }
func (direct) bindMount(msg message.Msg, source, target string, flags uintptr) error { func (direct) bindMount(source, target string, flags uintptr, eq bool) error {
return hostProc.bindMount(msg, source, target, flags) return hostProc.bindMount(source, target, flags, eq)
} }
func (direct) remount(msg message.Msg, target string, flags uintptr) error { func (direct) remount(target string, flags uintptr) error {
return hostProc.remount(msg, target, flags) return hostProc.remount(target, flags)
} }
func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error { func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
return mountTmpfs(k, fsname, target, flags, size, perm) 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 { func (direct) ensureFile(name string, perm, pperm os.FileMode) error {
return ensureFile(name, perm, pperm) 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) return seccomp.Load(rules, flags)
} }
func (direct) notify(c chan<- os.Signal, sig ...os.Signal) { signal.Notify(c, sig...) } 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) return syscall.Wait4(pid, wstatus, options, rusage)
} }
func (direct) printf(msg message.Msg, format string, v ...any) { msg.GetLogger().Printf(format, v...) } func (direct) printf(format string, v ...any) { log.Printf(format, v...) }
func (direct) fatal(msg message.Msg, v ...any) { msg.GetLogger().Fatal(v...) } func (direct) fatal(v ...any) { log.Fatal(v...) }
func (direct) fatalf(msg message.Msg, format string, v ...any) { msg.GetLogger().Fatalf(format, 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 ( import (
"bytes" "bytes"
"fmt"
"io" "io"
"io/fs" "io/fs"
"log"
"os" "os"
"os/exec" "os/exec"
"reflect" "reflect"
@@ -17,9 +15,7 @@ import (
"time" "time"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/container/stub" "hakurei.app/container/stub"
"hakurei.app/message"
) )
type opValidTestCase struct { type opValidTestCase struct {
@@ -33,12 +29,10 @@ func checkOpsValid(t *testing.T, testCases []opValidTestCase) {
t.Run("valid", func(t *testing.T) { t.Run("valid", func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
if got := tc.op.Valid(); got != tc.want { if got := tc.op.Valid(); got != tc.want {
t.Errorf("Valid: %v, want %v", got, tc.want) t.Errorf("Valid: %v, want %v", got, tc.want)
@@ -59,12 +53,10 @@ func checkOpsBuilder(t *testing.T, testCases []opsBuilderTestCase) {
t.Run("build", func(t *testing.T) { t.Run("build", func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
if !slices.EqualFunc(*tc.ops, tc.want, func(op Op, v Op) bool { return op.Is(v) }) { if !slices.EqualFunc(*tc.ops, tc.want, func(op Op, v Op) bool { return op.Is(v) }) {
t.Errorf("Ops: %#v, want %#v", tc.ops, tc.want) t.Errorf("Ops: %#v, want %#v", tc.ops, tc.want)
@@ -85,12 +77,10 @@ func checkOpIs(t *testing.T, testCases []opIsTestCase) {
t.Run("is", func(t *testing.T) { t.Run("is", func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
if got := tc.op.Is(tc.v); got != tc.want { if got := tc.op.Is(tc.v); got != tc.want {
t.Errorf("Is: %v, want %v", got, tc.want) t.Errorf("Is: %v, want %v", got, tc.want)
@@ -113,17 +103,15 @@ func checkOpMeta(t *testing.T, testCases []opMetaTestCase) {
t.Run("meta", func(t *testing.T) { t.Run("meta", func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
t.Run("prefix", func(t *testing.T) { t.Run("prefix", func(t *testing.T) {
t.Helper() t.Helper()
if got, _ := tc.op.prefix(); got != tc.wantPrefix { if got := tc.op.prefix(); got != tc.wantPrefix {
t.Errorf("prefix: %q, want %q", got, tc.wantPrefix) t.Errorf("prefix: %q, want %q", got, tc.wantPrefix)
} }
}) })
@@ -148,7 +136,7 @@ func call(name string, args stub.ExpectArgs, ret any, err error) stub.Call {
type simpleTestCase struct { type simpleTestCase struct {
name string name string
f func(k *kstub) error f func(k syscallDispatcher) error
want stub.Expect want stub.Expect
wantErr error wantErr error
} }
@@ -159,11 +147,9 @@ func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
wait4signal := make(chan struct{}) wait4signal := make(chan struct{})
lockNotify := make(chan struct{}) k := &kstub{wait4signal, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, s} }, tc.want)}
k := &kstub{wait4signal, lockNotify, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, lockNotify, s} }, tc.want)}
defer stub.HandleExit(t) defer stub.HandleExit(t)
if err := tc.f(k); !reflect.DeepEqual(err, tc.wantErr) { if err := tc.f(k); !reflect.DeepEqual(err, tc.wantErr) {
t.Errorf("%s: error = %v, want %v", fname, err, tc.wantErr) t.Errorf("%s: error = %v, want %v", fname, err, tc.wantErr)
@@ -194,18 +180,16 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
t.Run("behaviour", func(t *testing.T) { t.Run("behaviour", func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
k := &kstub{nil, nil, stub.New(t, state := &setupState{Params: tc.params}
func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, nil, s} }, 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)}, 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) defer stub.HandleExit(t)
errEarly := tc.op.early(state, k) errEarly := tc.op.early(state, k)
k.Expects(stub.CallSeparator) k.Expects(stub.CallSeparator)
@@ -323,19 +307,12 @@ const (
type kstub struct { type kstub struct {
wait4signal chan struct{} wait4signal chan struct{}
lockNotify chan struct{}
*stub.Stub[syscallDispatcher] *stub.Stub[syscallDispatcher]
} }
func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) } func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) }
func (k *kstub) lockOSThread() { func (k *kstub) lockOSThread() { k.Helper(); k.Expects("lockOSThread") }
k.Helper()
expect := k.Expects("lockOSThread")
if k.lockNotify != nil && expect.Ret == magicWait4Signal {
<-k.lockNotify
}
}
func (k *kstub) setPtracer(pid uintptr) error { func (k *kstub) setPtracer(pid uintptr) error {
k.Helper() 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) setNoNewPrivs() error { k.Helper(); return k.Expects("setNoNewPrivs").Err }
func (k *kstub) lastcap(msg message.Msg) uintptr { func (k *kstub) lastcap() uintptr { k.Helper(); return k.Expects("lastcap").Ret.(uintptr) }
k.Helper()
k.checkMsg(msg)
return k.Expects("lastcap").Ret.(uintptr)
}
func (k *kstub) capset(hdrp *capHeader, datap *[2]capData) error { func (k *kstub) capset(hdrp *capHeader, datap *[2]capData) error {
k.Helper() k.Helper()
@@ -430,18 +403,17 @@ func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error
return 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.Helper()
k.checkMsg(msg)
return k.Expects("bindMount").Error( return k.Expects("bindMount").Error(
stub.CheckArg(k.Stub, "source", source, 0), stub.CheckArg(k.Stub, "source", source, 0),
stub.CheckArg(k.Stub, "target", target, 1), 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.Helper()
k.checkMsg(msg)
return k.Expects("remount").Error( return k.Expects("remount").Error(
stub.CheckArg(k.Stub, "target", target, 0), stub.CheckArg(k.Stub, "target", target, 0),
stub.CheckArg(k.Stub, "flags", flags, 1)) 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)) stub.CheckArg(k.Stub, "pperm", pperm, 2))
} }
func (*kstub) mustLoopback(message.Msg) { /* noop */ } func (k *kstub) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error {
func (k *kstub) seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error {
k.Helper() k.Helper()
return k.Expects("seccompLoad").Error( return k.Expects("seccompLoad").Error(
stub.CheckArgReflect(k.Stub, "rules", rules, 0), stub.CheckArgReflect(k.Stub, "rules", rules, 0),
@@ -482,10 +452,6 @@ func (k *kstub) notify(c chan<- os.Signal, sig ...os.Signal) {
k.FailNow() k.FailNow()
} }
if k.lockNotify != nil && expect.Ret == magicWait4Signal {
defer close(k.lockNotify)
}
// export channel for external instrumentation // export channel for external instrumentation
if chanf, ok := expect.Args[0].(func(c chan<- os.Signal)); ok && chanf != nil { if chanf, ok := expect.Args[0].(func(c chan<- os.Signal)); ok && chanf != nil {
chanf(c) chanf(c)
@@ -729,7 +695,7 @@ func (k *kstub) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage
return return
} }
func (k *kstub) printf(_ message.Msg, format string, v ...any) { func (k *kstub) printf(format string, v ...any) {
k.Helper() k.Helper()
if k.Expects("printf").Error( if k.Expects("printf").Error(
stub.CheckArg(k.Stub, "format", format, 0), 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() k.Helper()
if k.Expects("fatal").Error( if k.Expects("fatal").Error(
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil { stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil {
@@ -747,7 +713,7 @@ func (k *kstub) fatal(_ message.Msg, v ...any) {
panic(stub.PanicExit) panic(stub.PanicExit)
} }
func (k *kstub) fatalf(_ message.Msg, format string, v ...any) { func (k *kstub) fatalf(format string, v ...any) {
k.Helper() k.Helper()
if k.Expects("fatalf").Error( if k.Expects("fatalf").Error(
stub.CheckArg(k.Stub, "format", format, 0), stub.CheckArg(k.Stub, "format", format, 0),
@@ -757,36 +723,7 @@ func (k *kstub) fatalf(_ message.Msg, format string, v ...any) {
panic(stub.PanicExit) panic(stub.PanicExit)
} }
func (k *kstub) checkMsg(msg message.Msg) { func (k *kstub) verbose(v ...any) {
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) {
k.Helper() k.Helper()
if k.Expects("verbose").Error( if k.Expects("verbose").Error(
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil { 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() k.Helper()
if k.Expects("verbosef").Error( if k.Expects("verbosef").Error(
stub.CheckArg(k.Stub, "format", format, 0), 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) suspend() { k.Helper(); k.Expects("suspend") }
func (k *kstub) Resume() bool { k.Helper(); return k.Expects("resume").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) beforeExit() { k.Helper(); k.Expects("beforeExit") }

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -6,47 +6,42 @@ import (
"syscall" "syscall"
"testing" "testing"
"hakurei.app/container/check"
"hakurei.app/container/std"
"hakurei.app/container/stub" "hakurei.app/container/stub"
) )
func TestBindMountOp(t *testing.T) { func TestBindMountOp(t *testing.T) {
t.Parallel()
checkOpBehaviour(t, []opBehaviourTestCase{ checkOpBehaviour(t, []opBehaviourTestCase{
{"ENOENT not optional", new(Params), &BindMountOp{ {"ENOENT not optional", new(Params), &BindMountOp{
Source: check.MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: check.MustAbs("/bin/"), Target: MustAbs("/bin/"),
}, []stub.Call{ }, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "", syscall.ENOENT), call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "", syscall.ENOENT),
}, syscall.ENOENT, nil, nil}, }, syscall.ENOENT, nil, nil},
{"skip optional", new(Params), &BindMountOp{ {"skip optional", new(Params), &BindMountOp{
Source: check.MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: check.MustAbs("/bin/"), Target: MustAbs("/bin/"),
Flags: std.BindOptional, Flags: BindOptional,
}, []stub.Call{ }, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "", syscall.ENOENT), call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "", syscall.ENOENT),
}, nil, nil, nil}, }, nil, nil, nil},
{"success optional", new(Params), &BindMountOp{ {"success optional", new(Params), &BindMountOp{
Source: check.MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: check.MustAbs("/bin/"), Target: MustAbs("/bin/"),
Flags: std.BindOptional, Flags: BindOptional,
}, []stub.Call{ }, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil), call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []stub.Call{ }, nil, []stub.Call{
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil), call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, 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), call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
}, nil}, }, nil},
{"ensureFile device", new(Params), &BindMountOp{ {"ensureFile device", new(Params), &BindMountOp{
Source: check.MustAbs("/dev/null"), Source: MustAbs("/dev/null"),
Target: check.MustAbs("/dev/null"), Target: MustAbs("/dev/null"),
Flags: std.BindWritable | std.BindDevice, Flags: BindWritable | BindDevice,
}, []stub.Call{ }, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil), call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
}, nil, []stub.Call{ }, nil, []stub.Call{
@@ -55,63 +50,60 @@ func TestBindMountOp(t *testing.T) {
}, stub.UniqueError(5)}, }, stub.UniqueError(5)},
{"mkdirAll ensure", new(Params), &BindMountOp{ {"mkdirAll ensure", new(Params), &BindMountOp{
Source: check.MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: check.MustAbs("/bin/"), Target: MustAbs("/bin/"),
Flags: std.BindEnsure, Flags: BindEnsure,
}, []stub.Call{ }, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/bin/", os.FileMode(0700)}, nil, stub.UniqueError(4)), call("mkdirAll", stub.ExpectArgs{"/bin/", os.FileMode(0700)}, nil, stub.UniqueError(4)),
}, stub.UniqueError(4), nil, nil}, }, stub.UniqueError(4), nil, nil},
{"success ensure", new(Params), &BindMountOp{ {"success ensure", new(Params), &BindMountOp{
Source: check.MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: check.MustAbs("/usr/bin/"), Target: MustAbs("/usr/bin/"),
Flags: std.BindEnsure, Flags: BindEnsure,
}, []stub.Call{ }, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/bin/", os.FileMode(0700)}, nil, nil), call("mkdirAll", stub.ExpectArgs{"/bin/", os.FileMode(0700)}, nil, nil),
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil), call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []stub.Call{ }, nil, []stub.Call{
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil), call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
call("mkdirAll", stub.ExpectArgs{"/sysroot/usr/bin", os.FileMode(0700)}, nil, 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), call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/usr/bin", uintptr(0x4005), false}, nil, nil),
}, nil}, }, nil},
{"success device ro", new(Params), &BindMountOp{ {"success device ro", new(Params), &BindMountOp{
Source: check.MustAbs("/dev/null"), Source: MustAbs("/dev/null"),
Target: check.MustAbs("/dev/null"), Target: MustAbs("/dev/null"),
Flags: std.BindDevice, Flags: BindDevice,
}, []stub.Call{ }, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil), call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
}, nil, []stub.Call{ }, nil, []stub.Call{
call("stat", stub.ExpectArgs{"/host/dev/null"}, isDirFi(false), nil), 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("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), call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4001), false}, nil, nil),
}, nil}, }, nil},
{"success device", new(Params), &BindMountOp{ {"success device", new(Params), &BindMountOp{
Source: check.MustAbs("/dev/null"), Source: MustAbs("/dev/null"),
Target: check.MustAbs("/dev/null"), Target: MustAbs("/dev/null"),
Flags: std.BindWritable | std.BindDevice, Flags: BindWritable | BindDevice,
}, []stub.Call{ }, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil), call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
}, nil, []stub.Call{ }, nil, []stub.Call{
call("stat", stub.ExpectArgs{"/host/dev/null"}, isDirFi(false), nil), 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("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), call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4000), false}, nil, nil),
}, nil}, }, nil},
{"evalSymlinks", new(Params), &BindMountOp{ {"evalSymlinks", new(Params), &BindMountOp{
Source: check.MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: check.MustAbs("/bin/"), Target: MustAbs("/bin/"),
}, []stub.Call{ }, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", stub.UniqueError(3)), call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", stub.UniqueError(3)),
}, stub.UniqueError(3), nil, nil}, }, stub.UniqueError(3), nil, nil},
{"stat", new(Params), &BindMountOp{ {"stat", new(Params), &BindMountOp{
Source: check.MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: check.MustAbs("/bin/"), Target: MustAbs("/bin/"),
}, []stub.Call{ }, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil), call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []stub.Call{ }, nil, []stub.Call{
@@ -119,8 +111,8 @@ func TestBindMountOp(t *testing.T) {
}, stub.UniqueError(2)}, }, stub.UniqueError(2)},
{"mkdirAll", new(Params), &BindMountOp{ {"mkdirAll", new(Params), &BindMountOp{
Source: check.MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: check.MustAbs("/bin/"), Target: MustAbs("/bin/"),
}, []stub.Call{ }, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil), call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []stub.Call{ }, nil, []stub.Call{
@@ -129,47 +121,30 @@ func TestBindMountOp(t *testing.T) {
}, stub.UniqueError(1)}, }, stub.UniqueError(1)},
{"bindMount", new(Params), &BindMountOp{ {"bindMount", new(Params), &BindMountOp{
Source: check.MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: check.MustAbs("/bin/"), Target: MustAbs("/bin/"),
}, []stub.Call{ }, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil), call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []stub.Call{ }, nil, []stub.Call{
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil), call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, 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)), call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, stub.UniqueError(0)),
}, 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{ {"success", new(Params), &BindMountOp{
Source: check.MustAbs("/bin/"), Source: MustAbs("/bin/"),
Target: check.MustAbs("/bin/"), Target: MustAbs("/bin/"),
}, []stub.Call{ }, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil), call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []stub.Call{ }, nil, []stub.Call{
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil), call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, 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), call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
}, nil}, }, nil},
}) })
t.Run("unreachable", func(t *testing.T) { t.Run("unreachable", func(t *testing.T) {
t.Parallel()
t.Run("nil sourceFinal not optional", func(t *testing.T) { t.Run("nil sourceFinal not optional", func(t *testing.T) {
t.Parallel()
wantErr := OpStateError("bind") wantErr := OpStateError("bind")
if err := new(BindMountOp).apply(nil, nil); !errors.Is(err, wantErr) { if err := new(BindMountOp).apply(nil, nil); !errors.Is(err, wantErr) {
t.Errorf("apply: error = %v, want %v", err, wantErr) t.Errorf("apply: error = %v, want %v", err, wantErr)
@@ -180,21 +155,21 @@ func TestBindMountOp(t *testing.T) {
checkOpsValid(t, []opValidTestCase{ checkOpsValid(t, []opValidTestCase{
{"nil", (*BindMountOp)(nil), false}, {"nil", (*BindMountOp)(nil), false},
{"zero", new(BindMountOp), false}, {"zero", new(BindMountOp), false},
{"nil source", &BindMountOp{Target: check.MustAbs("/")}, false}, {"nil source", &BindMountOp{Target: MustAbs("/")}, false},
{"nil target", &BindMountOp{Source: check.MustAbs("/")}, false}, {"nil target", &BindMountOp{Source: MustAbs("/")}, false},
{"flag optional ensure", &BindMountOp{Source: check.MustAbs("/"), Target: check.MustAbs("/"), Flags: std.BindOptional | std.BindEnsure}, false}, {"flag optional ensure", &BindMountOp{Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindOptional | BindEnsure}, false},
{"valid", &BindMountOp{Source: check.MustAbs("/"), Target: check.MustAbs("/")}, true}, {"valid", &BindMountOp{Source: MustAbs("/"), Target: MustAbs("/")}, true},
}) })
checkOpsBuilder(t, []opsBuilderTestCase{ checkOpsBuilder(t, []opsBuilderTestCase{
{"autoetc", new(Ops).Bind( {"autoetc", new(Ops).Bind(
check.MustAbs("/etc/"), MustAbs("/etc/"),
check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"), MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
0, 0,
), Ops{ ), Ops{
&BindMountOp{ &BindMountOp{
Source: check.MustAbs("/etc/"), Source: MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"), Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, },
}}, }},
}) })
@@ -203,45 +178,45 @@ func TestBindMountOp(t *testing.T) {
{"zero", new(BindMountOp), new(BindMountOp), false}, {"zero", new(BindMountOp), new(BindMountOp), false},
{"internal ne", &BindMountOp{ {"internal ne", &BindMountOp{
Source: check.MustAbs("/etc/"), Source: MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"), Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, &BindMountOp{ }, &BindMountOp{
Source: check.MustAbs("/etc/"), Source: MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"), Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
sourceFinal: check.MustAbs("/etc/"), sourceFinal: MustAbs("/etc/"),
}, true}, }, true},
{"flags differs", &BindMountOp{ {"flags differs", &BindMountOp{
Source: check.MustAbs("/etc/"), Source: MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"), Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, &BindMountOp{ }, &BindMountOp{
Source: check.MustAbs("/etc/"), Source: MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"), Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Flags: std.BindOptional, Flags: BindOptional,
}, false}, }, false},
{"source differs", &BindMountOp{ {"source differs", &BindMountOp{
Source: check.MustAbs("/.hakurei/etc/"), Source: MustAbs("/.hakurei/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"), Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, &BindMountOp{ }, &BindMountOp{
Source: check.MustAbs("/etc/"), Source: MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"), Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, false}, }, false},
{"target differs", &BindMountOp{ {"target differs", &BindMountOp{
Source: check.MustAbs("/etc/"), Source: MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"), Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, &BindMountOp{ }, &BindMountOp{
Source: check.MustAbs("/etc/"), Source: MustAbs("/etc/"),
Target: check.MustAbs("/etc/"), Target: MustAbs("/etc/"),
}, false}, }, false},
{"equals", &BindMountOp{ {"equals", &BindMountOp{
Source: check.MustAbs("/etc/"), Source: MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"), Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, &BindMountOp{ }, &BindMountOp{
Source: check.MustAbs("/etc/"), Source: MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"), Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, true}, }, true},
}) })
@@ -249,14 +224,14 @@ func TestBindMountOp(t *testing.T) {
{"invalid", new(BindMountOp), "mounting", "<invalid>"}, {"invalid", new(BindMountOp), "mounting", "<invalid>"},
{"autoetc", &BindMountOp{ {"autoetc", &BindMountOp{
Source: check.MustAbs("/etc/"), Source: MustAbs("/etc/"),
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"), Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, "mounting", `"/etc/" on "/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659" flags 0x0`}, }, "mounting", `"/etc/" on "/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659" flags 0x0`},
{"hostdev", &BindMountOp{ {"hostdev", &BindMountOp{
Source: check.MustAbs("/dev/"), Source: MustAbs("/dev/"),
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Flags: std.BindWritable | std.BindDevice, Flags: BindWritable | BindDevice,
}, "mounting", `"/dev/" flags 0x6`}, }, "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" "fmt"
"path" "path"
. "syscall" . "syscall"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
) )
func init() { gob.Register(new(MountDevOp)) } func init() { gob.Register(new(MountDevOp)) }
// Dev appends an [Op] that mounts a subset of host /dev. // 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}) *f = append(*f, &MountDevOp{target, mqueue, false})
return f return f
} }
// DevWritable appends an [Op] that mounts a writable subset of host /dev. // 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]. // 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}) *f = append(*f, &MountDevOp{target, mqueue, true})
return f 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 Mqueue is true, a private instance of [FstypeMqueue] is mounted.
// If Write is true, the resulting mount point is left writable. // If Write is true, the resulting mount point is left writable.
type MountDevOp struct { type MountDevOp struct {
Target *check.Absolute Target *Absolute
Mqueue bool Mqueue bool
Write bool Write bool
} }
@@ -49,25 +46,25 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
return err return err
} }
if err := k.bindMount( if err := k.bindMount(
state, toHost(FHSDev+name),
toHost(fhs.Dev+name),
targetPath, targetPath,
0, 0,
true,
); err != nil { ); err != nil {
return err return err
} }
} }
for i, name := range []string{"stdin", "stdout", "stderr"} { for i, name := range []string{"stdin", "stdout", "stderr"} {
if err := k.symlink( if err := k.symlink(
fhs.Proc+"self/fd/"+string(rune(i+'0')), FHSProc+"self/fd/"+string(rune(i+'0')),
path.Join(target, name), path.Join(target, name),
); err != nil { ); err != nil {
return err return err
} }
} }
for _, pair := range [][2]string{ for _, pair := range [][2]string{
{fhs.Proc + "self/fd", "fd"}, {FHSProc + "self/fd", "fd"},
{fhs.Proc + "kcore", "core"}, {FHSProc + "kcore", "core"},
{"pts/ptmx", "ptmx"}, {"pts/ptmx", "ptmx"},
} { } {
if err := k.symlink(pair[0], path.Join(target, pair[1])); err != nil { if err := k.symlink(pair[0], path.Join(target, pair[1])); err != nil {
@@ -97,10 +94,10 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
if name, err := k.readlink(hostProc.stdout()); err != nil { if name, err := k.readlink(hostProc.stdout()); err != nil {
return err return err
} else if err = k.bindMount( } else if err = k.bindMount(
state,
toHost(name), toHost(name),
consolePath, consolePath,
0, 0,
false,
); err != nil { ); err != nil {
return err return err
} }
@@ -121,12 +118,11 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
return nil return nil
} }
if err := k.remount(state, target, MS_RDONLY); err != nil { if err := k.remount(target, MS_RDONLY); err != nil {
return err return err
} }
return k.mountTmpfs(SourceTmpfs, devShmPath, MS_NOSUID|MS_NODEV, 0, 01777) return k.mountTmpfs(SourceTmpfs, devShmPath, MS_NOSUID|MS_NODEV, 0, 01777)
} }
func (d *MountDevOp) late(*setupState, syscallDispatcher) error { return nil }
func (d *MountDevOp) Is(op Op) bool { func (d *MountDevOp) Is(op Op) bool {
vd, ok := op.(*MountDevOp) vd, ok := op.(*MountDevOp)
@@ -135,7 +131,7 @@ func (d *MountDevOp) Is(op Op) bool {
d.Mqueue == vd.Mqueue && d.Mqueue == vd.Mqueue &&
d.Write == vd.Write d.Write == vd.Write
} }
func (*MountDevOp) prefix() (string, bool) { return "mounting", true } func (*MountDevOp) prefix() string { return "mounting" }
func (d *MountDevOp) String() string { func (d *MountDevOp) String() string {
if d.Mqueue { if d.Mqueue {
return fmt.Sprintf("dev on %q with mqueue", d.Target) return fmt.Sprintf("dev on %q with mqueue", d.Target)

View File

@@ -4,23 +4,20 @@ import (
"os" "os"
"testing" "testing"
"hakurei.app/container/check"
"hakurei.app/container/stub" "hakurei.app/container/stub"
) )
func TestMountDevOp(t *testing.T) { func TestMountDevOp(t *testing.T) {
t.Parallel()
checkOpBehaviour(t, []opBehaviourTestCase{ checkOpBehaviour(t, []opBehaviourTestCase{
{"mountTmpfs", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"mountTmpfs", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, stub.UniqueError(27)), call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, stub.UniqueError(27)),
}, stub.UniqueError(27)}, }, stub.UniqueError(27)},
{"ensureFile null", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"ensureFile null", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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)}, }, stub.UniqueError(26)},
{"bindMount null", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"bindMount null", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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)}, }, stub.UniqueError(25)},
{"ensureFile zero", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"ensureFile zero", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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)}, }, stub.UniqueError(24)},
{"bindMount zero", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"bindMount zero", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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)}, }, stub.UniqueError(23)},
{"ensureFile full", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"ensureFile full", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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)}, }, stub.UniqueError(22)},
{"bindMount full", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"bindMount full", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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)}, }, stub.UniqueError(21)},
{"ensureFile random", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"ensureFile random", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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)}, }, stub.UniqueError(20)},
{"bindMount random", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"bindMount random", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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)}, }, stub.UniqueError(19)},
{"ensureFile urandom", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"ensureFile urandom", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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)}, }, stub.UniqueError(18)},
{"bindMount urandom", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"bindMount urandom", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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)}, }, stub.UniqueError(17)},
{"ensureFile tty", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"ensureFile tty", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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)}, }, stub.UniqueError(16)},
{"bindMount tty", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"bindMount tty", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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)}, }, stub.UniqueError(15)},
{"symlink stdin", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"symlink stdin", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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)}, }, stub.UniqueError(14)},
{"symlink stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"symlink stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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)}, }, stub.UniqueError(13)},
{"symlink stderr", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"symlink stderr", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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)}, }, stub.UniqueError(12)},
{"symlink fd", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"symlink fd", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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)}, }, stub.UniqueError(11)},
{"symlink kcore", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"symlink kcore", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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)}, }, stub.UniqueError(10)},
{"symlink ptmx", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"symlink ptmx", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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)}, }, stub.UniqueError(9)},
{"mkdir shm", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"mkdir shm", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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)}, }, stub.UniqueError(8)},
{"mkdir devpts", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"mkdir devpts", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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)}, }, stub.UniqueError(7)},
{"mount devpts", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"mount devpts", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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)}, }, stub.UniqueError(6)},
{"ensureFile stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"ensureFile stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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)}, }, stub.UniqueError(5)},
{"readlink stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"readlink stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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)}, }, stub.UniqueError(4)},
{"bindMount stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"bindMount stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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)}, }, stub.UniqueError(3)},
{"mkdir mqueue", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"mkdir mqueue", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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)}, }, stub.UniqueError(2)},
{"mount mqueue", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"mount mqueue", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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)}, }, stub.UniqueError(1)},
{"success no session", &Params{ParentPerm: 0755}, &MountDevOp{ {"success no session", &Params{ParentPerm: 0755}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
Write: true, Write: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
@@ -589,7 +586,7 @@ func TestMountDevOp(t *testing.T) {
}, nil}, }, nil},
{"success no tty", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"success no tty", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
Write: true, Write: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
@@ -621,7 +618,7 @@ func TestMountDevOp(t *testing.T) {
}, nil}, }, nil},
{"remount", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"remount", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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), 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)}, }, stub.UniqueError(0)},
{"success no mqueue", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"success no mqueue", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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), 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}, }, nil},
{"success rw", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"success rw", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
Write: true, Write: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
@@ -721,7 +718,7 @@ func TestMountDevOp(t *testing.T) {
}, nil}, }, nil},
{"success", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ {"success", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
call("mountTmpfs", stub.ExpectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil), 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{ checkOpsValid(t, []opValidTestCase{
{"nil", (*MountDevOp)(nil), false}, {"nil", (*MountDevOp)(nil), false},
{"zero", new(MountDevOp), false}, {"zero", new(MountDevOp), false},
{"valid", &MountDevOp{Target: check.MustAbs("/dev/")}, true}, {"valid", &MountDevOp{Target: MustAbs("/dev/")}, true},
}) })
checkOpsBuilder(t, []opsBuilderTestCase{ checkOpsBuilder(t, []opsBuilderTestCase{
{"dev", new(Ops).Dev(check.MustAbs("/dev/"), true), Ops{ {"dev", new(Ops).Dev(MustAbs("/dev/"), true), Ops{
&MountDevOp{ &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, },
}}, }},
{"dev writable", new(Ops).DevWritable(check.MustAbs("/.hakurei/dev/"), false), Ops{ {"dev writable", new(Ops).DevWritable(MustAbs("/.hakurei/dev/"), false), Ops{
&MountDevOp{ &MountDevOp{
Target: check.MustAbs("/.hakurei/dev/"), Target: MustAbs("/.hakurei/dev/"),
Write: true, Write: true,
}, },
}}, }},
@@ -783,46 +780,46 @@ func TestMountDevOp(t *testing.T) {
{"zero", new(MountDevOp), new(MountDevOp), false}, {"zero", new(MountDevOp), new(MountDevOp), false},
{"write differs", &MountDevOp{ {"write differs", &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, &MountDevOp{ }, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
Write: true, Write: true,
}, false}, }, false},
{"mqueue differs", &MountDevOp{ {"mqueue differs", &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
}, &MountDevOp{ }, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, false}, }, false},
{"target differs", &MountDevOp{ {"target differs", &MountDevOp{
Target: check.MustAbs("/"), Target: MustAbs("/"),
Mqueue: true, Mqueue: true,
}, &MountDevOp{ }, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, false}, }, false},
{"equals", &MountDevOp{ {"equals", &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, &MountDevOp{ }, &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, true}, }, true},
}) })
checkOpMeta(t, []opMetaTestCase{ checkOpMeta(t, []opMetaTestCase{
{"mqueue", &MountDevOp{ {"mqueue", &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
Mqueue: true, Mqueue: true,
}, "mounting", `dev on "/dev/" with mqueue`}, }, "mounting", `dev on "/dev/" with mqueue`},
{"dev", &MountDevOp{ {"dev", &MountDevOp{
Target: check.MustAbs("/dev/"), Target: MustAbs("/dev/"),
}, "mounting", `dev on "/dev/"`}, }, "mounting", `dev on "/dev/"`},
}) })
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,8 +7,6 @@ import (
"os" "os"
"strconv" "strconv"
. "syscall" . "syscall"
"hakurei.app/container/check"
) )
func init() { gob.Register(new(MountTmpfsOp)) } 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]. // 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}) *f = append(*f, &MountTmpfsOp{SourceTmpfsEphemeral, target, MS_NOSUID | MS_NODEV, size, perm})
return f return f
} }
// Readonly appends an [Op] that mounts read-only tmpfs on container path [MountTmpfsOp.Path]. // 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}) *f = append(*f, &MountTmpfsOp{SourceTmpfsReadonly, target, MS_RDONLY | MS_NOSUID | MS_NODEV, 0, perm})
return f return f
} }
@@ -34,7 +32,7 @@ func (f *Ops) Readonly(target *check.Absolute, perm os.FileMode) *Ops {
// MountTmpfsOp mounts [FstypeTmpfs] on container Path. // MountTmpfsOp mounts [FstypeTmpfs] on container Path.
type MountTmpfsOp struct { type MountTmpfsOp struct {
FSName string FSName string
Path *check.Absolute Path *Absolute
Flags uintptr Flags uintptr
Size int Size int
Perm os.FileMode 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) return k.mountTmpfs(t.FSName, toSysroot(t.Path.String()), t.Flags, t.Size, t.Perm)
} }
func (t *MountTmpfsOp) late(*setupState, syscallDispatcher) error { return nil }
func (t *MountTmpfsOp) Is(op Op) bool { func (t *MountTmpfsOp) Is(op Op) bool {
vt, ok := op.(*MountTmpfsOp) vt, ok := op.(*MountTmpfsOp)
@@ -59,5 +56,5 @@ func (t *MountTmpfsOp) Is(op Op) bool {
t.Size == vt.Size && t.Size == vt.Size &&
t.Perm == vt.Perm 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) } 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" "syscall"
"testing" "testing"
"hakurei.app/container/check"
"hakurei.app/container/stub" "hakurei.app/container/stub"
) )
func TestMountTmpfsOp(t *testing.T) { func TestMountTmpfsOp(t *testing.T) {
t.Parallel()
t.Run("size error", func(t *testing.T) { t.Run("size error", func(t *testing.T) {
t.Parallel()
tmpfsSizeError := TmpfsSizeError(-1) tmpfsSizeError := TmpfsSizeError(-1)
want := "tmpfs size -1 out of bounds" want := "tmpfs size -1 out of bounds"
if got := tmpfsSizeError.Error(); got != want { if got := tmpfsSizeError.Error(); got != want {
@@ -28,7 +24,7 @@ func TestMountTmpfsOp(t *testing.T) {
{"success", new(Params), &MountTmpfsOp{ {"success", new(Params), &MountTmpfsOp{
FSName: "ephemeral", FSName: "ephemeral",
Path: check.MustAbs("/run/user/1000/"), Path: MustAbs("/run/user/1000/"),
Size: 1 << 10, Size: 1 << 10,
Perm: 0700, Perm: 0700,
}, nil, nil, []stub.Call{ }, nil, nil, []stub.Call{
@@ -46,19 +42,19 @@ func TestMountTmpfsOp(t *testing.T) {
{"nil", (*MountTmpfsOp)(nil), false}, {"nil", (*MountTmpfsOp)(nil), false},
{"zero", new(MountTmpfsOp), false}, {"zero", new(MountTmpfsOp), false},
{"nil path", &MountTmpfsOp{FSName: "tmpfs"}, false}, {"nil path", &MountTmpfsOp{FSName: "tmpfs"}, false},
{"zero fsname", &MountTmpfsOp{Path: check.MustAbs("/tmp/")}, false}, {"zero fsname", &MountTmpfsOp{Path: MustAbs("/tmp/")}, false},
{"valid", &MountTmpfsOp{FSName: "tmpfs", Path: check.MustAbs("/tmp/")}, true}, {"valid", &MountTmpfsOp{FSName: "tmpfs", Path: MustAbs("/tmp/")}, true},
}) })
checkOpsBuilder(t, []opsBuilderTestCase{ checkOpsBuilder(t, []opsBuilderTestCase{
{"runtime", new(Ops).Tmpfs( {"runtime", new(Ops).Tmpfs(
check.MustAbs("/run/user"), MustAbs("/run/user"),
1<<10, 1<<10,
0755, 0755,
), Ops{ ), Ops{
&MountTmpfsOp{ &MountTmpfsOp{
FSName: "ephemeral", FSName: "ephemeral",
Path: check.MustAbs("/run/user"), Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV, Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10, Size: 1 << 10,
Perm: 0755, Perm: 0755,
@@ -66,12 +62,12 @@ func TestMountTmpfsOp(t *testing.T) {
}}, }},
{"nscd", new(Ops).Readonly( {"nscd", new(Ops).Readonly(
check.MustAbs("/var/run/nscd"), MustAbs("/var/run/nscd"),
0755, 0755,
), Ops{ ), Ops{
&MountTmpfsOp{ &MountTmpfsOp{
FSName: "readonly", FSName: "readonly",
Path: check.MustAbs("/var/run/nscd"), Path: MustAbs("/var/run/nscd"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV | syscall.MS_RDONLY, Flags: syscall.MS_NOSUID | syscall.MS_NODEV | syscall.MS_RDONLY,
Perm: 0755, Perm: 0755,
}, },
@@ -83,13 +79,13 @@ func TestMountTmpfsOp(t *testing.T) {
{"fsname differs", &MountTmpfsOp{ {"fsname differs", &MountTmpfsOp{
FSName: "readonly", FSName: "readonly",
Path: check.MustAbs("/run/user"), Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV, Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10, Size: 1 << 10,
Perm: 0755, Perm: 0755,
}, &MountTmpfsOp{ }, &MountTmpfsOp{
FSName: "ephemeral", FSName: "ephemeral",
Path: check.MustAbs("/run/user"), Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV, Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10, Size: 1 << 10,
Perm: 0755, Perm: 0755,
@@ -97,13 +93,13 @@ func TestMountTmpfsOp(t *testing.T) {
{"path differs", &MountTmpfsOp{ {"path differs", &MountTmpfsOp{
FSName: "ephemeral", FSName: "ephemeral",
Path: check.MustAbs("/run/user/differs"), Path: MustAbs("/run/user/differs"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV, Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10, Size: 1 << 10,
Perm: 0755, Perm: 0755,
}, &MountTmpfsOp{ }, &MountTmpfsOp{
FSName: "ephemeral", FSName: "ephemeral",
Path: check.MustAbs("/run/user"), Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV, Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10, Size: 1 << 10,
Perm: 0755, Perm: 0755,
@@ -111,13 +107,13 @@ func TestMountTmpfsOp(t *testing.T) {
{"flags differs", &MountTmpfsOp{ {"flags differs", &MountTmpfsOp{
FSName: "ephemeral", FSName: "ephemeral",
Path: check.MustAbs("/run/user"), Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV | syscall.MS_RDONLY, Flags: syscall.MS_NOSUID | syscall.MS_NODEV | syscall.MS_RDONLY,
Size: 1 << 10, Size: 1 << 10,
Perm: 0755, Perm: 0755,
}, &MountTmpfsOp{ }, &MountTmpfsOp{
FSName: "ephemeral", FSName: "ephemeral",
Path: check.MustAbs("/run/user"), Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV, Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10, Size: 1 << 10,
Perm: 0755, Perm: 0755,
@@ -125,13 +121,13 @@ func TestMountTmpfsOp(t *testing.T) {
{"size differs", &MountTmpfsOp{ {"size differs", &MountTmpfsOp{
FSName: "ephemeral", FSName: "ephemeral",
Path: check.MustAbs("/run/user"), Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV, Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1, Size: 1,
Perm: 0755, Perm: 0755,
}, &MountTmpfsOp{ }, &MountTmpfsOp{
FSName: "ephemeral", FSName: "ephemeral",
Path: check.MustAbs("/run/user"), Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV, Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10, Size: 1 << 10,
Perm: 0755, Perm: 0755,
@@ -139,13 +135,13 @@ func TestMountTmpfsOp(t *testing.T) {
{"perm differs", &MountTmpfsOp{ {"perm differs", &MountTmpfsOp{
FSName: "ephemeral", FSName: "ephemeral",
Path: check.MustAbs("/run/user"), Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV, Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10, Size: 1 << 10,
Perm: 0700, Perm: 0700,
}, &MountTmpfsOp{ }, &MountTmpfsOp{
FSName: "ephemeral", FSName: "ephemeral",
Path: check.MustAbs("/run/user"), Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV, Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10, Size: 1 << 10,
Perm: 0755, Perm: 0755,
@@ -153,13 +149,13 @@ func TestMountTmpfsOp(t *testing.T) {
{"equals", &MountTmpfsOp{ {"equals", &MountTmpfsOp{
FSName: "ephemeral", FSName: "ephemeral",
Path: check.MustAbs("/run/user"), Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV, Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10, Size: 1 << 10,
Perm: 0755, Perm: 0755,
}, &MountTmpfsOp{ }, &MountTmpfsOp{
FSName: "ephemeral", FSName: "ephemeral",
Path: check.MustAbs("/run/user"), Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV, Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10, Size: 1 << 10,
Perm: 0755, Perm: 0755,
@@ -169,7 +165,7 @@ func TestMountTmpfsOp(t *testing.T) {
checkOpMeta(t, []opMetaTestCase{ checkOpMeta(t, []opMetaTestCase{
{"runtime", &MountTmpfsOp{ {"runtime", &MountTmpfsOp{
FSName: "ephemeral", FSName: "ephemeral",
Path: check.MustAbs("/run/user"), Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV, Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10, Size: 1 << 10,
Perm: 0755, Perm: 0755,

View File

@@ -5,7 +5,7 @@ import (
"syscall" "syscall"
"unsafe" "unsafe"
"hakurei.app/container/std" "hakurei.app/container/seccomp"
) )
// include/uapi/linux/landlock.h // include/uapi/linux/landlock.h
@@ -14,8 +14,7 @@ const (
LANDLOCK_CREATE_RULESET_VERSION = 1 << iota LANDLOCK_CREATE_RULESET_VERSION = 1 << iota
) )
// LandlockAccessFS is bitmask of handled filesystem actions. type LandlockAccessFS uintptr
type LandlockAccessFS uint64
const ( const (
LANDLOCK_ACCESS_FS_EXECUTE LandlockAccessFS = 1 << iota 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 uintptr
type LandlockAccessNet uint64
const ( const (
LANDLOCK_ACCESS_NET_BIND_TCP LandlockAccessNet = 1 << iota 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 uintptr
type LandlockScope uint64
const ( const (
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET LandlockScope = 1 << iota 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 { type RulesetAttr struct {
// Bitmask of handled filesystem actions. // Bitmask of handled filesystem actions.
HandledAccessFS LandlockAccessFS HandledAccessFS LandlockAccessFS
@@ -216,7 +212,7 @@ func (rulesetAttr *RulesetAttr) Create(flags uintptr) (fd int, err error) {
size = unsafe.Sizeof(*rulesetAttr) 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) fd = int(rulesetFd)
err = errno err = errno
@@ -235,7 +231,7 @@ func LandlockGetABI() (int, error) {
} }
func LandlockRestrictSelf(rulesetFd int, flags uintptr) 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 { if r != 0 {
return errno return errno
} }

View File

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

View File

@@ -4,10 +4,10 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"strings"
. "syscall" . "syscall"
"hakurei.app/container/vfs" "hakurei.app/container/vfs"
"hakurei.app/message"
) )
/* /*
@@ -59,6 +59,7 @@ const (
FstypeNULL = zeroString FstypeNULL = zeroString
// FstypeProc represents the proc pseudo-filesystem. // FstypeProc represents the proc pseudo-filesystem.
// A fully visible instance of proc must be available in the mount namespace for proc to be mounted. // 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" FstypeProc = "proc"
// FstypeDevpts represents the devpts pseudo-filesystem. // FstypeDevpts represents the devpts pseudo-filesystem.
// This type of filesystem is usually mounted on /dev/pts. // 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. // OptionOverlayUserxattr represents the userxattr option of the overlay pseudo-filesystem.
// Use the "user.overlay." xattr namespace instead of "trusted.overlay.". // Use the "user.overlay." xattr namespace instead of "trusted.overlay.".
OptionOverlayUserxattr = "userxattr" 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. // 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 // 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 { if err := p.k.mount(source, target, FstypeNULL, MS_SILENT|MS_BIND|flags&MS_REC, zeroString); err != nil {
return err 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. // 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 // syscallDispatcher methods bindMount, remount must not be called from this function
var targetFinal string var targetFinal string
@@ -107,7 +122,7 @@ func (p *procPaths) remount(msg message.Msg, target string, flags uintptr) error
} else { } else {
targetFinal = v targetFinal = v
if targetFinal != target { 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 return err
} }
if err = remountWithFlags(p.k, msg, n, mf); err != nil { if err = remountWithFlags(p.k, n, mf); err != nil {
return err return err
} }
if flags&MS_REC == 0 { if flags&MS_REC == 0 {
@@ -150,7 +165,7 @@ func (p *procPaths) remount(msg message.Msg, target string, flags uintptr) error
continue 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 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]. // 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 // syscallDispatcher methods bindMount, remount must not be called from this function
kf, unmatched := n.Flags() kf, unmatched := n.Flags()
if len(unmatched) != 0 { if len(unmatched) != 0 {
msg.Verbosef("unmatched vfs options: %q", unmatched) k.verbosef("unmatched vfs options: %q", unmatched)
} }
if kf&mf != mf { if kf&mf != mf {
@@ -199,3 +214,20 @@ func parentPerm(perm os.FileMode) os.FileMode {
} }
return os.FileMode(pperm) 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) { func TestBindMount(t *testing.T) {
t.Parallel()
checkSimple(t, "bindMount", []simpleTestCase{ checkSimple(t, "bindMount", []simpleTestCase{
{"mount", func(k *kstub) error { {"mount", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).bindMount(nil, "/host/nix", "/sysroot/nix", syscall.MS_RDONLY) return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY, true)
}, stub.Expect{Calls: []stub.Call{ }, 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)), call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, stub.UniqueError(0xbad)),
}}, stub.UniqueError(0xbad)}, }}, stub.UniqueError(0xbad)},
{"success ne", func(k *kstub) error { {"success ne", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).bindMount(k, "/host/nix", "/sysroot/.host-nix", syscall.MS_RDONLY) return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/.host-nix", syscall.MS_RDONLY, false)
}, stub.Expect{Calls: []stub.Call{ }, 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("mount", stub.ExpectArgs{"/host/nix", "/sysroot/.host-nix", "", uintptr(0x9000), ""}, nil, nil),
call("remount", stub.ExpectArgs{"/sysroot/.host-nix", uintptr(1)}, nil, nil), call("remount", stub.ExpectArgs{"/sysroot/.host-nix", uintptr(1)}, nil, nil),
}}, nil}, }}, nil},
{"success", func(k *kstub) error { {"success", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).bindMount(k, "/host/nix", "/sysroot/nix", syscall.MS_RDONLY) return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY, true)
}, stub.Expect{Calls: []stub.Call{ }, 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("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, nil),
call("remount", stub.ExpectArgs{"/sysroot/nix", uintptr(1)}, nil, nil), call("remount", stub.ExpectArgs{"/sysroot/nix", uintptr(1)}, nil, nil),
}}, nil}, }}, nil},
@@ -36,8 +37,6 @@ func TestBindMount(t *testing.T) {
} }
func TestRemount(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 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 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 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 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 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 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 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=10000,gid=10000 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 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 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 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` 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{ checkSimple(t, "remount", []simpleTestCase{
{"evalSymlinks", func(k *kstub) error { {"evalSymlinks", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", stub.UniqueError(6)), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", stub.UniqueError(6)),
}}, stub.UniqueError(6)}, }}, stub.UniqueError(6)},
{"open", func(k *kstub) error { {"open", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), 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)}}, }}, &os.PathError{Op: "open", Path: "/sysroot/nix", Err: stub.UniqueError(5)}},
{"readlink", func(k *kstub) error { {"readlink", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdead, nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", stub.UniqueError(4)), call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", stub.UniqueError(4)),
}}, stub.UniqueError(4)}, }}, stub.UniqueError(4)},
{"close", func(k *kstub) error { {"close", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdead, nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", nil), call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdead}, nil, stub.UniqueError(3)), call("close", stub.ExpectArgs{0xdeadbeef}, nil, stub.UniqueError(3)),
}}, &os.PathError{Op: "close", Path: "/sysroot/nix", Err: stub.UniqueError(3)}}, }}, &os.PathError{Op: "close", Path: "/sysroot/nix", Err: stub.UniqueError(3)}},
{"mountinfo no match", func(k *kstub) error { {"mountinfo no match", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount(k, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/.hakurei", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/.hakurei", nil),
call("verbosef", stub.ExpectArgs{"target resolves to %q", []any{"/sysroot/.hakurei"}}, nil, 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("open", stub.ExpectArgs{"/sysroot/.hakurei", 0x280000, uint32(0)}, 0xdeadbeef, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/.hakurei", nil), call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/.hakurei", nil),
call("close", stub.ExpectArgs{0xdead}, nil, nil), call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil), call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
}}, &vfs.DecoderError{Op: "unfold", Line: -1, Err: vfs.UnfoldTargetError("/sysroot/.hakurei")}}, }}, &vfs.DecoderError{Op: "unfold", Line: -1, Err: vfs.UnfoldTargetError("/sysroot/.hakurei")}},
{"mountinfo", func(k *kstub) error { {"mountinfo", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdead, nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", nil), call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdead}, nil, nil), call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile("\x00"), nil), call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile("\x00"), nil),
}}, &vfs.DecoderError{Op: "parse", Line: 0, Err: vfs.ErrMountInfoFields}}, }}, &vfs.DecoderError{Op: "parse", Line: 0, Err: vfs.ErrMountInfoFields}},
{"mount", func(k *kstub) error { {"mount", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdead, nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", nil), call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdead}, nil, nil), call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), 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)), call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, stub.UniqueError(2)),
}}, stub.UniqueError(2)}, }}, stub.UniqueError(2)},
{"mount propagate", func(k *kstub) error { {"mount propagate", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdead, nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", nil), call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdead}, nil, nil), call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), 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", "", uintptr(0x209027), ""}, nil, nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, stub.UniqueError(1)), call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, stub.UniqueError(1)),
}}, stub.UniqueError(1)}, }}, stub.UniqueError(1)},
{"success toplevel", func(k *kstub) error { {"success toplevel", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/bin", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/bin", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/bin"}, "/sysroot/bin", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/bin"}, "/sysroot/bin", nil),
call("open", stub.ExpectArgs{"/sysroot/bin", 0x280000, uint32(0)}, 0xbabe, 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), call("mount", stub.ExpectArgs{"none", "/sysroot/bin", "", uintptr(0x209027), ""}, nil, nil),
}}, nil}, }}, nil},
{"success EACCES", func(k *kstub) error { {"success EACCES", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdead, nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", nil), call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdead}, nil, nil), call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), 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", "", 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/.ro-store", "", uintptr(0x209027), ""}, nil, syscall.EACCES),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil), call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil),
}}, nil}, }}, nil},
{"success no propagate", func(k *kstub) error { {"success no propagate", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdead, nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", nil), call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdead}, nil, nil), call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), 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", "", uintptr(0x209027), ""}, nil, nil),
}}, nil}, }}, nil},
{"success case sensitive", func(k *kstub) error { {"success case sensitive", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdead, nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", nil), call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdead}, nil, nil), call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), 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", "", uintptr(0x209027), ""}, nil, nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", 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), call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil),
}}, nil}, }}, nil},
{"success", func(k *kstub) error { {"success", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount(k, "/sysroot/.nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount("/sysroot/.nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/.nix"}, "/sysroot/NIX", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/.nix"}, "/sysroot/NIX", nil),
call("verbosef", stub.ExpectArgs{"target resolves to %q", []any{"/sysroot/NIX"}}, nil, 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("open", stub.ExpectArgs{"/sysroot/NIX", 0x280000, uint32(0)}, 0xdeadbeef, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", nil), call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdead}, nil, nil), call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil),
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), 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", "", uintptr(0x209027), ""}, nil, nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", 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) { func TestRemountWithFlags(t *testing.T) {
t.Parallel()
checkSimple(t, "remountWithFlags", []simpleTestCase{ checkSimple(t, "remountWithFlags", []simpleTestCase{
{"noop unmatched", func(k *kstub) error { {"noop unmatched", func(k syscallDispatcher) error {
return remountWithFlags(k, k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime,cat"}}, 0) return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime,cat"}}, 0)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("verbosef", stub.ExpectArgs{"unmatched vfs options: %q", []any{[]string{"cat"}}}, nil, nil), call("verbosef", stub.ExpectArgs{"unmatched vfs options: %q", []any{[]string{"cat"}}}, nil, nil),
}}, nil}, }}, nil},
{"noop", func(k *kstub) error { {"noop", func(k syscallDispatcher) error {
return remountWithFlags(k, nil, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, 0) return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, 0)
}, stub.Expect{}, nil}, }, stub.Expect{}, nil},
{"success", func(k *kstub) error { {"success", func(k syscallDispatcher) error {
return remountWithFlags(k, nil, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, syscall.MS_RDONLY) return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, syscall.MS_RDONLY)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("mount", stub.ExpectArgs{"none", "", "", uintptr(0x209021), ""}, nil, nil), call("mount", stub.ExpectArgs{"none", "", "", uintptr(0x209021), ""}, nil, nil),
}}, nil}, }}, nil},
@@ -242,23 +239,21 @@ func TestRemountWithFlags(t *testing.T) {
} }
func TestMountTmpfs(t *testing.T) { func TestMountTmpfs(t *testing.T) {
t.Parallel()
checkSimple(t, "mountTmpfs", []simpleTestCase{ 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) return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, stub.UniqueError(0)), call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, stub.UniqueError(0)),
}}, 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) return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 0, 0710)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0750)}, nil, nil), 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), call("mount", stub.ExpectArgs{"ephemeral", "/sysroot/run/user/1000", "tmpfs", uintptr(0), "mode=0710"}, nil, 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) return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, nil), 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) { func TestParentPerm(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
perm os.FileMode perm os.FileMode
want os.FileMode want os.FileMode
@@ -285,10 +278,29 @@ func TestParentPerm(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.perm.String(), func(t *testing.T) { t.Run(tc.perm.String(), func(t *testing.T) {
t.Parallel()
if got := parentPerm(tc.perm); got != tc.want { if got := parentPerm(tc.perm); got != tc.want {
t.Errorf("parentPerm: %#o, want %#o", 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. // 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 { if r, w, err := os.Pipe(); err != nil {
return -1, nil, err return -1, nil, err
} else { } else {
fd := 3 + len(*extraFiles) fd := 3 + len(*extraFiles)
*extraFiles = append(*extraFiles, r) *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 return nil, ErrReceiveEnv
} else { } else {
if fd, err := strconv.Atoi(s); err != nil { if fd, err := strconv.Atoi(s); err != nil {
return nil, optionalErrorUnwrap(err) return nil, errors.Unwrap(err)
} else { } else {
setup = os.NewFile(uintptr(fd), "setup") setup = os.NewFile(uintptr(fd), "setup")
if setup == nil { if setup == nil {

View File

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

View File

@@ -9,18 +9,84 @@ import (
"strings" "strings"
"syscall" "syscall"
"hakurei.app/container/fhs"
"hakurei.app/container/vfs" "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 ( const (
// Nonexistent is a path that cannot exist. // Nonexistent is a path that cannot exist.
// /proc is chosen because a system with covered /proc is unsupported by this package. // /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" hostDir = "host"
sysrootPath = fhs.Root + sysrootDir sysrootPath = FHSRoot + sysrootDir
sysrootDir = "sysroot" sysrootDir = "sysroot"
) )

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,130 +9,122 @@
#define LEN(arr) (sizeof(arr) / sizeof((arr)[0])) #define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
int32_t hakurei_scmp_make_filter( int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch,
int *ret_p, uintptr_t allocate_p, uint32_t multiarch,
uint32_t arch, uint32_t multiarch, struct hakurei_syscall_rule *rules,
struct hakurei_syscall_rule *rules, size_t rules_sz, hakurei_export_flag flags) {
size_t rules_sz, hakurei_export_flag flags) { int i;
int i; int last_allowed_family;
int last_allowed_family; int disallowed;
int disallowed; struct hakurei_syscall_rule *rule;
struct hakurei_syscall_rule *rule;
void *buf;
size_t len = 0;
int32_t res = 0; /* refer to resPrefix for message */ int32_t res = 0; /* refer to resPrefix for message */
/* Blocklist all but unix, inet, inet6 and netlink */ /* Blocklist all but unix, inet, inet6 and netlink */
struct { struct {
int family; int family;
hakurei_export_flag flags_mask; hakurei_export_flag flags_mask;
} socket_family_allowlist[] = { } socket_family_allowlist[] = {
/* NOTE: Keep in numerical order */ /* NOTE: Keep in numerical order */
{AF_UNSPEC, 0}, {AF_UNSPEC, 0},
{AF_LOCAL, 0}, {AF_LOCAL, 0},
{AF_INET, 0}, {AF_INET, 0},
{AF_INET6, 0}, {AF_INET6, 0},
{AF_NETLINK, 0}, {AF_NETLINK, 0},
{AF_CAN, HAKUREI_EXPORT_CAN}, {AF_CAN, HAKUREI_EXPORT_CAN},
{AF_BLUETOOTH, HAKUREI_EXPORT_BLUETOOTH}, {AF_BLUETOOTH, HAKUREI_EXPORT_BLUETOOTH},
}; };
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW); scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
if (ctx == NULL) { if (ctx == NULL) {
res = 1; res = 1;
goto out;
} else
errno = 0;
/* We only really need to handle arches on multiarch systems.
* If only one arch is supported the default is fine */
if (arch != 0) {
/* This *adds* the target arch, instead of replacing the
* native one. This is not ideal, because we'd like to only
* allow the target arch, but we can't really disallow the
* native arch at this point, because then bubblewrap
* couldn't continue running. */
*ret_p = seccomp_arch_add(ctx, arch);
if (*ret_p < 0 && *ret_p != -EEXIST) {
res = 2;
goto out;
}
if (flags & HAKUREI_EXPORT_MULTIARCH && multiarch != 0) {
*ret_p = seccomp_arch_add(ctx, multiarch);
if (*ret_p < 0 && *ret_p != -EEXIST) {
res = 3;
goto out; goto out;
} else }
errno = 0;
/* We only really need to handle arches on multiarch systems.
* If only one arch is supported the default is fine */
if (arch != 0) {
/* This *adds* the target arch, instead of replacing the
* native one. This is not ideal, because we'd like to only
* allow the target arch, but we can't really disallow the
* native arch at this point, because then bubblewrap
* couldn't continue running. */
*ret_p = seccomp_arch_add(ctx, arch);
if (*ret_p < 0 && *ret_p != -EEXIST) {
res = 2;
goto out;
}
if (flags & HAKUREI_EXPORT_MULTIARCH && multiarch != 0) {
*ret_p = seccomp_arch_add(ctx, multiarch);
if (*ret_p < 0 && *ret_p != -EEXIST) {
res = 3;
goto out;
}
}
} }
}
for (i = 0; i < rules_sz; i++) { for (i = 0; i < rules_sz; i++) {
rule = &rules[i]; rule = &rules[i];
assert(rule->m_errno == EPERM || rule->m_errno == ENOSYS); assert(rule->m_errno == EPERM || rule->m_errno == ENOSYS);
if (rule->arg) 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),
else rule->syscall, 1, *rule->arg);
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno), rule->syscall, 0); else
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno),
rule->syscall, 0);
if (*ret_p == -EFAULT) { if (*ret_p == -EFAULT) {
res = 4; res = 4;
goto out; goto out;
} else if (*ret_p < 0) { } else if (*ret_p < 0) {
res = 5; res = 5;
goto out; goto out;
}
} }
}
/* Socket filtering doesn't work on e.g. i386, so ignore failures here /* Socket filtering doesn't work on e.g. i386, so ignore failures here
* However, we need to user seccomp_rule_add_exact to avoid libseccomp doing * However, we need to user seccomp_rule_add_exact to avoid libseccomp doing
* something else: https://github.com/seccomp/libseccomp/issues/8 */ * something else: https://github.com/seccomp/libseccomp/issues/8 */
last_allowed_family = -1; last_allowed_family = -1;
for (i = 0; i < LEN(socket_family_allowlist); i++) { for (i = 0; i < LEN(socket_family_allowlist); i++) {
if (socket_family_allowlist[i].flags_mask != 0 && 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) !=
continue; 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;
/* Blocklist the in-between valid families */ disallowed < socket_family_allowlist[i].family; disallowed++) {
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_EQ, disallowed)); /* Blocklist the in-between valid families */
} seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT),
last_allowed_family = socket_family_allowlist[i].family; SCMP_SYS(socket), 1,
SCMP_A0(SCMP_CMP_EQ, disallowed));
} }
/* Blocklist the rest */ last_allowed_family = socket_family_allowlist[i].family;
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1)); }
/* Blocklist the rest */
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1,
SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1));
if (allocate_p == 0) { if (fd < 0) {
*ret_p = seccomp_load(ctx); *ret_p = seccomp_load(ctx);
if (*ret_p != 0) { if (*ret_p != 0) {
res = 7; res = 7;
goto out; goto out;
}
} else {
*ret_p = seccomp_export_bpf_mem(ctx, NULL, &len);
if (*ret_p != 0) {
res = 6;
goto out;
}
buf = hakurei_scmp_allocate(allocate_p, len);
if (buf == NULL) {
res = 4;
goto out;
}
*ret_p = seccomp_export_bpf_mem(ctx, buf, &len);
if (*ret_p != 0) {
res = 6;
goto out;
}
} }
} else {
*ret_p = seccomp_export_bpf(ctx, fd);
if (*ret_p != 0) {
res = 6;
goto out;
}
}
out: out:
if (ctx) if (ctx)
seccomp_release(ctx); seccomp_release(ctx);
return res; return res;
} }

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