144 Commits

Author SHA1 Message Date
cat 9d932d1039 release: 0.2.1
Test / Create distribution (push) Successful in 44s
Release / Create release (push) Successful in 52s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m14s
Test / Hpkg (push) Successful in 4m20s
Test / Sandbox (race detector) (push) Successful in 4m43s
Test / Hakurei (race detector) (push) Successful in 5m55s
Test / Flake checks (push) Successful in 2m8s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-26 03:33:45 +09:00
cat 9bc8532d56 container/initdev: mount tmpfs on shm for ro dev
Test / Create distribution (push) Successful in 26s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 2m51s
Test / Hpkg (push) Successful in 3m58s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hakurei (race detector) (push) Successful in 4m46s
Test / Flake checks (push) Successful in 1m26s
Programs expect /dev/shm to be a writable tmpfs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-26 03:27:07 +09:00
cat 07194c74cb release: 0.2.0
Test / Create distribution (push) Successful in 47s
Release / Create release (push) Successful in 55s
Test / Sandbox (push) Successful in 2m29s
Test / Sandbox (race detector) (push) Successful in 4m30s
Test / Hpkg (push) Successful in 5m10s
Test / Hakurei (race detector) (push) Successful in 5m19s
Test / Hakurei (push) Successful in 3m57s
Test / Flake checks (push) Successful in 1m42s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-26 02:23:59 +09:00
cat 4cf694d2b3 hst: use hsu userid for share path suffix
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m8s
Test / Hakurei (push) Successful in 3m11s
Test / Hpkg (push) Successful in 4m8s
Test / Sandbox (race detector) (push) Successful in 4m31s
Test / Hakurei (race detector) (push) Successful in 5m8s
Test / Flake checks (push) Successful in 1m25s
The privileged user is identifier to hakurei through its hsu userid. Using the kernel uid here makes little sense and is a leftover design choice from before hsu was implemented.

Closes #7.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-26 02:16:33 +09:00
cat c9facb746b hst/config: remove data field, rename dir to home
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m10s
Test / Hpkg (push) Successful in 4m5s
Test / Sandbox (race detector) (push) Successful in 4m27s
Test / Hakurei (race detector) (push) Successful in 5m7s
Test / Flake checks (push) Successful in 1m28s
There is no reason to give the home directory special treatment, as this behaviour can be quite confusing. The home directory also does not necessarily require its own mount point, it could be provided by a parent or simply be ephemeral.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-26 00:56:10 +09:00
cat 878b66022e hst/fsbind: optional ensure source
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m18s
Test / Hakurei (push) Successful in 3m22s
Test / Hpkg (push) Successful in 4m17s
Test / Sandbox (race detector) (push) Successful in 5m33s
Test / Hakurei (race detector) (push) Successful in 3m1s
Test / Flake checks (push) Successful in 1m29s
This exposes the BindEnsure flag of BindMountOp.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-26 00:50:23 +09:00
cat 2e0a4795f6 container/initbind: optional ensure host directory
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m19s
Test / Sandbox (race detector) (push) Successful in 4m34s
Test / Hakurei (race detector) (push) Successful in 5m11s
Test / Flake checks (push) Successful in 1m46s
This is used for ensuring persistent data directories specific to the container.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-26 00:44:45 +09:00
cat c328b584c0 hst/fslink: improve string representation
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m7s
Test / Hakurei (push) Successful in 3m14s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m29s
Test / Hakurei (race detector) (push) Successful in 5m9s
Test / Flake checks (push) Successful in 1m25s
This shortens the representation of most common use cases and generally improves readability.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-25 22:52:48 +09:00
cat 9585b35d5b hst/config: remove symlink field
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m15s
Test / Hpkg (push) Successful in 4m10s
Test / Sandbox (race detector) (push) Successful in 4m27s
Test / Hakurei (race detector) (push) Successful in 5m12s
Test / Hakurei (push) Successful in 2m11s
Test / Flake checks (push) Successful in 1m29s
Closes #6.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-25 22:23:54 +09:00
cat 26cafe3e80 hst/fs: implement link fstype
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m16s
Test / Hpkg (push) Successful in 4m8s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 5m9s
Test / Hakurei (push) Successful in 2m31s
Test / Flake checks (push) Successful in 1m40s
Symlinks do not require special treatment, and doing this allows placing links in order.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-25 21:57:38 +09:00
cat 125f150784 hst/fs: update doc comments
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m18s
Test / Hakurei (push) Successful in 3m22s
Test / Hpkg (push) Successful in 4m15s
Test / Sandbox (race detector) (push) Successful in 4m34s
Test / Hakurei (race detector) (push) Successful in 5m14s
Test / Flake checks (push) Successful in 1m32s
The Type method no longer exists on the interface. Update doc comments to reflect that.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-25 21:11:39 +09:00
cat 0dcac55a0c hst/config: remove container etc field
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m25s
Test / Hakurei (push) Successful in 3m18s
Test / Hpkg (push) Successful in 4m14s
Test / Sandbox (race detector) (push) Successful in 4m32s
Test / Hakurei (race detector) (push) Successful in 5m19s
Test / Flake checks (push) Successful in 1m29s
This no longer needs special treatment since it can be specified as a generic filesystem entry.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-25 19:24:33 +09:00
cat 6d202d73b4 hst/fsbind: optional autoetc behaviour
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m18s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m31s
Test / Hakurei (race detector) (push) Successful in 5m6s
Test / Hakurei (push) Successful in 2m24s
Test / Flake checks (push) Successful in 1m29s
This generalises the special field allowing any special behaviour to be matched from target.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-25 18:38:19 +09:00
cat 1438096339 hst/config: handle filesystem entry targeting root
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m20s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 5m6s
Test / Hakurei (push) Successful in 2m10s
Test / Flake checks (push) Successful in 1m24s
This allows any fstype supported by hst to be directly mounted on sysroot. A special case in internal/app applies the matching entry early and excludes it from path hiding.

Closes #5.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-25 17:52:57 +09:00
cat 059164d4fa hst/fsbind: optional autoroot behaviour
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m17s
Test / Hakurei (push) Successful in 3m10s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m33s
Test / Hakurei (race detector) (push) Successful in 5m9s
Test / Flake checks (push) Successful in 1m23s
This allows autoroot to be configured via Filesystem.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-25 17:44:12 +09:00
cat 8db906ee64 container/dispatcher: remove exit stub test log
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m18s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m30s
Test / Hakurei (race detector) (push) Successful in 5m11s
Test / Flake checks (push) Successful in 1m30s
Turns out testing.T does not like being called in defer.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-25 17:33:35 +09:00
cat cedfceded5 container/autoroot: remove prefix field
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m12s
Test / Hpkg (push) Successful in 4m14s
Test / Sandbox (race detector) (push) Successful in 5m23s
Test / Hakurei (race detector) (push) Successful in 3m2s
Test / Flake checks (push) Successful in 1m23s
This field has been a noop for a long time. Remove it to prevent further confusion.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-25 03:39:20 +09:00
cat 33d2dcce1b container/initoverlay: internal bypass sysroot prefix
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m7s
Test / Hakurei (push) Successful in 3m12s
Test / Hpkg (push) Successful in 4m3s
Test / Sandbox (race detector) (push) Successful in 4m31s
Test / Hakurei (race detector) (push) Successful in 5m7s
Test / Flake checks (push) Successful in 1m23s
This is for supporting overlay mounts for autoroot.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-25 02:42:22 +09:00
cat 2baa2d7063 container/init: measure init behaviour
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m17s
Test / Hpkg (push) Successful in 4m13s
Test / Sandbox (race detector) (push) Successful in 4m33s
Test / Hakurei (race detector) (push) Successful in 5m8s
Test / Flake checks (push) Successful in 1m25s
This used to be entirely done via integration tests, with almost no hope of error injection and coverage profile. These tests significantly increase confidence of future work in this area.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-24 04:52:32 +09:00
cat 0166833431 container/dispatcher: start goroutine in dispatcher
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m13s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m28s
Test / Hakurei (race detector) (push) Successful in 5m6s
Test / Hakurei (push) Successful in 2m24s
Test / Flake checks (push) Successful in 1m38s
This allows instrumentation of calls from goroutine without relying on finalizers.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-23 21:58:40 +09:00
cat b3da3da525 container/init: avoid multiple lastcap calls
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m24s
Test / Hpkg (push) Successful in 4m18s
Test / Sandbox (race detector) (push) Successful in 4m27s
Test / Hakurei (race detector) (push) Successful in 5m14s
Test / Flake checks (push) Successful in 1m19s
This reduces the size of []kexpect in the test suite.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-23 11:09:11 +09:00
cat 1b3902df78 container/dispatcher: instrument each goroutine individually
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 44s
Test / Hakurei (push) Successful in 2m33s
Test / Sandbox (race detector) (push) Successful in 2m35s
Test / Hakurei (race detector) (push) Successful in 3m25s
Test / Hpkg (push) Successful in 3m41s
Test / Flake checks (push) Successful in 1m30s
Scheduler nondeterminism cannot be accounted for, so do this instead.

There should not be any performance penalty as these calls are optimised out for direct.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-23 11:07:16 +09:00
cat ea1e3ebae9 container/params: pass fd instead of file
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 4m12s
Test / Sandbox (race detector) (push) Successful in 4m29s
Test / Hakurei (race detector) (push) Successful in 5m6s
Test / Flake checks (push) Successful in 1m29s
The file is very difficult to stub. Pass fd instead as it is the value that is actually useful.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-23 00:16:46 +09:00
cat 1c692bfb79 container/init: call lockOSThread through dispatcher
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m8s
Test / Hpkg (push) Successful in 4m6s
Test / Sandbox (race detector) (push) Successful in 4m31s
Test / Hakurei (race detector) (push) Successful in 5m5s
Test / Hakurei (push) Successful in 2m8s
Test / Flake checks (push) Successful in 1m20s
This degrades test performance if not stubbed out.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-22 22:24:14 +09:00
cat 141a18999f container: move integration test helpers
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m13s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m42s
Test / Hakurei (race detector) (push) Successful in 5m8s
Test / Hakurei (push) Successful in 42s
Test / Flake checks (push) Successful in 1m38s
With the new instrumentation it is now possible to run init code outside integration tests.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-22 22:07:19 +09:00
cat afe23600d2 container/path: use syscall dispatcher
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m8s
Test / Hakurei (push) Successful in 3m14s
Test / Hpkg (push) Successful in 4m8s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Flake checks (push) Successful in 1m39s
This allows path and mount functions to be instrumented.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-22 22:02:21 +09:00
cat 09d2844981 container/init: wrap syscall helper functions
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m7s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 3m59s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hakurei (race detector) (push) Successful in 5m6s
Test / Flake checks (push) Successful in 1m26s
This allows tests to stub all kernel behaviour, enabling measurement of all function call arguments and error injection.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-22 19:27:31 +09:00
cat d500d6e559 system/dbus: share host net ns for abstract
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m5s
Test / Hakurei (push) Successful in 3m3s
Test / Hpkg (push) Successful in 4m3s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 4m58s
Test / Flake checks (push) Successful in 1m19s
Host abstract unix sockets are only accessible when also in the init net ns.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-21 21:55:23 +09:00
cat 5b73316ae0 container/syscall: doc comments from manpages
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 4m0s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 4m58s
Test / Flake checks (push) Successful in 1m25s
These are pulled straight from the manpages.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-21 00:33:46 +09:00
cat 5d8a2199b6 container/init: op interface valid method
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m12s
Test / Hpkg (push) Successful in 3m58s
Test / Sandbox (race detector) (push) Successful in 4m19s
Test / Hakurei (race detector) (push) Successful in 4m57s
Test / Flake checks (push) Successful in 1m25s
Check ops early and eliminate duplicate checks.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-21 00:18:50 +09:00
cat a1482ecdd0 container/inittmpfs: check path equivalence by value
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m5s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m21s
Test / Hakurei (race detector) (push) Successful in 4m57s
Test / Flake checks (push) Successful in 1m19s
Fixes regression introduced while integrating Absolute.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-20 20:17:28 +09:00
cat a07f9ed84c container/initsymlink: check path equivalence by value
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m4s
Test / Hakurei (push) Successful in 3m3s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m22s
Test / Hakurei (race detector) (push) Successful in 4m59s
Test / Flake checks (push) Successful in 1m19s
Fixes regression introduced while integrating Absolute.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-20 20:03:02 +09:00
cat 51304b03af container/initremount: check path equivalence by value
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m11s
Test / Hakurei (push) Successful in 3m5s
Test / Hpkg (push) Successful in 4m6s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 5m3s
Test / Flake checks (push) Successful in 1m19s
Fixes regression introduced while integrating Absolute.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-20 19:55:51 +09:00
cat c6397b941f container/initproc: check path equivalence by value
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m10s
Test / Hpkg (push) Successful in 4m8s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hakurei (race detector) (push) Successful in 4m58s
Test / Flake checks (push) Successful in 1m19s
Fixes regression introduced while integrating Absolute.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-20 19:29:45 +09:00
cat d65e5f817a container/initplace: check path equivalence by value
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m25s
Test / Hakurei (push) Successful in 3m6s
Test / Hpkg (push) Successful in 4m5s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 5m1s
Test / Flake checks (push) Successful in 1m19s
Fixes regression introduced while integrating Absolute.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-20 19:19:27 +09:00
cat 696e593898 container/initoverlay: check path equivalence by value
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m4s
Test / Hakurei (push) Successful in 3m7s
Test / Hpkg (push) Successful in 4m7s
Test / Sandbox (race detector) (push) Successful in 4m27s
Test / Hakurei (race detector) (push) Successful in 4m56s
Test / Flake checks (push) Successful in 1m19s
Fixes regression introduced while integrating Absolute.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-20 17:33:15 +09:00
cat 97ab24feef container/init: use absolute compare method
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m3s
Test / Hpkg (push) Successful in 4m4s
Test / Sandbox (race detector) (push) Successful in 4m25s
Test / Hakurei (race detector) (push) Successful in 4m59s
Test / Flake checks (push) Successful in 1m19s
More checks are also added.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-20 17:14:36 +09:00
cat 31f0dd36df absolute: efficient equivalence check method
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m16s
Test / Hakurei (push) Successful in 3m3s
Test / Hpkg (push) Successful in 3m53s
Test / Sandbox (race detector) (push) Successful in 4m16s
Test / Hakurei (race detector) (push) Successful in 4m58s
Test / Flake checks (push) Successful in 1m20s
This is more efficient and makes the call site cleaner.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-20 17:06:38 +09:00
cat 9aec2f46fe container/initdev: check path equivalence by value
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m5s
Test / Hpkg (push) Successful in 4m3s
Test / Sandbox (race detector) (push) Successful in 4m20s
Test / Hakurei (race detector) (push) Successful in 5m2s
Test / Flake checks (push) Successful in 1m28s
Fixes regression introduced while integrating Absolute.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-20 02:55:45 +09:00
cat 022cc26b2e container/capability: check CAP_TO_INDEX and CAP_TO_MASK
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m18s
Test / Hpkg (push) Successful in 4m6s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 5m2s
Test / Flake checks (push) Successful in 1m27s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-20 02:45:00 +09:00
cat b4c018da8f container/autoetc: do not bypass absolute check
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m23s
Test / Hakurei (push) Successful in 3m14s
Test / Hpkg (push) Successful in 4m7s
Test / Sandbox (race detector) (push) Successful in 4m31s
Test / Hakurei (race detector) (push) Successful in 5m3s
Test / Flake checks (push) Successful in 1m27s
This can now be done cleanly via path function wrappers.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-20 02:37:11 +09:00
cat 66f52407d3 container/initmkdir: check path equivalence by value
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m7s
Test / Hakurei (push) Successful in 3m14s
Test / Hpkg (push) Successful in 3m59s
Test / Sandbox (race detector) (push) Successful in 4m27s
Test / Hakurei (race detector) (push) Successful in 5m1s
Test / Flake checks (push) Successful in 1m39s
Fixes regression introduced while integrating Absolute.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-20 02:32:22 +09:00
cat e463faf649 container/initbind: check path equivalence by value
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m11s
Test / Hpkg (push) Successful in 4m6s
Test / Sandbox (race detector) (push) Successful in 4m23s
Test / Hakurei (race detector) (push) Successful in 5m2s
Test / Hakurei (push) Successful in 2m21s
Test / Flake checks (push) Successful in 1m29s
Same problem as autoroot, never updated the checks after integrating Absolute.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-20 02:22:04 +09:00
cat 375acb476d container/autoroot: check host path equivalence by value
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m21s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 4m12s
Test / Sandbox (race detector) (push) Successful in 4m25s
Test / Hakurei (race detector) (push) Successful in 5m1s
Test / Flake checks (push) Successful in 1m28s
This will never return true otherwise unless the equivalent paths happen to be interned by the caller.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-20 02:14:39 +09:00
cat c81c9a9d75 container/init: split setup ops into individual files
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 4m14s
Test / Sandbox (race detector) (push) Successful in 4m32s
Test / Hakurei (race detector) (push) Successful in 5m4s
Test / Flake checks (push) Successful in 1m27s
This significantly increases readability.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-20 01:28:31 +09:00
cat 339e4080dc container/ops: move Op type to init file
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m17s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 4m8s
Test / Sandbox (race detector) (push) Successful in 4m22s
Test / Hakurei (race detector) (push) Successful in 5m2s
Test / Flake checks (push) Successful in 1m28s
This helps with the eventual separation of all setup ops into individual files.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-20 01:11:24 +09:00
cat e0533aaa68 container/autoroot: filter dentry with empty name
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m5s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 5m1s
Test / Flake checks (push) Successful in 1m28s
This is unreachable, but nice to have just in case.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-20 01:03:49 +09:00
cat 13c7083bc0 container: ptrace protection via Yama LSM
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 40s
Test / Sandbox (race detector) (push) Successful in 41s
Test / Hakurei (push) Successful in 44s
Test / Hpkg (push) Successful in 41s
Test / Hakurei (race detector) (push) Successful in 1m49s
Test / Flake checks (push) Successful in 1m23s
This is only a nice to have feature as the init process has no additional privileges and the monitor process was never reachable anyway.

