60 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
196 changed files with 6995 additions and 4802 deletions

View File

@@ -2,47 +2,69 @@ package main
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"io" "io"
"log" "log"
"os" "os"
"os/exec"
"os/user" "os/user"
"strconv" "strconv"
"sync" "sync"
"time" "time"
_ "unsafe"
"hakurei.app/command" "hakurei.app/command"
"hakurei.app/container" "hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal" "hakurei.app/internal"
"hakurei.app/internal/app" "hakurei.app/internal/app"
"hakurei.app/internal/app/state" "hakurei.app/internal/app/state"
"hakurei.app/internal/hlog" "hakurei.app/message"
"hakurei.app/system"
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
) )
func buildCommand(ctx context.Context, 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 ( var (
flagVerbose bool flagVerbose bool
flagJSON 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(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable") Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")
c.Command("shim", command.UsageInternal, func([]string) error { app.ShimMain(); return errSuccess }) c.Command("shim", command.UsageInternal, func([]string) error { app.ShimMain(); return errSuccess })
c.Command("app", "Load app from configuration file", func(args []string) error { c.Command("app", "Load and start container from configuration file", func(args []string) error {
if len(args) < 1 { if len(args) < 1 {
log.Fatal("app requires at least 1 argument") log.Fatal("app requires at least 1 argument")
} }
// config extraArgs... // config extraArgs...
config := tryPath(args[0]) config := tryPath(msg, args[0])
config.Args = append(config.Args, args[1:]...) if config != nil && config.Container != nil {
config.Container.Args = append(config.Container.Args, args[1:]...)
}
app.Main(ctx, config) app.Main(ctx, msg, config)
panic("unreachable") panic("unreachable")
}) })
@@ -62,14 +84,8 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
flagWayland, flagX11, flagDBus, flagPulse bool flagWayland, flagX11, flagDBus, flagPulse bool
) )
c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error { c.NewCommand("run", "Configure and start a permissive container", func(args []string) error {
// initialise config from flags if flagIdentity < hst.IdentityMin || flagIdentity > hst.IdentityMax {
config := &hst.Config{
ID: flagID,
Args: args,
}
if flagIdentity < 0 || flagIdentity > 9999 {
log.Fatalf("identity %d out of range", flagIdentity) log.Fatalf("identity %d out of range", flagIdentity)
} }
@@ -78,15 +94,15 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
passwd *user.User passwd *user.User
passwdOnce sync.Once passwdOnce sync.Once
passwdFunc = func() { passwdFunc = func() {
us := strconv.Itoa(app.HsuUid(new(app.Hsu).MustID(), flagIdentity)) us := strconv.Itoa(app.HsuUid(new(app.Hsu).MustIDMsg(msg), flagIdentity))
if u, err := user.LookupId(us); err != nil { 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{ passwd = &user.User{
Uid: us, Uid: us,
Gid: us, Gid: us,
Username: "chronos", Username: "chronos",
Name: "Hakurei Permissive Default", Name: "Hakurei Permissive Default",
HomeDir: container.FHSVarEmpty, HomeDir: fhs.VarEmpty,
} }
} else { } else {
passwd = u passwd = u
@@ -94,60 +110,128 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
} }
) )
if flagHomeDir == "os" { // paths are identical, resolve inner shell and program path
passwdOnce.Do(passwdFunc) shell := fhs.AbsRoot.Append("bin", "sh")
flagHomeDir = passwd.HomeDir 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
}
} }
if flagUserName == "chronos" { var et hst.Enablement
passwdOnce.Do(passwdFunc)
flagUserName = passwd.Username
}
config.Identity = flagIdentity
config.Groups = flagGroups
config.Username = flagUserName
if a, err := container.NewAbs(flagHomeDir); err != nil {
log.Fatal(err.Error())
return err
} else {
config.Home = a
}
var e system.Enablement
if flagWayland { if flagWayland {
e |= system.EWayland et |= hst.EWayland
} }
if flagX11 { if flagX11 {
e |= system.EX11 et |= hst.EX11
} }
if flagDBus { if flagDBus {
e |= system.EDBus et |= hst.EDBus
} }
if flagPulse { if flagPulse {
e |= system.EPulse 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)
config.Container.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.Enablements = hst.NewEnablements(e)
// parse D-Bus config file from flags if applicable // parse D-Bus config file from flags if applicable
if flagDBus { if flagDBus {
if flagDBusConfigSession == "builtin" { if flagDBusConfigSession == "builtin" {
config.SessionBus = dbus.NewConfig(flagID, true, flagDBusMpris) config.SessionBus = dbus.NewConfig(flagID, true, flagDBusMpris)
} else { } else {
if conf, err := dbus.NewConfigFromFile(flagDBusConfigSession); err != nil { 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) log.Fatalf("cannot load session bus proxy config from %q: %s", flagDBusConfigSession, err)
} else {
config.SessionBus = conf
} }
} }
// system bus proxy is optional // system bus proxy is optional
if flagDBusConfigSystem != "nil" { if flagDBusConfigSystem != "nil" {
if conf, err := dbus.NewConfigFromFile(flagDBusConfigSystem); err != 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) log.Fatalf("cannot load system bus proxy config from %q: %s", flagDBusConfigSystem, err)
} else {
config.SystemBus = conf
} }
} }
@@ -162,7 +246,7 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
} }
} }
app.Main(ctx, config) app.Main(ctx, msg, config)
panic("unreachable") panic("unreachable")
}). }).
Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"), Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),
@@ -202,11 +286,13 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
case 1: // instance case 1: // instance
name := args[0] name := args[0]
config, entry := tryShort(name) config, entry := tryShort(msg, name)
if config == nil { if config == nil {
config = tryPath(name) config = tryPath(msg, name)
}
if !printShowInstance(os.Stdout, time.Now().UTC(), entry, config, flagShort, flagJSON) {
os.Exit(1)
} }
printShowInstance(os.Stdout, time.Now().UTC(), entry, config, flagShort, flagJSON)
default: default:
log.Fatal("show requires 1 argument") log.Fatal("show requires 1 argument")
@@ -219,31 +305,16 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
var flagShort bool var flagShort bool
c.NewCommand("ps", "List active instances", func(args []string) error { c.NewCommand("ps", "List active instances", func(args []string) error {
var sc hst.Paths var sc hst.Paths
app.CopyPaths(&sc, new(app.Hsu).MustID()) app.CopyPaths().Copy(&sc, new(app.Hsu).MustID())
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(sc.RunDirPath.String()), flagShort, flagJSON) printPs(os.Stdout, time.Now().UTC(), state.NewMulti(msg, sc.RunDirPath.String()), flagShort, flagJSON)
return errSuccess return errSuccess
}).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id") }).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id")
} }
c.Command("version", "Display version information", func(args []string) error { c.Command("version", "Display version information", func(args []string) error { fmt.Println(internal.Version()); return errSuccess })
fmt.Println(internal.Version()) c.Command("license", "Show full license text", func(args []string) error { fmt.Println(license); return errSuccess })
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("license", "Show full license text", func(args []string) error {
fmt.Println(license)
return errSuccess
})
c.Command("template", "Produce a config template", func(args []string) error {
printJSON(os.Stdout, false, hst.Template())
return errSuccess
})
c.Command("help", "Show this help message", func([]string) error {
c.PrintHelp()
return errSuccess
})
return c return c
} }

View File

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

View File

