138 Commits

Author SHA1 Message Date
7638a44fa6 treewide: parallel tests
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Hakurei (push) Successful in 44s
Test / Sandbox (push) Successful in 41s
Test / Hakurei (race detector) (push) Successful in 44s
Test / Sandbox (race detector) (push) Successful in 41s
Test / Hpkg (push) Successful in 41s
Test / Flake checks (push) Successful in 1m24s
Most tests already had no global state, however parallel was never enabled. This change enables it for all applicable tests.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Closes #3.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Closes #11.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Closes #14.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

@@ -7,9 +7,12 @@ import (
"testing"
"hakurei.app/command"
"hakurei.app/message"
)
func TestHelp(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
args []string
@@ -20,8 +23,8 @@ func TestHelp(t *testing.T) {
Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS]
Commands:
app Load app from configuration file
run Configure and start a permissive default sandbox
app Load and start container from configuration file
run Configure and start a permissive container
show Show live or local app configuration
ps List active instances
version Display version information
@@ -67,8 +70,10 @@ Flags:
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
out := new(bytes.Buffer)
c := buildCommand(out)
c := buildCommand(t.Context(), message.NewMsg(nil), new(earlyHardeningErrs), out)
if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) {
t.Errorf("Parse: error = %v; want %v",
err, command.ErrHelp)

View File

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

View File

@@ -11,18 +11,19 @@ import (
"syscall"
"hakurei.app/hst"
"hakurei.app/internal/app"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/message"
)
func tryPath(name string) (config *hst.Config) {
func tryPath(msg message.Msg, name string) (config *hst.Config) {
var r io.Reader
config = new(hst.Config)
if name != "-" {
r = tryFd(name)
r = tryFd(msg, name)
if r == nil {
hlog.Verbose("load configuration from file")
msg.Verbose("load configuration from file")
if f, err := os.Open(name); err != nil {
log.Fatalf("cannot access configuration file %q: %s", name, err)
@@ -48,14 +49,14 @@ func tryPath(name string) (config *hst.Config) {
return
}
func tryFd(name string) io.ReadCloser {
func tryFd(msg message.Msg, name string) io.ReadCloser {
if v, err := strconv.Atoi(name); err != nil {
if !errors.Is(err, strconv.ErrSyntax) {
hlog.Verbosef("name cannot be interpreted as int64: %v", err)
msg.Verbosef("name cannot be interpreted as int64: %v", err)
}
return nil
} else {
hlog.Verbosef("trying config stream from %d", v)
msg.Verbosef("trying config stream from %d", v)
fd := uintptr(v)
if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 {
if errors.Is(errno, syscall.EBADF) {
@@ -67,7 +68,7 @@ func tryFd(name string) io.ReadCloser {
}
}
func tryShort(name string) (config *hst.Config, entry *state.State) {
func tryShort(msg message.Msg, name string) (config *hst.Config, entry *state.State) {
likePrefix := false
if len(name) <= 32 {
likePrefix = true
@@ -85,9 +86,11 @@ func tryShort(name string) (config *hst.Config, entry *state.State) {
// try to match from state store
if likePrefix && len(name) >= 8 {
hlog.Verbose("argument looks like prefix")
msg.Verbose("argument looks like prefix")
s := state.NewMulti(std.Paths().RunDirPath.String())
var sc hst.Paths
app.CopyPaths().Copy(&sc, new(app.Hsu).MustID())
s := state.NewMulti(msg, sc.RunDirPath.String())
if entries, err := state.Join(s); err != nil {
log.Printf("cannot join store: %v", err)
// drop to fetch from file
@@ -101,7 +104,7 @@ func tryShort(name string) (config *hst.Config, entry *state.State) {
break
}
hlog.Verbosef("instance %s skipped", v)
msg.Verbosef("instance %s skipped", v)
}
}
}

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"io"
"log"
"os"
"slices"
"strconv"
"strings"
@@ -13,24 +12,17 @@ import (
"time"
"hakurei.app/hst"
"hakurei.app/internal/app"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/system/dbus"
"hakurei.app/message"
)
func printShowSystem(output io.Writer, short, flagJSON bool) {
t := newPrinter(output)
defer t.MustFlush()
info := &hst.Info{Paths: std.Paths()}
// get hid by querying uid of identity 0
if uid, err := std.Uid(0); err != nil {
hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:")
os.Exit(1)
} else {
info.User = (uid / 10000) - 100
}
info := &hst.Info{User: new(app.Hsu).MustID()}
app.CopyPaths().Copy(&info.Paths, info.User)
if flagJSON {
printJSON(output, short, info)
@@ -47,7 +39,9 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
func printShowInstance(
output io.Writer, now time.Time,
instance *state.State, config *hst.Config,
short, flagJSON bool) {
short, flagJSON bool) (valid bool) {
valid = true
if flagJSON {
if instance != nil {
printJSON(output, short, instance)
@@ -60,8 +54,11 @@ func printShowInstance(
t := newPrinter(output)
defer t.MustFlush()
if config.Container == nil {
mustPrint(output, "Warning: this configuration uses permissive defaults!\n\n")
if err := config.Validate(); err != nil {
valid = false
if m, ok := message.GetMessage(err); ok {
mustPrint(output, "Error: "+m+"!\n\n")
}
}
if instance != nil {
@@ -81,11 +78,11 @@ func printShowInstance(
if len(config.Groups) > 0 {
t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", "))
}
if config.Home != nil {
t.Printf(" Home:\t%s\n", config.Home)
}
if config.Container != nil {
params := config.Container
if params.Home != nil {
t.Printf(" Home:\t%s\n", params.Home)
}
if params.Hostname != "" {
t.Printf(" Hostname:\t%s\n", params.Hostname)
}
@@ -108,12 +105,12 @@ func printShowInstance(
}
t.Printf(" Flags:\t%s\n", strings.Join(flags, " "))
if config.Path != nil {
t.Printf(" Path:\t%s\n", config.Path)
if params.Path != nil {
t.Printf(" Path:\t%s\n", params.Path)
}
if len(params.Args) > 0 {
t.Printf(" Arguments:\t%s\n", strings.Join(params.Args, " "))
}
}
if len(config.Args) > 0 {
t.Printf(" Arguments:\t%s\n", strings.Join(config.Args, " "))
}
t.Printf("\n")
@@ -122,6 +119,7 @@ func printShowInstance(
t.Printf("Filesystem\n")
for _, f := range config.Container.Filesystem {
if !f.Valid() {
valid = false
t.Println(" <invalid>")
continue
}
@@ -141,7 +139,7 @@ func printShowInstance(
}
}
printDBus := func(c *dbus.Config) {
printDBus := func(c *hst.BusConfig) {
t.Printf(" Filter:\t%v\n", c.Filter)
if len(c.See) > 0 {
t.Printf(" See:\t%q\n", c.See)
@@ -169,6 +167,8 @@ func printShowInstance(
printDBus(config.SystemBus)
t.Printf("\n")
}
return
}
func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON bool) {

View File

@@ -7,7 +7,6 @@ import (
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/system/dbus"
)
var (
@@ -27,13 +26,16 @@ var (
testAppTime = time.Unix(0, 9).UTC()
)
func Test_printShowInstance(t *testing.T) {
func TestPrintShowInstance(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
instance *state.State
config *hst.Config
short, json bool
want string
valid bool
}{
{"config", nil, hst.Template(), false, false, `App
Identity: 9 (org.chromium.Chromium)
@@ -49,8 +51,7 @@ Filesystem
autoroot:w:/var/lib/hakurei/base/org.debian
autoetc:/etc/
w+ephemeral(-rwxr-xr-x):/tmp/
w*/nix/store:/mnt-root/nix/.rw-store/upper:/mnt-root/nix/.rw-store/work:/mnt-root/nix/.ro-store
*/nix/store
w*/nix/store:/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper:/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work:/var/lib/hakurei/base/org.nixos/ro-store
/run/current-system@
/run/opengl-driver@
w-/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
@@ -71,21 +72,25 @@ System bus
Filter: true
Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"]
`},
{"config pd", nil, new(hst.Config), false, false, `Warning: this configuration uses permissive defaults!
`, true},
{"config pd", nil, new(hst.Config), false, false, `Error: configuration missing container state!
App
Identity: 0
Enablements: (no enablements)
`},
{"config flag none", nil, &hst.Config{Container: new(hst.ContainerConfig)}, false, false, `App
`, false},
{"config flag none", nil, &hst.Config{Container: new(hst.ContainerConfig)}, false, false, `Error: container configuration missing path to home directory!
App
Identity: 0
Enablements: (no enablements)
Flags: none
`},
{"config nil entries", nil, &hst.Config{Container: &hst.ContainerConfig{Filesystem: make([]hst.FilesystemConfigJSON, 1)}, ExtraPerms: make([]*hst.ExtraPermConfig, 1)}, false, false, `App
`, false},
{"config nil entries", nil, &hst.Config{Container: &hst.ContainerConfig{Filesystem: make([]hst.FilesystemConfigJSON, 1)}, ExtraPerms: make([]*hst.ExtraPermConfig, 1)}, false, false, `Error: container configuration missing path to home directory!
App
Identity: 0
Enablements: (no enablements)
Flags: none
@@ -95,8 +100,8 @@ Filesystem
Extra ACL
`},
{"config pd dbus see", nil, &hst.Config{SessionBus: &dbus.Config{See: []string{"org.example.test"}}}, false, false, `Warning: this configuration uses permissive defaults!
`, false},
{"config pd dbus see", nil, &hst.Config{SessionBus: &hst.BusConfig{See: []string{"org.example.test"}}}, false, false, `Error: configuration missing container state!
App
Identity: 0
@@ -106,7 +111,7 @@ Session bus
Filter: false
See: ["org.example.test"]
`},
`, false},
{"instance", testState, hst.Template(), false, false, `State
Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (3735928559)
@@ -126,8 +131,7 @@ Filesystem
autoroot:w:/var/lib/hakurei/base/org.debian
autoetc:/etc/
w+ephemeral(-rwxr-xr-x):/tmp/
w*/nix/store:/mnt-root/nix/.rw-store/upper:/mnt-root/nix/.rw-store/work:/mnt-root/nix/.ro-store
*/nix/store
w*/nix/store:/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper:/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work:/var/lib/hakurei/base/org.nixos/ro-store
/run/current-system@
/run/opengl-driver@
w-/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
@@ -148,8 +152,8 @@ System bus
Filter: true
Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"]
`},
{"instance pd", testState, new(hst.Config), false, false, `Warning: this configuration uses permissive defaults!
`, true},
{"instance pd", testState, new(hst.Config), false, false, `Error: configuration missing container state!
State
Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (3735928559)
@@ -159,10 +163,10 @@ App
Identity: 0
Enablements: (no enablements)
`},
`, false},
{"json nil", nil, nil, false, true, `null
`},
`, true},
{"json instance", testState, nil, false, true, `{
"instance": [
142,
@@ -185,14 +189,6 @@ App
"pid": 3735928559,
"config": {
"id": "org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"enablements": {
"wayland": true,
"dbus": true,
@@ -234,9 +230,6 @@ App
"broadcast": null,
"filter": true
},
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"extra_perms": [
{
"ensure": true,
@@ -259,8 +252,6 @@ App
"container": {
"hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1,
"seccomp_presets": 1,
"seccomp_compat": true,
"devel": true,
"userns": true,
@@ -299,14 +290,10 @@ App
"type": "overlay",
"dst": "/nix/store",
"lower": [
"/mnt-root/nix/.ro-store"
"/var/lib/hakurei/base/org.nixos/ro-store"
],
"upper": "/mnt-root/nix/.rw-store/upper",
"work": "/mnt-root/nix/.rw-store/work"
},
{
"type": "bind",
"src": "/nix/store"
"upper": "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper",
"work": "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work"
},
{
"type": "link",
@@ -333,22 +320,25 @@ App
"dev": true,
"optional": true
}
],
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
]
}
},
"time": "1970-01-01T00:00:00.000000009Z"
}
`},
`, true},
{"json config", nil, hst.Template(), false, true, `{
"id": "org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"enablements": {
"wayland": true,
"dbus": true,
@@ -390,9 +380,6 @@ App
"broadcast": null,
"filter": true
},
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"extra_perms": [
{
"ensure": true,
@@ -415,8 +402,6 @@ App
"container": {
"hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1,
"seccomp_presets": 1,
"seccomp_compat": true,
"devel": true,
"userns": true,
@@ -455,14 +440,10 @@ App
"type": "overlay",
"dst": "/nix/store",
"lower": [
"/mnt-root/nix/.ro-store"
"/var/lib/hakurei/base/org.nixos/ro-store"
],
"upper": "/mnt-root/nix/.rw-store/upper",
"work": "/mnt-root/nix/.rw-store/work"
},
{
"type": "bind",
"src": "/nix/store"
"upper": "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper",
"work": "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work"
},
{
"type": "link",
@@ -489,26 +470,43 @@ App
"dev": true,
"optional": true
}
],
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
]
}
}
`},
`, true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
output := new(strings.Builder)
printShowInstance(output, testTime, tc.instance, tc.config, tc.short, tc.json)
gotValid := printShowInstance(output, testTime, tc.instance, tc.config, tc.short, tc.json)
if got := output.String(); got != tc.want {
t.Errorf("printShowInstance: got\n%s\nwant\n%s",
got, tc.want)
t.Errorf("printShowInstance: \n%s\nwant\n%s", got, tc.want)
return
}
if gotValid != tc.valid {
t.Errorf("printShowInstance: valid = %v, want %v", gotValid, tc.valid)
}
})
}
}
func Test_printPs(t *testing.T) {
func TestPrintPs(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
entries state.Entries
@@ -551,14 +549,6 @@ func Test_printPs(t *testing.T) {
"pid": 3735928559,
"config": {
"id": "org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"enablements": {
"wayland": true,
"dbus": true,
@@ -600,9 +590,6 @@ func Test_printPs(t *testing.T) {
"broadcast": null,
"filter": true
},
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"extra_perms": [
{
"ensure": true,
@@ -625,8 +612,6 @@ func Test_printPs(t *testing.T) {
"container": {
"hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1,
"seccomp_presets": 1,
"seccomp_compat": true,
"devel": true,
"userns": true,
@@ -665,14 +650,10 @@ func Test_printPs(t *testing.T) {
"type": "overlay",
"dst": "/nix/store",
"lower": [
"/mnt-root/nix/.ro-store"
"/var/lib/hakurei/base/org.nixos/ro-store"
],
"upper": "/mnt-root/nix/.rw-store/upper",
"work": "/mnt-root/nix/.rw-store/work"
},
{
"type": "bind",
"src": "/nix/store"
"upper": "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper",
"work": "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work"
},
{
"type": "link",
@@ -699,6 +680,17 @@ func Test_printPs(t *testing.T) {
"dev": true,
"optional": true
}
],
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
]
}
},
@@ -712,6 +704,8 @@ func Test_printPs(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
output := new(strings.Builder)
printPs(output, testTime, stubStore(tc.entries), tc.short, tc.json)
if got := output.String(); got != tc.want {

View File

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

View File

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

View File

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

View File

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

View File

@@ -62,11 +62,11 @@ def check_state(name, enablements):
config = instance['config']
if len(config['args']) != 1 or not (config['args'][0].startswith("/nix/store/")) or f"hakurei-{name}-" not in (config['args'][0]):
raise Exception(f"unexpected args {instance['config']['args']}")
if len(config['container']['args']) != 1 or not (config['container']['args'][0].startswith("/nix/store/")) or f"hakurei-{name}-" not in (config['container']['args'][0]):
raise Exception(f"unexpected args {config['container']['args']}")
if config['enablements'] != enablements:
raise Exception(f"unexpected enablements {instance['config']['enablements']}")
raise Exception(f"unexpected enablements {config['enablements']}")
start_all()

View File

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

View File

@@ -1,5 +1,7 @@
package main
// minimise imports to avoid inadvertently calling init or global variable functions
import (
"bytes"
"fmt"
@@ -15,10 +17,13 @@ import (
const (
hsuConfFile = "/etc/hsurc"
envShim = "HAKUREI_SHIM"
envAID = "HAKUREI_APP_ID"
envIdentity = "HAKUREI_IDENTITY"
envGroups = "HAKUREI_GROUPS"
PR_SET_NO_NEW_PRIVS = 0x26
identityMin = 0
identityMax = 9999
)
func main() {
@@ -29,6 +34,9 @@ func main() {
if os.Geteuid() != 0 {
log.Fatal("this program must be owned by uid 0 and have the setuid bit set")
}
if os.Getegid() != os.Getgid() {
log.Fatal("this program must not have the setgid bit set")
}
puid := os.Getuid()
if puid == 0 {
@@ -48,8 +56,8 @@ func main() {
}
// uid = 1000000 +
// fid * 10000 +
// aid
// id * 10000 +
// identity
uid := 1000000
// refuse to run if hsurc is not protected correctly
@@ -62,29 +70,25 @@ func main() {
}
// authenticate before accepting user input
var id int
if f, err := os.Open(hsuConfFile); err != nil {
log.Fatal(err)
} else if fid, ok := mustParseConfig(f, puid); !ok {
} else if v, ok := mustParseConfig(f, puid); !ok {
log.Fatalf("uid %d is not in the hsurc file", puid)
} else {
uid += fid * 10000
}
id = v
if err = f.Close(); err != nil {
log.Fatal(err)
}
// 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
uid += id * 10000
}
// pass through setup fd to shim
var shimSetupFd string
if s, ok := os.LookupEnv(envShim); !ok {
// hakurei requests target uid
// print resolved uid and exit
fmt.Print(uid)
// hakurei requests hsurc user id
fmt.Print(id)
os.Exit(0)
} else if len(s) != 1 || s[0] > '9' || s[0] < '3' {
log.Fatal("HAKUREI_SHIM holds an invalid value")
@@ -92,6 +96,15 @@ func main() {
shimSetupFd = s
}
// allowed identity range 0 to 9999
if as, ok := os.LookupEnv(envIdentity); !ok {
log.Fatal("HAKUREI_IDENTITY not set")
} else if identity, err := parseUint32Fast(as); err != nil || identity < identityMin || identity > identityMax {
log.Fatal("invalid identity")
} else {
uid += identity
}
// supplementary groups
var suppGroups, suppCurrent []int

View File

@@ -6,32 +6,46 @@ import (
"testing"
)
func Test_parseUint32Fast(t *testing.T) {
func TestParseUint32Fast(t *testing.T) {
t.Parallel()
t.Run("zero-length", func(t *testing.T) {
t.Parallel()
if _, err := parseUint32Fast(""); err == nil || err.Error() != "zero length string" {
t.Errorf(`parseUint32Fast(""): error = %v`, err)
return
}
})
t.Run("overflow", func(t *testing.T) {
t.Parallel()
if _, err := parseUint32Fast("10000000000"); err == nil || err.Error() != "string too long" {
t.Errorf("parseUint32Fast: error = %v", err)
return
}
})
t.Run("invalid byte", func(t *testing.T) {
t.Parallel()
if _, err := parseUint32Fast("meow"); err == nil || err.Error() != "invalid character 'm' at index 0" {
t.Errorf(`parseUint32Fast("meow"): error = %v`, err)
return
}
})
t.Run("full range", func(t *testing.T) {
t.Parallel()
testRange := func(i, end int) {
for ; i < end; i++ {
s := strconv.Itoa(i)
w := i
t.Run("parse "+s, func(t *testing.T) {
t.Parallel()
v, err := parseUint32Fast(s)
if err != nil {
t.Errorf("parseUint32Fast(%q): error = %v",
@@ -55,7 +69,9 @@ func Test_parseUint32Fast(t *testing.T) {
})
}
func Test_parseConfig(t *testing.T) {
func TestParseConfig(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
puid, want int
@@ -71,6 +87,8 @@ func Test_parseConfig(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
fid, ok, err := parseConfig(bytes.NewBufferString(tc.rc), tc.puid)
if err == nil && tc.wantErr != "" {
t.Errorf("parseConfig: error = %v; wantErr %q",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

13
container/bits/bits.go Normal file
View File

@@ -0,0 +1,13 @@
// Package bits contains constants for configuring the container.
package bits
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
)

20
container/bits/seccomp.go Normal file
View File

@@ -0,0 +1,20 @@
package bits
// FilterPreset specifies parts of the syscall filter preset to enable.
type FilterPreset int
const (
// PresetExt are project-specific extensions.
PresetExt FilterPreset = 1 << iota
// PresetDenyNS denies namespace setup syscalls.
PresetDenyNS
// PresetDenyTTY denies faking input.
PresetDenyTTY
// PresetDenyDevel denies development-related syscalls.
PresetDenyDevel
// PresetLinux32 sets PER_LINUX32.
PresetLinux32
// PresetStrict is a strict preset useful as a default value.
PresetStrict = PresetExt | PresetDenyNS | PresetDenyTTY | PresetDenyDevel
)

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package container
package check_test
import (
"bytes"
@@ -9,9 +9,17 @@ import (
"strings"
"syscall"
"testing"
_ "unsafe"
. "hakurei.app/container/check"
)
//go:linkname unsafeAbs hakurei.app/container/check.unsafeAbs
func unsafeAbs(_ string) *Absolute
func TestAbsoluteError(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
@@ -21,8 +29,8 @@ func TestAbsoluteError(t *testing.T) {
}{
{"EINVAL", new(AbsoluteError), syscall.EINVAL, true},
{"not EINVAL", new(AbsoluteError), syscall.EBADE, false},
{"ne val", new(AbsoluteError), &AbsoluteError{"etc"}, false},
{"equals", &AbsoluteError{"etc"}, &AbsoluteError{"etc"}, true},
{"ne val", new(AbsoluteError), &AbsoluteError{Pathname: "etc"}, false},
{"equals", &AbsoluteError{Pathname: "etc"}, &AbsoluteError{Pathname: "etc"}, true},
}
for _, tc := range testCases {
@@ -32,14 +40,18 @@ func TestAbsoluteError(t *testing.T) {
}
t.Run("string", func(t *testing.T) {
t.Parallel()
want := `path "etc" is not absolute`
if got := (&AbsoluteError{"etc"}).Error(); got != want {
if got := (&AbsoluteError{Pathname: "etc"}).Error(); got != want {
t.Errorf("Error: %q, want %q", got, want)
}
})
}
func TestNewAbs(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
@@ -48,12 +60,14 @@ func TestNewAbs(t *testing.T) {
wantErr error
}{
{"good", "/etc", MustAbs("/etc"), nil},
{"not absolute", "etc", nil, &AbsoluteError{"etc"}},
{"zero", "", nil, &AbsoluteError{""}},
{"not absolute", "etc", nil, &AbsoluteError{Pathname: "etc"}},
{"zero", "", nil, &AbsoluteError{Pathname: ""}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, err := NewAbs(tc.pathname)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("NewAbs: %#v, want %#v", got, tc.want)
@@ -65,6 +79,8 @@ func TestNewAbs(t *testing.T) {
}
t.Run("must", func(t *testing.T) {
t.Parallel()
defer func() {
wantPanic := `path "etc" is not absolute`
@@ -79,13 +95,17 @@ func TestNewAbs(t *testing.T) {
func TestAbsoluteString(t *testing.T) {
t.Run("passthrough", func(t *testing.T) {
t.Parallel()
pathname := "/etc"
if got := (&Absolute{pathname}).String(); got != pathname {
if got := unsafeAbs(pathname).String(); got != pathname {
t.Errorf("String: %q, want %q", got, pathname)
}
})
t.Run("zero", func(t *testing.T) {
t.Parallel()
defer func() {
wantPanic := "attempted use of zero Absolute"
@@ -99,6 +119,8 @@ func TestAbsoluteString(t *testing.T) {
}
func TestAbsoluteIs(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
a, v *Absolute
@@ -114,6 +136,8 @@ func TestAbsoluteIs(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.a.Is(tc.v); got != tc.want {
t.Errorf("Is: %v, want %v", got, tc.want)
}
@@ -127,6 +151,8 @@ type sCheck struct {
}
func TestCodecAbsolute(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
a *Absolute
@@ -147,7 +173,7 @@ func TestCodecAbsolute(t *testing.T) {
`"/etc"`, `{"val":"/etc","magic":3236757504}`},
{"not absolute", nil,
&AbsoluteError{"etc"},
&AbsoluteError{Pathname: "etc"},
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\a\xff\x80\x00\x03etc",
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x04\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x03etc\x01\xfb\x01\x81\xda\x00\x00\x00",
@@ -161,13 +187,18 @@ func TestCodecAbsolute(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("gob", func(t *testing.T) {
if tc.gob == "\x00" && tc.sGob == "\x00" {
// these values mark the current test to skip gob
return
}
t.Parallel()
t.Run("encode", func(t *testing.T) {
t.Parallel()
// encode is unchecked
if errors.Is(tc.wantErr, syscall.EINVAL) {
return
@@ -204,6 +235,8 @@ func TestCodecAbsolute(t *testing.T) {
})
t.Run("decode", func(t *testing.T) {
t.Parallel()
{
var gotA *Absolute
err := gob.NewDecoder(strings.NewReader(tc.gob)).Decode(&gotA)
@@ -238,7 +271,11 @@ func TestCodecAbsolute(t *testing.T) {
})
t.Run("json", func(t *testing.T) {
t.Parallel()
t.Run("marshal", func(t *testing.T) {
t.Parallel()
// marshal is unchecked
if errors.Is(tc.wantErr, syscall.EINVAL) {
return
@@ -273,6 +310,8 @@ func TestCodecAbsolute(t *testing.T) {
})
t.Run("unmarshal", func(t *testing.T) {
t.Parallel()
{
var gotA *Absolute
err := json.Unmarshal([]byte(tc.json), &gotA)
@@ -308,6 +347,8 @@ func TestCodecAbsolute(t *testing.T) {
}
t.Run("json passthrough", func(t *testing.T) {
t.Parallel()
wantErr := "invalid character ':' looking for beginning of value"
if err := new(Absolute).UnmarshalJSON([]byte(":3")); err == nil || err.Error() != wantErr {
t.Errorf("UnmarshalJSON: error = %v, want %s", err, wantErr)
@@ -316,7 +357,11 @@ func TestCodecAbsolute(t *testing.T) {
}
func TestAbsoluteWrap(t *testing.T) {
t.Parallel()
t.Run("join", func(t *testing.T) {
t.Parallel()
want := "/etc/nix/nix.conf"
if got := MustAbs("/etc").Append("nix", "nix.conf"); got.String() != want {
t.Errorf("Append: %q, want %q", got, want)
@@ -324,6 +369,8 @@ func TestAbsoluteWrap(t *testing.T) {
})
t.Run("dir", func(t *testing.T) {
t.Parallel()
want := "/"
if got := MustAbs("/etc").Dir(); got.String() != want {
t.Errorf("Dir: %q, want %q", got, want)
@@ -331,6 +378,8 @@ func TestAbsoluteWrap(t *testing.T) {
})
t.Run("sort", func(t *testing.T) {
t.Parallel()
want := []*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/sys")}
got := []*Absolute{MustAbs("/proc"), MustAbs("/sys"), MustAbs("/etc")}
SortAbs(got)
@@ -340,6 +389,8 @@ func TestAbsoluteWrap(t *testing.T) {
})
t.Run("compact", func(t *testing.T) {
t.Parallel()
want := []*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/sys")}
if got := CompactAbs([]*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/proc"), MustAbs("/sys")}); !reflect.DeepEqual(got, want) {
t.Errorf("CompactAbs: %#v, want %#v", got, want)

View File

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

View File

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

View File

@@ -14,7 +14,11 @@ import (
. "syscall"
"time"
"hakurei.app/container/bits"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp"
"hakurei.app/message"
)
const (
@@ -49,22 +53,23 @@ type (
cmd *exec.Cmd
ctx context.Context
msg message.Msg
Params
}
// Params holds container configuration and is safe to serialise.
Params struct {
// Working directory in the container.
Dir *Absolute
Dir *check.Absolute
// Initial process environment.
Env []string
// Pathname of initial process in the container.
Path *Absolute
Path *check.Absolute
// Initial process argv.
Args []string
// Deliver SIGINT to the initial process on context cancellation.
ForwardCancel bool
// time to wait for linger processes after death of initial process
// Time to wait for processes lingering after the initial process terminates.
AdoptWaitDelay time.Duration
// Mapped Uid in user namespace.
@@ -81,7 +86,7 @@ type (
// Extra seccomp flags.
SeccompFlags seccomp.ExportFlag
// Seccomp presets. Has no effect unless SeccompRules is zero-length.
SeccompPresets seccomp.FilterPreset
SeccompPresets bits.FilterPreset
// Do not load seccomp program.
SeccompDisable bool
@@ -99,28 +104,77 @@ type (
}
)
// Start starts the container init. The init process blocks until Serve is called.
func (p *Container) Start() error {
if p.cmd != nil {
return errors.New("container: already started")
// A StartError contains additional information on a container startup failure.
type StartError struct {
// Fatal suggests whether this error should be considered fatal for the entire program.
Fatal bool
// Step refers to the part of the setup this error is returned from.
Step string
// Err is the underlying error.
Err error
// Origin is whether this error originated from the [Container.Start] method.
Origin bool
// Passthrough is whether the Error method is passed through to Err.
Passthrough bool
}
func (e *StartError) Unwrap() error { return e.Err }
func (e *StartError) Error() string {
if e.Passthrough {
return e.Err.Error()
}
if p.Ops == nil || len(*p.Ops) == 0 {
return errors.New("container: starting an empty container")
if e.Origin {
return e.Step
}
ctx, cancel := context.WithCancel(p.ctx)
p.cancel = cancel
{
var syscallError *os.SyscallError
if errors.As(e.Err, &syscallError) && syscallError != nil {
return e.Step + " " + syscallError.Error()
}
}
return e.Step + ": " + e.Err.Error()
}
// Message returns a user-facing error message.
func (e *StartError) Message() string {
if e.Passthrough {
switch {
case errors.As(e.Err, new(*os.PathError)),
errors.As(e.Err, new(*os.SyscallError)):
return "cannot " + e.Err.Error()
default:
return e.Err.Error()
}
}
if e.Origin {
return e.Step
}
return "cannot " + e.Error()
}
// Start starts the container init. The init process blocks until Serve is called.
func (p *Container) Start() error {
if p == nil || p.cmd == nil ||
p.Ops == nil || len(*p.Ops) == 0 {
return errors.New("container: starting an invalid container")
}
if p.cmd.Process != nil {
return errors.New("container: already started")
}
// map to overflow id to work around ownership checks
if p.Uid < 1 {
p.Uid = OverflowUid()
p.Uid = OverflowUid(p.msg)
}
if p.Gid < 1 {
p.Gid = OverflowGid()
p.Gid = OverflowGid(p.msg)
}
if !p.RetainSession {
p.SeccompPresets |= seccomp.PresetDenyTTY
p.SeccompPresets |= bits.PresetDenyTTY
}
if p.AdoptWaitDelay == 0 {
@@ -131,16 +185,24 @@ func (p *Container) Start() error {
p.AdoptWaitDelay = 0
}
p.cmd = exec.CommandContext(ctx, MustExecutable())
if p.cmd.Stdin == nil {
p.cmd.Stdin = p.Stdin
}
if p.cmd.Stdout == nil {
p.cmd.Stdout = p.Stdout
}
if p.cmd.Stderr == nil {
p.cmd.Stderr = p.Stderr
}
p.cmd.Args = []string{initName}
p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
p.cmd.WaitDelay = p.WaitDelay
if p.Cancel != nil {
p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
} else {
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(CancelSignal) }
}
p.cmd.Dir = FHSRoot
p.cmd.Dir = fhs.Root
p.cmd.SysProcAttr = &SysProcAttr{
Setsid: !p.RetainSession,
Pdeathsig: SIGKILL,
@@ -167,8 +229,7 @@ func (p *Container) Start() error {
// place setup pipe before user supplied extra files, this is later restored by init
if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil {
return wrapErrSuffix(err,
"cannot create shim setup pipe:")
return &StartError{true, "set up params stream", err, false, false}
} else {
p.setup = e
p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
@@ -183,8 +244,7 @@ func (p *Container) Start() error {
done <- func() error { // setup depending on per-thread state must happen here
// PR_SET_NO_NEW_PRIVS: depends on per-thread state but acts on all processes created from that thread
if err := SetNoNewPrivs(); err != nil {
return wrapErrSuffix(err,
"prctl(PR_SET_NO_NEW_PRIVS):")
return &StartError{true, "prctl(PR_SET_NO_NEW_PRIVS)", err, false, false}
}
// landlock: depends on per-thread state but acts on a process group
@@ -200,31 +260,27 @@ func (p *Container) Start() error {
// already covered by namespaces (pid)
goto landlockOut
}
return wrapErrSuffix(err,
"landlock does not appear to be enabled:")
return &StartError{false, "get landlock ABI", err, false, false}
} else if abi < 6 {
if p.HostAbstract {
// see above comment
goto landlockOut
}
return msg.WrapErr(ENOSYS,
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET")
return &StartError{false, "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", ENOSYS, true, false}
} else {
msg.Verbosef("landlock abi version %d", abi)
p.msg.Verbosef("landlock abi version %d", abi)
}
if rulesetFd, err := rulesetAttr.Create(0); err != nil {
return wrapErrSuffix(err,
"cannot create landlock ruleset:")
return &StartError{true, "create landlock ruleset", err, false, false}
} else {
msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
p.msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
if err = LandlockRestrictSelf(rulesetFd, 0); err != nil {
_ = Close(rulesetFd)
return wrapErrSuffix(err,
"cannot enforce landlock ruleset:")
return &StartError{true, "enforce landlock ruleset", err, false, false}
}
if err = Close(rulesetFd); err != nil {
msg.Verbosef("cannot close landlock ruleset: %v", err)
p.msg.Verbosef("cannot close landlock ruleset: %v", err)
// not fatal
}
}
@@ -232,9 +288,9 @@ func (p *Container) Start() error {
landlockOut:
}
msg.Verbose("starting container init")
p.msg.Verbose("starting container init")
if err := p.cmd.Start(); err != nil {
return msg.WrapErr(err, err.Error())
return &StartError{false, "start container init", err, false, true}
}
return nil
}()
@@ -257,12 +313,12 @@ func (p *Container) Serve() error {
if p.Path == nil {
p.cancel()
return msg.WrapErr(EINVAL, "invalid executable pathname")
return &StartError{false, "invalid executable pathname", EINVAL, true, false}
}
// do not transmit nil
if p.Dir == nil {
p.Dir = AbsFHSRoot
p.Dir = fhs.AbsRoot
}
if p.SeccompRules == nil {
p.SeccompRules = make([]seccomp.NativeRule, 0)
@@ -274,7 +330,7 @@ func (p *Container) Serve() error {
Getuid(),
Getgid(),
len(p.ExtraFiles),
msg.IsVerbose(),
p.msg.IsVerbose(),
},
)
if err != nil {
@@ -285,7 +341,7 @@ func (p *Container) Serve() error {
// Wait waits for the container init process to exit and releases any resources associated with the [Container].
func (p *Container) Wait() error {
if p.cmd == nil {
if p.cmd == nil || p.cmd.Process == nil {
return EINVAL
}
@@ -297,6 +353,36 @@ func (p *Container) Wait() error {
return err
}
// StdinPipe calls the [exec.Cmd] method with the same name.
func (p *Container) StdinPipe() (w io.WriteCloser, err error) {
if p.Stdin != nil {
return nil, errors.New("container: Stdin already set")
}
w, err = p.cmd.StdinPipe()
p.Stdin = p.cmd.Stdin
return
}
// StdoutPipe calls the [exec.Cmd] method with the same name.
func (p *Container) StdoutPipe() (r io.ReadCloser, err error) {
if p.Stdout != nil {
return nil, errors.New("container: Stdout already set")
}
r, err = p.cmd.StdoutPipe()
p.Stdout = p.cmd.Stdout
return
}
// StderrPipe calls the [exec.Cmd] method with the same name.
func (p *Container) StderrPipe() (r io.ReadCloser, err error) {
if p.Stderr != nil {
return nil, errors.New("container: Stderr already set")
}
r, err = p.cmd.StderrPipe()
p.Stderr = p.cmd.Stderr
return
}
func (p *Container) String() string {
return fmt.Sprintf("argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x",
p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets))
@@ -311,13 +397,21 @@ func (p *Container) ProcessState() *os.ProcessState {
}
// New returns the address to a new instance of [Container] that requires further initialisation before use.
func New(ctx context.Context) *Container {
return &Container{ctx: ctx, Params: Params{Ops: new(Ops)}}
func New(ctx context.Context, msg message.Msg) *Container {
if msg == nil {
msg = message.NewMsg(nil)
}
p := &Container{ctx: ctx, msg: msg, Params: Params{Ops: new(Ops)}}
c, cancel := context.WithCancel(ctx)
p.cancel = cancel
p.cmd = exec.CommandContext(c, MustExecutable(msg))
return p
}
// NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields.
func NewCommand(ctx context.Context, pathname *Absolute, name string, args ...string) *Container {
z := New(ctx)
func NewCommand(ctx context.Context, msg message.Msg, pathname *check.Absolute, name string, args ...string) *Container {
z := New(ctx, msg)
z.Path = pathname
z.Args = append([]string{name}, args...)
return z

View File

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

View File

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

View File

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

124
container/errors.go Normal file
View File

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

179
container/errors_test.go Normal file
View File

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

View File

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

View File

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

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

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

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

@@ -0,0 +1,38 @@
// 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/"
// 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

@@ -3,6 +3,7 @@ package container
import (
"errors"
"fmt"
"log"
"os"
"os/exec"
"path"
@@ -11,7 +12,9 @@ import (
. "syscall"
"time"
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp"
"hakurei.app/message"
)
const (
@@ -29,7 +32,7 @@ const (
it should be noted that none of this should become relevant at any point since the resulting
intermediate root tmpfs should be effectively anonymous */
intermediateHostPath = FHSProc + "self/fd"
intermediateHostPath = fhs.Proc + "self/fd"
// setup params file descriptor
setupEnv = "HAKUREI_SETUP"
@@ -47,7 +50,9 @@ type (
// apply is called in intermediate root.
apply(state *setupState, k syscallDispatcher) error
prefix() string
// prefix returns a log message prefix, and whether this Op prints no identifying message on its own.
prefix() (string, bool)
Is(op Op) bool
Valid() bool
fmt.Stringer
@@ -57,6 +62,7 @@ type (
setupState struct {
nonrepeatable uintptr
*Params
message.Msg
}
)
@@ -68,6 +74,16 @@ const (
nrAutoRoot
)
// OpRepeatError is returned applying a repeated nonrepeatable [Op].
type OpRepeatError string
func (e OpRepeatError) Error() string { return string(e) + " is not repeatable" }
// OpStateError indicates an impossible internal state has been reached in an [Op].
type OpStateError string
func (o OpStateError) Error() string { return "impossible " + string(o) + " state reached" }
// initParams are params passed from parent.
type initParams struct {
Params
@@ -79,20 +95,23 @@ type initParams struct {
Verbose bool
}
func Init(prepareLogger func(prefix string), setVerbose func(verbose bool)) {
initEntrypoint(direct{}, prepareLogger, setVerbose)
// Init is called by [TryArgv0] if the current process is the container init.
func Init(msg message.Msg) {
if msg == nil {
panic("attempting to call initEntrypoint with nil msg")
}
initEntrypoint(direct{}, msg)
}
func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setVerbose func(verbose bool)) {
func initEntrypoint(k syscallDispatcher, msg message.Msg) {
k.lockOSThread()
prepareLogger("init")
if k.getpid() != 1 {
k.fatal("this process must run as pid 1")
k.fatal(msg, "this process must run as pid 1")
}
if err := k.setPtracer(0); err != nil {
k.verbosef("cannot enable ptrace protection via Yama LSM: %v", err)
msg.Verbosef("cannot enable ptrace protection via Yama LSM: %v", err)
// not fatal: this program has no additional privileges at initial program start
}
@@ -104,65 +123,65 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
)
if f, err := k.receive(setupEnv, &params, &setupFd); err != nil {
if errors.Is(err, EBADF) {
k.fatal("invalid setup descriptor")
k.fatal(msg, "invalid setup descriptor")
}
if errors.Is(err, ErrNotSet) {
k.fatal("HAKUREI_SETUP not set")
if errors.Is(err, ErrReceiveEnv) {
k.fatal(msg, setupEnv+" not set")
}
k.fatalf("cannot decode init setup payload: %v", err)
k.fatalf(msg, "cannot decode init setup payload: %v", err)
} else {
if params.Ops == nil {
k.fatal("invalid setup parameters")
k.fatal(msg, "invalid setup parameters")
}
if params.ParentPerm == 0 {
params.ParentPerm = 0755
}
setVerbose(params.Verbose)
k.verbose("received setup parameters")
msg.SwapVerbose(params.Verbose)
msg.Verbose("received setup parameters")
closeSetup = f
offsetSetup = int(setupFd + 1)
}
// write uid/gid map here so parent does not need to set dumpable
if err := k.setDumpable(SUID_DUMP_USER); err != nil {
k.fatalf("cannot set SUID_DUMP_USER: %v", err)
k.fatalf(msg, "cannot set SUID_DUMP_USER: %v", err)
}
if err := k.writeFile(FHSProc+"self/uid_map",
if err := k.writeFile(fhs.Proc+"self/uid_map",
append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...),
0); err != nil {
k.fatalf("%v", err)
k.fatalf(msg, "%v", err)
}
if err := k.writeFile(FHSProc+"self/setgroups",
if err := k.writeFile(fhs.Proc+"self/setgroups",
[]byte("deny\n"),
0); err != nil && !os.IsNotExist(err) {
k.fatalf("%v", err)
k.fatalf(msg, "%v", err)
}
if err := k.writeFile(FHSProc+"self/gid_map",
if err := k.writeFile(fhs.Proc+"self/gid_map",
append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...),
0); err != nil {
k.fatalf("%v", err)
k.fatalf(msg, "%v", err)
}
if err := k.setDumpable(SUID_DUMP_DISABLE); err != nil {
k.fatalf("cannot set SUID_DUMP_DISABLE: %v", err)
k.fatalf(msg, "cannot set SUID_DUMP_DISABLE: %v", err)
}
oldmask := k.umask(0)
if params.Hostname != "" {
if err := k.sethostname([]byte(params.Hostname)); err != nil {
k.fatalf("cannot set hostname: %v", err)
k.fatalf(msg, "cannot set hostname: %v", err)
}
}
// cache sysctl before pivot_root
lastcap := k.lastcap()
lastcap := k.lastcap(msg)
if err := k.mount(zeroString, FHSRoot, zeroString, MS_SILENT|MS_SLAVE|MS_REC, zeroString); err != nil {
k.fatalf("cannot make / rslave: %v", err)
if err := k.mount(zeroString, fhs.Root, zeroString, MS_SILENT|MS_SLAVE|MS_REC, zeroString); err != nil {
k.fatalf(msg, "cannot make / rslave: %v", optionalErrorUnwrap(err))
}
state := &setupState{Params: &params.Params}
state := &setupState{Params: &params.Params, Msg: msg}
/* early is called right before pivot_root into intermediate root;
this step is mostly for gathering information that would otherwise be difficult to obtain
@@ -170,40 +189,41 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
the state of the mount namespace */
for i, op := range *params.Ops {
if op == nil || !op.Valid() {
k.fatalf("invalid op at index %d", i)
k.fatalf(msg, "invalid op at index %d", i)
}
if err := op.early(state, k); err != nil {
k.printBaseErr(err,
fmt.Sprintf("cannot prepare op at index %d:", i))
k.beforeExit()
k.exit(1)
if m, ok := messageFromError(err); ok {
k.fatal(msg, m)
} else {
k.fatalf(msg, "cannot prepare op at index %d: %v", i, err)
}
}
}
if err := k.mount(SourceTmpfsRootfs, intermediateHostPath, FstypeTmpfs, MS_NODEV|MS_NOSUID, zeroString); err != nil {
k.fatalf("cannot mount intermediate root: %v", err)
k.fatalf(msg, "cannot mount intermediate root: %v", optionalErrorUnwrap(err))
}
if err := k.chdir(intermediateHostPath); err != nil {
k.fatalf("cannot enter intermediate host path: %v", err)
k.fatalf(msg, "cannot enter intermediate host path: %v", err)
}
if err := k.mkdir(sysrootDir, 0755); err != nil {
k.fatalf("%v", err)
k.fatalf(msg, "%v", err)
}
if err := k.mount(sysrootDir, sysrootDir, zeroString, MS_SILENT|MS_BIND|MS_REC, zeroString); err != nil {
k.fatalf("cannot bind sysroot: %v", err)
k.fatalf(msg, "cannot bind sysroot: %v", optionalErrorUnwrap(err))
}
if err := k.mkdir(hostDir, 0755); err != nil {
k.fatalf("%v", err)
k.fatalf(msg, "%v", err)
}
// pivot_root uncovers intermediateHostPath in hostDir
if err := k.pivotRoot(intermediateHostPath, hostDir); err != nil {
k.fatalf("cannot pivot into intermediate root: %v", err)
k.fatalf(msg, "cannot pivot into intermediate root: %v", err)
}
if err := k.chdir(FHSRoot); err != nil {
k.fatalf("cannot enter intermediate root: %v", err)
if err := k.chdir(fhs.Root); err != nil {
k.fatalf(msg, "cannot enter intermediate root: %v", err)
}
/* apply is called right after pivot_root and entering the new root;
@@ -212,62 +232,65 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
chdir is allowed but discouraged */
for i, op := range *params.Ops {
// ops already checked during early setup
k.verbosef("%s %s", op.prefix(), op)
if prefix, ok := op.prefix(); ok {
msg.Verbosef("%s %s", prefix, op)
}
if err := op.apply(state, k); err != nil {
k.printBaseErr(err,
fmt.Sprintf("cannot apply op at index %d:", i))
k.beforeExit()
k.exit(1)
if m, ok := messageFromError(err); ok {
k.fatal(msg, m)
} else {
k.fatalf(msg, "cannot apply op at index %d: %v", i, err)
}
}
}
// setup requiring host root complete at this point
if err := k.mount(hostDir, hostDir, zeroString, MS_SILENT|MS_REC|MS_PRIVATE, zeroString); err != nil {
k.fatalf("cannot make host root rprivate: %v", err)
k.fatalf(msg, "cannot make host root rprivate: %v", optionalErrorUnwrap(err))
}
if err := k.unmount(hostDir, MNT_DETACH); err != nil {
k.fatalf("cannot unmount host root: %v", err)
k.fatalf(msg, "cannot unmount host root: %v", err)
}
{
var fd int
if err := IgnoringEINTR(func() (err error) {
fd, err = k.open(FHSRoot, O_DIRECTORY|O_RDONLY, 0)
fd, err = k.open(fhs.Root, O_DIRECTORY|O_RDONLY, 0)
return
}); err != nil {
k.fatalf("cannot open intermediate root: %v", err)
k.fatalf(msg, "cannot open intermediate root: %v", err)
}
if err := k.chdir(sysrootPath); err != nil {
k.fatalf("cannot enter sysroot: %v", err)
k.fatalf(msg, "cannot enter sysroot: %v", err)
}
if err := k.pivotRoot(".", "."); err != nil {
k.fatalf("cannot pivot into sysroot: %v", err)
k.fatalf(msg, "cannot pivot into sysroot: %v", err)
}
if err := k.fchdir(fd); err != nil {
k.fatalf("cannot re-enter intermediate root: %v", err)
k.fatalf(msg, "cannot re-enter intermediate root: %v", err)
}
if err := k.unmount(".", MNT_DETACH); err != nil {
k.fatalf("cannot unmount intermediate root: %v", err)
k.fatalf(msg, "cannot unmount intermediate root: %v", err)
}
if err := k.chdir(FHSRoot); err != nil {
k.fatalf("cannot enter root: %v", err)
if err := k.chdir(fhs.Root); err != nil {
k.fatalf(msg, "cannot enter root: %v", err)
}
if err := k.close(fd); err != nil {
k.fatalf("cannot close intermediate root: %v", err)
k.fatalf(msg, "cannot close intermediate root: %v", err)
}
}
if err := k.capAmbientClearAll(); err != nil {
k.fatalf("cannot clear the ambient capability set: %v", err)
k.fatalf(msg, "cannot clear the ambient capability set: %v", err)
}
for i := uintptr(0); i <= lastcap; i++ {
if params.Privileged && i == CAP_SYS_ADMIN {
continue
}
if err := k.capBoundingSetDrop(i); err != nil {
k.fatalf("cannot drop capability from bounding set: %v", err)
k.fatalf(msg, "cannot drop capability from bounding set: %v", err)
}
}
@@ -276,29 +299,29 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN)
if err := k.capAmbientRaise(CAP_SYS_ADMIN); err != nil {
k.fatalf("cannot raise CAP_SYS_ADMIN: %v", err)
k.fatalf(msg, "cannot raise CAP_SYS_ADMIN: %v", err)
}
}
if err := k.capset(
&capHeader{_LINUX_CAPABILITY_VERSION_3, 0},
&[2]capData{{0, keep[0], keep[0]}, {0, keep[1], keep[1]}},
); err != nil {
k.fatalf("cannot capset: %v", err)
k.fatalf(msg, "cannot capset: %v", err)
}
if !params.SeccompDisable {
rules := params.SeccompRules
if len(rules) == 0 { // non-empty rules slice always overrides presets
k.verbosef("resolving presets %#x", params.SeccompPresets)
msg.Verbosef("resolving presets %#x", params.SeccompPresets)
rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags)
}
if err := k.seccompLoad(rules, params.SeccompFlags); err != nil {
// this also indirectly asserts PR_SET_NO_NEW_PRIVS
k.fatalf("cannot load syscall filter: %v", err)
k.fatalf(msg, "cannot load syscall filter: %v", err)
}
k.verbosef("%d filter rules loaded", len(rules))
msg.Verbosef("%d filter rules loaded", len(rules))
} else {
k.verbose("syscall filter not configured")
msg.Verbose("syscall filter not configured")
}
extraFiles := make([]*os.File, params.Count)
@@ -315,14 +338,14 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
cmd.ExtraFiles = extraFiles
cmd.Dir = params.Dir.String()
k.verbosef("starting initial program %s", params.Path)
msg.Verbosef("starting initial program %s", params.Path)
if err := k.start(cmd); err != nil {
k.fatalf("%v", err)
k.fatalf(msg, "%v", err)
}
k.suspend()
msg.Suspend()
if err := closeSetup(); err != nil {
k.printf("cannot close setup pipe: %v", err)
k.printf(msg, "cannot close setup pipe: %v", err)
// not fatal
}
@@ -356,7 +379,7 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
}
}
if !errors.Is(err, ECHILD) {
k.printf("unexpected wait4 response: %v", err)
k.printf(msg, "unexpected wait4 response: %v", err)
}
close(done)
@@ -373,50 +396,50 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
for {
select {
case s := <-sig:
if k.resume() {
k.verbosef("%s after process start", s.String())
if msg.Resume() {
msg.Verbosef("%s after process start", s.String())
} else {
k.verbosef("got %s", s.String())
msg.Verbosef("got %s", s.String())
}
if s == CancelSignal && params.ForwardCancel && cmd.Process != nil {
k.verbose("forwarding context cancellation")
msg.Verbose("forwarding context cancellation")
if err := k.signal(cmd, os.Interrupt); err != nil {
k.printf("cannot forward cancellation: %v", err)
k.printf(msg, "cannot forward cancellation: %v", err)
}
continue
}
k.beforeExit()
msg.BeforeExit()
k.exit(0)
case w := <-info:
if w.wpid == cmd.Process.Pid {
// initial process exited, output is most likely available again
k.resume()
msg.Resume()
switch {
case w.wstatus.Exited():
r = w.wstatus.ExitStatus()
k.verbosef("initial process exited with code %d", w.wstatus.ExitStatus())
msg.Verbosef("initial process exited with code %d", w.wstatus.ExitStatus())
case w.wstatus.Signaled():
r = 128 + int(w.wstatus.Signal())
k.verbosef("initial process exited with signal %s", w.wstatus.Signal())
msg.Verbosef("initial process exited with signal %s", w.wstatus.Signal())
default:
r = 255
k.verbosef("initial process exited with status %#x", w.wstatus)
msg.Verbosef("initial process exited with status %#x", w.wstatus)
}
go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
}
case <-done:
k.beforeExit()
msg.BeforeExit()
k.exit(r)
case <-timeout:
k.printf("timeout exceeded waiting for lingering processes")
k.beforeExit()
k.printf(msg, "timeout exceeded waiting for lingering processes")
msg.BeforeExit()
k.exit(r)
}
}
@@ -425,10 +448,16 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
const initName = "init"
// TryArgv0 calls [Init] if the last element of argv0 is "init".
func TryArgv0(v Msg, prepare func(prefix string), setVerbose func(verbose bool)) {
// If a nil msg is passed, the system logger is used instead.
func TryArgv0(msg message.Msg) {
if msg == nil {
log.SetPrefix(initName + ": ")
log.SetFlags(0)
msg = message.NewMsg(log.Default())
}
if len(os.Args) > 0 && path.Base(os.Args[0]) == initName {
msg = v
Init(prepare, setVerbose)
Init(msg)
msg.BeforeExit()
os.Exit(0)
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,8 +29,8 @@ func TestSetupReceive(t *testing.T) {
})
}
if _, err := container.Receive(key, nil, nil); !errors.Is(err, container.ErrNotSet) {
t.Errorf("Receive: error = %v, want %v", err, container.ErrNotSet)
if _, err := container.Receive(key, nil, nil); !errors.Is(err, container.ErrReceiveEnv) {
t.Errorf("Receive: error = %v, want %v", err, container.ErrReceiveEnv)
}
})
@@ -38,8 +38,8 @@ func TestSetupReceive(t *testing.T) {
const key = "TEST_ENV_FORMAT"
t.Setenv(key, "")
if _, err := container.Receive(key, nil, nil); !errors.Is(err, container.ErrFdFormat) {
t.Errorf("Receive: error = %v, want %v", err, container.ErrFdFormat)
if _, err := container.Receive(key, nil, nil); !errors.Is(err, strconv.ErrSyntax) {
t.Errorf("Receive: error = %v, want %v", err, strconv.ErrSyntax)
}
})
@@ -47,8 +47,8 @@ func TestSetupReceive(t *testing.T) {
const key = "TEST_ENV_RANGE"
t.Setenv(key, "-1")
if _, err := container.Receive(key, nil, nil); !errors.Is(err, syscall.EBADF) {
t.Errorf("Receive: error = %v, want %v", err, syscall.EBADF)
if _, err := container.Receive(key, nil, nil); !errors.Is(err, syscall.EDOM) {
t.Errorf("Receive: error = %v, want %v", err, syscall.EDOM)
}
})

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,13 +3,14 @@ package seccomp_test
import (
"encoding/hex"
"hakurei.app/container/bits"
"hakurei.app/container/seccomp"
)
type (
bpfPreset = struct {
seccomp.ExportFlag
seccomp.FilterPreset
bits.FilterPreset
}
bpfLookup map[bpfPreset][]byte
)

View File

@@ -117,7 +117,7 @@ func Export(fd int, rules []NativeRule, flags ExportFlag) error {
var ret C.int
rulesPinner := new(runtime.Pinner)
var rulesPinner runtime.Pinner
for i := range rules {
rule := &rules[i]
rulesPinner.Pin(rule)
@@ -189,6 +189,5 @@ func syscallResolveName(s string) (trap int) {
v := C.CString(s)
trap = int(C.seccomp_syscall_resolve_name(v))
C.free(unsafe.Pointer(v))
return
}

View File

@@ -8,10 +8,13 @@ import (
"syscall"
"testing"
. "hakurei.app/container/bits"
. "hakurei.app/container/seccomp"
)
func TestExport(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
flags ExportFlag
@@ -31,14 +34,15 @@ func TestExport(t *testing.T) {
{"hakurei tty", 0, PresetExt | PresetDenyNS | PresetDenyDevel, false},
}
buf := make([]byte, 8)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
e := New(Preset(tc.presets, tc.flags), tc.flags)
want := bpfExpected[bpfPreset{tc.flags, tc.presets}]
digest := sha512.New()
if _, err := io.CopyBuffer(digest, e, buf); (err != nil) != tc.wantErr {
if _, err := io.Copy(digest, e); (err != nil) != tc.wantErr {
t.Errorf("Exporter: error = %v, wantErr %v", err, tc.wantErr)
return
}
@@ -46,7 +50,7 @@ func TestExport(t *testing.T) {
t.Errorf("Close: error = %v", err)
}
if got := digest.Sum(nil); !slices.Equal(got, want) {
t.Fatalf("Export() hash = %x, want %x",
t.Fatalf("Export: hash = %x, want %x",
got, want)
return
}

View File

@@ -4,46 +4,33 @@ package seccomp
import (
. "syscall"
"hakurei.app/container/bits"
)
type FilterPreset int
const (
// PresetExt are project-specific extensions.
PresetExt FilterPreset = 1 << iota
// PresetDenyNS denies namespace setup syscalls.
PresetDenyNS
// PresetDenyTTY denies faking input.
PresetDenyTTY
// PresetDenyDevel denies development-related syscalls.
PresetDenyDevel
// PresetLinux32 sets PER_LINUX32.
PresetLinux32
)
func Preset(presets FilterPreset, flags ExportFlag) (rules []NativeRule) {
func Preset(presets bits.FilterPreset, flags ExportFlag) (rules []NativeRule) {
allowedPersonality := PER_LINUX
if presets&PresetLinux32 != 0 {
if presets&bits.PresetLinux32 != 0 {
allowedPersonality = PER_LINUX32
}
presetDevelFinal := presetDevel(ScmpDatum(allowedPersonality))
l := len(presetCommon)
if presets&PresetDenyNS != 0 {
if presets&bits.PresetDenyNS != 0 {
l += len(presetNamespace)
}
if presets&PresetDenyTTY != 0 {
if presets&bits.PresetDenyTTY != 0 {
l += len(presetTTY)
}
if presets&PresetDenyDevel != 0 {
if presets&bits.PresetDenyDevel != 0 {
l += len(presetDevelFinal)
}
if flags&AllowMultiarch == 0 {
l += len(presetEmu)
}
if presets&PresetExt != 0 {
if presets&bits.PresetExt != 0 {
l += len(presetCommonExt)
if presets&PresetDenyNS != 0 {
if presets&bits.PresetDenyNS != 0 {
l += len(presetNamespaceExt)
}
if flags&AllowMultiarch == 0 {
@@ -53,21 +40,21 @@ func Preset(presets FilterPreset, flags ExportFlag) (rules []NativeRule) {
rules = make([]NativeRule, 0, l)
rules = append(rules, presetCommon...)
if presets&PresetDenyNS != 0 {
if presets&bits.PresetDenyNS != 0 {
rules = append(rules, presetNamespace...)
}
if presets&PresetDenyTTY != 0 {
if presets&bits.PresetDenyTTY != 0 {
rules = append(rules, presetTTY...)
}
if presets&PresetDenyDevel != 0 {
if presets&bits.PresetDenyDevel != 0 {
rules = append(rules, presetDevelFinal...)
}
if flags&AllowMultiarch == 0 {
rules = append(rules, presetEmu...)
}
if presets&PresetExt != 0 {
if presets&bits.PresetExt != 0 {
rules = append(rules, presetCommonExt...)
if presets&PresetDenyNS != 0 {
if presets&bits.PresetDenyNS != 0 {
rules = append(rules, presetNamespaceExt...)
}
if flags&AllowMultiarch == 0 {

View File

@@ -8,10 +8,6 @@ import (
"hakurei.app/helper/proc"
)
const (
PresetStrict = PresetExt | PresetDenyNS | PresetDenyTTY | PresetDenyDevel
)
// New returns an inactive Encoder instance.
func New(rules []NativeRule, flags ExportFlag) *Encoder { return &Encoder{newExporter(rules, flags)} }
@@ -25,9 +21,7 @@ Methods of Encoder are not safe for concurrent use.
An Encoder must not be copied after first use.
*/
type Encoder struct {
*exporter
}
type Encoder struct{ *exporter }
func (e *Encoder) Read(p []byte) (n int, err error) {
if err = e.prepare(); err != nil {

View File

@@ -10,6 +10,8 @@ import (
)
func TestLibraryError(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
sample *seccomp.LibraryError
@@ -41,6 +43,8 @@ func TestLibraryError(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if errors.Is(tc.sample, tc.compare) != tc.wantIs {
t.Errorf("errors.Is(%#v, %#v) did not return %v",
tc.sample, tc.compare, tc.wantIs)
@@ -54,6 +58,8 @@ func TestLibraryError(t *testing.T) {
}
t.Run("invalid", func(t *testing.T) {
t.Parallel()
wantPanic := "invalid libseccomp error"
defer func() {
if r := recover(); r != wantPanic {

View File

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

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

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

View File

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

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

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

View File

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

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

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

109
container/stub/exit_test.go Normal file
View File

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

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

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

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

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

View File

@@ -2,10 +2,12 @@ package container
import (
"bytes"
"log"
"os"
"strconv"
"sync"
"hakurei.app/container/fhs"
"hakurei.app/message"
)
var (
@@ -17,31 +19,33 @@ var (
)
const (
kernelOverflowuidPath = FHSProcSys + "kernel/overflowuid"
kernelOverflowgidPath = FHSProcSys + "kernel/overflowgid"
kernelCapLastCapPath = FHSProcSys + "kernel/cap_last_cap"
kernelOverflowuidPath = fhs.ProcSys + "kernel/overflowuid"
kernelOverflowgidPath = fhs.ProcSys + "kernel/overflowgid"
kernelCapLastCapPath = fhs.ProcSys + "kernel/cap_last_cap"
)
func mustReadSysctl() {
if v, err := os.ReadFile(kernelOverflowuidPath); err != nil {
log.Fatalf("cannot read %q: %v", kernelOverflowuidPath, err)
} else if kernelOverflowuid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
log.Fatalf("cannot interpret %q: %v", kernelOverflowuidPath, err)
}
func mustReadSysctl(msg message.Msg) {
sysctlOnce.Do(func() {
if v, err := os.ReadFile(kernelOverflowuidPath); err != nil {
msg.GetLogger().Fatalf("cannot read %q: %v", kernelOverflowuidPath, err)
} else if kernelOverflowuid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
msg.GetLogger().Fatalf("cannot interpret %q: %v", kernelOverflowuidPath, err)
}
if v, err := os.ReadFile(kernelOverflowgidPath); err != nil {
log.Fatalf("cannot read %q: %v", kernelOverflowgidPath, err)
} else if kernelOverflowgid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
log.Fatalf("cannot interpret %q: %v", kernelOverflowgidPath, err)
}
if v, err := os.ReadFile(kernelOverflowgidPath); err != nil {
msg.GetLogger().Fatalf("cannot read %q: %v", kernelOverflowgidPath, err)
} else if kernelOverflowgid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
msg.GetLogger().Fatalf("cannot interpret %q: %v", kernelOverflowgidPath, err)
}
if v, err := os.ReadFile(kernelCapLastCapPath); err != nil {
log.Fatalf("cannot read %q: %v", kernelCapLastCapPath, err)
} else if kernelCapLastCap, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
log.Fatalf("cannot interpret %q: %v", kernelCapLastCapPath, err)
}
if v, err := os.ReadFile(kernelCapLastCapPath); err != nil {
msg.GetLogger().Fatalf("cannot read %q: %v", kernelCapLastCapPath, err)
} else if kernelCapLastCap, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
msg.GetLogger().Fatalf("cannot interpret %q: %v", kernelCapLastCapPath, err)
}
})
}
func OverflowUid() int { sysctlOnce.Do(mustReadSysctl); return kernelOverflowuid }
func OverflowGid() int { sysctlOnce.Do(mustReadSysctl); return kernelOverflowgid }
func LastCap() uintptr { sysctlOnce.Do(mustReadSysctl); return uintptr(kernelCapLastCap) }
func OverflowUid(msg message.Msg) int { mustReadSysctl(msg); return kernelOverflowuid }
func OverflowGid(msg message.Msg) int { mustReadSysctl(msg); return kernelOverflowgid }
func LastCap(msg message.Msg) uintptr { mustReadSysctl(msg); return uintptr(kernelCapLastCap) }

View File

@@ -7,6 +7,8 @@ import (
)
func TestUnmangle(t *testing.T) {
t.Parallel()
testCases := []struct {
want string
sample string
@@ -17,6 +19,7 @@ func TestUnmangle(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.want, func(t *testing.T) {
t.Parallel()
got := vfs.Unmangle(tc.sample)
if got != tc.want {
t.Errorf("Unmangle: %q, want %q",

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,17 @@
package vfs_test
import (
"errors"
"reflect"
"slices"
"strings"
"syscall"
"testing"
"hakurei.app/container/vfs"
)
func TestUnfold(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
sample string
@@ -26,7 +26,7 @@ func TestUnfold(t *testing.T) {
"no match",
sampleMountinfoBase,
"/mnt",
syscall.ESTALE, nil, nil, nil,
&vfs.DecoderError{Op: "unfold", Line: -1, Err: vfs.UnfoldTargetError("/mnt")}, nil, nil, nil,
},
{
"cover",
@@ -52,10 +52,12 @@ func TestUnfold(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
got, err := d.Unfold(tc.target)
if !errors.Is(err, tc.wantErr) {
if !reflect.DeepEqual(err, tc.wantErr) {
t.Errorf("Unfold: error = %v, wantErr %v",
err, tc.wantErr)
}

4
dist/comp/_hakurei vendored
View File

@@ -54,8 +54,8 @@ __hakurei_instances() {
{
local -a _hakurei_cmds
_hakurei_cmds=(
"app:Load app from configuration file"
"run:Configure and start a permissive default sandbox"
"app:Load and start container from configuration file"
"run:Configure and start a permissive container"
"show:Show live or local app configuration"
"ps:List active instances"
"version:Display version information"

2
dist/install.sh vendored
View File

@@ -4,7 +4,7 @@ cd "$(dirname -- "$0")" || exit 1
install -vDm0755 "bin/hakurei" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hakurei"
install -vDm0755 "bin/hpkg" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hpkg"
install -vDm6511 "bin/hsu" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hsu"
install -vDm4511 "bin/hsu" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hsu"
if [ ! -f "${HAKUREI_INSTALL_PREFIX}/etc/hsurc" ]; then
install -vDm0400 "hsurc.default" "${HAKUREI_INSTALL_PREFIX}/etc/hsurc"
fi

12
flake.lock generated
View File

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

View File

@@ -11,27 +11,29 @@ import (
)
func TestArgsString(t *testing.T) {
t.Parallel()
wantString := strings.Join(wantArgs, " ")
if got := argsWt.(fmt.Stringer).String(); got != wantString {
t.Errorf("String: %q, want %q",
got, wantString)
t.Errorf("String: %q, want %q", got, wantString)
}
}
func TestNewCheckedArgs(t *testing.T) {
t.Parallel()
args := []string{"\x00"}
if _, err := helper.NewCheckedArgs(args); !errors.Is(err, syscall.EINVAL) {
t.Errorf("NewCheckedArgs: error = %v, wantErr %v",
err, syscall.EINVAL)
t.Errorf("NewCheckedArgs: error = %v, wantErr %v", err, syscall.EINVAL)
}
t.Run("must panic", func(t *testing.T) {
t.Parallel()
badPayload := []string{"\x00"}
defer func() {
wantPanic := "invalid argument"
if r := recover(); r != wantPanic {
t.Errorf("MustNewCheckedArgs: panic = %v, wantPanic %v",
r, wantPanic)
t.Errorf("MustNewCheckedArgs: panic = %v, wantPanic %v", r, wantPanic)
}
}()
helper.MustNewCheckedArgs(badPayload)

View File

@@ -10,13 +10,16 @@ import (
"sync"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/helper/proc"
"hakurei.app/message"
)
// New initialises a Helper instance with wt as the null-terminated argument writer.
func New(
ctx context.Context,
pathname *container.Absolute, name string,
msg message.Msg,
pathname *check.Absolute, name string,
wt io.WriterTo,
stat bool,
argF func(argsFd, statFd int) []string,
@@ -26,7 +29,7 @@ func New(
var args []string
h := new(helperContainer)
h.helperFiles, args = newHelperFiles(ctx, wt, stat, argF, extraFiles)
h.Container = container.NewCommand(ctx, pathname, name, args...)
h.Container = container.NewCommand(ctx, msg, pathname, name, args...)
h.WaitDelay = WaitDelay
if cmdF != nil {
cmdF(h.Container)

View File

@@ -7,14 +7,16 @@ import (
"testing"
"hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/helper"
)
func TestContainer(t *testing.T) {
t.Run("start empty container", func(t *testing.T) {
h := helper.New(t.Context(), container.MustAbs(container.Nonexistent), "hakurei", argsWt, false, argF, nil, nil)
t.Run("start invalid container", func(t *testing.T) {
h := helper.New(t.Context(), nil, check.MustAbs(container.Nonexistent), "hakurei", argsWt, false, argF, nil, nil)
wantErr := "container: starting an empty container"
wantErr := "container: starting an invalid container"
if err := h.Start(); err == nil || err.Error() != wantErr {
t.Errorf("Start: error = %v, wantErr %q",
err, wantErr)
@@ -22,7 +24,7 @@ func TestContainer(t *testing.T) {
})
t.Run("valid new helper nil check", func(t *testing.T) {
if got := helper.New(t.Context(), container.MustAbs(container.Nonexistent), "hakurei", argsWt, false, argF, nil, nil); got == nil {
if got := helper.New(t.Context(), nil, check.MustAbs(container.Nonexistent), "hakurei", argsWt, false, argF, nil, nil); got == nil {
t.Errorf("New(%q, %q) got nil",
argsWt, "hakurei")
return
@@ -31,12 +33,12 @@ func TestContainer(t *testing.T) {
t.Run("implementation compliance", func(t *testing.T) {
testHelper(t, func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper {
return helper.New(ctx, container.MustAbs(os.Args[0]), "helper", argsWt, stat, argF, func(z *container.Container) {
return helper.New(ctx, nil, check.MustAbs(os.Args[0]), "helper", argsWt, stat, argF, func(z *container.Container) {
setOutput(&z.Stdout, &z.Stderr)
z.
Bind(container.AbsFHSRoot, container.AbsFHSRoot, 0).
Proc(container.AbsFHSProc).
Dev(container.AbsFHSDev, true)
Bind(fhs.AbsRoot, fhs.AbsRoot, 0).
Proc(fhs.AbsProc).
Dev(fhs.AbsDev, true)
}, nil)
})
})

View File

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

View File

@@ -6,11 +6,18 @@ import (
"os/exec"
"sync/atomic"
"syscall"
"testing"
"time"
)
var FulfillmentTimeout = 2 * time.Second
func init() {
if testing.Testing() {
FulfillmentTimeout *= 10
}
}
// A File is an extra file with deferred initialisation.
type File interface {
// Init initialises File state. Init must not be called more than once.

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