Closes #4.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-20 00:43:55 +09:00
cat 6947ff04e0 system/dbus/proc: host abstract only when not binding
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m7s
Test / Hpkg (push) Successful in 3m58s
Test / Sandbox (race detector) (push) Successful in 4m20s
Test / Hakurei (race detector) (push) Successful in 5m4s
Test / Flake checks (push) Successful in 1m30s
The test failure seems to be caused by an unrelated bug in xdg-dbus-proxy.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-19 23:39:14 +09:00
cat 140fe21237 container/params: check setup/receive behaviour
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m16s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m20s
Test / Hakurei (race detector) (push) Successful in 5m1s
Test / Hakurei (push) Successful in 2m7s
Test / Flake checks (push) Successful in 1m22s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-18 22:30:34 +09:00
cat f52d2c7db6 container/path: check create and mountinfo helpers
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m11s
Test / Hakurei (push) Successful in 3m7s
Test / Hpkg (push) Successful in 4m4s
Test / Sandbox (race detector) (push) Successful in 4m28s
Test / Hakurei (race detector) (push) Successful in 5m3s
Test / Flake checks (push) Successful in 1m25s
These can quite easily be checked within the framework. The scanner fault injection might require updating at some point if the implementation changes.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-18 21:30:28 +09:00
cat 3c9e547c4a cmd/hpkg: add deprecation notice
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m1s
Test / Hpkg (push) Successful in 4m0s
Test / Sandbox (race detector) (push) Successful in 4m23s
Test / Hakurei (race detector) (push) Successful in 4m57s
Test / Flake checks (push) Successful in 1m19s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-18 17:00:27 +09:00
cat a3988c1a77 hst: rename net and abstract fields
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m25s
Test / Hakurei (race detector) (push) Successful in 5m3s
Test / Flake checks (push) Successful in 1m22s
This makes more sense and matches the container library.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-18 16:48:01 +09:00
netadr 5db0714072 container: optionally isolate host abstract UNIX domain sockets via landlock
Test / Create distribution (pull_request) Successful in 33s
Test / Sandbox (pull_request) Successful in 2m10s
Test / Hpkg (pull_request) Successful in 4m1s
Test / Sandbox (race detector) (pull_request) Successful in 4m19s
Test / Hakurei (pull_request) Successful in 4m55s
Test / Hakurei (race detector) (pull_request) Successful in 5m0s
Test / Create distribution (push) Successful in 27s
Test / Sandbox (race detector) (push) Successful in 44s
Test / Sandbox (push) Successful in 44s
Test / Hakurei (push) Successful in 47s
Test / Hakurei (race detector) (push) Successful in 47s
Test / Hpkg (push) Successful in 45s
Test / Flake checks (pull_request) Successful in 1m47s
Test / Flake checks (push) Successful in 1m36s
2025-08-18 16:28:14 +09:00
cat 69a4ab8105 container: move PR_SET_NO_NEW_PRIVS to parent
Test / Create distribution (push) Successful in 28s
Test / Create distribution (pull_request) Successful in 24s
Test / Sandbox (push) Successful in 2m9s
Test / Sandbox (pull_request) Successful in 1m51s
Test / Hpkg (push) Successful in 4m17s
Test / Hpkg (pull_request) Successful in 3m45s
Test / Sandbox (race detector) (push) Successful in 4m25s
Test / Sandbox (race detector) (pull_request) Successful in 4m8s
Test / Hakurei (race detector) (push) Successful in 5m8s
Test / Hakurei (race detector) (pull_request) Successful in 4m50s
Test / Hakurei (push) Successful in 5m12s
Test / Hakurei (pull_request) Successful in 40s
Test / Flake checks (push) Successful in 1m40s
Test / Flake checks (pull_request) Successful in 1m24s
This allows some LSM setup in the parent.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-18 11:46:02 +09:00
cat 22d577ab49 test/sandbox: do not discard stderr getting hash
Test / Create distribution (push) Successful in 31s
Test / Create distribution (pull_request) Successful in 29s
Test / Sandbox (push) Successful in 45s
Test / Hakurei (push) Successful in 47s
Test / Hakurei (race detector) (push) Successful in 48s
Test / Hpkg (push) Successful in 46s
Test / Sandbox (pull_request) Successful in 45s
Test / Hakurei (pull_request) Successful in 49s
Test / Hakurei (race detector) (pull_request) Successful in 49s
Test / Hpkg (pull_request) Successful in 46s
Test / Sandbox (race detector) (pull_request) Successful in 1m16s
Test / Sandbox (race detector) (push) Successful in 1m25s
Test / Flake checks (pull_request) Successful in 1m35s
Test / Flake checks (push) Successful in 1m34s
This is the first hakurei run in the test, if the container outright fails to start this is often where it happens, so throwing away the output is very unhelpful.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-18 11:36:13 +09:00
cat 83a1c75f1a app: set up acl on X11 socket
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m22s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hpkg (push) Successful in 4m25s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Flake checks (push) Successful in 1m38s
The socket is typically owned by the priv-user, and inaccessible by the target user, so just allowing access to the directory is not enough. This change fixes this oversight and add checks that will also be useful for merging security/hakurei#1.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-18 11:30:58 +09:00
cat 0ac6e99818 container: start from locked thread
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m17s
Test / Hpkg (push) Successful in 4m14s
Test / Sandbox (race detector) (push) Successful in 4m28s
Test / Hakurei (race detector) (push) Successful in 5m12s
Test / Flake checks (push) Successful in 1m33s
This allows setup that relies on per-thread state like securebits and landlock, from the parent side.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-17 17:42:22 +09:00
cat f35733810e container: check output helper functions
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m18s
Test / Hakurei (push) Successful in 3m28s
Test / Hpkg (push) Successful in 4m25s
Test / Sandbox (race detector) (push) Successful in 4m35s
Test / Hakurei (race detector) (push) Successful in 5m17s
Test / Flake checks (push) Successful in 1m46s
The container test suite has always been somewhat inadequate due to the inability of coverage tooling to reach into containers. This has become an excuse for not testing non-container code as well, which lead to the general lack of confidence when working with container code. This change aims to be one of many to address that to some extent.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-17 02:59:37 +09:00
cat 9c1a5d43ba container: enforce nonrepeatable autoetc and autoroot
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m6s
Test / Hakurei (push) Successful in 3m4s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m18s
Test / Hakurei (race detector) (push) Successful in 4m57s
Test / Flake checks (push) Successful in 1m21s
These keep track of some internal state, and they don't make sense to have multiple instances of anyway, so instead of dealing with that, just make them nonrepetable.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-17 01:43:11 +09:00
cat 8aa65f28c6 container: allow additional state between ops
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m8s
Test / Sandbox (race detector) (push) Successful in 4m21s
Test / Hakurei (race detector) (push) Successful in 5m8s
Test / Flake checks (push) Successful in 1m26s
This is useful for ops that need to be aware of previous instances of themselves.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-17 01:32:07 +09:00
cat f9edec7e41 hst: merge miscellaneous files
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m10s
Test / Hpkg (push) Successful in 4m7s
Test / Sandbox (race detector) (push) Successful in 4m21s
Test / Hakurei (race detector) (push) Successful in 5m5s
Test / Flake checks (push) Successful in 1m24s
These structs were going to be bigger at some point. They turned out not to be.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-16 02:32:57 +09:00
cat 305c600cf5 hst: move container type to config
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m7s
Test / Hpkg (push) Successful in 3m55s
Test / Sandbox (race detector) (push) Successful in 4m18s
Test / Hakurei (race detector) (push) Successful in 3m5s
Test / Flake checks (push) Successful in 1m33s
Container state initialisation is no longer implemented in hst so splitting them no longer makes sense.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-16 02:28:36 +09:00
cat 8dd3e1ee5d hst/fs: rename method Target to Path
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m7s
Test / Hakurei (push) Successful in 3m7s
Test / Hpkg (push) Successful in 3m50s
Test / Sandbox (race detector) (push) Successful in 4m17s
Test / Hakurei (race detector) (push) Successful in 5m3s
Test / Flake checks (push) Successful in 1m27s
This allows adapter structs to use the same field names as Op structs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-16 02:06:41 +09:00
cat 4ffeec3004 hst/enablement: editor friendly enablement adaptor
Test / Create distribution (push) Successful in 35s
Test / Hakurei (push) Successful in 45s
Test / Sandbox (push) Successful in 43s
Test / Hakurei (race detector) (push) Successful in 45s
Test / Sandbox (race detector) (push) Successful in 43s
Test / Hpkg (push) Successful in 3m17s
Test / Flake checks (push) Successful in 1m27s
Having the bit field value here (in decimal, no less) is unfriendly to text editors. Use a bunch of booleans here to improve ease of use.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-15 05:16:51 +09:00
cat 9ed3ba85ea hst/fs: implement overlay fstype
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m8s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 3m59s
Test / Sandbox (race detector) (push) Successful in 4m20s
Test / Hakurei (race detector) (push) Successful in 5m1s
Test / Flake checks (push) Successful in 1m27s
This finally exposes overlay mounts in the high level hakurei API.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-15 04:00:55 +09:00
cat 4433c993fa nix: check config via hakurei
Test / Create distribution (push) Successful in 33s
Test / Hpkg (push) Successful in 40s
Test / Sandbox (push) Successful in 1m28s
Test / Sandbox (race detector) (push) Successful in 2m20s
Test / Hakurei (push) Successful in 2m26s
Test / Hakurei (race detector) (push) Successful in 3m5s
Test / Flake checks (push) Successful in 1m24s
This is unfortunately the only feasible way of doing this in nix.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-15 03:27:54 +09:00
cat 430991c39b hst/fs: remove type method
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m3s
Test / Hakurei (push) Successful in 3m7s
Test / Hpkg (push) Successful in 3m51s
Test / Sandbox (race detector) (push) Successful in 4m14s
Test / Hakurei (race detector) (push) Successful in 4m54s
Test / Flake checks (push) Successful in 1m28s
Having a method that returns the canonical string representation of its type seemed like a much better idea for an implementation that never made it to staging. Remove it here and clean up marshal type assertions.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-15 00:39:02 +09:00
cat ba3227bf15 container: export overlay escape
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m21s
Test / Hakurei (push) Successful in 3m23s
Test / Hpkg (push) Successful in 4m14s
Test / Sandbox (race detector) (push) Successful in 4m22s
Test / Hakurei (race detector) (push) Successful in 5m8s
Test / Flake checks (push) Successful in 1m22s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-14 23:44:11 +09:00
cat 0e543a58b3 hst/fs: valid method on underlying interface
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m59s
Test / Hakurei (push) Successful in 3m6s
Test / Hpkg (push) Successful in 4m16s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 5m7s
Test / Flake checks (push) Successful in 1m39s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-14 21:36:22 +09:00
cat c989e7785a hst/info: include extra information
Test / Create distribution (push) Successful in 43s
Test / Sandbox (push) Successful in 2m34s
Test / Hakurei (push) Successful in 3m45s
Test / Sandbox (race detector) (push) Successful in 4m33s
Test / Hpkg (push) Successful in 4m41s
Test / Hakurei (race detector) (push) Successful in 5m25s
Test / Flake checks (push) Successful in 1m37s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-14 19:52:03 +09:00
cat 332d90d6c7 container/path: remove unused path
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m9s
Test / Sandbox (race detector) (push) Successful in 4m19s
Test / Hpkg (push) Successful in 4m35s
Test / Hakurei (race detector) (push) Successful in 5m23s
Test / Hakurei (push) Successful in 2m40s
Test / Flake checks (push) Successful in 1m39s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-14 05:00:09 +09:00
cat 99ac96511b hst/fs: interface filesystem config
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m37s
Test / Sandbox (race detector) (push) Successful in 4m23s
Test / Hpkg (push) Successful in 4m27s
Test / Hakurei (race detector) (push) Successful in 5m22s
Test / Flake checks (push) Successful in 1m22s
This allows mount points to be represented by different underlying structs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-14 04:52:49 +09:00
cat e99d7affb0 container: use absolute for pathname
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m59s
Test / Hakurei (push) Successful in 2m58s
Test / Hpkg (push) Successful in 3m45s
Test / Sandbox (race detector) (push) Successful in 4m11s
Test / Hakurei (race detector) (push) Successful in 4m47s
Test / Flake checks (push) Successful in 1m26s
This is simultaneously more efficient and less error-prone. This change caused minor API changes in multiple other packages.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-11 04:56:42 +09:00
cat 41ac2be965 container/absolute: wrap safe stdlib functions
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m0s
Test / Hakurei (push) Successful in 2m57s
Test / Hpkg (push) Successful in 3m52s
Test / Sandbox (race detector) (push) Successful in 4m4s
Test / Hakurei (race detector) (push) Successful in 4m49s
Test / Flake checks (push) Successful in 1m31s
These functions do not change the absoluteness of a pathname.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-10 03:11:10 +09:00
cat 02271583fb container: remove PATH lookup behaviour
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m57s
Test / Hakurei (push) Successful in 2m57s
Test / Hpkg (push) Successful in 3m58s
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 2m42s
Test / Flake checks (push) Successful in 1m25s
This is way higher level than the container package and does not even work unless every path is mounted in the exact same location.