@@ -13,8 +13,7 @@ import (
"syscall" "syscall"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/internal" "hakurei.app/message"
"hakurei.app/internal/hlog"
) )
var ( var (
@@ -24,20 +23,20 @@ var (
license string license string
) )
func init() { hlog.Prepare("hakurei") } // earlyHardeningErrs are errors collected while setting up early hardening feature.
type earlyHardeningErrs struct{ yamaLSM, dumpable error }
func main() { func main() {
// early init path, skips root check and duplicate PR_SET_DUMPABLE // early init path, skips root check and duplicate PR_SET_DUMPABLE
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput) container.TryArgv0(nil)
if err := container.SetPtracer(0); err != nil { log.SetPrefix("hakurei: ")
hlog.Verbosef("cannot enable ptrace protection via Yama LSM: %v", err) log.SetFlags(0)
// not fatal: this program runs as the privileged user msg := message.NewMsg(log.Default())
}
if err := container.SetDumpable(container.SUID_DUMP_DISABLE); err != nil { early := earlyHardeningErrs{
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err) yamaLSM: container.SetPtracer(0),
// not fatal: this program runs as the privileged user dumpable: container.SetDumpable(container.SUID_DUMP_DISABLE),
} }
if os.Geteuid() == 0 { if os.Geteuid() == 0 {
@@ -48,10 +47,10 @@ func main() {
syscall.SIGINT, syscall.SIGTERM) syscall.SIGINT, syscall.SIGTERM)
defer stop() // unreachable defer stop() // unreachable
buildCommand(ctx, os.Stderr).MustParse(os.Args[1:], func(err error) { buildCommand(ctx, msg, &early, os.Stderr).MustParse(os.Args[1:], func(err error) {
hlog.Verbosef("command returned %v", err) msg.Verbosef("command returned %v", err)
if errors.Is(err, errSuccess) { if errors.Is(err, errSuccess) {
hlog.BeforeExit() msg.BeforeExit()
os.Exit(0) os.Exit(0)
} }
// this catches faulty command handlers that fail to return before this point // this catches faulty command handlers that fail to return before this point

View File

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

View File

@@ -14,7 +14,7 @@ import (
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/app" "hakurei.app/internal/app"
"hakurei.app/internal/app/state" "hakurei.app/internal/app/state"
"hakurei.app/system/dbus" "hakurei.app/message"
) )
func printShowSystem(output io.Writer, short, flagJSON bool) { func printShowSystem(output io.Writer, short, flagJSON bool) {
@@ -22,7 +22,7 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
defer t.MustFlush() defer t.MustFlush()
info := &hst.Info{User: new(app.Hsu).MustID()} info := &hst.Info{User: new(app.Hsu).MustID()}
app.CopyPaths(&info.Paths, info.User) app.CopyPaths().Copy(&info.Paths, info.User)
if flagJSON { if flagJSON {
printJSON(output, short, info) printJSON(output, short, info)
@@ -39,7 +39,9 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
func printShowInstance( func printShowInstance(
output io.Writer, now time.Time, output io.Writer, now time.Time,
instance *state.State, config *hst.Config, instance *state.State, config *hst.Config,
short, flagJSON bool) { short, flagJSON bool) (valid bool) {
valid = true
if flagJSON { if flagJSON {
if instance != nil { if instance != nil {
printJSON(output, short, instance) printJSON(output, short, instance)
@@ -52,8 +54,11 @@ func printShowInstance(
t := newPrinter(output) t := newPrinter(output)
defer t.MustFlush() defer t.MustFlush()
if config.Container == nil { if err := config.Validate(); err != nil {
mustPrint(output, "Warning: this configuration uses permissive defaults!\n\n") valid = false
if m, ok := message.GetMessage(err); ok {
mustPrint(output, "Error: "+m+"!\n\n")
}
} }
if instance != nil { if instance != nil {
@@ -73,11 +78,11 @@ func printShowInstance(
if len(config.Groups) > 0 { if len(config.Groups) > 0 {
t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", ")) t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", "))
} }
if config.Home != nil {
t.Printf(" Home:\t%s\n", config.Home)
}
if config.Container != nil { if config.Container != nil {
params := config.Container params := config.Container
if params.Home != nil {
t.Printf(" Home:\t%s\n", params.Home)
}
if params.Hostname != "" { if params.Hostname != "" {
t.Printf(" Hostname:\t%s\n", params.Hostname) t.Printf(" Hostname:\t%s\n", params.Hostname)
} }
@@ -100,12 +105,12 @@ func printShowInstance(
} }
t.Printf(" Flags:\t%s\n", strings.Join(flags, " ")) t.Printf(" Flags:\t%s\n", strings.Join(flags, " "))
if config.Path != nil { if params.Path != nil {
t.Printf(" Path:\t%s\n", config.Path) 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") t.Printf("\n")
@@ -114,6 +119,7 @@ func printShowInstance(
t.Printf("Filesystem\n") t.Printf("Filesystem\n")
for _, f := range config.Container.Filesystem { for _, f := range config.Container.Filesystem {
if !f.Valid() { if !f.Valid() {
valid = false
t.Println(" <invalid>") t.Println(" <invalid>")
continue continue
} }
@@ -133,7 +139,7 @@ func printShowInstance(
} }
} }
printDBus := func(c *dbus.Config) { printDBus := func(c *hst.BusConfig) {
t.Printf(" Filter:\t%v\n", c.Filter) t.Printf(" Filter:\t%v\n", c.Filter)
if len(c.See) > 0 { if len(c.See) > 0 {
t.Printf(" See:\t%q\n", c.See) t.Printf(" See:\t%q\n", c.See)
@@ -161,6 +167,8 @@ func printShowInstance(
printDBus(config.SystemBus) printDBus(config.SystemBus)
t.Printf("\n") t.Printf("\n")
} }
return
} }
func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON bool) { 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/hst"
"hakurei.app/internal/app/state" "hakurei.app/internal/app/state"
"hakurei.app/system/dbus"
) )
var ( var (
@@ -27,13 +26,16 @@ var (
testAppTime = time.Unix(0, 9).UTC() testAppTime = time.Unix(0, 9).UTC()
) )
func Test_printShowInstance(t *testing.T) { func TestPrintShowInstance(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
instance *state.State instance *state.State
config *hst.Config config *hst.Config
short, json bool short, json bool
want string want string
valid bool
}{ }{
{"config", nil, hst.Template(), false, false, `App {"config", nil, hst.Template(), false, false, `App
Identity: 9 (org.chromium.Chromium) Identity: 9 (org.chromium.Chromium)
@@ -49,8 +51,7 @@ Filesystem
autoroot:w:/var/lib/hakurei/base/org.debian autoroot:w:/var/lib/hakurei/base/org.debian
autoetc:/etc/ autoetc:/etc/
w+ephemeral(-rwxr-xr-x):/tmp/ 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 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
*/nix/store
/run/current-system@ /run/current-system@
/run/opengl-driver@ /run/opengl-driver@
w-/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium w-/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
@@ -71,21 +72,25 @@ System bus
Filter: true Filter: true
Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"] Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"]
`}, `, true},
{"config pd", nil, new(hst.Config), false, false, `Warning: this configuration uses permissive defaults! {"config pd", nil, new(hst.Config), false, false, `Error: configuration missing container state!
App App
Identity: 0 Identity: 0
Enablements: (no enablements) Enablements: (no enablements)
`}, `, false},
{"config flag none", nil, &hst.Config{Container: new(hst.ContainerConfig)}, false, false, `App {"config flag none", nil, &hst.Config{Container: new(hst.ContainerConfig)}, false, false, `Error: container configuration missing path to home directory!
App
Identity: 0 Identity: 0
Enablements: (no enablements) Enablements: (no enablements)
Flags: none Flags: none
`}, `, false},
{"config nil entries", nil, &hst.Config{Container: &hst.ContainerConfig{Filesystem: make([]hst.FilesystemConfigJSON, 1)}, ExtraPerms: make([]*hst.ExtraPermConfig, 1)}, false, false, `App {"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 Identity: 0
Enablements: (no enablements) Enablements: (no enablements)
Flags: none Flags: none
@@ -95,8 +100,8 @@ Filesystem
Extra ACL Extra ACL
`}, `, false},
{"config pd dbus see", nil, &hst.Config{SessionBus: &dbus.Config{See: []string{"org.example.test"}}}, false, false, `Warning: this configuration uses permissive defaults! {"config pd dbus see", nil, &hst.Config{SessionBus: &hst.BusConfig{See: []string{"org.example.test"}}}, false, false, `Error: configuration missing container state!
App App
Identity: 0 Identity: 0
@@ -106,7 +111,7 @@ Session bus
Filter: false Filter: false
See: ["org.example.test"] See: ["org.example.test"]
`}, `, false},
{"instance", testState, hst.Template(), false, false, `State {"instance", testState, hst.Template(), false, false, `State
Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (3735928559) Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (3735928559)
@@ -126,8 +131,7 @@ Filesystem
autoroot:w:/var/lib/hakurei/base/org.debian autoroot:w:/var/lib/hakurei/base/org.debian
autoetc:/etc/ autoetc:/etc/
w+ephemeral(-rwxr-xr-x):/tmp/ 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 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
*/nix/store
/run/current-system@ /run/current-system@
/run/opengl-driver@ /run/opengl-driver@
w-/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium w-/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
@@ -148,8 +152,8 @@ System bus
Filter: true Filter: true
Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"] Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"]
`}, `, true},
{"instance pd", testState, new(hst.Config), false, false, `Warning: this configuration uses permissive defaults! {"instance pd", testState, new(hst.Config), false, false, `Error: configuration missing container state!
State State
Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (3735928559) Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (3735928559)
@@ -159,10 +163,10 @@ App
Identity: 0 Identity: 0
Enablements: (no enablements) Enablements: (no enablements)
`}, `, false},
{"json nil", nil, nil, false, true, `null {"json nil", nil, nil, false, true, `null
`}, `, true},
{"json instance", testState, nil, false, true, `{ {"json instance", testState, nil, false, true, `{
"instance": [ "instance": [
142, 142,
@@ -185,14 +189,6 @@ App
"pid": 3735928559, "pid": 3735928559,
"config": { "config": {
"id": "org.chromium.Chromium", "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": { "enablements": {
"wayland": true, "wayland": true,
"dbus": true, "dbus": true,
@@ -234,9 +230,6 @@ App
"broadcast": null, "broadcast": null,
"filter": true "filter": true
}, },
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"extra_perms": [ "extra_perms": [
{ {
"ensure": true, "ensure": true,
@@ -259,8 +252,6 @@ App
"container": { "container": {
"hostname": "localhost", "hostname": "localhost",
"wait_delay": -1, "wait_delay": -1,
"seccomp_flags": 1,
"seccomp_presets": 1,
"seccomp_compat": true, "seccomp_compat": true,
"devel": true, "devel": true,
"userns": true, "userns": true,
@@ -299,14 +290,10 @@ App
"type": "overlay", "type": "overlay",
"dst": "/nix/store", "dst": "/nix/store",
"lower": [ "lower": [
"/mnt-root/nix/.ro-store" "/var/lib/hakurei/base/org.nixos/ro-store"
], ],
"upper": "/mnt-root/nix/.rw-store/upper", "upper": "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper",
"work": "/mnt-root/nix/.rw-store/work" "work": "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work"
},
{
"type": "bind",
"src": "/nix/store"
}, },
{ {
"type": "link", "type": "link",
@@ -333,22 +320,25 @@ App
"dev": true, "dev": true,
"optional": 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" "time": "1970-01-01T00:00:00.000000009Z"
} }
`}, `, true},
{"json config", nil, hst.Template(), false, true, `{ {"json config", nil, hst.Template(), false, true, `{
"id": "org.chromium.Chromium", "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": { "enablements": {
"wayland": true, "wayland": true,
"dbus": true, "dbus": true,
@@ -390,9 +380,6 @@ App
"broadcast": null, "broadcast": null,
"filter": true "filter": true
}, },
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"extra_perms": [ "extra_perms": [
{ {
"ensure": true, "ensure": true,
@@ -415,8 +402,6 @@ App
"container": { "container": {
"hostname": "localhost", "hostname": "localhost",
"wait_delay": -1, "wait_delay": -1,
"seccomp_flags": 1,
"seccomp_presets": 1,
"seccomp_compat": true, "seccomp_compat": true,
"devel": true, "devel": true,
"userns": true, "userns": true,
@@ -455,14 +440,10 @@ App
"type": "overlay", "type": "overlay",
"dst": "/nix/store", "dst": "/nix/store",
"lower": [ "lower": [
"/mnt-root/nix/.ro-store" "/var/lib/hakurei/base/org.nixos/ro-store"
], ],
"upper": "/mnt-root/nix/.rw-store/upper", "upper": "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper",
"work": "/mnt-root/nix/.rw-store/work" "work": "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work"
},
{
"type": "bind",
"src": "/nix/store"
}, },
{ {
"type": "link", "type": "link",
@@ -489,26 +470,43 @@ App
"dev": true, "dev": true,
"optional": 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 { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
output := new(strings.Builder) 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 { if got := output.String(); got != tc.want {
t.Errorf("printShowInstance: got\n%s\nwant\n%s", t.Errorf("printShowInstance: \n%s\nwant\n%s", got, tc.want)
got, tc.want)
return 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 { testCases := []struct {
name string name string
entries state.Entries entries state.Entries
@@ -551,14 +549,6 @@ func Test_printPs(t *testing.T) {
"pid": 3735928559, "pid": 3735928559,
"config": { "config": {
"id": "org.chromium.Chromium", "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": { "enablements": {
"wayland": true, "wayland": true,
"dbus": true, "dbus": true,
@@ -600,9 +590,6 @@ func Test_printPs(t *testing.T) {
"broadcast": null, "broadcast": null,
"filter": true "filter": true
}, },
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"extra_perms": [ "extra_perms": [
{ {
"ensure": true, "ensure": true,
@@ -625,8 +612,6 @@ func Test_printPs(t *testing.T) {
"container": { "container": {
"hostname": "localhost", "hostname": "localhost",
"wait_delay": -1, "wait_delay": -1,
"seccomp_flags": 1,
"seccomp_presets": 1,
"seccomp_compat": true, "seccomp_compat": true,
"devel": true, "devel": true,
"userns": true, "userns": true,
@@ -665,14 +650,10 @@ func Test_printPs(t *testing.T) {
"type": "overlay", "type": "overlay",
"dst": "/nix/store", "dst": "/nix/store",
"lower": [ "lower": [
"/mnt-root/nix/.ro-store" "/var/lib/hakurei/base/org.nixos/ro-store"
], ],
"upper": "/mnt-root/nix/.rw-store/upper", "upper": "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper",
"work": "/mnt-root/nix/.rw-store/work" "work": "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work"
},
{
"type": "bind",
"src": "/nix/store"
}, },
{ {
"type": "link", "type": "link",
@@ -699,6 +680,17 @@ func Test_printPs(t *testing.T) {
"dev": true, "dev": true,
"optional": 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 { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
output := new(strings.Builder) output := new(strings.Builder)
printPs(output, testTime, stubStore(tc.entries), tc.short, tc.json) printPs(output, testTime, stubStore(tc.entries), tc.short, tc.json)
if got := output.String(); got != tc.want { if got := output.String(); got != tc.want {

View File

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

View File

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

View File

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

View File

@@ -62,11 +62,11 @@ def check_state(name, enablements):
config = instance['config'] 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]): 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 {instance['config']['args']}") raise Exception(f"unexpected args {config['container']['args']}")
if config['enablements'] != enablements: if config['enablements'] != enablements:
raise Exception(f"unexpected enablements {instance['config']['enablements']}") raise Exception(f"unexpected enablements {config['enablements']}")
start_all() start_all()

View File

@@ -2,38 +2,24 @@ package main
import ( import (
"context" "context"
"os"
"strings" "strings"
"hakurei.app/container" "hakurei.app/container/check"
"hakurei.app/container/seccomp" "hakurei.app/container/fhs"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal" "hakurei.app/message"
) )
func withNixDaemon( func withNixDaemon(
ctx context.Context, ctx context.Context,
msg message.Msg,
action string, command []string, net bool, updateConfig func(config *hst.Config) *hst.Config, action string, command []string, net bool, updateConfig func(config *hst.Config) *hst.Config,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(), app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
) { ) {
mustRunAppDropShell(ctx, updateConfig(&hst.Config{ mustRunAppDropShell(ctx, msg, updateConfig(&hst.Config{
ID: app.ID, 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{ ExtraPerms: []*hst.ExtraPermConfig{
{Path: dataHome, Execute: true}, {Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true}, {Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
@@ -42,36 +28,48 @@ func withNixDaemon(
Identity: app.Identity, Identity: app.Identity,
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action, Hostname: formatHostname(app.Name) + "-" + action,
Userns: true, // nix sandbox requires userns Userns: true, // nix sandbox requires userns
HostNet: net, HostNet: net,
SeccompFlags: seccomp.AllowMultiarch, Multiarch: true,
Tty: dropShell, Tty: dropShell,
Filesystem: []hst.FilesystemConfigJSON{ 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.FSBind{Source: pathSet.nixPath, Target: pathNix, Write: true}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}}, {FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}}, {FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: 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}}, {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) }), dropShell, beforeFail)
} }
func withCacheDir( func withCacheDir(
ctx context.Context, 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()) { app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
mustRunAppDropShell(ctx, &hst.Config{ mustRunAppDropShell(ctx, msg, &hst.Config{
ID: app.ID, 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{ ExtraPerms: []*hst.ExtraPermConfig{
{Path: dataHome, Execute: true}, {Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true}, {Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
@@ -81,28 +79,38 @@ func withCacheDir(
Identity: app.Identity, Identity: app.Identity,
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action, Hostname: formatHostname(app.Name) + "-" + action,
SeccompFlags: seccomp.AllowMultiarch, Multiarch: true,
Tty: dropShell, Tty: dropShell,
Filesystem: []hst.FilesystemConfigJSON{ 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.FSBind{Source: workDir.Append("nix"), Target: pathNix}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}}, {FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}}, {FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}}, {FilesystemConfig: &hst.FSLink{Target: fhs.AbsUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Source: workDir, Target: hst.AbsTmp.Append("bundle")}}, {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}}, {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) }, 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 { if dropShell {
config.Args = []string{bash, "-l"} if config.Container != nil {
mustRunApp(ctx, config, beforeFail) config.Container.Args = []string{bash, "-l"}
}
mustRunApp(ctx, msg, config, beforeFail)
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 package main
// minimise imports to avoid inadvertently calling init or global variable functions
import ( import (
"bytes" "bytes"
"fmt" "fmt"
@@ -19,6 +21,9 @@ const (
envGroups = "HAKUREI_GROUPS" envGroups = "HAKUREI_GROUPS"
PR_SET_NO_NEW_PRIVS = 0x26 PR_SET_NO_NEW_PRIVS = 0x26
identityMin = 0
identityMax = 9999
) )
func main() { func main() {
@@ -29,6 +34,9 @@ func main() {
if os.Geteuid() != 0 { if os.Geteuid() != 0 {
log.Fatal("this program must be owned by uid 0 and have the setuid bit set") log.Fatal("this program must be owned by uid 0 and have the setuid bit set")
} }
if os.Getegid() != os.Getgid() {
log.Fatal("this program must not have the setgid bit set")
}
puid := os.Getuid() puid := os.Getuid()
if puid == 0 { if puid == 0 {
@@ -91,7 +99,7 @@ func main() {
// allowed identity range 0 to 9999 // allowed identity range 0 to 9999
if as, ok := os.LookupEnv(envIdentity); !ok { if as, ok := os.LookupEnv(envIdentity); !ok {
log.Fatal("HAKUREI_IDENTITY not set") log.Fatal("HAKUREI_IDENTITY not set")
} else if identity, err := parseUint32Fast(as); err != nil || identity < 0 || identity > 9999 { } else if identity, err := parseUint32Fast(as); err != nil || identity < identityMin || identity > identityMax {
log.Fatal("invalid identity") log.Fatal("invalid identity")
} else { } else {
uid += identity uid += identity

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,19 +3,23 @@ package container
import ( import (
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/message"
) )
func init() { gob.Register(new(AutoRootOp)) } func init() { gob.Register(new(AutoRootOp)) }
// Root appends an [Op] that expands a directory into a toplevel bind mount mirror on container root. // Root appends an [Op] that expands a directory into a toplevel bind mount mirror on container root.
// This is not a generic setup op. It is implemented here to reduce ipc overhead. // This is not a generic setup op. It is implemented here to reduce ipc overhead.
func (f *Ops) Root(host *Absolute, flags int) *Ops { func (f *Ops) Root(host *check.Absolute, flags int) *Ops {
*f = append(*f, &AutoRootOp{host, flags, nil}) *f = append(*f, &AutoRootOp{host, flags, nil})
return f return f
} }
type AutoRootOp struct { type AutoRootOp struct {
Host *Absolute Host *check.Absolute
// passed through to bindMount // passed through to bindMount
Flags int Flags int
@@ -34,11 +38,11 @@ func (r *AutoRootOp) early(state *setupState, k syscallDispatcher) error {
r.resolved = make([]*BindMountOp, 0, len(d)) r.resolved = make([]*BindMountOp, 0, len(d))
for _, ent := range d { for _, ent := range d {
name := ent.Name() name := ent.Name()
if IsAutoRootBindable(name) { if IsAutoRootBindable(state, name) {
// careful: the Valid method is skipped, make sure this is always valid // careful: the Valid method is skipped, make sure this is always valid
op := &BindMountOp{ op := &BindMountOp{
Source: r.Host.Append(name), Source: r.Host.Append(name),
Target: AbsFHSRoot.Append(name), Target: fhs.AbsRoot.Append(name),
Flags: r.Flags, Flags: r.Flags,
} }
if err = op.early(state, k); err != nil { if err = op.early(state, k); err != nil {
@@ -78,7 +82,7 @@ func (r *AutoRootOp) String() string {
} }
// IsAutoRootBindable returns whether a dir entry name is selected for AutoRoot. // 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 { switch name {
case "proc", "dev", "tmp", "mnt", "etc": case "proc", "dev", "tmp", "mnt", "etc":

View File

@@ -5,11 +5,15 @@ import (
"os" "os"
"testing" "testing"
"hakurei.app/container/bits"
"hakurei.app/container/check"
"hakurei.app/container/stub" "hakurei.app/container/stub"
"hakurei.app/message"
) )
func TestAutoRootOp(t *testing.T) { func TestAutoRootOp(t *testing.T) {
t.Run("nonrepeatable", func(t *testing.T) { t.Run("nonrepeatable", func(t *testing.T) {
t.Parallel()
wantErr := OpRepeatError("autoroot") wantErr := OpRepeatError("autoroot")
if err := new(AutoRootOp).apply(&setupState{nonrepeatable: nrAutoRoot}, nil); !errors.Is(err, wantErr) { if err := new(AutoRootOp).apply(&setupState{nonrepeatable: nrAutoRoot}, nil); !errors.Is(err, wantErr) {
t.Errorf("apply: error = %v, want %v", err, wantErr) t.Errorf("apply: error = %v, want %v", err, wantErr)
@@ -18,15 +22,15 @@ func TestAutoRootOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{ checkOpBehaviour(t, []opBehaviourTestCase{
{"readdir", &Params{ParentPerm: 0750}, &AutoRootOp{ {"readdir", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: MustAbs("/"), Host: check.MustAbs("/"),
Flags: BindWritable, Flags: bits.BindWritable,
}, []stub.Call{ }, []stub.Call{
call("readdir", stub.ExpectArgs{"/"}, stubDir(), stub.UniqueError(2)), call("readdir", stub.ExpectArgs{"/"}, stubDir(), stub.UniqueError(2)),
}, stub.UniqueError(2), nil, nil}, }, stub.UniqueError(2), nil, nil},
{"early", &Params{ParentPerm: 0750}, &AutoRootOp{ {"early", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: MustAbs("/"), Host: check.MustAbs("/"),
Flags: BindWritable, Flags: bits.BindWritable,
}, []stub.Call{ }, []stub.Call{
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64", call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil), "lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
@@ -34,8 +38,8 @@ func TestAutoRootOp(t *testing.T) {
}, stub.UniqueError(1), nil, nil}, }, stub.UniqueError(1), nil, nil},
{"apply", &Params{ParentPerm: 0750}, &AutoRootOp{ {"apply", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: MustAbs("/"), Host: check.MustAbs("/"),
Flags: BindWritable, Flags: bits.BindWritable,
}, []stub.Call{ }, []stub.Call{
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64", call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil), "lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
@@ -55,8 +59,8 @@ func TestAutoRootOp(t *testing.T) {
}, stub.UniqueError(0)}, }, stub.UniqueError(0)},
{"success pd", &Params{ParentPerm: 0750}, &AutoRootOp{ {"success pd", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: MustAbs("/"), Host: check.MustAbs("/"),
Flags: BindWritable, Flags: bits.BindWritable,
}, []stub.Call{ }, []stub.Call{
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64", call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil), "lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
@@ -86,7 +90,7 @@ func TestAutoRootOp(t *testing.T) {
}, nil}, }, nil},
{"success", &Params{ParentPerm: 0750}, &AutoRootOp{ {"success", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: MustAbs("/var/lib/planterette/base/debian:f92c9052"), Host: check.MustAbs("/var/lib/planterette/base/debian:f92c9052"),
}, []stub.Call{ }, []stub.Call{
call("readdir", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052"}, stubDir("bin", "dev", "etc", "home", "lib64", call("readdir", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052"}, stubDir("bin", "dev", "etc", "home", "lib64",
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil), "lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
@@ -119,14 +123,14 @@ func TestAutoRootOp(t *testing.T) {
checkOpsValid(t, []opValidTestCase{ checkOpsValid(t, []opValidTestCase{
{"nil", (*AutoRootOp)(nil), false}, {"nil", (*AutoRootOp)(nil), false},
{"zero", new(AutoRootOp), false}, {"zero", new(AutoRootOp), false},
{"valid", &AutoRootOp{Host: MustAbs("/")}, true}, {"valid", &AutoRootOp{Host: check.MustAbs("/")}, true},
}) })
checkOpsBuilder(t, []opsBuilderTestCase{ checkOpsBuilder(t, []opsBuilderTestCase{
{"pd", new(Ops).Root(MustAbs("/"), BindWritable), Ops{ {"pd", new(Ops).Root(check.MustAbs("/"), bits.BindWritable), Ops{
&AutoRootOp{ &AutoRootOp{
Host: MustAbs("/"), Host: check.MustAbs("/"),
Flags: BindWritable, Flags: bits.BindWritable,
}, },
}}, }},
}) })
@@ -135,64 +139,74 @@ func TestAutoRootOp(t *testing.T) {
{"zero", new(AutoRootOp), new(AutoRootOp), false}, {"zero", new(AutoRootOp), new(AutoRootOp), false},
{"internal ne", &AutoRootOp{ {"internal ne", &AutoRootOp{
Host: MustAbs("/"), Host: check.MustAbs("/"),
Flags: BindWritable, Flags: bits.BindWritable,
}, &AutoRootOp{ }, &AutoRootOp{
Host: MustAbs("/"), Host: check.MustAbs("/"),
Flags: BindWritable, Flags: bits.BindWritable,
resolved: []*BindMountOp{new(BindMountOp)}, resolved: []*BindMountOp{new(BindMountOp)},
}, true}, }, true},
{"flags differs", &AutoRootOp{ {"flags differs", &AutoRootOp{
Host: MustAbs("/"), Host: check.MustAbs("/"),
Flags: BindWritable | BindDevice, Flags: bits.BindWritable | bits.BindDevice,
}, &AutoRootOp{ }, &AutoRootOp{
Host: MustAbs("/"), Host: check.MustAbs("/"),
Flags: BindWritable, Flags: bits.BindWritable,
}, false}, }, false},
{"host differs", &AutoRootOp{ {"host differs", &AutoRootOp{
Host: MustAbs("/tmp/"), Host: check.MustAbs("/tmp/"),
Flags: BindWritable, Flags: bits.BindWritable,
}, &AutoRootOp{ }, &AutoRootOp{
Host: MustAbs("/"), Host: check.MustAbs("/"),
Flags: BindWritable, Flags: bits.BindWritable,
}, false}, }, false},
{"equals", &AutoRootOp{ {"equals", &AutoRootOp{
Host: MustAbs("/"), Host: check.MustAbs("/"),
Flags: BindWritable, Flags: bits.BindWritable,
}, &AutoRootOp{ }, &AutoRootOp{
Host: MustAbs("/"), Host: check.MustAbs("/"),
Flags: BindWritable, Flags: bits.BindWritable,
}, true}, }, true},
}) })
checkOpMeta(t, []opMetaTestCase{ checkOpMeta(t, []opMetaTestCase{
{"root", &AutoRootOp{ {"root", &AutoRootOp{
Host: MustAbs("/"), Host: check.MustAbs("/"),
Flags: BindWritable, Flags: bits.BindWritable,
}, "setting up", `auto root "/" flags 0x2`}, }, "setting up", `auto root "/" flags 0x2`},
}) })
} }
func TestIsAutoRootBindable(t *testing.T) { func TestIsAutoRootBindable(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
want bool want bool
log bool
}{ }{
{"proc", false}, {"proc", false, false},
{"dev", false}, {"dev", false, false},
{"tmp", false}, {"tmp", false, false},
{"mnt", false}, {"mnt", false, false},
{"etc", false}, {"etc", false, false},
{"", false}, {"", false, true},
{"var", true}, {"var", true, false},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { 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) 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" import "testing"
func TestCapToIndex(t *testing.T) { func TestCapToIndex(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
cap uintptr cap uintptr
@@ -14,6 +16,7 @@ func TestCapToIndex(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := capToIndex(tc.cap); got != tc.want { if got := capToIndex(tc.cap); got != tc.want {
t.Errorf("capToIndex: %#x, want %#x", got, tc.want) t.Errorf("capToIndex: %#x, want %#x", got, tc.want)
} }
@@ -22,6 +25,8 @@ func TestCapToIndex(t *testing.T) {
} }
func TestCapToMask(t *testing.T) { func TestCapToMask(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
cap uintptr cap uintptr
@@ -33,6 +38,7 @@ func TestCapToMask(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := capToMask(tc.cap); got != tc.want { if got := capToMask(tc.cap); got != tc.want {
t.Errorf("capToMask: %#x, want %#x", got, tc.want) t.Errorf("capToMask: %#x, want %#x", got, tc.want)
} }

View File

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

View File

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

View File

@@ -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" . "syscall"
"time" "time"
"hakurei.app/container/bits"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/message"
) )
const ( const (
@@ -49,17 +53,18 @@ type (
cmd *exec.Cmd cmd *exec.Cmd
ctx context.Context ctx context.Context
msg message.Msg
Params Params
} }
// Params holds container configuration and is safe to serialise. // Params holds container configuration and is safe to serialise.
Params struct { Params struct {
// Working directory in the container. // Working directory in the container.
Dir *Absolute Dir *check.Absolute
// Initial process environment. // Initial process environment.
Env []string Env []string
// Pathname of initial process in the container. // Pathname of initial process in the container.
Path *Absolute Path *check.Absolute
// Initial process argv. // Initial process argv.
Args []string Args []string
// Deliver SIGINT to the initial process on context cancellation. // Deliver SIGINT to the initial process on context cancellation.
@@ -81,7 +86,7 @@ type (
// Extra seccomp flags. // Extra seccomp flags.
SeccompFlags seccomp.ExportFlag SeccompFlags seccomp.ExportFlag
// Seccomp presets. Has no effect unless SeccompRules is zero-length. // Seccomp presets. Has no effect unless SeccompRules is zero-length.
SeccompPresets seccomp.FilterPreset SeccompPresets bits.FilterPreset
// Do not load seccomp program. // Do not load seccomp program.
SeccompDisable bool SeccompDisable bool
@@ -162,14 +167,14 @@ func (p *Container) Start() error {
// map to overflow id to work around ownership checks // map to overflow id to work around ownership checks
if p.Uid < 1 { if p.Uid < 1 {
p.Uid = OverflowUid() p.Uid = OverflowUid(p.msg)
} }
if p.Gid < 1 { if p.Gid < 1 {
p.Gid = OverflowGid() p.Gid = OverflowGid(p.msg)
} }
if !p.RetainSession { if !p.RetainSession {
p.SeccompPresets |= seccomp.PresetDenyTTY p.SeccompPresets |= bits.PresetDenyTTY
} }
if p.AdoptWaitDelay == 0 { if p.AdoptWaitDelay == 0 {
@@ -197,7 +202,7 @@ func (p *Container) Start() error {
} else { } else {
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(CancelSignal) } p.cmd.Cancel = func() error { return p.cmd.Process.Signal(CancelSignal) }
} }
p.cmd.Dir = FHSRoot p.cmd.Dir = fhs.Root
p.cmd.SysProcAttr = &SysProcAttr{ p.cmd.SysProcAttr = &SysProcAttr{
Setsid: !p.RetainSession, Setsid: !p.RetainSession,
Pdeathsig: SIGKILL, Pdeathsig: SIGKILL,
@@ -263,19 +268,19 @@ func (p *Container) Start() error {
} }
return &StartError{false, "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", ENOSYS, true, false} return &StartError{false, "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", ENOSYS, true, false}
} else { } else {
msg.Verbosef("landlock abi version %d", abi) p.msg.Verbosef("landlock abi version %d", abi)
} }
if rulesetFd, err := rulesetAttr.Create(0); err != nil { if rulesetFd, err := rulesetAttr.Create(0); err != nil {
return &StartError{true, "create landlock ruleset", err, false, false} return &StartError{true, "create landlock ruleset", err, false, false}
} else { } else {
msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr) p.msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
if err = LandlockRestrictSelf(rulesetFd, 0); err != nil { if err = LandlockRestrictSelf(rulesetFd, 0); err != nil {
_ = Close(rulesetFd) _ = Close(rulesetFd)
return &StartError{true, "enforce landlock ruleset", err, false, false} return &StartError{true, "enforce landlock ruleset", err, false, false}
} }
if err = Close(rulesetFd); err != nil { if err = Close(rulesetFd); err != nil {
msg.Verbosef("cannot close landlock ruleset: %v", err) p.msg.Verbosef("cannot close landlock ruleset: %v", err)
// not fatal // not fatal
} }
} }
@@ -283,7 +288,7 @@ func (p *Container) Start() error {
landlockOut: landlockOut:
} }
msg.Verbose("starting container init") p.msg.Verbose("starting container init")
if err := p.cmd.Start(); err != nil { if err := p.cmd.Start(); err != nil {
return &StartError{false, "start container init", err, false, true} return &StartError{false, "start container init", err, false, true}
} }
@@ -313,7 +318,7 @@ func (p *Container) Serve() error {
// do not transmit nil // do not transmit nil
if p.Dir == nil { if p.Dir == nil {
p.Dir = AbsFHSRoot p.Dir = fhs.AbsRoot
} }
if p.SeccompRules == nil { if p.SeccompRules == nil {
p.SeccompRules = make([]seccomp.NativeRule, 0) p.SeccompRules = make([]seccomp.NativeRule, 0)
@@ -325,7 +330,7 @@ func (p *Container) Serve() error {
Getuid(), Getuid(),
Getgid(), Getgid(),
len(p.ExtraFiles), len(p.ExtraFiles),
msg.IsVerbose(), p.msg.IsVerbose(),
}, },
) )
if err != nil { if err != nil {
@@ -392,17 +397,21 @@ func (p *Container) ProcessState() *os.ProcessState {
} }
// New returns the address to a new instance of [Container] that requires further initialisation before use. // New returns the address to a new instance of [Container] that requires further initialisation before use.
func New(ctx context.Context) *Container { func New(ctx context.Context, msg message.Msg) *Container {
p := &Container{ctx: ctx, Params: Params{Ops: new(Ops)}} if msg == nil {
msg = message.NewMsg(nil)
}
p := &Container{ctx: ctx, msg: msg, Params: Params{Ops: new(Ops)}}
c, cancel := context.WithCancel(ctx) c, cancel := context.WithCancel(ctx)
p.cancel = cancel p.cancel = cancel
p.cmd = exec.CommandContext(c, MustExecutable()) p.cmd = exec.CommandContext(c, MustExecutable(msg))
return p return p
} }
// NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields. // NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields.
func NewCommand(ctx context.Context, pathname *Absolute, name string, args ...string) *Container { func NewCommand(ctx context.Context, msg message.Msg, pathname *check.Absolute, name string, args ...string) *Container {
z := New(ctx) z := New(ctx, msg)
z.Path = pathname z.Path = pathname
z.Args = append([]string{name}, args...) z.Args = append([]string{name}, args...)
return z return z

View File

@@ -20,15 +20,18 @@ import (
"hakurei.app/command" "hakurei.app/command"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/bits"
"hakurei.app/container/check"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/container/vfs" "hakurei.app/container/vfs"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/ldd" "hakurei.app/ldd"
"hakurei.app/message"
) )
func TestStartError(t *testing.T) { func TestStartError(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
err error err error
@@ -136,6 +139,8 @@ func TestStartError(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("error", func(t *testing.T) { t.Run("error", func(t *testing.T) {
if got := tc.err.Error(); got != tc.s { if got := tc.err.Error(); got != tc.s {
t.Errorf("Error: %q, want %q", got, tc.s) t.Errorf("Error: %q, want %q", got, tc.s)
@@ -152,13 +157,13 @@ func TestStartError(t *testing.T) {
}) })
t.Run("msg", func(t *testing.T) { t.Run("msg", func(t *testing.T) {
if got, ok := container.GetErrorMessage(tc.err); !ok { if got, ok := message.GetMessage(tc.err); !ok {
if tc.msg != "" { if tc.msg != "" {
t.Errorf("GetErrorMessage: err does not implement MessageError") t.Errorf("GetMessage: err does not implement MessageError")
} }
return return
} else if got != tc.msg { } else if got != tc.msg {
t.Errorf("GetErrorMessage: %q, want %q", got, tc.msg) t.Errorf("GetMessage: %q, want %q", got, tc.msg)
} }
}) })
}) })
@@ -201,33 +206,33 @@ var containerTestCases = []struct {
rules []seccomp.NativeRule rules []seccomp.NativeRule
flags seccomp.ExportFlag flags seccomp.ExportFlag
presets seccomp.FilterPreset presets bits.FilterPreset
}{ }{
{"minimal", true, false, false, true, {"minimal", true, false, false, true,
emptyOps, emptyMnt, emptyOps, emptyMnt,
1000, 100, nil, 0, seccomp.PresetStrict}, 1000, 100, nil, 0, bits.PresetStrict},
{"allow", true, true, true, false, {"allow", true, true, true, false,
emptyOps, emptyMnt, emptyOps, emptyMnt,
1000, 100, nil, 0, seccomp.PresetExt | seccomp.PresetDenyDevel}, 1000, 100, nil, 0, bits.PresetExt | bits.PresetDenyDevel},
{"no filter", false, true, true, true, {"no filter", false, true, true, true,
emptyOps, emptyMnt, emptyOps, emptyMnt,
1000, 100, nil, 0, seccomp.PresetExt}, 1000, 100, nil, 0, bits.PresetExt},
{"custom rules", true, true, true, false, {"custom rules", true, true, true, false,
emptyOps, emptyMnt, emptyOps, emptyMnt,
1, 31, []seccomp.NativeRule{{Syscall: seccomp.ScmpSyscall(syscall.SYS_SETUID), Errno: seccomp.ScmpErrno(syscall.EPERM)}}, 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, {"tmpfs", true, false, false, true,
earlyOps(new(container.Ops). earlyOps(new(container.Ops).
Tmpfs(hst.AbsTmp, 0, 0755), Tmpfs(hst.AbsPrivateTmp, 0, 0755),
), ),
earlyMnt( 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, {"dev", true, true /* go test output is not a tty */, false, false,
earlyOps(new(container.Ops). earlyOps(new(container.Ops).
Dev(container.MustAbs("/dev"), true), Dev(check.MustAbs("/dev"), true),
), ),
earlyMnt( earlyMnt(
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore), ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
@@ -241,11 +246,11 @@ var containerTestCases = []struct {
ent("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"), ent("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore), ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
), ),
1971, 100, nil, 0, seccomp.PresetStrict}, 1971, 100, nil, 0, bits.PresetStrict},
{"dev no mqueue", true, true /* go test output is not a tty */, false, false, {"dev no mqueue", true, true /* go test output is not a tty */, false, false,
earlyOps(new(container.Ops). earlyOps(new(container.Ops).
Dev(container.MustAbs("/dev"), false), Dev(check.MustAbs("/dev"), false),
), ),
earlyMnt( earlyMnt(
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore), ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
@@ -258,24 +263,24 @@ var containerTestCases = []struct {
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"), ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore), ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
), ),
1971, 100, nil, 0, seccomp.PresetStrict}, 1971, 100, nil, 0, bits.PresetStrict},
{"overlay", true, false, false, true, {"overlay", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) { func(t *testing.T) (*container.Ops, context.Context) {
tempDir := container.MustAbs(t.TempDir()) tempDir := check.MustAbs(t.TempDir())
lower0, lower1, upper, work := lower0, lower1, upper, work :=
tempDir.Append("lower0"), tempDir.Append("lower0"),
tempDir.Append("lower1"), tempDir.Append("lower1"),
tempDir.Append("upper"), tempDir.Append("upper"),
tempDir.Append("work") tempDir.Append("work")
for _, a := range []*container.Absolute{lower0, lower1, upper, work} { for _, a := range []*check.Absolute{lower0, lower1, upper, work} {
if err := os.Mkdir(a.String(), 0755); err != nil { if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err) t.Fatalf("Mkdir: error = %v", err)
} }
} }
return new(container.Ops). return new(container.Ops).
Overlay(hst.AbsTmp, upper, work, lower0, lower1), Overlay(hst.AbsPrivateTmp, upper, work, lower0, lower1),
context.WithValue(context.WithValue(context.WithValue(context.WithValue(t.Context(), context.WithValue(context.WithValue(context.WithValue(context.WithValue(t.Context(),
testVal("lower1"), lower1), testVal("lower1"), lower1),
testVal("lower0"), lower0), testVal("lower0"), lower0),
@@ -284,74 +289,74 @@ var containerTestCases = []struct {
}, },
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry { func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{ return []*vfs.MountInfoEntry{
ent("/", hst.Tmp, "rw", "overlay", "overlay", ent("/", hst.PrivateTmp, "rw", "overlay", "overlay",
"rw,lowerdir="+ "rw,lowerdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+ container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*check.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+ container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
",upperdir="+ ",upperdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(*container.Absolute).String())+ container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(*check.Absolute).String())+
",workdir="+ ",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"), ",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, {"overlay ephemeral", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) { func(t *testing.T) (*container.Ops, context.Context) {
tempDir := container.MustAbs(t.TempDir()) tempDir := check.MustAbs(t.TempDir())
lower0, lower1 := lower0, lower1 :=
tempDir.Append("lower0"), tempDir.Append("lower0"),
tempDir.Append("lower1") 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 { if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err) t.Fatalf("Mkdir: error = %v", err)
} }
} }
return new(container.Ops). return new(container.Ops).
OverlayEphemeral(hst.AbsTmp, lower0, lower1), OverlayEphemeral(hst.AbsPrivateTmp, lower0, lower1),
t.Context() t.Context()
}, },
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry { func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{ return []*vfs.MountInfoEntry{
// contains random suffix // contains random suffix
ent("/", hst.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, {"overlay readonly", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) { func(t *testing.T) (*container.Ops, context.Context) {
tempDir := container.MustAbs(t.TempDir()) tempDir := check.MustAbs(t.TempDir())
lower0, lower1 := lower0, lower1 :=
tempDir.Append("lower0"), tempDir.Append("lower0"),
tempDir.Append("lower1") 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 { if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err) t.Fatalf("Mkdir: error = %v", err)
} }
} }
return new(container.Ops). return new(container.Ops).
OverlayReadonly(hst.AbsTmp, lower0, lower1), OverlayReadonly(hst.AbsPrivateTmp, lower0, lower1),
context.WithValue(context.WithValue(t.Context(), context.WithValue(context.WithValue(t.Context(),
testVal("lower1"), lower1), testVal("lower1"), lower1),
testVal("lower0"), lower0) testVal("lower0"), lower0)
}, },
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry { func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{ return []*vfs.MountInfoEntry{
ent("/", hst.Tmp, "rw", "overlay", "overlay", ent("/", hst.PrivateTmp, "rw", "overlay", "overlay",
"ro,lowerdir="+ "ro,lowerdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+ container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*check.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+ container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
",redirect_dir=nofollow,userxattr"), ",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) { func TestContainer(t *testing.T) {
replaceOutput(t) t.Parallel()
t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) { t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) {
wantErr := context.Canceled wantErr := context.Canceled
@@ -386,13 +391,15 @@ func TestContainer(t *testing.T) {
for i, tc := range containerTestCases { for i, tc := range containerTestCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
wantOps, wantOpsCtx := tc.ops(t) wantOps, wantOpsCtx := tc.ops(t)
wantMnt := tc.mnt(t, wantOpsCtx) wantMnt := tc.mnt(t, wantOpsCtx)
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout) ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
defer cancel() defer cancel()
var libPaths []*container.Absolute var libPaths []*check.Absolute
c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i)) c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i))
c.Uid = tc.uid c.Uid = tc.uid
c.Gid = tc.gid c.Gid = tc.gid
@@ -413,11 +420,11 @@ func TestContainer(t *testing.T) {
c.HostNet = tc.net c.HostNet = tc.net
c. c.
Readonly(container.MustAbs(pathReadonly), 0755). Readonly(check.MustAbs(pathReadonly), 0755).
Tmpfs(container.MustAbs("/tmp"), 0, 0755). Tmpfs(check.MustAbs("/tmp"), 0, 0755).
Place(container.MustAbs("/etc/hostname"), []byte(c.Hostname)) Place(check.MustAbs("/etc/hostname"), []byte(c.Hostname))
// needs /proc to check mountinfo // 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 // mountinfo cannot be resolved directly by helper due to libPaths nondeterminism
mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths)) mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
@@ -448,10 +455,10 @@ func TestContainer(t *testing.T) {
_, _ = output.WriteTo(os.Stdout) _, _ = output.WriteTo(os.Stdout)
t.Fatalf("cannot serialise expected mount points: %v", err) t.Fatalf("cannot serialise expected mount points: %v", err)
} }
c.Place(container.MustAbs(pathWantMnt), want.Bytes()) c.Place(check.MustAbs(pathWantMnt), want.Bytes())
if tc.ro { if tc.ro {
c.Remount(container.MustAbs("/"), syscall.MS_RDONLY) c.Remount(check.MustAbs("/"), syscall.MS_RDONLY)
} }
if err := c.Start(); err != nil { if err := c.Start(); err != nil {
@@ -505,6 +512,7 @@ func testContainerCancel(
waitCheck func(t *testing.T, c *container.Container), waitCheck func(t *testing.T, c *container.Container),
) func(t *testing.T) { ) func(t *testing.T) {
return func(t *testing.T) { return func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout) ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
c := helperNewContainer(ctx, "block") c := helperNewContainer(ctx, "block")
@@ -547,12 +555,14 @@ func testContainerCancel(
} }
func TestContainerString(t *testing.T) { 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.SeccompFlags |= seccomp.AllowMultiarch
c.SeccompRules = seccomp.Preset( c.SeccompRules = seccomp.Preset(
seccomp.PresetExt|seccomp.PresetDenyNS|seccomp.PresetDenyTTY, bits.PresetExt|bits.PresetDenyNS|bits.PresetDenyTTY,
c.SeccompFlags) c.SeccompFlags)
c.SeccompPresets = seccomp.PresetStrict c.SeccompPresets = bits.PresetStrict
want := `argv: ["ldd" "/usr/bin/env"], filter: true, rules: 65, flags: 0x1, presets: 0xf` want := `argv: ["ldd" "/usr/bin/env"], filter: true, rules: 65, flags: 0x1, presets: 0xf`
if got := c.String(); got != want { if got := c.String(); got != want {
t.Errorf("String: %s, want %s", got, want) t.Errorf("String: %s, want %s", got, want)
@@ -683,13 +693,13 @@ const (
) )
var ( var (
absHelperInnerPath = container.MustAbs(helperInnerPath) absHelperInnerPath = check.MustAbs(helperInnerPath)
) )
var helperCommands []func(c command.Command) var helperCommands []func(c command.Command)
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput) container.TryArgv0(nil)
if os.Getenv(envDoCheck) == "1" { if os.Getenv(envDoCheck) == "1" {
c := command.New(os.Stderr, log.Printf, "helper", func(args []string) error { c := command.New(os.Stderr, log.Printf, "helper", func(args []string) error {
@@ -711,13 +721,14 @@ func TestMain(m *testing.M) {
os.Exit(m.Run()) os.Exit(m.Run())
} }
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*container.Absolute, args ...string) (c *container.Container) { func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*check.Absolute, args ...string) (c *container.Container) {
c = container.NewCommand(ctx, absHelperInnerPath, "helper", args...) msg := message.NewMsg(nil)
c = container.NewCommand(ctx, msg, absHelperInnerPath, "helper", args...)
c.Env = append(c.Env, envDoCheck+"=1") 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 // 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) log.Fatalf("ldd: %v", err)
} else { } else {
*libPaths = ldd.Path(entries) *libPaths = ldd.Path(entries)
@@ -730,5 +741,5 @@ func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*container.Abso
} }
func helperNewContainer(ctx context.Context, args ...string) (c *container.Container) { 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 ( import (
"io" "io"
"io/fs" "io/fs"
"log"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
@@ -12,6 +11,7 @@ import (
"syscall" "syscall"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/message"
) )
type osFile interface { type osFile interface {
@@ -38,7 +38,7 @@ type syscallDispatcher interface {
setNoNewPrivs() error setNoNewPrivs() error
// lastcap provides [LastCap]. // lastcap provides [LastCap].
lastcap() uintptr lastcap(msg message.Msg) uintptr
// capset provides capset. // capset provides capset.
capset(hdrp *capHeader, datap *[2]capData) error capset(hdrp *capHeader, datap *[2]capData) error
// capBoundingSetDrop provides capBoundingSetDrop. // capBoundingSetDrop provides capBoundingSetDrop.
@@ -53,9 +53,9 @@ type syscallDispatcher interface {
receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
// bindMount provides procPaths.bindMount. // bindMount provides procPaths.bindMount.
bindMount(source, target string, flags uintptr) error bindMount(msg message.Msg, source, target string, flags uintptr) error
// remount provides procPaths.remount. // remount provides procPaths.remount.
remount(target string, flags uintptr) error remount(msg message.Msg, target string, flags uintptr) error
// mountTmpfs provides mountTmpfs. // mountTmpfs provides mountTmpfs.
mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error
// ensureFile provides ensureFile. // ensureFile provides ensureFile.
@@ -122,22 +122,12 @@ type syscallDispatcher interface {
// wait4 provides syscall.Wait4 // wait4 provides syscall.Wait4
wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error)
// printf provides [log.Printf]. // printf provides the Printf method of [log.Logger].
printf(format string, v ...any) printf(msg message.Msg, format string, v ...any)
// fatal provides [log.Fatal] // fatal provides the Fatal method of [log.Logger]
fatal(v ...any) fatal(msg message.Msg, v ...any)
// fatalf provides [log.Fatalf] // fatalf provides the Fatalf method of [log.Logger]
fatalf(format string, v ...any) fatalf(msg message.Msg, format string, v ...any)
// verbose provides [Msg.Verbose].
verbose(v ...any)
// verbosef provides [Msg.Verbosef].
verbosef(format string, v ...any)
// suspend provides [Msg.Suspend].
suspend()
// resume provides [Msg.Resume].
resume() bool
// beforeExit provides [Msg.BeforeExit].
beforeExit()
} }
// direct implements syscallDispatcher on the current kernel. // direct implements syscallDispatcher on the current kernel.
@@ -151,7 +141,7 @@ func (direct) setPtracer(pid uintptr) error { return SetPtracer(pid) }
func (direct) setDumpable(dumpable uintptr) error { return SetDumpable(dumpable) } func (direct) setDumpable(dumpable uintptr) error { return SetDumpable(dumpable) }
func (direct) setNoNewPrivs() error { return SetNoNewPrivs() } func (direct) setNoNewPrivs() error { return SetNoNewPrivs() }
func (direct) lastcap() 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) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) }
func (direct) capBoundingSetDrop(cap uintptr) error { return capBoundingSetDrop(cap) } func (direct) capBoundingSetDrop(cap uintptr) error { return capBoundingSetDrop(cap) }
func (direct) capAmbientClearAll() error { return capAmbientClearAll() } func (direct) capAmbientClearAll() error { return capAmbientClearAll() }
@@ -161,11 +151,11 @@ func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
return Receive(key, e, fdp) return Receive(key, e, fdp)
} }
func (direct) bindMount(source, target string, flags uintptr) error { func (direct) bindMount(msg message.Msg, source, target string, flags uintptr) error {
return hostProc.bindMount(source, target, flags) return hostProc.bindMount(msg, source, target, flags)
} }
func (direct) remount(target string, flags uintptr) error { func (direct) remount(msg message.Msg, target string, flags uintptr) error {
return hostProc.remount(target, flags) return hostProc.remount(msg, target, flags)
} }
func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error { func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
return mountTmpfs(k, fsname, target, flags, size, perm) return mountTmpfs(k, fsname, target, flags, size, perm)
@@ -232,11 +222,6 @@ func (direct) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *s
return syscall.Wait4(pid, wstatus, options, rusage) return syscall.Wait4(pid, wstatus, options, rusage)
} }
func (direct) printf(format string, v ...any) { log.Printf(format, v...) } func (direct) printf(msg message.Msg, format string, v ...any) { msg.GetLogger().Printf(format, v...) }
func (direct) fatal(v ...any) { log.Fatal(v...) } func (direct) fatal(msg message.Msg, v ...any) { msg.GetLogger().Fatal(v...) }
func (direct) fatalf(format string, v ...any) { log.Fatalf(format, v...) } func (direct) fatalf(msg message.Msg, format string, v ...any) { msg.GetLogger().Fatalf(format, v...) }
func (direct) verbose(v ...any) { msg.Verbose(v...) }
func (direct) verbosef(format string, v ...any) { msg.Verbosef(format, v...) }
func (direct) suspend() { msg.Suspend() }
func (direct) resume() bool { return msg.Resume() }
func (direct) beforeExit() { msg.BeforeExit() }

View File

@@ -2,8 +2,10 @@ package container
import ( import (
"bytes" "bytes"
"fmt"
"io" "io"
"io/fs" "io/fs"
"log"
"os" "os"
"os/exec" "os/exec"
"reflect" "reflect"
@@ -16,6 +18,7 @@ import (
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/container/stub" "hakurei.app/container/stub"
"hakurei.app/message"
) )
type opValidTestCase struct { type opValidTestCase struct {
@@ -29,10 +32,12 @@ func checkOpsValid(t *testing.T, testCases []opValidTestCase) {
t.Run("valid", func(t *testing.T) { t.Run("valid", func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
if got := tc.op.Valid(); got != tc.want { if got := tc.op.Valid(); got != tc.want {
t.Errorf("Valid: %v, want %v", got, tc.want) t.Errorf("Valid: %v, want %v", got, tc.want)
@@ -53,10 +58,12 @@ func checkOpsBuilder(t *testing.T, testCases []opsBuilderTestCase) {
t.Run("build", func(t *testing.T) { t.Run("build", func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
if !slices.EqualFunc(*tc.ops, tc.want, func(op Op, v Op) bool { return op.Is(v) }) { if !slices.EqualFunc(*tc.ops, tc.want, func(op Op, v Op) bool { return op.Is(v) }) {
t.Errorf("Ops: %#v, want %#v", tc.ops, tc.want) t.Errorf("Ops: %#v, want %#v", tc.ops, tc.want)
@@ -77,10 +84,12 @@ func checkOpIs(t *testing.T, testCases []opIsTestCase) {
t.Run("is", func(t *testing.T) { t.Run("is", func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
if got := tc.op.Is(tc.v); got != tc.want { if got := tc.op.Is(tc.v); got != tc.want {
t.Errorf("Is: %v, want %v", got, tc.want) t.Errorf("Is: %v, want %v", got, tc.want)
@@ -103,10 +112,12 @@ func checkOpMeta(t *testing.T, testCases []opMetaTestCase) {
t.Run("meta", func(t *testing.T) { t.Run("meta", func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
t.Run("prefix", func(t *testing.T) { t.Run("prefix", func(t *testing.T) {
t.Helper() t.Helper()
@@ -136,7 +147,7 @@ func call(name string, args stub.ExpectArgs, ret any, err error) stub.Call {
type simpleTestCase struct { type simpleTestCase struct {
name string name string
f func(k syscallDispatcher) error f func(k *kstub) error
want stub.Expect want stub.Expect
wantErr error wantErr error
} }
@@ -147,6 +158,7 @@ func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
wait4signal := make(chan struct{}) wait4signal := make(chan struct{})
k := &kstub{wait4signal, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, s} }, tc.want)} k := &kstub{wait4signal, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, s} }, tc.want)}
@@ -180,16 +192,18 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
t.Run("behaviour", func(t *testing.T) { t.Run("behaviour", func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
state := &setupState{Params: tc.params}
k := &kstub{nil, stub.New(t, k := &kstub{nil, stub.New(t,
func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, s} }, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, s} },
stub.Expect{Calls: slices.Concat(tc.early, []stub.Call{{Name: stub.CallSeparator}}, tc.apply)}, stub.Expect{Calls: slices.Concat(tc.early, []stub.Call{{Name: stub.CallSeparator}}, tc.apply)},
)} )}
state := &setupState{Params: tc.params, Msg: k}
defer stub.HandleExit(t) defer stub.HandleExit(t)
errEarly := tc.op.early(state, k) errEarly := tc.op.early(state, k)
k.Expects(stub.CallSeparator) k.Expects(stub.CallSeparator)
@@ -327,7 +341,11 @@ func (k *kstub) setDumpable(dumpable uintptr) error {
} }
func (k *kstub) setNoNewPrivs() error { k.Helper(); return k.Expects("setNoNewPrivs").Err } func (k *kstub) setNoNewPrivs() error { k.Helper(); return k.Expects("setNoNewPrivs").Err }
func (k *kstub) lastcap() uintptr { k.Helper(); return k.Expects("lastcap").Ret.(uintptr) } 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 { func (k *kstub) capset(hdrp *capHeader, datap *[2]capData) error {
k.Helper() k.Helper()
@@ -403,16 +421,18 @@ func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error
return return
} }
func (k *kstub) bindMount(source, target string, flags uintptr) error { func (k *kstub) bindMount(msg message.Msg, source, target string, flags uintptr) error {
k.Helper() k.Helper()
k.checkMsg(msg)
return k.Expects("bindMount").Error( return k.Expects("bindMount").Error(
stub.CheckArg(k.Stub, "source", source, 0), stub.CheckArg(k.Stub, "source", source, 0),
stub.CheckArg(k.Stub, "target", target, 1), stub.CheckArg(k.Stub, "target", target, 1),
stub.CheckArg(k.Stub, "flags", flags, 2)) stub.CheckArg(k.Stub, "flags", flags, 2))
} }
func (k *kstub) remount(target string, flags uintptr) error { func (k *kstub) remount(msg message.Msg, target string, flags uintptr) error {
k.Helper() k.Helper()
k.checkMsg(msg)
return k.Expects("remount").Error( return k.Expects("remount").Error(
stub.CheckArg(k.Stub, "target", target, 0), stub.CheckArg(k.Stub, "target", target, 0),
stub.CheckArg(k.Stub, "flags", flags, 1)) stub.CheckArg(k.Stub, "flags", flags, 1))
@@ -694,7 +714,7 @@ func (k *kstub) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage
return return
} }
func (k *kstub) printf(format string, v ...any) { func (k *kstub) printf(_ message.Msg, format string, v ...any) {
k.Helper() k.Helper()
if k.Expects("printf").Error( if k.Expects("printf").Error(
stub.CheckArg(k.Stub, "format", format, 0), stub.CheckArg(k.Stub, "format", format, 0),
@@ -703,7 +723,7 @@ func (k *kstub) printf(format string, v ...any) {
} }
} }
func (k *kstub) fatal(v ...any) { func (k *kstub) fatal(_ message.Msg, v ...any) {
k.Helper() k.Helper()
if k.Expects("fatal").Error( if k.Expects("fatal").Error(
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil { stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil {
@@ -712,7 +732,7 @@ func (k *kstub) fatal(v ...any) {
panic(stub.PanicExit) panic(stub.PanicExit)
} }
func (k *kstub) fatalf(format string, v ...any) { func (k *kstub) fatalf(_ message.Msg, format string, v ...any) {
k.Helper() k.Helper()
if k.Expects("fatalf").Error( if k.Expects("fatalf").Error(
stub.CheckArg(k.Stub, "format", format, 0), stub.CheckArg(k.Stub, "format", format, 0),
@@ -722,7 +742,35 @@ func (k *kstub) fatalf(format string, v ...any) {
panic(stub.PanicExit) panic(stub.PanicExit)
} }
func (k *kstub) verbose(v ...any) { func (k *kstub) checkMsg(msg message.Msg) {
k.Helper()
var target *kstub
if state, ok := msg.(*setupState); ok {
target = state.Msg.(*kstub)
} else {
target = msg.(*kstub)
}
if k != target {
panic(fmt.Sprintf("unexpected Msg: %#v", msg))
}
}
func (k *kstub) GetLogger() *log.Logger { panic("unreachable") }
func (k *kstub) IsVerbose() bool { 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() k.Helper()
if k.Expects("verbose").Error( if k.Expects("verbose").Error(
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil { stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil {
@@ -730,7 +778,7 @@ func (k *kstub) verbose(v ...any) {
} }
} }
func (k *kstub) verbosef(format string, v ...any) { func (k *kstub) Verbosef(format string, v ...any) {
k.Helper() k.Helper()
if k.Expects("verbosef").Error( if k.Expects("verbosef").Error(
stub.CheckArg(k.Stub, "format", format, 0), stub.CheckArg(k.Stub, "format", format, 0),
@@ -739,6 +787,6 @@ func (k *kstub) verbosef(format string, v ...any) {
} }
} }
func (k *kstub) suspend() { k.Helper(); k.Expects("suspend") } 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) Resume() bool { k.Helper(); return k.Expects("resume").Ret.(bool) }
func (k *kstub) beforeExit() { k.Helper(); k.Expects("beforeExit") } func (k *kstub) BeforeExit() { k.Helper(); k.Expects("beforeExit") }

View File

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

View File

@@ -8,11 +8,14 @@ import (
"syscall" "syscall"
"testing" "testing"
"hakurei.app/container/check"
"hakurei.app/container/stub" "hakurei.app/container/stub"
"hakurei.app/container/vfs" "hakurei.app/container/vfs"
) )
func TestMessageFromError(t *testing.T) { func TestMessageFromError(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
err error err error
@@ -34,7 +37,7 @@ func TestMessageFromError(t *testing.T) {
Err: stub.UniqueError(0xdeadbeef), Err: stub.UniqueError(0xdeadbeef),
}, "cannot mount /sysroot: unique error 3735928559 injected by the test suite", true}, }, "cannot mount /sysroot: unique error 3735928559 injected by the test suite", true},
{"absolute", &AbsoluteError{"etc/mtab"}, {"absolute", &check.AbsoluteError{Pathname: "etc/mtab"},
`path "etc/mtab" is not absolute`, true}, `path "etc/mtab" is not absolute`, true},
{"repeat", OpRepeatError("autoetc"), {"repeat", OpRepeatError("autoetc"),
@@ -53,6 +56,7 @@ func TestMessageFromError(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, ok := messageFromError(tc.err) got, ok := messageFromError(tc.err)
if got != tc.want { if got != tc.want {
t.Errorf("messageFromError: %q, want %q", got, tc.want) t.Errorf("messageFromError: %q, want %q", got, tc.want)
@@ -65,6 +69,8 @@ func TestMessageFromError(t *testing.T) {
} }
func TestMountError(t *testing.T) { func TestMountError(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
err error err error
@@ -110,6 +116,7 @@ func TestMountError(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("is", func(t *testing.T) { t.Run("is", func(t *testing.T) {
if !errors.Is(tc.err, tc.errno) { if !errors.Is(tc.err, tc.errno) {
t.Errorf("Is: %#v is not %v", tc.err, tc.errno) t.Errorf("Is: %#v is not %v", tc.err, tc.errno)
@@ -124,6 +131,7 @@ func TestMountError(t *testing.T) {
} }
t.Run("zero", func(t *testing.T) { t.Run("zero", func(t *testing.T) {
t.Parallel()
if errors.Is(new(MountError), syscall.Errno(0)) { if errors.Is(new(MountError), syscall.Errno(0)) {
t.Errorf("Is: zero MountError unexpected true") t.Errorf("Is: zero MountError unexpected true")
} }
@@ -131,6 +139,8 @@ func TestMountError(t *testing.T) {
} }
func TestErrnoFallback(t *testing.T) { func TestErrnoFallback(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
err error err error
@@ -153,6 +163,7 @@ func TestErrnoFallback(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
errno, err := errnoFallback(tc.name, Nonexistent, tc.err) errno, err := errnoFallback(tc.name, Nonexistent, tc.err)
if errno != tc.wantErrno { if errno != tc.wantErrno {
t.Errorf("errnoFallback: errno = %v, want %v", errno, tc.wantErrno) t.Errorf("errnoFallback: errno = %v, want %v", errno, tc.wantErrno)

View File

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

View File

@@ -5,13 +5,14 @@ import (
"testing" "testing"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/message"
) )
func TestExecutable(t *testing.T) { func TestExecutable(t *testing.T) {
t.Parallel()
for i := 0; i < 16; i++ { for i := 0; i < 16; i++ {
if got := container.MustExecutable(); got != os.Args[0] { if got := container.MustExecutable(message.NewMsg(nil)); got != os.Args[0] {
t.Errorf("MustExecutable: %q, want %q", t.Errorf("MustExecutable: %q, want %q", got, os.Args[0])
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 ( import (
"errors" "errors"
"fmt" "fmt"
"log"
"os" "os"
"os/exec" "os/exec"
"path" "path"
@@ -11,7 +12,9 @@ import (
. "syscall" . "syscall"
"time" "time"
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/message"
) )
const ( const (
@@ -29,7 +32,7 @@ const (
it should be noted that none of this should become relevant at any point since the resulting it should be noted that none of this should become relevant at any point since the resulting
intermediate root tmpfs should be effectively anonymous */ intermediate root tmpfs should be effectively anonymous */
intermediateHostPath = FHSProc + "self/fd" intermediateHostPath = fhs.Proc + "self/fd"
// setup params file descriptor // setup params file descriptor
setupEnv = "HAKUREI_SETUP" setupEnv = "HAKUREI_SETUP"
@@ -59,6 +62,7 @@ type (
setupState struct { setupState struct {
nonrepeatable uintptr nonrepeatable uintptr
*Params *Params
message.Msg
} }
) )
@@ -91,20 +95,23 @@ type initParams struct {
Verbose bool Verbose bool
} }
func Init(prepareLogger func(prefix string), setVerbose func(verbose bool)) { // Init is called by [TryArgv0] if the current process is the container init.
initEntrypoint(direct{}, prepareLogger, setVerbose) 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() k.lockOSThread()
prepareLogger("init")
if k.getpid() != 1 { 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 { 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 // not fatal: this program has no additional privileges at initial program start
} }
@@ -116,65 +123,65 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
) )
if f, err := k.receive(setupEnv, &params, &setupFd); err != nil { if f, err := k.receive(setupEnv, &params, &setupFd); err != nil {
if errors.Is(err, EBADF) { if errors.Is(err, EBADF) {
k.fatal("invalid setup descriptor") k.fatal(msg, "invalid setup descriptor")
} }
if errors.Is(err, ErrReceiveEnv) { if errors.Is(err, ErrReceiveEnv) {
k.fatal("HAKUREI_SETUP not set") 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 { } else {
if params.Ops == nil { if params.Ops == nil {
k.fatal("invalid setup parameters") k.fatal(msg, "invalid setup parameters")
} }
if params.ParentPerm == 0 { if params.ParentPerm == 0 {
params.ParentPerm = 0755 params.ParentPerm = 0755
} }
setVerbose(params.Verbose) msg.SwapVerbose(params.Verbose)
k.verbose("received setup parameters") msg.Verbose("received setup parameters")
closeSetup = f closeSetup = f
offsetSetup = int(setupFd + 1) offsetSetup = int(setupFd + 1)
} }
// write uid/gid map here so parent does not need to set dumpable // write uid/gid map here so parent does not need to set dumpable
if err := k.setDumpable(SUID_DUMP_USER); err != nil { if err := k.setDumpable(SUID_DUMP_USER); err != nil {
k.fatalf("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"...), append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...),
0); err != nil { 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"), []byte("deny\n"),
0); err != nil && !os.IsNotExist(err) { 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"...), append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...),
0); err != nil { 0); err != nil {
k.fatalf("%v", err) k.fatalf(msg, "%v", err)
} }
if err := k.setDumpable(SUID_DUMP_DISABLE); err != nil { 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) oldmask := k.umask(0)
if params.Hostname != "" { if params.Hostname != "" {
if err := k.sethostname([]byte(params.Hostname)); err != nil { if err := k.sethostname([]byte(params.Hostname)); err != nil {
k.fatalf("cannot set hostname: %v", err) k.fatalf(msg, "cannot set hostname: %v", err)
} }
} }
// cache sysctl before pivot_root // 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 { if err := k.mount(zeroString, fhs.Root, zeroString, MS_SILENT|MS_SLAVE|MS_REC, zeroString); err != nil {
k.fatalf("cannot make / rslave: %v", err) 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; /* early is called right before pivot_root into intermediate root;
this step is mostly for gathering information that would otherwise be difficult to obtain this step is mostly for gathering information that would otherwise be difficult to obtain
@@ -182,41 +189,41 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
the state of the mount namespace */ the state of the mount namespace */
for i, op := range *params.Ops { for i, op := range *params.Ops {
if op == nil || !op.Valid() { if op == nil || !op.Valid() {
k.fatalf("invalid op at index %d", i) k.fatalf(msg, "invalid op at index %d", i)
} }
if err := op.early(state, k); err != nil { if err := op.early(state, k); err != nil {
if m, ok := messageFromError(err); ok { if m, ok := messageFromError(err); ok {
k.fatal(m) k.fatal(msg, m)
} else { } else {
k.fatalf("cannot prepare op at index %d: %v", i, err) 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 { 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 { 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 { 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 { 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 { if err := k.mkdir(hostDir, 0755); err != nil {
k.fatalf("%v", err) k.fatalf(msg, "%v", err)
} }
// pivot_root uncovers intermediateHostPath in hostDir // pivot_root uncovers intermediateHostPath in hostDir
if err := k.pivotRoot(intermediateHostPath, hostDir); err != nil { if err := k.pivotRoot(intermediateHostPath, hostDir); err != nil {
k.fatalf("cannot pivot into intermediate root: %v", err) k.fatalf(msg, "cannot pivot into intermediate root: %v", err)
} }
if err := k.chdir(FHSRoot); err != nil { if err := k.chdir(fhs.Root); err != nil {
k.fatalf("cannot enter intermediate root: %v", err) k.fatalf(msg, "cannot enter intermediate root: %v", err)
} }
/* apply is called right after pivot_root and entering the new root; /* apply is called right after pivot_root and entering the new root;
@@ -226,64 +233,64 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
for i, op := range *params.Ops { for i, op := range *params.Ops {
// ops already checked during early setup // ops already checked during early setup
if prefix, ok := op.prefix(); ok { if prefix, ok := op.prefix(); ok {
k.verbosef("%s %s", prefix, op) msg.Verbosef("%s %s", prefix, op)
} }
if err := op.apply(state, k); err != nil { if err := op.apply(state, k); err != nil {
if m, ok := messageFromError(err); ok { if m, ok := messageFromError(err); ok {
k.fatal(m) k.fatal(msg, m)
} else { } else {
k.fatalf("cannot apply op at index %d: %v", i, err) k.fatalf(msg, "cannot apply op at index %d: %v", i, err)
} }
} }
} }
// setup requiring host root complete at this point // setup requiring host root complete at this point
if err := k.mount(hostDir, hostDir, zeroString, MS_SILENT|MS_REC|MS_PRIVATE, zeroString); err != nil { if err := k.mount(hostDir, hostDir, zeroString, MS_SILENT|MS_REC|MS_PRIVATE, zeroString); err != nil {
k.fatalf("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 { 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 var fd int
if err := IgnoringEINTR(func() (err error) { 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 return
}); err != nil { }); 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 { 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 { 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 { 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 { 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 { if err := k.chdir(fhs.Root); err != nil {
k.fatalf("cannot enter root: %v", err) k.fatalf(msg, "cannot enter root: %v", err)
} }
if err := k.close(fd); err != nil { 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 { 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++ { for i := uintptr(0); i <= lastcap; i++ {
if params.Privileged && i == CAP_SYS_ADMIN { if params.Privileged && i == CAP_SYS_ADMIN {
continue continue
} }
if err := k.capBoundingSetDrop(i); err != nil { if err := k.capBoundingSetDrop(i); err != nil {
k.fatalf("cannot drop capability from bounding set: %v", err) k.fatalf(msg, "cannot drop capability from bounding set: %v", err)
} }
} }
@@ -292,29 +299,29 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN) keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN)
if err := k.capAmbientRaise(CAP_SYS_ADMIN); err != nil { if err := k.capAmbientRaise(CAP_SYS_ADMIN); err != nil {
k.fatalf("cannot raise CAP_SYS_ADMIN: %v", err) k.fatalf(msg, "cannot raise CAP_SYS_ADMIN: %v", err)
} }
} }
if err := k.capset( if err := k.capset(
&capHeader{_LINUX_CAPABILITY_VERSION_3, 0}, &capHeader{_LINUX_CAPABILITY_VERSION_3, 0},
&[2]capData{{0, keep[0], keep[0]}, {0, keep[1], keep[1]}}, &[2]capData{{0, keep[0], keep[0]}, {0, keep[1], keep[1]}},
); err != nil { ); err != nil {
k.fatalf("cannot capset: %v", err) k.fatalf(msg, "cannot capset: %v", err)
} }
if !params.SeccompDisable { if !params.SeccompDisable {
rules := params.SeccompRules rules := params.SeccompRules
if len(rules) == 0 { // non-empty rules slice always overrides presets if len(rules) == 0 { // non-empty rules slice always overrides presets
k.verbosef("resolving presets %#x", params.SeccompPresets) msg.Verbosef("resolving presets %#x", params.SeccompPresets)
rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags) rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags)
} }
if err := k.seccompLoad(rules, params.SeccompFlags); err != nil { if err := k.seccompLoad(rules, params.SeccompFlags); err != nil {
// this also indirectly asserts PR_SET_NO_NEW_PRIVS // this also indirectly asserts PR_SET_NO_NEW_PRIVS
k.fatalf("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 { } else {
k.verbose("syscall filter not configured") msg.Verbose("syscall filter not configured")
} }
extraFiles := make([]*os.File, params.Count) extraFiles := make([]*os.File, params.Count)
@@ -331,14 +338,14 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
cmd.ExtraFiles = extraFiles cmd.ExtraFiles = extraFiles
cmd.Dir = params.Dir.String() 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 { 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 { if err := closeSetup(); err != nil {
k.printf("cannot close setup pipe: %v", err) k.printf(msg, "cannot close setup pipe: %v", err)
// not fatal // not fatal
} }
@@ -372,7 +379,7 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
} }
} }
if !errors.Is(err, ECHILD) { if !errors.Is(err, ECHILD) {
k.printf("unexpected wait4 response: %v", err) k.printf(msg, "unexpected wait4 response: %v", err)
} }
close(done) close(done)
@@ -389,50 +396,50 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
for { for {
select { select {
case s := <-sig: case s := <-sig:
if k.resume() { if msg.Resume() {
k.verbosef("%s after process start", s.String()) msg.Verbosef("%s after process start", s.String())
} else { } else {
k.verbosef("got %s", s.String()) msg.Verbosef("got %s", s.String())
} }
if s == CancelSignal && params.ForwardCancel && cmd.Process != nil { 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 { 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 continue
} }
k.beforeExit() msg.BeforeExit()
k.exit(0) k.exit(0)
case w := <-info: case w := <-info:
if w.wpid == cmd.Process.Pid { if w.wpid == cmd.Process.Pid {
// initial process exited, output is most likely available again // initial process exited, output is most likely available again
k.resume() msg.Resume()
switch { switch {
case w.wstatus.Exited(): case w.wstatus.Exited():
r = w.wstatus.ExitStatus() 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(): case w.wstatus.Signaled():
r = 128 + int(w.wstatus.Signal()) 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: default:
r = 255 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) }() go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
} }
case <-done: case <-done:
k.beforeExit() msg.BeforeExit()
k.exit(r) k.exit(r)
case <-timeout: case <-timeout:
k.printf("timeout exceeded waiting for lingering processes") k.printf(msg, "timeout exceeded waiting for lingering processes")
k.beforeExit() msg.BeforeExit()
k.exit(r) k.exit(r)
} }
} }
@@ -441,10 +448,16 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
const initName = "init" const initName = "init"
// TryArgv0 calls [Init] if the last element of argv0 is "init". // TryArgv0 calls [Init] if the last element of argv0 is "init".
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 { if len(os.Args) > 0 && path.Base(os.Args[0]) == initName {
msg = v Init(msg)
Init(prepare, setVerbose)
msg.BeforeExit() msg.BeforeExit()
os.Exit(0) os.Exit(0)
} }

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -6,30 +6,34 @@ import (
"syscall" "syscall"
"testing" "testing"
"hakurei.app/container/bits"
"hakurei.app/container/check"
"hakurei.app/container/stub" "hakurei.app/container/stub"
) )
func TestBindMountOp(t *testing.T) { func TestBindMountOp(t *testing.T) {
t.Parallel()
checkOpBehaviour(t, []opBehaviourTestCase{ checkOpBehaviour(t, []opBehaviourTestCase{
{"ENOENT not optional", new(Params), &BindMountOp{ {"ENOENT not optional", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: check.MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: check.MustAbs("/bin/"),
}, []stub.Call{ }, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "", syscall.ENOENT), call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "", syscall.ENOENT),
}, syscall.ENOENT, nil, nil}, }, syscall.ENOENT, nil, nil},
{"skip optional", new(Params), &BindMountOp{ {"skip optional", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: check.MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: check.MustAbs("/bin/"),
Flags: BindOptional, Flags: bits.BindOptional,
}, []stub.Call{ }, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "", syscall.ENOENT), call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "", syscall.ENOENT),
}, nil, nil, nil}, }, nil, nil, nil},
{"success optional", new(Params), &BindMountOp{ {"success optional", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: check.MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: check.MustAbs("/bin/"),
Flags: BindOptional, Flags: bits.BindOptional,
}, []stub.Call{ }, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil), call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []stub.Call{ }, nil, []stub.Call{
@@ -40,9 +44,9 @@ func TestBindMountOp(t *testing.T) {
}, nil}, }, nil},
{"ensureFile device", new(Params), &BindMountOp{ {"ensureFile device", new(Params), &BindMountOp{
Source: MustAbs("/dev/null"), Source: check.MustAbs("/dev/null"),
Target: MustAbs("/dev/null"), Target: check.MustAbs("/dev/null"),
Flags: BindWritable | BindDevice, Flags: bits.BindWritable | bits.BindDevice,
}, []stub.Call{ }, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil), call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
}, nil, []stub.Call{ }, nil, []stub.Call{
@@ -51,17 +55,17 @@ func TestBindMountOp(t *testing.T) {
}, stub.UniqueError(5)}, }, stub.UniqueError(5)},
{"mkdirAll ensure", new(Params), &BindMountOp{ {"mkdirAll ensure", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: check.MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: check.MustAbs("/bin/"),
Flags: BindEnsure, Flags: bits.BindEnsure,
}, []stub.Call{ }, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/bin/", os.FileMode(0700)}, nil, stub.UniqueError(4)), call("mkdirAll", stub.ExpectArgs{"/bin/", os.FileMode(0700)}, nil, stub.UniqueError(4)),
}, stub.UniqueError(4), nil, nil}, }, stub.UniqueError(4), nil, nil},
{"success ensure", new(Params), &BindMountOp{ {"success ensure", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: check.MustAbs("/bin/"),
Target: MustAbs("/usr/bin/"), Target: check.MustAbs("/usr/bin/"),
Flags: BindEnsure, Flags: bits.BindEnsure,
}, []stub.Call{ }, []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/bin/", os.FileMode(0700)}, nil, nil), call("mkdirAll", stub.ExpectArgs{"/bin/", os.FileMode(0700)}, nil, nil),
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil), call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
@@ -73,9 +77,9 @@ func TestBindMountOp(t *testing.T) {
}, nil}, }, nil},
{"success device ro", new(Params), &BindMountOp{ {"success device ro", new(Params), &BindMountOp{
Source: MustAbs("/dev/null"), Source: check.MustAbs("/dev/null"),
Target: MustAbs("/dev/null"), Target: check.MustAbs("/dev/null"),
Flags: BindDevice, Flags: bits.BindDevice,
}, []stub.Call{ }, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil), call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
}, nil, []stub.Call{ }, nil, []stub.Call{
@@ -86,9 +90,9 @@ func TestBindMountOp(t *testing.T) {
}, nil}, }, nil},
{"success device", new(Params), &BindMountOp{ {"success device", new(Params), &BindMountOp{
Source: MustAbs("/dev/null"), Source: check.MustAbs("/dev/null"),
Target: MustAbs("/dev/null"), Target: check.MustAbs("/dev/null"),
Flags: BindWritable | BindDevice, Flags: bits.BindWritable | bits.BindDevice,
}, []stub.Call{ }, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil), call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
}, nil, []stub.Call{ }, nil, []stub.Call{
@@ -99,15 +103,15 @@ func TestBindMountOp(t *testing.T) {
}, nil}, }, nil},
{"evalSymlinks", new(Params), &BindMountOp{ {"evalSymlinks", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: check.MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: check.MustAbs("/bin/"),
}, []stub.Call{ }, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", stub.UniqueError(3)), call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", stub.UniqueError(3)),
}, stub.UniqueError(3), nil, nil}, }, stub.UniqueError(3), nil, nil},
{"stat", new(Params), &BindMountOp{ {"stat", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: check.MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: check.MustAbs("/bin/"),
}, []stub.Call{ }, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil), call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []stub.Call{ }, nil, []stub.Call{
@@ -115,8 +119,8 @@ func TestBindMountOp(t *testing.T) {
}, stub.UniqueError(2)}, }, stub.UniqueError(2)},
{"mkdirAll", new(Params), &BindMountOp{ {"mkdirAll", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: check.MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: check.MustAbs("/bin/"),
}, []stub.Call{ }, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil), call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []stub.Call{ }, nil, []stub.Call{
@@ -125,8 +129,8 @@ func TestBindMountOp(t *testing.T) {
}, stub.UniqueError(1)}, }, stub.UniqueError(1)},
{"bindMount", new(Params), &BindMountOp{ {"bindMount", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: check.MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: check.MustAbs("/bin/"),
}, []stub.Call{ }, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil), call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []stub.Call{ }, nil, []stub.Call{
@@ -137,8 +141,8 @@ func TestBindMountOp(t *testing.T) {
}, stub.UniqueError(0)}, }, stub.UniqueError(0)},
{"success eval equals", new(Params), &BindMountOp{ {"success eval equals", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: check.MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: check.MustAbs("/bin/"),
}, []stub.Call{ }, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/bin", nil), call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/bin", nil),
}, nil, []stub.Call{ }, nil, []stub.Call{
@@ -149,8 +153,8 @@ func TestBindMountOp(t *testing.T) {
}, nil}, }, nil},
{"success", new(Params), &BindMountOp{ {"success", new(Params), &BindMountOp{
Source: MustAbs("/bin/"), Source: check.MustAbs("/bin/"),
Target: MustAbs("/bin/"), Target: check.MustAbs("/bin/"),
}, []stub.Call{ }, []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil), call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
}, nil, []stub.Call{ }, nil, []stub.Call{
@@ -162,7 +166,10 @@ func TestBindMountOp(t *testing.T) {
}) })
t.Run("unreachable", func(t *testing.T) { t.Run("unreachable", func(t *testing.T) {
t.Parallel()
t.Run("nil sourceFinal not optional", func(t *testing.T) { t.Run("nil sourceFinal not optional", func(t *testing.T) {
t.Parallel()
wantErr := OpStateError("bind") wantErr := OpStateError("bind")
if err := new(BindMountOp).apply(nil, nil); !errors.Is(err, wantErr) { if err := new(BindMountOp).apply(nil, nil); !errors.Is(err, wantErr) {
t.Errorf("apply: error = %v, want %v", err, wantErr) t.Errorf("apply: error = %v, want %v", err, wantErr)
@@ -173,21 +180,21 @@ func TestBindMountOp(t *testing.T) {
checkOpsValid(t, []opValidTestCase{ checkOpsValid(t, []opValidTestCase{
{"nil", (*BindMountOp)(nil), false}, {"nil", (*BindMountOp)(nil), false},
{"zero", new(BindMountOp), false}, {"zero", new(BindMountOp), false},
{"nil source", &BindMountOp{Target: MustAbs("/")}, false}, {"nil source", &BindMountOp{Target: check.MustAbs("/")}, false},
{"nil target", &BindMountOp{Source: MustAbs("/")}, false}, {"nil target", &BindMountOp{Source: check.MustAbs("/")}, false},
{"flag optional ensure", &BindMountOp{Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindOptional | BindEnsure}, false}, {"flag optional ensure", &BindMountOp{Source: check.MustAbs("/"), Target: check.MustAbs("/"), Flags: bits.BindOptional | bits.BindEnsure}, false},
{"valid", &BindMountOp{Source: MustAbs("/"), Target: MustAbs("/")}, true}, {"valid", &BindMountOp{Source: check.MustAbs("/"), Target: check.MustAbs("/")}, true},
}) })
checkOpsBuilder(t, []opsBuilderTestCase{ checkOpsBuilder(t, []opsBuilderTestCase{
{"autoetc", new(Ops).Bind( {"autoetc", new(Ops).Bind(
MustAbs("/etc/"), check.MustAbs("/etc/"),
MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"), check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
0, 0,
), Ops{ ), Ops{
&BindMountOp{ &BindMountOp{
Source: MustAbs("/etc/"), Source: check.MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"), Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, },
}}, }},
}) })
@@ -196,45 +203,45 @@ func TestBindMountOp(t *testing.T) {
{"zero", new(BindMountOp), new(BindMountOp), false}, {"zero", new(BindMountOp), new(BindMountOp), false},
{"internal ne", &BindMountOp{ {"internal ne", &BindMountOp{
Source: MustAbs("/etc/"), Source: check.MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"), Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, &BindMountOp{ }, &BindMountOp{
Source: MustAbs("/etc/"), Source: check.MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"), Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
sourceFinal: MustAbs("/etc/"), sourceFinal: check.MustAbs("/etc/"),
}, true}, }, true},
{"flags differs", &BindMountOp{ {"flags differs", &BindMountOp{
Source: MustAbs("/etc/"), Source: check.MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"), Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, &BindMountOp{ }, &BindMountOp{
Source: MustAbs("/etc/"), Source: check.MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"), Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Flags: BindOptional, Flags: bits.BindOptional,
}, false}, }, false},
{"source differs", &BindMountOp{ {"source differs", &BindMountOp{
Source: MustAbs("/.hakurei/etc/"), Source: check.MustAbs("/.hakurei/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"), Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, &BindMountOp{ }, &BindMountOp{
Source: MustAbs("/etc/"), Source: check.MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"), Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, false}, }, false},
{"target differs", &BindMountOp{ {"target differs", &BindMountOp{
Source: MustAbs("/etc/"), Source: check.MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"), Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, &BindMountOp{ }, &BindMountOp{
Source: MustAbs("/etc/"), Source: check.MustAbs("/etc/"),
Target: MustAbs("/etc/"), Target: check.MustAbs("/etc/"),
}, false}, }, false},
{"equals", &BindMountOp{ {"equals", &BindMountOp{
Source: MustAbs("/etc/"), Source: check.MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"), Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, &BindMountOp{ }, &BindMountOp{
Source: MustAbs("/etc/"), Source: check.MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"), Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, true}, }, true},
}) })
@@ -242,14 +249,14 @@ func TestBindMountOp(t *testing.T) {
{"invalid", new(BindMountOp), "mounting", "<invalid>"}, {"invalid", new(BindMountOp), "mounting", "<invalid>"},
{"autoetc", &BindMountOp{ {"autoetc", &BindMountOp{
Source: MustAbs("/etc/"), Source: check.MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"), Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, "mounting", `"/etc/" on "/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659" flags 0x0`}, }, "mounting", `"/etc/" on "/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659" flags 0x0`},
{"hostdev", &BindMountOp{ {"hostdev", &BindMountOp{
Source: MustAbs("/dev/"), Source: check.MustAbs("/dev/"),
Target: MustAbs("/dev/"), Target: check.MustAbs("/dev/"),
Flags: BindWritable | BindDevice, Flags: bits.BindWritable | bits.BindDevice,
}, "mounting", `"/dev/" flags 0x6`}, }, "mounting", `"/dev/" flags 0x6`},
}) })
} }

View File

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

View File

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

View File

@@ -4,19 +4,21 @@ import (
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"os" "os"
"hakurei.app/container/check"
) )
func init() { gob.Register(new(MkdirOp)) } func init() { gob.Register(new(MkdirOp)) }
// Mkdir appends an [Op] that creates a directory in the container filesystem. // Mkdir appends an [Op] that creates a directory in the container filesystem.
func (f *Ops) Mkdir(name *Absolute, perm os.FileMode) *Ops { func (f *Ops) Mkdir(name *check.Absolute, perm os.FileMode) *Ops {
*f = append(*f, &MkdirOp{name, perm}) *f = append(*f, &MkdirOp{name, perm})
return f return f
} }
// MkdirOp creates a directory at container Path with permission bits set to Perm. // MkdirOp creates a directory at container Path with permission bits set to Perm.
type MkdirOp struct { type MkdirOp struct {
Path *Absolute Path *check.Absolute
Perm os.FileMode Perm os.FileMode
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,18 +4,20 @@ import (
"encoding/gob" "encoding/gob"
"fmt" "fmt"
. "syscall" . "syscall"
"hakurei.app/container/check"
) )
func init() { gob.Register(new(MountProcOp)) } func init() { gob.Register(new(MountProcOp)) }
// Proc appends an [Op] that mounts a private instance of proc. // Proc appends an [Op] that mounts a private instance of proc.
func (f *Ops) Proc(target *Absolute) *Ops { func (f *Ops) Proc(target *check.Absolute) *Ops {
*f = append(*f, &MountProcOp{target}) *f = append(*f, &MountProcOp{target})
return f return f
} }
// MountProcOp mounts a new instance of [FstypeProc] on container path Target. // MountProcOp mounts a new instance of [FstypeProc] on container path Target.
type MountProcOp struct{ Target *Absolute } type MountProcOp struct{ Target *check.Absolute }
func (p *MountProcOp) Valid() bool { return p != nil && p.Target != nil } func (p *MountProcOp) Valid() bool { return p != nil && p.Target != nil }
func (p *MountProcOp) early(*setupState, syscallDispatcher) error { return nil } func (p *MountProcOp) early(*setupState, syscallDispatcher) error { return nil }

View File

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

View File

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

View File

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

View File

@@ -4,19 +4,21 @@ import (
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"path" "path"
"hakurei.app/container/check"
) )
func init() { gob.Register(new(SymlinkOp)) } func init() { gob.Register(new(SymlinkOp)) }
// Link appends an [Op] that creates a symlink in the container filesystem. // Link appends an [Op] that creates a symlink in the container filesystem.
func (f *Ops) Link(target *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}) *f = append(*f, &SymlinkOp{target, linkName, dereference})
return f return f
} }
// SymlinkOp optionally dereferences LinkName and creates a symlink at container path Target. // SymlinkOp optionally dereferences LinkName and creates a symlink at container path Target.
type SymlinkOp struct { type SymlinkOp struct {
Target *Absolute Target *check.Absolute
// LinkName is an arbitrary uninterpreted pathname. // LinkName is an arbitrary uninterpreted pathname.
LinkName string LinkName string
@@ -28,8 +30,8 @@ func (l *SymlinkOp) Valid() bool { return l != nil && l.Target != nil && l.LinkN
func (l *SymlinkOp) early(_ *setupState, k syscallDispatcher) error { func (l *SymlinkOp) early(_ *setupState, k syscallDispatcher) error {
if l.Dereference { if l.Dereference {
if !isAbs(l.LinkName) { if !path.IsAbs(l.LinkName) {
return &AbsoluteError{l.LinkName} return &check.AbsoluteError{Pathname: l.LinkName}
} }
if name, err := k.readlink(l.LinkName); err != nil { if name, err := k.readlink(l.LinkName); err != nil {
return err return err

View File

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

View File

@@ -7,6 +7,8 @@ import (
"os" "os"
"strconv" "strconv"
. "syscall" . "syscall"
"hakurei.app/container/check"
) )
func init() { gob.Register(new(MountTmpfsOp)) } func init() { gob.Register(new(MountTmpfsOp)) }
@@ -18,13 +20,13 @@ func (e TmpfsSizeError) Error() string {
} }
// Tmpfs appends an [Op] that mounts tmpfs on container path [MountTmpfsOp.Path]. // Tmpfs appends an [Op] that mounts tmpfs on container path [MountTmpfsOp.Path].
func (f *Ops) Tmpfs(target *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}) *f = append(*f, &MountTmpfsOp{SourceTmpfsEphemeral, target, MS_NOSUID | MS_NODEV, size, perm})
return f return f
} }
// Readonly appends an [Op] that mounts read-only tmpfs on container path [MountTmpfsOp.Path]. // Readonly appends an [Op] that mounts read-only tmpfs on container path [MountTmpfsOp.Path].
func (f *Ops) Readonly(target *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}) *f = append(*f, &MountTmpfsOp{SourceTmpfsReadonly, target, MS_RDONLY | MS_NOSUID | MS_NODEV, 0, perm})
return f return f
} }
@@ -32,7 +34,7 @@ func (f *Ops) Readonly(target *Absolute, perm os.FileMode) *Ops {
// MountTmpfsOp mounts [FstypeTmpfs] on container Path. // MountTmpfsOp mounts [FstypeTmpfs] on container Path.
type MountTmpfsOp struct { type MountTmpfsOp struct {
FSName string FSName string
Path *Absolute Path *check.Absolute
Flags uintptr Flags uintptr
Size int Size int
Perm os.FileMode Perm os.FileMode

View File

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

View File

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

View File

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

View File

@@ -10,22 +10,24 @@ import (
) )
func TestBindMount(t *testing.T) { func TestBindMount(t *testing.T) {
t.Parallel()
checkSimple(t, "bindMount", []simpleTestCase{ checkSimple(t, "bindMount", []simpleTestCase{
{"mount", func(k syscallDispatcher) error { {"mount", func(k *kstub) error {
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY) return newProcPaths(k, hostPath).bindMount(nil, "/host/nix", "/sysroot/nix", syscall.MS_RDONLY)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, stub.UniqueError(0xbad)), call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, stub.UniqueError(0xbad)),
}}, stub.UniqueError(0xbad)}, }}, stub.UniqueError(0xbad)},
{"success ne", func(k syscallDispatcher) error { {"success ne", func(k *kstub) error {
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/.host-nix", syscall.MS_RDONLY) return newProcPaths(k, hostPath).bindMount(k, "/host/nix", "/sysroot/.host-nix", syscall.MS_RDONLY)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/.host-nix", "", uintptr(0x9000), ""}, nil, nil), call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/.host-nix", "", uintptr(0x9000), ""}, nil, nil),
call("remount", stub.ExpectArgs{"/sysroot/.host-nix", uintptr(1)}, nil, nil), call("remount", stub.ExpectArgs{"/sysroot/.host-nix", uintptr(1)}, nil, nil),
}}, nil}, }}, nil},
{"success", func(k syscallDispatcher) error { {"success", func(k *kstub) error {
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY) return newProcPaths(k, hostPath).bindMount(k, "/host/nix", "/sysroot/nix", syscall.MS_RDONLY)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, nil), call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, nil),
call("remount", stub.ExpectArgs{"/sysroot/nix", uintptr(1)}, nil, nil), call("remount", stub.ExpectArgs{"/sysroot/nix", uintptr(1)}, nil, nil),
@@ -34,6 +36,8 @@ func TestBindMount(t *testing.T) {
} }
func TestRemount(t *testing.T) { func TestRemount(t *testing.T) {
t.Parallel()
const sampleMountinfoNix = `254 407 253:0 / /host rw,relatime master:1 - ext4 /dev/disk/by-label/nixos rw const sampleMountinfoNix = `254 407 253:0 / /host rw,relatime master:1 - ext4 /dev/disk/by-label/nixos rw
255 254 0:28 / /host/mnt/.ro-cwd ro,noatime master:2 - 9p cwd ro,access=client,msize=16384,trans=virtio 255 254 0:28 / /host/mnt/.ro-cwd ro,noatime master:2 - 9p cwd ro,access=client,msize=16384,trans=virtio
256 254 0:29 / /host/nix/.ro-store rw,relatime master:3 - 9p nix-store rw,cache=f,access=client,msize=16384,trans=virtio 256 254 0:29 / /host/nix/.ro-store rw,relatime master:3 - 9p nix-store rw,cache=f,access=client,msize=16384,trans=virtio
@@ -77,29 +81,29 @@ func TestRemount(t *testing.T) {
416 415 0:30 / /sysroot/nix/store ro,relatime master:5 - overlay overlay rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work` 416 415 0:30 / /sysroot/nix/store ro,relatime master:5 - overlay overlay rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work`
checkSimple(t, "remount", []simpleTestCase{ checkSimple(t, "remount", []simpleTestCase{
{"evalSymlinks", func(k syscallDispatcher) error { {"evalSymlinks", func(k *kstub) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", stub.UniqueError(6)), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", stub.UniqueError(6)),
}}, stub.UniqueError(6)}, }}, stub.UniqueError(6)},
{"open", func(k syscallDispatcher) error { {"open", func(k *kstub) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, stub.UniqueError(5)), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, stub.UniqueError(5)),
}}, &os.PathError{Op: "open", Path: "/sysroot/nix", Err: stub.UniqueError(5)}}, }}, &os.PathError{Op: "open", Path: "/sysroot/nix", Err: stub.UniqueError(5)}},
{"readlink", func(k syscallDispatcher) error { {"readlink", func(k *kstub) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, 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)), call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", stub.UniqueError(4)),
}}, stub.UniqueError(4)}, }}, stub.UniqueError(4)},
{"close", func(k syscallDispatcher) error { {"close", func(k *kstub) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
@@ -107,8 +111,8 @@ func TestRemount(t *testing.T) {
call("close", stub.ExpectArgs{0xdeadbeef}, nil, stub.UniqueError(3)), call("close", stub.ExpectArgs{0xdeadbeef}, nil, stub.UniqueError(3)),
}}, &os.PathError{Op: "close", Path: "/sysroot/nix", Err: stub.UniqueError(3)}}, }}, &os.PathError{Op: "close", Path: "/sysroot/nix", Err: stub.UniqueError(3)}},
{"mountinfo no match", func(k syscallDispatcher) error { {"mountinfo no match", func(k *kstub) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount(k, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/.hakurei", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/.hakurei", nil),
call("verbosef", stub.ExpectArgs{"target resolves to %q", []any{"/sysroot/.hakurei"}}, nil, nil), call("verbosef", stub.ExpectArgs{"target resolves to %q", []any{"/sysroot/.hakurei"}}, nil, nil),
@@ -118,8 +122,8 @@ func TestRemount(t *testing.T) {
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil), call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
}}, &vfs.DecoderError{Op: "unfold", Line: -1, Err: vfs.UnfoldTargetError("/sysroot/.hakurei")}}, }}, &vfs.DecoderError{Op: "unfold", Line: -1, Err: vfs.UnfoldTargetError("/sysroot/.hakurei")}},
{"mountinfo", func(k syscallDispatcher) error { {"mountinfo", func(k *kstub) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
@@ -128,8 +132,8 @@ func TestRemount(t *testing.T) {
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile("\x00"), nil), call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile("\x00"), nil),
}}, &vfs.DecoderError{Op: "parse", Line: 0, Err: vfs.ErrMountInfoFields}}, }}, &vfs.DecoderError{Op: "parse", Line: 0, Err: vfs.ErrMountInfoFields}},
{"mount", func(k syscallDispatcher) error { {"mount", func(k *kstub) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
@@ -139,8 +143,8 @@ func TestRemount(t *testing.T) {
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, stub.UniqueError(2)), call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, stub.UniqueError(2)),
}}, stub.UniqueError(2)}, }}, stub.UniqueError(2)},
{"mount propagate", func(k syscallDispatcher) error { {"mount propagate", func(k *kstub) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
@@ -151,8 +155,8 @@ func TestRemount(t *testing.T) {
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, stub.UniqueError(1)), call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, stub.UniqueError(1)),
}}, stub.UniqueError(1)}, }}, stub.UniqueError(1)},
{"success toplevel", func(k syscallDispatcher) error { {"success toplevel", func(k *kstub) error {
return newProcPaths(k, hostPath).remount("/sysroot/bin", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount(nil, "/sysroot/bin", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/bin"}, "/sysroot/bin", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/bin"}, "/sysroot/bin", nil),
call("open", stub.ExpectArgs{"/sysroot/bin", 0x280000, uint32(0)}, 0xbabe, nil), call("open", stub.ExpectArgs{"/sysroot/bin", 0x280000, uint32(0)}, 0xbabe, nil),
@@ -162,8 +166,8 @@ func TestRemount(t *testing.T) {
call("mount", stub.ExpectArgs{"none", "/sysroot/bin", "", uintptr(0x209027), ""}, nil, nil), call("mount", stub.ExpectArgs{"none", "/sysroot/bin", "", uintptr(0x209027), ""}, nil, nil),
}}, nil}, }}, nil},
{"success EACCES", func(k syscallDispatcher) error { {"success EACCES", func(k *kstub) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
@@ -175,8 +179,8 @@ func TestRemount(t *testing.T) {
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil), call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil),
}}, nil}, }}, nil},
{"success no propagate", func(k syscallDispatcher) error { {"success no propagate", func(k *kstub) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
@@ -186,8 +190,8 @@ func TestRemount(t *testing.T) {
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil), call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil),
}}, nil}, }}, nil},
{"success case sensitive", func(k syscallDispatcher) error { {"success case sensitive", func(k *kstub) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil),
@@ -199,8 +203,8 @@ func TestRemount(t *testing.T) {
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil), call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil),
}}, nil}, }}, nil},
{"success", func(k syscallDispatcher) error { {"success", func(k *kstub) error {
return newProcPaths(k, hostPath).remount("/sysroot/.nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount(k, "/sysroot/.nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/.nix"}, "/sysroot/NIX", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/.nix"}, "/sysroot/NIX", nil),
call("verbosef", stub.ExpectArgs{"target resolves to %q", []any{"/sysroot/NIX"}}, nil, nil), call("verbosef", stub.ExpectArgs{"target resolves to %q", []any{"/sysroot/NIX"}}, nil, nil),
@@ -216,19 +220,21 @@ func TestRemount(t *testing.T) {
} }
func TestRemountWithFlags(t *testing.T) { func TestRemountWithFlags(t *testing.T) {
t.Parallel()
checkSimple(t, "remountWithFlags", []simpleTestCase{ checkSimple(t, "remountWithFlags", []simpleTestCase{
{"noop unmatched", func(k syscallDispatcher) error { {"noop unmatched", func(k *kstub) error {
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime,cat"}}, 0) return remountWithFlags(k, k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime,cat"}}, 0)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("verbosef", stub.ExpectArgs{"unmatched vfs options: %q", []any{[]string{"cat"}}}, nil, nil), call("verbosef", stub.ExpectArgs{"unmatched vfs options: %q", []any{[]string{"cat"}}}, nil, nil),
}}, nil}, }}, nil},
{"noop", func(k syscallDispatcher) error { {"noop", func(k *kstub) error {
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, 0) return remountWithFlags(k, nil, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, 0)
}, stub.Expect{}, nil}, }, stub.Expect{}, nil},
{"success", func(k syscallDispatcher) error { {"success", func(k *kstub) error {
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, syscall.MS_RDONLY) return remountWithFlags(k, nil, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, syscall.MS_RDONLY)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("mount", stub.ExpectArgs{"none", "", "", uintptr(0x209021), ""}, nil, nil), call("mount", stub.ExpectArgs{"none", "", "", uintptr(0x209021), ""}, nil, nil),
}}, nil}, }}, nil},
@@ -236,21 +242,23 @@ func TestRemountWithFlags(t *testing.T) {
} }
func TestMountTmpfs(t *testing.T) { func TestMountTmpfs(t *testing.T) {
t.Parallel()
checkSimple(t, "mountTmpfs", []simpleTestCase{ checkSimple(t, "mountTmpfs", []simpleTestCase{
{"mkdirAll", func(k syscallDispatcher) error { {"mkdirAll", func(k *kstub) error {
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700) return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, stub.UniqueError(0)), call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, stub.UniqueError(0)),
}}, stub.UniqueError(0)}, }}, stub.UniqueError(0)},
{"success no size", func(k syscallDispatcher) error { {"success no size", func(k *kstub) error {
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 0, 0710) return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 0, 0710)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0750)}, nil, nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0750)}, nil, nil),
call("mount", stub.ExpectArgs{"ephemeral", "/sysroot/run/user/1000", "tmpfs", uintptr(0), "mode=0710"}, nil, nil), call("mount", stub.ExpectArgs{"ephemeral", "/sysroot/run/user/1000", "tmpfs", uintptr(0), "mode=0710"}, nil, nil),
}}, nil}, }}, nil},
{"success", func(k syscallDispatcher) error { {"success", func(k *kstub) error {
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700) return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, nil),
@@ -260,6 +268,8 @@ func TestMountTmpfs(t *testing.T) {
} }
func TestParentPerm(t *testing.T) { func TestParentPerm(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
perm os.FileMode perm os.FileMode
want os.FileMode want os.FileMode
@@ -275,29 +285,10 @@ func TestParentPerm(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.perm.String(), func(t *testing.T) { t.Run(tc.perm.String(), func(t *testing.T) {
t.Parallel()
if got := parentPerm(tc.perm); got != tc.want { if got := parentPerm(tc.perm); got != tc.want {
t.Errorf("parentPerm: %#o, want %#o", got, tc.want) t.Errorf("parentPerm: %#o, want %#o", got, tc.want)
} }
}) })
} }
} }
func TestEscapeOverlayDataSegment(t *testing.T) {
testCases := []struct {
name string
s string
want string
}{
{"zero", zeroString, zeroString},
{"multi", `\\\:,:,\\\`, `\\\\\\\:\,\:\,\\\\\\`},
{"bwrap", `/path :,\`, `/path \:\,\\`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := EscapeOverlayDataSegment(tc.s); got != tc.want {
t.Errorf("escapeOverlayDataSegment: %s, want %s", got, tc.want)
}
})
}
}

View File

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

View File

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

View File

@@ -31,7 +31,7 @@ func Receive(key string, e any, fdp *uintptr) (func() error, error) {
return nil, ErrReceiveEnv return nil, ErrReceiveEnv
} else { } else {
if fd, err := strconv.Atoi(s); err != nil { if fd, err := strconv.Atoi(s); err != nil {
return nil, errors.Unwrap(err) return nil, optionalErrorUnwrap(err)
} else { } else {
setup = os.NewFile(uintptr(fd), "setup") setup = os.NewFile(uintptr(fd), "setup")
if setup == nil { if setup == nil {

View File

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

View File

@@ -10,6 +10,7 @@ import (
"testing" "testing"
"unsafe" "unsafe"
"hakurei.app/container/check"
"hakurei.app/container/vfs" "hakurei.app/container/vfs"
) )
@@ -49,8 +50,8 @@ func TestToHost(t *testing.T) {
} }
} }
// InternalToHostOvlEscape exports toHost passed to EscapeOverlayDataSegment. // InternalToHostOvlEscape exports toHost passed to [check.EscapeOverlayDataSegment].
func InternalToHostOvlEscape(s string) string { return EscapeOverlayDataSegment(toHost(s)) } func InternalToHostOvlEscape(s string) string { return check.EscapeOverlayDataSegment(toHost(s)) }
func TestCreateFile(t *testing.T) { func TestCreateFile(t *testing.T) {
t.Run("nonexistent", func(t *testing.T) { t.Run("nonexistent", func(t *testing.T) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,46 +4,33 @@ package seccomp
import ( import (
. "syscall" . "syscall"
"hakurei.app/container/bits"
) )
type FilterPreset int func Preset(presets bits.FilterPreset, flags ExportFlag) (rules []NativeRule) {
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) {
allowedPersonality := PER_LINUX allowedPersonality := PER_LINUX
if presets&PresetLinux32 != 0 { if presets&bits.PresetLinux32 != 0 {
allowedPersonality = PER_LINUX32 allowedPersonality = PER_LINUX32
} }
presetDevelFinal := presetDevel(ScmpDatum(allowedPersonality)) presetDevelFinal := presetDevel(ScmpDatum(allowedPersonality))
l := len(presetCommon) l := len(presetCommon)
if presets&PresetDenyNS != 0 { if presets&bits.PresetDenyNS != 0 {
l += len(presetNamespace) l += len(presetNamespace)
} }
if presets&PresetDenyTTY != 0 { if presets&bits.PresetDenyTTY != 0 {
l += len(presetTTY) l += len(presetTTY)
} }
if presets&PresetDenyDevel != 0 { if presets&bits.PresetDenyDevel != 0 {
l += len(presetDevelFinal) l += len(presetDevelFinal)
} }
if flags&AllowMultiarch == 0 { if flags&AllowMultiarch == 0 {
l += len(presetEmu) l += len(presetEmu)
} }
if presets&PresetExt != 0 { if presets&bits.PresetExt != 0 {
l += len(presetCommonExt) l += len(presetCommonExt)
if presets&PresetDenyNS != 0 { if presets&bits.PresetDenyNS != 0 {
l += len(presetNamespaceExt) l += len(presetNamespaceExt)
} }
if flags&AllowMultiarch == 0 { if flags&AllowMultiarch == 0 {
@@ -53,21 +40,21 @@ func Preset(presets FilterPreset, flags ExportFlag) (rules []NativeRule) {
rules = make([]NativeRule, 0, l) rules = make([]NativeRule, 0, l)
rules = append(rules, presetCommon...) rules = append(rules, presetCommon...)
if presets&PresetDenyNS != 0 { if presets&bits.PresetDenyNS != 0 {
rules = append(rules, presetNamespace...) rules = append(rules, presetNamespace...)
} }
if presets&PresetDenyTTY != 0 { if presets&bits.PresetDenyTTY != 0 {
rules = append(rules, presetTTY...) rules = append(rules, presetTTY...)
} }
if presets&PresetDenyDevel != 0 { if presets&bits.PresetDenyDevel != 0 {
rules = append(rules, presetDevelFinal...) rules = append(rules, presetDevelFinal...)
} }
if flags&AllowMultiarch == 0 { if flags&AllowMultiarch == 0 {
rules = append(rules, presetEmu...) rules = append(rules, presetEmu...)
} }
if presets&PresetExt != 0 { if presets&bits.PresetExt != 0 {
rules = append(rules, presetCommonExt...) rules = append(rules, presetCommonExt...)
if presets&PresetDenyNS != 0 { if presets&bits.PresetDenyNS != 0 {
rules = append(rules, presetNamespaceExt...) rules = append(rules, presetNamespaceExt...)
} }
if flags&AllowMultiarch == 0 { if flags&AllowMultiarch == 0 {

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,10 @@ import (
) )
func TestUniqueError(t *testing.T) { func TestUniqueError(t *testing.T) {
t.Parallel()
t.Run("format", func(t *testing.T) { t.Run("format", func(t *testing.T) {
t.Parallel()
want := "unique error 2989 injected by the test suite" want := "unique error 2989 injected by the test suite"
if got := stub.UniqueError(0xbad).Error(); got != want { if got := stub.UniqueError(0xbad).Error(); got != want {
t.Errorf("Error: %q, want %q", got, want) t.Errorf("Error: %q, want %q", got, want)
@@ -17,13 +20,17 @@ func TestUniqueError(t *testing.T) {
}) })
t.Run("is", func(t *testing.T) { t.Run("is", func(t *testing.T) {
t.Parallel()
t.Run("type", func(t *testing.T) { t.Run("type", func(t *testing.T) {
t.Parallel()
if errors.Is(stub.UniqueError(0), syscall.ENOTRECOVERABLE) { if errors.Is(stub.UniqueError(0), syscall.ENOTRECOVERABLE) {
t.Error("Is: unexpected true") t.Error("Is: unexpected true")
} }
}) })
t.Run("val", func(t *testing.T) { t.Run("val", func(t *testing.T) {
t.Parallel()
if errors.Is(stub.UniqueError(0), stub.UniqueError(1)) { if errors.Is(stub.UniqueError(0), stub.UniqueError(1)) {
t.Error("Is: unexpected true") t.Error("Is: unexpected true")
} }

View File

@@ -32,13 +32,20 @@ func (o *overrideTFailNow) Fail() {
} }
func TestHandleExit(t *testing.T) { func TestHandleExit(t *testing.T) {
t.Parallel()
t.Run("exit", func(t *testing.T) { t.Run("exit", func(t *testing.T) {
t.Parallel()
defer stub.HandleExit(t) defer stub.HandleExit(t)
panic(stub.PanicExit) panic(stub.PanicExit)
}) })
t.Run("goexit", func(t *testing.T) { t.Run("goexit", func(t *testing.T) {
t.Parallel()
t.Run("FailNow", func(t *testing.T) { t.Run("FailNow", func(t *testing.T) {
t.Parallel()
ot := &overrideTFailNow{T: t} ot := &overrideTFailNow{T: t}
defer func() { defer func() {
if !ot.failNow { if !ot.failNow {
@@ -50,6 +57,8 @@ func TestHandleExit(t *testing.T) {
}) })
t.Run("Fail", func(t *testing.T) { t.Run("Fail", func(t *testing.T) {
t.Parallel()
ot := &overrideTFailNow{T: t} ot := &overrideTFailNow{T: t}
defer func() { defer func() {
if !ot.fail { if !ot.fail {
@@ -62,11 +71,16 @@ func TestHandleExit(t *testing.T) {
}) })
t.Run("nil", func(t *testing.T) { t.Run("nil", func(t *testing.T) {
t.Parallel()
defer stub.HandleExit(t) defer stub.HandleExit(t)
}) })
t.Run("passthrough", func(t *testing.T) { t.Run("passthrough", func(t *testing.T) {
t.Parallel()
t.Run("toplevel", func(t *testing.T) { t.Run("toplevel", func(t *testing.T) {
t.Parallel()
defer func() { defer func() {
want := 0xcafebabe want := 0xcafebabe
if r := recover(); r != want { if r := recover(); r != want {
@@ -79,6 +93,8 @@ func TestHandleExit(t *testing.T) {
}) })
t.Run("new", func(t *testing.T) { t.Run("new", func(t *testing.T) {
t.Parallel()
defer func() { defer func() {
want := 0xcafe want := 0xcafe
if r := recover(); r != want { if r := recover(); r != want {

View File

@@ -45,12 +45,17 @@ 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)} return &Stub[K]{TB: tb, makeK: makeK, want: want, wg: new(sync.WaitGroup)}
} }
func (s *Stub[K]) FailNow() { panic(panicFailNow) } func (s *Stub[K]) FailNow() { s.Helper(); panic(panicFailNow) }
func (s *Stub[K]) Fatal(args ...any) { s.Error(args...); panic(panicFatal) } func (s *Stub[K]) Fatal(args ...any) { s.Helper(); s.Error(args...); panic(panicFatal) }
func (s *Stub[K]) Fatalf(format string, args ...any) { s.Errorf(format, args...); panic(panicFatalf) } func (s *Stub[K]) Fatalf(format string, args ...any) {
func (s *Stub[K]) SkipNow() { panic("invalid call to SkipNow") } s.Helper()
func (s *Stub[K]) Skip(...any) { panic("invalid call to Skip") } s.Errorf(format, args...)
func (s *Stub[K]) Skipf(string, ...any) { panic("invalid call to Skipf") } 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 // New calls f in a new goroutine
func (s *Stub[K]) New(f func(k K)) { func (s *Stub[K]) New(f func(k K)) {

View File

@@ -36,49 +36,65 @@ func (t *overrideT) Errorf(format string, args ...any) {
} }
func TestStub(t *testing.T) { func TestStub(t *testing.T) {
t.Parallel()
t.Run("goexit", func(t *testing.T) { t.Run("goexit", func(t *testing.T) {
t.Parallel()
t.Run("FailNow", func(t *testing.T) { t.Run("FailNow", func(t *testing.T) {
t.Parallel()
defer func() { defer func() {
if r := recover(); r != panicFailNow { if r := recover(); r != panicFailNow {
t.Errorf("recover: %v", r) t.Errorf("recover: %v", r)
} }
}() }()
new(stubHolder).FailNow() stubHolder{&Stub[stubHolder]{TB: t}}.FailNow()
}) })
t.Run("SkipNow", func(t *testing.T) { t.Run("SkipNow", func(t *testing.T) {
t.Parallel()
defer func() { defer func() {
want := "invalid call to SkipNow" want := "invalid call to SkipNow"
if r := recover(); r != want { if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want) t.Errorf("recover: %v, want %v", r, want)
} }
}() }()
new(stubHolder).SkipNow() stubHolder{&Stub[stubHolder]{TB: t}}.SkipNow()
}) })
t.Run("Skip", func(t *testing.T) { t.Run("Skip", func(t *testing.T) {
t.Parallel()
defer func() { defer func() {
want := "invalid call to Skip" want := "invalid call to Skip"
if r := recover(); r != want { if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want) t.Errorf("recover: %v, want %v", r, want)
} }
}() }()
new(stubHolder).Skip() stubHolder{&Stub[stubHolder]{TB: t}}.Skip()
}) })
t.Run("Skipf", func(t *testing.T) { t.Run("Skipf", func(t *testing.T) {
t.Parallel()
defer func() { defer func() {
want := "invalid call to Skipf" want := "invalid call to Skipf"
if r := recover(); r != want { if r := recover(); r != want {
t.Errorf("recover: %v, want %v", r, want) t.Errorf("recover: %v, want %v", r, want)
} }
}() }()
new(stubHolder).Skipf("") stubHolder{&Stub[stubHolder]{TB: t}}.Skipf("")
}) })
}) })
t.Run("new", func(t *testing.T) { t.Run("new", func(t *testing.T) {
t.Parallel()
t.Run("success", func(t *testing.T) { t.Run("success", func(t *testing.T) {
t.Parallel()
s := New(t, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{ s := New(t, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
{"New", ExpectArgs{}, nil, nil}, {"New", ExpectArgs{}, nil, nil},
}, Tracks: []Expect{{Calls: []Call{ }, Tracks: []Expect{{Calls: []Call{
@@ -112,6 +128,8 @@ func TestStub(t *testing.T) {
}) })
t.Run("overrun", func(t *testing.T) { t.Run("overrun", func(t *testing.T) {
t.Parallel()
ot := &overrideT{T: t} ot := &overrideT{T: t}
ot.error.Store(checkError(t, "New: track overrun")) ot.error.Store(checkError(t, "New: track overrun"))
s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{ s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
@@ -135,7 +153,11 @@ func TestStub(t *testing.T) {
}) })
t.Run("expects", func(t *testing.T) { t.Run("expects", func(t *testing.T) {
t.Parallel()
t.Run("overrun", func(t *testing.T) { t.Run("overrun", func(t *testing.T) {
t.Parallel()
ot := &overrideT{T: t} ot := &overrideT{T: t}
ot.error.Store(checkError(t, "Expects: advancing beyond expected calls")) ot.error.Store(checkError(t, "Expects: advancing beyond expected calls"))
s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{}) s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{})
@@ -143,7 +165,11 @@ func TestStub(t *testing.T) {
}) })
t.Run("separator", func(t *testing.T) { t.Run("separator", func(t *testing.T) {
t.Parallel()
t.Run("overrun", func(t *testing.T) { t.Run("overrun", func(t *testing.T) {
t.Parallel()
ot := &overrideT{T: t} ot := &overrideT{T: t}
ot.errorf.Store(checkErrorf(t, "Expects: func = %s, separator overrun", "meow")) 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{ s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
@@ -153,6 +179,8 @@ func TestStub(t *testing.T) {
}) })
t.Run("mismatch", func(t *testing.T) { t.Run("mismatch", func(t *testing.T) {
t.Parallel()
ot := &overrideT{T: t} ot := &overrideT{T: t}
ot.errorf.Store(checkErrorf(t, "Expects: separator, want %s", "panic")) ot.errorf.Store(checkErrorf(t, "Expects: separator, want %s", "panic"))
s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{ s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
@@ -163,6 +191,8 @@ func TestStub(t *testing.T) {
}) })
t.Run("mismatch", func(t *testing.T) { t.Run("mismatch", func(t *testing.T) {
t.Parallel()
ot := &overrideT{T: t} ot := &overrideT{T: t}
ot.errorf.Store(checkErrorf(t, "Expects: func = %s, want %s", "meow", "nya")) 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{ s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
@@ -176,6 +206,8 @@ func TestStub(t *testing.T) {
func TestCheckArg(t *testing.T) { func TestCheckArg(t *testing.T) {
t.Run("oob negative", func(t *testing.T) { t.Run("oob negative", func(t *testing.T) {
t.Parallel()
defer func() { defer func() {
want := "invalid call to CheckArg" want := "invalid call to CheckArg"
if r := recover(); r != want { if r := recover(); r != want {
@@ -191,12 +223,14 @@ func TestCheckArg(t *testing.T) {
{"panic", ExpectArgs{PanicExit}, nil, nil}, {"panic", ExpectArgs{PanicExit}, nil, nil},
{"meow", ExpectArgs{-1}, nil, nil}, {"meow", ExpectArgs{-1}, nil, nil},
}}) }})
t.Run("match", func(t *testing.T) { t.Run("match", func(t *testing.T) {
s.Expects("panic") s.Expects("panic")
if !CheckArg(s, "v", PanicExit, 0) { if !CheckArg(s, "v", PanicExit, 0) {
t.Errorf("CheckArg: unexpected false") t.Errorf("CheckArg: unexpected false")
} }
}) })
t.Run("mismatch", func(t *testing.T) { t.Run("mismatch", func(t *testing.T) {
defer HandleExit(t) defer HandleExit(t)
s.Expects("meow") s.Expects("meow")
@@ -205,6 +239,7 @@ func TestCheckArg(t *testing.T) {
t.Errorf("CheckArg: unexpected true") t.Errorf("CheckArg: unexpected true")
} }
}) })
t.Run("oob", func(t *testing.T) { t.Run("oob", func(t *testing.T) {
s.pos++ s.pos++
defer func() { defer func() {
@@ -218,7 +253,11 @@ func TestCheckArg(t *testing.T) {
} }
func TestCheckArgReflect(t *testing.T) { func TestCheckArgReflect(t *testing.T) {
t.Parallel()
t.Run("oob lower", func(t *testing.T) { t.Run("oob lower", func(t *testing.T) {
t.Parallel()
defer func() { defer func() {
want := "invalid call to CheckArgReflect" want := "invalid call to CheckArgReflect"
if r := recover(); r != want { if r := recover(); r != want {

View File

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

View File

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

View File

@@ -17,6 +17,8 @@ import (
) )
func TestDecoderError(t *testing.T) { func TestDecoderError(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
err *vfs.DecoderError err *vfs.DecoderError
@@ -35,13 +37,17 @@ func TestDecoderError(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("error", func(t *testing.T) { t.Run("error", func(t *testing.T) {
t.Parallel()
if got := tc.err.Error(); got != tc.want { if got := tc.err.Error(); got != tc.want {
t.Errorf("Error: %s, want %s", got, tc.want) t.Errorf("Error: %s, want %s", got, tc.want)
} }
}) })
t.Run("is", func(t *testing.T) { t.Run("is", func(t *testing.T) {
t.Parallel()
if !errors.Is(tc.err, tc.target) { if !errors.Is(tc.err, tc.target) {
t.Errorf("Is: unexpected false") t.Errorf("Is: unexpected false")
} }
@@ -54,6 +60,8 @@ func TestDecoderError(t *testing.T) {
} }
func TestMountInfo(t *testing.T) { func TestMountInfo(t *testing.T) {
t.Parallel()
testCases := []mountInfoTest{ testCases := []mountInfoTest{
{"count", sampleMountinfoBase + ` {"count", sampleMountinfoBase + `
21 20 0:53/ /mnt/test rw,relatime - tmpfs rw 21 20 0:53/ /mnt/test rw,relatime - tmpfs rw
@@ -187,7 +195,10 @@ id 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("decode", func(t *testing.T) { t.Run("decode", func(t *testing.T) {
t.Parallel()
var got *vfs.MountInfo var got *vfs.MountInfo
d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample)) d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
err := d.Decode(&got) err := d.Decode(&got)
@@ -206,6 +217,7 @@ id 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
}) })
t.Run("iter", func(t *testing.T) { t.Run("iter", func(t *testing.T) {
t.Parallel()
d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample)) d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
tc.check(t, d, "Entries", tc.check(t, d, "Entries",
d.Entries(), d.Err) d.Entries(), d.Err)
@@ -217,6 +229,7 @@ id 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
}) })
t.Run("yield", func(t *testing.T) { t.Run("yield", func(t *testing.T) {
t.Parallel()
d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample)) d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
v := false v := false
d.Entries()(func(entry *vfs.MountInfoEntry) bool { v = !v; return v }) d.Entries()(func(entry *vfs.MountInfoEntry) bool { v = !v; return v })

View File

@@ -10,6 +10,8 @@ import (
) )
func TestUnfold(t *testing.T) { func TestUnfold(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
sample string sample string
@@ -50,6 +52,8 @@ func TestUnfold(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample)) d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
got, err := d.Unfold(tc.target) got, err := d.Unfold(tc.target)

4
dist/comp/_hakurei vendored
View File

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

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/hakurei" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hakurei"
install -vDm0755 "bin/hpkg" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hpkg" 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 if [ ! -f "${HAKUREI_INSTALL_PREFIX}/etc/hsurc" ]; then
install -vDm0400 "hsurc.default" "${HAKUREI_INSTALL_PREFIX}/etc/hsurc" install -vDm0400 "hsurc.default" "${HAKUREI_INSTALL_PREFIX}/etc/hsurc"
fi fi

View File

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

View File

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

View File

@@ -7,12 +7,14 @@ import (
"testing" "testing"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/helper" "hakurei.app/helper"
) )
func TestContainer(t *testing.T) { func TestContainer(t *testing.T) {
t.Run("start invalid container", func(t *testing.T) { t.Run("start invalid container", func(t *testing.T) {
h := helper.New(t.Context(), container.MustAbs(container.Nonexistent), "hakurei", argsWt, false, argF, nil, nil) h := helper.New(t.Context(), nil, check.MustAbs(container.Nonexistent), "hakurei", argsWt, false, argF, nil, nil)
wantErr := "container: starting an invalid container" wantErr := "container: starting an invalid container"
if err := h.Start(); err == nil || err.Error() != wantErr { if err := h.Start(); err == nil || err.Error() != wantErr {
@@ -22,7 +24,7 @@ func TestContainer(t *testing.T) {
}) })
t.Run("valid new helper nil check", func(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", t.Errorf("New(%q, %q) got nil",
argsWt, "hakurei") argsWt, "hakurei")
return return
@@ -31,12 +33,12 @@ func TestContainer(t *testing.T) {
t.Run("implementation compliance", func(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 { 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) setOutput(&z.Stdout, &z.Stderr)
z. z.
Bind(container.AbsFHSRoot, container.AbsFHSRoot, 0). Bind(fhs.AbsRoot, fhs.AbsRoot, 0).
Proc(container.AbsFHSProc). Proc(fhs.AbsProc).
Dev(container.AbsFHSDev, true) Dev(fhs.AbsDev, true)
}, nil) }, nil)
}) })
}) })

View File

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

View File

@@ -68,39 +68,35 @@ func genericStub(argsFile, statFile *os.File) {
} }
} }
// simulate status pipe behaviour
if statFile != nil { if statFile != nil {
// simulate status pipe behaviour
var epoll int
if fd, err := syscall.EpollCreate1(0); err != nil {
panic("cannot open epoll fd: " + err.Error())
} else {
defer func() {
if err = syscall.Close(fd); err != nil {
panic("cannot close epoll fd: " + err.Error())
}
}()
epoll = fd
}
if err := syscall.EpollCtl(epoll, syscall.EPOLL_CTL_ADD, int(statFile.Fd()), &syscall.EpollEvent{}); err != nil {
panic("cannot add status pipe to epoll: " + err.Error())
}
if _, err := statFile.Write([]byte{'x'}); err != nil { if _, err := statFile.Write([]byte{'x'}); err != nil {
panic("cannot write to status pipe: " + err.Error()) panic("cannot write to status pipe: " + err.Error())
} }
done := make(chan struct{}) // wait for status pipe close
go func() { events := make([]syscall.EpollEvent, 1)
// wait for status pipe close if _, err := syscall.EpollWait(epoll, events, -1); err != nil {
var epoll int panic("cannot poll status pipe: " + err.Error())
if fd, err := syscall.EpollCreate1(0); err != nil { }
panic("cannot open epoll fd: " + err.Error()) if events[0].Events != syscall.EPOLLERR {
} else { panic(strconv.Itoa(int(events[0].Events)))
defer func() {
if err = syscall.Close(fd); err != nil {
panic("cannot close epoll fd: " + err.Error())
}
}()
epoll = fd
}
if err := syscall.EpollCtl(epoll, syscall.EPOLL_CTL_ADD, int(statFile.Fd()), &syscall.EpollEvent{}); err != nil {
panic("cannot add status pipe to epoll: " + err.Error())
}
events := make([]syscall.EpollEvent, 1)
if _, err := syscall.EpollWait(epoll, events, -1); err != nil {
panic("cannot poll status pipe: " + err.Error())
}
if events[0].Events != syscall.EPOLLERR {
panic(strconv.Itoa(int(events[0].Events)))
} }
close(done)
}()
<-done
} }
} }

View File

@@ -6,12 +6,6 @@ import (
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/helper" "hakurei.app/helper"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) { container.TryArgv0(nil); helper.InternalHelperStub(); os.Exit(m.Run()) }
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
helper.InternalHelperStub()
os.Exit(m.Run())
}

View File

@@ -1,112 +1,180 @@
package hst package hst
import ( import (
"errors"
"strconv"
"time" "time"
"hakurei.app/container" "hakurei.app/container/check"
"hakurei.app/container/seccomp"
"hakurei.app/system/dbus"
) )
const Tmp = "/.hakurei" // PrivateTmp is a private writable path in a hakurei container.
const PrivateTmp = "/.hakurei"
var AbsTmp = container.MustAbs(Tmp) // AbsPrivateTmp is a [check.Absolute] representation of [PrivateTmp].
var AbsPrivateTmp = check.MustAbs(PrivateTmp)
const (
// WaitDelayDefault is used when WaitDelay has its zero value.
WaitDelayDefault = 5 * time.Second
// WaitDelayMax is used if WaitDelay exceeds its value.
WaitDelayMax = 30 * time.Second
// IdentityMin is the minimum value of [Config.Identity]. This is enforced by cmd/hsu.
IdentityMin = 0
// IdentityMax is the maximum value of [Config.Identity]. This is enforced by cmd/hsu.
IdentityMax = 9999
// ShimExitRequest is returned when the priv side process requests shim exit.
ShimExitRequest = 254
// ShimExitOrphan is returned when the shim is orphaned before priv side delivers a signal.
ShimExitOrphan = 3
)
// Config is used to seal an app implementation.
type ( type (
// Config configures an application container, implemented in internal/app.
Config struct { Config struct {
// reverse-DNS style arbitrary identifier string from config; // Reverse-DNS style configured arbitrary identifier string.
// passed to wayland security-context-v1 as application ID // Passed to wayland security-context-v1 and used as part of defaults in dbus session proxy.
// and used as part of defaults in dbus session proxy
ID string `json:"id"` ID string `json:"id"`
// absolute path to executable file // System services to make available in the container.
Path *container.Absolute `json:"path,omitempty"`
// final args passed to container init
Args []string `json:"args"`
// system services to make available in the container
Enablements *Enablements `json:"enablements,omitempty"` Enablements *Enablements `json:"enablements,omitempty"`
// session D-Bus proxy configuration; // Session D-Bus proxy configuration.
// nil makes session bus proxy assume built-in defaults // If set to nil, session bus proxy assume built-in defaults.
SessionBus *dbus.Config `json:"session_bus,omitempty"` SessionBus *BusConfig `json:"session_bus,omitempty"`
// system D-Bus proxy configuration; // System D-Bus proxy configuration.
// nil disables system bus proxy // If set to nil, system bus proxy is disabled.
SystemBus *dbus.Config `json:"system_bus,omitempty"` SystemBus *BusConfig `json:"system_bus,omitempty"`
// direct access to wayland socket; when this gets set no attempt is made to attach security-context-v1 // Direct access to wayland socket, no attempt is made to attach security-context-v1
// and the bare socket is mounted to the sandbox // and the bare socket is made available to the container.
DirectWayland bool `json:"direct_wayland,omitempty"` DirectWayland bool `json:"direct_wayland,omitempty"`
// passwd username in container, defaults to passwd name of target uid or chronos // Extra acl update ops to perform before setuid.
Username string `json:"username,omitempty"`
// absolute path to shell
Shell *container.Absolute `json:"shell"`
// directory to enter and use as home in the container mount namespace
Home *container.Absolute `json:"home"`
// extra acl ops to perform before setuid
ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"` ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
// numerical application id, used for init user namespace credentials // Numerical application id, passed to hsu, used to derive init user namespace credentials.
Identity int `json:"identity"` Identity int `json:"identity"`
// list of supplementary groups inherited by container processes // Init user namespace supplementary groups inherited by all container processes.
Groups []string `json:"groups"` Groups []string `json:"groups"`
// abstract container configuration baseline // High level configuration applied to the underlying [container].
Container *ContainerConfig `json:"container"` Container *ContainerConfig `json:"container"`
} }
// ContainerConfig describes the container configuration baseline to which the app implementation adds upon. // ContainerConfig describes the container configuration to be applied to an underlying [container].
ContainerConfig struct { ContainerConfig struct {
// container hostname // Container UTS namespace hostname.
Hostname string `json:"hostname,omitempty"` Hostname string `json:"hostname,omitempty"`
// duration to wait for after interrupting a container's initial process in nanoseconds; // Duration in nanoseconds to wait for after interrupting the initial process.
// a negative value causes the container to be terminated immediately on cancellation // Defaults to [WaitDelayDefault] if zero, or [WaitDelayMax] if greater than [WaitDelayMax].
// Values lesser than zero is equivalent to zero, bypassing [WaitDelayDefault].
WaitDelay time.Duration `json:"wait_delay,omitempty"` WaitDelay time.Duration `json:"wait_delay,omitempty"`
// extra seccomp flags // Emit Flatpak-compatible seccomp filter programs.
SeccompFlags seccomp.ExportFlag `json:"seccomp_flags"`
// extra seccomp presets
SeccompPresets seccomp.FilterPreset `json:"seccomp_presets"`
// disable project-specific filter extensions
SeccompCompat bool `json:"seccomp_compat,omitempty"` SeccompCompat bool `json:"seccomp_compat,omitempty"`
// allow ptrace and friends // Allow ptrace and friends.
Devel bool `json:"devel,omitempty"` Devel bool `json:"devel,omitempty"`
// allow userns creation in container // Allow userns creation and container setup syscalls.
Userns bool `json:"userns,omitempty"` Userns bool `json:"userns,omitempty"`
// share host net namespace // Share host net namespace.
HostNet bool `json:"host_net,omitempty"` HostNet bool `json:"host_net,omitempty"`
// share abstract unix socket scope // Share abstract unix socket scope.
HostAbstract bool `json:"host_abstract,omitempty"` HostAbstract bool `json:"host_abstract,omitempty"`
// allow dangerous terminal I/O // Allow dangerous terminal I/O (faking input).
Tty bool `json:"tty,omitempty"` Tty bool `json:"tty,omitempty"`
// allow multiarch // Allow multiarch.
Multiarch bool `json:"multiarch,omitempty"` Multiarch bool `json:"multiarch,omitempty"`
// initial process environment variables // Initial process environment variables.
Env map[string]string `json:"env"` Env map[string]string `json:"env"`
// map target user uid to privileged user uid in the user namespace;
// some programs fail to connect to dbus session running as a different uid, /* Map target user uid to privileged user uid in the container user namespace.
// this option works around it by mapping priv-side caller uid in container
Some programs fail to connect to dbus session running as a different uid,
this option works around it by mapping priv-side caller uid in container. */
MapRealUID bool `json:"map_real_uid"` MapRealUID bool `json:"map_real_uid"`
// pass through all devices // Mount /dev/ from the init mount namespace as-is in the container mount namespace.
Device bool `json:"device,omitempty"` Device bool `json:"device,omitempty"`
// container mount points;
// if the first element targets /, it is inserted early and excluded from path hiding /* Container mount points.
If the first element targets /, it is inserted early and excluded from path hiding. */
Filesystem []FilesystemConfigJSON `json:"filesystem"` Filesystem []FilesystemConfigJSON `json:"filesystem"`
// String used as the username of the emulated user, validated against the default NAME_REGEX from adduser.
// Defaults to passwd name of target uid or chronos.
Username string `json:"username,omitempty"`
// Pathname of shell in the container filesystem to use for the emulated user.
Shell *check.Absolute `json:"shell"`
// Directory in the container filesystem to enter and use as the home directory of the emulated user.
Home *check.Absolute `json:"home"`
// Pathname to executable file in the container filesystem.
Path *check.Absolute `json:"path,omitempty"`
// Final args passed to the initial program.
Args []string `json:"args"`
} }
) )
var (
// ErrConfigNull is returned by [Config.Validate] for an invalid configuration that contains a null value for any
// field that must not be null.
ErrConfigNull = errors.New("unexpected null in config")
// ErrIdentityBounds is returned by [Config.Validate] for an out of bounds [Config.Identity] value.
ErrIdentityBounds = errors.New("identity out of bounds")
)
// Validate checks [Config] and returns [AppError] if an invalid value is encountered.
func (config *Config) Validate() error {
if config == nil {
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
Msg: "invalid configuration"}
}
// this is checked again in hsu
if config.Identity < IdentityMin || config.Identity > IdentityMax {
return &AppError{Step: "validate configuration", Err: ErrIdentityBounds,
Msg: "identity " + strconv.Itoa(config.Identity) + " out of range"}
}
if err := config.SessionBus.CheckInterfaces("session"); err != nil {
return err
}
if err := config.SystemBus.CheckInterfaces("system"); err != nil {
return err
}
if config.Container == nil {
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
Msg: "configuration missing container state"}
}
if config.Container.Home == nil {
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
Msg: "container configuration missing path to home directory"}
}
if config.Container.Shell == nil {
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
Msg: "container configuration missing path to shell"}
}
if config.Container.Path == nil {
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
Msg: "container configuration missing path to initial program"}
}
return nil
}
// ExtraPermConfig describes an acl update op. // ExtraPermConfig describes an acl update op.
type ExtraPermConfig struct { type ExtraPermConfig struct {
Ensure bool `json:"ensure,omitempty"` Ensure bool `json:"ensure,omitempty"`
Path *container.Absolute `json:"path"` Path *check.Absolute `json:"path"`
Read bool `json:"r,omitempty"` Read bool `json:"r,omitempty"`
Write bool `json:"w,omitempty"` Write bool `json:"w,omitempty"`
Execute bool `json:"x,omitempty"` Execute bool `json:"x,omitempty"`
} }
func (e *ExtraPermConfig) String() string { func (e *ExtraPermConfig) String() string {

View File

@@ -1,13 +1,63 @@
package hst_test package hst_test
import ( import (
"reflect"
"testing" "testing"
"hakurei.app/container" "hakurei.app/container/fhs"
"hakurei.app/hst" "hakurei.app/hst"
) )
func TestConfigValidate(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
config *hst.Config
wantErr error
}{
{"nil", nil, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
Msg: "invalid configuration"}},
{"identity lower", &hst.Config{Identity: -1}, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
Msg: "identity -1 out of range"}},
{"identity upper", &hst.Config{Identity: 10000}, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
Msg: "identity 10000 out of range"}},
{"dbus session", &hst.Config{SessionBus: &hst.BusConfig{See: []string{""}}},
&hst.BadInterfaceError{Interface: "", Segment: "session"}},
{"dbus system", &hst.Config{SystemBus: &hst.BusConfig{See: []string{""}}},
&hst.BadInterfaceError{Interface: "", Segment: "system"}},
{"container", &hst.Config{}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
Msg: "configuration missing container state"}},
{"home", &hst.Config{Container: &hst.ContainerConfig{}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
Msg: "container configuration missing path to home directory"}},
{"shell", &hst.Config{Container: &hst.ContainerConfig{
Home: fhs.AbsTmp,
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
Msg: "container configuration missing path to shell"}},
{"path", &hst.Config{Container: &hst.ContainerConfig{
Home: fhs.AbsTmp,
Shell: fhs.AbsTmp,
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
Msg: "container configuration missing path to initial program"}},
{"valid", &hst.Config{Container: &hst.ContainerConfig{
Home: fhs.AbsTmp,
Shell: fhs.AbsTmp,
Path: fhs.AbsTmp,
}}, nil},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if err := tc.config.Validate(); !reflect.DeepEqual(err, tc.wantErr) {
t.Errorf("Validate: error = %#v, want %#v", err, tc.wantErr)
}
})
}
}
func TestExtraPermConfig(t *testing.T) { func TestExtraPermConfig(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
config *hst.ExtraPermConfig config *hst.ExtraPermConfig
@@ -15,18 +65,19 @@ func TestExtraPermConfig(t *testing.T) {
}{ }{
{"nil", nil, "<invalid>"}, {"nil", nil, "<invalid>"},
{"nil path", &hst.ExtraPermConfig{Path: nil}, "<invalid>"}, {"nil path", &hst.ExtraPermConfig{Path: nil}, "<invalid>"},
{"r", &hst.ExtraPermConfig{Path: container.AbsFHSRoot, Read: true}, "r--:/"}, {"r", &hst.ExtraPermConfig{Path: fhs.AbsRoot, Read: true}, "r--:/"},
{"r+", &hst.ExtraPermConfig{Ensure: true, Path: container.AbsFHSRoot, Read: true}, "r--+:/"}, {"r+", &hst.ExtraPermConfig{Ensure: true, Path: fhs.AbsRoot, Read: true}, "r--+:/"},
{"w", &hst.ExtraPermConfig{Path: hst.AbsTmp, Write: true}, "-w-:/.hakurei"}, {"w", &hst.ExtraPermConfig{Path: hst.AbsPrivateTmp, Write: true}, "-w-:/.hakurei"},
{"w+", &hst.ExtraPermConfig{Ensure: true, Path: hst.AbsTmp, Write: true}, "-w-+:/.hakurei"}, {"w+", &hst.ExtraPermConfig{Ensure: true, Path: hst.AbsPrivateTmp, Write: true}, "-w-+:/.hakurei"},
{"x", &hst.ExtraPermConfig{Path: container.AbsFHSRunUser, Execute: true}, "--x:/run/user/"}, {"x", &hst.ExtraPermConfig{Path: fhs.AbsRunUser, Execute: true}, "--x:/run/user/"},
{"x+", &hst.ExtraPermConfig{Ensure: true, Path: container.AbsFHSRunUser, Execute: true}, "--x+:/run/user/"}, {"x+", &hst.ExtraPermConfig{Ensure: true, Path: fhs.AbsRunUser, Execute: true}, "--x+:/run/user/"},
{"rwx", &hst.ExtraPermConfig{Path: container.AbsFHSTmp, Read: true, Write: true, Execute: true}, "rwx:/tmp/"}, {"rwx", &hst.ExtraPermConfig{Path: fhs.AbsTmp, Read: true, Write: true, Execute: true}, "rwx:/tmp/"},
{"rwx+", &hst.ExtraPermConfig{Ensure: true, Path: container.AbsFHSTmp, Read: true, Write: true, Execute: true}, "rwx+:/tmp/"}, {"rwx+", &hst.ExtraPermConfig{Ensure: true, Path: fhs.AbsTmp, Read: true, Write: true, Execute: true}, "rwx+:/tmp/"},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.config.String(); got != tc.want { if got := tc.config.String(); got != tc.want {
t.Errorf("String: %q, want %q", got, tc.want) t.Errorf("String: %q, want %q", got, tc.want)
} }

109
hst/dbus.go Normal file
View File

@@ -0,0 +1,109 @@
package hst
import (
"strconv"
"strings"
)
// BadInterfaceError is returned when Interface fails an undocumented check in xdg-dbus-proxy,
// which would have cause a silent failure.
type BadInterfaceError struct {
// Interface is the offending interface string.
Interface string
// Segment is passed through from the [BusConfig.CheckInterfaces] argument.
Segment string
}
func (e *BadInterfaceError) Message() string { return e.Error() }
func (e *BadInterfaceError) Error() string {
if e == nil {
return "<nil>"
}
return "bad interface string " + strconv.Quote(e.Interface) + " in " + e.Segment + " bus configuration"
}
// BusConfig configures the xdg-dbus-proxy process.
type BusConfig struct {
// See set 'see' policy for NAME (--see=NAME)
See []string `json:"see"`
// Talk set 'talk' policy for NAME (--talk=NAME)
Talk []string `json:"talk"`
// Own set 'own' policy for NAME (--own=NAME)
Own []string `json:"own"`
// Call set RULE for calls on NAME (--call=NAME=RULE)
Call map[string]string `json:"call"`
// Broadcast set RULE for broadcasts from NAME (--broadcast=NAME=RULE)
Broadcast map[string]string `json:"broadcast"`
// Log turn on logging (--log)
Log bool `json:"log,omitempty"`
// Filter enable filtering (--filter)
Filter bool `json:"filter"`
}
// Interfaces iterates over all interface strings specified in [BusConfig].
func (c *BusConfig) Interfaces(yield func(string) bool) {
if c == nil {
return
}
for _, iface := range c.See {
if !yield(iface) {
return
}
}
for _, iface := range c.Talk {
if !yield(iface) {
return
}
}
for _, iface := range c.Own {
if !yield(iface) {
return
}
}
for iface := range c.Call {
if !yield(iface) {
return
}
}
for iface := range c.Broadcast {
if !yield(iface) {
return
}
}
}
// CheckInterfaces checks for invalid interface strings based on an undocumented check in xdg-dbus-error,
// returning [BadInterfaceError] if one is encountered.
func (c *BusConfig) CheckInterfaces(segment string) error {
if c == nil {
return nil
}
for iface := range c.Interfaces {
/*
xdg-dbus-proxy fails without output when this condition is not met:
char *dot = strrchr (filter->interface, '.');
if (dot != NULL)
{
*dot = 0;
if (strcmp (dot + 1, "*") != 0)
filter->member = g_strdup (dot + 1);
}
trim ".*" since they are removed before searching for '.':
if (g_str_has_suffix (name, ".*"))
{
name[strlen (name) - 2] = 0;
wildcard = TRUE;
}
*/
if strings.IndexByte(strings.TrimSuffix(iface, ".*"), '.') == -1 {
return &BadInterfaceError{iface, segment}
}
}
return nil
}

118
hst/dbus_test.go Normal file
View File

@@ -0,0 +1,118 @@
package hst_test
import (
"reflect"
"slices"
"testing"
"hakurei.app/hst"
"hakurei.app/message"
)
func TestBadInterfaceError(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
err error
want string
}{
{"nil", (*hst.BadInterfaceError)(nil), "<nil>"},
{"session", &hst.BadInterfaceError{Interface: "\x00", Segment: "session"},
`bad interface string "\x00" in session bus configuration`},
{"system", &hst.BadInterfaceError{Interface: "\x01", Segment: "system"},
`bad interface string "\x01" in system bus configuration`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if gotError := tc.err.Error(); gotError != tc.want {
t.Errorf("Error: %s, want %s", gotError, tc.want)
}
if gotMessage, ok := message.GetMessage(tc.err); !ok {
t.Error("GetMessage: ok = false")
} else if gotMessage != tc.want {
t.Errorf("GetMessage: %s, want %s", gotMessage, tc.want)
}
})
}
}
func TestBusConfigInterfaces(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
c *hst.BusConfig
cutoff int
want []string
}{
{"nil", nil, 0, nil},
{"all", &hst.BusConfig{
See: []string{"see"}, Talk: []string{"talk"}, Own: []string{"own"},
Call: map[string]string{"call": "unreachable"},
Broadcast: map[string]string{"broadcast": "unreachable"},
}, 0, []string{"see", "talk", "own", "call", "broadcast"}},
{"all cutoff", &hst.BusConfig{
See: []string{"see"}, Talk: []string{"talk"}, Own: []string{"own"},
Call: map[string]string{"call": "unreachable"},
Broadcast: map[string]string{"broadcast": "unreachable"},
}, 3, []string{"see", "talk", "own"}},
{"cutoff see", &hst.BusConfig{See: []string{"see"}}, 1, []string{"see"}},
{"cutoff talk", &hst.BusConfig{Talk: []string{"talk"}}, 1, []string{"talk"}},
{"cutoff own", &hst.BusConfig{Own: []string{"own"}}, 1, []string{"own"}},
{"cutoff call", &hst.BusConfig{Call: map[string]string{"call": "unreachable"}}, 1, []string{"call"}},
{"cutoff broadcast", &hst.BusConfig{Broadcast: map[string]string{"broadcast": "unreachable"}}, 1, []string{"broadcast"}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var got []string
if tc.cutoff > 0 {
var i int
got = make([]string, 0, tc.cutoff)
for v := range tc.c.Interfaces {
i++
got = append(got, v)
if i == tc.cutoff {
break
}
}
} else {
got = slices.Collect(tc.c.Interfaces)
}
if !slices.Equal(got, tc.want) {
t.Errorf("Interfaces: %q, want %q", got, tc.want)
}
})
}
}
func TestBusConfigCheckInterfaces(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
c *hst.BusConfig
err error
}{
{"nil", nil, nil},
{"zero", &hst.BusConfig{See: []string{""}},
&hst.BadInterfaceError{Interface: "", Segment: "zero"}},
{"suffix", &hst.BusConfig{See: []string{".*"}},
&hst.BadInterfaceError{Interface: ".*", Segment: "suffix"}},
{"valid suffix", &hst.BusConfig{See: []string{"..*"}}, nil},
{"valid", &hst.BusConfig{See: []string{"."}}, nil},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if err := tc.c.CheckInterfaces(tc.name); !reflect.DeepEqual(err, tc.err) {
t.Errorf("CheckInterfaces: error = %#v, want %#v", err, tc.err)
}
})
}
}

View File

@@ -2,31 +2,78 @@ package hst
import ( import (
"encoding/json" "encoding/json"
"fmt"
"strings"
"syscall" "syscall"
"hakurei.app/system"
) )
// NewEnablements returns the address of [system.Enablement] as [Enablements]. // Enablement represents an optional host service to export to the target user.
func NewEnablements(e system.Enablement) *Enablements { return (*Enablements)(&e) } type Enablement byte
// enablementsJSON is the [json] representation of the [system.Enablement] bit field. const (
type enablementsJSON struct { // EWayland exposes a wayland pathname socket via security-context-v1.
EWayland Enablement = 1 << iota
// EX11 adds the target user via X11 ChangeHosts and exposes the X11 pathname socket.
EX11
// EDBus enables the per-container xdg-dbus-proxy daemon.
EDBus
// EPulse copies the PulseAudio cookie to [hst.PrivateTmp] and exposes the PulseAudio socket.
EPulse
// EM is a noop.
EM
)
// String returns a string representation of the flags set on [Enablement].
func (e Enablement) String() string {
switch e {
case 0:
return "(no enablements)"
case EWayland:
return "wayland"
case EX11:
return "x11"
case EDBus:
return "dbus"
case EPulse:
return "pulseaudio"
default:
buf := new(strings.Builder)
buf.Grow(32)
for i := Enablement(1); i < EM; i <<= 1 {
if e&i != 0 {
buf.WriteString(", " + i.String())
}
}
if buf.Len() == 0 {
return fmt.Sprintf("e%x", byte(e))
}
return strings.TrimPrefix(buf.String(), ", ")
}
}
// NewEnablements returns the address of [Enablement] as [Enablements].
func NewEnablements(e Enablement) *Enablements { return (*Enablements)(&e) }
// Enablements is the [json] adapter for [Enablement].
type Enablements Enablement
// enablementsJSON is the [json] representation of [Enablements].
type enablementsJSON = struct {
Wayland bool `json:"wayland,omitempty"` Wayland bool `json:"wayland,omitempty"`
X11 bool `json:"x11,omitempty"` X11 bool `json:"x11,omitempty"`
DBus bool `json:"dbus,omitempty"` DBus bool `json:"dbus,omitempty"`
Pulse bool `json:"pulse,omitempty"` Pulse bool `json:"pulse,omitempty"`
} }
// Enablements is the [json] adapter for [system.Enablement]. // Unwrap returns the underlying [Enablement].
type Enablements system.Enablement func (e *Enablements) Unwrap() Enablement {
// Unwrap returns the underlying [system.Enablement].
func (e *Enablements) Unwrap() system.Enablement {
if e == nil { if e == nil {
return 0 return 0
} }
return system.Enablement(*e) return Enablement(*e)
} }
func (e *Enablements) MarshalJSON() ([]byte, error) { func (e *Enablements) MarshalJSON() ([]byte, error) {
@@ -34,10 +81,10 @@ func (e *Enablements) MarshalJSON() ([]byte, error) {
return nil, syscall.EINVAL return nil, syscall.EINVAL
} }
return json.Marshal(&enablementsJSON{ return json.Marshal(&enablementsJSON{
Wayland: system.Enablement(*e)&system.EWayland != 0, Wayland: Enablement(*e)&EWayland != 0,
X11: system.Enablement(*e)&system.EX11 != 0, X11: Enablement(*e)&EX11 != 0,
DBus: system.Enablement(*e)&system.EDBus != 0, DBus: Enablement(*e)&EDBus != 0,
Pulse: system.Enablement(*e)&system.EPulse != 0, Pulse: Enablement(*e)&EPulse != 0,
}) })
} }
@@ -51,18 +98,18 @@ func (e *Enablements) UnmarshalJSON(data []byte) error {
return err return err
} }
var ve system.Enablement var ve Enablement
if v.Wayland { if v.Wayland {
ve |= system.EWayland ve |= EWayland
} }
if v.X11 { if v.X11 {
ve |= system.EX11 ve |= EX11
} }
if v.DBus { if v.DBus {
ve |= system.EDBus ve |= EDBus
} }
if v.Pulse { if v.Pulse {
ve |= system.EPulse ve |= EPulse
} }
*e = Enablements(ve) *e = Enablements(ve)
return nil return nil

View File

@@ -7,10 +7,50 @@ import (
"testing" "testing"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/system"
) )
func TestEnablementString(t *testing.T) {
t.Parallel()
testCases := []struct {
flags hst.Enablement
want string
}{
{0, "(no enablements)"},
{hst.EWayland, "wayland"},
{hst.EX11, "x11"},
{hst.EDBus, "dbus"},
{hst.EPulse, "pulseaudio"},
{hst.EWayland | hst.EX11, "wayland, x11"},
{hst.EWayland | hst.EDBus, "wayland, dbus"},
{hst.EWayland | hst.EPulse, "wayland, pulseaudio"},
{hst.EX11 | hst.EDBus, "x11, dbus"},
{hst.EX11 | hst.EPulse, "x11, pulseaudio"},
{hst.EDBus | hst.EPulse, "dbus, pulseaudio"},
{hst.EWayland | hst.EX11 | hst.EDBus, "wayland, x11, dbus"},
{hst.EWayland | hst.EX11 | hst.EPulse, "wayland, x11, pulseaudio"},
{hst.EWayland | hst.EDBus | hst.EPulse, "wayland, dbus, pulseaudio"},
{hst.EX11 | hst.EDBus | hst.EPulse, "x11, dbus, pulseaudio"},
{hst.EWayland | hst.EX11 | hst.EDBus | hst.EPulse, "wayland, x11, dbus, pulseaudio"},
{1 << 5, "e20"},
{1 << 6, "e40"},
{1 << 7, "e80"},
}
for _, tc := range testCases {
t.Run(tc.want, func(t *testing.T) {
t.Parallel()
if got := tc.flags.String(); got != tc.want {
t.Errorf("String: %q, want %q", got, tc.want)
}
})
}
}
func TestEnablements(t *testing.T) { func TestEnablements(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
e *hst.Enablements e *hst.Enablements
@@ -19,16 +59,19 @@ func TestEnablements(t *testing.T) {
}{ }{
{"nil", nil, "null", `{"value":null,"magic":3236757504}`}, {"nil", nil, "null", `{"value":null,"magic":3236757504}`},
{"zero", hst.NewEnablements(0), `{}`, `{"value":{},"magic":3236757504}`}, {"zero", hst.NewEnablements(0), `{}`, `{"value":{},"magic":3236757504}`},
{"wayland", hst.NewEnablements(system.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`}, {"wayland", hst.NewEnablements(hst.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`},
{"x11", hst.NewEnablements(system.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`}, {"x11", hst.NewEnablements(hst.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`},
{"dbus", hst.NewEnablements(system.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`}, {"dbus", hst.NewEnablements(hst.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`},
{"pulse", hst.NewEnablements(system.EPulse), `{"pulse":true}`, `{"value":{"pulse":true},"magic":3236757504}`}, {"pulse", hst.NewEnablements(hst.EPulse), `{"pulse":true}`, `{"value":{"pulse":true},"magic":3236757504}`},
{"all", hst.NewEnablements(system.EWayland | system.EX11 | system.EDBus | system.EPulse), `{"wayland":true,"x11":true,"dbus":true,"pulse":true}`, `{"value":{"wayland":true,"x11":true,"dbus":true,"pulse":true},"magic":3236757504}`}, {"all", hst.NewEnablements(hst.EWayland | hst.EX11 | hst.EDBus | hst.EPulse), `{"wayland":true,"x11":true,"dbus":true,"pulse":true}`, `{"value":{"wayland":true,"x11":true,"dbus":true,"pulse":true},"magic":3236757504}`},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("marshal", func(t *testing.T) { t.Run("marshal", func(t *testing.T) {
t.Parallel()
if got, err := json.Marshal(tc.e); err != nil { if got, err := json.Marshal(tc.e); err != nil {
t.Fatalf("Marshal: error = %v", err) t.Fatalf("Marshal: error = %v", err)
} else if string(got) != tc.data { } else if string(got) != tc.data {
@@ -46,6 +89,8 @@ func TestEnablements(t *testing.T) {
}) })
t.Run("unmarshal", func(t *testing.T) { t.Run("unmarshal", func(t *testing.T) {
t.Parallel()
{ {
got := new(hst.Enablements) got := new(hst.Enablements)
if err := json.Unmarshal([]byte(tc.data), &got); err != nil { if err := json.Unmarshal([]byte(tc.data), &got); err != nil {
@@ -81,6 +126,8 @@ func TestEnablements(t *testing.T) {
} }
t.Run("unwrap", func(t *testing.T) { t.Run("unwrap", func(t *testing.T) {
t.Parallel()
t.Run("nil", func(t *testing.T) { t.Run("nil", func(t *testing.T) {
if got := (*hst.Enablements)(nil).Unwrap(); got != 0 { if got := (*hst.Enablements)(nil).Unwrap(); got != 0 {
t.Errorf("Unwrap: %v", got) t.Errorf("Unwrap: %v", got)
@@ -88,13 +135,15 @@ func TestEnablements(t *testing.T) {
}) })
t.Run("val", func(t *testing.T) { t.Run("val", func(t *testing.T) {
if got := hst.NewEnablements(system.EWayland | system.EPulse).Unwrap(); got != system.EWayland|system.EPulse { if got := hst.NewEnablements(hst.EWayland | hst.EPulse).Unwrap(); got != hst.EWayland|hst.EPulse {
t.Errorf("Unwrap: %v", got) t.Errorf("Unwrap: %v", got)
} }
}) })
}) })
t.Run("passthrough", func(t *testing.T) { t.Run("passthrough", func(t *testing.T) {
t.Parallel()
if _, err := (*hst.Enablements)(nil).MarshalJSON(); !errors.Is(err, syscall.EINVAL) { if _, err := (*hst.Enablements)(nil).MarshalJSON(); !errors.Is(err, syscall.EINVAL) {
t.Errorf("MarshalJSON: error = %v", err) t.Errorf("MarshalJSON: error = %v", err)
} }

View File

@@ -4,9 +4,10 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"os"
"reflect" "reflect"
"hakurei.app/container" "hakurei.app/container/check"
) )
// FilesystemConfig is an abstract representation of a mount point. // FilesystemConfig is an abstract representation of a mount point.
@@ -14,26 +15,48 @@ type FilesystemConfig interface {
// Valid returns whether the configuration is valid. // Valid returns whether the configuration is valid.
Valid() bool Valid() bool
// Path returns the target path in the container. // Path returns the target path in the container.
Path() *container.Absolute Path() *check.Absolute
// Host returns a slice of all host paths used by this operation. // Host returns a slice of all host paths used by this operation.
Host() []*container.Absolute Host() []*check.Absolute
// Apply appends the [container.Op] implementing this operation. // Apply appends the [container.Op] implementing this operation.
Apply(z *ApplyState) Apply(z *ApplyState)
fmt.Stringer fmt.Stringer
} }
// ApplyState holds the address of [container.Ops] and any relevant application state. // The Ops interface enables [FilesystemConfig] to queue container ops without depending on the container package.
type ApplyState struct { type Ops interface {
// AutoEtcPrefix is the prefix for [container.AutoEtcOp]. // Tmpfs appends an op that mounts tmpfs on a container path.
AutoEtcPrefix string Tmpfs(target *check.Absolute, size int, perm os.FileMode) Ops
// Readonly appends an op that mounts read-only tmpfs on a container path.
Readonly(target *check.Absolute, perm os.FileMode) Ops
*container.Ops // Bind appends an op that bind mounts a host path on a container path.
Bind(source, target *check.Absolute, flags int) Ops
// Overlay appends an op that mounts the overlay pseudo filesystem.
Overlay(target, state, work *check.Absolute, layers ...*check.Absolute) Ops
// OverlayReadonly appends an op that mounts the overlay pseudo filesystem readonly.
OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) Ops
// Link appends an op that creates a symlink in the container filesystem.
Link(target *check.Absolute, linkName string, dereference bool) Ops
// Root appends an op that expands a directory into a toplevel bind mount mirror on container root.
Root(host *check.Absolute, flags int) Ops
// Etc appends an op that expands host /etc into a toplevel symlink mirror with /etc semantics.
Etc(host *check.Absolute, prefix string) Ops
} }
var ( // ApplyState holds the address of [Ops] and any relevant application state.
ErrFSNull = errors.New("unexpected null in mount point") type ApplyState struct {
) // AutoEtcPrefix is the prefix for [FSBind] in autoetc [FSBind.Special] condition.
AutoEtcPrefix string
Ops
}
// ErrFSNull is returned by [json] on encountering a null [FilesystemConfig] value.
var ErrFSNull = errors.New("unexpected null in mount point")
// FSTypeError is returned when [ContainerConfig.Filesystem] contains an entry with invalid type. // FSTypeError is returned when [ContainerConfig.Filesystem] contains an entry with invalid type.
type FSTypeError string type FSTypeError string
@@ -66,7 +89,7 @@ func (f *FilesystemConfigJSON) Valid() bool {
return f != nil && f.FilesystemConfig != nil && f.FilesystemConfig.Valid() return f != nil && f.FilesystemConfig != nil && f.FilesystemConfig.Valid()
} }
// fsType holds the string representation of a [FilesystemConfig]'s concrete type. // fsType holds the string representation of the concrete type of [FilesystemConfig].
type fsType struct { type fsType struct {
Type string `json:"type"` Type string `json:"type"`
} }

View File

@@ -3,16 +3,20 @@ package hst_test
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"os"
"reflect" "reflect"
"strings" "strings"
"syscall" "syscall"
"testing" "testing"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/hst" "hakurei.app/hst"
) )
func TestFilesystemConfigJSON(t *testing.T) { func TestFilesystemConfigJSON(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
want hst.FilesystemConfigJSON want hst.FilesystemConfigJSON
@@ -84,7 +88,10 @@ func TestFilesystemConfigJSON(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("marshal", func(t *testing.T) { t.Run("marshal", func(t *testing.T) {
t.Parallel()
wantErr := tc.wantErr wantErr := tc.wantErr
if errors.As(wantErr, new(hst.FSTypeError)) { if errors.As(wantErr, new(hst.FSTypeError)) {
// for unsupported implementation tc // for unsupported implementation tc
@@ -120,6 +127,7 @@ func TestFilesystemConfigJSON(t *testing.T) {
}) })
t.Run("unmarshal", func(t *testing.T) { t.Run("unmarshal", func(t *testing.T) {
t.Parallel()
if tc.data == "\x00" && tc.sData == "\x00" { if tc.data == "\x00" && tc.sData == "\x00" {
if errors.As(tc.wantErr, new(hst.FSImplError)) { if errors.As(tc.wantErr, new(hst.FSImplError)) {
// this error is only returned on marshal // this error is only returned on marshal
@@ -161,6 +169,8 @@ func TestFilesystemConfigJSON(t *testing.T) {
} }
t.Run("valid", func(t *testing.T) { t.Run("valid", func(t *testing.T) {
t.Parallel()
if got := (*hst.FilesystemConfigJSON).Valid(nil); got { if got := (*hst.FilesystemConfigJSON).Valid(nil); got {
t.Errorf("Valid: %v, want false", got) t.Errorf("Valid: %v, want false", got)
} }
@@ -175,6 +185,7 @@ func TestFilesystemConfigJSON(t *testing.T) {
}) })
t.Run("passthrough", func(t *testing.T) { t.Run("passthrough", func(t *testing.T) {
t.Parallel()
if err := new(hst.FilesystemConfigJSON).UnmarshalJSON(make([]byte, 0)); err == nil { if err := new(hst.FilesystemConfigJSON).UnmarshalJSON(make([]byte, 0)); err == nil {
t.Errorf("UnmarshalJSON: error = %v", err) t.Errorf("UnmarshalJSON: error = %v", err)
} }
@@ -182,7 +193,10 @@ func TestFilesystemConfigJSON(t *testing.T) {
} }
func TestFSErrors(t *testing.T) { func TestFSErrors(t *testing.T) {
t.Parallel()
t.Run("type", func(t *testing.T) { t.Run("type", func(t *testing.T) {
t.Parallel()
want := `invalid filesystem type "cat"` want := `invalid filesystem type "cat"`
if got := hst.FSTypeError("cat").Error(); got != want { if got := hst.FSTypeError("cat").Error(); got != want {
t.Errorf("Error: %q, want %q", got, want) t.Errorf("Error: %q, want %q", got, want)
@@ -190,6 +204,8 @@ func TestFSErrors(t *testing.T) {
}) })
t.Run("impl", func(t *testing.T) { t.Run("impl", func(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
val hst.FilesystemConfig val hst.FilesystemConfig
@@ -203,6 +219,7 @@ func TestFSErrors(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
err := hst.FSImplError{Value: tc.val} err := hst.FSImplError{Value: tc.val}
if got := err.Error(); got != tc.want { if got := err.Error(); got != tc.want {
t.Errorf("Error: %q, want %q", got, tc.want) t.Errorf("Error: %q, want %q", got, tc.want)
@@ -216,11 +233,11 @@ type stubFS struct {
typeName string typeName string
} }
func (s stubFS) Valid() bool { return false } func (s stubFS) Valid() bool { return false }
func (s stubFS) Path() *container.Absolute { panic("unreachable") } func (s stubFS) Path() *check.Absolute { panic("unreachable") }
func (s stubFS) Host() []*container.Absolute { panic("unreachable") } func (s stubFS) Host() []*check.Absolute { panic("unreachable") }
func (s stubFS) Apply(*hst.ApplyState) { panic("unreachable") } func (s stubFS) Apply(*hst.ApplyState) { panic("unreachable") }
func (s stubFS) String() string { return "<invalid " + s.typeName + ">" } func (s stubFS) String() string { return "<invalid " + s.typeName + ">" }
type sCheck struct { type sCheck struct {
FS hst.FilesystemConfigJSON `json:"fs"` FS hst.FilesystemConfigJSON `json:"fs"`
@@ -232,23 +249,27 @@ type fsTestCase struct {
fs hst.FilesystemConfig fs hst.FilesystemConfig
valid bool valid bool
ops container.Ops ops container.Ops
path *container.Absolute path *check.Absolute
host []*container.Absolute host []*check.Absolute
str string str string
} }
func checkFs(t *testing.T, testCases []fsTestCase) { func checkFs(t *testing.T, testCases []fsTestCase) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("valid", func(t *testing.T) { t.Run("valid", func(t *testing.T) {
t.Parallel()
if got := tc.fs.Valid(); got != tc.valid { if got := tc.fs.Valid(); got != tc.valid {
t.Errorf("Valid: %v, want %v", got, tc.valid) t.Errorf("Valid: %v, want %v", got, tc.valid)
} }
}) })
t.Run("ops", func(t *testing.T) { t.Run("ops", func(t *testing.T) {
t.Parallel()
ops := new(container.Ops) ops := new(container.Ops)
tc.fs.Apply(&hst.ApplyState{AutoEtcPrefix: ":3", Ops: ops}) tc.fs.Apply(&hst.ApplyState{AutoEtcPrefix: ":3", Ops: opsAdapter{ops}})
if !reflect.DeepEqual(ops, &tc.ops) { if !reflect.DeepEqual(ops, &tc.ops) {
gotString := new(strings.Builder) gotString := new(strings.Builder)
for _, op := range *ops { for _, op := range *ops {
@@ -263,18 +284,21 @@ func checkFs(t *testing.T, testCases []fsTestCase) {
}) })
t.Run("path", func(t *testing.T) { t.Run("path", func(t *testing.T) {
t.Parallel()
if got := tc.fs.Path(); !reflect.DeepEqual(got, tc.path) { if got := tc.fs.Path(); !reflect.DeepEqual(got, tc.path) {
t.Errorf("Target: %q, want %q", got, tc.path) t.Errorf("Target: %q, want %q", got, tc.path)
} }
}) })
t.Run("host", func(t *testing.T) { t.Run("host", func(t *testing.T) {
t.Parallel()
if got := tc.fs.Host(); !reflect.DeepEqual(got, tc.host) { if got := tc.fs.Host(); !reflect.DeepEqual(got, tc.host) {
t.Errorf("Host: %q, want %q", got, tc.host) t.Errorf("Host: %q, want %q", got, tc.host)
} }
}) })
t.Run("string", func(t *testing.T) { t.Run("string", func(t *testing.T) {
t.Parallel()
if tc.str == "\x00" { if tc.str == "\x00" {
return return
} }
@@ -287,11 +311,45 @@ func checkFs(t *testing.T, testCases []fsTestCase) {
} }
} }
func m(pathname string) *container.Absolute { return container.MustAbs(pathname) } type opsAdapter struct{ *container.Ops }
func ms(pathnames ...string) []*container.Absolute {
as := make([]*container.Absolute, len(pathnames)) func (p opsAdapter) Tmpfs(target *check.Absolute, size int, perm os.FileMode) hst.Ops {
return opsAdapter{p.Ops.Tmpfs(target, size, perm)}
}
func (p opsAdapter) Readonly(target *check.Absolute, perm os.FileMode) hst.Ops {
return opsAdapter{p.Ops.Readonly(target, perm)}
}
func (p opsAdapter) Bind(source, target *check.Absolute, flags int) hst.Ops {
return opsAdapter{p.Ops.Bind(source, target, flags)}
}
func (p opsAdapter) Overlay(target, state, work *check.Absolute, layers ...*check.Absolute) hst.Ops {
return opsAdapter{p.Ops.Overlay(target, state, work, layers...)}
}
func (p opsAdapter) OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) hst.Ops {
return opsAdapter{p.Ops.OverlayReadonly(target, layers...)}
}
func (p opsAdapter) Link(target *check.Absolute, linkName string, dereference bool) hst.Ops {
return opsAdapter{p.Ops.Link(target, linkName, dereference)}
}
func (p opsAdapter) Root(host *check.Absolute, flags int) hst.Ops {
return opsAdapter{p.Ops.Root(host, flags)}
}
func (p opsAdapter) Etc(host *check.Absolute, prefix string) hst.Ops {
return opsAdapter{p.Ops.Etc(host, prefix)}
}
func m(pathname string) *check.Absolute { return check.MustAbs(pathname) }
func ms(pathnames ...string) []*check.Absolute {
as := make([]*check.Absolute, len(pathnames))
for i, pathname := range pathnames { for i, pathname := range pathnames {
as[i] = container.MustAbs(pathname) as[i] = check.MustAbs(pathname)
} }
return as return as
} }

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