This behaviour causes nothing but confusion and problems,

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-09 19:08:54 +09:00
cat ef54b2cd08 container/absolute: early absolute pathname check
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m1s
Test / Hakurei (push) Successful in 2m57s
Test / Hpkg (push) Successful in 3m50s
Test / Sandbox (race detector) (push) Successful in 4m13s
Test / Hakurei (race detector) (push) Successful in 4m48s
Test / Flake checks (push) Successful in 1m25s
This is less error-prone, and allows pathname to be checked once.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-09 18:53:46 +09:00
cat 82608164f6 container/params: remove confusingly named error
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 2m59s
Test / Hpkg (push) Successful in 3m53s
Test / Sandbox (race detector) (push) Successful in 4m16s
Test / Hakurei (race detector) (push) Successful in 4m49s
Test / Flake checks (push) Successful in 1m19s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-09 17:37:46 +09:00
cat edd6f2cfa9 container: document ambient capabilities
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m3s
Test / Hpkg (push) Successful in 3m54s
Test / Sandbox (race detector) (push) Successful in 4m20s
Test / Hakurei (race detector) (push) Successful in 4m45s
Test / Hakurei (push) Successful in 2m3s
Test / Flake checks (push) Successful in 1m22s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-08 02:11:55 +09:00
cat acffa76812 container/ops: implement overlay op
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m2s
Test / Hakurei (push) Successful in 2m57s
Test / Hpkg (push) Successful in 3m54s
Test / Sandbox (race detector) (push) Successful in 4m6s
Test / Hakurei (race detector) (push) Successful in 4m51s
Test / Flake checks (push) Successful in 1m22s
There are significant limitations to using the overlay mount, and the implementation in the kernel is quite quirky. For now the Op is quite robust, however a higher level interface for it has not been decided yet.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-08 01:54:48 +09:00
cat 8da76483e6 container/path: fix typo "paths"
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m57s
Test / Hakurei (push) Successful in 2m54s
Test / Hpkg (push) Successful in 3m53s
Test / Sandbox (race detector) (push) Successful in 3m57s
Test / Hakurei (race detector) (push) Successful in 4m37s
Test / Flake checks (push) Successful in 1m25s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-08 01:32:48 +09:00
cat 534c932906 container: test case runtime initialisation
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m5s
Test / Hpkg (push) Successful in 3m49s
Test / Sandbox (race detector) (push) Successful in 3m53s
Test / Hakurei (race detector) (push) Successful in 4m36s
Test / Hakurei (push) Successful in 2m10s
Test / Flake checks (push) Successful in 1m34s
This allows for more sophisticated test setup.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-08 01:03:35 +09:00
cat fee10fed4d container: test bypass output buffer on verbose
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m55s
Test / Hakurei (push) Successful in 2m55s
Test / Sandbox (race detector) (push) Successful in 3m51s
Test / Hpkg (push) Successful in 3m57s
Test / Hakurei (race detector) (push) Successful in 4m35s
Test / Flake checks (push) Successful in 1m25s
This restores verbose behaviour.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-08 00:57:27 +09:00
cat a4f7e92e1c test/interactive: helper scripts for tracing
Test / Hakurei (push) Successful in 41s
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 39s
Test / Hakurei (race detector) (push) Successful in 41s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Hpkg (push) Successful in 40s
Test / Flake checks (push) Successful in 1m26s
The vm state is discarded often, and it is quite cumbersome to set everything up again when the shell history is gone.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-08 00:56:25 +09:00
cat f1a53d6116 container: raise CAP_DAC_OVERRIDE
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m59s
Test / Hakurei (push) Successful in 2m54s
Test / Sandbox (race detector) (push) Successful in 3m52s
Test / Hpkg (push) Successful in 3m51s
Test / Hakurei (race detector) (push) Successful in 4m39s
Test / Flake checks (push) Successful in 1m25s
This is required for upperdir and workdir checks in overlayfs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-08 00:43:19 +09:00
cat b353c3deea nix: make src overlay writable
Test / Hakurei (push) Successful in 42s
Test / Create distribution (push) Successful in 33s
Test / Hakurei (race detector) (push) Successful in 42s
Test / Sandbox (push) Successful in 40s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Hpkg (push) Successful in 40s
Test / Flake checks (push) Successful in 1m23s
The lowerdir is in the nix store.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-07 18:07:19 +09:00
cat fde5f1ca64 container: buffer test output
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m2s
Test / Hakurei (push) Successful in 2m54s
Test / Hpkg (push) Successful in 3m56s
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m37s
Test / Flake checks (push) Successful in 1m26s
This further reduces noise on test failure by only passing through output of the failed test.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-07 02:55:58 +09:00
cat 4d0bdd84b5 container: test respect verbose flag
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m1s
Test / Hpkg (push) Successful in 3m52s
Test / Sandbox (race detector) (push) Successful in 4m1s
Test / Hakurei (race detector) (push) Successful in 4m35s
Test / Hakurei (push) Successful in 2m4s
Test / Flake checks (push) Successful in 1m36s
This reduces noise on test failure.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-07 02:50:00 +09:00
cat 72a931a71a nix: interactive nixos vm
Test / Hakurei (push) Successful in 41s
Test / Create distribution (push) Successful in 32s
Test / Hakurei (race detector) (push) Successful in 41s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Hpkg (push) Successful in 40s
Test / Flake checks (push) Successful in 1m26s
This is useful for quickly spinning up an ephemeral hakurei environment for testing changes or reproducing vm test failures.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-07 02:46:04 +09:00
cat 9a25542c6d container/init: use mount string constants
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m13s
Test / Sandbox (race detector) (push) Successful in 4m6s
Test / Hpkg (push) Successful in 4m22s
Test / Hakurei (race detector) (push) Successful in 4m49s
Test / Hakurei (push) Successful in 2m4s
Test / Flake checks (push) Successful in 1m13s
These literals were missed when the constants were first defined.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-04 04:00:05 +09:00
cat c6be82bcf9 container/path: fhs path constants
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m6s
Test / Hakurei (push) Successful in 3m6s
Test / Hpkg (push) Successful in 4m11s
Test / Sandbox (race detector) (push) Successful in 4m14s
Test / Hakurei (race detector) (push) Successful in 4m40s
Test / Flake checks (push) Successful in 1m18s
This increases readability since this can help disambiguate absolute paths from similarly named path segments.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-03 21:16:45 +09:00
cat 38245559dc container/ops: mount dev readonly
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m2s
Test / Hakurei (push) Successful in 2m57s
Test / Sandbox (race detector) (push) Successful in 3m53s
Test / Hpkg (push) Successful in 3m53s
Test / Hakurei (race detector) (push) Successful in 4m37s
Test / Flake checks (push) Successful in 1m18s
There is usually no good reason to write to /dev. This however doesn't work in internal/app because FilesystemConfig supplied by ContainerConfig might add entries to /dev, so internal/app follows DevWritable with Remount instead.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-03 19:18:53 +09:00
cat 7b416d47dc container/ops: merge mqueue and dev Ops
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 40s
Test / Sandbox (race detector) (push) Successful in 40s
Test / Hakurei (push) Successful in 43s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Hpkg (push) Successful in 41s
Test / Flake checks (push) Successful in 1m21s
There is no reason to mount mqueue anywhere else, and these Ops usually follow each other. This change merges them. This helps decrease IPC overhead and also enables mounting dev readonly.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-03 19:13:46 +09:00
cat 15170735ba container/mount: move tmpfs sysroot prefixing to caller
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m4s
Test / Sandbox (race detector) (push) Successful in 3m54s
Test / Hpkg (push) Successful in 4m0s
Test / Hakurei (race detector) (push) Successful in 4m34s
Test / Hakurei (push) Successful in 2m6s
Test / Flake checks (push) Successful in 1m18s
The mountTmpfs helper is a relatively low level function that is not exposed as part of the API. Prefixing sysroot here not only introduces overhead but is also quite error-prone.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-03 18:06:41 +09:00
cat 6a3886e9db container/op: unexport bind resolved source field
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m5s
Test / Hakurei (push) Successful in 2m57s
Test / Hpkg (push) Successful in 3m55s
Test / Sandbox (race detector) (push) Successful in 3m59s
Test / Hakurei (race detector) (push) Successful in 4m34s
Test / Flake checks (push) Successful in 1m21s
This is used for symlink resolution and is only used internally.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-03 17:57:37 +09:00
cat ff66296378 container/mount: mount data escape helper function
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m0s
Test / Hakurei (push) Successful in 2m56s
Test / Sandbox (race detector) (push) Successful in 3m57s
Test / Hpkg (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m38s
Test / Flake checks (push) Successful in 1m18s
For formatting user-supplied path strings into overlayfs mount data.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-03 17:46:14 +09:00
cat 347a79df72 container: improve clone flags readability
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m0s
Test / Sandbox (race detector) (push) Successful in 3m50s
Test / Hpkg (push) Successful in 3m50s
Test / Hakurei (race detector) (push) Successful in 4m31s
Test / Hakurei (push) Successful in 2m3s
Test / Flake checks (push) Successful in 1m15s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-02 18:19:44 +09:00
cat 0f78864a67 container/mount: export mount string constants
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m1s
Test / Hakurei (push) Successful in 2m56s
Test / Sandbox (race detector) (push) Successful in 3m47s
Test / Hpkg (push) Successful in 4m1s
Test / Hakurei (race detector) (push) Successful in 4m32s
Test / Flake checks (push) Successful in 1m19s
This improves code readability and should also be useful for callers choosing to preserve CAP_SYS_ADMIN.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-02 17:20:09 +09:00
cat b32b1975a8 hst/container: remove cover
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m6s
Test / Hakurei (push) Successful in 2m56s
Test / Sandbox (race detector) (push) Successful in 3m55s
Test / Hpkg (push) Successful in 3m55s
Test / Hakurei (race detector) (push) Successful in 4m31s
Test / Flake checks (push) Successful in 1m20s
This was never useful, and is now completely replaced by regular FilesystemConfig being able to mount tmpfs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-02 00:34:52 +09:00
cat 2b1eaa62f1 update github notice
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m0s
Test / Sandbox (race detector) (push) Successful in 3m52s
Test / Hpkg (push) Successful in 4m2s
Test / Hakurei (race detector) (push) Successful in 4m31s
Test / Flake checks (push) Successful in 1m20s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-02 00:21:16 +09:00
cat f13dca184c release: 0.1.3
Test / Create distribution (push) Successful in 44s
Release / Create release (push) Successful in 51s
Test / Sandbox (push) Successful in 2m3s
Test / Sandbox (race detector) (push) Successful in 4m3s
Test / Hpkg (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m46s
Test / Hakurei (push) Successful in 2m19s
Test / Flake checks (push) Successful in 1m19s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-02 00:02:54 +09:00
cat 3b8a3d3b00 app: remount root readonly
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 41s
Test / Sandbox (race detector) (push) Successful in 42s
Test / Hakurei (race detector) (push) Successful in 45s
Test / Hpkg (push) Successful in 44s
Test / Hakurei (push) Successful in 2m13s
Test / Flake checks (push) Successful in 1m25s
This does nothing for security, but should help avoid hiding bugs of programs developed in a hakurei container.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-01 23:56:28 +09:00
cat c5d24979f5 container/ops: expose remount as Op
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m2s
Test / Hakurei (push) Successful in 2m56s
Test / Hpkg (push) Successful in 3m53s
Test / Sandbox (race detector) (push) Successful in 3m56s
Test / Hakurei (race detector) (push) Successful in 4m34s
Test / Flake checks (push) Successful in 1m22s
This is useful for building a filesystem hierarchy then remounting it readonly.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-01 23:48:02 +09:00
cat 1dc780bca7 container/mount: separate remount from bind
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m5s
Test / Hakurei (push) Successful in 2m52s
Test / Sandbox (race detector) (push) Successful in 3m54s
Test / Hpkg (push) Successful in 3m59s
Test / Hakurei (race detector) (push) Successful in 4m34s
Test / Flake checks (push) Successful in 1m18s
Remount turns out to be useful in other places.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-01 23:32:38 +09:00
cat ec33061c92 nix: remove nscd cover
Test / Create distribution (push) Successful in 33s
Test / Hpkg (push) Successful in 40s
Test / Sandbox (push) Successful in 1m30s
Test / Hakurei (push) Successful in 2m18s
Test / Sandbox (race detector) (push) Successful in 2m21s
Test / Hakurei (race detector) (push) Successful in 2m50s
Test / Flake checks (push) Successful in 1m15s
This is a pd workaround that does nothing in the nixos module.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-01 22:04:58 +09:00
cat af0899de96 hst/container: mount tmpfs via magic src string
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 2m50s
Test / Sandbox (race detector) (push) Successful in 3m53s
Test / Hpkg (push) Successful in 3m54s
Test / Hakurei (race detector) (push) Successful in 4m30s
Test / Flake checks (push) Successful in 1m24s
There's often good reason to mount tmpfs in the container.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-01 21:23:52 +09:00
cat 547a2adaa4 container/mount: pass tmpfs flags
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m1s
Test / Sandbox (race detector) (push) Successful in 3m57s
Test / Hpkg (push) Successful in 3m55s
Test / Hakurei (race detector) (push) Successful in 4m30s
Test / Hakurei (push) Successful in 2m18s
Test / Flake checks (push) Successful in 1m14s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-01 18:59:06 +09:00
cat c02948e155 cmd/hakurei: print autoroot configuration
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m3s
Test / Hakurei (push) Successful in 3m3s
Test / Sandbox (race detector) (push) Successful in 4m8s
Test / Hpkg (push) Successful in 4m18s
Test / Hakurei (race detector) (push) Successful in 4m43s
Test / Flake checks (push) Successful in 1m22s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-01 04:29:01 +09:00
cat 387b86bcdd app: integrate container autoroot
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m25s
Test / Sandbox (race detector) (push) Successful in 4m13s
Test / Hpkg (push) Successful in 4m36s
Test / Hakurei (race detector) (push) Successful in 5m2s
Test / Hakurei (push) Successful in 2m40s
Test / Flake checks (push) Successful in 1m36s
Doing this instead of mounting directly on / because it's impossible to ensure a parent is available for every path hakurei wants to mount to. This situation is similar to autoetc hence the similar name, however a symlink mirror will not work in this case.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-01 04:21:54 +09:00
cat 4e85643865 container: implement autoroot as setup op
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m7s
Test / Sandbox (race detector) (push) Successful in 4m1s
Test / Hpkg (push) Successful in 4m5s
Test / Hakurei (race detector) (push) Successful in 4m43s
Test / Flake checks (push) Successful in 1m22s
This code is useful beyond just pd behaviour, and implementing it this way also reduces IPC overhead.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-01 04:04:36 +09:00
cat 987981df73 test/sandbox: check pd behaviour
Test / Create distribution (push) Successful in 34s
Test / Sandbox (race detector) (push) Successful in 42s
Test / Hakurei (push) Successful in 44s
Test / Sandbox (push) Successful in 42s
Test / Hakurei (race detector) (push) Successful in 45s
Test / Hpkg (push) Successful in 43s
Test / Flake checks (push) Successful in 1m23s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-01 03:27:02 +09:00
cat f14e7255be container/ops: use correct flags value in bind string
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m57s
Test / Sandbox (race detector) (push) Successful in 3m47s
Test / Hpkg (push) Successful in 3m54s
Test / Hakurei (race detector) (push) Successful in 4m31s
Test / Hakurei (push) Successful in 2m10s
Test / Flake checks (push) Successful in 1m22s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-01 00:54:08 +09:00
cat a8a79a8664 cmd/hpkg: rename from planterette
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m58s
Test / Sandbox (race detector) (push) Successful in 3m47s
Test / Hpkg (push) Successful in 3m54s
Test / Hakurei (race detector) (push) Successful in 4m32s
Test / Hakurei (push) Successful in 2m10s
Test / Flake checks (push) Successful in 1m19s
Planterette is now developed in another repository, so rename this proof of concept to avoid confusion.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-31 23:57:11 +09:00
cat 3ae0cec000 test: increase vm memory
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Planterette (push) Successful in 40s
Test / Hakurei (push) Successful in 2m11s
Test / Hakurei (race detector) (push) Successful in 2m42s
Test / Flake checks (push) Successful in 1m10s
This hopefully fixes the intermittent failures.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-31 22:08:01 +09:00
cat 4e518f11d8 container/ops: autoetc implementation to separate file
Test / Create distribution (push) Successful in 1m3s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m11s
Test / Sandbox (race detector) (push) Successful in 3m52s
Test / Planterette (push) Successful in 4m5s
Test / Hakurei (race detector) (push) Successful in 4m41s
Test / Flake checks (push) Successful in 1m14s
This is not a general purpose setup Op. Separate it so it is easier to find.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-31 19:54:03 +09:00
cat cb513bb1cd release: 0.1.2
Release / Create release (push) Successful in 1m55s
Test / Create distribution (push) Successful in 44s
Test / Sandbox (push) Successful in 1m43s
Test / Sandbox (race detector) (push) Successful in 2m25s
Test / Hakurei (push) Successful in 2m47s
Test / Hakurei (race detector) (push) Successful in 3m21s
Test / Planterette (push) Successful in 5m15s
Test / Flake checks (push) Successful in 1m17s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-29 03:11:33 +09:00
cat f7bd28118c hst: configurable wait delay
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m58s
Test / Hakurei (push) Successful in 2m47s
Test / Sandbox (race detector) (push) Successful in 3m56s
Test / Planterette (push) Successful in 3m58s
Test / Hakurei (race detector) (push) Successful in 4m31s
Test / Flake checks (push) Successful in 1m17s
This is useful for programs that take a long time to clean up.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-29 03:06:49 +09:00
cat 940ee00ffe container/init: configurable lingering process wait delay
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m57s
Test / Hakurei (push) Successful in 2m50s
Test / Planterette (push) Successful in 3m39s
Test / Sandbox (race detector) (push) Successful in 3m43s
Test / Hakurei (race detector) (push) Successful in 4m33s
Test / Flake checks (push) Successful in 1m16s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-29 02:38:17 +09:00
cat b43d104680 app: integrate interrupt forwarding
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m58s
Test / Hakurei (push) Successful in 2m53s
Test / Sandbox (race detector) (push) Successful in 3m53s
Test / Planterette (push) Successful in 3m53s
Test / Hakurei (race detector) (push) Successful in 4m31s
Test / Flake checks (push) Successful in 1m19s
This significantly increases usability of command line tools running through hakurei.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-29 02:23:06 +09:00
cat ddf48a6c22 app/shim: implement signal handler outcome in Go
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m53s
Test / Hakurei (push) Successful in 2m48s
Test / Planterette (push) Successful in 3m48s
Test / Sandbox (race detector) (push) Successful in 3m56s
Test / Hakurei (race detector) (push) Successful in 4m27s
Test / Flake checks (push) Successful in 1m13s
This needs to be done from the Go side eventually anyway to integrate the signal forwarding behaviour now supported by the container package.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-28 23:39:30 +09:00
cat a0f499e30a app/shim: separate signal handler implementation
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m57s
Test / Planterette (push) Successful in 3m44s
Test / Sandbox (race detector) (push) Successful in 3m50s
Test / Hakurei (race detector) (push) Successful in 4m25s
Test / Hakurei (push) Successful in 2m0s
Test / Flake checks (push) Successful in 1m19s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-28 21:52:53 +09:00
cat d6b07f12ff container: forward context cancellation
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m56s
Test / Hakurei (push) Successful in 2m47s
Test / Planterette (push) Successful in 3m40s
Test / Sandbox (race detector) (push) Successful in 3m45s
Test / Hakurei (race detector) (push) Successful in 4m29s
Test / Flake checks (push) Successful in 1m18s
This allows container processes to exit gracefully.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-28 01:45:38 +09:00
cat 65fe09caf9 container: check cancel signal delivery
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m55s
Test / Hakurei (push) Successful in 2m50s
Test / Sandbox (race detector) (push) Successful in 3m46s
Test / Planterette (push) Successful in 3m52s
Test / Hakurei (race detector) (push) Successful in 4m28s
Test / Flake checks (push) Successful in 1m18s
This change also makes some parts of the test more robust.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-28 01:04:29 +09:00
cat a1e5f020f4 container: improve doc comments
Test / Create distribution (push) Successful in 31s
Test / Sandbox (push) Successful in 2m3s
Test / Hakurei (push) Successful in 2m53s
Test / Sandbox (race detector) (push) Successful in 3m43s
Test / Planterette (push) Successful in 3m57s
Test / Hakurei (race detector) (push) Successful in 4m23s
Test / Flake checks (push) Successful in 1m10s
Putting them on the builder methods is more useful.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-27 12:27:42 +09:00
cat bd3fa53a55 container: access test case by index in helper
Test / Create distribution (push) Successful in 24s
Test / Hakurei (push) Successful in 40s
Test / Sandbox (push) Successful in 38s
Test / Hakurei (race detector) (push) Successful in 41s
Test / Sandbox (race detector) (push) Successful in 38s
Test / Planterette (push) Successful in 39s
Test / Flake checks (push) Successful in 1m17s
This is more elegant and allows for much easier extension of the tests. Mountinfo is still serialised however due to libPaths nondeterminism.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-26 18:59:19 +09:00
cat 625632c593 nix: update flake lock
Test / Create distribution (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 50s
Test / Sandbox (push) Successful in 52s
Test / Planterette (push) Successful in 50s
Test / Hakurei (race detector) (push) Successful in 57s
Test / Hakurei (push) Successful in 59s
Test / Flake checks (push) Successful in 1m53s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-26 18:57:54 +09:00
cat e71ae3b8c5 container: remove custom cmd initialisation
Test / Create distribution (push) Successful in 26s
Test / Hakurei (push) Successful in 45s
Test / Sandbox (push) Successful in 43s
Test / Hakurei (race detector) (push) Successful in 45s
Test / Sandbox (race detector) (push) Successful in 43s
Test / Planterette (push) Successful in 43s
Test / Flake checks (push) Successful in 1m27s
This part of the interface is very unintuitive and only used for testing, even in testing it is inelegant and can be done better.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-25 00:45:10 +09:00
cat 9d7a19d162 container: use more reliable nonexistence
Test / Create distribution (push) Successful in 45s
Test / Sandbox (push) Successful in 2m21s
Test / Hakurei (push) Successful in 3m8s
Test / Planterette (push) Successful in 3m55s
Test / Sandbox (race detector) (push) Successful in 4m6s
Test / Hakurei (race detector) (push) Successful in 4m41s
Test / Flake checks (push) Successful in 1m18s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-18 23:18:26 +09:00
cat 6ba19a7ba5 release: 0.1.1
Test / Create distribution (push) Successful in 38s
Release / Create release (push) Successful in 46s
Test / Sandbox (push) Successful in 48s
Test / Sandbox (race detector) (push) Successful in 49s
Test / Hakurei (push) Successful in 52s
Test / Planterette (push) Successful in 51s
Test / Hakurei (race detector) (push) Successful in 54s
Test / Flake checks (push) Successful in 1m29s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-09 05:42:31 +09:00
cat 749a2779f5 test/sandbox: add arm64 constants
Test / Create distribution (push) Successful in 24s
Test / Sandbox (push) Successful in 40s
Test / Hakurei (push) Successful in 42s
Test / Hakurei (race detector) (push) Successful in 42s
Test / Sandbox (race detector) (push) Successful in 38s
Test / Planterette (push) Successful in 40s
Test / Flake checks (push) Successful in 1m30s
Most of these are differences in qemu.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-09 05:36:35 +09:00
cat e574042d76 test/sandbox: verify seccomp on all test cases
Test / Hakurei (push) Successful in 42s
Test / Sandbox (push) Successful in 39s
Test / Hakurei (race detector) (push) Successful in 41s
Test / Create distribution (push) Successful in 33s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Planterette (push) Successful in 41s
Test / Flake checks (push) Successful in 1m17s
This change also makes seccomp hashes cross-platform.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-09 04:21:35 +09:00
cat 2b44493e8a test/sandbox: guard on testtool tag
Test / Hakurei (push) Successful in 40s
Test / Create distribution (push) Successful in 31s
Test / Hakurei (race detector) (push) Successful in 41s
Test / Planterette (push) Successful in 40s
Test / Sandbox (push) Successful in 1m30s
Test / Sandbox (race detector) (push) Successful in 1m43s
Test / Flake checks (push) Successful in 1m11s
This tool should not show up when building hakurei normally.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-07 20:11:29 +09:00
cat c30dd4e630 test/sandbox/seccomp: remove uselib
Test / Hakurei (push) Successful in 41s
Test / Create distribution (push) Successful in 32s
Test / Hakurei (race detector) (push) Successful in 41s
Test / Planterette (push) Successful in 40s
Test / Sandbox (push) Successful in 1m27s
Test / Sandbox (race detector) (push) Successful in 1m44s
Test / Flake checks (push) Successful in 1m12s
This syscall is not wired on all platforms. This test barely does anything anyway and seccomp is covered by the privileged test instrumentation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-07 15:28:55 +09:00
cat d90da1c8f5 container/seccomp: add arm64 constants
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m2s
Test / Hakurei (push) Successful in 2m52s
Test / Sandbox (race detector) (push) Successful in 3m9s
Test / Planterette (push) Successful in 3m40s
Test / Hakurei (race detector) (push) Successful in 4m28s
Test / Flake checks (push) Successful in 1m12s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-07 14:58:03 +09:00
cat 5853d7700f container/seccomp: move bpf hashes
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m56s
Test / Sandbox (race detector) (push) Successful in 3m7s
Test / Planterette (push) Successful in 3m35s
Test / Hakurei (race detector) (push) Successful in 4m23s
Test / Hakurei (push) Successful in 2m7s
Test / Flake checks (push) Successful in 1m19s
Filter programs are different across platforms. This representation is also much more readable.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-07 14:41:47 +09:00
cat d5c7523726 container/init: fix prctl call
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m50s
Test / Hakurei (push) Successful in 2m43s
Test / Sandbox (race detector) (push) Successful in 3m11s
Test / Planterette (push) Successful in 3m35s
Test / Hakurei (race detector) (push) Successful in 4m21s
Test / Flake checks (push) Successful in 1m8s
This is a very silly typo. Luckily has no effect due to an upper layer doing PR_SET_NO_NEW_PRIVS already.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-07 14:06:14 +09:00
cat ddfcc51b91 container: move capset implementation
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m46s
Test / Hakurei (push) Successful in 2m50s
Test / Sandbox (race detector) (push) Successful in 3m4s
Test / Planterette (push) Successful in 3m35s
Test / Hakurei (race detector) (push) Successful in 4m21s
Test / Flake checks (push) Successful in 1m10s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-07 13:47:13 +09:00
cat 8ebedbd88a container: move syscall constants
Test / Create distribution (push) Successful in 31s
Test / Sandbox (push) Successful in 1m55s
Test / Hakurei (push) Successful in 2m45s
Test / Sandbox (race detector) (push) Successful in 3m6s
Test / Planterette (push) Successful in 3m33s
Test / Hakurei (race detector) (push) Successful in 4m20s
Test / Flake checks (push) Successful in 1m10s
These aren't missing from all targets.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-07 13:23:01 +09:00
cat 84e8142a2d container/seccomp: move personality constants
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m50s
Test / Hakurei (push) Successful in 2m45s
Test / Sandbox (race detector) (push) Successful in 3m5s
Test / Planterette (push) Successful in 3m37s
Test / Hakurei (race detector) (push) Successful in 4m22s
Test / Flake checks (push) Successful in 1m8s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-07 12:44:32 +09:00
cat 2c7b7ad845 container/seccomp: cross-platform sysnum cutoff
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m54s
Test / Hakurei (push) Successful in 2m47s
Test / Sandbox (race detector) (push) Successful in 3m5s
Test / Planterette (push) Successful in 3m30s
Test / Hakurei (race detector) (push) Successful in 4m20s
Test / Flake checks (push) Successful in 1m10s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-07 12:27:00 +09:00
cat 72c2b66fc0 nix: cross-platform syscall wrapper
Test / Create distribution (push) Successful in 42s
Test / Sandbox (push) Successful in 54s
Test / Sandbox (race detector) (push) Successful in 52s
Test / Planterette (push) Successful in 51s
Test / Hakurei (push) Successful in 1m1s
Test / Hakurei (race detector) (push) Successful in 59s
Test / Flake checks (push) Successful in 1m6s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-07 04:22:55 +09:00
cat 356b42a406 container/init: use /proc/self as intermediate
Test / Create distribution (push) Successful in 43s
Test / Sandbox (push) Successful in 2m19s
Test / Sandbox (race detector) (push) Successful in 4m11s
Test / Hakurei (race detector) (push) Successful in 5m28s
Test / Hakurei (push) Successful in 2m10s
Test / Planterette (push) Successful in 38m44s
Test / Flake checks (push) Successful in 3m0s
Setting up via /tmp is okay, /proc/self/fd makes a lot more sense though for reasons described in the comment.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-06 02:14:35 +09:00
cat d9b6d48e7c add miscellaneous badges
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m51s
Test / Hakurei (push) Successful in 2m45s
Test / Sandbox (race detector) (push) Successful in 3m11s
Test / Planterette (push) Successful in 3m37s
Test / Hakurei (race detector) (push) Successful in 4m20s
Test / Flake checks (push) Successful in 1m9s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-03 18:04:09 +09:00
cat 087959e81b app: remove split implementation
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m56s
Test / Hakurei (push) Successful in 2m42s
Test / Sandbox (race detector) (push) Successful in 3m5s
Test / Planterette (push) Successful in 3m37s
Test / Hakurei (race detector) (push) Successful in 4m19s
Test / Flake checks (push) Successful in 1m7s
It is completely nonsensical and highly error-prone to have multiple implementations of this in the same build. This should be switched at compile time instead therefore the split packages are pointless.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-03 04:36:59 +09:00
175 changed files with 15096 additions and 3295 deletions
+5 -5
View File
@@ -73,20 +73,20 @@ jobs:
path: result/*
retention-days: 1
planterette:
name: Planterette
hpkg:
name: Hpkg
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run NixOS test
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.planterette
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.hpkg
- name: Upload test output
uses: actions/upload-artifact@v3
with:
name: "planterette-vm-output"
name: "hpkg-vm-output"
path: result/*
retention-days: 1
@@ -97,7 +97,7 @@ jobs:
- race
- sandbox
- sandbox-race
- planterette
- hpkg
runs-on: nix
steps:
- name: Checkout
+5 -1
View File
@@ -1 +1,5 @@
This port is solely for releasing to the github mirror and serves no purpose during development.
DO NOT ADD NEW ACTIONS HERE
This port is solely for releasing to the github mirror and serves no purpose during development.
All development happens at https://git.gensokyo.uk/security/hakurei. If you wish to contribute,
request for an account on git.gensokyo.uk.
+4 -1
View File
@@ -29,4 +29,7 @@ go.work.sum
/cmd/hakurei/LICENSE
# release
/dist/hakurei-*
/dist/hakurei-*
# interactive nixos vm
nixos.qcow2
+6 -1
View File
@@ -8,11 +8,16 @@
<p align="center">
<a href="https://pkg.go.dev/hakurei.app"><img src="https://pkg.go.dev/badge/hakurei.app.svg" alt="Go Reference" /></a>
<a href="https://git.gensokyo.uk/security/hakurei/actions"><img src="https://git.gensokyo.uk/security/hakurei/actions/workflows/test.yml/badge.svg?branch=staging&style=flat-square" alt="Gitea Workflow Status" /></a>
<br/>
<a href="https://git.gensokyo.uk/security/hakurei/releases"><img src="https://img.shields.io/gitea/v/release/security/hakurei?gitea_url=https%3A%2F%2Fgit.gensokyo.uk&color=purple" alt="Release" /></a>
<a href="https://goreportcard.com/report/hakurei.app"><img src="https://goreportcard.com/badge/hakurei.app" alt="Go Report Card" /></a>
<a href="https://hakurei.app"><img src="https://img.shields.io/website?url=https%3A%2F%2Fhakurei.app" alt="Website" /></a>
</p>
Hakurei is a tool for running sandboxed graphical applications as dedicated subordinate users on the Linux kernel.
It also implements [planterette (WIP)](cmd/planterette), a self-contained Android-like package manager with modern security features.
It implements the application container of [planterette (WIP)](https://git.gensokyo.uk/security/planterette),
a self-contained Android-like package manager with modern security features.
## NixOS Module usage
+21 -13
View File
@@ -13,12 +13,12 @@ import (
"syscall"
"time"
"hakurei.app/cmd/hakurei/internal/app"
"hakurei.app/cmd/hakurei/internal/app/instance"
"hakurei.app/cmd/hakurei/internal/state"
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/app"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/system"
"hakurei.app/system/dbus"
@@ -33,7 +33,7 @@ func buildCommand(out io.Writer) command.Command {
Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")
c.Command("shim", command.UsageInternal, func([]string) error { instance.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 {
if len(args) < 1 {
@@ -95,7 +95,7 @@ func buildCommand(out io.Writer) command.Command {
Gid: us,
Username: "chronos",
Name: "Hakurei Permissive Default",
HomeDir: "/var/empty",
HomeDir: container.FHSVarEmpty,
}
} else {
passwd = u
@@ -115,21 +115,29 @@ func buildCommand(out io.Writer) command.Command {
config.Identity = aid
config.Groups = groups
config.Data = homeDir
config.Username = userName
if a, err := container.NewAbs(homeDir); err != nil {
log.Fatal(err.Error())
return err
} else {
config.Home = a
}
var e system.Enablement
if wayland {
config.Enablements |= system.EWayland
e |= system.EWayland
}
if x11 {
config.Enablements |= system.EX11
e |= system.EX11
}
if dBus {
config.Enablements |= system.EDBus
e |= system.EDBus
}
if pulse {
config.Enablements |= system.EPulse
e |= system.EPulse
}
config.Enablements = hst.NewEnablements(e)
// parse D-Bus config file from flags if applicable
if dBus {
@@ -213,7 +221,7 @@ func buildCommand(out io.Writer) command.Command {
var psFlagShort bool
c.NewCommand("ps", "List active instances", func(args []string) error {
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(std.Paths().RunDirPath), psFlagShort, flagJSON)
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(std.Paths().RunDirPath.String()), psFlagShort, flagJSON)
return errSuccess
}).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id")
@@ -244,14 +252,14 @@ func runApp(config *hst.Config) {
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer stop() // unreachable
a := instance.MustNew(instance.ISetuid, ctx, std)
a := app.MustNew(ctx, std)
rs := new(app.RunState)
if sa, err := a.Seal(config); err != nil {
hlog.PrintBaseError(err, "cannot seal app:")
internal.Exit(1)
} else {
internal.Exit(instance.PrintRunStateErr(instance.ISetuid, rs, sa.Run(rs)))
internal.Exit(app.PrintRunStateErr(rs, sa.Run(rs)))
}
*(*int)(nil) = 0 // not reached
@@ -1,192 +0,0 @@
package common
import (
"errors"
"fmt"
"io/fs"
"maps"
"path"
"syscall"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/hst"
"hakurei.app/internal/sys"
"hakurei.app/system/dbus"
)
// in practice there should be less than 30 entries added by the runtime;
// allocating slightly more as a margin for future expansion
const preallocateOpsCount = 1 << 5
// NewContainer initialises [sandbox.Params] via [hst.ContainerConfig].
// Note that remaining container setup must be queued by the caller.
func NewContainer(s *hst.ContainerConfig, os sys.State, uid, gid *int) (*container.Params, map[string]string, error) {
if s == nil {
return nil, nil, syscall.EBADE
}
params := &container.Params{
Hostname: s.Hostname,
SeccompFlags: s.SeccompFlags,
SeccompPresets: s.SeccompPresets,
RetainSession: s.Tty,
HostNet: s.Net,
}
{
ops := make(container.Ops, 0, preallocateOpsCount+len(s.Filesystem)+len(s.Link)+len(s.Cover))
params.Ops = &ops
}
if s.Multiarch {
params.SeccompFlags |= seccomp.AllowMultiarch
}
if !s.SeccompCompat {
params.SeccompPresets |= seccomp.PresetExt
}
if !s.Devel {
params.SeccompPresets |= seccomp.PresetDenyDevel
}
if !s.Userns {
params.SeccompPresets |= seccomp.PresetDenyNS
}
if !s.Tty {
params.SeccompPresets |= seccomp.PresetDenyTTY
}
if s.MapRealUID {
/* some programs fail to connect to dbus session running as a different uid
so this workaround is introduced to map priv-side caller uid in container */
params.Uid = os.Getuid()
*uid = params.Uid
params.Gid = os.Getgid()
*gid = params.Gid
} else {
*uid = container.OverflowUid()
*gid = container.OverflowGid()
}
params.
Proc("/proc").
Tmpfs(hst.Tmp, 1<<12, 0755)
if !s.Device {
params.Dev("/dev").Mqueue("/dev/mqueue")
} else {
params.Bind("/dev", "/dev", container.BindWritable|container.BindDevice)
}
/* retrieve paths and hide them if they're made available in the sandbox;
this feature tries to improve user experience of permissive defaults, and
to warn about issues in custom configuration; it is NOT a security feature
and should not be treated as such, ALWAYS be careful with what you bind */
var hidePaths []string
sc := os.Paths()
hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath)
_, systemBusAddr := dbus.Address()
if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
return nil, nil, err
} else {
// there is usually only one, do not preallocate
for _, entry := range entries {
if entry.Method != "unix" {
continue
}
for _, pair := range entry.Values {
if pair[0] == "path" {
if path.IsAbs(pair[1]) {
// get parent dir of socket
dir := path.Dir(pair[1])
if dir == "." || dir == "/" {
os.Printf("dbus socket %q is in an unusual location", pair[1])
}
hidePaths = append(hidePaths, dir)
} else {
os.Printf("dbus socket %q is not absolute", pair[1])
}
}
}
}
}
hidePathMatch := make([]bool, len(hidePaths))
for i := range hidePaths {
if err := evalSymlinks(os, &hidePaths[i]); err != nil {
return nil, nil, err
}
}
for _, c := range s.Filesystem {
if c == nil {
continue
}
if !path.IsAbs(c.Src) {
return nil, nil, fmt.Errorf("src path %q is not absolute", c.Src)
}
dest := c.Dst
if c.Dst == "" {
dest = c.Src
} else if !path.IsAbs(dest) {
return nil, nil, fmt.Errorf("dst path %q is not absolute", dest)
}
srcH := c.Src
if err := evalSymlinks(os, &srcH); err != nil {
return nil, nil, err
}
for i := range hidePaths {
// skip matched entries
if hidePathMatch[i] {
continue
}
if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil {
return nil, nil, err
} else if ok {
hidePathMatch[i] = true
os.Printf("hiding paths from %q", c.Src)
}
}
var flags int
if c.Write {
flags |= container.BindWritable
}
if c.Device {
flags |= container.BindDevice | container.BindWritable
}
if !c.Must {
flags |= container.BindOptional
}
params.Bind(c.Src, dest, flags)
}
// cover matched paths
for i, ok := range hidePathMatch {
if ok {
params.Tmpfs(hidePaths[i], 1<<13, 0755)
}
}
for _, l := range s.Link {
params.Link(l[0], l[1])
}
return params, maps.Clone(s.Env), nil
}
func evalSymlinks(os sys.State, v *string) error {
if p, err := os.EvalSymlinks(*v); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return err
}
os.Printf("path %q does not yet exist", *v)
} else {
*v = p
}
return nil
}
@@ -1,17 +0,0 @@
package instance
import (
"syscall"
"hakurei.app/cmd/hakurei/internal/app"
"hakurei.app/cmd/hakurei/internal/app/internal/setuid"
)
func PrintRunStateErr(whence int, rs *app.RunState, runErr error) (code int) {
switch whence {
case ISetuid:
return setuid.PrintRunStateErr(rs, runErr)
default:
panic(syscall.EINVAL)
}
}
-33
View File
@@ -1,33 +0,0 @@
// Package instance exposes cross-package implementation details and provides constructors for builtin implementations.
package instance
import (
"context"
"log"
"syscall"
"hakurei.app/cmd/hakurei/internal/app"
"hakurei.app/cmd/hakurei/internal/app/internal/setuid"
"hakurei.app/internal/sys"
)
const (
ISetuid = iota
)
func New(whence int, ctx context.Context, os sys.State) (app.App, error) {
switch whence {
case ISetuid:
return setuid.New(ctx, os)
default:
return nil, syscall.EINVAL
}
}
func MustNew(whence int, ctx context.Context, os sys.State) app.App {
a, err := New(whence, ctx, os)
if err != nil {
log.Fatalf("cannot create app: %v", err)
}
return a
}
@@ -1,6 +0,0 @@
package instance
import "hakurei.app/cmd/hakurei/internal/app/internal/setuid"
// ShimMain is the main function of the shim process and runs as the unconstrained target user.
func ShimMain() { setuid.ShimMain() }
@@ -1,184 +0,0 @@
package setuid
import (
"context"
"errors"
"log"
"os"
"os/exec"
"os/signal"
"syscall"
"time"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
)
/*
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <signal.h>
static pid_t hakurei_shim_param_ppid = -1;
// this cannot unblock hlog since Go code is not async-signal-safe
static void hakurei_shim_sigaction(int sig, siginfo_t *si, void *ucontext) {
if (sig != SIGCONT || si == NULL) {
// unreachable
fprintf(stderr, "sigaction: sa_sigaction got invalid siginfo\n");
return;
}
// monitor requests shim exit
if (si->si_pid == hakurei_shim_param_ppid)
exit(254);
fprintf(stderr, "sigaction: got SIGCONT from process %d\n", si->si_pid);
// shim orphaned before monitor delivers a signal
if (getppid() != hakurei_shim_param_ppid)
exit(3);
}
void hakurei_shim_setup_cont_signal(pid_t ppid) {
struct sigaction new_action = {0}, old_action = {0};
if (sigaction(SIGCONT, NULL, &old_action) != 0)
return;
if (old_action.sa_handler != SIG_DFL) {
errno = ENOTRECOVERABLE;
return;
}
new_action.sa_sigaction = hakurei_shim_sigaction;
if (sigemptyset(&new_action.sa_mask) != 0)
return;
new_action.sa_flags = SA_ONSTACK | SA_SIGINFO;
if (sigaction(SIGCONT, &new_action, NULL) != 0)
return;
errno = 0;
hakurei_shim_param_ppid = ppid;
}
*/
import "C"
const shimEnv = "HAKUREI_SHIM"
type shimParams struct {
// monitor pid, checked against ppid in signal handler
Monitor int
// finalised container params
Container *container.Params
// path to outer home directory
Home string
// verbosity pass through
Verbose bool
}
// ShimMain is the main function of the shim process and runs as the unconstrained target user.
func ShimMain() {
hlog.Prepare("shim")
if err := container.SetDumpable(container.SUID_DUMP_DISABLE); err != nil {
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
}
var (
params shimParams
closeSetup func() error
)
if f, err := container.Receive(shimEnv, &params, nil); err != nil {
if errors.Is(err, container.ErrInvalid) {
log.Fatal("invalid config descriptor")
}
if errors.Is(err, container.ErrNotSet) {
log.Fatal("HAKUREI_SHIM not set")
}
log.Fatalf("cannot receive shim setup params: %v", err)
} else {
internal.InstallOutput(params.Verbose)
closeSetup = f
// the Go runtime does not expose siginfo_t so SIGCONT is handled in C to check si_pid
if _, err = C.hakurei_shim_setup_cont_signal(C.pid_t(params.Monitor)); err != nil {
log.Fatalf("cannot install SIGCONT handler: %v", err)
}
// pdeath_signal delivery is checked as if the dying process called kill(2), see kernel/exit.c
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGCONT), 0); errno != 0 {
log.Fatalf("cannot set parent-death signal: %v", errno)
}
}
if params.Container == nil || params.Container.Ops == nil {
log.Fatal("invalid container params")
}
// close setup socket
if err := closeSetup(); err != nil {
log.Printf("cannot close setup pipe: %v", err)
// not fatal
}
// ensure home directory as target user
if s, err := os.Stat(params.Home); err != nil {
if os.IsNotExist(err) {
if err = os.Mkdir(params.Home, 0700); err != nil {
log.Fatalf("cannot create home directory: %v", err)
}
} else {
log.Fatalf("cannot access home directory: %v", err)
}
// home directory is created, proceed
} else if !s.IsDir() {
log.Fatalf("path %q is not a directory", params.Home)
}
var name string
if len(params.Container.Args) > 0 {
name = params.Container.Args[0]
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop() // unreachable
z := container.New(ctx, name)
z.Params = *params.Container
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
z.Cancel = func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) }
z.WaitDelay = 2 * time.Second
if err := z.Start(); err != nil {
hlog.PrintBaseError(err, "cannot start container:")
os.Exit(1)
}
if err := z.Serve(); err != nil {
hlog.PrintBaseError(err, "cannot configure container:")
}
if err := seccomp.Load(
seccomp.Preset(seccomp.PresetStrict, seccomp.AllowMultiarch),
seccomp.AllowMultiarch,
); err != nil {
log.Fatalf("cannot load syscall filter: %v", err)
}
if err := z.Wait(); err != nil {
var exitError *exec.ExitError
if !errors.As(err, &exitError) {
if errors.Is(err, context.Canceled) {
os.Exit(2)
}
log.Printf("wait: %v", err)
os.Exit(127)
}
os.Exit(exitError.ExitCode())
}
}
+5
View File
@@ -30,6 +30,11 @@ func main() {
// early init path, skips root check and duplicate PR_SET_DUMPABLE
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
if err := container.SetPtracer(0); err != nil {
hlog.Verbosef("cannot enable ptrace protection via Yama LSM: %v", err)
// not fatal: this program runs as the privileged user
}
if err := container.SetDumpable(container.SUID_DUMP_DISABLE); err != nil {
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
// not fatal: this program runs as the privileged user
+2 -2
View File
@@ -10,8 +10,8 @@ import (
"strings"
"syscall"
"hakurei.app/cmd/hakurei/internal/state"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
)
@@ -87,7 +87,7 @@ func tryShort(name string) (config *hst.Config, entry *state.State) {
if likePrefix && len(name) >= 8 {
hlog.Verbose("argument looks like prefix")
s := state.NewMulti(std.Paths().RunDirPath)
s := state.NewMulti(std.Paths().RunDirPath.String())
if entries, err := state.Join(s); err != nil {
log.Printf("cannot join store: %v", err)
// drop to fetch from file
+25 -48
View File
@@ -12,8 +12,8 @@ import (
"text/tabwriter"
"time"
"hakurei.app/cmd/hakurei/internal/state"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/system/dbus"
)
@@ -22,9 +22,9 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
t := newPrinter(output)
defer t.MustFlush()
info := new(hst.Info)
info := &hst.Info{Paths: std.Paths()}
// get fid by querying uid of aid 0
// get hid by querying uid of identity 0
if uid, err := std.Uid(0); err != nil {
hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:")
os.Exit(1)
@@ -38,6 +38,10 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
}
t.Printf("User:\t%d\n", info.User)
t.Printf("TempDir:\t%s\n", info.TempDir)
t.Printf("SharePath:\t%s\n", info.SharePath)
t.Printf("RuntimePath:\t%s\n", info.RuntimePath)
t.Printf("RunDirPath:\t%s\n", info.RunDirPath)
}
func printShowInstance(
@@ -73,17 +77,17 @@ func printShowInstance(
} else {
t.Printf(" Identity:\t%d\n", config.Identity)
}
t.Printf(" Enablements:\t%s\n", config.Enablements.String())
t.Printf(" Enablements:\t%s\n", config.Enablements.Unwrap().String())
if len(config.Groups) > 0 {
t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", "))
}
if config.Data != "" {
t.Printf(" Data:\t%s\n", config.Data)
if config.Home != nil {
t.Printf(" Home:\t%s\n", config.Home)
}
if config.Container != nil {
container := config.Container
if container.Hostname != "" {
t.Printf(" Hostname:\t%s\n", container.Hostname)
params := config.Container
if params.Hostname != "" {
t.Printf(" Hostname:\t%s\n", params.Hostname)
}
flags := make([]string, 0, 7)
writeFlag := func(name string, value bool) {
@@ -91,30 +95,22 @@ func printShowInstance(
flags = append(flags, name)
}
}
writeFlag("userns", container.Userns)
writeFlag("devel", container.Devel)
writeFlag("net", container.Net)
writeFlag("device", container.Device)
writeFlag("tty", container.Tty)
writeFlag("mapuid", container.MapRealUID)
writeFlag("userns", params.Userns)
writeFlag("devel", params.Devel)
writeFlag("net", params.HostNet)
writeFlag("abstract", params.HostAbstract)
writeFlag("device", params.Device)
writeFlag("tty", params.Tty)
writeFlag("mapuid", params.MapRealUID)
writeFlag("directwl", config.DirectWayland)
writeFlag("autoetc", container.AutoEtc)
if len(flags) == 0 {
flags = append(flags, "none")
}
t.Printf(" Flags:\t%s\n", strings.Join(flags, " "))
etc := container.Etc
if etc == "" {
etc = "/etc"
if config.Path != nil {
t.Printf(" Path:\t%s\n", config.Path)
}
t.Printf(" Etc:\t%s\n", etc)
if len(container.Cover) > 0 {
t.Printf(" Cover:\t%s\n", strings.Join(container.Cover, " "))
}
t.Printf(" Path:\t%s\n", config.Path)
}
if len(config.Args) > 0 {
t.Printf(" Arguments:\t%s\n", strings.Join(config.Args, " "))
@@ -125,30 +121,11 @@ func printShowInstance(
if config.Container != nil && len(config.Container.Filesystem) > 0 {
t.Printf("Filesystem\n")
for _, f := range config.Container.Filesystem {
if f == nil {
if !f.Valid() {
t.Println(" <invalid>")
continue
}
expr := new(strings.Builder)
expr.Grow(3 + len(f.Src) + 1 + len(f.Dst))
if f.Device {
expr.WriteString(" d")
} else if f.Write {
expr.WriteString(" w")
} else {
expr.WriteString(" ")
}
if f.Must {
expr.WriteString("*")
} else {
expr.WriteString("+")
}
expr.WriteString(f.Src)
if f.Dst != "" {
expr.WriteString(":" + f.Dst)
}
t.Printf("%s\n", expr.String())
t.Printf(" %s\n", f)
}
t.Printf("\n")
}
+181 -93
View File
@@ -5,14 +5,13 @@ import (
"testing"
"time"
"hakurei.app/cmd/hakurei/internal/app"
"hakurei.app/cmd/hakurei/internal/state"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/system/dbus"
)
var (
testID = app.ID{
testID = state.ID{
0x8e, 0x2c, 0x76, 0xb0,
0x66, 0xda, 0xbe, 0x57,
0x4c, 0xf0, 0x73, 0xbd,
@@ -40,20 +39,21 @@ func Test_printShowInstance(t *testing.T) {
Identity: 9 (org.chromium.Chromium)
Enablements: wayland, dbus, pulseaudio
Groups: video, dialout, plugdev
Data: /var/lib/hakurei/u0/org.chromium.Chromium
Home: /data/data/org.chromium.Chromium
Hostname: localhost
Flags: userns devel net device tty mapuid autoetc
Etc: /etc
Cover: /var/run/nscd
Flags: userns devel net abstract device tty mapuid
Path: /run/current-system/sw/bin/chromium
Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
Filesystem
+/nix/store
+/run/current-system
+/run/opengl-driver
+/var/db/nix-channels
w*/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
autoroot:w:/var/lib/hakurei/base/org.debian
autoetc:/etc/
w+ephemeral(-rwxr-xr-x):/tmp/
w*/nix/store:/mnt-root/nix/.rw-store/upper:/mnt-root/nix/.rw-store/work:/mnt-root/nix/.ro-store
*/nix/store
/run/current-system@
/run/opengl-driver@
w-/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
d+/dev/dri
Extra ACL
@@ -83,18 +83,15 @@ App
Identity: 0
Enablements: (no enablements)
Flags: none
Etc: /etc
Path:
`},
{"config nil entries", nil, &hst.Config{Container: &hst.ContainerConfig{Filesystem: make([]*hst.FilesystemConfig, 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, `App
Identity: 0
Enablements: (no enablements)
Flags: none
Etc: /etc
Path:
Filesystem
<invalid>
Extra ACL
@@ -119,20 +116,21 @@ App
Identity: 9 (org.chromium.Chromium)
Enablements: wayland, dbus, pulseaudio
Groups: video, dialout, plugdev
Data: /var/lib/hakurei/u0/org.chromium.Chromium
Home: /data/data/org.chromium.Chromium
Hostname: localhost
Flags: userns devel net device tty mapuid autoetc
Etc: /etc
Cover: /var/run/nscd
Flags: userns devel net abstract device tty mapuid
Path: /run/current-system/sw/bin/chromium
Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
Filesystem
+/nix/store
+/run/current-system
+/run/opengl-driver
+/var/db/nix-channels
w*/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
autoroot:w:/var/lib/hakurei/base/org.debian
autoetc:/etc/
w+ephemeral(-rwxr-xr-x):/tmp/
w*/nix/store:/mnt-root/nix/.rw-store/upper:/mnt-root/nix/.rw-store/work:/mnt-root/nix/.ro-store
*/nix/store
/run/current-system@
/run/opengl-driver@
w-/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
d+/dev/dri
Extra ACL
@@ -195,7 +193,11 @@ App
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"enablements": 13,
"enablements": {
"wayland": true,
"dbus": true,
"pulse": true
},
"session_bus": {
"see": null,
"talk": [
@@ -234,8 +236,7 @@ App
},
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"data": "/var/lib/hakurei/u0/org.chromium.Chromium",
"dir": "/data/data/org.chromium.Chromium",
"home": "/data/data/org.chromium.Chromium",
"extra_perms": [
{
"ensure": true,
@@ -257,11 +258,14 @@ App
],
"container": {
"hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1,
"seccomp_presets": 1,
"seccomp_compat": true,
"devel": true,
"userns": true,
"net": true,
"host_net": true,
"host_abstract": true,
"tty": true,
"multiarch": true,
"env": {
@@ -273,38 +277,62 @@ App
"device": true,
"filesystem": [
{
"type": "bind",
"dst": "/",
"src": "/var/lib/hakurei/base/org.debian",
"write": true,
"special": true
},
{
"type": "bind",
"dst": "/etc/",
"src": "/etc/",
"special": true
},
{
"type": "ephemeral",
"dst": "/tmp/",
"write": true,
"perm": 493
},
{
"type": "overlay",
"dst": "/nix/store",
"lower": [
"/mnt-root/nix/.ro-store"
],
"upper": "/mnt-root/nix/.rw-store/upper",
"work": "/mnt-root/nix/.rw-store/work"
},
{
"type": "bind",
"src": "/nix/store"
},
{
"src": "/run/current-system"
"type": "link",
"dst": "/run/current-system",
"linkname": "/run/current-system",
"dereference": true
},
{
"src": "/run/opengl-driver"
},
{
"src": "/var/db/nix-channels"
"type": "link",
"dst": "/run/opengl-driver",
"linkname": "/run/opengl-driver",
"dereference": true
},
{
"type": "bind",
"dst": "/data/data/org.chromium.Chromium",
"src": "/var/lib/hakurei/u0/org.chromium.Chromium",
"write": true,
"require": true
"ensure": true
},
{
"type": "bind",
"src": "/dev/dri",
"dev": true
"dev": true,
"optional": true
}
],
"symlink": [
[
"/run/user/65534",
"/run/user/150"
]
],
"etc": "/etc",
"auto_etc": true,
"cover": [
"/var/run/nscd"
]
}
},
@@ -321,7 +349,11 @@ App
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"enablements": 13,
"enablements": {
"wayland": true,
"dbus": true,
"pulse": true
},
"session_bus": {
"see": null,
"talk": [
@@ -360,8 +392,7 @@ App
},
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"data": "/var/lib/hakurei/u0/org.chromium.Chromium",
"dir": "/data/data/org.chromium.Chromium",
"home": "/data/data/org.chromium.Chromium",
"extra_perms": [
{
"ensure": true,
@@ -383,11 +414,14 @@ App
],
"container": {
"hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1,
"seccomp_presets": 1,
"seccomp_compat": true,
"devel": true,
"userns": true,
"net": true,
"host_net": true,
"host_abstract": true,
"tty": true,
"multiarch": true,
"env": {
@@ -399,38 +433,62 @@ App
"device": true,
"filesystem": [
{
"type": "bind",
"dst": "/",
"src": "/var/lib/hakurei/base/org.debian",
"write": true,
"special": true
},
{
"type": "bind",
"dst": "/etc/",
"src": "/etc/",
"special": true
},
{
"type": "ephemeral",
"dst": "/tmp/",
"write": true,
"perm": 493
},
{
"type": "overlay",
"dst": "/nix/store",
"lower": [
"/mnt-root/nix/.ro-store"
],
"upper": "/mnt-root/nix/.rw-store/upper",
"work": "/mnt-root/nix/.rw-store/work"
},
{
"type": "bind",
"src": "/nix/store"
},
{
"src": "/run/current-system"
"type": "link",
"dst": "/run/current-system",
"linkname": "/run/current-system",
"dereference": true
},
{
"src": "/run/opengl-driver"
},
{
"src": "/var/db/nix-channels"
"type": "link",
"dst": "/run/opengl-driver",
"linkname": "/run/opengl-driver",
"dereference": true
},
{
"type": "bind",
"dst": "/data/data/org.chromium.Chromium",
"src": "/var/lib/hakurei/u0/org.chromium.Chromium",
"write": true,
"require": true
"ensure": true
},
{
"type": "bind",
"src": "/dev/dri",
"dev": true
"dev": true,
"optional": true
}
],
"symlink": [
[
"/run/user/65534",
"/run/user/150"
]
],
"etc": "/etc",
"auto_etc": true,
"cover": [
"/var/run/nscd"
]
}
}
@@ -460,7 +518,7 @@ func Test_printPs(t *testing.T) {
{"no entries", make(state.Entries), false, false, " Instance PID Application Uptime\n"},
{"no entries short", make(state.Entries), true, false, ""},
{"nil instance", state.Entries{testID: nil}, false, false, " Instance PID Application Uptime\n"},
{"state corruption", state.Entries{app.ID{}: testState}, false, false, " Instance PID Application Uptime\n"},
{"state corruption", state.Entries{state.ID{}: testState}, false, false, " Instance PID Application Uptime\n"},
{"valid pd", state.Entries{testID: &state.State{ID: testID, PID: 1 << 8, Config: new(hst.Config), Time: testAppTime}}, false, false, ` Instance PID Application Uptime
8e2c76b0 256 0 (app.hakurei.8e2c76b0) 1h2m32s
@@ -501,7 +559,11 @@ func Test_printPs(t *testing.T) {
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"enablements": 13,
"enablements": {
"wayland": true,
"dbus": true,
"pulse": true
},
"session_bus": {
"see": null,
"talk": [
@@ -540,8 +602,7 @@ func Test_printPs(t *testing.T) {
},
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"data": "/var/lib/hakurei/u0/org.chromium.Chromium",
"dir": "/data/data/org.chromium.Chromium",
"home": "/data/data/org.chromium.Chromium",
"extra_perms": [
{
"ensure": true,
@@ -563,11 +624,14 @@ func Test_printPs(t *testing.T) {
],
"container": {
"hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1,
"seccomp_presets": 1,
"seccomp_compat": true,
"devel": true,
"userns": true,
"net": true,
"host_net": true,
"host_abstract": true,
"tty": true,
"multiarch": true,
"env": {
@@ -579,38 +643,62 @@ func Test_printPs(t *testing.T) {
"device": true,
"filesystem": [
{
"type": "bind",
"dst": "/",
"src": "/var/lib/hakurei/base/org.debian",
"write": true,
"special": true
},
{
"type": "bind",
"dst": "/etc/",
"src": "/etc/",
"special": true
},
{
"type": "ephemeral",
"dst": "/tmp/",
"write": true,
"perm": 493
},
{
"type": "overlay",
"dst": "/nix/store",
"lower": [
"/mnt-root/nix/.ro-store"
],
"upper": "/mnt-root/nix/.rw-store/upper",
"work": "/mnt-root/nix/.rw-store/work"
},
{
"type": "bind",
"src": "/nix/store"
},
{
"src": "/run/current-system"
"type": "link",
"dst": "/run/current-system",
"linkname": "/run/current-system",
"dereference": true
},
{
"src": "/run/opengl-driver"
},
{
"src": "/var/db/nix-channels"
"type": "link",
"dst": "/run/opengl-driver",
"linkname": "/run/opengl-driver",
"dereference": true
},
{
"type": "bind",
"dst": "/data/data/org.chromium.Chromium",
"src": "/var/lib/hakurei/u0/org.chromium.Chromium",
"write": true,
"require": true
"ensure": true
},
{
"type": "bind",
"src": "/dev/dri",
"dev": true
"dev": true,
"optional": true
}
],
"symlink": [
[
"/run/user/65534",
"/run/user/150"
]
],
"etc": "/etc",
"auto_etc": true,
"cover": [
"/var/run/nscd"
]
}
},
+7
View File
@@ -0,0 +1,7 @@
This program is a proof of concept and is now deprecated. It is only kept
around for API demonstration purposes and to make the most out of the test
suite.
This program is replaced by planterette, which can be found at
https://git.gensokyo.uk/security/planterette. Development effort should be
focused there instead.
+41 -34
View File
@@ -4,11 +4,10 @@ import (
"encoding/json"
"log"
"os"
"path"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/hst"
"hakurei.app/system"
"hakurei.app/system/dbus"
)
@@ -27,7 +26,9 @@ type appInfo struct {
// passed through to [hst.Config]
Userns bool `json:"userns,omitempty"`
// passed through to [hst.Config]
Net bool `json:"net,omitempty"`
HostNet bool `json:"net,omitempty"`
// passed through to [hst.Config]
HostAbstract bool `json:"abstract,omitempty"`
// passed through to [hst.Config]
Device bool `json:"dev,omitempty"`
// passed through to [hst.Config]
@@ -41,7 +42,7 @@ type appInfo struct {
// passed through to [hst.Config]
SessionBus *dbus.Config `json:"session_bus,omitempty"`
// passed through to [hst.Config]
Enablements system.Enablement `json:"enablements"`
Enablements *hst.Enablements `json:"enablements,omitempty"`
// passed through to [hst.Config]
Multiarch bool `json:"multiarch,omitempty"`
@@ -55,18 +56,18 @@ type appInfo struct {
// store path to nixGL source
NixGL string `json:"nix_gl,omitempty"`
// store path to activate-and-exec script
Launcher string `json:"launcher"`
Launcher *container.Absolute `json:"launcher"`
// store path to /run/current-system
CurrentSystem string `json:"current_system"`
CurrentSystem *container.Absolute `json:"current_system"`
// store path to home-manager activation package
ActivationPackage string `json:"activation_package"`
}
func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool) *hst.Config {
func (app *appInfo) toHst(pathSet *appPathSet, pathname *container.Absolute, argv []string, flagDropShell bool) *hst.Config {
config := &hst.Config{
ID: app.ID,
Path: argv[0],
Path: pathname,
Args: argv,
Enablements: app.Enablements,
@@ -76,38 +77,36 @@ func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool
DirectWayland: app.DirectWayland,
Username: "hakurei",
Shell: shellPath,
Data: pathSet.homeDir,
Dir: path.Join("/data/data", app.ID),
Shell: pathShell,
Home: pathDataData.Append(app.ID),
Identity: app.Identity,
Groups: app.Groups,
Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name),
Devel: app.Devel,
Userns: app.Userns,
Net: app.Net,
Device: app.Device,
Tty: app.Tty || flagDropShell,
MapRealUID: app.MapRealUID,
Filesystem: []*hst.FilesystemConfig{
{Src: path.Join(pathSet.nixPath, "store"), Dst: "/nix/store", Must: true},
{Src: pathSet.metaPath, Dst: path.Join(hst.Tmp, "app"), Must: true},
{Src: "/etc/resolv.conf"},
{Src: "/sys/block"},
{Src: "/sys/bus"},
{Src: "/sys/class"},
{Src: "/sys/dev"},
{Src: "/sys/devices"},
Hostname: formatHostname(app.Name),
Devel: app.Devel,
Userns: app.Userns,
HostNet: app.HostNet,
HostAbstract: app.HostAbstract,
Device: app.Device,
Tty: app.Tty || flagDropShell,
MapRealUID: app.MapRealUID,
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append("store"), Target: pathNixStore}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.metaPath, Target: hst.AbsTmp.Append("app")}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSEtc.Append("resolv.conf"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("block"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("bus"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("class"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("dev"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("devices"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID), Source: pathSet.homeDir, Write: true, Ensure: true}},
},
Link: [][2]string{
{app.CurrentSystem, "/run/current-system"},
{"/run/current-system/sw/bin", "/bin"},
{"/run/current-system/sw/bin", "/usr/bin"},
},
Etc: path.Join(pathSet.cacheDir, "etc"),
AutoEtc: true,
},
ExtraPerms: []*hst.ExtraPermConfig{
{Path: dataHome, Execute: true},
@@ -140,6 +139,14 @@ func loadAppInfo(name string, beforeFail func()) *appInfo {
beforeFail()
log.Fatal("application identifier must not be empty")
}
if bundle.Launcher == nil {
beforeFail()
log.Fatal("launcher must not be empty")
}
if bundle.CurrentSystem == nil {
beforeFail()
log.Fatal("current-system must not be empty")
}
return bundle
}
@@ -171,7 +171,12 @@ let
broadcast = { };
});
enablements = (if allow_wayland then 1 else 0) + (if allow_x11 then 2 else 0) + (if allow_dbus then 4 else 0) + (if allow_pulse then 8 else 0);
enablements = {
wayland = allow_wayland;
x11 = allow_x11;
dbus = allow_dbus;
pulse = allow_pulse;
};
mesa = if gpu then mesaWrappers else null;
nix_gl = if gpu then nixGL else null;
@@ -215,15 +220,14 @@ stdenv.mkDerivation {
# create binary cache
closureInfo="${
closureInfo {
rootPaths =
[
homeManagerConfiguration.activationPackage
launcher
]
++ optionals gpu [
mesaWrappers
nixGL
];
rootPaths = [
homeManagerConfiguration.activationPackage
launcher
]
++ optionals gpu [
mesaWrappers
nixGL
];
}
}"
echo "copying application paths..."
@@ -11,20 +11,19 @@ import (
"syscall"
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
)
const shellPath = "/run/current-system/sw/bin/bash"
var (
errSuccess = errors.New("success")
)
func init() {
hlog.Prepare("planterette")
if err := os.Setenv("SHELL", shellPath); err != nil {
hlog.Prepare("hpkg")
if err := os.Setenv("SHELL", pathShell.String()); err != nil {
log.Fatalf("cannot set $SHELL: %v", err)
}
}
@@ -42,7 +41,7 @@ func main() {
flagVerbose bool
flagDropShell bool
)
c := command.New(os.Stderr, log.Printf, "planterette", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
c := command.New(os.Stderr, log.Printf, "hpkg", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next hakurei action")
@@ -66,7 +65,7 @@ func main() {
}
/*
Look up paths to programs started by planterette.
Look up paths to programs started by hpkg.
This is done here to ease error handling as cleanup is not yet required.
*/
@@ -81,31 +80,32 @@ func main() {
Extract package and set up for cleanup.
*/
var workDir string
if p, err := os.MkdirTemp("", "planterette.*"); err != nil {
var workDir *container.Absolute
if p, err := os.MkdirTemp("", "hpkg.*"); err != nil {
log.Printf("cannot create temporary directory: %v", err)
return err
} else {
workDir = p
} else if workDir, err = container.NewAbs(p); err != nil {
log.Printf("invalid temporary directory: %v", err)
return err
}
cleanup := func() {
// should be faster than a native implementation
mustRun(chmod, "-R", "+w", workDir)
mustRun(rm, "-rf", workDir)
mustRun(chmod, "-R", "+w", workDir.String())
mustRun(rm, "-rf", workDir.String())
}
beforeRunFail.Store(&cleanup)
mustRun(tar, "-C", workDir, "-xf", pkgPath)
mustRun(tar, "-C", workDir.String(), "-xf", pkgPath)
/*
Parse bundle and app metadata, do pre-install checks.
*/
bundle := loadAppInfo(path.Join(workDir, "bundle.json"), cleanup)
bundle := loadAppInfo(path.Join(workDir.String(), "bundle.json"), cleanup)
pathSet := pathSetByApp(bundle.ID)
a := bundle
if s, err := os.Stat(pathSet.metaPath); err != nil {
if s, err := os.Stat(pathSet.metaPath.String()); err != nil {
if !os.IsNotExist(err) {
cleanup()
log.Printf("cannot access %q: %v", pathSet.metaPath, err)
@@ -117,7 +117,7 @@ func main() {
log.Printf("metadata path %q is not a file", pathSet.metaPath)
return syscall.EBADMSG
} else {
a = loadAppInfo(pathSet.metaPath, cleanup)
a = loadAppInfo(pathSet.metaPath.String(), cleanup)
if a.ID != bundle.ID {
cleanup()
log.Printf("app %q claims to have identifier %q",
@@ -208,7 +208,7 @@ func main() {
*/
// serialise metadata to ensure consistency
if f, err := os.OpenFile(pathSet.metaPath+"~", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil {
if f, err := os.OpenFile(pathSet.metaPath.String()+"~", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil {
cleanup()
log.Printf("cannot create metadata file: %v", err)
return err
@@ -221,7 +221,7 @@ func main() {
// not fatal
}
if err := os.Rename(pathSet.metaPath+"~", pathSet.metaPath); err != nil {
if err := os.Rename(pathSet.metaPath.String()+"~", pathSet.metaPath.String()); err != nil {
cleanup()
log.Printf("cannot rename metadata file: %v", err)
return err
@@ -250,7 +250,7 @@ func main() {
id := args[0]
pathSet := pathSetByApp(id)
a := loadAppInfo(pathSet.metaPath, func() {})
a := loadAppInfo(pathSet.metaPath.String(), func() {})
if a.ID != id {
log.Printf("app %q claims to have identifier %q", id, a.ID)
return syscall.EBADE
@@ -274,13 +274,13 @@ func main() {
"--override-input nixpkgs path:/etc/nixpkgs " +
"path:" + a.NixGL + "#nixVulkanNvidia",
}, true, func(config *hst.Config) *hst.Config {
config.Container.Filesystem = append(config.Container.Filesystem, []*hst.FilesystemConfig{
{Src: "/etc/resolv.conf"},
{Src: "/sys/block"},
{Src: "/sys/bus"},
{Src: "/sys/class"},
{Src: "/sys/dev"},
{Src: "/sys/devices"},
config.Container.Filesystem = append(config.Container.Filesystem, []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSEtc.Append("resolv.conf"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("block"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("bus"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("class"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("dev"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("devices"), Optional: true}},
}...)
appendGPUFilesystem(config)
return config
@@ -291,15 +291,16 @@ func main() {
Create app configuration.
*/
pathname := a.Launcher
argv := make([]string, 1, len(args))
if !flagDropShell {
argv[0] = a.Launcher
if flagDropShell {
pathname = pathShell
argv[0] = bash
} else {
argv[0] = shellPath
argv[0] = a.Launcher.String()
}
argv = append(argv, args[1:]...)
config := a.toFst(pathSet, argv, flagDropShell)
config := a.toHst(pathSet, pathname, argv, flagDropShell)
/*
Expose GPU devices.
@@ -307,7 +308,7 @@ func main() {
if a.GPU {
config.Container.Filesystem = append(config.Container.Filesystem,
&hst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(hst.Tmp, "nixGL")})
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append(".nixGL"), Target: hst.AbsTmp.Append("nixGL")}})
appendGPUFilesystem(config)
}
+116
View File
@@ -0,0 +1,116 @@
package main
import (
"log"
"os"
"os/exec"
"strconv"
"sync/atomic"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal/hlog"
)
const bash = "bash"
var (
dataHome *container.Absolute
)
func init() {
// dataHome
if a, err := container.NewAbs(os.Getenv("HAKUREI_DATA_HOME")); err == nil {
dataHome = a
} else {
dataHome = container.AbsFHSVarLib.Append("hakurei/" + strconv.Itoa(os.Getuid()))
}
}
var (
pathBin = container.AbsFHSRoot.Append("bin")
pathNix = container.MustAbs("/nix/")
pathNixStore = pathNix.Append("store/")
pathCurrentSystem = container.AbsFHSRun.Append("current-system")
pathSwBin = pathCurrentSystem.Append("sw/bin/")
pathShell = pathSwBin.Append(bash)
pathData = container.MustAbs("/data")
pathDataData = pathData.Append("data")
)
func lookPath(file string) string {
if p, err := exec.LookPath(file); err != nil {
log.Fatalf("%s: command not found", file)
return ""
} else {
return p
}
}
var beforeRunFail = new(atomic.Pointer[func()])
func mustRun(name string, arg ...string) {
hlog.Verbosef("spawning process: %q %q", name, arg)
cmd := exec.Command(name, arg...)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
if f := beforeRunFail.Swap(nil); f != nil {
(*f)()
}
log.Fatalf("%s: %v", name, err)
}
}
type appPathSet struct {
// ${dataHome}/${id}
baseDir *container.Absolute
// ${baseDir}/app
metaPath *container.Absolute
// ${baseDir}/files
homeDir *container.Absolute
// ${baseDir}/cache
cacheDir *container.Absolute
// ${baseDir}/cache/nix
nixPath *container.Absolute
}
func pathSetByApp(id string) *appPathSet {
pathSet := new(appPathSet)
pathSet.baseDir = dataHome.Append(id)
pathSet.metaPath = pathSet.baseDir.Append("app")
pathSet.homeDir = pathSet.baseDir.Append("files")
pathSet.cacheDir = pathSet.baseDir.Append("cache")
pathSet.nixPath = pathSet.cacheDir.Append("nix")
return pathSet
}
func appendGPUFilesystem(config *hst.Config) {
config.Container.Filesystem = append(config.Container.Filesystem, []hst.FilesystemConfigJSON{
// flatpak commit 763a686d874dd668f0236f911de00b80766ffe79
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("dri"), Device: true, Optional: true}},
// mali
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("mali"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("mali0"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("umplock"), Device: true, Optional: true}},
// nvidia
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidiactl"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia-modeset"), Device: true, Optional: true}},
// nvidia OpenCL/CUDA
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia-uvm"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia-uvm-tools"), Device: true, Optional: true}},
// flatpak commit d2dff2875bb3b7e2cd92d8204088d743fd07f3ff
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia0"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia1"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia2"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia3"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia4"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia5"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia6"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia7"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia8"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia9"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia10"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia11"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia12"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia13"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia14"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia15"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia16"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia17"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia18"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia19"), Device: true, Optional: true}},
}...)
}
@@ -9,7 +9,7 @@ let
buildPackage = self.buildPackage.${system};
in
nixosTest {
name = "planterette";
name = "hpkg";
nodes.machine = {
environment.etc = {
"foot.pkg".source = callPackage ./foot.nix { inherit buildPackage; };
@@ -79,20 +79,20 @@ print(machine.succeed("sudo -u alice -i hakurei version"))
machine.wait_for_file("/run/user/1000/wayland-1")
machine.wait_for_file("/tmp/sway-ipc.sock")
# Prepare planterette directory:
# Prepare hpkg directory:
machine.succeed("install -dm 0700 -o alice -g users /var/lib/hakurei/1000")
# Install planterette app:
swaymsg("exec planterette -v install /etc/foot.pkg && touch /tmp/planterette-install-ok")
machine.wait_for_file("/tmp/planterette-install-ok")
# Install hpkg app:
swaymsg("exec hpkg -v install /etc/foot.pkg && touch /tmp/hpkg-install-ok")
machine.wait_for_file("/tmp/hpkg-install-ok")
# Start app (foot) with Wayland enablement:
swaymsg("exec planterette -v start org.codeberg.dnkl.foot")
swaymsg("exec hpkg -v start org.codeberg.dnkl.foot")
wait_for_window("hakurei@machine-foot")
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
machine.wait_for_file("/tmp/hakurei.1000/tmpdir/2/success-client")
machine.wait_for_file("/tmp/hakurei.0/tmpdir/2/success-client")
collect_state_ui("app_wayland")
check_state("foot", 13)
check_state("foot", {"wayland": True, "dbus": True, "pulse": True})
# Verify acl on XDG_RUNTIME_DIR:
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002"))
machine.send_chars("exit\n")
@@ -2,9 +2,9 @@ package main
import (
"context"
"path"
"strings"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/hst"
"hakurei.app/internal"
@@ -18,8 +18,8 @@ func withNixDaemon(
mustRunAppDropShell(ctx, updateConfig(&hst.Config{
ID: app.ID,
Path: shellPath,
Args: []string{shellPath, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
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
@@ -32,9 +32,8 @@ func withNixDaemon(
},
Username: "hakurei",
Shell: shellPath,
Data: pathSet.homeDir,
Dir: path.Join("/data/data", app.ID),
Shell: pathShell,
Home: pathDataData.Append(app.ID),
ExtraPerms: []*hst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
@@ -45,37 +44,34 @@ func withNixDaemon(
Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action,
Userns: true, // nix sandbox requires userns
Net: net,
HostNet: net,
SeccompFlags: seccomp.AllowMultiarch,
Tty: dropShell,
Filesystem: []*hst.FilesystemConfig{
{Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true},
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath, Target: pathNix, Write: true}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID), Source: pathSet.homeDir, Write: true, Ensure: true}},
},
Link: [][2]string{
{app.CurrentSystem, "/run/current-system"},
{"/run/current-system/sw/bin", "/bin"},
{"/run/current-system/sw/bin", "/usr/bin"},
},
Etc: path.Join(pathSet.cacheDir, "etc"),
AutoEtc: true,
},
}), dropShell, beforeFail)
}
func withCacheDir(
ctx context.Context,
action string, command []string, workDir string,
action string, command []string, workDir *container.Absolute,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
mustRunAppDropShell(ctx, &hst.Config{
ID: app.ID,
Path: shellPath,
Args: []string{shellPath, "-lc", strings.Join(command, " && ")},
Path: pathShell,
Args: []string{bash, "-lc", strings.Join(command, " && ")},
Username: "nixos",
Shell: shellPath,
Data: pathSet.cacheDir, // this also ensures cacheDir via shim
Dir: path.Join("/data/data", app.ID, "cache"),
Shell: pathShell,
Home: pathDataData.Append(app.ID, "cache"),
ExtraPerms: []*hst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
@@ -88,24 +84,22 @@ func withCacheDir(
Hostname: formatHostname(app.Name) + "-" + action,
SeccompFlags: seccomp.AllowMultiarch,
Tty: dropShell,
Filesystem: []*hst.FilesystemConfig{
{Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true},
{Src: workDir, Dst: path.Join(hst.Tmp, "bundle"), Must: true},
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: workDir.Append(container.FHSEtc), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: workDir.Append("nix"), Target: pathNix}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Source: workDir, Target: hst.AbsTmp.Append("bundle")}},
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID, "cache"), Source: pathSet.cacheDir, Write: true, Ensure: true}},
},
Link: [][2]string{
{app.CurrentSystem, "/run/current-system"},
{"/run/current-system/sw/bin", "/bin"},
{"/run/current-system/sw/bin", "/usr/bin"},
},
Etc: path.Join(workDir, "etc"),
AutoEtc: true,
},
}, dropShell, beforeFail)
}
func mustRunAppDropShell(ctx context.Context, config *hst.Config, dropShell bool, beforeFail func()) {
if dropShell {
config.Args = []string{shellPath, "-l"}
config.Args = []string{bash, "-l"}
mustRunApp(ctx, config, beforeFail)
beforeFail()
internal.Exit(0)
-101
View File
@@ -1,101 +0,0 @@
package main
import (
"log"
"os"
"os/exec"
"path"
"strconv"
"sync/atomic"
"hakurei.app/hst"
"hakurei.app/internal/hlog"
)
var (
dataHome string
)
func init() {
// dataHome
if p, ok := os.LookupEnv("HAKUREI_DATA_HOME"); ok {
dataHome = p
} else {
dataHome = "/var/lib/hakurei/" + strconv.Itoa(os.Getuid())
}
}
func lookPath(file string) string {
if p, err := exec.LookPath(file); err != nil {
log.Fatalf("%s: command not found", file)
return ""
} else {
return p
}
}
var beforeRunFail = new(atomic.Pointer[func()])
func mustRun(name string, arg ...string) {
hlog.Verbosef("spawning process: %q %q", name, arg)
cmd := exec.Command(name, arg...)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
if f := beforeRunFail.Swap(nil); f != nil {
(*f)()
}
log.Fatalf("%s: %v", name, err)
}
}
type appPathSet struct {
// ${dataHome}/${id}
baseDir string
// ${baseDir}/app
metaPath string
// ${baseDir}/files
homeDir string
// ${baseDir}/cache
cacheDir string
// ${baseDir}/cache/nix
nixPath string
}
func pathSetByApp(id string) *appPathSet {
pathSet := new(appPathSet)
pathSet.baseDir = path.Join(dataHome, id)
pathSet.metaPath = path.Join(pathSet.baseDir, "app")
pathSet.homeDir = path.Join(pathSet.baseDir, "files")
pathSet.cacheDir = path.Join(pathSet.baseDir, "cache")
pathSet.nixPath = path.Join(pathSet.cacheDir, "nix")
return pathSet
}
func appendGPUFilesystem(config *hst.Config) {
config.Container.Filesystem = append(config.Container.Filesystem, []*hst.FilesystemConfig{
// flatpak commit 763a686d874dd668f0236f911de00b80766ffe79
{Src: "/dev/dri", Device: true},
// mali
{Src: "/dev/mali", Device: true},
{Src: "/dev/mali0", Device: true},
{Src: "/dev/umplock", Device: true},
// nvidia
{Src: "/dev/nvidiactl", Device: true},
{Src: "/dev/nvidia-modeset", Device: true},
// nvidia OpenCL/CUDA
{Src: "/dev/nvidia-uvm", Device: true},
{Src: "/dev/nvidia-uvm-tools", Device: true},
// flatpak commit d2dff2875bb3b7e2cd92d8204088d743fd07f3ff
{Src: "/dev/nvidia0", Device: true}, {Src: "/dev/nvidia1", Device: true},
{Src: "/dev/nvidia2", Device: true}, {Src: "/dev/nvidia3", Device: true},
{Src: "/dev/nvidia4", Device: true}, {Src: "/dev/nvidia5", Device: true},
{Src: "/dev/nvidia6", Device: true}, {Src: "/dev/nvidia7", Device: true},
{Src: "/dev/nvidia8", Device: true}, {Src: "/dev/nvidia9", Device: true},
{Src: "/dev/nvidia10", Device: true}, {Src: "/dev/nvidia11", Device: true},
{Src: "/dev/nvidia12", Device: true}, {Src: "/dev/nvidia13", Device: true},
{Src: "/dev/nvidia14", Device: true}, {Src: "/dev/nvidia15", Device: true},
{Src: "/dev/nvidia16", Device: true}, {Src: "/dev/nvidia17", Device: true},
{Src: "/dev/nvidia18", Device: true}, {Src: "/dev/nvidia19", Device: true},
}...)
}
+107
View File
@@ -0,0 +1,107 @@
package container
import (
"encoding/json"
"errors"
"fmt"
"path"
"slices"
"strings"
"syscall"
)
// AbsoluteError is returned by [NewAbs] and holds the invalid pathname.
type AbsoluteError struct {
Pathname string
}
func (e *AbsoluteError) Error() string { return fmt.Sprintf("path %q is not absolute", e.Pathname) }
func (e *AbsoluteError) Is(target error) bool {
var ce *AbsoluteError
if !errors.As(target, &ce) {
return errors.Is(target, syscall.EINVAL)
}
return *e == *ce
}
// Absolute holds a pathname checked to be absolute.
type Absolute struct {
pathname string
}
// isAbs wraps [path.IsAbs] in case additional checks are added in the future.
func isAbs(pathname string) bool { return path.IsAbs(pathname) }
func (a *Absolute) String() string {
if a.pathname == zeroString {
panic("attempted use of zero Absolute")
}
return a.pathname
}
func (a *Absolute) Is(v *Absolute) bool {
if a == nil && v == nil {
return true
}
return a != nil && v != nil &&
a.pathname != zeroString && v.pathname != zeroString &&
a.pathname == v.pathname
}
// NewAbs checks pathname and returns a new [Absolute] if pathname is absolute.
func NewAbs(pathname string) (*Absolute, error) {
if !isAbs(pathname) {
return nil, &AbsoluteError{pathname}
}
return &Absolute{pathname}, nil
}
// MustAbs calls [NewAbs] and panics on error.
func MustAbs(pathname string) *Absolute {
if a, err := NewAbs(pathname); err != nil {
panic(err.Error())
} else {
return a
}
}
// Append calls [path.Join] with [Absolute] as the first element.
func (a *Absolute) Append(elem ...string) *Absolute {
return &Absolute{path.Join(append([]string{a.String()}, elem...)...)}
}
// Dir calls [path.Dir] with [Absolute] as its argument.
func (a *Absolute) Dir() *Absolute { return &Absolute{path.Dir(a.String())} }
func (a *Absolute) GobEncode() ([]byte, error) { return []byte(a.String()), nil }
func (a *Absolute) GobDecode(data []byte) error {
pathname := string(data)
if !isAbs(pathname) {
return &AbsoluteError{pathname}
}
a.pathname = pathname
return nil
}
func (a *Absolute) MarshalJSON() ([]byte, error) { return json.Marshal(a.String()) }
func (a *Absolute) UnmarshalJSON(data []byte) error {
var pathname string
if err := json.Unmarshal(data, &pathname); err != nil {
return err
}
if !isAbs(pathname) {
return &AbsoluteError{pathname}
}
a.pathname = pathname
return nil
}
// SortAbs calls [slices.SortFunc] for a slice of [Absolute].
func SortAbs(x []*Absolute) {
slices.SortFunc(x, func(a, b *Absolute) int { return strings.Compare(a.String(), b.String()) })
}
// CompactAbs calls [slices.CompactFunc] for a slice of [Absolute].
func CompactAbs(s []*Absolute) []*Absolute {
return slices.CompactFunc(s, func(a *Absolute, b *Absolute) bool { return a.String() == b.String() })
}
+348
View File
@@ -0,0 +1,348 @@
package container
import (
"bytes"
"encoding/gob"
"encoding/json"
"errors"
"reflect"
"strings"
"syscall"
"testing"
)
func TestAbsoluteError(t *testing.T) {
testCases := []struct {
name string
err error
cmp error
ok bool
}{
{"EINVAL", new(AbsoluteError), syscall.EINVAL, true},
{"not EINVAL", new(AbsoluteError), syscall.EBADE, false},
{"ne val", new(AbsoluteError), &AbsoluteError{"etc"}, false},
{"equals", &AbsoluteError{"etc"}, &AbsoluteError{"etc"}, true},
}
for _, tc := range testCases {
if got := errors.Is(tc.err, tc.cmp); got != tc.ok {
t.Errorf("Is: %v, want %v", got, tc.ok)
}
}
t.Run("string", func(t *testing.T) {
want := `path "etc" is not absolute`
if got := (&AbsoluteError{"etc"}).Error(); got != want {
t.Errorf("Error: %q, want %q", got, want)
}
})
}
func TestNewAbs(t *testing.T) {
testCases := []struct {
name string
pathname string
want *Absolute
wantErr error
}{
{"good", "/etc", MustAbs("/etc"), nil},
{"not absolute", "etc", nil, &AbsoluteError{"etc"}},
{"zero", "", nil, &AbsoluteError{""}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got, err := NewAbs(tc.pathname)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("NewAbs: %#v, want %#v", got, tc.want)
}
if !errors.Is(err, tc.wantErr) {
t.Errorf("NewAbs: error = %v, want %v", err, tc.wantErr)
}
})
}
t.Run("must", func(t *testing.T) {
defer func() {
wantPanic := `path "etc" is not absolute`
if r := recover(); r != wantPanic {
t.Errorf("MustAbs: panic = %v; want %v", r, wantPanic)
}
}()
MustAbs("etc")
})
}
func TestAbsoluteString(t *testing.T) {
t.Run("passthrough", func(t *testing.T) {
pathname := "/etc"
if got := (&Absolute{pathname}).String(); got != pathname {
t.Errorf("String: %q, want %q", got, pathname)
}
})
t.Run("zero", func(t *testing.T) {
defer func() {
wantPanic := "attempted use of zero Absolute"
if r := recover(); r != wantPanic {
t.Errorf("String: panic = %v, want %v", r, wantPanic)
}
}()
panic(new(Absolute).String())
})
}
func TestAbsoluteIs(t *testing.T) {
testCases := []struct {
name string
a, v *Absolute
want bool
}{
{"nil", (*Absolute)(nil), (*Absolute)(nil), true},
{"nil a", (*Absolute)(nil), MustAbs("/"), false},
{"nil v", MustAbs("/"), (*Absolute)(nil), false},
{"zero", new(Absolute), new(Absolute), false},
{"zero a", new(Absolute), MustAbs("/"), false},
{"zero v", MustAbs("/"), new(Absolute), false},
{"equals", MustAbs("/"), MustAbs("/"), true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := tc.a.Is(tc.v); got != tc.want {
t.Errorf("Is: %v, want %v", got, tc.want)
}
})
}
}
type sCheck struct {
Pathname *Absolute `json:"val"`
Magic int `json:"magic"`
}
func TestCodecAbsolute(t *testing.T) {
testCases := []struct {
name string
a *Absolute
wantErr error
gob, sGob string
json, sJson string
}{
{"nil", nil, nil,
"\x00", "\x00",
`null`, `{"val":null,"magic":3236757504}`},
{"good", MustAbs("/etc"),
nil,
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\b\xff\x80\x00\x04/etc",
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x04\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x10\xff\x84\x01\x04/etc\x01\xfb\x01\x81\xda\x00\x00\x00",
`"/etc"`, `{"val":"/etc","magic":3236757504}`},
{"not absolute", nil,
&AbsoluteError{"etc"},
"\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",
`"etc"`, `{"val":"etc","magic":3236757504}`},
{"zero", nil,
new(AbsoluteError),
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x04\xff\x80\x00\x00",
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x04\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\f\xff\x84\x01\x00\x01\xfb\x01\x81\xda\x00\x00\x00",
`""`, `{"val":"","magic":3236757504}`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("gob", func(t *testing.T) {
if tc.gob == "\x00" && tc.sGob == "\x00" {
// these values mark the current test to skip gob
return
}
t.Run("encode", func(t *testing.T) {
// encode is unchecked
if errors.Is(tc.wantErr, syscall.EINVAL) {
return
}
{
buf := new(bytes.Buffer)
err := gob.NewEncoder(buf).Encode(tc.a)
if !errors.Is(err, tc.wantErr) {
t.Errorf("Encode: error = %v, want %v", err, tc.wantErr)
}
if tc.wantErr != nil {
goto checkSEncode
}
if buf.String() != tc.gob {
t.Errorf("Encode:\n%q\nwant:\n%q", buf.String(), tc.gob)
}
}
checkSEncode:
{
buf := new(bytes.Buffer)
err := gob.NewEncoder(buf).Encode(&sCheck{tc.a, syscall.MS_MGC_VAL})
if !errors.Is(err, tc.wantErr) {
t.Errorf("Encode: error = %v, want %v", err, tc.wantErr)
}
if tc.wantErr != nil {
return
}
if buf.String() != tc.sGob {
t.Errorf("Encode:\n%q\nwant:\n%q", buf.String(), tc.sGob)
}
}
})
t.Run("decode", func(t *testing.T) {
{
var gotA *Absolute
err := gob.NewDecoder(strings.NewReader(tc.gob)).Decode(&gotA)
if !errors.Is(err, tc.wantErr) {
t.Errorf("Decode: error = %v, want %v", err, tc.wantErr)
}
if tc.wantErr != nil {
goto checkSDecode
}
if !reflect.DeepEqual(tc.a, gotA) {
t.Errorf("Decode: %#v, want %#v", tc.a, gotA)
}
}
checkSDecode:
{
var gotSCheck sCheck
err := gob.NewDecoder(strings.NewReader(tc.sGob)).Decode(&gotSCheck)
if !errors.Is(err, tc.wantErr) {
t.Errorf("Decode: error = %v, want %v", err, tc.wantErr)
}
if tc.wantErr != nil {
return
}
want := sCheck{tc.a, syscall.MS_MGC_VAL}
if !reflect.DeepEqual(gotSCheck, want) {
t.Errorf("Decode: %#v, want %#v", gotSCheck, want)
}
}
})
})
t.Run("json", func(t *testing.T) {
t.Run("marshal", func(t *testing.T) {
// marshal is unchecked
if errors.Is(tc.wantErr, syscall.EINVAL) {
return
}
{
d, err := json.Marshal(tc.a)
if !errors.Is(err, tc.wantErr) {
t.Errorf("Marshal: error = %v, want %v", err, tc.wantErr)
}
if tc.wantErr != nil {
goto checkSMarshal
}
if string(d) != tc.json {
t.Errorf("Marshal:\n%s\nwant:\n%s", string(d), tc.json)
}
}
checkSMarshal:
{
d, err := json.Marshal(&sCheck{tc.a, syscall.MS_MGC_VAL})
if !errors.Is(err, tc.wantErr) {
t.Errorf("Marshal: error = %v, want %v", err, tc.wantErr)
}
if tc.wantErr != nil {
return
}
if string(d) != tc.sJson {
t.Errorf("Marshal:\n%s\nwant:\n%s", string(d), tc.sJson)
}
}
})
t.Run("unmarshal", func(t *testing.T) {
{
var gotA *Absolute
err := json.Unmarshal([]byte(tc.json), &gotA)
if !errors.Is(err, tc.wantErr) {
t.Errorf("Unmarshal: error = %v, want %v", err, tc.wantErr)
}
if tc.wantErr != nil {
goto checkSUnmarshal
}
if !reflect.DeepEqual(tc.a, gotA) {
t.Errorf("Unmarshal: %#v, want %#v", tc.a, gotA)
}
}
checkSUnmarshal:
{
var gotSCheck sCheck
err := json.Unmarshal([]byte(tc.sJson), &gotSCheck)
if !errors.Is(err, tc.wantErr) {
t.Errorf("Unmarshal: error = %v, want %v", err, tc.wantErr)
}
if tc.wantErr != nil {
return
}
want := sCheck{tc.a, syscall.MS_MGC_VAL}
if !reflect.DeepEqual(gotSCheck, want) {
t.Errorf("Unmarshal: %#v, want %#v", gotSCheck, want)
}
}
})
})
})
}
t.Run("json passthrough", func(t *testing.T) {
wantErr := "invalid character ':' looking for beginning of value"
if err := new(Absolute).UnmarshalJSON([]byte(":3")); err == nil || err.Error() != wantErr {
t.Errorf("UnmarshalJSON: error = %v, want %s", err, wantErr)
}
})
}
func TestAbsoluteWrap(t *testing.T) {
t.Run("join", func(t *testing.T) {
want := "/etc/nix/nix.conf"
if got := MustAbs("/etc").Append("nix", "nix.conf"); got.String() != want {
t.Errorf("Append: %q, want %q", got, want)
}
})
t.Run("dir", func(t *testing.T) {
want := "/"
if got := MustAbs("/etc").Dir(); got.String() != want {
t.Errorf("Dir: %q, want %q", got, want)
}
})
t.Run("sort", func(t *testing.T) {
want := []*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/sys")}
got := []*Absolute{MustAbs("/proc"), MustAbs("/sys"), MustAbs("/etc")}
SortAbs(got)
if !reflect.DeepEqual(got, want) {
t.Errorf("SortAbs: %#v, want %#v", got, want)
}
})
t.Run("compact", func(t *testing.T) {
want := []*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/sys")}
if got := CompactAbs([]*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/proc"), MustAbs("/sys")}); !reflect.DeepEqual(got, want) {
t.Errorf("CompactAbs: %#v, want %#v", got, want)
}
})
}
+69
View File
@@ -0,0 +1,69 @@
package container
import (
"encoding/gob"
"fmt"
"io/fs"
)
func init() { gob.Register(new(AutoEtcOp)) }
// Etc appends an [Op] that expands host /etc into a toplevel symlink mirror with /etc semantics.
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
func (f *Ops) Etc(host *Absolute, prefix string) *Ops {
e := &AutoEtcOp{prefix}
f.Mkdir(AbsFHSEtc, 0755)
f.Bind(host, e.hostPath(), 0)
*f = append(*f, e)
return f
}
type AutoEtcOp struct{ Prefix string }
func (e *AutoEtcOp) Valid() bool { return e != nil }
func (e *AutoEtcOp) early(*setupState, syscallDispatcher) error { return nil }
func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
if state.nonrepeatable&nrAutoEtc != 0 {
return msg.WrapErr(fs.ErrInvalid, "autoetc is not repeatable")
}
state.nonrepeatable |= nrAutoEtc
const target = sysrootPath + FHSEtc
rel := e.hostRel() + "/"
if err := k.mkdirAll(target, 0755); err != nil {
return wrapErrSelf(err)
}
if d, err := k.readdir(toSysroot(e.hostPath().String())); err != nil {
return wrapErrSelf(err)
} else {
for _, ent := range d {
n := ent.Name()
switch n {
case ".host", "passwd", "group":
case "mtab":
if err = k.symlink(FHSProc+"mounts", target+n); err != nil {
return wrapErrSelf(err)
}
default:
if err = k.symlink(rel+n, target+n); err != nil {
return wrapErrSelf(err)
}
}
}
}
return nil
}
func (e *AutoEtcOp) hostPath() *Absolute { return AbsFHSEtc.Append(e.hostRel()) }
func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix }
func (e *AutoEtcOp) Is(op Op) bool {
ve, ok := op.(*AutoEtcOp)
return ok && e.Valid() && ve.Valid() && *e == *ve
}
func (*AutoEtcOp) prefix() string { return "setting up" }
func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }
+291
View File
@@ -0,0 +1,291 @@
package container
import (
"errors"
"io/fs"
"os"
"testing"
)
func TestAutoEtcOp(t *testing.T) {
t.Run("nonrepeatable", func(t *testing.T) {
wantErr := msg.WrapErr(fs.ErrInvalid, "autoetc is not repeatable")
if err := (&AutoEtcOp{Prefix: "81ceabb30d37bbdb3868004629cb84e9"}).apply(&setupState{nonrepeatable: nrAutoEtc}, nil); !errors.Is(err, wantErr) {
t.Errorf("apply: error = %v, want %v", err, wantErr)
}
})
checkOpBehaviour(t, []opBehaviourTestCase{
{"mkdirAll", new(Params), &AutoEtcOp{
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
}, nil, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, errUnique},
}, wrapErrSelf(errUnique)},
{"readdir", new(Params), &AutoEtcOp{
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
}, nil, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(), errUnique},
}, wrapErrSelf(errUnique)},
{"symlink", new(Params), &AutoEtcOp{
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
}, nil, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
"modprobe.d", "modules-load.d", "mtab", "nanorc", "netgroup", "nix", "nixos", "NIXOS", "nscd.conf",
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, errUnique},
}, wrapErrSelf(errUnique)},
{"symlink mtab", new(Params), &AutoEtcOp{
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
}, nil, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
"modprobe.d", "modules-load.d", "mtab", "nanorc", "netgroup", "nix", "nixos", "NIXOS", "nscd.conf",
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil},
{"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, errUnique},
}, wrapErrSelf(errUnique)},
{"success nested", new(Params), &AutoEtcOp{
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
}, nil, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
"modprobe.d", "modules-load.d", "mtab", "nanorc", "netgroup", "nix", "nixos", "NIXOS", "nscd.conf",
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil},
{"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil},
}, nil},
{"success", new(Params), &AutoEtcOp{
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
}, nil, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
"modprobe.d", "modules-load.d", "mtab", "nanorc", "netgroup", "nix", "nixos", "NIXOS", "nscd.conf",
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil},
{"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil},
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil},
}, nil},
})
checkOpsValid(t, []opValidTestCase{
{"nil", (*AutoEtcOp)(nil), false},
{"zero", new(AutoEtcOp), true},
{"populated", &AutoEtcOp{Prefix: ":3"}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"pd", new(Ops).Etc(MustAbs("/etc/"), "048090b6ed8f9ebb10e275ff5d8c0659"), Ops{
&MkdirOp{Path: MustAbs("/etc/"), Perm: 0755},
&BindMountOp{
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
},
&AutoEtcOp{Prefix: "048090b6ed8f9ebb10e275ff5d8c0659"},
}},
})
checkOpIs(t, []opIsTestCase{
{"zero", new(AutoEtcOp), new(AutoEtcOp), true},
{"differs", &AutoEtcOp{Prefix: "\x00"}, &AutoEtcOp{":3"}, false},
{"equals", &AutoEtcOp{Prefix: ":3"}, &AutoEtcOp{":3"}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"etc", &AutoEtcOp{
Prefix: ":3",
}, "setting up", "auto etc :3"},
})
t.Run("host path rel", func(t *testing.T) {
op := &AutoEtcOp{Prefix: "048090b6ed8f9ebb10e275ff5d8c0659"}
wantHostPath := "/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"
wantHostRel := ".host/048090b6ed8f9ebb10e275ff5d8c0659"
if got := op.hostPath(); got.String() != wantHostPath {
t.Errorf("hostPath: %q, want %q", got, wantHostPath)
}
if got := op.hostRel(); got != wantHostRel {
t.Errorf("hostRel: %q, want %q", got, wantHostRel)
}
})
}
+93
View File
@@ -0,0 +1,93 @@
package container
import (
"encoding/gob"
"fmt"
"io/fs"
)
func init() { gob.Register(new(AutoRootOp)) }
// Root appends an [Op] that expands a directory into a toplevel bind mount mirror on container root.
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
func (f *Ops) Root(host *Absolute, flags int) *Ops {
*f = append(*f, &AutoRootOp{host, flags, nil})
return f
}
type AutoRootOp struct {
Host *Absolute
// passed through to bindMount
Flags int
// obtained during early;
// these wrap the underlying Op because BindMountOp is relatively complex,
// so duplicating that code would be unwise
resolved []Op
}
func (r *AutoRootOp) Valid() bool { return r != nil && r.Host != nil }
func (r *AutoRootOp) early(state *setupState, k syscallDispatcher) error {
if d, err := k.readdir(r.Host.String()); err != nil {
return wrapErrSelf(err)
} else {
r.resolved = make([]Op, 0, len(d))
for _, ent := range d {
name := ent.Name()
if IsAutoRootBindable(name) {
op := &BindMountOp{
Source: r.Host.Append(name),
Target: AbsFHSRoot.Append(name),
Flags: r.Flags,
}
if err = op.early(state, k); err != nil {
return err
}
r.resolved = append(r.resolved, op)
}
}
return nil
}
}
func (r *AutoRootOp) apply(state *setupState, k syscallDispatcher) error {
if state.nonrepeatable&nrAutoRoot != 0 {
return msg.WrapErr(fs.ErrInvalid, "autoroot is not repeatable")
}
state.nonrepeatable |= nrAutoRoot
for _, op := range r.resolved {
k.verbosef("%s %s", op.prefix(), op)
if err := op.apply(state, k); err != nil {
return err
}
}
return nil
}
func (r *AutoRootOp) Is(op Op) bool {
vr, ok := op.(*AutoRootOp)
return ok && r.Valid() && vr.Valid() &&
r.Host.Is(vr.Host) &&
r.Flags == vr.Flags
}
func (*AutoRootOp) prefix() string { return "setting up" }
func (r *AutoRootOp) String() string {
return fmt.Sprintf("auto root %q flags %#x", r.Host, r.Flags)
}
// IsAutoRootBindable returns whether a dir entry name is selected for AutoRoot.
func IsAutoRootBindable(name string) bool {
switch name {
case "proc", "dev", "tmp", "mnt", "etc":
case "": // guard against accidentally binding /
// should be unreachable
msg.Verbose("got unexpected root entry")
default:
return true
}
return false
}
+200
View File
@@ -0,0 +1,200 @@
package container
import (
"errors"
"io/fs"
"os"
"testing"
)
func TestAutoRootOp(t *testing.T) {
t.Run("nonrepeatable", func(t *testing.T) {
wantErr := msg.WrapErr(fs.ErrInvalid, "autoroot is not repeatable")
if err := new(AutoRootOp).apply(&setupState{nonrepeatable: nrAutoRoot}, nil); !errors.Is(err, wantErr) {
t.Errorf("apply: error = %v, want %v", err, wantErr)
}
})
checkOpBehaviour(t, []opBehaviourTestCase{
{"readdir", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable,
}, []kexpect{
{"readdir", expectArgs{"/"}, stubDir(), errUnique},
}, wrapErrSelf(errUnique), nil, nil},
{"early", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable,
}, []kexpect{
{"readdir", expectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil},
{"evalSymlinks", expectArgs{"/bin"}, "", errUnique},
}, wrapErrSelf(errUnique), nil, nil},
{"apply", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable,
}, []kexpect{
{"readdir", expectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil},
{"evalSymlinks", expectArgs{"/bin"}, "/usr/bin", nil},
{"evalSymlinks", expectArgs{"/home"}, "/home", nil},
{"evalSymlinks", expectArgs{"/lib64"}, "/lib64", nil},
{"evalSymlinks", expectArgs{"/lost+found"}, "/lost+found", nil},
{"evalSymlinks", expectArgs{"/nix"}, "/nix", nil},
{"evalSymlinks", expectArgs{"/root"}, "/root", nil},
{"evalSymlinks", expectArgs{"/run"}, "/run", nil},
{"evalSymlinks", expectArgs{"/srv"}, "/srv", nil},
{"evalSymlinks", expectArgs{"/sys"}, "/sys", nil},
{"evalSymlinks", expectArgs{"/usr"}, "/usr", nil},
{"evalSymlinks", expectArgs{"/var"}, "/var", nil},
}, nil, []kexpect{
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr/bin"), MustAbs("/bin"), MustAbs("/bin"), BindWritable}}}, nil, nil},
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(false), errUnique},
}, wrapErrSelf(errUnique)},
{"success pd", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable,
}, []kexpect{
{"readdir", expectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil},
{"evalSymlinks", expectArgs{"/bin"}, "/usr/bin", nil},
{"evalSymlinks", expectArgs{"/home"}, "/home", nil},
{"evalSymlinks", expectArgs{"/lib64"}, "/lib64", nil},
{"evalSymlinks", expectArgs{"/lost+found"}, "/lost+found", nil},
{"evalSymlinks", expectArgs{"/nix"}, "/nix", nil},
{"evalSymlinks", expectArgs{"/root"}, "/root", nil},
{"evalSymlinks", expectArgs{"/run"}, "/run", nil},
{"evalSymlinks", expectArgs{"/srv"}, "/srv", nil},
{"evalSymlinks", expectArgs{"/sys"}, "/sys", nil},
{"evalSymlinks", expectArgs{"/usr"}, "/usr", nil},
{"evalSymlinks", expectArgs{"/var"}, "/var", nil},
}, nil, []kexpect{
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr/bin"), MustAbs("/bin"), MustAbs("/bin"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4004), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/home"), MustAbs("/home"), MustAbs("/home"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/home"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/home", "/sysroot/home", uintptr(0x4004), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/lib64"), MustAbs("/lib64"), MustAbs("/lib64"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/lib64"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/lib64", "/sysroot/lib64", uintptr(0x4004), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/lost+found"), MustAbs("/lost+found"), MustAbs("/lost+found"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/lost+found"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/lost+found", "/sysroot/lost+found", uintptr(0x4004), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/nix"), MustAbs("/nix"), MustAbs("/nix"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/nix"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/nix", "/sysroot/nix", uintptr(0x4004), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/root"), MustAbs("/root"), MustAbs("/root"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/root"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/root", "/sysroot/root", uintptr(0x4004), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/run"), MustAbs("/run"), MustAbs("/run"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/run"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/run", "/sysroot/run", uintptr(0x4004), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/srv"), MustAbs("/srv"), MustAbs("/srv"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/srv"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/srv", "/sysroot/srv", uintptr(0x4004), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/sys"), MustAbs("/sys"), MustAbs("/sys"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/sys"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/sys", "/sysroot/sys", uintptr(0x4004), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr"), MustAbs("/usr"), MustAbs("/usr"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/usr"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/usr", "/sysroot/usr", uintptr(0x4004), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var"), MustAbs("/var"), MustAbs("/var"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/var"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var", "/sysroot/var", uintptr(0x4004), false}, nil, nil},
}, nil},
{"success", &Params{ParentPerm: 0750}, &AutoRootOp{
Host: MustAbs("/var/lib/planterette/base/debian:f92c9052"),
}, []kexpect{
{"readdir", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, stubDir("bin", "dev", "etc", "home", "lib64",
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/bin"}, "/var/lib/planterette/base/debian:f92c9052/usr/bin", nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/home"}, "/var/lib/planterette/base/debian:f92c9052/home", nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/lib64"}, "/var/lib/planterette/base/debian:f92c9052/lib64", nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/lost+found"}, "/var/lib/planterette/base/debian:f92c9052/lost+found", nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/nix"}, "/var/lib/planterette/base/debian:f92c9052/nix", nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/root"}, "/var/lib/planterette/base/debian:f92c9052/root", nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/run"}, "/var/lib/planterette/base/debian:f92c9052/run", nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/srv"}, "/var/lib/planterette/base/debian:f92c9052/srv", nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/sys"}, "/var/lib/planterette/base/debian:f92c9052/sys", nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/usr"}, "/var/lib/planterette/base/debian:f92c9052/usr", nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/var"}, "/var/lib/planterette/base/debian:f92c9052/var", nil},
}, nil, []kexpect{
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/usr/bin"), MustAbs("/var/lib/planterette/base/debian:f92c9052/bin"), MustAbs("/bin"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/home"), MustAbs("/var/lib/planterette/base/debian:f92c9052/home"), MustAbs("/home"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home", "/sysroot/home", uintptr(0x4005), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/lib64"), MustAbs("/var/lib/planterette/base/debian:f92c9052/lib64"), MustAbs("/lib64"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64", "/sysroot/lib64", uintptr(0x4005), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/lost+found"), MustAbs("/var/lib/planterette/base/debian:f92c9052/lost+found"), MustAbs("/lost+found"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found", "/sysroot/lost+found", uintptr(0x4005), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/nix"), MustAbs("/var/lib/planterette/base/debian:f92c9052/nix"), MustAbs("/nix"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix", "/sysroot/nix", uintptr(0x4005), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/root"), MustAbs("/var/lib/planterette/base/debian:f92c9052/root"), MustAbs("/root"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root", "/sysroot/root", uintptr(0x4005), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/run"), MustAbs("/var/lib/planterette/base/debian:f92c9052/run"), MustAbs("/run"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run", "/sysroot/run", uintptr(0x4005), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/srv"), MustAbs("/var/lib/planterette/base/debian:f92c9052/srv"), MustAbs("/srv"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv", "/sysroot/srv", uintptr(0x4005), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/sys"), MustAbs("/var/lib/planterette/base/debian:f92c9052/sys"), MustAbs("/sys"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys", "/sysroot/sys", uintptr(0x4005), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/usr"), MustAbs("/var/lib/planterette/base/debian:f92c9052/usr"), MustAbs("/usr"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr", "/sysroot/usr", uintptr(0x4005), false}, nil, nil},
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/var"), MustAbs("/var/lib/planterette/base/debian:f92c9052/var"), MustAbs("/var"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var", "/sysroot/var", uintptr(0x4005), false}, nil, nil},
}, nil},
})
checkOpsValid(t, []opValidTestCase{
{"nil", (*AutoRootOp)(nil), false},
{"zero", new(AutoRootOp), false},
{"valid", &AutoRootOp{Host: MustAbs("/")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"pd", new(Ops).Root(MustAbs("/"), BindWritable), Ops{
&AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable,
},
}},
})
checkOpIs(t, []opIsTestCase{
{"zero", new(AutoRootOp), new(AutoRootOp), false},
{"internal ne", &AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable,
}, &AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable,
resolved: []Op{new(BindMountOp)},
}, true},
{"flags differs", &AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable | BindDevice,
}, &AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable,
}, false},
{"host differs", &AutoRootOp{
Host: MustAbs("/tmp/"),
Flags: BindWritable,
}, &AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable,
}, false},
{"equals", &AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable,
}, &AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable,
}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"root", &AutoRootOp{
Host: MustAbs("/"),
Flags: BindWritable,
}, "setting up", `auto root "/" flags 0x2`},
})
}
func TestIsAutoRootBindable(t *testing.T) {
testCases := []struct {
name string
want bool
}{
{"proc", false},
{"dev", false},
{"tmp", false},
{"mnt", false},
{"etc", false},
{"", false},
{"var", true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := IsAutoRootBindable(tc.name); got != tc.want {
t.Errorf("IsAutoRootBindable: %v, want %v", got, tc.want)
}
})
}
}
+89
View File
@@ -0,0 +1,89 @@
package container
import (
"syscall"
"unsafe"
)
const (
_LINUX_CAPABILITY_VERSION_3 = 0x20080522
PR_CAP_AMBIENT = 0x2f
PR_CAP_AMBIENT_RAISE = 0x2
PR_CAP_AMBIENT_CLEAR_ALL = 0x4
CAP_SYS_ADMIN = 0x15
CAP_SETPCAP = 0x8
CAP_DAC_OVERRIDE = 0x1
)
type (
capHeader struct {
version uint32
pid int32
}
capData struct {
effective uint32
permitted uint32
inheritable uint32
}
)
// See CAP_TO_INDEX in linux/capability.h:
func capToIndex(cap uintptr) uintptr { return cap >> 5 }
// See CAP_TO_MASK in linux/capability.h:
func capToMask(cap uintptr) uint32 { return 1 << uint(cap&31) }
func capset(hdrp *capHeader, datap *[2]capData) error {
r, _, errno := syscall.Syscall(
syscall.SYS_CAPSET,
uintptr(unsafe.Pointer(hdrp)),
uintptr(unsafe.Pointer(&datap[0])), 0,
)
if r != 0 {
return errno
}
return nil
}
// capBoundingSetDrop drops a capability from the calling thread's capability bounding set.
func capBoundingSetDrop(cap uintptr) error {
r, _, errno := syscall.Syscall(
syscall.SYS_PRCTL,
syscall.PR_CAPBSET_DROP,
cap, 0,
)
if r != 0 {
return errno
}
return nil
}
// capAmbientClearAll clears the ambient capability set of the calling thread.
func capAmbientClearAll() error {
r, _, errno := syscall.Syscall(
syscall.SYS_PRCTL,
PR_CAP_AMBIENT,
PR_CAP_AMBIENT_CLEAR_ALL, 0,
)
if r != 0 {
return errno
}
return nil
}
// capAmbientRaise adds to the ambient capability set of the calling thread.
func capAmbientRaise(cap uintptr) error {
r, _, errno := syscall.Syscall(
syscall.SYS_PRCTL,
PR_CAP_AMBIENT,
PR_CAP_AMBIENT_RAISE,
cap,
)
if r != 0 {
return errno
}
return nil
}
+41
View File
@@ -0,0 +1,41 @@
package container
import "testing"
func TestCapToIndex(t *testing.T) {
testCases := []struct {
name string
cap uintptr
want uintptr
}{
{"CAP_SYS_ADMIN", CAP_SYS_ADMIN, 0},
{"CAP_SETPCAP", CAP_SETPCAP, 0},
{"CAP_DAC_OVERRIDE", CAP_DAC_OVERRIDE, 0},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := capToIndex(tc.cap); got != tc.want {
t.Errorf("capToIndex: %#x, want %#x", got, tc.want)
}
})
}
}
func TestCapToMask(t *testing.T) {
testCases := []struct {
name string
cap uintptr
want uint32
}{
{"CAP_SYS_ADMIN", CAP_SYS_ADMIN, 0x200000},
{"CAP_SETPCAP", CAP_SETPCAP, 0x100},
{"CAP_DAC_OVERRIDE", CAP_DAC_OVERRIDE, 0x2},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := capToMask(tc.cap); got != tc.want {
t.Errorf("capToMask: %#x, want %#x", got, tc.want)
}
})
}
}
+154 -59
View File
@@ -9,7 +9,7 @@ import (
"io"
"os"
"os/exec"
"path"
"runtime"
"strconv"
. "syscall"
"time"
@@ -17,25 +17,28 @@ import (
"hakurei.app/container/seccomp"
)
const (
// CancelSignal is the signal expected by container init on context cancel.
// A custom [Container.Cancel] function must eventually deliver this signal.
CancelSignal = SIGTERM
)
type (
// Container represents a container environment being prepared or run.
// None of [Container] methods are safe for concurrent use.
Container struct {
// Name of initial process in the container.
name string
// Cgroup fd, nil to disable.
Cgroup *int
// ExtraFiles passed through to initial process in the container,
// with behaviour identical to its [exec.Cmd] counterpart.
ExtraFiles []*os.File
// Custom [exec.Cmd] initialisation function.
CommandContext func(ctx context.Context) (cmd *exec.Cmd)
// param encoder for shim and init
setup *gob.Encoder
// cancels cmd
cancel context.CancelFunc
// closed after Wait returns
wait chan struct{}
Stdin io.Reader
Stdout io.Writer
@@ -52,13 +55,17 @@ type (
// Params holds container configuration and is safe to serialise.
Params struct {
// Working directory in the container.
Dir string
Dir *Absolute
// Initial process environment.
Env []string
// Absolute path of initial process in the container. Overrides name.
Path string
// Pathname of initial process in the container.
Path *Absolute
// Initial process argv.
Args []string
// Deliver SIGINT to the initial process on context cancellation.
ForwardCancel bool
// time to wait for linger processes after death of initial process
AdoptWaitDelay time.Duration
// Mapped Uid in user namespace.
Uid int
@@ -68,6 +75,7 @@ type (
Hostname string
// Sequential container setup ops.
*Ops
// Seccomp system call filter rules.
SeccompRules []seccomp.NativeRule
// Extra seccomp flags.
@@ -76,6 +84,7 @@ type (
SeccompPresets seccomp.FilterPreset
// Do not load seccomp program.
SeccompDisable bool
// Permission bits of newly created parent directories.
// The zero value is interpreted as 0755.
ParentPerm os.FileMode
@@ -83,27 +92,25 @@ type (
RetainSession bool
// Do not [syscall.CLONE_NEWNET].
HostNet bool
// Do not [LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET].
HostAbstract bool
// Retain CAP_SYS_ADMIN.
Privileged bool
}
)
// Start starts the container init. The init process blocks until Serve is called.
func (p *Container) Start() error {
if p.cmd != nil {
return errors.New("sandbox: already started")
return errors.New("container: already started")
}
if p.Ops == nil || len(*p.Ops) == 0 {
return errors.New("sandbox: starting an empty container")
return errors.New("container: starting an empty container")
}
ctx, cancel := context.WithCancel(p.ctx)
p.cancel = cancel
var cloneFlags uintptr = CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP
if !p.HostNet {
cloneFlags |= CLONE_NEWNET
}
// map to overflow id to work around ownership checks
if p.Uid < 1 {
p.Uid = OverflowUid()
@@ -116,34 +123,47 @@ func (p *Container) Start() error {
p.SeccompPresets |= seccomp.PresetDenyTTY
}
if p.CommandContext != nil {
p.cmd = p.CommandContext(ctx)
} else {
p.cmd = exec.CommandContext(ctx, MustExecutable())
p.cmd.Args = []string{"init"}
if p.AdoptWaitDelay == 0 {
p.AdoptWaitDelay = 5 * time.Second
}
// to allow disabling this behaviour
if p.AdoptWaitDelay < 0 {
p.AdoptWaitDelay = 0
}
p.cmd = exec.CommandContext(ctx, MustExecutable())
p.cmd.Args = []string{initName}
p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
p.cmd.WaitDelay = p.WaitDelay
if p.Cancel != nil {
p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
} else {
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(SIGTERM) }
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(CancelSignal) }
}
p.cmd.Dir = "/"
p.cmd.Dir = FHSRoot
p.cmd.SysProcAttr = &SysProcAttr{
Setsid: !p.RetainSession,
Pdeathsig: SIGKILL,
Cloneflags: cloneFlags | CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS,
Setsid: !p.RetainSession,
Pdeathsig: SIGKILL,
Cloneflags: CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS |
CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP,
// remain privileged for setup
AmbientCaps: []uintptr{CAP_SYS_ADMIN, CAP_SETPCAP},
AmbientCaps: []uintptr{
// general container setup
CAP_SYS_ADMIN,
// drop capabilities
CAP_SETPCAP,
// overlay access to upperdir and workdir
CAP_DAC_OVERRIDE,
},
UseCgroupFD: p.Cgroup != nil,
}
if p.cmd.SysProcAttr.UseCgroupFD {
p.cmd.SysProcAttr.CgroupFD = *p.Cgroup
}
if !p.HostNet {
p.cmd.SysProcAttr.Cloneflags |= CLONE_NEWNET
}
// place setup pipe before user supplied extra files, this is later restored by init
if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil {
@@ -155,13 +175,78 @@ func (p *Container) Start() error {
}
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...)
msg.Verbose("starting container init")
if err := p.cmd.Start(); err != nil {
return msg.WrapErr(err, err.Error())
}
return nil
done := make(chan error, 1)
go func() {
runtime.LockOSThread()
p.wait = make(chan struct{})
done <- func() error { // setup depending on per-thread state must happen here
// PR_SET_NO_NEW_PRIVS: depends on per-thread state but acts on all processes created from that thread
if err := SetNoNewPrivs(); err != nil {
return wrapErrSuffix(err,
"prctl(PR_SET_NO_NEW_PRIVS):")
}
// landlock: depends on per-thread state but acts on a process group
{
rulesetAttr := &RulesetAttr{Scoped: LANDLOCK_SCOPE_SIGNAL}
if !p.HostAbstract {
rulesetAttr.Scoped |= LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
}
if abi, err := LandlockGetABI(); err != nil {
if p.HostAbstract {
// landlock can be skipped here as it restricts access to resources
// already covered by namespaces (pid)
goto landlockOut
}
return wrapErrSuffix(err,
"landlock does not appear to be enabled:")
} else if abi < 6 {
if p.HostAbstract {
// see above comment
goto landlockOut
}
return msg.WrapErr(ENOSYS,
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET")
} else {
msg.Verbosef("landlock abi version %d", abi)
}
if rulesetFd, err := rulesetAttr.Create(0); err != nil {
return wrapErrSuffix(err,
"cannot create landlock ruleset:")
} else {
msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
if err = LandlockRestrictSelf(rulesetFd, 0); err != nil {
_ = Close(rulesetFd)
return wrapErrSuffix(err,
"cannot enforce landlock ruleset:")
}
if err = Close(rulesetFd); err != nil {
msg.Verbosef("cannot close landlock ruleset: %v", err)
// not fatal
}
}
landlockOut:
}
msg.Verbose("starting container init")
if err := p.cmd.Start(); err != nil {
return msg.WrapErr(err, err.Error())
}
return nil
}()
// keep this thread alive until Wait returns for cancel
<-p.wait
}()
return <-done
}
// Serve serves [Container.Params] to the container init.
// Serve must only be called once.
func (p *Container) Serve() error {
if p.setup == nil {
panic("invalid serve")
@@ -170,33 +255,16 @@ func (p *Container) Serve() error {
setup := p.setup
p.setup = nil
if p.Path != "" && !path.IsAbs(p.Path) {
if p.Path == nil {
p.cancel()
return msg.WrapErr(EINVAL,
fmt.Sprintf("invalid executable path %q", p.Path))
return msg.WrapErr(EINVAL, "invalid executable pathname")
}
if p.Path == "" {
if p.name == "" {
p.Path = os.Getenv("SHELL")
if !path.IsAbs(p.Path) {
p.cancel()
return msg.WrapErr(EBADE,
"no command specified and $SHELL is invalid")
}
p.name = path.Base(p.Path)
} else if path.IsAbs(p.name) {
p.Path = p.name
} else if v, err := exec.LookPath(p.name); err != nil {
p.cancel()
return msg.WrapErr(err, err.Error())
} else {
p.Path = v
}
// do not transmit nil
if p.Dir == nil {
p.Dir = AbsFHSRoot
}
if p.SeccompRules == nil {
// do not transmit nil
p.SeccompRules = make([]seccomp.NativeRule, 0)
}
@@ -215,15 +283,42 @@ func (p *Container) Serve() error {
return err
}
func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() }
// Wait waits for the container init process to exit and releases any resources associated with the [Container].
func (p *Container) Wait() error {
if p.cmd == nil {
return EINVAL
}
err := p.cmd.Wait()
p.cancel()
if p.wait != nil && err == nil {
close(p.wait)
}
return err
}
func (p *Container) String() string {
return fmt.Sprintf("argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x",
p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets))
}
func New(ctx context.Context, name string, args ...string) *Container {
return &Container{name: name, ctx: ctx,
Params: Params{Args: append([]string{name}, args...), Dir: "/", Ops: new(Ops)},
// ProcessState returns the address to os.ProcessState held by the underlying [exec.Cmd].
func (p *Container) ProcessState() *os.ProcessState {
if p.cmd == nil {
return nil
}
return p.cmd.ProcessState
}
// New returns the address to a new instance of [Container] that requires further initialisation before use.
func New(ctx context.Context) *Container {
return &Container{ctx: ctx, Params: Params{Ops: new(Ops)}}
}
// NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields.
func NewCommand(ctx context.Context, pathname *Absolute, name string, args ...string) *Container {
z := New(ctx)
z.Path = pathname
z.Args = append([]string{name}, args...)
return z
}
+474 -179
View File
@@ -4,14 +4,19 @@ import (
"bytes"
"context"
"encoding/gob"
"errors"
"fmt"
"log"
"os"
"os/exec"
"os/signal"
"strconv"
"strings"
"syscall"
"testing"
"time"
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/container/vfs"
@@ -24,152 +29,307 @@ import (
const (
ignore = "\x00"
ignoreV = -1
pathPrefix = "/etc/hakurei/"
pathWantMnt = pathPrefix + "want-mnt"
pathReadonly = pathPrefix + "readonly"
)
type testVal any
func emptyOps(t *testing.T) (*container.Ops, context.Context) { return new(container.Ops), t.Context() }
func earlyOps(ops *container.Ops) func(t *testing.T) (*container.Ops, context.Context) {
return func(t *testing.T) (*container.Ops, context.Context) { return ops, t.Context() }
}
func emptyMnt(*testing.T, context.Context) []*vfs.MountInfoEntry { return nil }
func earlyMnt(mnt ...*vfs.MountInfoEntry) func(*testing.T, context.Context) []*vfs.MountInfoEntry {
return func(*testing.T, context.Context) []*vfs.MountInfoEntry { return mnt }
}
var containerTestCases = []struct {
name string
filter bool
session bool
net bool
ro bool
ops func(t *testing.T) (*container.Ops, context.Context)
mnt func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry
uid int
gid int
rules []seccomp.NativeRule
flags seccomp.ExportFlag
presets seccomp.FilterPreset
}{
{"minimal", true, false, false, true,
emptyOps, emptyMnt,
1000, 100, nil, 0, seccomp.PresetStrict},
{"allow", true, true, true, false,
emptyOps, emptyMnt,
1000, 100, nil, 0, seccomp.PresetExt | seccomp.PresetDenyDevel},
{"no filter", false, true, true, true,
emptyOps, emptyMnt,
1000, 100, nil, 0, seccomp.PresetExt},
{"custom rules", true, true, true, false,
emptyOps, emptyMnt,
1, 31, []seccomp.NativeRule{{seccomp.ScmpSyscall(syscall.SYS_SETUID), seccomp.ScmpErrno(syscall.EPERM), nil}}, 0, seccomp.PresetExt},
{"tmpfs", true, false, false, true,
earlyOps(new(container.Ops).
Tmpfs(hst.AbsTmp, 0, 0755),
),
earlyMnt(
ent("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", ignore),
),
9, 9, nil, 0, seccomp.PresetStrict},
{"dev", true, true /* go test output is not a tty */, false, false,
earlyOps(new(container.Ops).
Dev(container.MustAbs("/dev"), true),
),
earlyMnt(
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
ent("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
ent("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
),
1971, 100, nil, 0, seccomp.PresetStrict},
{"dev no mqueue", true, true /* go test output is not a tty */, false, false,
earlyOps(new(container.Ops).
Dev(container.MustAbs("/dev"), false),
),
earlyMnt(
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
ent("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
),
1971, 100, nil, 0, seccomp.PresetStrict},
{"overlay", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) {
tempDir := container.MustAbs(t.TempDir())
lower0, lower1, upper, work :=
tempDir.Append("lower0"),
tempDir.Append("lower1"),
tempDir.Append("upper"),
tempDir.Append("work")
for _, a := range []*container.Absolute{lower0, lower1, upper, work} {
if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err)
}
}
return new(container.Ops).
Overlay(hst.AbsTmp, upper, work, lower0, lower1),
context.WithValue(context.WithValue(context.WithValue(context.WithValue(t.Context(),
testVal("lower1"), lower1),
testVal("lower0"), lower0),
testVal("work"), work),
testVal("upper"), upper)
},
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{
ent("/", hst.Tmp, "rw", "overlay", "overlay",
"rw,lowerdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+
",upperdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(*container.Absolute).String())+
",workdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("work")).(*container.Absolute).String())+
",redirect_dir=nofollow,uuid=on,userxattr"),
}
},
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
{"overlay ephemeral", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) {
tempDir := container.MustAbs(t.TempDir())
lower0, lower1 :=
tempDir.Append("lower0"),
tempDir.Append("lower1")
for _, a := range []*container.Absolute{lower0, lower1} {
if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err)
}
}
return new(container.Ops).
OverlayEphemeral(hst.AbsTmp, lower0, lower1),
t.Context()
},
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{
// contains random suffix
ent("/", hst.Tmp, "rw", "overlay", "overlay", ignore),
}
},
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
{"overlay readonly", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) {
tempDir := container.MustAbs(t.TempDir())
lower0, lower1 :=
tempDir.Append("lower0"),
tempDir.Append("lower1")
for _, a := range []*container.Absolute{lower0, lower1} {
if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err)
}
}
return new(container.Ops).
OverlayReadonly(hst.AbsTmp, lower0, lower1),
context.WithValue(context.WithValue(t.Context(),
testVal("lower1"), lower1),
testVal("lower0"), lower0)
},
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{
ent("/", hst.Tmp, "rw", "overlay", "overlay",
"ro,lowerdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+
",redirect_dir=nofollow,userxattr"),
}
},
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
}
func TestContainer(t *testing.T) {
{
oldVerbose := hlog.Load()
oldOutput := container.GetOutput()
internal.InstallOutput(true)
t.Cleanup(func() { hlog.Store(oldVerbose) })
t.Cleanup(func() { container.SetOutput(oldOutput) })
}
replaceOutput(t)
testCases := []struct {
name string
filter bool
session bool
net bool
ops *container.Ops
mnt []*vfs.MountInfoEntry
host string
rules []seccomp.NativeRule
flags seccomp.ExportFlag
presets seccomp.FilterPreset
}{
{"minimal", true, false, false,
new(container.Ops), nil, "test-minimal",
nil, 0, seccomp.PresetStrict},
{"allow", true, true, true,
new(container.Ops), nil, "test-minimal",
nil, 0, seccomp.PresetExt | seccomp.PresetDenyDevel},
{"no filter", false, true, true,
new(container.Ops), nil, "test-no-filter",
nil, 0, seccomp.PresetExt},
{"custom rules", true, true, true,
new(container.Ops), nil, "test-no-filter",
[]seccomp.NativeRule{
{seccomp.ScmpSyscall(syscall.SYS_SETUID), seccomp.ScmpErrno(syscall.EPERM), nil},
}, 0, seccomp.PresetExt},
{"tmpfs", true, false, false,
new(container.Ops).
Tmpfs(hst.Tmp, 0, 0755),
[]*vfs.MountInfoEntry{
e("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
}, "test-tmpfs",
nil, 0, seccomp.PresetStrict},
{"dev", true, true /* go test output is not a tty */, false,
new(container.Ops).
Dev("/dev").
Mqueue("/dev/mqueue"),
[]*vfs.MountInfoEntry{
e("/", "/dev", "rw,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
e("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
e("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
}, "",
nil, 0, seccomp.PresetStrict},
}
t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) {
wantErr := context.Canceled
wantExitCode := 0
if err := c.Wait(); !errors.Is(err, wantErr) {
container.GetOutput().PrintBaseErr(err, "wait:")
t.Errorf("Wait: error = %v, want %v", err, wantErr)
}
if ps := c.ProcessState(); ps == nil {
t.Errorf("ProcessState unexpectedly returned nil")
} else if code := ps.ExitCode(); code != wantExitCode {
t.Errorf("ExitCode: %d, want %d", code, wantExitCode)
}
}))
for _, tc := range testCases {
t.Run("forward", testContainerCancel(func(c *container.Container) {
c.ForwardCancel = true
}, func(t *testing.T, c *container.Container) {
var exitError *exec.ExitError
if err := c.Wait(); !errors.As(err, &exitError) {
container.GetOutput().PrintBaseErr(err, "wait:")
t.Errorf("Wait: error = %v", err)
}
if code := exitError.ExitCode(); code != blockExitCodeInterrupt {
t.Errorf("ExitCode: %d, want %d", code, blockExitCodeInterrupt)
}
}))
for i, tc := range containerTestCases {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
wantOps, wantOpsCtx := tc.ops(t)
wantMnt := tc.mnt(t, wantOpsCtx)
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
defer cancel()
c := container.New(ctx, "/usr/bin/sandbox.test", "-test.v",
"-test.run=TestHelperCheckContainer", "--", "check", tc.host)
c.Uid = 1000
c.Gid = 100
c.Hostname = tc.host
c.CommandContext = commandContext
c.Stdout, c.Stderr = os.Stdout, os.Stderr
c.Ops = tc.ops
var libPaths []*container.Absolute
c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i))
c.Uid = tc.uid
c.Gid = tc.gid
c.Hostname = hostnameFromTestCase(tc.name)
output := new(bytes.Buffer)
if !testing.Verbose() {
c.Stdout, c.Stderr = output, output
} else {
c.Stdout, c.Stderr = os.Stdout, os.Stderr
}
c.WaitDelay = helperDefaultTimeout
*c.Ops = append(*c.Ops, *wantOps...)
c.SeccompRules = tc.rules
c.SeccompFlags = tc.flags | seccomp.AllowMultiarch
c.SeccompPresets = tc.presets
c.SeccompDisable = !tc.filter
c.RetainSession = tc.session
c.HostNet = tc.net
if c.Args[5] == "" {
if name, err := os.Hostname(); err != nil {
t.Fatalf("cannot get hostname: %v", err)
} else {
c.Args[5] = name
}
}
c.
Tmpfs("/tmp", 0, 0755).
Bind(os.Args[0], os.Args[0], 0).
Mkdir("/usr/bin", 0755).
Link(os.Args[0], "/usr/bin/sandbox.test").
Place("/etc/hostname", []byte(c.Args[5]))
// in case test has cgo enabled
var libPaths []string
if entries, err := ldd.ExecFilter(ctx,
commandContext,
func(v []byte) []byte {
return bytes.SplitN(v, []byte("TestHelperInit\n"), 2)[1]
}, os.Args[0]); err != nil {
log.Fatalf("ldd: %v", err)
} else {
libPaths = ldd.Path(entries)
}
for _, name := range libPaths {
c.Bind(name, name, 0)
}
Readonly(container.MustAbs(pathReadonly), 0755).
Tmpfs(container.MustAbs("/tmp"), 0, 0755).
Place(container.MustAbs("/etc/hostname"), []byte(c.Hostname))
// needs /proc to check mountinfo
c.Proc("/proc")
c.Proc(container.MustAbs("/proc"))
// mountinfo cannot be resolved directly by helper due to libPaths nondeterminism
mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
mnt = append(mnt, e("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore))
mnt = append(mnt, tc.mnt...)
mnt = append(mnt,
e("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
e(ignore, os.Args[0], "ro,nosuid,nodev,relatime", ignore, ignore, ignore),
e(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
ent("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
// Bind(os.Args[0], helperInnerPath, 0)
ent(ignore, helperInnerPath, "ro,nosuid,nodev,relatime", ignore, ignore, ignore),
)
for _, name := range libPaths {
mnt = append(mnt, e(ignore, name, "ro,nosuid,nodev,relatime", ignore, ignore, ignore))
for _, a := range libPaths {
// Bind(name, name, 0)
mnt = append(mnt, ent(ignore, a.String(), "ro,nosuid,nodev,relatime", ignore, ignore, ignore))
}
mnt = append(mnt, e("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"))
mnt = append(mnt, wantMnt...)
mnt = append(mnt,
// Readonly(pathReadonly, 0755)
ent("/", pathReadonly, "ro,nosuid,nodev", "tmpfs", "readonly", ignore),
// Tmpfs("/tmp", 0, 0755)
ent("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", ignore),
// Place("/etc/hostname", []byte(hostname))
ent(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
// Proc("/proc")
ent("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"),
// Place(pathWantMnt, want.Bytes())
ent(ignore, pathWantMnt, "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
)
want := new(bytes.Buffer)
if err := gob.NewEncoder(want).Encode(mnt); err != nil {
_, _ = output.WriteTo(os.Stdout)
t.Fatalf("cannot serialise expected mount points: %v", err)
}
c.Stdin = want
c.Place(container.MustAbs(pathWantMnt), want.Bytes())
if tc.ro {
c.Remount(container.MustAbs("/"), syscall.MS_RDONLY)
}
if err := c.Start(); err != nil {
hlog.PrintBaseError(err, "start:")
_, _ = output.WriteTo(os.Stdout)
container.GetOutput().PrintBaseErr(err, "start:")
t.Fatalf("cannot start container: %v", err)
} else if err = c.Serve(); err != nil {
hlog.PrintBaseError(err, "serve:")
_, _ = output.WriteTo(os.Stdout)
container.GetOutput().PrintBaseErr(err, "serve:")
t.Errorf("cannot serve setup params: %v", err)
}
if err := c.Wait(); err != nil {
hlog.PrintBaseError(err, "wait:")
_, _ = output.WriteTo(os.Stdout)
container.GetOutput().PrintBaseErr(err, "wait:")
t.Fatalf("wait: %v", err)
}
})
}
}
func e(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry {
func ent(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry {
return &vfs.MountInfoEntry{
ID: ignoreV,
Parent: ignoreV,
@@ -184,8 +344,52 @@ func e(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoE
}
}
func hostnameFromTestCase(name string) string {
return "test-" + strings.Join(strings.Fields(name), "-")
}
func testContainerCancel(
containerExtra func(c *container.Container),
waitCheck func(t *testing.T, c *container.Container),
) func(t *testing.T) {
return func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
c := helperNewContainer(ctx, "block")
c.Stdout, c.Stderr = os.Stdout, os.Stderr
c.WaitDelay = helperDefaultTimeout
if containerExtra != nil {
containerExtra(c)
}
ready := make(chan struct{})
if r, w, err := os.Pipe(); err != nil {
t.Fatalf("cannot pipe: %v", err)
} else {
c.ExtraFiles = append(c.ExtraFiles, w)
go func() {
defer close(ready)
if _, err = r.Read(make([]byte, 1)); err != nil {
panic(err.Error())
}
}()
}
if err := c.Start(); err != nil {
container.GetOutput().PrintBaseErr(err, "start:")
t.Fatalf("cannot start container: %v", err)
} else if err = c.Serve(); err != nil {
container.GetOutput().PrintBaseErr(err, "serve:")
t.Errorf("cannot serve setup params: %v", err)
}
<-ready
cancel()
waitCheck(t, c)
}
}
func TestContainerString(t *testing.T) {
c := container.New(t.Context(), "ldd", "/usr/bin/env")
c := container.NewCommand(t.Context(), container.MustAbs("/run/current-system/sw/bin/ldd"), "ldd", "/usr/bin/env")
c.SeccompFlags |= seccomp.AllowMultiarch
c.SeccompRules = seccomp.Preset(
seccomp.PresetExt|seccomp.PresetDenyNS|seccomp.PresetDenyTTY,
@@ -197,85 +401,176 @@ func TestContainerString(t *testing.T) {
}
}
func TestHelperInit(t *testing.T) {
if len(os.Args) != 5 || os.Args[4] != "init" {
return
}
container.SetOutput(hlog.Output{})
container.Init(hlog.Prepare, internal.InstallOutput)
}
const (
blockExitCodeInterrupt = 2
)
func TestHelperCheckContainer(t *testing.T) {
if len(os.Args) != 6 || os.Args[4] != "check" {
return
}
t.Run("user", func(t *testing.T) {
if uid := syscall.Getuid(); uid != 1000 {
t.Errorf("Getuid: %d, want 1000", uid)
}
if gid := syscall.Getgid(); gid != 100 {
t.Errorf("Getgid: %d, want 100", gid)
}
})
t.Run("hostname", func(t *testing.T) {
if name, err := os.Hostname(); err != nil {
t.Fatalf("cannot get hostname: %v", err)
} else if name != os.Args[5] {
t.Errorf("Hostname: %q, want %q", name, os.Args[5])
}
if p, err := os.ReadFile("/etc/hostname"); err != nil {
t.Fatalf("%v", err)
} else if string(p) != os.Args[5] {
t.Errorf("/etc/hostname: %q, want %q", string(p), os.Args[5])
}
})
t.Run("mount", func(t *testing.T) {
var mnt []*vfs.MountInfoEntry
if err := gob.NewDecoder(os.Stdin).Decode(&mnt); err != nil {
t.Fatalf("cannot receive expected mount points: %v", err)
}
var d *vfs.MountInfoDecoder
if f, err := os.Open("/proc/self/mountinfo"); err != nil {
t.Fatalf("cannot open mountinfo: %v", err)
} else {
d = vfs.NewMountInfoDecoder(f)
}
i := 0
for cur := range d.Entries() {
if i == len(mnt) {
t.Errorf("got more than %d entries", len(mnt))
break
func init() {
helperCommands = append(helperCommands, func(c command.Command) {
c.Command("block", command.UsageInternal, func(args []string) error {
if _, err := os.NewFile(3, "sync").Write([]byte{0}); err != nil {
return fmt.Errorf("write to sync pipe: %v", err)
}
{
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt)
go func() { <-sig; os.Exit(blockExitCodeInterrupt) }()
}
select {}
})
// ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",relatime")
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",noatime")
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",relatime")
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime")
if !cur.EqualWithIgnore(mnt[i], "\x00") {
t.Errorf("[FAIL] %s", cur)
c.Command("container", command.UsageInternal, func(args []string) error {
if len(args) != 1 {
return syscall.EINVAL
}
tc := containerTestCases[0]
if i, err := strconv.Atoi(args[0]); err != nil {
return fmt.Errorf("cannot parse test case index: %v", err)
} else {
t.Logf("[ OK ] %s", cur)
tc = containerTestCases[i]
}
i++
}
if err := d.Err(); err != nil {
t.Errorf("cannot parse mountinfo: %v", err)
}
if uid := syscall.Getuid(); uid != tc.uid {
return fmt.Errorf("uid: %d, want %d", uid, tc.uid)
}
if gid := syscall.Getgid(); gid != tc.gid {
return fmt.Errorf("gid: %d, want %d", gid, tc.gid)
}
if i != len(mnt) {
t.Errorf("got %d entries, want %d", i, len(mnt))
}
wantHost := hostnameFromTestCase(tc.name)
if host, err := os.Hostname(); err != nil {
return fmt.Errorf("cannot get hostname: %v", err)
} else if host != wantHost {
return fmt.Errorf("hostname: %q, want %q", host, wantHost)
}
if p, err := os.ReadFile("/etc/hostname"); err != nil {
return fmt.Errorf("cannot read /etc/hostname: %v", err)
} else if string(p) != wantHost {
return fmt.Errorf("/etc/hostname: %q, want %q", string(p), wantHost)
}
if _, err := os.Create(pathReadonly + "/nonexistent"); !errors.Is(err, syscall.EROFS) {
return err
}
{
var fail bool
var mnt []*vfs.MountInfoEntry
if f, err := os.Open(pathWantMnt); err != nil {
return fmt.Errorf("cannot open expected mount points: %v", err)
} else if err = gob.NewDecoder(f).Decode(&mnt); err != nil {
return fmt.Errorf("cannot parse expected mount points: %v", err)
} else if err = f.Close(); err != nil {
return fmt.Errorf("cannot close expected mount points: %v", err)
}
if tc.ro && len(mnt) > 0 {
// Remount("/", syscall.MS_RDONLY)
mnt[0].VfsOptstr = "ro,nosuid,nodev"
}
var d *vfs.MountInfoDecoder
if f, err := os.Open("/proc/self/mountinfo"); err != nil {
return fmt.Errorf("cannot open mountinfo: %v", err)
} else {
d = vfs.NewMountInfoDecoder(f)
}
i := 0
for cur := range d.Entries() {
if i == len(mnt) {
return fmt.Errorf("got more than %d entries", len(mnt))
}
// ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",relatime")
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",noatime")
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",relatime")
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime")
if !cur.EqualWithIgnore(mnt[i], "\x00") {
fail = true
log.Printf("[FAIL] %s", cur)
} else {
log.Printf("[ OK ] %s", cur)
}
i++
}
if err := d.Err(); err != nil {
return fmt.Errorf("cannot parse mountinfo: %v", err)
}
if i != len(mnt) {
return fmt.Errorf("got %d entries, want %d", i, len(mnt))
}
if fail {
return errors.New("one or more mountinfo entries do not match")
}
}
return nil
})
})
}
func commandContext(ctx context.Context) *exec.Cmd {
return exec.CommandContext(ctx, os.Args[0], "-test.v",
"-test.run=TestHelperInit", "--", "init")
const (
envDoCheck = "HAKUREI_TEST_DO_CHECK"
helperDefaultTimeout = 5 * time.Second
helperInnerPath = "/usr/bin/helper"
)
var (
absHelperInnerPath = container.MustAbs(helperInnerPath)
)
var helperCommands []func(c command.Command)
func TestMain(m *testing.M) {
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
if os.Getenv(envDoCheck) == "1" {
c := command.New(os.Stderr, log.Printf, "helper", func(args []string) error {
log.SetFlags(0)
log.SetPrefix("helper: ")
return nil
})
for _, f := range helperCommands {
f(c)
}
c.MustParse(os.Args[1:], func(err error) {
if err != nil {
log.Fatal(err.Error())
}
})
return
}
os.Exit(m.Run())
}
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*container.Absolute, args ...string) (c *container.Container) {
c = container.NewCommand(ctx, absHelperInnerPath, "helper", args...)
c.Env = append(c.Env, envDoCheck+"=1")
c.Bind(container.MustAbs(os.Args[0]), absHelperInnerPath, 0)
// in case test has cgo enabled
if entries, err := ldd.Exec(ctx, os.Args[0]); err != nil {
log.Fatalf("ldd: %v", err)
} else {
*libPaths = ldd.Path(entries)
}
for _, name := range *libPaths {
c.Bind(name, name, 0)
}
return
}
func helperNewContainer(ctx context.Context, args ...string) (c *container.Container) {
return helperNewContainerLibPaths(ctx, new([]*container.Absolute), args...)
}
+245
View File
@@ -0,0 +1,245 @@
package container
import (
"io"
"io/fs"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"runtime"
"syscall"
"hakurei.app/container/seccomp"
)
type osFile interface {
Name() string
io.Writer
fs.File
}
// syscallDispatcher provides methods that make state-dependent system calls as part of their behaviour.
type syscallDispatcher interface {
// new starts a goroutine with a new instance of syscallDispatcher.
// A syscallDispatcher must never be used in any goroutine other than the one owning it,
// just synchronising access is not enough, as this is for test instrumentation.
new(f func(k syscallDispatcher))
// lockOSThread provides [runtime.LockOSThread].
lockOSThread()
// setPtracer provides [SetPtracer].
setPtracer(pid uintptr) error
// setDumpable provides [SetDumpable].
setDumpable(dumpable uintptr) error
// setNoNewPrivs provides [SetNoNewPrivs].
setNoNewPrivs() error
// lastcap provides [LastCap].
lastcap() uintptr
// capset provides capset.
capset(hdrp *capHeader, datap *[2]capData) error
// capBoundingSetDrop provides capBoundingSetDrop.
capBoundingSetDrop(cap uintptr) error
// capAmbientClearAll provides capAmbientClearAll.
capAmbientClearAll() error
// capAmbientRaise provides capAmbientRaise.
capAmbientRaise(cap uintptr) error
// isatty provides [Isatty].
isatty(fd int) bool
// receive provides [Receive].
receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
// bindMount provides procPaths.bindMount.
bindMount(source, target string, flags uintptr, eq bool) error
// remount provides procPaths.remount.
remount(target string, flags uintptr) error
// mountTmpfs provides mountTmpfs.
mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error
// ensureFile provides ensureFile.
ensureFile(name string, perm, pperm os.FileMode) error
// seccompLoad provides [seccomp.Load].
seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error
// notify provides [signal.Notify].
notify(c chan<- os.Signal, sig ...os.Signal)
// start starts [os/exec.Cmd].
start(c *exec.Cmd) error
// signal signals the underlying process of [os/exec.Cmd].
signal(c *exec.Cmd, sig os.Signal) error
// evalSymlinks provides [filepath.EvalSymlinks].
evalSymlinks(path string) (string, error)
// exit provides [os.Exit].
exit(code int)
// getpid provides [os.Getpid].
getpid() int
// stat provides [os.Stat].
stat(name string) (os.FileInfo, error)
// mkdir provides [os.Mkdir].
mkdir(name string, perm os.FileMode) error
// mkdirTemp provides [os.MkdirTemp].
mkdirTemp(dir, pattern string) (string, error)
// mkdirAll provides [os.MkdirAll].
mkdirAll(path string, perm os.FileMode) error
// readdir provides [os.ReadDir].
readdir(name string) ([]os.DirEntry, error)
// openNew provides [os.Open].