767 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
cat e6967b8bbb release: 0.1.0
Test / Create distribution (push) Successful in 37s
Release / Create release (push) Successful in 46s
Test / Sandbox (push) Successful in 46s
Test / Sandbox (race detector) (push) Successful in 47s
Test / Hakurei (push) Successful in 51s
Test / Planterette (push) Successful in 49s
Test / Hakurei (race detector) (push) Successful in 53s
Test / Flake checks (push) Successful in 1m14s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-03 03:42:58 +09:00
cat d2f9a9b83b treewide: migrate to hakurei.app
Test / Create distribution (push) Successful in 24s
Test / Sandbox (push) Successful in 46s
Test / Hakurei (push) Successful in 2m9s
Test / Sandbox (race detector) (push) Successful in 3m14s
Test / Planterette (push) Successful in 3m41s
Test / Hakurei (race detector) (push) Successful in 3m40s
Test / Flake checks (push) Successful in 1m18s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-03 03:30:39 +09:00
cat 1b5ecd9eaf container: move out of toplevel
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m52s
Test / Sandbox (race detector) (push) Successful in 3m14s
Test / Planterette (push) Successful in 3m36s
Test / Hakurei (race detector) (push) Successful in 4m31s
Test / Hakurei (push) Successful in 2m3s
Test / Flake checks (push) Successful in 1m13s
This allows slightly easier use of the vanity url. This also provides some disambiguation between low level containers and hakurei app containers.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-03 02:59:43 +09:00
cat 82561d62b6 system: move system access packages
Test / Create distribution (push) Successful in 31s
Test / Sandbox (push) Successful in 1m52s
Test / Hakurei (push) Successful in 3m3s
Test / Planterette (push) Successful in 3m38s
Test / Hakurei (race detector) (push) Successful in 4m48s
Test / Sandbox (race detector) (push) Successful in 1m14s
Test / Flake checks (push) Successful in 1m6s
These packages loosely belong in the "system" package and "system" provides high level wrappers for all of them.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 21:52:07 +09:00
cat eec021cc4b hakurei: move container helpers toplevel
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m1s
Test / Hakurei (push) Successful in 2m52s
Test / Sandbox (race detector) (push) Successful in 3m8s
Test / Planterette (push) Successful in 3m32s
Test / Hakurei (race detector) (push) Successful in 4m27s
Test / Flake checks (push) Successful in 1m9s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 21:31:29 +09:00
cat a1d98823f8 hakurei: move container toplevel
Test / Create distribution (push) Successful in 31s
Test / Sandbox (push) Successful in 1m55s
Test / Hakurei (push) Successful in 2m47s
Test / Sandbox (race detector) (push) Successful in 3m16s
Test / Planterette (push) Successful in 3m32s
Test / Hakurei (race detector) (push) Successful in 4m25s
Test / Flake checks (push) Successful in 1m9s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 21:23:55 +09:00
cat 255b77d91d cmd/hakurei: move command handlers
Test / Create distribution (push) Successful in 31s
Test / Sandbox (push) Successful in 1m55s
Test / Hakurei (push) Successful in 2m49s
Test / Sandbox (race detector) (push) Successful in 3m8s
Test / Planterette (push) Successful in 3m32s
Test / Hakurei (race detector) (push) Successful in 4m31s
Test / Flake checks (push) Successful in 1m6s
The hakurei command is a bit ugly since it's also used for validating the command package. This alleviates some of the ugliness.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 20:59:17 +09:00
cat f84ec5a3f8 sandbox/wl: track generated files
Test / Create distribution (push) Successful in 31s
Test / Sandbox (push) Successful in 1m54s
Test / Hakurei (push) Successful in 2m58s
Test / Sandbox (race detector) (push) Successful in 3m16s
Test / Planterette (push) Successful in 3m36s
Test / Hakurei (race detector) (push) Successful in 4m31s
Test / Flake checks (push) Successful in 1m9s
This allows the package to be imported.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 20:52:22 +09:00
cat eb22a8bcc1 cmd/hakurei: move to cmd
Test / Create distribution (push) Successful in 31s
Test / Sandbox (push) Successful in 1m50s
Test / Hakurei (push) Successful in 3m2s
Test / Sandbox (race detector) (push) Successful in 3m18s
Test / Planterette (push) Successful in 3m36s
Test / Hakurei (race detector) (push) Successful in 4m35s
Test / Flake checks (push) Successful in 1m7s
Having it at the project root never made sense since the "ego" name was deprecated. This change finally addresses it.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 20:42:51 +09:00
cat 31aef905fa sandbox: expose seccomp interface
Test / Create distribution (push) Successful in 31s
Test / Sandbox (push) Successful in 1m59s
Test / Hakurei (push) Successful in 2m47s
Test / Sandbox (race detector) (push) Successful in 3m11s
Test / Planterette (push) Successful in 3m34s
Test / Hakurei (race detector) (push) Successful in 4m22s
Test / Flake checks (push) Successful in 1m8s
There's no point in artificially limiting and abstracting away these options. The higher level hakurei package is responsible for providing a secure baseline and sane defaults. The sandbox package should present everything to the caller.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 04:47:13 +09:00
cat a6887f7253 sandbox/seccomp: import dot for syscall
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m55s
Test / Sandbox (race detector) (push) Successful in 3m7s
Test / Planterette (push) Successful in 3m31s
Test / Hakurei (race detector) (push) Successful in 4m19s
Test / Hakurei (push) Successful in 1m57s
Test / Flake checks (push) Successful in 1m11s
This significantly increases readability in some places.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 02:30:35 +09:00
cat 69bd581af7 sandbox/seccomp: append suffix to ops
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m50s
Test / Hakurei (push) Successful in 2m54s
Test / Sandbox (race detector) (push) Successful in 3m9s
Test / Planterette (push) Successful in 4m5s
Test / Hakurei (race detector) (push) Successful in 4m44s
Test / Flake checks (push) Successful in 1m31s
This avoids clashes with stdlib names to allow for . imports.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 01:09:04 +09:00
cat 26b7afc890 sandbox/seccomp: prepare -> export
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m51s
Test / Sandbox (race detector) (push) Successful in 3m3s
Test / Planterette (push) Successful in 3m37s
Test / Hakurei (race detector) (push) Successful in 4m17s
Test / Hakurei (push) Successful in 2m12s
Test / Flake checks (push) Successful in 1m12s
Export makes a lot more sense, and also matches the libseccomp function.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 00:32:48 +09:00
cat d5532aade0 sandbox/seccomp: native rule slice in helpers
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m6s
Test / Hakurei (push) Successful in 2m49s
Test / Sandbox (race detector) (push) Successful in 3m8s
Test / Planterette (push) Successful in 3m33s
Test / Hakurei (race detector) (push) Successful in 4m16s
Test / Flake checks (push) Successful in 1m16s
These helper functions took FilterPreset as input for ease of integration. This moves them to []NativeRule.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 00:22:27 +09:00
cat 0c5409aec7 sandbox/seccomp: native rule type alias
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m57s
Test / Hakurei (push) Successful in 2m49s
Test / Sandbox (race detector) (push) Successful in 3m4s
Test / Planterette (push) Successful in 3m39s
Test / Hakurei (race detector) (push) Successful in 4m20s
Test / Flake checks (push) Successful in 1m13s
This makes it easier to keep API stable.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 00:00:08 +09:00
cat 1a8840bebc sandbox/seccomp: resolve rules natively
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m45s
Test / Hakurei (push) Successful in 2m49s
Test / Sandbox (race detector) (push) Successful in 3m1s
Test / Planterette (push) Successful in 3m31s
Test / Hakurei (race detector) (push) Successful in 4m18s
Test / Flake checks (push) Successful in 1m6s
This enables loading syscall filter policies from external cross-platform config files.

This also removes a significant amount of C code.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-01 22:11:32 +09:00
cat 1fb453dffe sandbox/seccomp: extra constants
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m59s
Test / Hakurei (push) Successful in 2m44s
Test / Sandbox (race detector) (push) Successful in 3m1s
Test / Planterette (push) Successful in 3m33s
Test / Hakurei (race detector) (push) Successful in 4m20s
Test / Flake checks (push) Successful in 1m7s
These all resolve to pseudo syscall numbers in libseccomp, but are necessary anyway for other platforms.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-01 20:15:42 +09:00
cat e03d702d08 sandbox/seccomp: implement syscall lookup
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m51s
Test / Hakurei (push) Successful in 2m52s
Test / Sandbox (race detector) (push) Successful in 3m20s
Test / Planterette (push) Successful in 3m40s
Test / Hakurei (race detector) (push) Successful in 4m18s
Test / Flake checks (push) Successful in 1m10s
This uses the Go map and is verified against libseccomp.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-01 00:35:27 +09:00
cat 241dc964a6 sandbox/seccomp: wire extra syscall
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m46s
Test / Hakurei (push) Successful in 2m48s
Test / Sandbox (race detector) (push) Successful in 3m6s
Test / Planterette (push) Successful in 40s
Test / Hakurei (race detector) (push) Successful in 2m39s
Test / Flake checks (push) Successful in 1m15s
These values are only useful for libseccomp.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-01 00:32:08 +09:00
cat 8ef71e14d5 sandbox/seccomp: emit syscall constants
Test / Create distribution (push) Successful in 44s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m8s
Test / Sandbox (race detector) (push) Successful in 3m18s
Test / Planterette (push) Successful in 3m55s
Test / Hakurei (race detector) (push) Successful in 4m37s
Test / Flake checks (push) Successful in 1m9s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-06-30 20:34:33 +09:00
cat 972f4006f0 treewide: switch to hakurei.app
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m0s
Test / Hakurei (push) Successful in 2m49s
Test / Sandbox (race detector) (push) Successful in 3m12s
Test / Planterette (push) Successful in 3m35s
Test / Hakurei (race detector) (push) Successful in 4m22s
Test / Flake checks (push) Successful in 1m7s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-06-26 04:01:02 +09:00
cat 9a8a047908 sandbox/seccomp: syscall name lookup table
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m58s
Test / Hakurei (push) Successful in 2m42s
Test / Sandbox (race detector) (push) Successful in 2m59s
Test / Planterette (push) Successful in 3m31s
Test / Hakurei (race detector) (push) Successful in 4m21s
Test / Flake checks (push) Successful in 1m9s
The script is from Go source of same name. The result is checked against libseccomp.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-06-26 03:49:07 +09:00
cat 863bf69ad3 treewide: reapply clang-format
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m51s
Test / Hakurei (push) Successful in 2m49s
Test / Sandbox (race detector) (push) Successful in 2m58s
Test / Planterette (push) Successful in 3m37s
Test / Hakurei (race detector) (push) Successful in 4m15s
Test / Flake checks (push) Successful in 1m8s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-06-25 23:43:42 +09:00
cat 0e957cc9c1 release: 0.0.2
Release / Create release (push) Successful in 43s
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 40s
Test / Hakurei (push) Successful in 45s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Planterette (push) Successful in 1m41s
Test / Hakurei (race detector) (push) Successful in 1m44s
Test / Flake checks (push) Successful in 1m14s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-06-25 21:11:11 +09:00
cat aa454b158f cmd/planterette: remove hsu special case
Test / Hakurei (push) Successful in 42s
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 40s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Sandbox (race detector) (push) Successful in 38s
Test / Planterette (push) Successful in 40s
Test / Flake checks (push) Successful in 1m15s
Remove special case and invoke hakurei out of process.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-06-25 20:50:24 +09:00
cat 7007bd6a1c workflows: port release workflow to github
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m55s
Test / Hakurei (push) Successful in 2m46s
Test / Sandbox (race detector) (push) Successful in 3m6s
Test / Fpkg (push) Successful in 3m31s
Test / Hakurei (race detector) (push) Successful in 4m15s
Test / Flake checks (push) Successful in 1m8s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-06-25 20:17:53 +09:00
cat 00efc95ee7 workflows: port test workflow to github
Test / Create distribution (push) Successful in 24s
Test / Sandbox (push) Successful in 1m29s
Test / Sandbox (race detector) (push) Successful in 2m54s
Test / Fpkg (push) Successful in 3m10s
Test / Hakurei (race detector) (push) Successful in 4m10s
Test / Hakurei (push) Successful in 1m57s
Test / Flake checks (push) Successful in 1m8s
This is a much less useful port of the test workflow and runs much slower due to runner limitations.

Still better than nothing though.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-06-25 19:37:45 +09:00
cat b380bb248c release: 0.0.1
Release / Create release (push) Successful in 40s
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 38s
Test / Sandbox (race detector) (push) Successful in 38s
Test / Hakurei (push) Successful in 42s
Test / Hakurei (race detector) (push) Successful in 41s
Test / Fpkg (push) Successful in 39s
Test / Flake checks (push) Successful in 1m10s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-06-25 05:05:06 +09:00
cat 87e008d56d treewide: rename to hakurei
Test / Create distribution (push) Successful in 43s
Test / Sandbox (push) Successful in 2m18s
Test / Hakurei (push) Successful in 3m10s
Test / Sandbox (race detector) (push) Successful in 3m30s
Test / Hakurei (race detector) (push) Successful in 4m43s
Test / Fpkg (push) Successful in 5m4s
Test / Flake checks (push) Successful in 1m12s
Fortify makes little sense for a container tool.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-06-25 04:57:41 +09:00
cat 3992073212 dist: move comp to dist
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m58s
Test / Fortify (push) Successful in 2m49s
Test / Sandbox (race detector) (push) Successful in 3m13s
Test / Fpkg (push) Successful in 3m39s
Test / Fortify (race detector) (push) Successful in 4m17s
Test / Flake checks (push) Successful in 1m9s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-06-18 17:01:16 +09:00
cat ef80b19f2f treewide: switch to clang-format
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m49s
Test / Fortify (push) Successful in 2m44s
Test / Sandbox (race detector) (push) Successful in 3m5s
Test / Fpkg (push) Successful in 3m32s
Test / Fortify (race detector) (push) Successful in 4m15s
Test / Flake checks (push) Successful in 1m4s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-06-18 13:45:34 +09:00
cat 717771ae80 app: share runtime dir
Test / Create distribution (push) Successful in 24s
Test / Sandbox (race detector) (push) Successful in 37s
Test / Sandbox (push) Successful in 37s
Test / Fortify (push) Successful in 40s
Test / Fortify (race detector) (push) Successful in 40s
Test / Fpkg (push) Successful in 38s
Test / Flake checks (push) Successful in 1m5s
This allows apps with the same identity to access the same runtime dir.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-06-08 03:24:48 +09:00
cat bf5772bd8a nix: deduplicate home-manager merging
Test / Create distribution (push) Successful in 44s
Test / Sandbox (push) Successful in 55s
Test / Sandbox (race detector) (push) Successful in 53s
Test / Fortify (race detector) (push) Successful in 50s
Test / Fpkg (push) Successful in 54s
Test / Fortify (push) Successful in 2m8s
Test / Flake checks (push) Successful in 1m7s
This becomes a problem when extraHomeConfig defines nixos module options.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-06-08 01:12:18 +09:00
cat 9a7c81a44e nix: go generate in src derivation
Test / Sandbox (push) Successful in 40s
Test / Fortify (race detector) (push) Successful in 49s
Test / Fortify (push) Successful in 50s
Test / Create distribution (push) Successful in 24s
Test / Sandbox (race detector) (push) Successful in 45s
Test / Fpkg (push) Successful in 39s
Test / Flake checks (push) Successful in 1m12s
This saves the generated files in the nix store and exposes them for use by external tools.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-06-07 03:10:36 +09:00
cat b7e991de5b nix: update flake lock
Test / Create distribution (push) Successful in 51s
Test / Sandbox (push) Successful in 15m56s
Test / Sandbox (race detector) (push) Successful in 16m5s
Test / Fpkg (push) Successful in 17m33s
Test / Fortify (race detector) (push) Successful in 2m28s
Test / Fortify (push) Successful in 40s
Test / Flake checks (push) Successful in 2m58s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-06-05 04:05:39 +09:00
cat 6c1205106d release: 0.4.1
Release / Create release (push) Successful in 59s
Test / Sandbox (push) Successful in 1m2s
Test / Sandbox (race detector) (push) Successful in 5m25s
Test / Create distribution (push) Successful in 28s
Test / Fpkg (push) Successful in 8m35s
Test / Fortify (push) Successful in 8m57s
Test / Fortify (race detector) (push) Successful in 10m5s
Test / Flake checks (push) Successful in 1m45s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-05-26 02:55:19 +09:00
cat 2ffca6984a nix: use reverse-DNS style id as unique identifier
Test / Create distribution (push) Successful in 19s
Test / Sandbox (push) Successful in 31s
Test / Fortify (push) Successful in 35s
Test / Sandbox (race detector) (push) Successful in 31s
Test / Fortify (race detector) (push) Successful in 35s
Test / Fpkg (push) Successful in 33s
Test / Flake checks (push) Successful in 1m7s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-05-25 20:12:30 +09:00
cat dde2516304 dbus: handle bizarre dbus proxy behaviour
Test / Create distribution (push) Successful in 28s
Test / Sandbox (push) Successful in 1m53s
Test / Fortify (push) Successful in 2m44s
Test / Sandbox (race detector) (push) Successful in 3m2s
Test / Fpkg (push) Successful in 3m36s
Test / Fortify (race detector) (push) Successful in 4m16s
Test / Flake checks (push) Successful in 1m17s
There is a strange behaviour in xdg-dbus-proxy where if any interface string when stripped of a single ".*" suffix does not contain a '.' byte anywhere, the program will exit with code 1 without any output. This checks for such conditions to make the failure less confusing.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-05-25 19:50:06 +09:00
cat f30a439bcd nix: improve common usability
Test / Create distribution (push) Successful in 19s
Test / Sandbox (push) Successful in 31s
Test / Fortify (push) Successful in 35s
Test / Sandbox (race detector) (push) Successful in 31s
Test / Fortify (race detector) (push) Successful in 35s
Test / Fpkg (push) Successful in 33s
Test / Flake checks (push) Successful in 1m7s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-05-16 04:40:12 +09:00
cat 008e9e7fc5 nix: update flake lock
Test / Create distribution (push) Successful in 28s
Test / Fortify (push) Successful in 38s
Test / Fortify (race detector) (push) Successful in 37s
Test / Fpkg (push) Successful in 35s
Test / Sandbox (push) Successful in 1m18s
Test / Sandbox (race detector) (push) Successful in 1m27s
Test / Flake checks (push) Successful in 2m47s
2025-05-07 21:35:37 +09:00
cat 23aefcd759 fortify: update help strings
Test / Create distribution (push) Successful in 30s
Test / Sandbox (push) Successful in 1m58s
Test / Sandbox (race detector) (push) Successful in 3m11s
Test / Fpkg (push) Successful in 4m24s
Test / Fortify (race detector) (push) Successful in 4m58s
Test / Fortify (push) Successful in 3m44s
Test / Flake checks (push) Successful in 1m34s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-05-07 19:06:36 +09:00
cat cb8b886446 nix: update flake lock
Test / Create distribution (push) Successful in 1m28s
Test / Fortify (push) Successful in 49m23s
Test / Fortify (race detector) (push) Successful in 49m56s
Test / Fpkg (push) Successful in 50m14s
Test / Sandbox (push) Successful in 1m18s
Test / Sandbox (race detector) (push) Successful in 1m20s
Test / Flake checks (push) Successful in 3m0s
2025-04-22 22:23:21 +09:00
cat 5979d8b1e0 dbus: clean up wrapper implementation
Test / Create distribution (push) Successful in 27s
Test / Sandbox (push) Successful in 1m50s
Test / Fortify (push) Successful in 2m49s
Test / Sandbox (race detector) (push) Successful in 3m4s
Test / Fpkg (push) Successful in 3m35s
Test / Fortify (race detector) (push) Successful in 4m13s
Test / Flake checks (push) Successful in 1m3s
The dbus proxy wrapper haven't been updated much ever since the helper interface was introduced.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-04-16 23:35:17 +09:00
cat e587112e63 test: check xdg-dbus-proxy termination
Test / Sandbox (race detector) (push) Successful in 31s
Test / Sandbox (push) Successful in 33s
Test / Create distribution (push) Successful in 28s
Test / Fpkg (push) Successful in 35s
Test / Fortify (push) Successful in 2m9s
Test / Fortify (race detector) (push) Successful in 2m37s
Test / Flake checks (push) Successful in 1m2s
This process runs outside the application container's pid namespace, so it is a good idea to check whether its lifecycle becomes decoupled from the application.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-04-15 20:45:31 +09:00
cat d6cf736abf release: 0.4.0
Release / Create release (push) Successful in 54s
Test / Sandbox (push) Successful in 47s
Test / Sandbox (race detector) (push) Successful in 4m44s
Test / Create distribution (push) Successful in 20s
Test / Fortify (race detector) (push) Successful in 6m42s
Test / Fpkg (push) Successful in 2m18s
Test / Fortify (push) Successful in 5m18s
Test / Flake checks (push) Successful in 2m42s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-04-13 11:10:45 +09:00
cat 15011c4173 app/instance/common: optimise ops allocation
Test / Create distribution (push) Successful in 26s
Test / Sandbox (push) Successful in 1m55s
Test / Fortify (push) Successful in 2m46s
Test / Sandbox (race detector) (push) Successful in 3m10s
Test / Fpkg (push) Successful in 3m52s
Test / Fortify (race detector) (push) Successful in 4m23s
Test / Flake checks (push) Successful in 1m2s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-04-13 03:49:07 +09:00
cat 31b7ddd122 fst: improve config
Test / Create distribution (push) Successful in 26s
Test / Sandbox (push) Successful in 1m50s
Test / Fortify (push) Successful in 2m46s
Test / Sandbox (race detector) (push) Successful in 2m59s
Test / Fortify (race detector) (push) Successful in 4m23s
Test / Fpkg (push) Successful in 5m25s
Test / Flake checks (push) Successful in 1m1s
The config struct more or less "grew" to what it is today. This change moves things around to make more sense and fixes nonsensical comments describing obsolete behaviour.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-04-13 03:30:19 +09:00
cat c460892cbd fst: check template
Test / Create distribution (push) Successful in 26s
Test / Sandbox (push) Successful in 1m51s
Test / Fortify (push) Successful in 2m39s
Test / Sandbox (race detector) (push) Successful in 3m7s
Test / Fpkg (push) Successful in 3m36s
Test / Fortify (race detector) (push) Successful in 4m14s
Test / Flake checks (push) Successful in 1m6s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-04-12 18:00:25 +09:00
cat 6309469e93 app/instance: wrap internal implementation
Test / Create distribution (push) Successful in 26s
Test / Sandbox (push) Successful in 1m44s
Test / Fortify (push) Successful in 2m37s
Test / Sandbox (race detector) (push) Successful in 2m59s
Test / Fpkg (push) Successful in 3m34s
Test / Fortify (race detector) (push) Successful in 4m6s
Test / Flake checks (push) Successful in 59s
This reduces the scope of the fst package, which was growing questionably large.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-04-12 13:56:41 +09:00
cat 0d7c1a9a43 app: rename app implementation package
Test / Create distribution (push) Successful in 26s
Test / Sandbox (push) Successful in 1m48s
Test / Fortify (push) Successful in 2m36s
Test / Sandbox (race detector) (push) Successful in 2m52s
Test / Fpkg (push) Successful in 3m32s
Test / Fortify (race detector) (push) Successful in 4m9s
Test / Flake checks (push) Successful in 1m4s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-04-12 10:54:24 +09:00
cat ae6f5ede19 fst: mount passthrough /dev writable
Test / Create distribution (push) Successful in 26s
Test / Sandbox (push) Successful in 1m50s
Test / Fortify (push) Successful in 2m39s
Test / Sandbox (race detector) (push) Successful in 3m1s
Test / Fpkg (push) Successful in 3m30s
Test / Fortify (race detector) (push) Successful in 4m13s
Test / Flake checks (push) Successful in 59s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-04-11 20:01:54 +09:00
cat 807d511c8b test/sandbox: check device outcome
Test / Fortify (push) Successful in 35s
Test / Create distribution (push) Successful in 26s
Test / Fortify (race detector) (push) Successful in 35s
Test / Fpkg (push) Successful in 34s
Test / Sandbox (push) Successful in 1m22s
Test / Sandbox (race detector) (push) Successful in 1m41s
Test / Flake checks (push) Successful in 1m5s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-04-11 19:55:16 +09:00
cat 2f4f21fb18 fst: rename device field
Test / Create distribution (push) Successful in 26s
Test / Sandbox (push) Successful in 1m46s
Test / Fortify (push) Successful in 2m39s
Test / Sandbox (race detector) (push) Successful in 3m1s
Test / Fpkg (push) Successful in 3m38s
Test / Fortify (race detector) (push) Successful in 4m10s
Test / Flake checks (push) Successful in 1m5s
Dev is very ambiguous. Rename it here alongside upcoming config changes.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-04-11 19:32:15 +09:00
cat 9967909460 sandbox: relative autoetc links
Test / Create distribution (push) Successful in 26s
Test / Sandbox (push) Successful in 1m44s
Test / Fortify (push) Successful in 2m41s
Test / Sandbox (race detector) (push) Successful in 2m48s
Test / Fpkg (push) Successful in 3m35s
Test / Fortify (race detector) (push) Successful in 4m13s
Test / Flake checks (push) Successful in 1m3s
This allows nested containers to use autoetc, and increases compatibility with other implementations.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-04-11 18:54:00 +09:00
cat c806f43881 sandbox: implement autoetc as setup op
Test / Create distribution (push) Successful in 27s
Test / Sandbox (push) Successful in 1m48s
Test / Fortify (push) Successful in 2m42s
Test / Sandbox (race detector) (push) Successful in 2m51s
Test / Fpkg (push) Successful in 3m37s
Test / Fortify (race detector) (push) Successful in 4m9s
Test / Flake checks (push) Successful in 1m4s
This significantly reduces setup op count and the readdir call now happens in the context of the init process.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-04-10 18:54:25 +09:00
cat 584405f7cc sandbox/seccomp: rename flag type and constants
Test / Create distribution (push) Successful in 27s
Test / Sandbox (push) Successful in 1m38s
Test / Fortify (push) Successful in 2m39s
Test / Sandbox (race detector) (push) Successful in 2m55s
Test / Fpkg (push) Successful in 3m26s
Test / Fortify (race detector) (push) Successful in 4m5s
Test / Flake checks (push) Successful in 56s
The names are ambiguous. Rename them to make more sense.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-04-08 01:59:45 +09:00
cat 50127ed5f9 fortify: print synthesised id in ps
Test / Create distribution (push) Successful in 26s
Test / Sandbox (push) Successful in 1m48s
Test / Fortify (push) Successful in 2m42s
Test / Sandbox (race detector) (push) Successful in 2m53s
Test / Fpkg (push) Successful in 3m30s
Test / Fortify (race detector) (push) Successful in 4m7s
Test / Flake checks (push) Successful in 1m2s
This is not the full synthesised id so it does not get too long.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-04-07 21:55:07 +09:00
cat b5eff27c40 fortify: check fst id string length
Test / Create distribution (push) Successful in 26s
Test / Sandbox (push) Successful in 1m44s
Test / Fortify (push) Successful in 2m42s
Test / Sandbox (race detector) (push) Successful in 2m49s
Test / Fpkg (push) Successful in 3m25s
Test / Fortify (race detector) (push) Successful in 4m9s
Test / Flake checks (push) Successful in 1m3s
This should never be a problem, however in case it happens printing a warning message is better than relying on the runtime to panic.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-04-07 21:39:46 +09:00
cat 74ba183256 app: install seccomp filter to shim
Test / Create distribution (push) Successful in 27s
Test / Sandbox (push) Successful in 1m57s
Test / Fortify (push) Successful in 2m53s
Test / Sandbox (race detector) (push) Successful in 3m5s
Test / Fpkg (push) Successful in 3m51s
Test / Fortify (race detector) (push) Successful in 4m19s
Test / Flake checks (push) Successful in 1m5s
This does not necessarily reduce attack surface but does not affect functionality or introduce any side effects, so is nice to have.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-04-07 04:13:08 +09:00
cat f885dede9b sandbox/seccomp: unexport println wrapper
Test / Create distribution (push) Successful in 27s
Test / Sandbox (push) Successful in 1m45s
Test / Fortify (push) Successful in 2m40s
Test / Sandbox (race detector) (push) Successful in 2m52s
Test / Fpkg (push) Successful in 3m25s
Test / Fortify (race detector) (push) Successful in 4m10s
Test / Flake checks (push) Successful in 1m6s
This is an implementation detail that was exported for the bwrap argument builder. The removal of that package allows it to be unexported.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-04-07 04:07:20 +09:00
cat e9a7cd526f app: improve shim process management
Test / Create distribution (push) Successful in 27s
Test / Sandbox (push) Successful in 1m45s
Test / Fortify (push) Successful in 2m36s
Test / Sandbox (race detector) (push) Successful in 2m49s
Test / Fpkg (push) Successful in 3m33s
Test / Fortify (race detector) (push) Successful in 4m13s
Test / Flake checks (push) Successful in 1m6s
This ensures a signal gets delivered to the process instead of relying on parent death behaviour.

SIGCONT was chosen as it is the only signal an unprivileged process is allowed to send to processes with different credentials.

A custom signal handler is installed because the Go runtime does not expose signal information other than which signal was received, and shim must check pid to ensure reasonable behaviour.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-04-07 03:55:17 +09:00
cat 12be7bc78e release: 0.3.3
Release / Create release (push) Successful in 34s
Test / Create distribution (push) Successful in 19s
Test / Sandbox (push) Successful in 30s
Test / Sandbox (race detector) (push) Successful in 29s
Test / Fortify (push) Successful in 35s
Test / Fortify (race detector) (push) Successful in 35s
Test / Fpkg (push) Successful in 33s
Test / Flake checks (push) Successful in 1m0s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-04-01 01:42:10 +09:00
cat 0ba8be659f sandbox: document less obvious parts of setup
Test / Create distribution (push) Successful in 29s
Test / Sandbox (push) Successful in 2m8s
Test / Fortify (push) Successful in 3m3s
Test / Sandbox (race detector) (push) Successful in 3m9s
Test / Fpkg (push) Successful in 4m22s
Test / Fortify (race detector) (push) Successful in 4m37s
Test / Flake checks (push) Successful in 1m19s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-04-01 01:21:04 +09:00
cat 022242a84a app: wayland socket in process share
Test / Create distribution (push) Successful in 29s
Test / Sandbox (push) Successful in 1m9s
Test / Fortify (push) Successful in 2m16s
Test / Sandbox (race detector) (push) Successful in 3m8s
Test / Fpkg (push) Successful in 3m35s
Test / Fortify (race detector) (push) Successful in 4m32s
Test / Flake checks (push) Successful in 1m24s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-04-01 00:53:04 +09:00
cat 8aeb06f53c app: share path setup on demand
Test / Create distribution (push) Successful in 28s
Test / Sandbox (race detector) (push) Successful in 34s
Test / Sandbox (push) Successful in 34s
Test / Fpkg (push) Successful in 39s
Test / Fortify (push) Successful in 2m16s
Test / Fortify (race detector) (push) Successful in 2m58s
Test / Flake checks (push) Successful in 1m33s
This removes the unnecessary creation and destruction of share paths when none of the enablements making use of them are set.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-04-01 00:47:32 +09:00
cat 4036da3b5c fst: optional configured shell path
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 1m45s
Test / Fortify (push) Successful in 2m28s
Test / Sandbox (race detector) (push) Successful in 2m45s
Test / Fpkg (push) Successful in 3m32s
Test / Fortify (race detector) (push) Successful in 4m5s
Test / Flake checks (push) Successful in 1m2s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-31 21:27:31 +09:00
cat 986105958c fortify: update show output
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 1m48s
Test / Fortify (push) Successful in 2m31s
Test / Sandbox (race detector) (push) Successful in 2m49s
Test / Fpkg (push) Successful in 3m27s
Test / Fortify (race detector) (push) Successful in 4m8s
Test / Flake checks (push) Successful in 1m0s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-31 04:54:10 +09:00
cat ecdd4d8202 fortify: clean ps output
Test / Create distribution (push) Successful in 26s
Test / Sandbox (push) Successful in 1m40s
Test / Fortify (push) Successful in 2m34s
Test / Sandbox (race detector) (push) Successful in 2m52s
Test / Fpkg (push) Successful in 3m36s
Test / Fortify (race detector) (push) Successful in 4m5s
Test / Flake checks (push) Successful in 1m3s
This format never changed ever since it was added. It used to show everything there is in a process state but that is no longer true for a long time. This change cleans it up in favour of `fortify show` displaying extra information.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-31 04:41:08 +09:00
cat bdee0c3921 nix: update flake lock
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 5m6s
Test / Sandbox (race detector) (push) Successful in 5m12s
Test / Fortify (push) Successful in 6m5s
Test / Fortify (race detector) (push) Successful in 6m39s
Test / Fpkg (push) Successful in 9m53s
Test / Flake checks (push) Successful in 1m20s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-30 23:15:18 +09:00
cat 48f634d046 release: 0.3.2
Release / Create release (push) Successful in 34s
Test / Sandbox (push) Successful in 34s
Test / Fortify (push) Successful in 59s
Test / Create distribution (push) Successful in 20s
Test / Sandbox (race detector) (push) Successful in 1m3s
Test / Fpkg (push) Successful in 1m16s
Test / Fortify (race detector) (push) Successful in 4m14s
Test / Flake checks (push) Successful in 1m9s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-30 23:05:57 +09:00
cat 2a46f5bb12 sandbox/seccomp: update doc comment
Test / Create distribution (push) Successful in 26s
Test / Sandbox (push) Successful in 1m43s
Test / Fortify (push) Successful in 2m44s
Test / Sandbox (race detector) (push) Successful in 2m58s
Test / Fpkg (push) Successful in 3m38s
Test / Fortify (race detector) (push) Successful in 4m9s
Test / Flake checks (push) Successful in 1m8s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-30 23:00:20 +09:00
cat 7f2c0af5ad fst: set multiarch bit
Test / Create distribution (push) Successful in 26s
Test / Sandbox (push) Successful in 1m57s
Test / Fortify (push) Successful in 2m45s
Test / Sandbox (race detector) (push) Successful in 2m55s
Test / Fpkg (push) Successful in 3m41s
Test / Fortify (race detector) (push) Successful in 4m10s
Test / Flake checks (push) Successful in 1m8s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-30 22:55:00 +09:00
cat 297b444dfb test: separate app and sandbox
Test / Create distribution (push) Successful in 26s
Test / Sandbox (push) Successful in 1m42s
Test / Fortify (push) Successful in 2m39s
Test / Sandbox (race detector) (push) Successful in 2m52s
Test / Fpkg (push) Successful in 3m37s
Test / Fortify (race detector) (push) Successful in 4m17s
Test / Flake checks (push) Successful in 1m6s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-30 22:09:46 +09:00
cat 89a05909a4 test: move test program to sandbox directory
Test / Create distribution (push) Successful in 27s
Test / Fpkg (push) Successful in 39s
Test / Fortify (push) Successful in 2m38s
Test / Data race detector (push) Successful in 3m22s
Test / Flake checks (push) Successful in 1m1s
This prepares for the separation of app and sandbox tests.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-30 21:09:16 +09:00
cat f772940768 test/sandbox: treat ESRCH as temporary failure
Test / Create distribution (push) Successful in 25s
Test / Fpkg (push) Successful in 33s
Test / Fortify (push) Successful in 2m30s
Test / Data race detector (push) Successful in 3m13s
Test / Flake checks (push) Successful in 52s
This is an ugly fix that makes various assumptions guaranteed to hold true in the testing vm. The test package is filtered by the build system so some ugliness is tolerable here.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-30 03:50:59 +09:00
cat 8886c40974 test/sandbox: separate check filter
Test / Create distribution (push) Successful in 26s
Test / Fpkg (push) Successful in 34s
Test / Fortify (push) Successful in 2m29s
Test / Data race detector (push) Successful in 3m12s
Test / Flake checks (push) Successful in 54s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-30 02:15:08 +09:00
cat 8b62e08b44 test: build test program in nixos config
Test / Create distribution (push) Successful in 26s
Test / Fpkg (push) Successful in 34s
Test / Data race detector (push) Successful in 3m18s
Test / Fortify (push) Successful in 1m53s
Test / Flake checks (push) Successful in 57s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-29 19:33:17 +09:00
cat 72c59f9229 nix: check share/applications in share package
Test / Create distribution (push) Successful in 27s
Test / Fpkg (push) Successful in 37s
Test / Data race detector (push) Successful in 3m9s
Test / Fortify (push) Successful in 2m2s
Test / Flake checks (push) Successful in 56s
This allows share directories without share/applications/ to build correctly.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-29 19:28:20 +09:00
cat ff3cfbb437 test/sandbox: check seccomp outcome
Test / Create distribution (push) Successful in 25s
Test / Fpkg (push) Successful in 33s
Test / Fortify (push) Successful in 2m27s
Test / Data race detector (push) Successful in 3m15s
Test / Flake checks (push) Successful in 56s
This is as ugly as it is because it has to have CAP_SYS_ADMIN and not be in seccomp mode.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-28 02:24:27 +09:00
cat c13eb70d7d sandbox/seccomp: add fortify default sample
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m39s
Test / Fpkg (push) Successful in 3m29s
Test / Data race detector (push) Successful in 4m34s
Test / Flake checks (push) Successful in 57s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-28 02:02:02 +09:00
cat 389402f955 test/sandbox/ptrace: generic filter block type
Test / Create distribution (push) Successful in 26s
Test / Fpkg (push) Successful in 34s
Test / Fortify (push) Successful in 2m28s
Test / Data race detector (push) Successful in 3m12s
Test / Flake checks (push) Successful in 59s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-28 01:47:24 +09:00
cat 660a2898dc test/sandbox/ptrace: dump seccomp bpf program
Test / Create distribution (push) Successful in 26s
Test / Fpkg (push) Successful in 34s
Test / Fortify (push) Successful in 2m21s
Test / Data race detector (push) Successful in 3m4s
Test / Flake checks (push) Successful in 55s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-28 01:35:56 +09:00
cat faf59e12c0 test/sandbox: expose test tool
Test / Create distribution (push) Successful in 27s
Test / Fpkg (push) Successful in 34s
Test / Fortify (push) Successful in 2m22s
Test / Data race detector (push) Successful in 3m11s
Test / Flake checks (push) Successful in 56s
Some test elements implemented in the test tool might need to run outside the sandbox. This change allows that to happen.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-28 00:08:47 +09:00
cat d97a03c7c6 test/sandbox: separate test tool source
Test / Create distribution (push) Successful in 26s
Test / Fpkg (push) Successful in 34s
Test / Fortify (push) Successful in 2m27s
Test / Data race detector (push) Successful in 3m11s
Test / Flake checks (push) Successful in 59s
This improves readability and allows gofmt to format the file.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-27 23:43:13 +09:00
cat a102178019 sys: update doc comment
Test / Create distribution (push) Successful in 28s
Test / Fortify (push) Successful in 2m45s
Test / Fpkg (push) Successful in 3m36s
Test / Data race detector (push) Successful in 4m32s
Test / Flake checks (push) Successful in 58s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-27 22:43:17 +09:00
cat e400862a12 state/multi: fix backend cache population race
Test / Create distribution (push) Successful in 27s
Test / Fortify (push) Successful in 2m46s
Test / Fpkg (push) Successful in 3m33s
Test / Data race detector (push) Successful in 4m37s
Test / Flake checks (push) Successful in 57s
This race is never able to happen since no caller concurrently requests the same aid yet.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-27 22:37:08 +09:00
cat 184e9db2b2 sandbox: support privileged container
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m34s
Test / Fpkg (push) Successful in 3m25s
Test / Data race detector (push) Successful in 4m27s
Test / Flake checks (push) Successful in 53s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-27 19:40:19 +09:00
cat 605d018be2 app/seal: check for '=' in envv
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m58s
Test / Fpkg (push) Successful in 3m50s
Test / Data race detector (push) Successful in 4m40s
Test / Flake checks (push) Successful in 55s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-27 18:25:23 +09:00
cat 78aaae7ee0 helper/args: copy args on wt creation
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m49s
Test / Data race detector (push) Successful in 3m4s
Test / Fpkg (push) Successful in 3m15s
Test / Flake checks (push) Successful in 1m1s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-27 18:22:07 +09:00
cat 5c82f1ed3e helper/stub: output to stdout
Test / Create distribution (push) Successful in 19s
Test / Fortify (push) Successful in 43s
Test / Fpkg (push) Successful in 1m26s
Test / Data race detector (push) Successful in 2m28s
Test / Flake checks (push) Successful in 1m0s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-27 17:25:10 +09:00
cat f8502c3ece test/sandbox: check environment
Test / Create distribution (push) Successful in 19s
Test / Fpkg (push) Successful in 34s
Test / Fortify (push) Successful in 41s
Test / Data race detector (push) Successful in 41s
Test / Flake checks (push) Successful in 56s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-27 03:16:33 +09:00
cat 996b42634d test/sandbox: invoke check program directly
Test / Create distribution (push) Successful in 26s
Test / Fpkg (push) Successful in 34s
Test / Fortify (push) Successful in 40s
Test / Data race detector (push) Successful in 2m47s
Test / Flake checks (push) Successful in 1m4s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-27 03:11:50 +09:00
cat 300571af47 app: pass through $SHELL
Test / Create distribution (push) Successful in 25s
Test / Fpkg (push) Successful in 33s
Test / Fortify (push) Successful in 39s
Test / Data race detector (push) Successful in 39s
Test / Flake checks (push) Successful in 55s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-27 01:22:40 +09:00
cat 32c90ef4e7 nix: pass through exec arguments
Test / Create distribution (push) Successful in 19s
Test / Fpkg (push) Successful in 34s
Test / Fortify (push) Successful in 41s
Test / Data race detector (push) Successful in 41s
Test / Flake checks (push) Successful in 56s
This is useful for when a wrapper script is unnecessary.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-27 03:04:46 +09:00
cat 2a4e2724a3 release: 0.3.1
Release / Create release (push) Successful in 35s
Test / Create distribution (push) Successful in 19s
Test / Fpkg (push) Successful in 33s
Test / Fortify (push) Successful in 39s
Test / Data race detector (push) Successful in 39s
Test / Flake checks (push) Successful in 55s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-26 07:48:50 +09:00
cat d613257841 sandbox/init: clear inheritable set
Test / Create distribution (push) Successful in 28s
Test / Fpkg (push) Successful in 3m52s
Test / Data race detector (push) Successful in 4m47s
Test / Fortify (push) Successful in 2m4s
Test / Flake checks (push) Successful in 57s
Inheritable should not be able to affect anything regardless of its value, due to no_new_privs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-26 07:46:13 +09:00
cat 18644d90be sandbox: wrap capset syscall
Test / Create distribution (push) Successful in 21s
Test / Fortify (push) Successful in 2m25s
Test / Data race detector (push) Successful in 3m10s
Test / Fpkg (push) Successful in 2m59s
Test / Flake checks (push) Successful in 1m4s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-26 07:44:07 +09:00
cat 52fcc48ac1 sandbox/init: drop capabilities
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m39s
Test / Fpkg (push) Successful in 3m31s
Test / Data race detector (push) Successful in 4m32s
Test / Flake checks (push) Successful in 58s
During development the syscall filter caused me to make an incorrect assumption about SysProcAttr.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-26 06:32:08 +09:00
cat 8b69bcd215 sandbox: cache kernel.cap_last_cap value
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m37s
Test / Fpkg (push) Successful in 3m33s
Test / Data race detector (push) Successful in 4m27s
Test / Flake checks (push) Successful in 59s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-26 06:19:19 +09:00
cat 2dd49c437c app: create XDG_RUNTIME_DIR with perm 0700
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m41s
Test / Fpkg (push) Successful in 3m31s
Test / Data race detector (push) Successful in 4m30s
Test / Flake checks (push) Successful in 59s
Many programs complain about this.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-26 02:49:37 +09:00
cat 92852d8235 release: 0.3.0
Test / Create distribution (push) Successful in 20s
Release / Create release (push) Successful in 35s
Test / Fortify (push) Successful in 2m45s
Test / Fpkg (push) Successful in 3m27s
Test / Data race detector (push) Successful in 4m20s
Test / Flake checks (push) Successful in 1m1s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-26 02:18:59 +09:00
cat 371dd5b938 nix: create current-system symlink
Test / Create distribution (push) Successful in 20s
Release / Create release (push) Successful in 27s
Test / Fpkg (push) Successful in 35s
Test / Fortify (push) Successful in 40s
Test / Data race detector (push) Successful in 40s
Test / Flake checks (push) Successful in 58s
This is copied at runtime because it appears to be impossible to obtain this path in nix.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-26 02:06:11 +09:00
cat 4836d570ae test: raise long timeout to 15 seconds
Test / Create distribution (push) Successful in 26s
Test / Fpkg (push) Successful in 34s
Test / Fortify (push) Successful in 2m20s
Test / Data race detector (push) Successful in 3m4s
Test / Flake checks (push) Successful in 57s
The race detector really slows down container tooling.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-26 01:59:05 +09:00
cat 985f9442e6 sandbox: copy symlink with magic prefix
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m39s
Test / Fpkg (push) Successful in 3m31s
Test / Data race detector (push) Successful in 2m40s
Test / Flake checks (push) Successful in 59s
This does not dereference the symlink, but only reads one level of it. This is useful for symlink targets that are not yet known at the time the configuration is emitted.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-26 01:42:39 +09:00
cat 67eb28466d nix: create opengl-driver symlink
Test / Create distribution (push) Successful in 25s
Test / Fpkg (push) Successful in 33s
Test / Fortify (push) Successful in 2m18s
Test / Data race detector (push) Successful in 3m3s
Test / Flake checks (push) Successful in 53s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-25 20:52:20 +09:00
cat c326c3f97d fst/sandbox: do not create /etc in advance
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m43s
Test / Fpkg (push) Successful in 3m36s
Test / Data race detector (push) Successful in 4m31s
Test / Flake checks (push) Successful in 56s
This is now handled by the setup op. This also gets rid of the hardcoded /etc path.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-25 20:00:34 +09:00
cat 971c79bb80 sandbox: remove hardcoded parent perm
Test / Create distribution (push) Successful in 27s
Test / Fortify (push) Successful in 2m43s
Test / Fpkg (push) Successful in 3m41s
Test / Data race detector (push) Successful in 4m32s
Test / Flake checks (push) Successful in 59s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-25 19:49:51 +09:00
cat f86d868274 sandbox: wrap error with its own text message
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m40s
Test / Fpkg (push) Successful in 3m33s
Test / Data race detector (push) Successful in 4m24s
Test / Flake checks (push) Successful in 57s
PathError has a pretty good text message, many of them are wrapped with its own text message. This change adds a function to do just that to improve readability.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-25 19:42:20 +09:00
cat 33940265a6 sandbox: do not ensure symlink target
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m40s
Test / Fpkg (push) Successful in 3m29s
Test / Data race detector (push) Successful in 4m31s
Test / Flake checks (push) Successful in 1m4s
This masks EEXIST on target and might clobber filesystems and lead to other confusing behaviour. Create its parent instead.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-25 19:30:53 +09:00
cat b39f3aeb59 helper: remove bubblewrap wrapper
Test / Create distribution (push) Successful in 19s
Test / Fortify (push) Successful in 2m12s
Test / Fpkg (push) Successful in 3m34s
Test / Data race detector (push) Successful in 4m19s
Test / Flake checks (push) Successful in 57s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-25 05:35:02 +09:00
cat 61dbfeffe7 sandbox/wl: move into sandbox
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m49s
Test / Fpkg (push) Successful in 3m54s
Test / Data race detector (push) Successful in 4m36s
Test / Flake checks (push) Successful in 58s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-25 05:26:37 +09:00
cat 532feb4bfa app: merge shim into app package
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m48s
Test / Fpkg (push) Successful in 3m39s
Test / Data race detector (push) Successful in 4m35s
Test / Flake checks (push) Successful in 56s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-25 05:21:47 +09:00
cat ec5e91b8c9 system: optimise string formatting
Test / Create distribution (push) Successful in 20s
Test / Fpkg (push) Successful in 36s
Test / Fortify (push) Successful in 42s
Test / Data race detector (push) Successful in 43s
Test / Flake checks (push) Successful in 1m10s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-25 04:42:30 +09:00
cat ee51320abf test: check revert type selection
Test / Create distribution (push) Successful in 20s
Test / Fortify (push) Successful in 2m18s
Test / Fpkg (push) Successful in 3m1s
Test / Data race detector (push) Successful in 4m32s
Test / Flake checks (push) Successful in 1m4s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-25 04:37:58 +09:00
cat 5c4058d5ac app: run in native sandbox
Test / Create distribution (push) Successful in 20s
Test / Fortify (push) Successful in 2m5s
Test / Fpkg (push) Successful in 3m0s
Test / Data race detector (push) Successful in 4m12s
Test / Flake checks (push) Successful in 1m4s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-25 01:52:49 +09:00
cat e732dca762 wl: fix sync pipe keepalive
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m30s
Test / Fpkg (push) Successful in 3m36s
Test / Data race detector (push) Successful in 4m14s
Test / Flake checks (push) Successful in 59s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-25 01:33:37 +09:00
cat a9adcd914b fortify/parse: omit try fd fallthrough message
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m33s
Test / Fpkg (push) Successful in 3m28s
Test / Data race detector (push) Successful in 4m12s
Test / Flake checks (push) Successful in 57s
This reduces noise in verbose output.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-25 01:21:11 +09:00
cat 3dd4ff29c8 test/sandbox: check mount table length
Test / Create distribution (push) Successful in 26s
Test / Fpkg (push) Successful in 37s
Test / Fortify (push) Successful in 2m20s
Test / Data race detector (push) Successful in 2m51s
Test / Flake checks (push) Successful in 1m0s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-24 16:36:53 +09:00
cat 61d86c5e10 test/sandbox: fix stdout tty check
Test / Create distribution (push) Successful in 27s
Test / Fpkg (push) Successful in 37s
Test / Fortify (push) Successful in 2m22s
Test / Data race detector (push) Successful in 2m57s
Test / Flake checks (push) Successful in 56s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-24 16:23:50 +09:00
cat d097eaa28f test/sandbox: unquote fail messages
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m31s
Test / Fpkg (push) Successful in 3m22s
Test / Data race detector (push) Successful in 4m22s
Test / Flake checks (push) Successful in 57s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-24 16:03:53 +09:00
cat ad3576c164 sandbox: resolve tty name
Test / Create distribution (push) Successful in 19s
Test / Fortify (push) Successful in 2m17s
Test / Fpkg (push) Successful in 3m15s
Test / Data race detector (push) Successful in 4m10s
Test / Flake checks (push) Successful in 56s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-24 16:03:07 +09:00
cat b989a4601a test/sandbox: fail on mismatched mount entry
Test / Create distribution (push) Successful in 26s
Test / Fpkg (push) Successful in 34s
Test / Fortify (push) Successful in 2m26s
Test / Data race detector (push) Successful in 2m47s
Test / Flake checks (push) Successful in 57s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-24 13:43:32 +09:00
cat a11237b158 sandbox/vfs: add doc comments
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m42s
Test / Fpkg (push) Successful in 3m40s
Test / Data race detector (push) Successful in 4m15s
Test / Flake checks (push) Successful in 55s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-24 13:21:55 +09:00
cat 40f00d570e sandbox: set mkdir perm
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m34s
Test / Fpkg (push) Successful in 3m26s
Test / Data race detector (push) Successful in 4m7s
Test / Flake checks (push) Successful in 57s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-24 12:51:39 +09:00
cat 0eb1bc6301 test/sandbox: verify outcome via mountinfo
Test / Fpkg (push) Successful in 36s
Test / Create distribution (push) Successful in 4m56s
Test / Fortify (push) Successful in 6m33s
Test / Data race detector (push) Successful in 7m3s
Test / Flake checks (push) Successful in 54s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-24 01:42:38 +09:00
cat 1eb837eab8 test/sandbox: warn about misuse in doc comment
Test / Create distribution (push) Successful in 26s
Test / Fpkg (push) Successful in 34s
Test / Fortify (push) Successful in 2m16s
Test / Data race detector (push) Successful in 2m45s
Test / Flake checks (push) Successful in 59s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-23 23:28:28 +09:00
cat 0a4e633db2 nix: filter test from source
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m42s
Test / Fpkg (push) Successful in 3m52s
Test / Data race detector (push) Successful in 4m19s
Test / Flake checks (push) Successful in 54s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-23 22:20:19 +09:00
cat e8809125d4 sandbox: verify outcome via mountinfo
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m39s
Test / Fpkg (push) Successful in 3m29s
Test / Data race detector (push) Successful in 4m17s
Test / Flake checks (push) Successful in 1m6s
This contains much more information than /proc/mounts and allows for more fields to be checked. This also removes the dependency on the test package.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-23 22:17:36 +09:00
cat 806ce18c0a test/sandbox: check mapuid outcome
Test / Create distribution (push) Successful in 27s
Test / Fpkg (push) Successful in 37s
Test / Fortify (push) Successful in 2m23s
Test / Data race detector (push) Successful in 2m50s
Test / Flake checks (push) Successful in 55s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-23 17:56:07 +09:00
cat b71d2bf534 test/sandbox: check tty outcome
Test / Create distribution (push) Successful in 25s
Test / Fpkg (push) Successful in 34s
Test / Fortify (push) Successful in 2m21s
Test / Data race detector (push) Successful in 2m48s
Test / Flake checks (push) Successful in 54s
This makes no difference currently but has different behaviour in the native sandbox.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-23 17:28:57 +09:00
cat 46059b1840 test/sandbox: print mismatching file content
Test / Create distribution (push) Successful in 25s
Test / Fpkg (push) Successful in 34s
Test / Fortify (push) Successful in 2m3s
Test / Data race detector (push) Successful in 2m32s
Test / Flake checks (push) Successful in 51s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-23 17:24:52 +09:00
cat d2c329bcea test: format path aid offsets
Test / Create distribution (push) Successful in 26s
Test / Fpkg (push) Successful in 36s
Test / Fortify (push) Successful in 2m12s
Test / Data race detector (push) Successful in 2m41s
Test / Flake checks (push) Successful in 51s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-23 17:21:14 +09:00
cat 2d379b5a38 test/sandbox: pass want file as argument
Test / Create distribution (push) Successful in 25s
Test / Fpkg (push) Successful in 33s
Test / Fortify (push) Successful in 2m7s
Test / Data race detector (push) Successful in 2m36s
Test / Flake checks (push) Successful in 49s
This avoids building the check program multiple times.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-23 15:00:59 +09:00
cat 75e0c5d406 test/sandbox: parse full test case
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m37s
Test / Fpkg (push) Successful in 3m52s
Test / Data race detector (push) Successful in 4m12s
Test / Flake checks (push) Successful in 50s
This makes declaring multiple tests much cleaner.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-23 14:53:50 +09:00
cat 770b37ae16 sandbox/vfs: match MS_NOSYMFOLLOW flag
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m27s
Test / Fpkg (push) Successful in 3m33s
Test / Data race detector (push) Successful in 4m10s
Test / Flake checks (push) Successful in 52s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-23 13:57:30 +09:00
cat c638193268 sandbox: apply vfs options to bind mounts
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m41s
Test / Fpkg (push) Successful in 3m45s
Test / Data race detector (push) Successful in 4m11s
Test / Flake checks (push) Successful in 56s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-23 05:27:57 +09:00
cat 8c3a817881 sandbox/vfs: unfold mount hierarchy
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m33s
Test / Fpkg (push) Successful in 3m31s
Test / Data race detector (push) Successful in 4m11s
Test / Flake checks (push) Successful in 53s
This presents all visible mount points under path. This is useful for applying extra vfs options to bind mounts.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-23 05:23:31 +09:00
cat e2fce321c1 sandbox/vfs: expose mountinfo line scanning
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m38s
Test / Fpkg (push) Successful in 3m30s
Test / Data race detector (push) Successful in 4m9s
Test / Flake checks (push) Successful in 51s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-23 02:46:58 +09:00
cat 241702ae3a go: 1.23
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m48s
Test / Data race detector (push) Successful in 4m22s
Test / Fpkg (push) Successful in 1h28m30s
Test / Flake checks (push) Successful in 49s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-22 18:20:06 +09:00
cat d21d9c5b1d sandbox/vfs: parse vfs options
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m36s
Test / Fpkg (push) Successful in 3m20s
Test / Data race detector (push) Successful in 4m4s
Test / Flake checks (push) Successful in 50s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-21 17:12:10 +09:00
cat a70daf2250 sandbox: resolve inverted flags in op
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m5s
Test / Data race detector (push) Successful in 2m30s
Test / Fpkg (push) Successful in 2m48s
Test / Flake checks (push) Successful in 48s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-21 12:58:38 +09:00
cat 632b18addd test/sandbox: rename misleading bind destination
Test / Create distribution (push) Successful in 25s
Test / Fpkg (push) Successful in 34s
Test / Fortify (push) Successful in 2m15s
Test / Data race detector (push) Successful in 2m49s
Test / Flake checks (push) Successful in 59s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-21 12:56:11 +09:00
cat a57a7a6a16 test/sandbox: check type handling host_passthrough
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m30s
Test / Fpkg (push) Successful in 3m30s
Test / Data race detector (push) Successful in 4m20s
Test / Flake checks (push) Successful in 52s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-21 12:21:08 +09:00
cat 5098b12e4a sandbox/vfs: count mountinfo entries
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m39s
Test / Fpkg (push) Successful in 3m37s
Test / Data race detector (push) Successful in 4m10s
Test / Flake checks (push) Successful in 52s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-21 12:14:33 +09:00
cat 9ddf5794dd sandbox/vfs: implement proc_pid_mountinfo(5) parser
Test / Create distribution (push) Successful in 28s
Test / Fortify (push) Successful in 2m43s
Test / Fpkg (push) Successful in 3m38s
Test / Data race detector (push) Successful in 4m11s
Test / Flake checks (push) Successful in 53s
Test cases are mostly taken from util-linux. This implementation is more correct and slightly faster than the one found in github:kubernetes/utils.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-21 00:35:49 +09:00
cat b74a08dda9 sandbox: prepare ops early
Test / Create distribution (push) Successful in 24s
Test / Fortify (push) Successful in 2m27s
Test / Fpkg (push) Successful in 3m33s
Test / Data race detector (push) Successful in 4m9s
Test / Flake checks (push) Successful in 53s
Some setup code needs to run in host root. This change allows that to happen.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-18 02:17:46 +09:00
cat 1b9408864f sandbox: pass cmd to cancel function
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m35s
Test / Fpkg (push) Successful in 3m36s
Test / Data race detector (push) Successful in 4m11s
Test / Flake checks (push) Successful in 49s
This is not usually in scope otherwise.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 22:36:39 +09:00
cat cc89dbdf63 sandbox: place files with content
Test / Create distribution (push) Successful in 24s
Test / Fortify (push) Successful in 2m35s
Test / Fpkg (push) Successful in 3m35s
Test / Data race detector (push) Successful in 4m7s
Test / Flake checks (push) Successful in 47s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 22:13:22 +09:00
cat 228f3301f2 sandbox: create directories
Test / Create distribution (push) Successful in 24s
Test / Fortify (push) Successful in 2m29s
Test / Fpkg (push) Successful in 3m30s
Test / Data race detector (push) Successful in 4m2s
Test / Flake checks (push) Successful in 48s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 22:03:06 +09:00
cat 07181138e5 sandbox/mount: pass absolute path
Test / Create distribution (push) Successful in 24s
Test / Fortify (push) Successful in 2m31s
Test / Fpkg (push) Successful in 3m24s
Test / Data race detector (push) Successful in 4m6s
Test / Flake checks (push) Successful in 48s
This should never be used unless there is a good reason to, like using a file in the intermediate root.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 21:53:31 +09:00
cat 816b372f14 sandbox: cancel process on serve error
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m32s
Test / Fpkg (push) Successful in 3m28s
Test / Data race detector (push) Successful in 4m4s
Test / Flake checks (push) Successful in 49s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 21:49:45 +09:00
cat d7eddd54a2 sandbox: rename params struct
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m33s
Test / Fpkg (push) Successful in 3m27s
Test / Data race detector (push) Successful in 4m3s
Test / Flake checks (push) Successful in 59s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 21:45:08 +09:00
cat 7c063833e0 internal/sys: wrap getuid/getgid
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m34s
Test / Fpkg (push) Successful in 3m26s
Test / Data race detector (push) Successful in 4m8s
Test / Flake checks (push) Successful in 50s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 17:10:03 +09:00
cat af3619d440 sandbox: create symlinks
Test / Create distribution (push) Successful in 24s
Test / Fortify (push) Successful in 2m30s
Test / Fpkg (push) Successful in 3m21s
Test / Data race detector (push) Successful in 4m3s
Test / Flake checks (push) Successful in 48s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 16:37:56 +09:00
cat 528674cb6e sandbox/init: fail early on nil op
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m27s
Test / Fpkg (push) Successful in 3m21s
Test / Data race detector (push) Successful in 4m3s
Test / Flake checks (push) Successful in 49s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 16:17:03 +09:00
cat 70c9757e26 sandbox/mount: rename device flag
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m28s
Test / Fpkg (push) Successful in 3m30s
Test / Data race detector (push) Successful in 4m5s
Test / Flake checks (push) Successful in 51s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 16:10:55 +09:00
cat c83a7e2efc sandbox: mount container /dev/mqueue
Test / Create distribution (push) Successful in 24s
Test / Fortify (push) Successful in 2m26s
Test / Fpkg (push) Successful in 3m21s
Test / Data race detector (push) Successful in 4m0s
Test / Flake checks (push) Successful in 49s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 15:42:40 +09:00
cat 904208b87f sandbox: unwrap path string
Test / Create distribution (push) Successful in 24s
Test / Fortify (push) Successful in 2m35s
Test / Fpkg (push) Successful in 3m21s
Test / Data race detector (push) Successful in 4m9s
Test / Flake checks (push) Successful in 50s
Mount proc and dev takes no additional parameters.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 15:33:20 +09:00
cat 007b52d81f sandbox/seccomp: check for both partial read outcomes
Test / Create distribution (push) Successful in 24s
Test / Fortify (push) Successful in 2m28s
Test / Fpkg (push) Successful in 3m17s
Test / Data race detector (push) Successful in 4m1s
Test / Flake checks (push) Successful in 47s
This eliminates intermittent test failures.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 12:51:21 +09:00
cat 3385538142 nix: clean up flake outputs
Test / Create distribution (push) Successful in 25s
Test / Fpkg (push) Successful in 32s
Test / Fortify (push) Successful in 2m0s
Test / Data race detector (push) Successful in 2m32s
Test / Flake checks (push) Successful in 48s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 12:26:19 +09:00
cat 24618ab9a1 sandbox: move out of internal
Test / Create distribution (push) Successful in 18s
Test / Fpkg (push) Successful in 2m40s
Test / Data race detector (push) Successful in 3m13s
Test / Fortify (push) Successful in 3m1s
Test / Flake checks (push) Successful in 51s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 02:55:36 +09:00
cat 9ce4706a07 sandbox: move params setup functions
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m37s
Test / Fpkg (push) Successful in 3m30s
Test / Data race detector (push) Successful in 4m8s
Test / Flake checks (push) Successful in 57s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 02:48:32 +09:00
cat 9a1f8e129f sandbox: wrap fmsg interface
Test / Create distribution (push) Successful in 24s
Test / Fortify (push) Successful in 2m27s
Test / Fpkg (push) Successful in 3m36s
Test / Data race detector (push) Successful in 4m16s
Test / Flake checks (push) Successful in 55s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 02:44:07 +09:00
cat ee10860357 seccomp: install output atomically
Test / Create distribution (push) Successful in 24s
Test / Fortify (push) Successful in 2m33s
Test / Fpkg (push) Successful in 3m17s
Test / Data race detector (push) Successful in 4m1s
Test / Flake checks (push) Successful in 49s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 01:10:27 +09:00
cat 44277dc0f1 dbus: run in native sandbox
Test / Create distribution (push) Successful in 24s
Test / Fortify (push) Successful in 2m31s
Test / Fpkg (push) Successful in 3m25s
Test / Data race detector (push) Successful in 4m5s
Test / Flake checks (push) Successful in 53s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 00:13:14 +09:00
cat bc54db54d2 ldd: always copy stderr
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m30s
Test / Fpkg (push) Successful in 3m34s
Test / Data race detector (push) Successful in 3m55s
Test / Flake checks (push) Successful in 53s
Dropping the buffer on success is unhelpful and could hide some useful information.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 00:08:00 +09:00
cat bf07b7cd9e ldd: mount /proc in container
Test / Create distribution (push) Successful in 25s
Test / Fpkg (push) Successful in 3m45s
Test / Data race detector (push) Successful in 4m0s
Test / Fortify (push) Successful in 1m54s
Test / Flake checks (push) Successful in 53s
This covers host /proc.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 00:01:03 +09:00
cat 5d3c8dcc92 test: raise timeout
Test / Create distribution (push) Successful in 25s
Test / Fpkg (push) Successful in 32s
Test / Fortify (push) Successful in 2m11s
Test / Data race detector (push) Successful in 2m42s
Test / Flake checks (push) Successful in 51s
Native container tooling is severely slowed down by race detector. Raise timeout so it reliably completes.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-16 23:51:17 +09:00
cat 48feca800f sandbox: check command function pointer
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m37s
Test / Fpkg (push) Successful in 3m25s
Test / Data race detector (push) Successful in 3m59s
Test / Flake checks (push) Successful in 55s
Setting default CommandContext on initialisation is somewhat of a footgun.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-16 23:29:14 +09:00
cat 42de09e896 helper: implement native container backend
Test / Create distribution (push) Successful in 24s
Test / Fortify (push) Successful in 2m36s
Test / Fpkg (push) Successful in 3m23s
Test / Data race detector (push) Successful in 3m52s
Test / Flake checks (push) Successful in 49s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-16 02:57:46 +09:00
cat 1576fea8a3 helper: raise WaitDelay during tests
Test / Create distribution (push) Successful in 24s
Test / Fpkg (push) Successful in 3m19s
Test / Data race detector (push) Successful in 3m54s
Test / Fortify (push) Successful in 1m39s
Test / Flake checks (push) Successful in 49s
Helper runs very slowly with race detector. This prevents it from timing out.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-16 02:49:41 +09:00
cat ae522ab364 test: run go tests with race detector
Test / Create distribution (push) Successful in 24s
Test / Fpkg (push) Successful in 32s
Test / Fortify (push) Successful in 2m21s
Test / Data race detector (push) Successful in 2m38s
Test / Flake checks (push) Successful in 48s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-16 02:07:42 +09:00
cat 273d97af85 ldd: lib paths resolve function
Test / Create distribution (push) Successful in 24s
Test / Fortify (push) Successful in 2m37s
Test / Fpkg (push) Successful in 3m37s
Test / Data race detector (push) Successful in 3m50s
Test / Flake checks (push) Successful in 56s
This is what always happens right after a ldd call, so implement it here.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-16 01:20:09 +09:00
cat 891316d924 helper/stub: copy args to stderr
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m33s
Test / Fpkg (push) Successful in 3m30s
Test / Data race detector (push) Successful in 3m52s
Test / Flake checks (push) Successful in 53s
Some helpers are implemented via go test itself in tests, and as a result stdout gets clobbered.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-16 00:39:42 +09:00
cat 9f5dad1998 sandbox: return on zero length ops
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m30s
Test / Fpkg (push) Successful in 3m24s
Test / Data race detector (push) Successful in 3m53s
Test / Flake checks (push) Successful in 52s
This dodges potentially confusing behaviour where init fails due to Ops being clobbered during transfer.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-16 00:32:36 +09:00
cat 6e7ddb2d2e helper: eliminate commandContext replacement
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m44s
Test / Fpkg (push) Successful in 3m42s
Test / Data race detector (push) Successful in 3m51s
Test / Flake checks (push) Successful in 57s
This is done more cleanly by modifying Args in cmdF.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-16 00:01:25 +09:00
cat bac4e67867 sandbox/init: early params nil check
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m31s
Test / Fpkg (push) Successful in 3m48s
Test / Data race detector (push) Successful in 3m53s
Test / Flake checks (push) Successful in 51s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-15 04:03:10 +09:00
cat 4230281194 sandbox: return error on doubled start
Test / Create distribution (push) Successful in 18s
Test / Fpkg (push) Successful in 35s
Test / Fortify (push) Successful in 38s
Test / Data race detector (push) Successful in 36s
Test / Flake checks (push) Successful in 58s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-15 03:30:14 +09:00
cat e64e7608ca sandbox: expose cancel behaviour
Test / Create distribution (push) Successful in 40s
Test / Fpkg (push) Successful in 11m53s
Test / Fortify (push) Successful in 1m57s
Test / Data race detector (push) Successful in 2m33s
Test / Flake checks (push) Successful in 58s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-15 03:04:27 +09:00
cat 10a21ce3ef helper: expose extra files to direct
Test / Create distribution (push) Successful in 42s
Test / Fpkg (push) Successful in 11m23s
Test / Fortify (push) Successful in 5m32s
Test / Data race detector (push) Successful in 2m35s
Test / Flake checks (push) Successful in 56s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-15 02:27:40 +09:00
cat 0f1f0e4364 helper: combine helper ipc setup
Test / Create distribution (push) Successful in 43s
Test / Fortify (push) Successful in 6m53s
Test / Fpkg (push) Successful in 11m51s
Test / Data race detector (push) Successful in 2m32s
Test / Flake checks (push) Successful in 56s
The two-step args call is no longer necessary since stat is passed on initialisation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-15 02:10:22 +09:00
cat f9bf20a3c7 helper: rearrange initialisation args
Test / Create distribution (push) Successful in 41s
Test / Fortify (push) Successful in 3m3s
Test / Data race detector (push) Successful in 4m32s
Test / Fpkg (push) Successful in 4m47s
Test / Flake checks (push) Successful in 1m3s
This improves consistency across two different helper implementations.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-15 01:06:31 +09:00
cat 73c1a83032 helper: move process wrapper to direct
Test / Create distribution (push) Successful in 27s
Test / Fortify (push) Successful in 2m42s
Test / Fpkg (push) Successful in 3m49s
Test / Data race detector (push) Successful in 4m1s
Test / Flake checks (push) Successful in 59s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-15 00:33:25 +09:00
cat f443d315ad helper: clean up interface
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m37s
Test / Fpkg (push) Successful in 3m40s
Test / Data race detector (push) Successful in 3m54s
Test / Flake checks (push) Successful in 59s
The helper interface was messy due to odd context acquisition order. That has changed, so this cleans it up.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-15 00:27:44 +09:00
cat 9e18d1de77 helper/proc: pass extra files and start
Test / Create distribution (push) Successful in 28s
Test / Fortify (push) Successful in 2m41s
Test / Fpkg (push) Successful in 3m38s
Test / Data race detector (push) Successful in 3m53s
Test / Flake checks (push) Successful in 59s
For integration with native container tooling.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-14 23:23:57 +09:00
cat 2647a71be1 seccomp: move out of helper
Test / Create distribution (push) Successful in 29s
Test / Fortify (push) Successful in 2m53s
Test / Fpkg (push) Successful in 4m0s
Test / Data race detector (push) Successful in 4m9s
Test / Flake checks (push) Successful in 59s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-14 22:42:40 +09:00
cat 7c60a4d8e8 helper: embed context on creation
Test / Create distribution (push) Successful in 24s
Test / Fortify (push) Successful in 2m34s
Test / Fpkg (push) Successful in 3m22s
Test / Data race detector (push) Successful in 3m44s
Test / Flake checks (push) Successful in 49s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-14 18:30:22 +09:00
cat 4bb5d9780f ldd: run in native sandbox
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m27s
Test / Fpkg (push) Successful in 3m22s
Test / Data race detector (push) Successful in 3m43s
Test / Flake checks (push) Successful in 48s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-14 17:55:55 +09:00
cat f41fd94628 sandbox: write uid/gid map as init
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m30s
Test / Fpkg (push) Successful in 3m21s
Test / Data race detector (push) Successful in 3m39s
Test / Flake checks (push) Successful in 48s
This avoids PR_SET_DUMPABLE in the parent process.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-14 17:42:22 +09:00
cat 94895bbacb sandbox: invert seccomp ruleset defaults
Test / Create distribution (push) Successful in 24s
Test / Fortify (push) Successful in 2m31s
Test / Fpkg (push) Successful in 3m20s
Test / Data race detector (push) Successful in 3m35s
Test / Flake checks (push) Successful in 50s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-14 02:38:32 +09:00
cat f332200ca4 sandbox: mount container /dev
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m29s
Test / Fpkg (push) Successful in 3m26s
Test / Data race detector (push) Successful in 3m33s
Test / Flake checks (push) Successful in 51s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-14 02:18:44 +09:00
cat 2eff470091 sandbox/mount: pass custom tmpfs name
Test / Create distribution (push) Successful in 27s
Test / Fortify (push) Successful in 2m51s
Test / Data race detector (push) Successful in 3m53s
Test / Fpkg (push) Successful in 3m59s
Test / Flake checks (push) Successful in 55s
The tmpfs driver allows arbitrary fsname.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-14 02:12:35 +09:00
cat a092b042ab sandbox: pass params to setup ops
Test / Create distribution (push) Successful in 20s
Test / Fortify (push) Successful in 2m5s
Test / Fpkg (push) Successful in 3m26s
Test / Data race detector (push) Successful in 3m49s
Test / Flake checks (push) Successful in 55s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-14 02:11:38 +09:00
cat e94b09d337 sandbox/mount: fix source flag path
Test / Create distribution (push) Successful in 20s
Test / Fortify (push) Successful in 2m6s
Test / Fpkg (push) Successful in 3m24s
Test / Data race detector (push) Successful in 3m56s
Test / Flake checks (push) Successful in 54s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-14 02:10:48 +09:00
cat 5d9e669d97 sandbox: separate tmpfs function from op
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m34s
Test / Fpkg (push) Successful in 3m25s
Test / Data race detector (push) Successful in 3m32s
Test / Flake checks (push) Successful in 52s
This is useful in the implementation of various other ops.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-14 00:21:20 +09:00
cat f1002157a5 sandbox: separate bind mount function from op
Test / Create distribution (push) Successful in 24s
Test / Fortify (push) Successful in 2m33s
Test / Fpkg (push) Successful in 3m26s
Test / Data race detector (push) Successful in 3m36s
Test / Flake checks (push) Successful in 53s
This is useful in the implementation of various other ops.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-14 00:16:41 +09:00
cat 4133b555ba internal/app: rename init to init0
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m27s
Test / Fpkg (push) Successful in 3m21s
Test / Data race detector (push) Successful in 3m40s
Test / Flake checks (push) Successful in 48s
This makes way for the new container init.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-13 21:57:54 +09:00
cat 9b1a60b5c9 sandbox: native container tooling
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m28s
Test / Fpkg (push) Successful in 3m23s
Test / Data race detector (push) Successful in 3m35s
Test / Flake checks (push) Successful in 48s
This should eventually replace bwrap.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-13 21:36:26 +09:00
cat beb3918809 test: run go test under regular user
Test / Create distribution (push) Successful in 24s
Test / Fpkg (push) Successful in 32s
Test / Fortify (push) Successful in 2m16s
Test / Data race detector (push) Successful in 2m46s
Test / Flake checks (push) Successful in 54s
By default test vm commands run as root, this causes buildFHSEnv bwrap to cover some parts of /proc, making it impossible to mount proc in a mount namespace created under it. Running as a regular user gets around this issue.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-13 20:56:32 +09:00
cat 2871426df2 test: print output of failed test
Test / Create distribution (push) Successful in 29s
Test / Fpkg (push) Successful in 36s
Test / Fortify (push) Successful in 2m21s
Test / Data race detector (push) Successful in 2m39s
Test / Flake checks (push) Successful in 53s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-13 16:40:15 +09:00
cat e048f31baa internal: pull EINTR loop from stdlib
Test / Create distribution (push) Successful in 20s
Test / Fpkg (push) Successful in 35s
Test / Fortify (push) Successful in 37s
Test / Data race detector (push) Successful in 36s
Test / Flake checks (push) Successful in 57s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-13 00:42:38 +09:00
cat 6af8b8859f sandbox: read overflow ids
Test / Create distribution (push) Successful in 19s
Test / Fortify (push) Successful in 1m53s
Test / Fpkg (push) Successful in 3m7s
Test / Data race detector (push) Successful in 3m33s
Test / Flake checks (push) Successful in 54s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-13 00:41:37 +09:00
cat f38ba7e923 test/sandbox: bypass fields
Test / Create distribution (push) Successful in 27s
Test / Fortify (push) Successful in 2m33s
Test / Fpkg (push) Successful in 3m26s
Test / Data race detector (push) Successful in 3m44s
Test / Flake checks (push) Successful in 53s
A field is bypassed if it contains a single null byte. This will never appear in the text format so is safe to use.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-13 00:00:58 +09:00
cat d22145a392 ldd: handle musl static behaviour
Test / Create distribution (push) Successful in 28s
Test / Fortify (push) Successful in 2m36s
Test / Fpkg (push) Successful in 3m24s
Test / Data race detector (push) Successful in 3m32s
Test / Flake checks (push) Successful in 50s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-12 23:29:43 +09:00
cat 29c3f8becb helper/seccomp: improve error handling
Test / Create distribution (push) Successful in 24s
Test / Fortify (push) Successful in 2m32s
Test / Fpkg (push) Successful in 3m18s
Test / Data race detector (push) Successful in 3m26s
Test / Flake checks (push) Successful in 47s
This passes both errno and libseccomp return value.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-12 15:52:48 +09:00
cat be16970e77 helper/seccomp: seccomp_load on negative fd
Test / Create distribution (push) Successful in 24s
Test / Fortify (push) Successful in 2m32s
Test / Fpkg (push) Successful in 3m23s
Test / Data race detector (push) Successful in 3m28s
Test / Flake checks (push) Successful in 50s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-12 15:18:52 +09:00
cat df266527f1 test/sandbox/mount: work around nondeterminism
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m30s
Test / Fpkg (push) Successful in 3m20s
Test / Data race detector (push) Successful in 3m35s
Test / Flake checks (push) Successful in 50s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-12 15:16:51 +09:00
cat c8ed7aae6e nix: update flake lock
Test / Create distribution (push) Successful in 42s
Test / Fortify (push) Successful in 24m42s
Test / Data race detector (push) Successful in 25m3s
Test / Fpkg (push) Successful in 25m40s
Test / Flake checks (push) Successful in 1m43s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-10 18:38:14 +09:00
cat 61e58aa14d helper/proc: expose setup file
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m34s
Test / Fpkg (push) Successful in 3m29s
Test / Data race detector (push) Successful in 3m27s
Test / Flake checks (push) Successful in 51s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-09 17:22:31 +09:00
cat 9e15898c8f internal/prctl: rename prctl wrappers
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m39s
Test / Data race detector (push) Successful in 3m29s
Test / Fpkg (push) Successful in 3m34s
Test / Flake checks (push) Successful in 51s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-07 22:56:35 +09:00
cat f7bd6a5a41 test/sandbox: check seccomp outcome
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m40s
Test / Fpkg (push) Successful in 3m39s
Test / Data race detector (push) Successful in 3m44s
Test / Flake checks (push) Successful in 51s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-04 13:30:16 +09:00
cat ea853e21d9 test/sandbox: check fs outcome
Test / Create distribution (push) Successful in 19s
Test / Fpkg (push) Successful in 33s
Test / Fortify (push) Successful in 35s
Test / Data race detector (push) Successful in 35s
Test / Flake checks (push) Successful in 52s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-03 01:02:09 +09:00
cat 0bd9b9e8fe test/sandbox: assert filesystem json
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m27s
Test / Fpkg (push) Successful in 3m30s
Test / Data race detector (push) Successful in 3m30s
Test / Flake checks (push) Successful in 57s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-02 23:23:04 +09:00
cat 39e32799b3 test/sandbox: compare filesystem hierarchy
Test / Create distribution (push) Successful in 27s
Test / Fortify (push) Successful in 2m34s
Test / Data race detector (push) Successful in 3m37s
Test / Fpkg (push) Successful in 3m41s
Test / Flake checks (push) Successful in 56s
For checking deterministic aspects of fs outcome.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-02 22:59:04 +09:00
cat 9953768de5 test: rename session message identifier
Test / Create distribution (push) Successful in 26s
Test / Fpkg (push) Successful in 35s
Test / Fortify (push) Successful in 2m14s
Test / Data race detector (push) Successful in 2m36s
Test / Flake checks (push) Successful in 56s
Labelling this as sway is misleading.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-02 22:47:33 +09:00
cat 0d3652b793 test/sandbox/assert: wrap printf
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m34s
Test / Data race detector (push) Successful in 3m30s
Test / Fpkg (push) Successful in 3m38s
Test / Flake checks (push) Successful in 50s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-02 18:37:46 +09:00
cat d8e9d71f87 test/sandbox: check mount outcome
Test / Create distribution (push) Successful in 21s
Test / Fpkg (push) Successful in 32s
Test / Fortify (push) Successful in 35s
Test / Data race detector (push) Successful in 35s
Test / Flake checks (push) Successful in 49s
Do this at the beginning of the test for early failure.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-28 15:56:15 +09:00
cat 558974b996 test/sandbox: assert mntent json
Test / Create distribution (push) Successful in 28s
Test / Fortify (push) Successful in 2m27s
Test / Fpkg (push) Successful in 3m24s
Test / Data race detector (push) Successful in 3m25s
Test / Flake checks (push) Successful in 49s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-28 15:40:58 +09:00
cat 4de4049713 test/sandbox: wrap libc getmntent
Test / Create distribution (push) Successful in 30s
Test / Fortify (push) Successful in 2m35s
Test / Data race detector (push) Successful in 3m23s
Test / Fpkg (push) Successful in 3m35s
Test / Flake checks (push) Successful in 50s
For checking mounts outcome.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-28 14:56:08 +09:00
cat 2d4cabe786 nix: increase nixfmt max width
Test / Create distribution (push) Successful in 30s
Test / Fpkg (push) Successful in 36s
Test / Data race detector (push) Successful in 35s
Test / Fortify (push) Successful in 39s
Test / Flake checks (push) Successful in 50s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-28 14:43:46 +09:00
cat 80f9b62d25 app: print comp values early
Test / Create distribution (push) Successful in 30s
Test / Fortify (push) Successful in 2m31s
Test / Fpkg (push) Successful in 3m27s
Test / Data race detector (push) Successful in 3m26s
Test / Flake checks (push) Successful in 51s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-26 22:27:55 +09:00
cat 673b648bd3 cmd/fpkg: call app in-process
Test / Create distribution (push) Successful in 28s
Test / Fortify (push) Successful in 2m31s
Test / Data race detector (push) Successful in 3m25s
Test / Fpkg (push) Successful in 3m29s
Test / Flake checks (push) Successful in 55s
Wrapping fortify is slow, painful and error-prone. Start apps in-process instead.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-26 19:51:44 +09:00
cat 45ad788c6d cmd/fsu: allow switch from fpkg
Test / Create distribution (push) Successful in 32s
Test / Fortify (push) Successful in 2m12s
Test / Data race detector (push) Successful in 2m30s
Test / Fpkg (push) Successful in 3m8s
Test / Flake checks (push) Successful in 49s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-26 19:42:28 +09:00
cat 56539d8db5 fortify: move internal commands up
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m30s
Test / Data race detector (push) Successful in 3m27s
Test / Fpkg (push) Successful in 3m34s
Test / Flake checks (push) Successful in 52s
This improves readability.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-26 18:02:11 +09:00
cat 840ceb615a app: handle RunState errors
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m27s
Test / Data race detector (push) Successful in 3m24s
Test / Fpkg (push) Successful in 3m30s
Test / Flake checks (push) Successful in 52s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-26 17:36:14 +09:00
cat 741d011543 fortify: configure seccomp logger early
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m27s
Test / Fpkg (push) Successful in 3m28s
Test / Data race detector (push) Successful in 3m27s
Test / Flake checks (push) Successful in 51s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-26 17:19:36 +09:00
cat d050b3de25 app: define errors in a separate file
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m28s
Test / Data race detector (push) Successful in 3m25s
Test / Fpkg (push) Successful in 3m31s
Test / Flake checks (push) Successful in 52s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-26 17:12:02 +09:00
cat 5de28800ad test: verify fsu ppid check
Test / Create distribution (push) Successful in 27s
Test / Fpkg (push) Successful in 33s
Test / Fortify (push) Successful in 1m44s
Test / Data race detector (push) Successful in 2m8s
Test / Flake checks (push) Successful in 51s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-26 16:51:57 +09:00
cat 8e50293ab7 test: remove sway process check
Test / Create distribution (push) Successful in 26s
Test / Fpkg (push) Successful in 34s
Test / Fortify (push) Successful in 1m50s
Test / Data race detector (push) Successful in 2m12s
Test / Flake checks (push) Successful in 54s
This eliminates the race where systemd restarts sway too quick.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-26 13:52:44 +09:00
cat 12c6d66bfd cmd/fpkg/test: nixos test fpkg install/start
Test / Create distribution (push) Successful in 27s
Test / Fortify (push) Successful in 2m33s
Test / Data race detector (push) Successful in 3m25s
Test / Fpkg (push) Successful in 38m26s
Test / Flake checks (push) Successful in 54s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-26 13:12:16 +09:00
cat d7d2bd33ed cmd/fpkg/build: expose nixos configuration
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 36s
Test / Data race detector (push) Successful in 36s
Test / Flake checks (push) Successful in 44s
This should be used sparingly as the NixOS closure is in the bootstrap store which compresses rather poorly.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-26 12:31:18 +09:00
cat c21a4cff14 nix: wrap fpkg
Test / Create distribution (push) Successful in 26s
Test / Data race detector (push) Successful in 2m11s
Test / Fortify (push) Successful in 2m24s
Test / Flake checks (push) Successful in 42s
This is usable on nixos now due to the static build.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-26 12:24:04 +09:00
cat 4fa38d6063 cmd/fpkg: use fortify path from internal
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m28s
Test / Data race detector (push) Successful in 3m22s
Test / Flake checks (push) Successful in 43s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-26 12:16:35 +09:00
cat 6d4ac3d9fd internal: store fortify path in internal
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m33s
Test / Data race detector (push) Successful in 3m20s
Test / Flake checks (push) Successful in 42s
This now makes more sense due to the changes in build system.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-26 12:03:25 +09:00
cat a5d2f040fb cmd/fpkg/build: run final build step in nix
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 34s
Test / Data race detector (push) Successful in 34s
Test / Flake checks (push) Successful in 41s
This used to be a script that had to be run outside of nix because the sandbox disallows access to nix store state. Turns out closureInfo is the proper way to do that.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-25 23:53:18 +09:00
cat c62689e17f nix: interrupt via tty
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 1m46s
Test / Data race detector (push) Successful in 2m9s
Test / Flake checks (push) Successful in 42s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-25 18:20:47 +09:00
cat 39dc8e7bd8 dbus: set process group id
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m18s
Test / Data race detector (push) Successful in 3m11s
Test / Flake checks (push) Successful in 40s
This stops signals sent by the TTY driver from propagating to the xdg-dbus-proxy process.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-25 18:12:41 +09:00
cat 5a732d153e nix: include fsu sources in dist build
Test / Create distribution (push) Successful in 20s
Test / Fortify (push) Successful in 37s
Test / Data race detector (push) Successful in 37s
Test / Flake checks (push) Successful in 46s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-25 01:32:47 +09:00
cat b4549c72be nix: verify silent signal exit
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 1m40s
Test / Data race detector (push) Successful in 2m1s
Test / Flake checks (push) Successful in 41s
This catches errors in the cleanup process initiated by a signal.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-25 01:22:16 +09:00
cat 1818dc3a4c system/acl: do not fail gone revert target
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m20s
Test / Data race detector (push) Successful in 3m3s
Test / Flake checks (push) Successful in 46s
A removed file effectively already has its ACLs stripped, so failing this makes no sense. Still print a message to warn about it.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-25 01:11:05 +09:00
cat 65094b63cd system/dbus: filter context cancellation error
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m21s
Test / Data race detector (push) Successful in 3m5s
Test / Flake checks (push) Successful in 41s
This message would otherwise show up when alternative exit path is taken due to a signal.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-25 00:57:35 +09:00
cat f0a082ec84 fortify: improve handling of RevertErr
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m17s
Test / Data race detector (push) Successful in 2m57s
Test / Flake checks (push) Successful in 43s
All this error wrapping is getting a bit ridiculous and I might want to do something about that somewhere down the line.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-25 00:45:00 +09:00
cat 751aa350ee nix: exclude files ending in ".py"
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m12s
Test / Data race detector (push) Successful in 2m59s
Test / Flake checks (push) Successful in 44s
This reduces rebuilds when debugging nixos tests.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-24 17:41:56 +09:00
cat e6cd2bb2a8 cmd/fpkg: integrate command handler
Test / Create distribution (push) Successful in 18s
Test / Fortify (push) Successful in 34s
Test / Data race detector (push) Successful in 1m39s
Test / Flake checks (push) Successful in 39s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 23:25:12 +09:00
cat 0fb72e5d99 cmd/fpkg/build: prepend extra nix flags
Test / Create distribution (push) Successful in 25s
Test / Data race detector (push) Successful in 35s
Test / Fortify (push) Successful in 35s
Test / Flake checks (push) Successful in 39s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 20:21:09 +09:00
cat 71135f339a release: 0.2.18
Test / Create distribution (push) Successful in 20s
Release / Create release (push) Successful in 33s
Test / Fortify (push) Successful in 2m4s
Test / Data race detector (push) Successful in 2m33s
Test / Flake checks (push) Successful in 48s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 18:52:33 +09:00
cat b6af8caffe nix: clean up directory structure
Test / Create distribution (push) Successful in 19s
Test / Fortify (push) Successful in 36s
Test / Data race detector (push) Successful in 56s
Test / Flake checks (push) Successful in 41s
Tests for fpkg is going to be in ./cmd/fpkg, so this central tests directory is no longer necessary.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 18:48:01 +09:00
cat e1a3549ea0 workflows: separate nixos tests from flake check
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m12s
Test / Data race detector (push) Successful in 3m0s
Test / Flake checks (push) Successful in 41s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 18:34:42 +09:00
cat 8bf162820b nix: separate fsu from package
Test / Create distribution (push) Successful in 26s
Test / Run NixOS test (push) Successful in 7m25s
This appears to be the only way to build them with different configuration. This enables static linking in the main package.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 18:13:37 +09:00
cat dccb366608 ldd: handle behaviour on static executable
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m27s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 18:02:33 +09:00
cat 83c8f0488b ldd: pass absolute path to bwrap
Test / Create distribution (push) Successful in 27s
Test / Run NixOS test (push) Successful in 3m31s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 17:46:22 +09:00
cat 478b27922c fortify: handle errors via MustParse
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m29s
The errSuccess behaviour is kept for beforeExit.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 12:57:59 +09:00
cat ba1498cd18 command: filter parse errors
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m29s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 12:55:10 +09:00
cat eda4d612c2 fortify: keep external files alive
Test / Create distribution (push) Successful in 19s
Test / Run NixOS test (push) Successful in 3m10s
This should eliminate sporadic failures, like the known double close in "seccomp".

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 03:24:37 +09:00
cat 2e7e160683 release: 0.2.17
Release / Create release (push) Successful in 33s
Test / Create distribution (push) Successful in 20s
Test / Run NixOS test (push) Successful in 3m50s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 02:59:31 +09:00
cat 79957f8ea7 fortify: test help message
Test / Create distribution (push) Successful in 19s
Test / Run NixOS test (push) Successful in 50s
This helps catch regressions in "command".

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 02:51:35 +09:00
cat 7e52463445 fortify: integrate command handler
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m24s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 02:35:02 +09:00
cat 89970f5197 command/flag: implement repeatable flag
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m29s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 02:25:31 +09:00
cat 35037705a9 command/flag: implement integer flag
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m23s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 02:02:01 +09:00
cat 647c6ea21b command: hide internal commands
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m26s
This marks commands as internal via a magic usage string.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 01:36:48 +09:00
cat 416d93e880 command: expose print help
Test / Create distribution (push) Successful in 26s
Test / Run NixOS test (push) Successful in 3m25s
This is useful for custom help commands.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 01:17:57 +09:00
cat 312753924b command: root early handler func special case
Test / Create distribution (push) Successful in 27s
Test / Run NixOS test (push) Successful in 3m27s
This allows for early initialisation with access to flags on the root node. This can be useful for configuring global state used by subcommands.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 00:55:18 +09:00
cat 54308f79d2 command: expose command with direct handling
Test / Create distribution (push) Successful in 26s
Test / Run NixOS test (push) Successful in 3m28s
This exposes flag set on commands with direct handling.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 00:24:03 +09:00
cat dfa3217037 command: implement builder and parser
Test / Create distribution (push) Successful in 20s
Test / Run NixOS test (push) Successful in 2m47s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-22 23:11:17 +09:00
cat 8000a2febb command: implement help builder
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m28s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-22 22:43:37 +09:00
cat 7bd48d3489 command: implement node structure
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m19s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-22 20:30:49 +09:00
cat b5eaeac11a command: declare command interface
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m23s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-22 20:29:47 +09:00
cat a9986aab6a system: document I methods
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m21s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-21 19:51:12 +09:00
cat ff30a5ab5d fst: remove empty file
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m24s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-21 19:27:08 +09:00
cat eb0c16dd8c cmd/fpkg: rename buildPackage file
Test / Create distribution (push) Successful in 26s
Test / Run NixOS test (push) Successful in 50s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-21 18:13:34 +09:00
cat 4fa1e97026 cmd/fpkg: rename shell to shellPath
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m26s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-21 17:50:20 +09:00
cat 64b6dc41ba nix: split integration test
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m24s
For adding tests for fpkg.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-21 17:05:17 +09:00
cat c64b8163e7 app: separate instance from process state
Test / Create distribution (push) Successful in 19s
Test / Run NixOS test (push) Successful in 1m59s
This works better for the implementation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-21 16:06:24 +09:00
cat 9d9a165379 release: 0.2.16
Release / Create release (push) Successful in 35s
Test / Create distribution (push) Successful in 22s
Test / Run NixOS test (push) Successful in 2m41s
Mostly refactor and cleanup, but also contains major fix to process lifecycle management.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-19 23:39:16 +09:00
cat d0dff1cac9 wl: check against null character
Test / Create distribution (push) Successful in 23s
Test / Run NixOS test (push) Successful in 54s
Wayland library takes null terminated strings.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-19 23:35:49 +09:00
cat 3c80fd2b0f app: defer system.I revert
Test / Create distribution (push) Successful in 19s
Test / Run NixOS test (push) Successful in 49s
Just returning an error after a successful call of commit will leave garbage behind with no way for the caller to clean them. This change ensures revert is always called after successful commit with at least per-process state enabled.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-19 21:12:11 +09:00
cat ef81828e0c app: remove share method
Test / Create distribution (push) Successful in 26s
Test / Run NixOS test (push) Successful in 2m3s
This is yet another implementation detail from before system.I, getting rid of this vastly cuts down on redundant seal state.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-19 16:20:25 +09:00
cat 2978a6f046 app: separate appSeal finalise method
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m27s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-19 12:33:51 +09:00
cat dfd9467523 app: merge seal with sys
Test / Create distribution (push) Successful in 27s
Test / Run NixOS test (push) Successful in 3m20s
The existence of the appSealSys struct was an implementation detail obsolete since system.I was integrated in 084cd84f36.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-19 01:36:29 +09:00
cat 53571f030e app: embed appSeal in app struct
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m22s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-19 01:10:37 +09:00
cat aa164081e1 app/seal: improve documentation
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m22s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-19 01:04:14 +09:00
cat 9a10eeab90 app/seal: embed enablements
Test / Create distribution (push) Successful in 26s
Test / Run NixOS test (push) Successful in 3m28s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-19 00:41:51 +09:00
cat d1f83f40d6 helper/bwrap: rename Write to WriteFile
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m25s
In case this might want to be an io.Writer.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-19 00:34:19 +09:00
cat a748d40745 app: store values with string representation
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m26s
Improves code readability without changing memory layout.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-19 00:25:00 +09:00
cat 648e1d641a app: separate interface from implementation
Test / Create distribution (push) Successful in 26s
Test / Run NixOS test (push) Successful in 3m31s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-18 23:07:28 +09:00
cat 3c327084d3 fst: declare wrappers for sandbox config
Test / Create distribution (push) Successful in 26s
Test / Run NixOS test (push) Successful in 3m30s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-18 23:04:13 +09:00
cat ffaa12b9d8 sys: wrap log methods
Test / Create distribution (push) Successful in 26s
Test / Run NixOS test (push) Successful in 3m31s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-18 22:52:09 +09:00
cat bf95127332 fst: move App interface declaration
Test / Create distribution (push) Successful in 26s
Test / Run NixOS test (push) Successful in 3m24s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-18 22:36:45 +09:00
cat e0f321b2c4 sys: rename from linux
Test / Create distribution (push) Successful in 26s
Test / Run NixOS test (push) Successful in 3m28s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-18 18:47:48 +09:00
cat 2c9c7fee5b linux: wrap fsu lookup error
Test / Create distribution (push) Successful in 35s
Test / Run NixOS test (push) Successful in 5m58s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-18 17:39:53 +09:00
cat d0400f3c81 fmsg: PrintBaseError skip empty message
Test / Create distribution (push) Successful in 24s
Test / Run NixOS test (push) Successful in 3m22s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-18 17:01:26 +09:00
cat e9b0f9faef fmsg: export logBaseError function
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m16s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-18 13:02:51 +09:00
cat e85be67fd9 acl: implement Update in C
Test / Create distribution (push) Successful in 18s
Test / Run NixOS test (push) Successful in 46s
The original implementation was effectively just writing C in Go.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-17 21:39:14 +09:00
cat 7e69893264 acl: rename UpdatePerms to Update
Test / Create distribution (push) Successful in 24s
Test / Run NixOS test (push) Successful in 3m21s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-17 20:33:18 +09:00
cat 38a3e6af03 system: make xcb internal
Test / Create distribution (push) Successful in 27s
Test / Run NixOS test (push) Successful in 3m29s
This package is hauntingly ugly. Move this to internal until it is removed or replaced.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-17 19:07:53 +09:00
cat 90cb01b274 system: move out of internal
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m17s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-17 19:00:43 +09:00
cat b1e1d5627e system: wrap console output functions
Test / Create distribution (push) Successful in 24s
Test / Run NixOS test (push) Successful in 3m13s
This eliminates all fmsg imports from internal/system.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-17 18:17:55 +09:00
cat 3ae2ab652e system/wayland: sync file at caller specified address
Test / Create distribution (push) Successful in 24s
Test / Run NixOS test (push) Successful in 3m14s
Storing this in sys is incredibly ugly: sys should be stateless and Ops must keep track of their state.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-17 13:24:17 +09:00
cat db71fbe22b system/tmpfiles: fail gracefully in API misuse
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m25s
Panicking here leaves garbage behind. Not ideal if this package is going to be exported.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-17 12:17:01 +09:00
cat 83e72c2b59 release: 0.2.15
Test / Create distribution (push) Successful in 19s
Release / Create release (push) Successful in 33s
Test / Run NixOS test (push) Successful in 3m12s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-17 00:13:04 +09:00
cat 82a072f641 system/tmpfiles: implement private tmpfiles
Test / Create distribution (push) Successful in 19s
Test / Run NixOS test (push) Successful in 3m30s
These are only available within the mount namespace and should significantly reduce attack surface.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-17 00:07:52 +09:00
cat 60c10c3f4a nix: run integration tests with race detector
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m4s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-16 20:58:08 +09:00
cat 468696f611 internal: beforeExit before reachable fatal calls
Test / Create distribution (push) Successful in 18s
Test / Run NixOS test (push) Successful in 47s
These are the only two calls to log.Fatal* reachable during suspended output. Call fmsg.BeforeExit here to catch that.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-16 19:03:34 +09:00
cat 29c38caac8 app/shim/manager: return error on bad fsu path
Test / Create distribution (push) Successful in 24s
Test / Run NixOS test (push) Successful in 2m9s
This results in a graceful failure that does not leave garbage behind.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-16 18:59:45 +09:00
cat e599b5583d fmsg: implement suspend in writer
Test / Create distribution (push) Successful in 24s
Test / Run NixOS test (push) Successful in 2m18s
This removes the requirement to call fmsg.Exit on every exit path, and enables direct use of the "log" package. However, fmsg.BeforeExit is still encouraged when possible to catch exit on suspended output.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-16 18:51:53 +09:00
cat 33a4ab11c2 internal: move shim and init into app
Test / Create distribution (push) Successful in 24s
Test / Run NixOS test (push) Successful in 2m7s
This structure makes more sense, as both processes are part of an app's lifecycle.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-16 16:28:46 +09:00
cat 1fa5e992e4 helper/bwrap: expose address of DataConfig
Test / Create distribution (push) Successful in 24s
Test / Run NixOS test (push) Successful in 2m7s
This allows the caller to defer fulfilling its payload.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-16 12:33:59 +09:00
cat c667b13a00 system: separate link Op implementation
Test / Create distribution (push) Successful in 24s
Test / Run NixOS test (push) Successful in 2m13s
This Op would still be useful after replacing the Tmpfiles interface, so isolate it here.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-16 12:15:26 +09:00
cat 90b86a5531 release: 0.2.14
Release / Create release (push) Successful in 24s
Test / Create distribution (push) Successful in 18s
Test / Run NixOS test (push) Successful in 50s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 23:05:02 +09:00
cat f545e154f0 workflows: use native nix runner
Test / Create distribution (push) Successful in 20s
Test / Run NixOS test (push) Successful in 51s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 22:58:04 +09:00
cat 268a90f1a5 app: improve WAYLAND_DISPLAY correctness
Test / Create distribution (push) Successful in 46s
Test / Run NixOS test (push) Successful in 3m35s
This now has identical behaviour as wayland C library.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 14:45:09 +09:00
cat 3054527ca5 fortify: prevent exit status 0 on app failure
Test / Create distribution (push) Successful in 46s
Test / Run NixOS test (push) Successful in 3m37s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 14:40:19 +09:00
cat ddb2f9c11b app: remove wayland socket hard link
Test / Create distribution (push) Successful in 49s
Test / Run NixOS test (push) Successful in 3m32s
This Op was not doing anything useful.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 10:54:00 +09:00
cat 6ae02e72fa nix: test direct_wayland behaviour
Test / Create distribution (push) Successful in 47s
Test / Run NixOS test (push) Successful in 3m35s
This should never be used outside tests unless you absolutely know what you're doing or are using GNOME.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 10:45:27 +09:00
cat 989fb5395f nix: remove unused configuration
Test / Create distribution (push) Successful in 49s
Test / Run NixOS test (push) Successful in 3m30s
User setup no longer depends on userdb.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 10:10:42 +09:00
cat f955b15b84 system: remove write mode tmpfiles
Test / Create distribution (push) Successful in 57s
Test / Run NixOS test (push) Successful in 3m42s
This interface is ugly and bug-prone. This change removes its write mode which has been obsoleted by CopyBind.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 03:22:20 +09:00
cat 0340c67995 app: port passwd and group files to copy
Test / Create distribution (push) Successful in 49s
Test / Run NixOS test (push) Successful in 3m41s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 03:19:06 +09:00
cat 72b0160aad helper/bwrap: implement file copy flags
Test / Create distribution (push) Successful in 49s
Test / Run NixOS test (push) Successful in 3m42s
These are significantly more efficient and less error-prone than mounting an external tmpfile. This should also reduce attack surface as the resulting files are private to its specific sandbox.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 03:13:15 +09:00
cat ea8d1c07df priv/shim: move /sbin/init setup to app
Test / Create distribution (push) Successful in 49s
Test / Run NixOS test (push) Successful in 3m36s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 03:06:10 +09:00
cat a0062d8275 fmsg: resume on exit
Test / Create distribution (push) Successful in 47s
Test / Run NixOS test (push) Successful in 3m32s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 02:22:09 +09:00
cat 43d2e4f5d7 nix: sway increase resolution
Test / Create distribution (push) Successful in 50s
Test / Run NixOS test (push) Successful in 3m46s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 02:21:24 +09:00
cat be7d944b39 helper/bwrap: PositionalArg implement fmt.Stringer
Test / Create distribution (push) Successful in 49s
Test / Run NixOS test (push) Successful in 3m28s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-15 00:11:48 +09:00
cat ace97952cc helper/bwrap: merge Args and FDArgs
Test / Create distribution (push) Successful in 1m13s
Test / Run NixOS test (push) Successful in 4m34s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-14 18:13:06 +09:00
cat 73146ea7fa dbus: remove BwrapStatic method
Test / Create distribution (push) Successful in 54s
Test / Run NixOS test (push) Successful in 8m20s
This method does not do anything and is not called from anywhere. It also does not make any sense as a public interface since the argument builder is no longer stateless.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-14 18:09:59 +09:00
cat 88040504b2 helper/bwrap: remove fmsg import
Test / Create distribution (push) Successful in 57s
Test / Run NixOS test (push) Successful in 8m13s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-14 18:05:00 +09:00
cat 1fd571d561 cmd/fsu: check parse behaviour
Test / Run NixOS test (push) Successful in 10m33s
Test / Create distribution (push) Successful in 1m8s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-14 16:43:55 +09:00
cat be30e2f11e cmd/fsu: revert offset in error message
Test / Create distribution (push) Successful in 51s
Test / Run NixOS test (push) Successful in 3m39s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-14 15:31:39 +09:00
cat aaebb8f3ab fortify: check print behaviour
Test / Create distribution (push) Successful in 1m10s
Test / Run NixOS test (push) Successful in 3m59s
These output are supposed to be deterministic, so checking them is a good way to catch regressions.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-14 14:44:28 +09:00
cat 1f74b636d3 state/join: use Join method when available
Test / Create distribution (push) Successful in 1m4s
Test / Run NixOS test (push) Successful in 4m11s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-14 14:11:02 +09:00
cat e431ab3c24 app: check username length against LOGIN_NAME_MAX
Test / Create distribution (push) Successful in 49s
Test / Run NixOS test (push) Successful in 3m46s
This limit is arbitrary, but it's good to enforce it anyway.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-14 12:44:55 +09:00
cat 3fba33687b fortify: print line after ps output
Test / Create distribution (push) Successful in 1m43s
Test / Run NixOS test (push) Successful in 9m18s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-14 12:23:20 +09:00
cat 820f48ef94 release: 0.2.13
Test / Create distribution (push) Successful in 1m56s
Release / Create release (push) Successful in 2m9s
Test / Run NixOS test (push) Successful in 4m38s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-13 23:45:54 +09:00
cat fe7d208cf7 helper: use generic extra files interface
Test / Create distribution (push) Successful in 1m38s
Test / Run NixOS test (push) Successful in 4m36s
This replaces the pipes object and integrates context into helper process lifecycle.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-13 23:34:15 +09:00
cat 60c2873750 helper/proc: cancel ec on parent ctx
Test / Create distribution (push) Successful in 1m31s
Test / Run NixOS test (push) Successful in 4m13s
This allows errors written during a timeout to be received and handled.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-13 23:08:28 +09:00
cat d1d20c06fb helper/seccomp: use sync.Once for closeWrite
Test / Create distribution (push) Successful in 1m29s
Test / Run NixOS test (push) Successful in 4m13s
This makes the code much cleaner, and eliminates the intermittent ErrInvalid errors.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-13 22:49:16 +09:00
cat 1e6a059668 helper/seccomp: benchmark exporter
Test / Create distribution (push) Successful in 1m44s
Test / Run NixOS test (push) Successful in 4m32s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-13 22:37:51 +09:00
cat 318df0f7e1 nix: test syscall filter
Test / Create distribution (push) Successful in 1m30s
Test / Run NixOS test (push) Successful in 4m17s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-13 22:01:16 +09:00
cat 58eb8f971d proc/pipe: implement args and stat file
Test / Create distribution (push) Successful in 1m30s
Test / Run NixOS test (push) Successful in 4m11s
This is a generic implementation of helper/pipe.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-13 19:57:24 +09:00
cat 0a1d7c01cd helper/proc: count dispatched errs
Test / Create distribution (push) Successful in 1m28s
Test / Run NixOS test (push) Successful in 3m59s
This helps debug implementation errors of [proc.File].

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-13 19:55:37 +09:00
cat 60ca1c6c55 helper/proc: store file addresses in linked list
Test / Create distribution (push) Successful in 1m28s
Test / Run NixOS test (push) Successful in 4m5s
Storing extra files as a slice requires the caller to allocate a large enough slice before initialising any file and never grow the slice.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-13 17:42:12 +09:00
cat 099da78af5 helper/seccomp: eliminate data race on pfd
Test / Create distribution (push) Successful in 2m10s
Test / Run NixOS test (push) Successful in 4m50s
Turns out the doc comment on os.File was lying about its methods being safe for concurrent use. The race detector picked up a data race from concurrent use of Fd and Close.

This change eliminates that by calling Fd in the prepare routine.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-13 10:40:51 +09:00
cat 18466cfd02 helper/proc: declare generic extra files interface
Test / Create distribution (push) Successful in 1m29s
Test / Run NixOS test (push) Successful in 4m4s
Helpers use extra files for various purposes. This provides a generic interface for implementing the fulfillment of these extra files without having to specifically handle them in the process creation code.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-11 16:34:47 +09:00
cat e14923ae53 helper/proc: move package out of internal
Test / Create distribution (push) Successful in 1m32s
Test / Run NixOS test (push) Successful in 4m6s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-08 13:03:45 +09:00
cat 7aff3ead3a nix: vm test remove unnecessary setup
Test / Create distribution (push) Successful in 1m27s
Test / Run NixOS test (push) Successful in 4m10s
This step is no longer required as the NixOS module is responsible for home directory creation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-07 22:29:56 +09:00
cat 72fb13dccc dbus: lock for read in public args interface
Test / Create distribution (push) Successful in 1m27s
Test / Run NixOS test (push) Successful in 4m2s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-07 13:42:29 +09:00
cat a48386bd56 system/dbus: dump messages on early fault
Test / Create distribution (push) Successful in 1m27s
Test / Run NixOS test (push) Successful in 4m14s
In the current app implementation this gets dumped in the wait method after resuming output. Wait is never called in an early fault condition, so any error messages get lost.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-07 13:20:56 +09:00
cat 2e52191404 system/dbus: dump method prints msgbuf
Test / Create distribution (push) Successful in 1m27s
Test / Run NixOS test (push) Successful in 4m1s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-07 13:16:54 +09:00
cat 568d7758d5 helper/seccomp: panic on invalid closeWrite use
Test / Create distribution (push) Successful in 1m46s
Test / Run NixOS test (push) Successful in 4m39s
Returning an error here puts exporter in an invalid state. The caller should guard against this condition instead.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-07 12:58:20 +09:00
cat 5b7b3fa9a4 helper/seccomp: implement reader interface via pipe
Test / Create distribution (push) Successful in 1m6s
Test / Run NixOS test (push) Successful in 2m44s
This also does not require the libc tmpfile call.

BPF programs emitted by libseccomp seems to be deterministic. The tests would catch regressions as it verifies the program against known good output backed by manual testing.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-03 19:43:03 +09:00
cat d58fb8c6ee workflows: fix nix store cache
Test / Create distribution (push) Successful in 1m13s
Test / Run NixOS test (push) Successful in 3m0s
Prefix does not seem to match correctly, this appears to be a Gitea implementation bug.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-01 21:16:13 +09:00
cat 5808fe61c3 nix: vm test set sway background
Test / Create distribution (push) Successful in 2m36s
Test / Run NixOS test (push) Successful in 6m32s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-25 22:28:04 +09:00
cat f338d3bb4b nix: update flake lock
Test / Create distribution (push) Successful in 3m6s
Test / Run NixOS test (push) Successful in 6m32s
2025-01-25 19:46:33 +09:00
cat 8d04dd72f1 nix: mount nvidia devices
Test / Create distribution (push) Successful in 1m43s
Test / Run NixOS test (push) Successful in 3m33s
These non-standard paths are required in the sandbox for nvidia drivers to work.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-25 18:05:18 +09:00
cat 21735a8abe release: 0.2.12
Test / Create distribution (push) Successful in 2m25s
Release / Create release (push) Successful in 4m6s
Test / Run NixOS test (push) Successful in 4m49s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-25 13:40:48 +09:00
cat 34272672b1 nix: verify silent output when not running with -v
Test / Create distribution (push) Successful in 1m51s
Test / Run NixOS test (push) Successful in 4m40s
This checks behaviour of fmsg and seccomp.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-25 13:38:18 +09:00
cat 7b96cd6ded helper/seccomp: do not call F_println if not verbose
Test / Create distribution (push) Successful in 1m42s
Test / Run NixOS test (push) Successful in 3m34s
This (slightly) improves performance.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-25 13:19:38 +09:00
cat 163f15e93f helper/seccomp: separate seccomp package
Test / Create distribution (push) Successful in 1m39s
Test / Run NixOS test (push) Successful in 3m31s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-25 12:59:11 +09:00
cat 016da20443 nix: expose compat flag in nixos module
Test / Create distribution (push) Successful in 1m55s
Test / Run NixOS test (push) Successful in 4m6s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-25 12:42:48 +09:00
cat 37780456a7 helper: block more unusual/privileged syscalls
Test / Create distribution (push) Successful in 1m44s
Test / Run NixOS test (push) Successful in 3m35s
These are toggled by F_EXT and exposed as SyscallPolicy.Compat in the Go interface.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-25 12:35:47 +09:00
cat efacaa40fa nix: set deny_devel correctly
Test / Create distribution (push) Successful in 1m55s
Test / Run NixOS test (push) Successful in 3m51s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-24 00:50:35 +09:00
cat ad6d0ee55f workflows: rename integration test artifact
Test / Create distribution (push) Successful in 1m53s
Test / Run NixOS test (push) Successful in 3m45s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-24 00:30:39 +09:00
cat cf791469d8 workflows: gc store and purge old caches
Test / Create distribution (push) Successful in 1m39s
Test / Run NixOS test (push) Successful in 3m32s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-24 00:25:57 +09:00
cat be14421775 workflows: merge test build job into test
Test / Create distribution (push) Successful in 2m8s
Test / Run NixOS test (push) Successful in 3m57s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-24 00:22:44 +09:00
cat 045983d7f4 wl: separate inline C
Build / Create distribution (push) Successful in 1m41s
Test / Run NixOS test (push) Successful in 3m29s
Having a huge blurb of inline C hurts readability on web pages and some text editors.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-23 22:06:29 +09:00
cat 7106b00968 release: 0.2.11
Build / Create distribution (push) Successful in 3m51s
Release / Create release (push) Successful in 4m12s
Test / Run NixOS test (push) Successful in 6m17s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-23 20:49:49 +09:00
cat 96d5d8a396 nix: apply shared home config to reserved aid
Build / Create distribution (push) Successful in 2m16s
Test / Run NixOS test (push) Successful in 5m43s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-23 20:48:04 +09:00
cat 8a00a83c71 nix: expose syscall filter policy
Build / Create distribution (push) Successful in 1m31s
Test / Run NixOS test (push) Successful in 1m52s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-23 17:24:42 +09:00
cat 134247b57d nix: configure target users via nixos
Build / Create distribution (push) Successful in 2m0s
Test / Run NixOS test (push) Successful in 3m46s
This makes patching home-manager no longer necessary.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-23 17:04:19 +09:00
cat b5bb7654da nix: redirect sway output to journal
Build / Create distribution (push) Successful in 2m8s
Test / Run NixOS test (push) Successful in 3m58s
This makes swaymsg exec output appear in test output.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-23 16:08:22 +09:00
cat cc1efa22e2 fst: add missing fields to template
Build / Create distribution (push) Successful in 1m28s
Test / Run NixOS test (push) Successful in 3m43s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-22 12:09:25 +09:00
cat 580128922b cmd/fpkg: expose syscall policy options
Build / Create distribution (push) Successful in 1m34s
Test / Run NixOS test (push) Successful in 3m44s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-22 12:01:30 +09:00
cat 23e1152baa app/share: clean BaseError message
Build / Create distribution (push) Successful in 1m35s
Test / Run NixOS test (push) Successful in 3m42s
This removes trailing '\n' in the PulseAudio warning.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-22 11:54:16 +09:00
cat 8c51012ef5 dbus: enable syscall filter
Build / Create distribution (push) Successful in 1m33s
Test / Run NixOS test (push) Successful in 3m42s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-22 11:49:23 +09:00
cat 5a64cdaf4f ldd: enable syscall filter
Build / Create distribution (push) Successful in 1m55s
Test / Run NixOS test (push) Successful in 4m6s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-22 02:00:49 +09:00
cat a30f5e1226 fortify: set up seccomp verbose logging early
Build / Create distribution (push) Successful in 1m34s
Test / Run NixOS test (push) Successful in 4m4s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-22 01:58:54 +09:00
cat 9a239fa1a5 helper/bwrap: integrate seccomp into helper interface
Build / Create distribution (push) Successful in 1m36s
Test / Run NixOS test (push) Successful in 3m40s
This makes API usage much cleaner, and encapsulates all bwrap arguments in argsWt.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-22 01:52:57 +09:00
cat 82029948e6 proc: append to ExtraFiles slice pointer
Build / Create distribution (push) Successful in 1m30s
Test / Run NixOS test (push) Successful in 4m4s
This is useful for initialising extra files before command.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-21 12:51:39 +09:00
cat dfcdc5ce20 state: store config in separate gob stream
Build / Create distribution (push) Successful in 1m37s
Test / Run NixOS test (push) Successful in 3m38s
This enables early serialisation of config.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-21 12:10:58 +09:00
cat fa0616b274 fortify: print permissive defaults warning early
Build / Create distribution (push) Successful in 1m47s
Test / Run NixOS test (push) Successful in 4m1s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-21 12:05:31 +09:00
cat 20a3d4c458 proc/priv/shim: resolve and load seccomp rules
Build / Create distribution (push) Successful in 1m33s
Test / Run NixOS test (push) Successful in 3m36s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-20 23:52:56 +09:00
cat 3df344828f proc/priv/shim: seccomp bpf filter via libseccomp
Build / Create distribution (push) Successful in 1m59s
Test / Run NixOS test (push) Successful in 4m11s
Rulesets adapted from Flatpak for compatibility.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-20 23:39:47 +09:00
cat 27f5922d5c fst: include syscall filter configuration
Build / Create distribution (push) Successful in 3m0s
Test / Run NixOS test (push) Successful in 5m19s
This value is passed through to shim.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-20 21:12:39 +09:00
cat 2cf1f46ea2 nix: test show without --short
Build / Create distribution (push) Successful in 3m36s
Test / Run NixOS test (push) Successful in 6m45s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-20 21:10:24 +09:00
cat 3c55fc8e86 proc/priv/shim: do not log bwrap args
Build / Create distribution (push) Successful in 1m22s
Test / Run NixOS test (push) Successful in 3m30s
This message is very long and does not serve much real purpose. Remove it to de-clutter verbose messages.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-20 19:51:28 +09:00
cat eb0ef2d115 helper/bwrap: generic extra file interface
Build / Create distribution (push) Successful in 1m32s
Test / Run NixOS test (push) Successful in 3m50s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-20 00:20:04 +09:00
cat 2f70506865 helper/bwrap: move sync to helper state
Build / Create distribution (push) Successful in 1m25s
Test / Run NixOS test (push) Successful in 3m33s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-19 18:38:13 +09:00
cat cae567c109 proc/priv/shim: remove unnecessary state
Build / Create distribution (push) Successful in 1m27s
Test / Run NixOS test (push) Successful in 3m37s
These values are only used during process creation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-19 18:09:07 +09:00
cat 1ec901f79e release: 0.2.10
Build / Create distribution (push) Successful in 1m32s
Test / Run NixOS test (push) Successful in 3m39s
Release / Create release (push) Successful in 1m30s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-18 22:50:08 +09:00
cat 715addaccd helper/bwrap: append --sync-fd before --
Build / Create distribution (push) Successful in 1m26s
Test / Run NixOS test (push) Successful in 3m26s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-18 12:30:03 +09:00
cat b31d055e20 proc/priv/init: early init check
Build / Create distribution (push) Successful in 1m39s
Test / Run NixOS test (push) Successful in 3m45s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-18 12:33:33 +09:00
cat 7baca66a56 proc: remove duplicate compile-time fortify reference
Build / Create distribution (push) Successful in 1m46s
Test / Run NixOS test (push) Successful in 3m44s
This is no longer needed since shim and init are now part of the main program.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-18 11:59:33 +09:00
cat 27d2914286 proc/priv/init: merge init into main program
Build / Create distribution (push) Successful in 1m47s
Test / Run NixOS test (push) Successful in 3m46s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-18 11:47:01 +09:00
cat ea8f228af3 proc/priv/shim: merge shim into main program
Build / Create distribution (push) Successful in 2m15s
Test / Run NixOS test (push) Successful in 2m53s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-17 23:43:32 +09:00
cat 16db3dabe2 internal: do PR_SET_PDEATHSIG once
Build / Create distribution (push) Successful in 3m7s
Test / Run NixOS test (push) Successful in 4m40s
This prctl affects the entire process, doing it on every OS thread is pointless.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-17 23:08:46 +09:00
cat c4de450217 nix: do not force static linking on nix
Build / Create distribution (push) Successful in 3m14s
Test / Run NixOS test (push) Successful in 3m25s
In a typical Nix or NixOS-based setup, the entire /nix/store directory is available to the sandbox.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-17 22:56:16 +09:00
cat b60c01f440 fortify: switch to static linking
Build / Create distribution (push) Successful in 1m43s
Test / Run NixOS test (push) Successful in 4m32s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-16 17:32:52 +09:00
cat 124743ffd3 app: expose single run method
Tests / Go tests (push) Successful in 1m1s
Nix / NixOS tests (push) Successful in 3m20s
App is no longer just a simple [exec.Cmd] wrapper, so exposing these steps separately no longer makes sense and actually hinders proper error handling, cleanup and cancellation. This change removes the five-second wait when the shim dies before receiving the payload, and provides caller the ability to gracefully stop execution of the confined process.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-15 23:39:51 +09:00
cat be4d8b6300 release: 0.2.9
Create distribution / Release (push) Successful in 1m21s
Tests / Go tests (push) Successful in 46s
Nix / NixOS tests (push) Successful in 3m6s
This release mostly contains permissive defaults fixes and optimisations. It also contains a proof of concept version of fpkg.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-15 13:14:43 +09:00
cat 3e11ce6868 helper/bwrap: separate sequential/static args
Tests / Go tests (push) Successful in 41s
Nix / NixOS tests (push) Successful in 3m59s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-15 13:07:06 +09:00
cat 562f5ed797 fst: hide sockets exposed via Filesystem
Tests / Go tests (push) Successful in 40s
Nix / NixOS tests (push) Successful in 2m49s
This is mostly useful for permissive defaults.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-15 10:13:18 +09:00
cat db03565614 fst: move sandbox struct to separate file
Tests / Go tests (push) Successful in 1m0s
Nix / NixOS tests (push) Successful in 3m9s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-15 09:42:44 +09:00
cat 7d99e45b88 helper/bwrap: register OverlayConfig with gob
Tests / Go tests (push) Successful in 58s
Nix / NixOS tests (push) Successful in 3m5s
This is required for copying bwrap configurations across processes.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-14 12:25:10 +09:00
cat 1651eb06df dbus: implement dbus_parse_address
Tests / Go tests (push) Successful in 1m14s
Nix / NixOS tests (push) Successful in 7m36s
This parses D-Bus addresses according to spec. It does significantly fewer copies than dbus_parse_address.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-12 23:24:03 +09:00
cat ac543a1ce8 dbus: rename makeTestCases
Tests / Go tests (push) Successful in 2m36s
Nix / NixOS tests (push) Successful in 10m5s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-12 23:21:28 +09:00
cat e2489059c1 helper/bwrap: implement overlayfs builder
Tests / Go tests (push) Successful in 33s
Nix / NixOS tests (push) Successful in 4m5s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-05 20:09:35 +09:00
cat 2e3f6a4c51 helper/bwrap: move test out of bwrap package
Tests / Go tests (push) Successful in 36s
Nix / NixOS tests (push) Successful in 4m51s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-05 19:45:24 +09:00
cat 2162029f46 helper/bwrap: add json struct tag to filesystem
Tests / Go tests (push) Successful in 38s
Nix / NixOS tests (push) Successful in 4m43s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-05 19:41:04 +09:00
cat a1148edd00 fst/config: allocate filesystem slice
Tests / Go tests (push) Successful in 32s
Nix / NixOS tests (push) Successful in 4m5s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-04 00:16:41 +09:00
cat 6acd0d4e88 linux/std: handle fsu exit status 1
Tests / Go tests (push) Successful in 34s
Nix / NixOS tests (push) Successful in 2m27s
Printing "exit status 1" is confusing. This handles the ExitError and returns EACCES instead.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-01 21:34:57 +09:00
cat 35b7142317 fortify: show system info when instance is not specified
Tests / Go tests (push) Successful in 38s
Nix / NixOS tests (push) Successful in 4m32s
This contains useful information not obtainable by external tools.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-01-01 19:35:50 +09:00
cat c4d6651cae update reverse-DNS style identifiers
Tests / Go tests (push) Successful in 1m6s
Nix / NixOS tests (push) Successful in 4m11s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-31 16:16:38 +09:00
cat 22a4b99674 cmd/fpkg/install: deduplicate nix store
Tests / Go tests (push) Successful in 41s
Nix / NixOS tests (push) Successful in 4m43s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-30 02:25:04 +09:00
cat 1464ef774b cmd/fpkg: expose nixGL wrappers
Tests / Go tests (push) Successful in 35s
Nix / NixOS tests (push) Successful in 4m6s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-30 02:02:20 +09:00
cat 66ba4cea5c cmd/fpkg: remove workDir acl from activation
Tests / Go tests (push) Successful in 33s
Nix / NixOS tests (push) Successful in 3m56s
Activation does not require access to workDir, and by this point all information is available in dataHome.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-29 23:48:45 +09:00
cat f8d0786509 cmd/fpkg: include nixGL source in inner store
Tests / Go tests (push) Successful in 34s
Nix / NixOS tests (push) Successful in 4m24s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-29 23:37:11 +09:00
cat 56a73bb019 nix: create nixpkgs symlink
Tests / Go tests (push) Successful in 34s
Nix / NixOS tests (push) Successful in 4m25s
This is included as part of the system as nixGL needs to be built somewhere between activation and start.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-29 23:23:11 +09:00
cat fb8abf63db nix: update flake lock
Tests / Go tests (push) Successful in 40s
Nix / NixOS tests (push) Successful in 4m15s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-29 23:14:16 +09:00
cat 63802c5f0d nix: nixos test create parent directory
Tests / Go tests (push) Successful in 37s
Nix / NixOS tests (push) Successful in 4m9s
This tests directory creation in shim.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-29 22:36:53 +09:00
cat aff80b6b00 cmd/fpkg: optional network access when invoking with nix daemon
Tests / Go tests (push) Successful in 34s
Nix / NixOS tests (push) Successful in 3m36s
This is useful for building nixGL.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-29 18:32:44 +09:00
cat a98a176907 cmd/fpkg: bind and document more gpu devices
Tests / Go tests (push) Successful in 34s
Nix / NixOS tests (push) Successful in 3m40s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-29 18:25:26 +09:00
cat 5302879b88 cmd/fpkg: improve readability of fortify invocations
Tests / Go tests (push) Successful in 34s
Nix / NixOS tests (push) Successful in 3m41s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-29 17:55:56 +09:00
cat 891b3cbde7 cmd/fpkg: compare all three store paths
Tests / Go tests (push) Successful in 34s
Nix / NixOS tests (push) Successful in 3m39s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-29 17:10:41 +09:00
cat c795293f36 cmd/fpkg: clean up broken links before activation
Tests / Go tests (push) Successful in 35s
Nix / NixOS tests (push) Successful in 3m38s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-29 15:21:40 +09:00
cat 42e1043300 nix: set home-manager user information
Tests / Go tests (push) Successful in 33s
Nix / NixOS tests (push) Successful in 2m36s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-29 15:11:36 +09:00
cat 5416b07daa nix: remove unused argument 'self'
Tests / Go tests (push) Successful in 34s
Nix / NixOS tests (push) Successful in 2m36s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-29 14:49:55 +09:00
cat e57a0e9bf2 nix: rename fortifyBundle to buildPackage
Tests / Go tests (push) Successful in 34s
Nix / NixOS tests (push) Successful in 2m35s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-29 14:35:37 +09:00
cat ab48706ebe dist: install fpkg to /usr/bin
Tests / Go tests (push) Successful in 36s
Nix / NixOS tests (push) Successful in 2m25s
This is a high level user-facing tool.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-29 01:04:53 +09:00
cat c1a459a0b1 cmd/fpkg/start: correct drop to shell wording
Tests / Go tests (push) Successful in 52s
Nix / NixOS tests (push) Successful in 4m27s
Activation no longer happens during application startup.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-29 00:56:14 +09:00
cat 5125e96ecf nix: generate application package build script
Tests / Go tests (push) Successful in 55s
Nix / NixOS tests (push) Successful in 4m24s
This takes some metadata, sandbox options, a launch script and a list of home-manager modules. The result needs to be executed in an environment with nix daemon access, and it produces the final package file.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-29 00:42:21 +09:00
cat e0e2f40e84 cmd/fpkg: app bundle helper
Tests / Go tests (push) Successful in 43s
Nix / NixOS tests (push) Successful in 4m25s
This helper program creates fortify configuration for running an application bundle. The activate action wraps a home-manager activation package and ensures each generation gets activated once.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-26 13:21:49 +09:00
cat bf8094c6ca internal: include path to fortify main program
Tests / Go tests (push) Successful in 36s
Nix / NixOS tests (push) Successful in 4m6s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-26 12:48:48 +09:00
cat 2e3bb1893e release: 0.2.8
Tests / Go tests (push) Successful in 42s
Create distribution / Release (push) Successful in 1m0s
Nix / NixOS tests (push) Successful in 3m53s
This release mostly fixes bugs uncovered when running fortify on a generic linux distribution.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-29 01:09:47 +09:00
cat 9b206072fa cmd/fshim: ensure data directory
Tests / Go tests (push) Successful in 36s
Nix / NixOS tests (push) Successful in 3m33s
Ensuring home directory in shim causes the directory to be owned by the target user.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-28 14:39:01 +09:00
cat b9e2003d5b app: ensure extra paths
Tests / Go tests (push) Successful in 36s
Nix / NixOS tests (push) Successful in 3m37s
The primary use case for extra perms is app-specific state directories, which may or may not exist (first run of any app).

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-28 14:07:49 +09:00
cat 66ec0d882f dist: build with -trimpath
Tests / Go tests (push) Successful in 35s
Nix / NixOS tests (push) Successful in 3m26s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-28 13:44:05 +09:00
cat 847b667489 app: extra acl entries from configuration
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-28 13:23:27 +09:00
cat c70f0612ad fortify/print: skip nil filesystem entries
Tests / Go tests (push) Successful in 31s
Nix / NixOS tests (push) Successful in 3m24s
This fixes a panic when displaying configurations with nil filesystem entries.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-28 12:14:42 +09:00
cat 85e5b097fd fst/config: add template etc entry
Tests / Go tests (push) Successful in 31s
Nix / NixOS tests (push) Successful in 3m21s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-28 12:05:32 +09:00
cat 0107620d8c app: merge share methods
Tests / Go tests (push) Successful in 32s
Nix / NixOS tests (push) Successful in 3m25s
This significantly increases readability and makes order of ops more obvious.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-28 11:12:35 +09:00
cat fc26659ea1 fst/config: autoetc read custom path
Tests / Go tests (push) Successful in 43s
Nix / NixOS tests (push) Successful in 3m40s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-27 18:57:44 +09:00
cat 1f173a469c system/dbus: fix inverted system bus state
Tests / Go tests (push) Successful in 33s
Nix / NixOS tests (push) Successful in 3m38s
Debug message and socket cleanup gets missed due to this value being inverted.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-27 18:38:11 +09:00
cat 2fdbd6a4dd fst/config: alternative /etc directory
Tests / Go tests (push) Successful in 32s
Nix / NixOS tests (push) Successful in 3m41s
This is useful for static /etc directories provided by self-contained application packages, or in cases where autoetc is useful for paths other than /etc.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-27 18:06:26 +09:00
cat aef847b5ae helper/bwrap: fix typo in --dir config builder
Tests / Go tests (push) Successful in 32s
Nix / NixOS tests (push) Successful in 3m33s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-27 15:34:43 +09:00
cat 0a2aa5823b cmd/fshim: bind finit inside sandbox
Tests / Go tests (push) Successful in 34s
Nix / NixOS tests (push) Successful in 3m32s
The outer finit executable is normally inaccessible inside the sandbox. This was obscured by the current Nix-based setup exposing /nix/store to the sandbox.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-27 14:44:57 +09:00
cat b956ce4052 ldd: trim leading and trailing white spaces from name
Tests / Go tests (push) Successful in 33s
Nix / NixOS tests (push) Successful in 3m31s
Glibc emits ldd output with \t prefix for formatting. Remove that here.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-26 16:53:01 +09:00
cat dc579dc610 dbus/run: bind ldd entry absolute name
Tests / Go tests (push) Successful in 32s
Nix / NixOS tests (push) Successful in 3m35s
The ld.so entry has an absolute name. They are usually symlinks so binding path does not guarantee ld.so availability under its expected path in the mount namespace.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-26 16:36:03 +09:00
cat ade57c39af ldd: add fhs glibc test case
Tests / Go tests (push) Successful in 33s
Nix / NixOS tests (push) Successful in 3m34s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-26 16:33:02 +09:00
cat 614ad86a5b dbus: fail on LookPath error
Tests / Go tests (push) Successful in 35s
Nix / NixOS tests (push) Successful in 3m24s
An absolute path to xdg-dbus-proxy is required.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-26 16:08:48 +09:00
cat 831dc6a181 dist: create checksum in dist directory
Tests / Go tests (push) Successful in 35s
Nix / NixOS tests (push) Successful in 3m38s
This makes verification easier.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-26 15:14:35 +09:00
cat c67b8ab9ac fst/config: improve correctness of comments
Tests / Go tests (push) Successful in 33s
Nix / NixOS tests (push) Successful in 3m26s
The meanings of many of these fields have changed since they were added.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-26 00:45:29 +09:00
cat 7c5aaa38e2 dist: include zsh completion
Tests / Go tests (push) Successful in 33s
Nix / NixOS tests (push) Successful in 3m26s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-25 23:41:54 +09:00
cat b52b1a5f90 dist/install: do not replace existing fsurc
Tests / Go tests (push) Successful in 38s
Nix / NixOS tests (push) Successful in 3m28s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-25 23:37:15 +09:00
cat 9fc82d67b7 fortify/parse: accept config stream fd
Tests / Go tests (push) Successful in 36s
Nix / NixOS tests (push) Successful in 3m29s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-23 20:09:07 +09:00
cat 70bffeaa1e fortify: clean up config loading
Tests / Go tests (push) Successful in 40s
Nix / NixOS tests (push) Successful in 3m28s
Move duplicate code to function. Also handle - as config from stdin.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-23 17:57:54 +09:00
cat c109ac2653 release: 0.2.7
Tests / Go tests (push) Successful in 47s
Create distribution / Release (push) Successful in 1m5s
Nix / NixOS tests (push) Successful in 4m40s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-22 13:34:50 +09:00
cat 58f8731b2e nix: include fortify show output
Tests / Go tests (push) Successful in 35s
Nix / NixOS tests (push) Successful in 3m40s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-22 13:28:21 +09:00
cat 8a9ba5e0ad fortify: show short mode omit filesystems
Tests / Go tests (push) Successful in 36s
Nix / NixOS tests (push) Successful in 3m19s
Filesystem information can be quite noisy in permissive defaults.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-22 13:20:33 +09:00
cat f608f28a6a app: mount /dev/kvm in permissive defaults
Tests / Go tests (push) Successful in 35s
Nix / NixOS tests (push) Successful in 3m21s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-22 12:37:24 +09:00
cat aecfae1874 fortify: sort by time of start
Tests / Go tests (push) Successful in 35s
Nix / NixOS tests (push) Successful in 3m14s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-22 12:06:54 +09:00
cat 27f2b53d18 fortify: sort ps output
Tests / Go tests (push) Successful in 37s
Nix / NixOS tests (push) Successful in 3m20s
This ensures consistency between runs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-22 11:59:53 +09:00
cat 5838963265 nix: test dbus via notify-send
Tests / Go tests (push) Successful in 1m28s
Nix / NixOS tests (push) Successful in 4m0s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-22 11:31:12 +09:00
cat e8594cf670 fortify: print short instance id in non-json short mode
Tests / Go tests (push) Successful in 1m23s
Nix / NixOS tests (push) Successful in 3m28s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-22 11:02:19 +09:00
cat 5c73acb56f release: 0.2.6
Tests / Go tests (push) Successful in 48s
Create distribution / Release (push) Successful in 1m12s
Nix / NixOS tests (push) Successful in 3m59s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-22 01:18:21 +09:00
cat 76ca2a92ee nix: check state store contents
Tests / Go tests (push) Successful in 39s
Nix / NixOS tests (push) Successful in 3m27s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-22 01:10:48 +09:00
cat f2869c4235 fortify: serialise ps with string as key
Tests / Go tests (push) Successful in 40s
Nix / NixOS tests (push) Successful in 2m59s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-21 23:52:48 +09:00
cat bf11241649 fortify: zsh complete show instance list
Tests / Go tests (push) Successful in 37s
Nix / NixOS tests (push) Successful in 2m59s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-21 21:13:53 +09:00
cat cb98baa19d fortify: clean up ps formatting code
Tests / Go tests (push) Successful in 38s
Nix / NixOS tests (push) Successful in 3m1s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-21 20:34:40 +09:00
cat 4f4c690d38 fortify: move json indent call
Tests / Go tests (push) Successful in 36s
Nix / NixOS tests (push) Successful in 2m59s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-21 19:06:25 +09:00
cat df7f692e61 fortify: move show formatting out of main
Tests / Go tests (push) Successful in 38s
Nix / NixOS tests (push) Successful in 3m1s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-21 18:33:30 +09:00
cat 7a8b625a57 app: rename /fortify to /.fortify
Tests / Go tests (push) Successful in 35s
Nix / NixOS tests (push) Successful in 2m57s
Also removed the inner share tmpfs mount.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-21 18:11:32 +09:00
cat 8bf12bbe68 nix: clear terminal prior to screenshot
Tests / Go tests (push) Successful in 35s
Nix / NixOS tests (push) Successful in 2m50s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-21 18:04:17 +09:00
cat f8c3d53327 nix: test pulseaudio pass through
Tests / Go tests (push) Successful in 39s
Nix / NixOS tests (push) Successful in 3m15s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-21 17:58:14 +09:00
cat 74fe74e6b5 app: do not fail on missing cookie
Tests / Go tests (push) Successful in 40s
Nix / NixOS tests (push) Successful in 2m55s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-21 17:56:21 +09:00
cat ed8ee5eb4b nix: filter nix files from src
Tests / Go tests (push) Successful in 38s
Nix / NixOS tests (push) Successful in 3m7s
This prevents constant rebuilds when debugging integration tests.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-21 17:39:42 +09:00
cat af4d92b785 nix: test dbus proxy
Tests / Go tests (push) Successful in 37s
Nix / NixOS tests (push) Successful in 3m1s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-21 14:19:50 +09:00
cat ce04dd52ca nix: background go test
Tests / Go tests (push) Successful in 38s
Nix / NixOS tests (push) Successful in 3m0s
Go test takes significant time.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-21 13:58:57 +09:00
cat 3d042f4992 nix: remove workspace switching
Tests / Go tests (push) Successful in 36s
Nix / NixOS tests (push) Successful in 3m17s
Switching workspaces does not test anything and introduces unnecessary wait time.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-21 13:52:05 +09:00
cat 68660a2ad4 fortify: config/state pretty-print subcommand
Tests / Go tests (push) Successful in 43s
Nix / NixOS tests (push) Successful in 3m8s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-21 12:29:04 +09:00
cat b9cc318314 system: implement Enablements String method
Tests / Go tests (push) Successful in 40s
Nix / NixOS tests (push) Successful in 3m9s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-20 23:21:19 +09:00
cat ed10574dea state: store join util
Tests / Go tests (push) Successful in 39s
Nix / NixOS tests (push) Successful in 3m5s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2024-12-20 19:05:39 +09:00
cat 195b717e01 release: 0.2.5
Tests / Go tests (push) Successful in 49s
Create distribution / Release (push) Successful in 1m6s
Nix / NixOS tests (push) Successful in 1m23s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-20 00:28:48 +09:00
cat df6fc298f6 migrate to git.gensokyo.uk/security/fortify
Tests / Go tests (push) Successful in 2m55s
Nix / NixOS tests (push) Successful in 5m10s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-20 00:20:02 +09:00
cat eae3034260 state: expose aids and use instance id as key
Tests / Go tests (push) Successful in 39s
Nix / NixOS tests (push) Successful in 3m26s
Fortify state store instances was specific to aids due to outdated design decisions carried over from the ego rewrite. That no longer makes sense in the current application, so the interface now enables a single store object to manage all transient state.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-19 21:36:17 +09:00
cat 5ea7333431 fst: implement app id parser
Tests / Go tests (push) Successful in 40s
Nix / NixOS tests (push) Successful in 3m8s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-19 18:19:47 +09:00
cat f796622c35 state: rename simple store implementation
Tests / Go tests (push) Successful in 42s
Nix / NixOS tests (push) Successful in 3m4s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-19 11:48:48 +09:00
cat 5d25bee786 fortify: remove systemd check
Tests / Go tests (push) Successful in 38s
Nix / NixOS tests (push) Successful in 3m3s
This is no longer necessary as fortify no longer integrates with external user switchers.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-19 11:14:31 +09:00
cat b48ece3bb0 acl: use test-managed tmpdir
Tests / Go tests (push) Successful in 44s
Nix / NixOS tests (push) Successful in 3m7s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-19 11:08:13 +09:00
cat 9f95f60400 release: 0.2.4
Tests / Go tests (push) Successful in 52s
Create distribution / Release (push) Successful in 1m9s
Nix / NixOS tests (push) Successful in 1m23s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-18 23:52:52 +09:00
cat 90dd57f75d workflows: cache nix store
Tests / Go tests (push) Successful in 45s
Nix / NixOS tests (push) Successful in 1m11s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-18 23:38:39 +09:00
cat 141f2e3685 workflows: cache apt packages
Tests / Go tests (push) Successful in 36s
Nix / NixOS tests (push) Successful in 5m43s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-18 23:05:28 +09:00
cat 73aa285e8f workflows: upload nixos test output
Tests / Go tests (push) Successful in 44s
Nix / NixOS tests (push) Successful in 5m45s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-18 20:32:40 +09:00
cat 6e87fc02dd workflows: build and upload test distribution
Tests / Go tests (push) Successful in 43s
Nix / NixOS tests (push) Successful in 5m33s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-18 20:28:35 +09:00
cat 52f21a19f3 cmd/fshim: switch to setup pipe
Tests / Go tests (push) Successful in 38s
Nix / NixOS tests (push) Successful in 5m43s
The socket-based approach is no longer necessary as fsu allows extra files and sudo compatibility is no longer relevant.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-18 19:39:25 +09:00
cat 7be53a2438 cmd/fshim: switch to generic setup func
Tests / Go tests (push) Successful in 38s
Nix / NixOS tests (push) Successful in 5m47s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-18 17:20:31 +09:00
cat 7f29b37a32 proc: setup payload send
Generic setup payload encoder adapted from fshim.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-18 17:20:01 +09:00
cat f69e8e753e cmd/finit: switch to generic receive func
Tests / Go tests (push) Successful in 38s
Nix / NixOS tests (push) Successful in 5m40s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-18 16:49:19 +09:00
cat ef8fd37e9d proc: setup payload receive
Generic implementation of setup payload receiver adapted from finit.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-18 16:48:41 +09:00
cat 2f676c9d6e fst: rename from fipc
Tests / Go tests (push) Successful in 38s
Nix / NixOS tests (push) Successful in 5m48s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-18 15:50:46 +09:00
cat bbace8f84b nix: increase cpu count
Tests / Go tests (push) Successful in 38s
Nix / NixOS tests (push) Successful in 5m41s
This improves performance, especially when kvm is inaccessible.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-18 15:32:52 +09:00
cat 2efedf56c0 nix: collect fortify ps output
Tests / Go tests (push) Successful in 38s
Nix / NixOS tests (push) Successful in 10m38s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-18 13:48:39 +09:00
cat b752ec4468 fipc: export config struct
Tests / Go tests (push) Successful in 1m12s
Nix / NixOS tests (push) Successful in 10m51s
Also store full config as part of state.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-18 13:45:55 +09:00
cat 5d00805a7c nix: check acl rollback
Tests / Go tests (push) Successful in 1m1s
Nix / NixOS tests (push) Successful in 10m32s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-18 12:49:32 +09:00
cat 7b6052a473 nix: run Go tests in nixos
Tests / Go tests (push) Successful in 41s
Nix / NixOS tests (push) Successful in 9m56s
Nix build environment does not support ACLs in any filesystem. This allows acl tests to run.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-17 21:16:55 +09:00
cat 38653c6ab5 release: 0.2.3
Tests / Go tests (push) Successful in 55s
Create distribution / Release (push) Successful in 1m1s
Nix / NixOS tests (push) Successful in 5m5s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-17 14:06:17 +09:00
cat b5cbbeab90 dist: generate distribution tarball
Tests / Go tests (push) Successful in 46s
Create distribution / Release (push) Successful in 49s
Nix / NixOS tests (push) Successful in 5m9s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-17 14:02:54 +09:00
cat c3ba0c3cce nix: rename nixos test
Tests / Go tests (push) Successful in 39s
Nix / NixOS tests (push) Successful in 5m0s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-17 13:02:12 +09:00
cat b453f70ca2 cmd/fsu: check uid range before syscall
Tests / Go tests (push) Successful in 43s
Nix / NixOS tests (push) Successful in 5m0s
This limits potential exploits to the fortify uid range.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-17 13:01:36 +09:00
cat c2b178e626 xcb: refactor and clean up
Tests / Go tests (push) Successful in 45s
Nix / NixOS tests (push) Successful in 5m2s
No clean way to write Go tests for this package. Will rely on NixOS tests for now.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-17 12:46:36 +09:00
cat aeda40fc92 nix: test x11 permissive defaults
Tests / Go tests (push) Successful in 40s
Nix / NixOS tests (push) Successful in 4m51s
Also invoke glinfo/wayland-info as part of tests.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-17 12:40:29 +09:00
cat 65dc39956f workflows: set action names
Tests / Go tests (push) Successful in 42s
Nix / NixOS tests (push) Successful in 4m33s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-17 11:12:39 +09:00
cat 35505c8a26 workflows: invoke nix flake checks
check / nix-flake-check (push) Successful in 4m32s
test / test (push) Successful in 37s
Integration tests are implemented as nix flake checks.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-17 10:49:43 +09:00
cat 3f993021f8 nix: permissive defaults nixos test
test / test (push) Successful in 37s
Adapted from nixos sway integration tests.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-16 22:56:10 +09:00
cat 4d3bd5338f nix: implement flake checks
test / test (push) Successful in 36s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-16 20:54:28 +09:00
cat 138666d753 nix: skip acl test
test / test (push) Successful in 39s
The nix build environment does not support ACLs.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-16 19:29:01 +09:00
cat f4628e181b acl: create test file in tmpdir
test / test (push) Successful in 37s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-16 18:58:09 +09:00
cat c8a90666c5 acl: refactor and clean up
test / test (push) Successful in 37s
Move all C code to c.go, switch to pkg-config, set up finalizer for acl.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-16 18:27:19 +09:00
cat ee41b37606 acl: add tests
test / test (push) Successful in 37s
These tests test UpdatePerm correctness by parsing getfacl output.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-16 16:00:31 +09:00
cat e3f1d7ba60 release: 0.2.2
release / release (push) Successful in 44s
test / test (push) Successful in 35s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-07 21:47:22 +09:00
cat 39e3ac3ccd nix: require /etc/userdb nix-daemon
test / test (push) Successful in 36s
There seems to be some kind of credential caching in nix-daemon.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-07 21:07:57 +09:00
cat 33c95b80ca cmd/fuserdb: rename home directories
test / test (push) Successful in 36s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-07 20:23:46 +09:00
cat 40cc8a68d1 nix: rename home directories
test / test (push) Successful in 38s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-07 20:15:37 +09:00
cat f773c92411 system: prevent duplicate Wayland op
test / test (push) Successful in 36s
Wayland is implemented as an Op to enforce dependency and cleanup, its implementation does not allow multiple instances on a single sys object, nor would doing that make any sense.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-07 19:45:37 +09:00
cat 16ab734fcd update README document
test / test (push) Successful in 37s
A lot of this information is no longer true since fsu. Remove them for now and write up proper documentation later.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-06 17:04:36 +09:00
cat cc816a1aaa proc: cleaner extra files
test / test (push) Successful in 37s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-06 16:05:04 +09:00
cat b3ef53b193 app: integrate security-context-v1
test / test (push) Successful in 37s
Should be able to get rid of XDG_RUNTIME_DIR share after this.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-06 04:25:33 +09:00
cat 8d0573405a helper/bwrap: implement sync fd
test / test (push) Successful in 38s
This is required by wayland security-context-v1.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-06 04:21:37 +09:00
cat 38e92edb8e system/wayland: integrate security-context-v1
test / test (push) Successful in 37s
Had to pass the sync fd through sys. The rest are just part of a standard Op.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-06 04:20:15 +09:00
cat 2d606b1f4b wl: implement security-context-v1
test / test (push) Successful in 38s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-06 04:15:13 +09:00
cat 1b5b089c78 fortify: rename --dbus-id to --id
test / test (push) Successful in 19s
This value is no longer specific to D-Bus defaults.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-06 03:26:09 +09:00
cat 6b8ddca7b4 nix: track nixos stable 24.11
test / test (push) Successful in 25s
Reduce rebuilds during development on my system.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-06 00:44:04 +09:00
cat 95668ac998 nix: expose no_new_session in module
test / test (push) Successful in 14s
Useful for shells and terminal programs like chat clients.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-28 00:19:06 +09:00
cat b291f0b710 app: add nixos-based config test case
test / test (push) Successful in 20s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-21 12:13:21 +09:00
cat 3a20b149ce update README document
test / test (push) Successful in 26s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-21 11:22:34 +09:00
cat 30b8bce90a fortify: zsh completion
test / test (push) Successful in 22s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-20 01:25:19 +09:00
cat de0d78daae release: 0.2.1
release / release (push) Successful in 1m4s
test / test (push) Successful in 20s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-19 21:03:50 +09:00
cat 6bf33ce507 fortify: use resolved username
test / test (push) Successful in 21s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-19 21:03:09 +09:00
cat 9faf3b3596 app: validate username
test / test (push) Successful in 23s
This value is used for passwd generation. Bad input can cause very confusing issues. This is not a security issue, however validation will improve user experience.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-19 21:01:41 +09:00
cat d99c8b1fb4 release: 0.2.0
release / release (push) Successful in 44s
test / test (push) Successful in 22s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-19 18:15:09 +09:00
cat 6e4870775f update README document
test / test (push) Successful in 20s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-19 18:14:06 +09:00
cat 0a546885e3 nix: update options doc
test / test (push) Successful in 22s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-19 18:12:35 +09:00
cat 653d69da0a nix: module descriptions
test / test (push) Successful in 24s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-19 18:10:57 +09:00
cat f8256137ae nix: separate module options from implementation
test / test (push) Successful in 25s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-19 17:08:22 +09:00
cat 54b47b0315 nix: copy pixmaps directory to share package
test / test (push) Successful in 21s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-18 14:46:08 +09:00
cat ae2628e57a cmd/fshim/ipc: install signal handler on shim start
test / test (push) Successful in 20s
Getting killed at this point will result in inconsistent state.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-18 13:33:46 +09:00
cat c026a4b5dc fortify: permissive defaults resolve home directory from os
test / test (push) Successful in 21s
When starting with the permissive defaults "run" command, attempt to resolve home directory from os by default and fall back to /var/empty.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-18 13:01:07 +09:00
cat 748a0ae2c8 nix: wrap program from libexec
test / test (push) Successful in 24s
This avoids renaming the fortify binary.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-18 12:58:47 +09:00
cat 8f3f0c7bbf nix: integrate dynamic users
test / test (push) Successful in 21s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-18 02:49:48 +09:00
cat 05b7dbf066 app: alternative inner home path
test / test (push) Successful in 24s
Support binding home to an alternative path in the mount namespace.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-18 00:18:21 +09:00
cat 866270ff05 fmsg: add to wg prior to enqueue
test / test (push) Successful in 27s
Adding after channel write is racy.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-17 23:50:02 +09:00
cat c1fad649e8 app/start: check for cleanup and abort condition
test / test (push) Successful in 21s
Dirty fix. Will rewrite after fsu integration complete.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-17 23:41:52 +09:00
cat b5f01ef20b app: append # for ChangeHosts message with numerical uid
test / test (push) Successful in 21s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-17 23:40:37 +09:00
cat 2e23cef7bb cmd/fuserdb: generate group entries
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-17 23:31:06 +09:00
cat 6a6d30af1f cmd/fuserdb: systemd userdb drop-in entries generator
test / test (push) Successful in 20s
This provides user records via nss-systemd. Static drop-in entries are generated to reduce complexity and attack surface.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-17 02:16:02 +09:00
cat df33123bd7 app: integrate fsu
test / test (push) Successful in 21s
This removes the dependency on external user switchers like sudo/machinectl and decouples fortify user ids from the passwd database.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-16 21:19:45 +09:00
cat 1a09b55bd4 nix: remove portal paths from default
test / test (push) Successful in 27s
Despite presenting itself as a generic desktop integration interface, xdg-desktop portal is highly flatpak-centric and only supports flatpak and snap in practice. It is a significant attack surface to begin with as it is a privileged process which accepts input from unprivileged processes, and the lack of support for anything other than fortify also introduces various information leaks when exposed to fortify as it treats fortified programs as unsandboxed, privileged programs in many cases.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-10 22:24:17 +09:00
cat 9a13b311ac app/config: rename map_real_uid from use_real_uid
test / test (push) Successful in 19s
This option only changes mapped uid in the user namespace.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-09 12:01:34 +09:00
cat 45fead18c3 cmd/fshim: set no_new_privs flag
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-09 11:50:56 +09:00
cat 431aa32291 nix: remove absolute Exec paths
test / test (push) Successful in 26s
Absolute paths set for Exec causes the program to be launched as the privileged user.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-08 02:05:47 +09:00
cat 3962705126 nix: keep fshim and finit names
test / test (push) Successful in 22s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-06 14:59:28 +09:00
cat ad80be721b nix: improve start script
test / test (push) Successful in 23s
Zsh store path in shebang. Replace writeShellScript with writeScript since runtimeShell is not overridable.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-06 14:09:41 +09:00
cat f831948bca release: 0.1.0
release / release (push) Successful in 28s
test / test (push) Successful in 21s
This release significantly changes the command line interface, and updates the NixOS module to finally produce meaningful sandbox configuration.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-06 04:37:43 +09:00
cat 2e31b3d3a1 update README document
test / test (push) Successful in 32s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-06 04:35:52 +09:00
cat 4d90e73366 nix: generate strict sandbox configuration
test / test (push) Successful in 22s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-06 04:25:15 +09:00
cat 3dfc1fcd56 app: support full /dev access
test / test (push) Successful in 22s
Also moved /dev/fortify to /fortify since it is impossible to create new directories in /dev from the init namespace and bind mounting its contents has undesirable side effects.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-06 03:49:39 +09:00
cat 89bafd0c22 fortify: root check before command handling
test / test (push) Successful in 22s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-05 12:57:03 +09:00
cat 861bb1274f fortify: override default usage function
test / test (push) Successful in 23s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-05 00:12:31 +09:00
cat 714818c8aa fortify: implement cleaner argument structure
test / test (push) Successful in 24s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-05 00:07:36 +09:00
cat 69cc64ef56 linux: provide access to stdout
test / test (push) Successful in 22s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-04 22:55:46 +09:00
cat fc25ac2523 app: separate auto etc from permissive defaults
test / test (push) Successful in 23s
Populating /etc with symlinks is quite useful even outside the permissive defaults usage pattern.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-04 22:18:05 +09:00
cat d909b1190a app/config: UseRealUID as true in template
test / test (push) Successful in 24s
The template is based on a Chromium setup, which this workaround was created for.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-04 19:45:31 +09:00
cat cfd05b10f1 release: 0.0.11
release / release (push) Successful in 28s
test / test (push) Successful in 19s
This will be the final release before major command line interface changes. This version is tagged as it contains many fixes that still impacts the permissive defaults usage pattern.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-04 13:46:47 +09:00
cat aa067436a7 workflows: build all packages with full ldflags
test / test (push) Successful in 20s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-04 13:43:57 +09:00
cat d7df24c999 fmsg: drop messages when msgbuf is full during withhold
test / test (push) Successful in 20s
Logging functions are not expected to block. This change fixes multiple hangs where more than 64 messages are produced during withhold.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-04 12:56:19 +09:00
cat 88abcbe0b2 cmd/fsu: remove import of internal package
test / test (push) Successful in 24s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-04 12:32:14 +09:00
cat af15b1c048 app: support mapping target uid as privileged uid in sandbox
test / test (push) Successful in 40s
Chromium's D-Bus client implementation refuses to work when its getuid call returns a different value than what the D-Bus server is running as. The reason behind this is not fully understood, but this workaround is implemented to support chromium and electron apps. This is not used by default since it has many side effects that break many other programs, like SSH on NixOS.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-04 03:15:39 +09:00
cat 7962681f4a app: format mapped uid instead of real uid
test / test (push) Successful in 19s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-04 00:49:32 +09:00
cat bfcce3ff75 system/dbus: buffer xdg-dbus-proxy messages
test / test (push) Successful in 21s
Pointing xdg-dbus-proxy to stdout/stderr makes a huge mess. This change enables app to neatly print out prefixed xdg-dbus-proxy messages after output is resumed.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-03 03:07:02 +09:00
cat 8cd3651bb6 cmd/fshim/ipc: friendly setup timeout message
test / test (push) Successful in 22s
This message eventually gets returned by the app's Start method, so they should be wrapped to provide a friendly message.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-03 02:03:30 +09:00
cat 422d8e00d5 fortify: replace direct syscall with prctl wrapper
test / test (push) Successful in 20s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-02 17:00:25 +09:00
cat 584732f80a cmd: shim and init into separate binaries
test / test (push) Successful in 19s
This change also fixes a deadlock when shim fails to connect and complete the setup.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-11-02 03:13:57 +09:00
cat 4b7b899bb3 add package doc comments
test / test (push) Successful in 19s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-28 20:57:59 +09:00
cat 563c39c2d9 release: 0.0.10
release / release (push) Successful in 24s
test / test (push) Successful in 19s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-28 20:38:10 +09:00
cat aa1f96eeeb fsu: check parent executable path
test / test (push) Successful in 19s
Only allow main program to launch fsu. This change and further checks in the main program reduces attack surface.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-28 18:52:23 +09:00
cat 431dc095e5 app/start: skip cleanup if shim is nil
test / test (push) Successful in 19s
Shim is created before any system operation happens.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-28 14:21:15 +09:00
cat 60e91b9b0f shim: expose checkPid in constructor
test / test (push) Successful in 1m44s
This will be supported soon when launching via fsu.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-28 00:02:55 +09:00
cat d9cb2a9f2b fsu: implement simple setuid user switcher
Contains path to fortify, set at compile time, authenticates based on a simple uid range assignment file which also acts as the allow list.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-28 00:02:34 +09:00
cat 09feda3783 fortify: exit if seal returns error
test / test (push) Successful in 20s
Wait should not be called on an unsealed App.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-27 23:18:16 +09:00
cat 51e84ba8a5 system/dbus: compare sealed value by string
test / test (push) Successful in 19s
Stringer method of dbus.Proxy returns a string representation of its args stream when sealed.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-27 12:09:34 +09:00
cat 7df9d8d01d system: move sd_booted implementation to os abstraction
This implements lazy loading of the systemd marker (they are not accessed in init and shim) and ensures consistent behaviour when running with a stub.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-27 12:09:34 +09:00
cat 6d8bcb63f2 release: 0.0.9
release / release (push) Successful in 27s
test / test (push) Successful in 22s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-27 01:25:24 +09:00
cat c7b77d6e5e fmsg: initialise dequeue prior to withhold/resume
test / test (push) Successful in 23s
This fixes the hang on resume when no messages were ever printed.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-27 01:24:30 +09:00
cat 2f34627d37 release: 0.0.8
release / release (push) Successful in 31s
test / test (push) Successful in 20s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-27 00:49:50 +09:00
cat 1d6ea81205 shim: user switcher process management struct
test / test (push) Successful in 19s
This change moves all user switcher and shim management to the shim package and withholds output while shim is alive. This also eliminated all exit scenarios where revert is skipped.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-27 00:46:15 +09:00
cat ae1a102882 fmsg: support temporarily withholding output
test / test (push) Successful in 31s
Trying to print to a shared stdout is a terrible idea. This change makes it possible to withhold output for the lifetime of the sandbox.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-26 23:09:32 +09:00
cat 093e99d062 app: separate nixos test cases from tests
test / test (push) Successful in 20s
Test cases are very long, separating them improves editor performance.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 17:44:29 +09:00
cat ad7e389eee app: test app permissive defaults sealing behaviour
test / test (push) Successful in 20s
This test seals App against a deterministic os stub and checks the resulting sys and bwrap values against known correct ones. The effects of sys and bwrap on the OS and sandbox is deterministic and tested in their own respective packages.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 17:12:13 +09:00
cat 5b249e4a66 system: print number of ops completed at point of failure
test / test (push) Successful in 21s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 17:08:21 +09:00
cat 2a348c7f91 system: include more info in ACL Stringer
test / test (push) Successful in 24s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 16:23:22 +09:00
cat eb767e7642 app/start: cleaner command not found message
test / test (push) Successful in 27s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 16:12:18 +09:00
cat 3bfe8dbf5d internal: ReadDir wrapper return fs.DirEntry
test / test (push) Successful in 25s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 14:56:29 +09:00
cat 8fa791a2f8 app/seal: symlink /etc entries in permissive default
test / test (push) Successful in 20s
Fortify overrides /etc/passwd and /etc/group in the sandbox. Bind mounting /etc results in them being replaced when the passwd database is updated on host.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 13:31:57 +09:00
cat b932ac8260 app/config: support creating symlinks within sandbox
test / test (push) Successful in 21s
This is already supported by the underlying bwrap helper. This change exposes access to it in Config.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 13:29:01 +09:00
cat 050ffceb27 helper/bwrap: register generic PermConfig types with gob
test / test (push) Successful in 21s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 13:26:01 +09:00
cat 31350d74e5 shim: kill shim if setup becomes impossible
test / test (push) Successful in 23s
This prevents a hang when setup faults but the shim keeps waiting on the socket. Setup is automatically aborted when the shim is killed.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 13:19:37 +09:00
cat 3b82cc55de internal: use fallback paths when XDG_RUNTIME_DIR is not absolute
test / test (push) Successful in 24s
There are scenarios where XDG_RUNTIME_DIR is set to an empty string or garbage. This check tries to ensure reasonable behaviour in most of those cases.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-25 12:14:57 +09:00
cat 6bc5be7e5a internal: wrap calls to os standard library functions
test / test (push) Successful in 19s
This change helps tests stub out and simulate OS behaviour during the sealing process. This also removes dependency on XDG_RUNTIME_DIR as the internal.System implementation provided to App provides a compat directory inside the tmpdir-based share when XDG_RUNTIME_DIR is unavailable.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-23 21:46:21 +09:00
cat e35c5fe3ed system: sys comparison method
test / test (push) Successful in 24s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-23 14:15:13 +09:00
cat 20195ece47 system: return sys in queueing methods
test / test (push) Successful in 54s
This enables building an instance in a single statement.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-23 12:34:16 +09:00
cat cafed5f234 shim: abort setup on failed start and process exit
test / test (push) Successful in 25s
Shim setup listens on a socket in the process share, if shim setup hasn't happened on exit revert will fail. This change makes sure shim setup is aborted on a doomed launch.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-21 21:23:56 +09:00
cat 42e0b168e3 fmsg: produce all output through fmsg
test / test (push) Successful in 17s
The behaviour of print functions from package fmt is not thread safe. Functions provided by fmsg wrap around Logger methods. This makes prefix much cleaner and makes it easy to deal with future changes to logging.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-21 20:47:02 +09:00
cat 380d1f4585 app: move wayland mediation to shim package
test / test (push) Successful in 29s
Values used in the Wayland mediation implementation is stored in various struct fields strewn across multiple app structs and checks are messy and confusing. This commit unifies them into a single struct and access it using much better looking methods.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-21 18:46:06 +09:00
cat 133f23e0de release: 0.0.7
release / release (push) Successful in 21s
test / test (push) Successful in 11s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-20 19:50:59 +09:00
cat 65af1684e3 migrate to git.ophivana.moe/security/fortify
test / test (push) Successful in 14s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-20 19:50:13 +09:00
cat cdda33555c update README document
We have a highly configurable sandbox now, just not really the Android sandbox.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-20 00:24:50 +09:00
cat ad0034b09a app: move app ID to app struct
App ID is inherent to App, and it makes no sense to generate it as part of the app sealing process.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-20 00:22:18 +09:00
cat 1da845d78b workflows: call apt-get without sudo
Workflow scripts run as root in act-runner containers, so calling sudo is redundant and pointless.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-18 22:56:49 +09:00
cat 55bb348d5f state: store launch method instead of launcher path
Launcher path is constant for each launch method on the same system.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-18 22:25:09 +09:00
cat ecce832d93 release: 0.0.6
release / release (push) Successful in 1m46s
test / test (push) Successful in 1m39s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-18 01:26:42 +09:00
cat 65bd7d18db app/share: fix order to ensure SharePath before any of its subdirectories
shareTmpdirChild happened to request an ephemeral dir within SharePath and was called before shareRuntime which ensures that path. This commit moves SharePath initialisation to shareSystem and moves shareTmpdirChild into ShareSystem. Further cleanup and tests are desperately needed for the app package but for now this fix will have to do.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-18 01:21:58 +09:00
cat 4ebb98649e release: 0.0.5
release / release (push) Successful in 1m26s
test / test (push) Successful in 3m6s
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-17 20:48:41 +09:00
cat 919e5b5cd5 init: start timeout only if reaped PID is the initial process
Fix a very obvious bug introduced in 5401882ed0.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-17 20:46:48 +09:00
cat 40161c5938 nix: remove fortify package from default devShell
This change makes it possible to start a devShell when tests aren't passing.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-17 20:35:10 +09:00
cat 679e719f9e system: tests for all Op implementations except DBus
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-10-17 20:28:55 +09:00
331 changed files with 33340 additions and 6413 deletions
+7 -25
View File
@@ -1,42 +1,24 @@
name: release
name: Release
on:
push:
tags:
- '*'
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
name: Create release
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup go
uses: https://github.com/actions/setup-go@v5
with:
go-version: '>=1.23.0'
- name: Get dependencies
run: >-
sudo apt-get update &&
sudo apt-get install -y
gcc
pkg-config
libacl1-dev
if: ${{ runner.os == 'Linux' }}
- name: Build for Linux
run: >-
sh -c "go build -v -ldflags '-s -w -X main.Version=${{ github.ref_name }}' -o bin/fortify &&
sha256sum --tag -b bin/fortify > bin/fortify.sha256"
- name: Build for release
run: nix build --print-out-paths --print-build-logs .#dist
- name: Release
id: use-go-action
uses: https://gitea.com/actions/release-action@main
with:
files: |-
bin/**
result/hakurei-**
api_key: '${{secrets.RELEASE_TOKEN}}'
+114 -21
View File
@@ -1,37 +1,130 @@
name: test
name: Test
on:
- push
- pull_request
jobs:
test:
runs-on: ubuntu-latest
hakurei:
name: Hakurei
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.hakurei
- name: Upload test output
uses: actions/upload-artifact@v3
with:
fetch-depth: 0
name: "hakurei-vm-output"
path: result/*
retention-days: 1
- name: Setup go
uses: https://github.com/actions/setup-go@v5
race:
name: Hakurei (race detector)
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.race
- name: Upload test output
uses: actions/upload-artifact@v3
with:
go-version: '>=1.23.0'
name: "hakurei-race-vm-output"
path: result/*
retention-days: 1
- name: Get dependencies
run: >-
sudo apt-get update &&
sudo apt-get install -y
gcc
pkg-config
libacl1-dev
if: ${{ runner.os == 'Linux' }}
sandbox:
name: Sandbox
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run tests
run: >-
go test ./...
- name: Run NixOS test
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.sandbox
- name: Build for Linux
- name: Upload test output
uses: actions/upload-artifact@v3
with:
name: "sandbox-vm-output"
path: result/*
retention-days: 1
sandbox-race:
name: Sandbox (race detector)
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.sandbox-race
- name: Upload test output
uses: actions/upload-artifact@v3
with:
name: "sandbox-race-vm-output"
path: result/*
retention-days: 1
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.hpkg
- name: Upload test output
uses: actions/upload-artifact@v3
with:
name: "hpkg-vm-output"
path: result/*
retention-days: 1
check:
name: Flake checks
needs:
- hakurei
- race
- sandbox
- sandbox-race
- hpkg
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run checks
run: nix --print-build-logs --experimental-features 'nix-command flakes' flake check
dist:
name: Create distribution
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build for test
id: build-test
run: >-
sh -c "go build -v -ldflags '-s -w -X main.Version=${{ github.ref_name }}' -o bin/fortify &&
sha256sum --tag -b bin/fortify > bin/fortify.sha256"
export HAKUREI_REV="$(git rev-parse --short HEAD)" &&
sed -i.old 's/version = /version = "0.0.0-'$HAKUREI_REV'"; # version = /' package.nix &&
nix build --print-out-paths --print-build-logs .#dist &&
mv package.nix.old package.nix &&
echo "rev=$HAKUREI_REV" >> $GITHUB_OUTPUT
- name: Upload test build
uses: actions/upload-artifact@v3
with:
name: "hakurei-${{ steps.build-test.outputs.rev }}"
path: result/*
retention-days: 1
+5
View File
@@ -0,0 +1,5 @@
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.
+46
View File
@@ -0,0 +1,46 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
name: Create release
runs-on: ubuntu-latest
permissions:
packages: write
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Nix
uses: nixbuild/nix-quick-install-action@v32
with:
nix_conf: |
keep-env-derivations = true
keep-outputs = true
- name: Restore and cache Nix store
uses: nix-community/cache-nix-action@v6
with:
primary-key: build-${{ runner.os }}-${{ hashFiles('**/*.nix') }}
restore-prefixes-first-match: build-${{ runner.os }}-
gc-max-store-size-linux: 1G
purge: true
purge-prefixes: build-${{ runner.os }}-
purge-created: 60
purge-primary-key: never
- name: Build for release
run: nix build --print-out-paths --print-build-logs .#dist
- name: Release
uses: softprops/action-gh-release@v2
with:
files: |-
result/hakurei-**
+48
View File
@@ -0,0 +1,48 @@
name: Test
on:
- push
jobs:
dist:
name: Create distribution
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Nix
uses: nixbuild/nix-quick-install-action@v32
with:
nix_conf: |
keep-env-derivations = true
keep-outputs = true
- name: Restore and cache Nix store
uses: nix-community/cache-nix-action@v6
with:
primary-key: build-${{ runner.os }}-${{ hashFiles('**/*.nix') }}
restore-prefixes-first-match: build-${{ runner.os }}-
gc-max-store-size-linux: 1G
purge: true
purge-prefixes: build-${{ runner.os }}-
purge-created: 60
purge-primary-key: never
- name: Build for test
id: build-test
run: >-
export HAKUREI_REV="$(git rev-parse --short HEAD)" &&
sed -i.old 's/version = /version = "0.0.0-'$HAKUREI_REV'"; # version = /' package.nix &&
nix build --print-out-paths --print-build-logs .#dist &&
mv package.nix.old package.nix &&
echo "rev=$HAKUREI_REV" >> $GITHUB_OUTPUT
- name: Upload test build
uses: actions/upload-artifact@v4
with:
name: "hakurei-${{ steps.build-test.outputs.rev }}"
path: result/*
retention-days: 1
+12 -2
View File
@@ -4,7 +4,8 @@
*.dll
*.so
*.dylib
/fortify
*.pkg
/hakurei
# Test binary, built with `go test -c`
*.test
@@ -22,4 +23,13 @@ go.work.sum
# env file
.env
.idea
.vscode
.vscode
# go generate
/cmd/hakurei/LICENSE
# release
/dist/hakurei-*
# interactive nixos vm
nixos.qcow2
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2024 Ophestra Umiker
Copyright (c) 2024-2025 Ophestra
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+122 -123
View File
@@ -1,130 +1,172 @@
Fortify
=======
<p align="center">
<a href="https://git.gensokyo.uk/security/hakurei">
<picture>
<img src="https://basement.gensokyo.uk/images/yukari1.png" width="200px" alt="Yukari">
</picture>
</a>
</p>
[![Go Reference](https://pkg.go.dev/badge/git.ophivana.moe/cat/fortify.svg)](https://pkg.go.dev/git.ophivana.moe/cat/fortify)
<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>
Lets you run graphical applications as another user ~~in an Android-like sandbox environment~~ (WIP) with a nice NixOS
module to configure target users and provide launchers and desktop files for your privileged user.
Hakurei is a tool for running sandboxed graphical applications as dedicated subordinate users on the Linux kernel.
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.
Why would you want this?
## NixOS Module usage
- It protects the desktop environment from applications.
- It protects applications from each other.
- It provides UID isolation on top of ~~the standard application sandbox~~ (WIP).
There are a few different things to set up for this to work:
- A set of users, each for a group of applications that should be allowed access to each other
- A tool to switch users, currently sudo and machinectl are supported.
- If you are running NixOS, the module in this repository can take care of launchers and desktop files in the privileged
user's environment, as well as packages and extra home-manager configuration for target users.
If you have a flakes-enabled nix environment, you can try out the tool by running:
```shell
nix run git+https://git.ophivana.moe/cat/fortify -- -h
```
## Module usage
The NixOS module currently requires home-manager and impermanence to function correctly.
The NixOS module currently requires home-manager to configure subordinate users. Full module documentation can be found [here](options.md).
To use the module, import it into your configuration with
```nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
fortify = {
url = "git+https://git.ophivana.moe/cat/fortify";
hakurei = {
url = "git+https://git.gensokyo.uk/security/hakurei";
# Optional but recommended to limit the size of your system closure.
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, fortify, ... }:
outputs = { self, nixpkgs, hakurei, ... }:
{
nixosConfigurations.fortify = nixpkgs.lib.nixosSystem {
nixosConfigurations.hakurei = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
fortify.nixosModules.fortify
hakurei.nixosModules.hakurei
];
};
};
}
```
This adds the `environment.fortify` option:
This adds the `environment.hakurei` option:
```nix
{ pkgs, ... }:
{
environment.fortify = {
environment.hakurei = {
enable = true;
user = "nixos";
stateDir = "/var/lib/persist/module";
target = {
chronos = {
launchers = {
weechat.method = "sudo";
claws-mail.capability.pulse = false;
stateDir = "/var/lib/hakurei";
users = {
alice = 0;
nixos = 10;
};
discord = {
command = "vesktop --ozone-platform-hint=wayland";
share = pkgs.vesktop;
commonPaths = [
{
src = "/sdcard";
write = true;
}
];
extraHomeConfig = {
home.stateVersion = "23.05";
};
apps = {
"org.chromium.Chromium" = {
name = "chromium";
identity = 1;
packages = [ pkgs.chromium ];
userns = true;
mapRealUid = true;
dbus = {
system = {
filter = true;
talk = [
"org.bluez"
"org.freedesktop.Avahi"
"org.freedesktop.UPower"
];
};
chromium.dbus = {
configSystem = {
filter = true;
session =
f:
f {
talk = [
"org.bluez"
"org.freedesktop.Avahi"
"org.freedesktop.UPower"
];
};
config = {
filter = true;
talk = [
"org.freedesktop.DBus"
"org.freedesktop.FileManager1"
"org.freedesktop.Notifications"
"org.freedesktop.ScreenSaver"
"org.freedesktop.secrets"
"org.kde.kwalletd5"
"org.kde.kwalletd5"
"org.kde.kwalletd6"
];
];
own = [
"org.chromium.Chromium.*"
"org.mpris.MediaPlayer2.org.chromium.Chromium.*"
"org.mpris.MediaPlayer2.chromium.*"
];
call = {
"org.freedesktop.portal.*" = "*";
};
broadcast = {
"org.freedesktop.portal.*" = "@/org/freedesktop/portal/*";
};
call = { };
broadcast = { };
};
};
};
packages = with pkgs; [
weechat
claws-mail
vesktop
chromium
];
persistence.directories = [
".config/weechat"
".claws-mail"
".config/vesktop"
};
"org.claws_mail.Claws-Mail" = {
name = "claws-mail";
identity = 2;
packages = [ pkgs.claws-mail ];
gpu = false;
capability.pulse = false;
};
"org.weechat" = {
name = "weechat";
identity = 3;
shareUid = true;
packages = [ pkgs.weechat ];
capability = {
wayland = false;
x11 = false;
dbus = true;
pulse = false;
};
};
"dev.vencord.Vesktop" = {
name = "discord";
identity = 3;
shareUid = true;
packages = [ pkgs.vesktop ];
share = pkgs.vesktop;
command = "vesktop --ozone-platform-hint=wayland";
userns = true;
mapRealUid = true;
capability.x11 = true;
dbus = {
session =
f:
f {
talk = [ "org.kde.StatusNotifierWatcher" ];
own = [ ];
call = { };
broadcast = { };
};
system.filter = true;
};
};
"io.looking-glass" = {
name = "looking-glass-client";
identity = 4;
useCommonPaths = false;
groups = [ "plugdev" ];
extraPaths = [
{
src = "/dev/shm/looking-glass";
write = true;
}
];
extraConfig = {
programs.looking-glass-client.enable = true;
@@ -134,46 +176,3 @@ This adds the `environment.fortify` option:
};
}
```
* `enable` determines whether the module should be enabled or not. Useful when sharing configurations between graphical
and headless systems. Defaults to `false`.
* `user` specifies the privileged user with access to fortified applications.
* `stateDir` is the path to your persistent storage location. It is directly passed through to the impermanence module.
* `target` is an attribute set of submodules, where the attribute name is the username of the unprivileged target user.
The available options are:
* `packages`, the list of packages to make available in the target user's environment.
* `persistence`, user persistence attribute set passed to impermanence.
* `extraConfig`, extra home-manager configuration for the target user.
* `launchers`, attribute set where the attribute name is the name of the launcher.
The available options are:
* `command`, the command to run as the target user. Defaults to launcher name.
* `dbus.config`, D-Bus proxy custom configuration.
* `dbus.configSystem`, D-Bus system bus custom configuration, null to disable.
* `dbus.id`, D-Bus application id, has no effect if `dbus.config` is set.
* `dbus.mpris`, whether to enable MPRIS defaults, has no effect if `dbus.config` is set.
* `capability.wayland`, whether to share the Wayland socket.
* `capability.x11`, whether to share the X11 socket and allow connection.
* `capability.dbus`, whether to proxy D-Bus.
* `capability.pulse`, whether to share the PulseAudio socket and cookie.
* `share`, package containing desktop/icon files. Defaults to launcher name.
* `method`, the launch method for the sandboxed program, can be `"fortify"`, `"fortify-sudo"`, `"sudo"`.
-99
View File
@@ -1,99 +0,0 @@
package acl
import (
"errors"
"fmt"
"syscall"
"unsafe"
)
//#include <stdlib.h>
//#include <sys/acl.h>
//#include <acl/libacl.h>
//#cgo linux LDFLAGS: -lacl
import "C"
type acl struct {
val C.acl_t
freed bool
}
func aclGetFile(path string, t C.acl_type_t) (*acl, error) {
p := C.CString(path)
a, err := C.acl_get_file(p, t)
C.free(unsafe.Pointer(p))
if errors.Is(err, syscall.ENODATA) {
err = nil
}
return &acl{val: a, freed: false}, err
}
func (a *acl) setFile(path string, t C.acl_type_t) error {
if C.acl_valid(a.val) != 0 {
return fmt.Errorf("invalid acl")
}
p := C.CString(path)
_, err := C.acl_set_file(p, t, a.val)
C.free(unsafe.Pointer(p))
return err
}
func (a *acl) removeEntry(tt C.acl_tag_t, tq int) error {
var e C.acl_entry_t
// get first entry
if r, err := C.acl_get_entry(a.val, C.ACL_FIRST_ENTRY, &e); err != nil {
return err
} else if r == 0 {
// return on acl with no entries
return nil
}
for {
if r, err := C.acl_get_entry(a.val, C.ACL_NEXT_ENTRY, &e); err != nil {
return err
} else if r == 0 {
// return on drained acl
return nil
}
var (
q int
t C.acl_tag_t
)
// get current entry tag type
if _, err := C.acl_get_tag_type(e, &t); err != nil {
return err
}
// get current entry qualifier
if rq, err := C.acl_get_qualifier(e); err != nil {
// neither ACL_USER nor ACL_GROUP
if errors.Is(err, syscall.EINVAL) {
continue
}
return err
} else {
q = *(*int)(rq)
C.acl_free(rq)
}
// delete on match
if t == tt && q == tq {
_, err := C.acl_delete_entry(a.val, e)
return err
}
}
}
func (a *acl) free() {
if a.freed {
panic("acl already freed")
}
C.acl_free(unsafe.Pointer(a.val))
a.freed = true
}
-88
View File
@@ -1,88 +0,0 @@
package acl
import "unsafe"
//#include <stdlib.h>
//#include <sys/acl.h>
//#include <acl/libacl.h>
//#cgo linux LDFLAGS: -lacl
import "C"
const (
Read = C.ACL_READ
Write = C.ACL_WRITE
Execute = C.ACL_EXECUTE
TypeDefault = C.ACL_TYPE_DEFAULT
TypeAccess = C.ACL_TYPE_ACCESS
UndefinedTag = C.ACL_UNDEFINED_TAG
UserObj = C.ACL_USER_OBJ
User = C.ACL_USER
GroupObj = C.ACL_GROUP_OBJ
Group = C.ACL_GROUP
Mask = C.ACL_MASK
Other = C.ACL_OTHER
)
type Perm C.acl_perm_t
func UpdatePerm(path string, uid int, perms ...Perm) error {
// read acl from file
a, err := aclGetFile(path, TypeAccess)
if err != nil {
return err
}
// free acl on return if get is successful
defer a.free()
// remove existing entry
if err = a.removeEntry(User, uid); err != nil {
return err
}
// create new entry if perms are passed
if len(perms) > 0 {
// create new acl entry
var e C.acl_entry_t
if _, err = C.acl_create_entry(&a.val, &e); err != nil {
return err
}
// get perm set of new entry
var p C.acl_permset_t
if _, err = C.acl_get_permset(e, &p); err != nil {
return err
}
// add target perms
for _, perm := range perms {
if _, err = C.acl_add_perm(p, C.acl_perm_t(perm)); err != nil {
return err
}
}
// set perm set to new entry
if _, err = C.acl_set_permset(e, p); err != nil {
return err
}
// set user tag to new entry
if _, err = C.acl_set_tag_type(e, User); err != nil {
return err
}
// set qualifier (uid) to new entry
if _, err = C.acl_set_qualifier(e, unsafe.Pointer(&uid)); err != nil {
return err
}
}
// calculate mask after update
if _, err = C.acl_calc_mask(&a.val); err != nil {
return err
}
// write acl to file
return a.setFile(path, TypeAccess)
}
+266
View File
@@ -0,0 +1,266 @@
package main
import (
"context"
"fmt"
"io"
"log"
"os"
"os/signal"
"os/user"
"strconv"
"sync"
"syscall"
"time"
"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"
)
func buildCommand(out io.Writer) command.Command {
var (
flagVerbose bool
flagJSON bool
)
c := command.New(out, log.Printf, "hakurei", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")
c.Command("shim", command.UsageInternal, func([]string) error { app.ShimMain(); return errSuccess })
c.Command("app", "Load app from configuration file", func(args []string) error {
if len(args) < 1 {
log.Fatal("app requires at least 1 argument")
}
// config extraArgs...
config := tryPath(args[0])
config.Args = append(config.Args, args[1:]...)
runApp(config)
panic("unreachable")
})
{
var (
dbusConfigSession string
dbusConfigSystem string
mpris bool
dbusVerbose bool
fid string
aid int
groups command.RepeatableFlag
homeDir string
userName string
wayland, x11, dBus, pulse bool
)
c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error {
// initialise config from flags
config := &hst.Config{
ID: fid,
Args: args,
}
if aid < 0 || aid > 9999 {
log.Fatalf("aid %d out of range", aid)
}
// resolve home/username from os when flag is unset
var (
passwd *user.User
passwdOnce sync.Once
passwdFunc = func() {
var us string
if uid, err := std.Uid(aid); err != nil {
hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:")
os.Exit(1)
} else {
us = strconv.Itoa(uid)
}
if u, err := user.LookupId(us); err != nil {
hlog.Verbosef("cannot look up uid %s", us)
passwd = &user.User{
Uid: us,
Gid: us,
Username: "chronos",
Name: "Hakurei Permissive Default",
HomeDir: container.FHSVarEmpty,
}
} else {
passwd = u
}
}
)
if homeDir == "os" {
passwdOnce.Do(passwdFunc)
homeDir = passwd.HomeDir
}
if userName == "chronos" {
passwdOnce.Do(passwdFunc)
userName = passwd.Username
}
config.Identity = aid
config.Groups = groups
config.Username = userName
if a, err := container.NewAbs(homeDir); err != nil {
log.Fatal(err.Error())
return err
} else {
config.Home = a
}
var e system.Enablement
if wayland {
e |= system.EWayland
}
if x11 {
e |= system.EX11
}
if dBus {
e |= system.EDBus
}
if pulse {
e |= system.EPulse
}
config.Enablements = hst.NewEnablements(e)
// parse D-Bus config file from flags if applicable
if dBus {
if dbusConfigSession == "builtin" {
config.SessionBus = dbus.NewConfig(fid, true, mpris)
} else {
if conf, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
log.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err)
} else {
config.SessionBus = conf
}
}
// system bus proxy is optional
if dbusConfigSystem != "nil" {
if conf, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil {
log.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err)
} else {
config.SystemBus = conf
}
}
// override log from configuration
if dbusVerbose {
config.SessionBus.Log = true
config.SystemBus.Log = true
}
}
// invoke app
runApp(config)
panic("unreachable")
}).
Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"),
"Path to session bus proxy config file, or \"builtin\" for defaults").
Flag(&dbusConfigSystem, "dbus-system", command.StringFlag("nil"),
"Path to system bus proxy config file, or \"nil\" to disable").
Flag(&mpris, "mpris", command.BoolFlag(false),
"Allow owning MPRIS D-Bus path, has no effect if custom config is available").
Flag(&dbusVerbose, "dbus-log", command.BoolFlag(false),
"Force buffered logging in the D-Bus proxy").
Flag(&fid, "id", command.StringFlag(""),
"Reverse-DNS style Application identifier, leave empty to inherit instance identifier").
Flag(&aid, "a", command.IntFlag(0),
"Application identity").
Flag(nil, "g", &groups,
"Groups inherited by all container processes").
Flag(&homeDir, "d", command.StringFlag("os"),
"Container home directory").
Flag(&userName, "u", command.StringFlag("chronos"),
"Passwd user name within sandbox").
Flag(&wayland, "wayland", command.BoolFlag(false),
"Enable connection to Wayland via security-context-v1").
Flag(&x11, "X", command.BoolFlag(false),
"Enable direct connection to X11").
Flag(&dBus, "dbus", command.BoolFlag(false),
"Enable proxied connection to D-Bus").
Flag(&pulse, "pulse", command.BoolFlag(false),
"Enable direct connection to PulseAudio")
}
var showFlagShort bool
c.NewCommand("show", "Show live or local app configuration", func(args []string) error {
switch len(args) {
case 0: // system
printShowSystem(os.Stdout, showFlagShort, flagJSON)
case 1: // instance
name := args[0]
config, entry := tryShort(name)
if config == nil {
config = tryPath(name)
}
printShowInstance(os.Stdout, time.Now().UTC(), entry, config, showFlagShort, flagJSON)
default:
log.Fatal("show requires 1 argument")
}
return errSuccess
}).Flag(&showFlagShort, "short", command.BoolFlag(false), "Omit filesystem information")
var psFlagShort bool
c.NewCommand("ps", "List active instances", func(args []string) error {
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(std.Paths().RunDirPath.String()), psFlagShort, flagJSON)
return errSuccess
}).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id")
c.Command("version", "Display version information", func(args []string) error {
fmt.Println(internal.Version())
return errSuccess
})
c.Command("license", "Show full license text", func(args []string) error {
fmt.Println(license)
return errSuccess
})
c.Command("template", "Produce a config template", func(args []string) error {
printJSON(os.Stdout, false, hst.Template())
return errSuccess
})
c.Command("help", "Show this help message", func([]string) error {
c.PrintHelp()
return errSuccess
})
return c
}
func runApp(config *hst.Config) {
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer stop() // unreachable
a := app.MustNew(ctx, std)
rs := new(app.RunState)
if sa, err := a.Seal(config); err != nil {
hlog.PrintBaseError(err, "cannot seal app:")
internal.Exit(1)
} else {
internal.Exit(app.PrintRunStateErr(rs, sa.Run(rs)))
}
*(*int)(nil) = 0 // not reached
}
+81
View File
@@ -0,0 +1,81 @@
package main
import (
"bytes"
"errors"
"flag"
"testing"
"hakurei.app/command"
)
func TestHelp(t *testing.T) {
testCases := []struct {
name string
args []string
want string
}{
{
"main", []string{}, `
Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS]
Commands:
app Load app from configuration file
run Configure and start a permissive default sandbox
show Show live or local app configuration
ps List active instances
version Display version information
license Show full license text
template Produce a config template
help Show this help message
`,
},
{
"run", []string{"run", "-h"}, `
Usage: hakurei run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--wayland] [-X] [--dbus] [--pulse] COMMAND [OPTIONS]
Flags:
-X Enable direct connection to X11
-a int
Application identity
-d string
Container home directory (default "os")
-dbus
Enable proxied connection to D-Bus
-dbus-config string
Path to session bus proxy config file, or "builtin" for defaults (default "builtin")
-dbus-log
Force buffered logging in the D-Bus proxy
-dbus-system string
Path to system bus proxy config file, or "nil" to disable (default "nil")
-g value
Groups inherited by all container processes
-id string
Reverse-DNS style Application identifier, leave empty to inherit instance identifier
-mpris
Allow owning MPRIS D-Bus path, has no effect if custom config is available
-pulse
Enable direct connection to PulseAudio
-u string
Passwd user name within sandbox (default "chronos")
-wayland
Enable connection to Wayland via security-context-v1
`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
out := new(bytes.Buffer)
c := buildCommand(out)
if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) {
t.Errorf("Parse: error = %v; want %v",
err, command.ErrHelp)
}
if got := out.String(); got != tc.want {
t.Errorf("Parse: %s want %s", got, tc.want)
}
})
}
}
+56
View File
@@ -0,0 +1,56 @@
package main
// this works around go:embed '..' limitation
//go:generate cp ../../LICENSE .
import (
_ "embed"
"errors"
"log"
"os"
"hakurei.app/container"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/internal/sys"
)
var (
errSuccess = errors.New("success")
//go:embed LICENSE
license string
)
func init() { hlog.Prepare("hakurei") }
var std sys.State = new(sys.Std)
func main() {
// early init path, skips root check and duplicate PR_SET_DUMPABLE
container.TryArgv0(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
}
if os.Geteuid() == 0 {
log.Fatal("this program must not run as root")
}
buildCommand(os.Stderr).MustParse(os.Args[1:], func(err error) {
hlog.Verbosef("command returned %v", err)
if errors.Is(err, errSuccess) {
hlog.BeforeExit()
os.Exit(0)
}
// this catches faulty command handlers that fail to return before this point
})
log.Fatal("unreachable")
}
+110
View File
@@ -0,0 +1,110 @@
package main
import (
"encoding/json"
"errors"
"io"
"log"
"os"
"strconv"
"strings"
"syscall"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
)
func tryPath(name string) (config *hst.Config) {
var r io.Reader
config = new(hst.Config)
if name != "-" {
r = tryFd(name)
if r == nil {
hlog.Verbose("load configuration from file")
if f, err := os.Open(name); err != nil {
log.Fatalf("cannot access configuration file %q: %s", name, err)
} else {
// finalizer closes f
r = f
}
} else {
defer func() {
if err := r.(io.ReadCloser).Close(); err != nil {
log.Printf("cannot close config fd: %v", err)
}
}()
}
} else {
r = os.Stdin
}
if err := json.NewDecoder(r).Decode(&config); err != nil {
log.Fatalf("cannot load configuration: %v", err)
}
return
}
func tryFd(name string) io.ReadCloser {
if v, err := strconv.Atoi(name); err != nil {
if !errors.Is(err, strconv.ErrSyntax) {
hlog.Verbosef("name cannot be interpreted as int64: %v", err)
}
return nil
} else {
hlog.Verbosef("trying config stream from %d", v)
fd := uintptr(v)
if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 {
if errors.Is(errno, syscall.EBADF) {
return nil
}
log.Fatalf("cannot get fd %d: %v", fd, errno)
}
return os.NewFile(fd, strconv.Itoa(v))
}
}
func tryShort(name string) (config *hst.Config, entry *state.State) {
likePrefix := false
if len(name) <= 32 {
likePrefix = true
for _, c := range name {
if c >= '0' && c <= '9' {
continue
}
if c >= 'a' && c <= 'f' {
continue
}
likePrefix = false
break
}
}
// try to match from state store
if likePrefix && len(name) >= 8 {
hlog.Verbose("argument looks like prefix")
s := state.NewMulti(std.Paths().RunDirPath.String())
if entries, err := state.Join(s); err != nil {
log.Printf("cannot join store: %v", err)
// drop to fetch from file
} else {
for id := range entries {
v := id.String()
if strings.HasPrefix(v, name) {
// match, use config from this state entry
entry = entries[id]
config = entry.Config
break
}
hlog.Verbosef("instance %s skipped", v)
}
}
}
return
}
+296
View File
@@ -0,0 +1,296 @@
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"os"
"slices"
"strconv"
"strings"
"text/tabwriter"
"time"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/system/dbus"
)
func printShowSystem(output io.Writer, short, flagJSON bool) {
t := newPrinter(output)
defer t.MustFlush()
info := &hst.Info{Paths: std.Paths()}
// get hid by querying uid of identity 0
if uid, err := std.Uid(0); err != nil {
hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:")
os.Exit(1)
} else {
info.User = (uid / 10000) - 100
}
if flagJSON {
printJSON(output, short, info)
return
}
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(
output io.Writer, now time.Time,
instance *state.State, config *hst.Config,
short, flagJSON bool) {
if flagJSON {
if instance != nil {
printJSON(output, short, instance)
} else {
printJSON(output, short, config)
}
return
}
t := newPrinter(output)
defer t.MustFlush()
if config.Container == nil {
mustPrint(output, "Warning: this configuration uses permissive defaults!\n\n")
}
if instance != nil {
t.Printf("State\n")
t.Printf(" Instance:\t%s (%d)\n", instance.ID.String(), instance.PID)
t.Printf(" Uptime:\t%s\n", now.Sub(instance.Time).Round(time.Second).String())
t.Printf("\n")
}
t.Printf("App\n")
if config.ID != "" {
t.Printf(" Identity:\t%d (%s)\n", config.Identity, config.ID)
} else {
t.Printf(" Identity:\t%d\n", config.Identity)
}
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.Home != nil {
t.Printf(" Home:\t%s\n", config.Home)
}
if config.Container != nil {
params := config.Container
if params.Hostname != "" {
t.Printf(" Hostname:\t%s\n", params.Hostname)
}
flags := make([]string, 0, 7)
writeFlag := func(name string, value bool) {
if value {
flags = append(flags, name)
}
}
writeFlag("userns", params.Userns)
writeFlag("devel", params.Devel)
writeFlag("net", params.HostNet)
writeFlag("abstract", params.HostAbstract)
writeFlag("device", params.Device)
writeFlag("tty", params.Tty)
writeFlag("mapuid", params.MapRealUID)
writeFlag("directwl", config.DirectWayland)
if len(flags) == 0 {
flags = append(flags, "none")
}
t.Printf(" Flags:\t%s\n", strings.Join(flags, " "))
if config.Path != nil {
t.Printf(" Path:\t%s\n", config.Path)
}
}
if len(config.Args) > 0 {
t.Printf(" Arguments:\t%s\n", strings.Join(config.Args, " "))
}
t.Printf("\n")
if !short {
if config.Container != nil && len(config.Container.Filesystem) > 0 {
t.Printf("Filesystem\n")
for _, f := range config.Container.Filesystem {
if !f.Valid() {
t.Println(" <invalid>")
continue
}
t.Printf(" %s\n", f)
}
t.Printf("\n")
}
if len(config.ExtraPerms) > 0 {
t.Printf("Extra ACL\n")
for _, p := range config.ExtraPerms {
if p == nil {
continue
}
t.Printf(" %s\n", p.String())
}
t.Printf("\n")
}
}
printDBus := func(c *dbus.Config) {
t.Printf(" Filter:\t%v\n", c.Filter)
if len(c.See) > 0 {
t.Printf(" See:\t%q\n", c.See)
}
if len(c.Talk) > 0 {
t.Printf(" Talk:\t%q\n", c.Talk)
}
if len(c.Own) > 0 {
t.Printf(" Own:\t%q\n", c.Own)
}
if len(c.Call) > 0 {
t.Printf(" Call:\t%q\n", c.Call)
}
if len(c.Broadcast) > 0 {
t.Printf(" Broadcast:\t%q\n", c.Broadcast)
}
}
if config.SessionBus != nil {
t.Printf("Session bus\n")
printDBus(config.SessionBus)
t.Printf("\n")
}
if config.SystemBus != nil {
t.Printf("System bus\n")
printDBus(config.SystemBus)
t.Printf("\n")
}
}
func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON bool) {
var entries state.Entries
if e, err := state.Join(s); err != nil {
log.Fatalf("cannot join store: %v", err)
} else {
entries = e
}
if err := s.Close(); err != nil {
log.Printf("cannot close store: %v", err)
}
if !short && flagJSON {
es := make(map[string]*state.State, len(entries))
for id, instance := range entries {
es[id.String()] = instance
}
printJSON(output, short, es)
return
}
// sort state entries by id string to ensure consistency between runs
exp := make([]*expandedStateEntry, 0, len(entries))
for id, instance := range entries {
// gracefully skip nil states
if instance == nil {
log.Printf("got invalid state entry %s", id.String())
continue
}
// gracefully skip inconsistent states
if id != instance.ID {
log.Printf("possible store corruption: entry %s has id %s",
id.String(), instance.ID.String())
continue
}
exp = append(exp, &expandedStateEntry{s: id.String(), State: instance})
}
slices.SortFunc(exp, func(a, b *expandedStateEntry) int { return a.Time.Compare(b.Time) })
if short {
if flagJSON {
v := make([]string, len(exp))
for i, e := range exp {
v[i] = e.s
}
printJSON(output, short, v)
} else {
for _, e := range exp {
mustPrintln(output, e.s[:8])
}
}
return
}
t := newPrinter(output)
defer t.MustFlush()
t.Println("\tInstance\tPID\tApplication\tUptime")
for _, e := range exp {
if len(e.s) != 1<<5 {
// unreachable
log.Printf("possible store corruption: invalid instance string %s", e.s)
continue
}
as := "(No configuration information)"
if e.Config != nil {
as = strconv.Itoa(e.Config.Identity)
id := e.Config.ID
if id == "" {
id = "app.hakurei." + e.s[:8]
}
as += " (" + id + ")"
}
t.Printf("\t%s\t%d\t%s\t%s\n",
e.s[:8], e.PID, as, now.Sub(e.Time).Round(time.Second).String())
}
}
type expandedStateEntry struct {
s string
*state.State
}
func printJSON(output io.Writer, short bool, v any) {
encoder := json.NewEncoder(output)
if !short {
encoder.SetIndent("", " ")
}
if err := encoder.Encode(v); err != nil {
log.Fatalf("cannot serialise: %v", err)
}
}
func newPrinter(output io.Writer) *tp { return &tp{tabwriter.NewWriter(output, 0, 1, 4, ' ', 0)} }
type tp struct{ *tabwriter.Writer }
func (p *tp) Printf(format string, a ...any) {
if _, err := fmt.Fprintf(p, format, a...); err != nil {
log.Fatalf("cannot write to tabwriter: %v", err)
}
}
func (p *tp) Println(a ...any) {
if _, err := fmt.Fprintln(p, a...); err != nil {
log.Fatalf("cannot write to tabwriter: %v", err)
}
}
func (p *tp) MustFlush() {
if err := p.Writer.Flush(); err != nil {
log.Fatalf("cannot flush tabwriter: %v", err)
}
}
func mustPrint(output io.Writer, a ...any) {
if _, err := fmt.Fprint(output, a...); err != nil {
log.Fatalf("cannot print: %v", err)
}
}
func mustPrintln(output io.Writer, a ...any) {
if _, err := fmt.Fprintln(output, a...); err != nil {
log.Fatalf("cannot print: %v", err)
}
}
+732
View File
@@ -0,0 +1,732 @@
package main
import (
"strings"
"testing"
"time"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/system/dbus"
)
var (
testID = state.ID{
0x8e, 0x2c, 0x76, 0xb0,
0x66, 0xda, 0xbe, 0x57,
0x4c, 0xf0, 0x73, 0xbd,
0xb4, 0x6e, 0xb5, 0xc1,
}
testState = &state.State{
ID: testID,
PID: 0xDEADBEEF,
Config: hst.Template(),
Time: testAppTime,
}
testTime = time.Unix(3752, 1).UTC()
testAppTime = time.Unix(0, 9).UTC()
)
func Test_printShowInstance(t *testing.T) {
testCases := []struct {
name string
instance *state.State
config *hst.Config
short, json bool
want string
}{
{"config", nil, hst.Template(), false, false, `App
Identity: 9 (org.chromium.Chromium)
Enablements: wayland, dbus, pulseaudio
Groups: video, dialout, plugdev
Home: /data/data/org.chromium.Chromium
Hostname: localhost
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
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
--x+:/var/lib/hakurei/u0
rwx:/var/lib/hakurei/u0/org.chromium.Chromium
Session bus
Filter: true
Talk: ["org.freedesktop.Notifications" "org.freedesktop.FileManager1" "org.freedesktop.ScreenSaver" "org.freedesktop.secrets" "org.kde.kwalletd5" "org.kde.kwalletd6" "org.gnome.SessionManager"]
Own: ["org.chromium.Chromium.*" "org.mpris.MediaPlayer2.org.chromium.Chromium.*" "org.mpris.MediaPlayer2.chromium.*"]
Call: map["org.freedesktop.portal.*":"*"]
Broadcast: map["org.freedesktop.portal.*":"@/org/freedesktop/portal/*"]
System bus
Filter: true
Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"]
`},
{"config pd", nil, new(hst.Config), false, false, `Warning: this configuration uses permissive defaults!
App
Identity: 0
Enablements: (no enablements)
`},
{"config flag none", nil, &hst.Config{Container: new(hst.ContainerConfig)}, false, false, `App
Identity: 0
Enablements: (no enablements)
Flags: none
`},
{"config nil entries", nil, &hst.Config{Container: &hst.ContainerConfig{Filesystem: make([]hst.FilesystemConfigJSON, 1)}, ExtraPerms: make([]*hst.ExtraPermConfig, 1)}, false, false, `App
Identity: 0
Enablements: (no enablements)
Flags: none
Filesystem
<invalid>
Extra ACL
`},
{"config pd dbus see", nil, &hst.Config{SessionBus: &dbus.Config{See: []string{"org.example.test"}}}, false, false, `Warning: this configuration uses permissive defaults!
App
Identity: 0
Enablements: (no enablements)
Session bus
Filter: false
See: ["org.example.test"]
`},
{"instance", testState, hst.Template(), false, false, `State
Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (3735928559)
Uptime: 1h2m32s
App
Identity: 9 (org.chromium.Chromium)
Enablements: wayland, dbus, pulseaudio
Groups: video, dialout, plugdev
Home: /data/data/org.chromium.Chromium
Hostname: localhost
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
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
--x+:/var/lib/hakurei/u0
rwx:/var/lib/hakurei/u0/org.chromium.Chromium
Session bus
Filter: true
Talk: ["org.freedesktop.Notifications" "org.freedesktop.FileManager1" "org.freedesktop.ScreenSaver" "org.freedesktop.secrets" "org.kde.kwalletd5" "org.kde.kwalletd6" "org.gnome.SessionManager"]
Own: ["org.chromium.Chromium.*" "org.mpris.MediaPlayer2.org.chromium.Chromium.*" "org.mpris.MediaPlayer2.chromium.*"]
Call: map["org.freedesktop.portal.*":"*"]
Broadcast: map["org.freedesktop.portal.*":"@/org/freedesktop/portal/*"]
System bus
Filter: true
Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"]
`},
{"instance pd", testState, new(hst.Config), false, false, `Warning: this configuration uses permissive defaults!
State
Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (3735928559)
Uptime: 1h2m32s
App
Identity: 0
Enablements: (no enablements)
`},
{"json nil", nil, nil, false, true, `null
`},
{"json instance", testState, nil, false, true, `{
"instance": [
142,
44,
118,
176,
102,
218,
190,
87,
76,
240,
115,
189,
180,
110,
181,
193
],
"pid": 3735928559,
"config": {
"id": "org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"enablements": {
"wayland": true,
"dbus": true,
"pulse": true
},
"session_bus": {
"see": null,
"talk": [
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager"
],
"own": [
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"
],
"call": {
"org.freedesktop.portal.*": "*"
},
"broadcast": {
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"
},
"filter": true
},
"system_bus": {
"see": null,
"talk": [
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower"
],
"own": null,
"call": null,
"broadcast": null,
"filter": true
},
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"extra_perms": [
{
"ensure": true,
"path": "/var/lib/hakurei/u0",
"x": true
},
{
"path": "/var/lib/hakurei/u0/org.chromium.Chromium",
"r": true,
"w": true,
"x": true
}
],
"identity": 9,
"groups": [
"video",
"dialout",
"plugdev"
],
"container": {
"hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1,
"seccomp_presets": 1,
"seccomp_compat": true,
"devel": true,
"userns": true,
"host_net": true,
"host_abstract": true,
"tty": true,
"multiarch": true,
"env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
},
"map_real_uid": true,
"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"
},
{
"type": "link",
"dst": "/run/current-system",
"linkname": "/run/current-system",
"dereference": true
},
{
"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,
"ensure": true
},
{
"type": "bind",
"src": "/dev/dri",
"dev": true,
"optional": true
}
]
}
},
"time": "1970-01-01T00:00:00.000000009Z"
}
`},
{"json config", nil, hst.Template(), false, true, `{
"id": "org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"enablements": {
"wayland": true,
"dbus": true,
"pulse": true
},
"session_bus": {
"see": null,
"talk": [
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager"
],
"own": [
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"
],
"call": {
"org.freedesktop.portal.*": "*"
},
"broadcast": {
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"
},
"filter": true
},
"system_bus": {
"see": null,
"talk": [
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower"
],
"own": null,
"call": null,
"broadcast": null,
"filter": true
},
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"extra_perms": [
{
"ensure": true,
"path": "/var/lib/hakurei/u0",
"x": true
},
{
"path": "/var/lib/hakurei/u0/org.chromium.Chromium",
"r": true,
"w": true,
"x": true
}
],
"identity": 9,
"groups": [
"video",
"dialout",
"plugdev"
],
"container": {
"hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1,
"seccomp_presets": 1,
"seccomp_compat": true,
"devel": true,
"userns": true,
"host_net": true,
"host_abstract": true,
"tty": true,
"multiarch": true,
"env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
},
"map_real_uid": true,
"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"
},
{
"type": "link",
"dst": "/run/current-system",
"linkname": "/run/current-system",
"dereference": true
},
{
"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,
"ensure": true
},
{
"type": "bind",
"src": "/dev/dri",
"dev": true,
"optional": true
}
]
}
}
`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
output := new(strings.Builder)
printShowInstance(output, testTime, tc.instance, tc.config, tc.short, tc.json)
if got := output.String(); got != tc.want {
t.Errorf("printShowInstance: got\n%s\nwant\n%s",
got, tc.want)
return
}
})
}
}
func Test_printPs(t *testing.T) {
testCases := []struct {
name string
entries state.Entries
short, json bool
want string
}{
{"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{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
`},
{"valid", state.Entries{testID: testState}, false, false, ` Instance PID Application Uptime
8e2c76b0 3735928559 9 (org.chromium.Chromium) 1h2m32s
`},
{"valid short", state.Entries{testID: testState}, true, false, "8e2c76b0\n"},
{"valid json", state.Entries{testID: testState}, false, true, `{
"8e2c76b066dabe574cf073bdb46eb5c1": {
"instance": [
142,
44,
118,
176,
102,
218,
190,
87,
76,
240,
115,
189,
180,
110,
181,
193
],
"pid": 3735928559,
"config": {
"id": "org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"enablements": {
"wayland": true,
"dbus": true,
"pulse": true
},
"session_bus": {
"see": null,
"talk": [
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager"
],
"own": [
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"
],
"call": {
"org.freedesktop.portal.*": "*"
},
"broadcast": {
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"
},
"filter": true
},
"system_bus": {
"see": null,
"talk": [
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower"
],
"own": null,
"call": null,
"broadcast": null,
"filter": true
},
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"extra_perms": [
{
"ensure": true,
"path": "/var/lib/hakurei/u0",
"x": true
},
{
"path": "/var/lib/hakurei/u0/org.chromium.Chromium",
"r": true,
"w": true,
"x": true
}
],
"identity": 9,
"groups": [
"video",
"dialout",
"plugdev"
],
"container": {
"hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1,
"seccomp_presets": 1,
"seccomp_compat": true,
"devel": true,
"userns": true,
"host_net": true,
"host_abstract": true,
"tty": true,
"multiarch": true,
"env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
},
"map_real_uid": true,
"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"
},
{
"type": "link",
"dst": "/run/current-system",
"linkname": "/run/current-system",
"dereference": true
},
{
"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,
"ensure": true
},
{
"type": "bind",
"src": "/dev/dri",
"dev": true,
"optional": true
}
]
}
},
"time": "1970-01-01T00:00:00.000000009Z"
}
}
`},
{"valid short json", state.Entries{testID: testState}, true, true, `["8e2c76b066dabe574cf073bdb46eb5c1"]
`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
output := new(strings.Builder)
printPs(output, testTime, stubStore(tc.entries), tc.short, tc.json)
if got := output.String(); got != tc.want {
t.Errorf("printPs: got\n%s\nwant\n%s",
got, tc.want)
return
}
})
}
}
// stubStore implements [state.Store] and returns test samples via [state.Joiner].
type stubStore state.Entries
func (s stubStore) Join() (state.Entries, error) { return state.Entries(s), nil }
func (s stubStore) Do(int, func(c state.Cursor)) (bool, error) { panic("unreachable") }
func (s stubStore) List() ([]int, error) { panic("unreachable") }
func (s stubStore) Close() error { return nil }
+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.
+161
View File
@@ -0,0 +1,161 @@
package main
import (
"encoding/json"
"log"
"os"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/hst"
"hakurei.app/system/dbus"
)
type appInfo struct {
Name string `json:"name"`
Version string `json:"version"`
// passed through to [hst.Config]
ID string `json:"id"`
// passed through to [hst.Config]
Identity int `json:"identity"`
// passed through to [hst.Config]
Groups []string `json:"groups,omitempty"`
// passed through to [hst.Config]
Devel bool `json:"devel,omitempty"`
// passed through to [hst.Config]
Userns bool `json:"userns,omitempty"`
// passed through to [hst.Config]
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]
Tty bool `json:"tty,omitempty"`
// passed through to [hst.Config]
MapRealUID bool `json:"map_real_uid,omitempty"`
// passed through to [hst.Config]
DirectWayland bool `json:"direct_wayland,omitempty"`
// passed through to [hst.Config]
SystemBus *dbus.Config `json:"system_bus,omitempty"`
// passed through to [hst.Config]
SessionBus *dbus.Config `json:"session_bus,omitempty"`
// passed through to [hst.Config]
Enablements *hst.Enablements `json:"enablements,omitempty"`
// passed through to [hst.Config]
Multiarch bool `json:"multiarch,omitempty"`
// passed through to [hst.Config]
Bluetooth bool `json:"bluetooth,omitempty"`
// allow gpu access within sandbox
GPU bool `json:"gpu"`
// store path to nixGL mesa wrappers
Mesa string `json:"mesa,omitempty"`
// store path to nixGL source
NixGL string `json:"nix_gl,omitempty"`
// store path to activate-and-exec script
Launcher *container.Absolute `json:"launcher"`
// store path to /run/current-system
CurrentSystem *container.Absolute `json:"current_system"`
// store path to home-manager activation package
ActivationPackage string `json:"activation_package"`
}
func (app *appInfo) toHst(pathSet *appPathSet, pathname *container.Absolute, argv []string, flagDropShell bool) *hst.Config {
config := &hst.Config{
ID: app.ID,
Path: pathname,
Args: argv,
Enablements: app.Enablements,
SystemBus: app.SystemBus,
SessionBus: app.SessionBus,
DirectWayland: app.DirectWayland,
Username: "hakurei",
Shell: pathShell,
Home: pathDataData.Append(app.ID),
Identity: app.Identity,
Groups: app.Groups,
Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name),
Devel: app.Devel,
Userns: app.Userns,
HostNet: app.HostNet,
HostAbstract: app.HostAbstract,
Device: app.Device,
Tty: app.Tty || flagDropShell,
MapRealUID: app.MapRealUID,
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: 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}},
},
},
ExtraPerms: []*hst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
},
}
if app.Multiarch {
config.Container.SeccompFlags |= seccomp.AllowMultiarch
}
if app.Bluetooth {
config.Container.SeccompFlags |= seccomp.AllowBluetooth
}
return config
}
func loadAppInfo(name string, beforeFail func()) *appInfo {
bundle := new(appInfo)
if f, err := os.Open(name); err != nil {
beforeFail()
log.Fatalf("cannot open bundle: %v", err)
} else if err = json.NewDecoder(f).Decode(&bundle); err != nil {
beforeFail()
log.Fatalf("cannot parse bundle metadata: %v", err)
} else if err = f.Close(); err != nil {
log.Printf("cannot close bundle metadata: %v", err)
// not fatal
}
if bundle.ID == "" {
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
}
func formatHostname(name string) string {
if h, err := os.Hostname(); err != nil {
log.Printf("cannot get hostname: %v", err)
return "hakurei-" + name
} else {
return h + "-" + name
}
}
+256
View File
@@ -0,0 +1,256 @@
{
nixpkgsFor,
system,
nixpkgs,
home-manager,
}:
{
lib,
stdenv,
closureInfo,
writeScript,
runtimeShell,
writeText,
symlinkJoin,
vmTools,
runCommand,
fetchFromGitHub,
zstd,
nix,
sqlite,
name ? throw "name is required",
version ? throw "version is required",
pname ? "${name}-${version}",
modules ? [ ],
nixosModules ? [ ],
script ? ''
exec "$SHELL" "$@"
'',
id ? name,
identity ? throw "identity is required",
groups ? [ ],
userns ? false,
net ? true,
dev ? false,
no_new_session ? false,
map_real_uid ? false,
direct_wayland ? false,
system_bus ? null,
session_bus ? null,
allow_wayland ? true,
allow_x11 ? false,
allow_dbus ? true,
allow_pulse ? true,
gpu ? allow_wayland || allow_x11,
}:
let
inherit (lib) optionals;
homeManagerConfiguration = home-manager.lib.homeManagerConfiguration {
pkgs = nixpkgsFor.${system};
modules = modules ++ [
{
home = {
username = "hakurei";
homeDirectory = "/data/data/${id}";
stateVersion = "22.11";
};
}
];
};
launcher = writeScript "hakurei-${pname}" ''
#!${runtimeShell} -el
${script}
'';
extraNixOSConfig =
{ pkgs, ... }:
{
environment = {
etc.nixpkgs.source = nixpkgs.outPath;
systemPackages = [ pkgs.nix ];
};
imports = nixosModules;
};
nixos = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
extraNixOSConfig
{ nix.settings.experimental-features = [ "flakes" ]; }
{ nix.settings.experimental-features = [ "nix-command" ]; }
{ boot.isContainer = true; }
{ system.stateVersion = "22.11"; }
];
};
etc = vmTools.runInLinuxVM (
runCommand "etc" { } ''
mkdir -p /etc
${nixos.config.system.build.etcActivationCommands}
# remove unused files
rm -rf /etc/sudoers
mkdir -p $out
tar -C /etc -cf "$out/etc.tar" .
''
);
extendSessionDefault = id: ext: {
filter = true;
talk = [ "org.freedesktop.Notifications" ] ++ ext.talk;
own =
(optionals (id != null) [
"${id}.*"
"org.mpris.MediaPlayer2.${id}.*"
])
++ ext.own;
inherit (ext) call broadcast;
};
nixGL = fetchFromGitHub {
owner = "nix-community";
repo = "nixGL";
rev = "310f8e49a149e4c9ea52f1adf70cdc768ec53f8a";
hash = "sha256-lnzZQYG0+EXl/6NkGpyIz+FEOc/DSEG57AP1VsdeNrM=";
};
mesaWrappers =
let
isIntelX86Platform = system == "x86_64-linux";
nixGLPackages = import (nixGL + "/default.nix") {
pkgs = nixpkgs.legacyPackages.${system};
enable32bits = isIntelX86Platform;
enableIntelX86Extensions = isIntelX86Platform;
};
in
symlinkJoin {
name = "nixGL-mesa";
paths = with nixGLPackages; [
nixGLIntel
nixVulkanIntel
];
};
info = builtins.toJSON {
inherit
name
version
id
identity
launcher
groups
userns
net
dev
no_new_session
map_real_uid
direct_wayland
system_bus
gpu
;
session_bus =
if session_bus != null then
(session_bus (extendSessionDefault id))
else
(extendSessionDefault id {
talk = [ ];
own = [ ];
call = { };
broadcast = { };
});
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;
current_system = nixos.config.system.build.toplevel;
activation_package = homeManagerConfiguration.activationPackage;
};
in
stdenv.mkDerivation {
name = "${pname}.pkg";
inherit version;
__structuredAttrs = true;
nativeBuildInputs = [
zstd
nix
sqlite
];
buildCommand = ''
NIX_ROOT="$(mktemp -d)"
export USER="nobody"
# create bootstrap store
bootstrapClosureInfo="${
closureInfo {
rootPaths = [
nix
nixos.config.system.build.toplevel
];
}
}"
echo "copying bootstrap store paths..."
mkdir -p "$NIX_ROOT/nix/store"
xargs -n 1 -a "$bootstrapClosureInfo/store-paths" cp -at "$NIX_ROOT/nix/store/"
NIX_REMOTE="local?root=$NIX_ROOT" nix-store --load-db < "$bootstrapClosureInfo/registration"
NIX_REMOTE="local?root=$NIX_ROOT" nix-store --optimise
sqlite3 "$NIX_ROOT/nix/var/nix/db/db.sqlite" "UPDATE ValidPaths SET registrationTime = ''${SOURCE_DATE_EPOCH}"
chmod -R +r "$NIX_ROOT/nix/var"
# create binary cache
closureInfo="${
closureInfo {
rootPaths = [
homeManagerConfiguration.activationPackage
launcher
]
++ optionals gpu [
mesaWrappers
nixGL
];
}
}"
echo "copying application paths..."
TMP_STORE="$(mktemp -d)"
mkdir -p "$TMP_STORE/nix/store"
xargs -n 1 -a "$closureInfo/store-paths" cp -at "$TMP_STORE/nix/store/"
NIX_REMOTE="local?root=$TMP_STORE" nix-store --load-db < "$closureInfo/registration"
sqlite3 "$TMP_STORE/nix/var/nix/db/db.sqlite" "UPDATE ValidPaths SET registrationTime = ''${SOURCE_DATE_EPOCH}"
NIX_REMOTE="local?root=$TMP_STORE" nix --offline --extra-experimental-features nix-command \
--verbose --log-format raw-with-logs \
copy --all --no-check-sigs --to \
"file://$NIX_ROOT/res?compression=zstd&compression-level=19&parallel-compression=true"
# package /etc
mkdir -p "$NIX_ROOT/etc"
tar -C "$NIX_ROOT/etc" -xf "${etc}/etc.tar"
# write metadata
cp "${writeText "bundle.json" info}" "$NIX_ROOT/bundle.json"
# create an intermediate file to improve zstd performance
INTER="$(mktemp)"
tar -C "$NIX_ROOT" -cf "$INTER" .
zstd -T0 -19 -fo "$out" "$INTER"
'';
}
+334
View File
@@ -0,0 +1,334 @@
package main
import (
"context"
"encoding/json"
"errors"
"log"
"os"
"os/signal"
"path"
"syscall"
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
)
var (
errSuccess = errors.New("success")
)
func init() {
hlog.Prepare("hpkg")
if err := os.Setenv("SHELL", pathShell.String()); err != nil {
log.Fatalf("cannot set $SHELL: %v", err)
}
}
func main() {
if os.Geteuid() == 0 {
log.Fatal("this program must not run as root")
}
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer stop() // unreachable
var (
flagVerbose bool
flagDropShell bool
)
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")
{
var (
flagDropShellActivate bool
)
c.NewCommand("install", "Install an application from its package", func(args []string) error {
if len(args) != 1 {
log.Println("invalid argument")
return syscall.EINVAL
}
pkgPath := args[0]
if !path.IsAbs(pkgPath) {
if dir, err := os.Getwd(); err != nil {
log.Printf("cannot get current directory: %v", err)
return err
} else {
pkgPath = path.Join(dir, pkgPath)
}
}
/*
Look up paths to programs started by hpkg.
This is done here to ease error handling as cleanup is not yet required.
*/
var (
_ = lookPath("zstd")
tar = lookPath("tar")
chmod = lookPath("chmod")
rm = lookPath("rm")
)
/*
Extract package and set up for cleanup.
*/
var workDir *container.Absolute
if p, err := os.MkdirTemp("", "hpkg.*"); err != nil {
log.Printf("cannot create temporary directory: %v", err)
return err
} else if workDir, err = 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.String())
mustRun(rm, "-rf", workDir.String())
}
beforeRunFail.Store(&cleanup)
mustRun(tar, "-C", workDir.String(), "-xf", pkgPath)
/*
Parse bundle and app metadata, do pre-install checks.
*/
bundle := loadAppInfo(path.Join(workDir.String(), "bundle.json"), cleanup)
pathSet := pathSetByApp(bundle.ID)
a := bundle
if s, err := os.Stat(pathSet.metaPath.String()); err != nil {
if !os.IsNotExist(err) {
cleanup()
log.Printf("cannot access %q: %v", pathSet.metaPath, err)
return err
}
// did not modify app, clean installation condition met later
} else if s.IsDir() {
cleanup()
log.Printf("metadata path %q is not a file", pathSet.metaPath)
return syscall.EBADMSG
} else {
a = loadAppInfo(pathSet.metaPath.String(), cleanup)
if a.ID != bundle.ID {
cleanup()
log.Printf("app %q claims to have identifier %q",
bundle.ID, a.ID)
return syscall.EBADE
}
// sec: should verify credentials
}
if a != bundle {
// do not try to re-install
if a.NixGL == bundle.NixGL &&
a.CurrentSystem == bundle.CurrentSystem &&
a.Launcher == bundle.Launcher &&
a.ActivationPackage == bundle.ActivationPackage {
cleanup()
log.Printf("package %q is identical to local application %q",
pkgPath, a.ID)
return errSuccess
}
// identity determines uid
if a.Identity != bundle.Identity {
cleanup()
log.Printf("package %q identity %d differs from installed %d",
pkgPath, bundle.Identity, a.Identity)
return syscall.EBADE
}
// sec: should compare version string
hlog.Verbosef("installing application %q version %q over local %q",
bundle.ID, bundle.Version, a.Version)
} else {
hlog.Verbosef("application %q clean installation", bundle.ID)
// sec: should install credentials
}
/*
Setup steps for files owned by the target user.
*/
withCacheDir(ctx, "install", []string{
// export inner bundle path in the environment
"export BUNDLE=" + hst.Tmp + "/bundle",
// replace inner /etc
"mkdir -p etc",
"chmod -R +w etc",
"rm -rf etc",
"cp -dRf $BUNDLE/etc etc",
// replace inner /nix
"mkdir -p nix",
"chmod -R +w nix",
"rm -rf nix",
"cp -dRf /nix nix",
// copy from binary cache
"nix copy --offline --no-check-sigs --all --from file://$BUNDLE/res --to $PWD",
// deduplicate nix store
"nix store --offline --store $PWD optimise",
// make cache directory world-readable for autoetc
"chmod 0755 .",
}, workDir, bundle, pathSet, flagDropShell, cleanup)
if bundle.GPU {
withCacheDir(ctx, "mesa-wrappers", []string{
// link nixGL mesa wrappers
"mkdir -p nix/.nixGL",
"ln -s " + bundle.Mesa + "/bin/nixGLIntel nix/.nixGL/nixGL",
"ln -s " + bundle.Mesa + "/bin/nixVulkanIntel nix/.nixGL/nixVulkan",
}, workDir, bundle, pathSet, false, cleanup)
}
/*
Activate home-manager generation.
*/
withNixDaemon(ctx, "activate", []string{
// clean up broken links
"mkdir -p .local/state/{nix,home-manager}",
"chmod -R +w .local/state/{nix,home-manager}",
"rm -rf .local/state/{nix,home-manager}",
// run activation script
bundle.ActivationPackage + "/activate",
}, false, func(config *hst.Config) *hst.Config { return config },
bundle, pathSet, flagDropShellActivate, cleanup)
/*
Installation complete. Write metadata to block re-installs or downgrades.
*/
// serialise metadata to ensure consistency
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
} else if err = json.NewEncoder(f).Encode(bundle); err != nil {
cleanup()
log.Printf("cannot write metadata: %v", err)
return err
} else if err = f.Close(); err != nil {
log.Printf("cannot close metadata file: %v", err)
// not fatal
}
if err := os.Rename(pathSet.metaPath.String()+"~", pathSet.metaPath.String()); err != nil {
cleanup()
log.Printf("cannot rename metadata file: %v", err)
return err
}
cleanup()
return errSuccess
}).
Flag(&flagDropShellActivate, "s", command.BoolFlag(false), "Drop to a shell on activation")
}
{
var (
flagDropShellNixGL bool
flagAutoDrivers bool
)
c.NewCommand("start", "Start an application", func(args []string) error {
if len(args) < 1 {
log.Println("invalid argument")
return syscall.EINVAL
}
/*
Parse app metadata.
*/
id := args[0]
pathSet := pathSetByApp(id)
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
}
/*
Prepare nixGL.
*/
if a.GPU && flagAutoDrivers {
withNixDaemon(ctx, "nix-gl", []string{
"mkdir -p /nix/.nixGL/auto",
"rm -rf /nix/.nixGL/auto",
"export NIXPKGS_ALLOW_UNFREE=1",
"nix build --impure " +
"--out-link /nix/.nixGL/auto/opengl " +
"--override-input nixpkgs path:/etc/nixpkgs " +
"path:" + a.NixGL,
"nix build --impure " +
"--out-link /nix/.nixGL/auto/vulkan " +
"--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.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
}, a, pathSet, flagDropShellNixGL, func() {})
}
/*
Create app configuration.
*/
pathname := a.Launcher
argv := make([]string, 1, len(args))
if flagDropShell {
pathname = pathShell
argv[0] = bash
} else {
argv[0] = a.Launcher.String()
}
argv = append(argv, args[1:]...)
config := a.toHst(pathSet, pathname, argv, flagDropShell)
/*
Expose GPU devices.
*/
if a.GPU {
config.Container.Filesystem = append(config.Container.Filesystem,
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append(".nixGL"), Target: hst.AbsTmp.Append("nixGL")}})
appendGPUFilesystem(config)
}
/*
Spawn app.
*/
mustRunApp(ctx, config, func() {})
return errSuccess
}).
Flag(&flagDropShellNixGL, "s", command.BoolFlag(false), "Drop to a shell on nixGL build").
Flag(&flagAutoDrivers, "auto-drivers", command.BoolFlag(false), "Attempt automatic opengl driver detection")
}
c.MustParse(os.Args[1:], func(err error) {
hlog.Verbosef("command returned %v", err)
if errors.Is(err, errSuccess) {
hlog.BeforeExit()
os.Exit(0)
}
})
log.Fatal("unreachable")
}
+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}},
}...)
}
+60
View File
@@ -0,0 +1,60 @@
package main
import (
"context"
"encoding/json"
"errors"
"io"
"log"
"os"
"os/exec"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
)
var hakureiPath = internal.MustHakureiPath()
func mustRunApp(ctx context.Context, config *hst.Config, beforeFail func()) {
var (
cmd *exec.Cmd
st io.WriteCloser
)
if r, w, err := os.Pipe(); err != nil {
beforeFail()
log.Fatalf("cannot pipe: %v", err)
} else {
if hlog.Load() {
cmd = exec.CommandContext(ctx, hakureiPath, "-v", "app", "3")
} else {
cmd = exec.CommandContext(ctx, hakureiPath, "app", "3")
}
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.ExtraFiles = []*os.File{r}
st = w
}
go func() {
if err := json.NewEncoder(st).Encode(config); err != nil {
beforeFail()
log.Fatalf("cannot send configuration: %v", err)
}
}()
if err := cmd.Start(); err != nil {
beforeFail()
log.Fatalf("cannot start hakurei: %v", err)
}
if err := cmd.Wait(); err != nil {
var exitError *exec.ExitError
if errors.As(err, &exitError) {
beforeFail()
internal.Exit(exitError.ExitCode())
} else {
beforeFail()
log.Fatalf("cannot wait: %v", err)
}
}
}
+62
View File
@@ -0,0 +1,62 @@
{ pkgs, ... }:
{
users.users = {
alice = {
isNormalUser = true;
description = "Alice Foobar";
password = "foobar";
uid = 1000;
};
};
home-manager.users.alice.home.stateVersion = "24.11";
# Automatically login on tty1 as a normal user:
services.getty.autologinUser = "alice";
environment = {
variables = {
SWAYSOCK = "/tmp/sway-ipc.sock";
WLR_RENDERER = "pixman";
};
};
# Automatically configure and start Sway when logging in on tty1:
programs.bash.loginShellInit = ''
if [ "$(tty)" = "/dev/tty1" ]; then
set -e
mkdir -p ~/.config/sway
(sed s/Mod4/Mod1/ /etc/sway/config &&
echo 'output * bg ${pkgs.nixos-artwork.wallpapers.simple-light-gray.gnomeFilePath} fill' &&
echo 'output Virtual-1 res 1680x1050') > ~/.config/sway/config
sway --validate
systemd-cat --identifier=session sway && touch /tmp/sway-exit-ok
fi
'';
programs.sway.enable = true;
virtualisation = {
diskSize = 6 * 1024;
qemu.options = [
# Need to switch to a different GPU driver than the default one (-vga std) so that Sway can launch:
"-vga none -device virtio-gpu-pci"
# Increase zstd performance:
"-smp 8"
];
};
environment.hakurei = {
enable = true;
stateDir = "/var/lib/hakurei";
users.alice = 0;
extraHomeConfig = {
home.stateVersion = "23.05";
};
};
}
+34
View File
@@ -0,0 +1,34 @@
{
nixosTest,
callPackage,
system,
self,
}:
let
buildPackage = self.buildPackage.${system};
in
nixosTest {
name = "hpkg";
nodes.machine = {
environment.etc = {
"foot.pkg".source = callPackage ./foot.nix { inherit buildPackage; };
};
imports = [
./configuration.nix
self.nixosModules.hakurei
self.inputs.home-manager.nixosModules.home-manager
];
};
# adapted from nixos sway integration tests
# testScriptWithTypes:49: error: Cannot call function of unknown type
# (machine.succeed if succeed else machine.execute)(
# ^
# Found 1 error in 1 file (checked 1 source file)
skipTypeCheck = true;
testScript = builtins.readFile ./test.py;
}
+48
View File
@@ -0,0 +1,48 @@
{
lib,
buildPackage,
foot,
wayland-utils,
inconsolata,
}:
buildPackage {
name = "foot";
inherit (foot) version;
identity = 2;
id = "org.codeberg.dnkl.foot";
modules = [
{
home.packages = [
foot
# For wayland-info:
wayland-utils
];
}
];
nixosModules = [
{
# To help with OCR:
environment.etc."xdg/foot/foot.ini".text = lib.generators.toINI { } {
main = {
font = "inconsolata:size=14";
};
colors = rec {
foreground = "000000";
background = "ffffff";
regular2 = foreground;
};
};
fonts.packages = [ inconsolata ];
}
];
script = ''
exec foot "$@"
'';
}
+108
View File
@@ -0,0 +1,108 @@
import json
import shlex
q = shlex.quote
NODE_GROUPS = ["nodes", "floating_nodes"]
def swaymsg(command: str = "", succeed=True, type="command"):
assert command != "" or type != "command", "Must specify command or type"
shell = q(f"swaymsg -t {q(type)} -- {q(command)}")
with machine.nested(
f"sending swaymsg {shell!r}" + " (allowed to fail)" * (not succeed)
):
ret = (machine.succeed if succeed else machine.execute)(
f"su - alice -c {shell}"
)
# execute also returns a status code, but disregard.
if not succeed:
_, ret = ret
if not succeed and not ret:
return None
parsed = json.loads(ret)
return parsed
def walk(tree):
yield tree
for group in NODE_GROUPS:
for node in tree.get(group, []):
yield from walk(node)
def wait_for_window(pattern):
def func(last_chance):
nodes = (node["name"] for node in walk(swaymsg(type="get_tree")))
if last_chance:
nodes = list(nodes)
machine.log(f"Last call! Current list of windows: {nodes}")
return any(pattern in name for name in nodes)
retry(func)
def collect_state_ui(name):
swaymsg(f"exec hakurei ps > '/tmp/{name}.ps'")
machine.copy_from_vm(f"/tmp/{name}.ps", "")
swaymsg(f"exec hakurei --json ps > '/tmp/{name}.json'")
machine.copy_from_vm(f"/tmp/{name}.json", "")
machine.screenshot(name)
def check_state(name, enablements):
instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei --json ps"))
if len(instances) != 1:
raise Exception(f"unexpected state length {len(instances)}")
instance = next(iter(instances.values()))
config = instance['config']
if len(config['args']) != 1 or not (config['args'][0].startswith("/nix/store/")) or f"hakurei-{name}-" not in (config['args'][0]):
raise Exception(f"unexpected args {instance['config']['args']}")
if config['enablements'] != enablements:
raise Exception(f"unexpected enablements {instance['config']['enablements']}")
start_all()
machine.wait_for_unit("multi-user.target")
# To check hakurei's version:
print(machine.succeed("sudo -u alice -i hakurei version"))
# Wait for Sway to complete startup:
machine.wait_for_file("/run/user/1000/wayland-1")
machine.wait_for_file("/tmp/sway-ipc.sock")
# Prepare hpkg directory:
machine.succeed("install -dm 0700 -o alice -g users /var/lib/hakurei/1000")
# 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 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.0/tmpdir/2/success-client")
collect_state_ui("app_wayland")
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")
machine.wait_until_fails("pgrep foot")
# Verify acl cleanup on XDG_RUNTIME_DIR:
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002")
# Exit Sway and verify process exit status 0:
swaymsg("exit", succeed=False)
machine.wait_for_file("/tmp/sway-exit-ok")
# Print hakurei runDir contents:
print(machine.succeed("find /run/user/1000/hakurei"))
+108
View File
@@ -0,0 +1,108 @@
package main
import (
"context"
"strings"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/hst"
"hakurei.app/internal"
)
func withNixDaemon(
ctx context.Context,
action string, command []string, net bool, updateConfig func(config *hst.Config) *hst.Config,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
) {
mustRunAppDropShell(ctx, updateConfig(&hst.Config{
ID: app.ID,
Path: pathShell,
Args: []string{bash, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
// start nix-daemon
"nix-daemon --store / & " +
// wait for socket to appear
"(while [ ! -S /nix/var/nix/daemon-socket/socket ]; do sleep 0.01; done) && " +
// create directory so nix stops complaining
"mkdir -p /nix/var/nix/profiles/per-user/root/channels && " +
strings.Join(command, " && ") +
// terminate nix-daemon
" && pkill nix-daemon",
},
Username: "hakurei",
Shell: pathShell,
Home: pathDataData.Append(app.ID),
ExtraPerms: []*hst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
},
Identity: app.Identity,
Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action,
Userns: true, // nix sandbox requires userns
HostNet: net,
SeccompFlags: seccomp.AllowMultiarch,
Tty: dropShell,
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath, Target: pathNix, Write: true}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID), Source: pathSet.homeDir, Write: true, Ensure: true}},
},
},
}), dropShell, beforeFail)
}
func withCacheDir(
ctx context.Context,
action string, command []string, workDir *container.Absolute,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
mustRunAppDropShell(ctx, &hst.Config{
ID: app.ID,
Path: pathShell,
Args: []string{bash, "-lc", strings.Join(command, " && ")},
Username: "nixos",
Shell: pathShell,
Home: pathDataData.Append(app.ID, "cache"),
ExtraPerms: []*hst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
{Path: workDir, Execute: true},
},
Identity: app.Identity,
Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action,
SeccompFlags: seccomp.AllowMultiarch,
Tty: dropShell,
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}},
},
},
}, dropShell, beforeFail)
}
func mustRunAppDropShell(ctx context.Context, config *hst.Config, dropShell bool, beforeFail func()) {
if dropShell {
config.Args = []string{bash, "-l"}
mustRunApp(ctx, config, beforeFail)
beforeFail()
internal.Exit(0)
}
mustRunApp(ctx, config, beforeFail)
}
+146
View File
@@ -0,0 +1,146 @@
package main
import (
"bytes"
"fmt"
"log"
"os"
"path"
"slices"
"strconv"
"strings"
"syscall"
)
const (
hsuConfFile = "/etc/hsurc"
envShim = "HAKUREI_SHIM"
envAID = "HAKUREI_APP_ID"
envGroups = "HAKUREI_GROUPS"
PR_SET_NO_NEW_PRIVS = 0x26
)
func main() {
log.SetFlags(0)
log.SetPrefix("hsu: ")
log.SetOutput(os.Stderr)
if os.Geteuid() != 0 {
log.Fatal("this program must be owned by uid 0 and have the setuid bit set")
}
puid := os.Getuid()
if puid == 0 {
log.Fatal("this program must not be started by root")
}
var toolPath string
pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
if p, err := os.Readlink(pexe); err != nil {
log.Fatalf("cannot read parent executable path: %v", err)
} else if strings.HasSuffix(p, " (deleted)") {
log.Fatal("hakurei executable has been deleted")
} else if p != mustCheckPath(hmain) {
log.Fatal("this program must be started by hakurei")
} else {
toolPath = p
}
// uid = 1000000 +
// fid * 10000 +
// aid
uid := 1000000
// refuse to run if hsurc is not protected correctly
if s, err := os.Stat(hsuConfFile); err != nil {
log.Fatal(err)
} else if s.Mode().Perm() != 0400 {
log.Fatal("bad hsurc perm")
} else if st := s.Sys().(*syscall.Stat_t); st.Uid != 0 || st.Gid != 0 {
log.Fatal("hsurc must be owned by uid 0")
}
// authenticate before accepting user input
if f, err := os.Open(hsuConfFile); err != nil {
log.Fatal(err)
} else if fid, ok := mustParseConfig(f, puid); !ok {
log.Fatalf("uid %d is not in the hsurc file", puid)
} else {
uid += fid * 10000
}
// allowed aid range 0 to 9999
if as, ok := os.LookupEnv(envAID); !ok {
log.Fatal("HAKUREI_APP_ID not set")
} else if aid, err := parseUint32Fast(as); err != nil || aid < 0 || aid > 9999 {
log.Fatal("invalid aid")
} else {
uid += aid
}
// pass through setup fd to shim
var shimSetupFd string
if s, ok := os.LookupEnv(envShim); !ok {
// hakurei requests target uid
// print resolved uid and exit
fmt.Print(uid)
os.Exit(0)
} else if len(s) != 1 || s[0] > '9' || s[0] < '3' {
log.Fatal("HAKUREI_SHIM holds an invalid value")
} else {
shimSetupFd = s
}
// supplementary groups
var suppGroups, suppCurrent []int
if gs, ok := os.LookupEnv(envGroups); ok {
if cur, err := os.Getgroups(); err != nil {
log.Fatalf("cannot get groups: %v", err)
} else {
suppCurrent = cur
}
// parse space-separated list of group ids
gss := bytes.Split([]byte(gs), []byte{' '})
suppGroups = make([]int, len(gss)+1)
for i, s := range gss {
if gid, err := strconv.Atoi(string(s)); err != nil {
log.Fatalf("cannot parse %q: %v", string(s), err)
} else if gid > 0 && gid != uid && gid != os.Getgid() && slices.Contains(suppCurrent, gid) {
suppGroups[i] = gid
} else {
log.Fatalf("invalid gid %d", gid)
}
}
suppGroups[len(suppGroups)-1] = uid
} else {
suppGroups = []int{uid}
}
// final bounds check to catch any bugs
if uid < 1000000 || uid >= 2000000 {
panic("uid out of bounds")
}
// careful! users in the allowlist is effectively allowed to drop groups via hsu
if err := syscall.Setresgid(uid, uid, uid); err != nil {
log.Fatalf("cannot set gid: %v", err)
}
if err := syscall.Setgroups(suppGroups); err != nil {
log.Fatalf("cannot set supplementary groups: %v", err)
}
if err := syscall.Setresuid(uid, uid, uid); err != nil {
log.Fatalf("cannot set uid: %v", err)
}
if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 {
log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
}
if err := syscall.Exec(toolPath, []string{"hakurei", "shim"}, []string{envShim + "=" + shimSetupFd}); err != nil {
log.Fatalf("cannot start shim: %v", err)
}
panic("unreachable")
}
+23
View File
@@ -0,0 +1,23 @@
{
lib,
buildGoModule,
hakurei ? abort "hakurei package required",
}:
buildGoModule {
pname = "${hakurei.pname}-hsu";
inherit (hakurei) version;
src = ./.;
inherit (hakurei) vendorHash;
env.CGO_ENABLED = 0;
preBuild = ''
go mod init hsu >& /dev/null
'';
ldflags = lib.attrsets.foldlAttrs (
ldflags: name: value:
ldflags ++ [ "-X main.${name}=${value}" ]
) [ "-s -w" ] { hmain = "${hakurei}/libexec/hakurei"; };
}
+67
View File
@@ -0,0 +1,67 @@
package main
import (
"bufio"
"errors"
"fmt"
"io"
"log"
"strings"
)
func parseUint32Fast(s string) (int, error) {
sLen := len(s)
if sLen < 1 {
return -1, errors.New("zero length string")
}
if sLen > 10 {
return -1, errors.New("string too long")
}
n := 0
for i, ch := range []byte(s) {
ch -= '0'
if ch > 9 {
return -1, fmt.Errorf("invalid character '%s' at index %d", string(ch+'0'), i)
}
n = n*10 + int(ch)
}
return n, nil
}
func parseConfig(r io.Reader, puid int) (fid int, ok bool, err error) {
s := bufio.NewScanner(r)
var line, puid0 int
for s.Scan() {
line++
// <puid> <fid>
lf := strings.SplitN(s.Text(), " ", 2)
if len(lf) != 2 {
return -1, false, fmt.Errorf("invalid entry on line %d", line)
}
puid0, err = parseUint32Fast(lf[0])
if err != nil || puid0 < 1 {
return -1, false, fmt.Errorf("invalid parent uid on line %d", line)
}
ok = puid0 == puid
if ok {
// allowed fid range 0 to 99
if fid, err = parseUint32Fast(lf[1]); err != nil || fid < 0 || fid > 99 {
return -1, false, fmt.Errorf("invalid identity on line %d", line)
}
return
}
}
return -1, false, s.Err()
}
func mustParseConfig(r io.Reader, puid int) (int, bool) {
fid, ok, err := parseConfig(r, puid)
if err != nil {
log.Fatal(err)
}
return fid, ok
}
+96
View File
@@ -0,0 +1,96 @@
package main
import (
"bytes"
"strconv"
"testing"
)
func Test_parseUint32Fast(t *testing.T) {
t.Run("zero-length", func(t *testing.T) {
if _, err := parseUint32Fast(""); err == nil || err.Error() != "zero length string" {
t.Errorf(`parseUint32Fast(""): error = %v`, err)
return
}
})
t.Run("overflow", func(t *testing.T) {
if _, err := parseUint32Fast("10000000000"); err == nil || err.Error() != "string too long" {
t.Errorf("parseUint32Fast: error = %v", err)
return
}
})
t.Run("invalid byte", func(t *testing.T) {
if _, err := parseUint32Fast("meow"); err == nil || err.Error() != "invalid character 'm' at index 0" {
t.Errorf(`parseUint32Fast("meow"): error = %v`, err)
return
}
})
t.Run("full range", func(t *testing.T) {
testRange := func(i, end int) {
for ; i < end; i++ {
s := strconv.Itoa(i)
w := i
t.Run("parse "+s, func(t *testing.T) {
t.Parallel()
v, err := parseUint32Fast(s)
if err != nil {
t.Errorf("parseUint32Fast(%q): error = %v",
s, err)
return
}
if v != w {
t.Errorf("parseUint32Fast(%q): got %v",
s, v)
return
}
})
}
}
testRange(0, 5000)
testRange(105000, 110000)
testRange(23005000, 23010000)
testRange(456005000, 456010000)
testRange(7890005000, 7890010000)
})
}
func Test_parseConfig(t *testing.T) {
testCases := []struct {
name string
puid, want int
wantErr string
rc string
}{
{"empty", 0, -1, "", ``},
{"invalid field", 0, -1, "invalid entry on line 1", `9`},
{"invalid puid", 0, -1, "invalid parent uid on line 1", `f 9`},
{"invalid fid", 1000, -1, "invalid identity on line 1", `1000 f`},
{"match", 1000, 0, "", `1000 0`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
fid, ok, err := parseConfig(bytes.NewBufferString(tc.rc), tc.puid)
if err == nil && tc.wantErr != "" {
t.Errorf("parseConfig: error = %v; wantErr %q",
err, tc.wantErr)
return
}
if err != nil && err.Error() != tc.wantErr {
t.Errorf("parseConfig: error = %q; wantErr %q",
err, tc.wantErr)
return
}
if ok == (tc.want == -1) {
t.Errorf("parseConfig: ok = %v; want %v",
ok, tc.want)
return
}
if fid != tc.want {
t.Errorf("parseConfig: fid = %v; want %v",
fid, tc.want)
}
})
}
}
+20
View File
@@ -0,0 +1,20 @@
package main
import (
"log"
"path"
)
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
var (
hmain = compPoison
)
func mustCheckPath(p string) string {
if p != compPoison && p != "" && path.IsAbs(p) {
return p
}
log.Fatal("this program is compiled incorrectly")
return compPoison
}
+65
View File
@@ -0,0 +1,65 @@
package command
import (
"flag"
"fmt"
"io"
)
// New initialises a root Node.
func New(output io.Writer, logf LogFunc, name string, early HandlerFunc) Command {
c := rootNode{newNode(output, logf, name, "")}
c.f = early
return c
}
func newNode(output io.Writer, logf LogFunc, name, usage string) *node {
n := &node{
name: name, usage: usage,
out: output, logf: logf,
set: flag.NewFlagSet(name, flag.ContinueOnError),
}
n.set.SetOutput(output)
n.set.Usage = func() {
_ = n.writeHelp()
if n.suffix.Len() > 0 {
_, _ = fmt.Fprintln(output, "Flags:")
n.set.PrintDefaults()
_, _ = fmt.Fprintln(output)
}
}
return n
}
func (n *node) Command(name, usage string, f HandlerFunc) Node {
n.NewCommand(name, usage, f)
return n
}
func (n *node) NewCommand(name, usage string, f HandlerFunc) Flag[Node] {
if f == nil {
panic("invalid handler")
}
if name == "" || usage == "" {
panic("invalid subcommand")
}
s := newNode(n.out, n.logf, name, usage)
s.f = f
if !n.adopt(s) {
panic("attempted to initialise subcommand with non-unique name")
}
return s
}
func (n *node) New(name, usage string) Node {
if name == "" || usage == "" {
panic("invalid subcommand tree")
}
s := newNode(n.out, n.logf, name, usage)
if !n.adopt(s) {
panic("attempted to initialise subcommand tree with non-unique name")
}
return s
}
+56
View File
@@ -0,0 +1,56 @@
package command_test
import (
"testing"
"hakurei.app/command"
)
func TestBuild(t *testing.T) {
c := command.New(nil, nil, "test", nil)
stubHandler := func([]string) error { panic("unreachable") }
t.Run("nil direct handler", func(t *testing.T) {
defer checkRecover(t, "Command", "invalid handler")
c.Command("name", "usage", nil)
})
t.Run("direct zero length", func(t *testing.T) {
wantPanic := "invalid subcommand"
t.Run("zero length name", func(t *testing.T) { defer checkRecover(t, "Command", wantPanic); c.Command("", "usage", stubHandler) })
t.Run("zero length usage", func(t *testing.T) { defer checkRecover(t, "Command", wantPanic); c.Command("name", "", stubHandler) })
})
t.Run("direct adopt unique names", func(t *testing.T) {
c.Command("d0", "usage", stubHandler)
c.Command("d1", "usage", stubHandler)
})
t.Run("direct adopt non-unique name", func(t *testing.T) {
defer checkRecover(t, "Command", "attempted to initialise subcommand with non-unique name")
c.Command("d0", "usage", stubHandler)
})
t.Run("zero length", func(t *testing.T) {
wantPanic := "invalid subcommand tree"
t.Run("zero length name", func(t *testing.T) { defer checkRecover(t, "New", wantPanic); c.New("", "usage") })
t.Run("zero length usage", func(t *testing.T) { defer checkRecover(t, "New", wantPanic); c.New("name", "") })
})
t.Run("direct adopt unique names", func(t *testing.T) {
c.New("t0", "usage")
c.New("t1", "usage")
})
t.Run("direct adopt non-unique name", func(t *testing.T) {
defer checkRecover(t, "Command", "attempted to initialise subcommand tree with non-unique name")
c.New("t0", "usage")
})
}
func checkRecover(t *testing.T, name, wantPanic string) {
if r := recover(); r != wantPanic {
t.Errorf("%s: panic = %v; wantPanic %v",
name, r, wantPanic)
}
}
+55
View File
@@ -0,0 +1,55 @@
// Package command implements generic nested command parsing.
package command
import (
"flag"
"strings"
)
// UsageInternal causes the command to be hidden from help text when set as the usage string.
const UsageInternal = "internal"
type (
// HandlerFunc is called when matching a directly handled subcommand tree.
HandlerFunc = func(args []string) error
// LogFunc is the function signature of a printf function.
LogFunc = func(format string, a ...any)
// FlagDefiner is a deferred flag definer value, usually encapsulating the default value.
FlagDefiner interface {
// Define defines the flag in set.
Define(b *strings.Builder, set *flag.FlagSet, p any, name, usage string)
}
Flag[T any] interface {
// Flag defines a generic flag type in Node's flag set.
Flag(p any, name string, value FlagDefiner, usage string) T
}
Command interface {
Parse(arguments []string) error
// MustParse determines exit outcomes for Parse errors
// and calls handleError if [HandlerFunc] returns a non-nil error.
MustParse(arguments []string, handleError func(error))
baseNode[Command]
}
Node baseNode[Node]
baseNode[T any] interface {
// Command appends a subcommand with direct command handling.
Command(name, usage string, f HandlerFunc) T
// New returns a new subcommand tree.
New(name, usage string) (sub Node)
// NewCommand returns a new subcommand with direct command handling.
NewCommand(name, usage string, f HandlerFunc) (sub Flag[Node])
// PrintHelp prints a help message to the configured writer.
PrintHelp()
Flag[T]
}
)
+77
View File
@@ -0,0 +1,77 @@
package command
import (
"errors"
"flag"
"strings"
)
// FlagError wraps errors returned by [flag].
type FlagError struct{ error }
func (e FlagError) Success() bool { return errors.Is(e.error, flag.ErrHelp) }
func (e FlagError) Is(target error) bool {
return (e.error == nil && target == nil) ||
((e.error != nil && target != nil) && e.error.Error() == target.Error())
}
func (n *node) Flag(p any, name string, value FlagDefiner, usage string) Node {
value.Define(&n.suffix, n.set, p, name, usage)
return n
}
// StringFlag is the default value of a string flag.
type StringFlag string
func (v StringFlag) Define(b *strings.Builder, set *flag.FlagSet, p any, name, usage string) {
set.StringVar(p.(*string), name, string(v), usage)
b.WriteString(" [" + prettyFlag(name) + " <value>]")
}
// IntFlag is the default value of an int flag.
type IntFlag int
func (v IntFlag) Define(b *strings.Builder, set *flag.FlagSet, p any, name, usage string) {
set.IntVar(p.(*int), name, int(v), usage)
b.WriteString(" [" + prettyFlag(name) + " <int>]")
}
// BoolFlag is the default value of a bool flag.
type BoolFlag bool
func (v BoolFlag) Define(b *strings.Builder, set *flag.FlagSet, p any, name, usage string) {
set.BoolVar(p.(*bool), name, bool(v), usage)
b.WriteString(" [" + prettyFlag(name) + "]")
}
// RepeatableFlag implements an ordered, repeatable string flag.
type RepeatableFlag []string
func (r *RepeatableFlag) String() string {
if r == nil {
return "<nil>"
}
return strings.Join(*r, " ")
}
func (r *RepeatableFlag) Set(v string) error {
*r = append(*r, v)
return nil
}
func (r *RepeatableFlag) Define(b *strings.Builder, set *flag.FlagSet, _ any, name, usage string) {
set.Var(r, name, usage)
b.WriteString(" [" + prettyFlag(name) + " <value>]")
}
// this has no effect on parse outcome
func prettyFlag(name string) string {
switch len(name) {
case 0:
panic("zero length flag name")
case 1:
return "-" + name
default:
return "--" + name
}
}
+53
View File
@@ -0,0 +1,53 @@
package command
import (
"errors"
"fmt"
"io"
"strings"
"text/tabwriter"
)
var ErrHelp = errors.New("help requested")
func (n *node) PrintHelp() { _ = n.writeHelp() }
func (n *node) writeHelp() error {
if _, err := fmt.Fprintf(n.out,
"\nUsage:\t%s [-h | --help]%s COMMAND [OPTIONS]\n",
strings.Join(append(n.prefix, n.name), " "), &n.suffix,
); err != nil {
return err
}
if n.child != nil {
if _, err := fmt.Fprint(n.out, "\nCommands:\n"); err != nil {
return err
}
}
tw := tabwriter.NewWriter(n.out, 0, 1, 4, ' ', 0)
if err := n.child.writeCommands(tw); err != nil {
return err
}
if err := tw.Flush(); err != nil {
return err
}
_, err := n.out.Write([]byte{'\n'})
if err == nil {
err = ErrHelp
}
return err
}
func (n *node) writeCommands(w io.Writer) error {
if n == nil {
return nil
}
if n.usage != UsageInternal {
if _, err := fmt.Fprintf(w, "\t%s\t%s\n", n.name, n.usage); err != nil {
return err
}
}
return n.next.writeCommands(w)
}
+40
View File
@@ -0,0 +1,40 @@
package command
import (
"flag"
"io"
"strings"
)
type node struct {
child, next *node
name, usage string
out io.Writer
logf LogFunc
prefix []string
suffix strings.Builder
f HandlerFunc
set *flag.FlagSet
}
func (n *node) adopt(v *node) bool {
if n.child != nil {
return n.child.append(v)
}
n.child = v
return true
}
func (n *node) append(v *node) bool {
if n.name == v.name {
return false
}
if n.next != nil {
return n.next.append(v)
}
n.next = v
return true
}
+105
View File
@@ -0,0 +1,105 @@
package command
import (
"errors"
"log"
"os"
)
var (
ErrEmptyTree = errors.New("subcommand tree has no nodes")
ErrNoMatch = errors.New("did not match any subcommand")
)
func (n *node) Parse(arguments []string) error {
if n.usage == "" { // root node has zero length usage
if n.next != nil {
panic("invalid toplevel state")
}
goto match
}
if len(arguments) == 0 {
// unreachable: zero length args cause upper level to return with a help message
panic("attempted to parse with zero length args")
}
if arguments[0] != n.name {
if n.next == nil {
n.printf("%q is not a valid command", arguments[0])
return ErrNoMatch
}
n.next.prefix = n.prefix
return n.next.Parse(arguments)
}
arguments = arguments[1:]
match:
if n.child != nil {
// propagate help prefix early: flag set usage dereferences help
n.child.prefix = append(n.prefix, n.name)
}
if n.set.Parsed() {
panic("invalid set state")
}
if err := n.set.Parse(arguments); err != nil {
return FlagError{err}
}
args := n.set.Args()
if n.child != nil {
if n.f != nil {
if n.usage != "" { // root node early special case
panic("invalid subcommand tree state")
}
// special case: root node calls HandlerFunc for initialisation
if err := n.f(nil); err != nil {
return err
}
}
if len(args) == 0 {
return n.writeHelp()
}
return n.child.Parse(args)
}
if n.f == nil {
n.printf("%q has no subcommands", n.name)
return ErrEmptyTree
}
return n.f(args)
}
func (n *node) printf(format string, a ...any) {
if n.logf == nil {
log.Printf(format, a...)
} else {
n.logf(format, a...)
}
}
func (n *node) MustParse(arguments []string, handleError func(error)) {
switch err := n.Parse(arguments); err {
case nil:
return
case ErrHelp:
os.Exit(0)
case ErrNoMatch:
os.Exit(1)
case ErrEmptyTree:
os.Exit(1)
default:
var flagError FlagError
if !errors.As(err, &flagError) { // returned by HandlerFunc
handleError(err)
os.Exit(1)
}
if flagError.Success() {
os.Exit(0)
}
os.Exit(1)
}
}
+344
View File
@@ -0,0 +1,344 @@
package command_test
import (
"bytes"
"errors"
"flag"
"fmt"
"io"
"log"
"strings"
"testing"
"hakurei.app/command"
)
func TestParse(t *testing.T) {
testCases := []struct {
name string
buildTree func(wout, wlog io.Writer) command.Command
args []string
want string
wantLog string
wantErr error
}{
{
"d=0 empty sub",
func(wout, wlog io.Writer) command.Command { return command.New(wout, newLogFunc(wlog), "root", nil) },
[]string{""},
"", "test: \"root\" has no subcommands\n", command.ErrEmptyTree,
},
{
"d=0 empty sub garbage",
func(wout, wlog io.Writer) command.Command { return command.New(wout, newLogFunc(wlog), "root", nil) },
[]string{"a", "b", "c", "d"},
"", "test: \"root\" has no subcommands\n", command.ErrEmptyTree,
},
{
"d=0 no match",
buildTestCommand,
[]string{"nonexistent"},
"", "test: \"nonexistent\" is not a valid command\n", command.ErrNoMatch,
},
{
"d=0 direct error",
buildTestCommand,
[]string{"error"},
"", "", errSuccess,
},
{
"d=0 direct error garbage",
buildTestCommand,
[]string{"error", "0", "1", "2"},
"", "", errSuccess,
},
{
"d=0 direct success out of order",
buildTestCommand,
[]string{"succeed"},
"", "", nil,
},
{
"d=0 direct success output",
buildTestCommand,
[]string{"print", "0", "1", "2"},
"012", "", nil,
},
{
"d=0 out of order string flag",
buildTestCommand,
[]string{"string", "--string", "64d3b4b7b21788585845060e2199a78f"},
"flag provided but not defined: -string\n\nUsage:\ttest string [-h | --help] COMMAND [OPTIONS]\n\n", "",
errors.New("flag provided but not defined: -string"),
},
{
"d=0 string flag",
buildTestCommand,
[]string{"--string", "64d3b4b7b21788585845060e2199a78f", "string"},
"64d3b4b7b21788585845060e2199a78f", "", nil,
},
{
"d=0 int flag",
buildTestCommand,
[]string{"--int", "2147483647", "int"},
"2147483647", "", nil,
},
{
"d=0 repeat flag",
buildTestCommand,
[]string{"--repeat", "0", "--repeat", "1", "--repeat", "2", "--repeat", "3", "--repeat", "4", "repeat"},
"[0 1 2 3 4]", "", nil,
},
{
"d=0 bool flag",
buildTestCommand,
[]string{"-v", "succeed"},
"", "test: verbose\n", nil,
},
{
"d=0 bool flag early error",
buildTestCommand,
[]string{"--fail", "succeed"},
"", "", errSuccess,
},
{
"d=1 empty sub",
buildTestCommand,
[]string{"empty"},
"", "test: \"empty\" has no subcommands\n", command.ErrEmptyTree,
},
{
"d=1 empty sub garbage",
buildTestCommand,
[]string{"empty", "a", "b", "c", "d"},
"", "test: \"empty\" has no subcommands\n", command.ErrEmptyTree,
},
{
"d=1 empty sub help",
buildTestCommand,
[]string{"empty", "-h"},
"\nUsage:\ttest empty [-h | --help] COMMAND [OPTIONS]\n\n", "", flag.ErrHelp,
},
{
"d=1 no match",
buildTestCommand,
[]string{"join", "23aa3bb0", "34986782", "d8859355", "cd9ac317", ", "},
"", "test: \"23aa3bb0\" is not a valid command\n", command.ErrNoMatch,
},
{
"d=1 direct success out",
buildTestCommand,
[]string{"join", "out", "23aa3bb0", "34986782", "d8859355", "cd9ac317", ", "},
"23aa3bb0, 34986782, d8859355, cd9ac317", "", nil,
},
{
"d=1 direct success log",
buildTestCommand,
[]string{"join", "log", "23aa3bb0", "34986782", "d8859355", "cd9ac317", ", "},
"", "test: 23aa3bb0, 34986782, d8859355, cd9ac317\n", nil,
},
{
"d=4 empty sub",
buildTestCommand,
[]string{"deep", "d=2", "d=3", "d=4"},
"", "test: \"d=4\" has no subcommands\n", command.ErrEmptyTree},
{
"d=0 help",
buildTestCommand,
[]string{},
`
Usage: test [-h | --help] [-v] [--fail] [--string <value>] [--int <int>] [--repeat <value>] COMMAND [OPTIONS]
Commands:
error return an error
print wraps Fprint
string print string passed by flag
int print int passed by flag
repeat print repeated values passed by flag
empty empty subcommand
join wraps strings.Join
succeed this command succeeds
deep top level of command tree with various levels
`, "", command.ErrHelp,
},
{
"d=0 help flag",
buildTestCommand,
[]string{"-h"},
`
Usage: test [-h | --help] [-v] [--fail] [--string <value>] [--int <int>] [--repeat <value>] COMMAND [OPTIONS]
Commands:
error return an error
print wraps Fprint
string print string passed by flag
int print int passed by flag
repeat print repeated values passed by flag
empty empty subcommand
join wraps strings.Join
succeed this command succeeds
deep top level of command tree with various levels
Flags:
-fail
fail early
-int int
store value for the "int" command (default -1)
-repeat value
store value for the "repeat" command
-string string
store value for the "string" command (default "default")
-v verbose output
`, "", flag.ErrHelp,
},
{
"d=1 help",
buildTestCommand,
[]string{"join"},
`
Usage: test join [-h | --help] COMMAND [OPTIONS]
Commands:
out write result to wout
log log result to wlog
`, "", command.ErrHelp,
},
{
"d=1 help flag",
buildTestCommand,
[]string{"join", "-h"},
`
Usage: test join [-h | --help] COMMAND [OPTIONS]
Commands:
out write result to wout
log log result to wlog
`, "", flag.ErrHelp,
},
{
"d=2 help",
buildTestCommand,
[]string{"deep", "d=2"},
`
Usage: test deep d=2 [-h | --help] COMMAND [OPTIONS]
Commands:
d=3 relative third level
`, "", command.ErrHelp,
},
{
"d=2 help flag",
buildTestCommand,
[]string{"deep", "d=2", "-h"},
`
Usage: test deep d=2 [-h | --help] COMMAND [OPTIONS]
Commands:
d=3 relative third level
`, "", flag.ErrHelp,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
wout, wlog := new(bytes.Buffer), new(bytes.Buffer)
c := tc.buildTree(wout, wlog)
if err := c.Parse(tc.args); !errors.Is(err, tc.wantErr) {
t.Errorf("Parse: error = %v; wantErr %v", err, tc.wantErr)
}
if got := wout.String(); got != tc.want {
t.Errorf("Parse: %s want %s", got, tc.want)
}
if gotLog := wlog.String(); gotLog != tc.wantLog {
t.Errorf("Parse: log = %s wantLog %s", gotLog, tc.wantLog)
}
})
}
}
var (
errJoinLen = errors.New("not enough arguments to join")
errSuccess = errors.New("success")
)
func buildTestCommand(wout, wlog io.Writer) (c command.Command) {
var (
flagVerbose bool
flagFail bool
flagString string
flagInt int
flagRepeat command.RepeatableFlag
)
logf := newLogFunc(wlog)
c = command.New(wout, logf, "test", func([]string) error {
if flagVerbose {
logf("verbose")
}
if flagFail {
return errSuccess
}
return nil
}).
Flag(&flagVerbose, "v", command.BoolFlag(false), "verbose output").
Flag(&flagFail, "fail", command.BoolFlag(false), "fail early").
Command("error", "return an error", func([]string) error {
return errSuccess
}).
Command("print", "wraps Fprint", func(args []string) error {
a := make([]any, len(args))
for i, v := range args {
a[i] = v
}
_, err := fmt.Fprint(wout, a...)
return err
}).
Flag(&flagString, "string", command.StringFlag("default"), "store value for the \"string\" command").
Command("string", "print string passed by flag", func(args []string) error { _, err := fmt.Fprint(wout, flagString); return err }).
Flag(&flagInt, "int", command.IntFlag(-1), "store value for the \"int\" command").
Command("int", "print int passed by flag", func(args []string) error { _, err := fmt.Fprint(wout, flagInt); return err }).
Flag(nil, "repeat", &flagRepeat, "store value for the \"repeat\" command").
Command("repeat", "print repeated values passed by flag", func(args []string) error { _, err := fmt.Fprint(wout, flagRepeat); return err })
c.New("empty", "empty subcommand")
c.New("hidden", command.UsageInternal)
c.New("join", "wraps strings.Join").
Command("out", "write result to wout", func(args []string) error {
if len(args) == 0 {
return errJoinLen
}
_, err := fmt.Fprint(wout, strings.Join(args[:len(args)-1], args[len(args)-1]))
return err
}).
Command("log", "log result to wlog", func(args []string) error {
if len(args) == 0 {
return errJoinLen
}
logf("%s", strings.Join(args[:len(args)-1], args[len(args)-1]))
return nil
})
c.Command("succeed", "this command succeeds", func([]string) error { return nil })
c.New("deep", "top level of command tree with various levels").
New("d=2", "relative second level").
New("d=3", "relative third level").
New("d=4", "relative fourth level")
return
}
func newLogFunc(w io.Writer) command.LogFunc { return log.New(w, "test: ", 0).Printf }
+54
View File
@@ -0,0 +1,54 @@
package command
import (
"flag"
"testing"
)
func TestParseUnreachable(t *testing.T) {
// top level bypasses name matching and recursive calls to Parse
// returns when encountering zero-length args
t.Run("zero-length args", func(t *testing.T) {
defer checkRecover(t, "Parse", "attempted to parse with zero length args")
_ = newNode(panicWriter{}, nil, " ", " ").Parse(nil)
})
// top level must not have siblings
t.Run("toplevel siblings", func(t *testing.T) {
defer checkRecover(t, "Parse", "invalid toplevel state")
n := newNode(panicWriter{}, nil, " ", "")
n.append(newNode(panicWriter{}, nil, " ", " "))
_ = n.Parse(nil)
})
// a node with descendents must not have a direct handler
t.Run("sub handle conflict", func(t *testing.T) {
defer checkRecover(t, "Parse", "invalid subcommand tree state")
n := newNode(panicWriter{}, nil, " ", " ")
n.adopt(newNode(panicWriter{}, nil, " ", " "))
n.f = func([]string) error { panic("unreachable") }
_ = n.Parse([]string{" "})
})
// this would only happen if a node was matched twice
t.Run("parsed flag set", func(t *testing.T) {
defer checkRecover(t, "Parse", "invalid set state")
n := newNode(panicWriter{}, nil, " ", "")
set := flag.NewFlagSet("parsed", flag.ContinueOnError)
set.SetOutput(panicWriter{})
_ = set.Parse(nil)
n.set = set
_ = n.Parse(nil)
})
}
type panicWriter struct{}
func (p panicWriter) Write([]byte) (int, error) { panic("unreachable") }
func checkRecover(t *testing.T, name, wantPanic string) {
if r := recover(); r != wantPanic {
t.Errorf("%s: panic = %v; wantPanic %v",
name, r, wantPanic)
}
}
+14
View File
@@ -0,0 +1,14 @@
package command
// the top level node wants [Command] returned for its builder methods
type rootNode struct{ *node }
func (r rootNode) Command(name, usage string, f HandlerFunc) Command {
r.node.Command(name, usage, f)
return r
}
func (r rootNode) Flag(p any, name string, value FlagDefiner, usage string) Command {
r.node.Flag(p, name, value, usage)
return r
}
-136
View File
@@ -1,136 +0,0 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"git.ophivana.moe/cat/fortify/dbus"
"git.ophivana.moe/cat/fortify/internal"
"git.ophivana.moe/cat/fortify/internal/app"
"git.ophivana.moe/cat/fortify/internal/system"
)
var (
printTemplate bool
confPath string
dbusConfigSession string
dbusConfigSystem string
dbusID string
mpris bool
dbusVerbose bool
userName string
enablements [system.ELen]bool
launchMethodText string
)
func init() {
flag.BoolVar(&printTemplate, "template", false, "Print a full config template and exit")
// config file, disables every other flag here
flag.StringVar(&confPath, "c", "nil", "Path to full app configuration, or \"nil\" to configure from flags")
flag.StringVar(&dbusConfigSession, "dbus-config", "builtin", "Path to D-Bus proxy config file, or \"builtin\" for defaults")
flag.StringVar(&dbusConfigSystem, "dbus-system", "nil", "Path to system D-Bus proxy config file, or \"nil\" to disable")
flag.StringVar(&dbusID, "dbus-id", "", "D-Bus ID of application, leave empty to disable own paths, has no effect if custom config is available")
flag.BoolVar(&mpris, "mpris", false, "Allow owning MPRIS D-Bus path, has no effect if custom config is available")
flag.BoolVar(&dbusVerbose, "dbus-log", false, "Force logging in the D-Bus proxy")
flag.StringVar(&userName, "u", "chronos", "Passwd name of user to run as")
flag.BoolVar(&enablements[system.EWayland], "wayland", false, "Share Wayland socket")
flag.BoolVar(&enablements[system.EX11], "X", false, "Share X11 socket and allow connection")
flag.BoolVar(&enablements[system.EDBus], "dbus", false, "Proxy D-Bus connection")
flag.BoolVar(&enablements[system.EPulse], "pulse", false, "Share PulseAudio socket and cookie")
}
func init() {
methodHelpString := "Method of launching the child process, can be one of \"sudo\""
if internal.SdBootedV {
methodHelpString += ", \"systemd\""
}
flag.StringVar(&launchMethodText, "method", "sudo", methodHelpString)
}
func tryTemplate() {
if printTemplate {
if s, err := json.MarshalIndent(app.Template(), "", " "); err != nil {
fatalf("cannot generate template: %v", err)
panic("unreachable")
} else {
fmt.Println(string(s))
}
os.Exit(0)
}
}
func loadConfig() *app.Config {
if confPath == "nil" {
// config from flags
return configFromFlags()
} else {
// config from file
c := new(app.Config)
if f, err := os.Open(confPath); err != nil {
fatalf("cannot access config file '%s': %s\n", confPath, err)
panic("unreachable")
} else if err = json.NewDecoder(f).Decode(&c); err != nil {
fatalf("cannot parse config file '%s': %s\n", confPath, err)
panic("unreachable")
} else {
return c
}
}
}
func configFromFlags() (config *app.Config) {
// initialise config from flags
config = &app.Config{
ID: dbusID,
User: userName,
Command: flag.Args(),
Method: launchMethodText,
}
// enablements from flags
for i := system.Enablement(0); i < system.Enablement(system.ELen); i++ {
if enablements[i] {
config.Confinement.Enablements.Set(i)
}
}
// parse D-Bus config file from flags if applicable
if enablements[system.EDBus] {
if dbusConfigSession == "builtin" {
config.Confinement.SessionBus = dbus.NewConfig(dbusID, true, mpris)
} else {
if c, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
fatalf("cannot load session bus proxy config from %q: %s\n", dbusConfigSession, err)
} else {
config.Confinement.SessionBus = c
}
}
// system bus proxy is optional
if dbusConfigSystem != "nil" {
if c, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil {
fatalf("cannot load system bus proxy config from %q: %s\n", dbusConfigSystem, err)
} else {
config.Confinement.SystemBus = c
}
}
// override log from configuration
if dbusVerbose {
config.Confinement.SessionBus.Log = true
config.Confinement.SystemBus.Log = true
}
}
return
}
+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)
}
})
}
}
+324
View File
@@ -0,0 +1,324 @@
// Package container implements unprivileged Linux containers with built-in support for syscall filtering.
package container
import (
"context"
"encoding/gob"
"errors"
"fmt"
"io"
"os"
"os/exec"
"runtime"
"strconv"
. "syscall"
"time"
"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 {
// Cgroup fd, nil to disable.
Cgroup *int
// ExtraFiles passed through to initial process in the container,
// with behaviour identical to its [exec.Cmd] counterpart.
ExtraFiles []*os.File
// param 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
Stderr io.Writer
Cancel func(cmd *exec.Cmd) error
WaitDelay time.Duration
cmd *exec.Cmd
ctx context.Context
Params
}
// Params holds container configuration and is safe to serialise.
Params struct {
// Working directory in the container.
Dir *Absolute
// Initial process environment.
Env []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
// Mapped Gid in user namespace.
Gid int
// Hostname value in UTS namespace.
Hostname string
// Sequential container setup ops.
*Ops
// Seccomp system call filter rules.
SeccompRules []seccomp.NativeRule
// Extra seccomp flags.
SeccompFlags seccomp.ExportFlag
// Seccomp presets. Has no effect unless SeccompRules is zero-length.
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
// Do not syscall.Setsid.
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("container: already started")
}
if p.Ops == nil || len(*p.Ops) == 0 {
return errors.New("container: starting an empty container")
}
ctx, cancel := context.WithCancel(p.ctx)
p.cancel = cancel
// map to overflow id to work around ownership checks
if p.Uid < 1 {
p.Uid = OverflowUid()
}
if p.Gid < 1 {
p.Gid = OverflowGid()
}
if !p.RetainSession {
p.SeccompPresets |= seccomp.PresetDenyTTY
}
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(CancelSignal) }
}
p.cmd.Dir = FHSRoot
p.cmd.SysProcAttr = &SysProcAttr{
Setsid: !p.RetainSession,
Pdeathsig: SIGKILL,
Cloneflags: CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS |
CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP,
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 {
return wrapErrSuffix(err,
"cannot create shim setup pipe:")
} else {
p.setup = e
p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
}
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...)
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")
}
setup := p.setup
p.setup = nil
if p.Path == nil {
p.cancel()
return msg.WrapErr(EINVAL, "invalid executable pathname")
}
// do not transmit nil
if p.Dir == nil {
p.Dir = AbsFHSRoot
}
if p.SeccompRules == nil {
p.SeccompRules = make([]seccomp.NativeRule, 0)
}
err := setup.Encode(
&initParams{
p.Params,
Getuid(),
Getgid(),
len(p.ExtraFiles),
msg.IsVerbose(),
},
)
if err != nil {
p.cancel()
}
return err
}
// 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))
}
// 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
}
+576
View File
@@ -0,0 +1,576 @@
package container_test
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"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/ldd"
)
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) {
replaceOutput(t)
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)
}
}))
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) {
wantOps, wantOpsCtx := tc.ops(t)
wantMnt := tc.mnt(t, wantOpsCtx)
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
defer cancel()
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
c.
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(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,
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 _, a := range libPaths {
// Bind(name, name, 0)
mnt = append(mnt, ent(ignore, a.String(), "ro,nosuid,nodev,relatime", ignore, ignore, ignore))
}
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.Place(container.MustAbs(pathWantMnt), want.Bytes())
if tc.ro {
c.Remount(container.MustAbs("/"), syscall.MS_RDONLY)
}
if err := c.Start(); err != nil {
_, _ = output.WriteTo(os.Stdout)
container.GetOutput().PrintBaseErr(err, "start:")
t.Fatalf("cannot start container: %v", err)
} else if err = c.Serve(); err != nil {
_, _ = output.WriteTo(os.Stdout)
container.GetOutput().PrintBaseErr(err, "serve:")
t.Errorf("cannot serve setup params: %v", err)
}
if err := c.Wait(); err != nil {
_, _ = output.WriteTo(os.Stdout)
container.GetOutput().PrintBaseErr(err, "wait:")
t.Fatalf("wait: %v", err)
}
})
}
}
func ent(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry {
return &vfs.MountInfoEntry{
ID: ignoreV,
Parent: ignoreV,
Devno: vfs.DevT{ignoreV, ignoreV},
Root: root,
Target: target,
VfsOptstr: vfsOptstr,
OptFields: []string{ignore},
FsType: fsType,
Source: source,
FsOptstr: fsOptstr,
}
}
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.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,
c.SeccompFlags)
c.SeccompPresets = seccomp.PresetStrict
want := `argv: ["ldd" "/usr/bin/env"], filter: true, rules: 65, flags: 0x1, presets: 0xf`
if got := c.String(); got != want {
t.Errorf("String: %s, want %s", got, want)
}
}
const (
blockExitCodeInterrupt = 2
)
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 {}
})
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 {
tc = containerTestCases[i]
}
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)
}
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
})
})
}
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].
openNew(name string) (osFile, error)
// writeFile provides [os.WriteFile].
writeFile(name string, data []byte, perm os.FileMode) error
// createTemp provides [os.CreateTemp].
createTemp(dir, pattern string) (osFile, error)
// remove provides os.Remove.
remove(name string) error
// newFile provides os.NewFile.
newFile(fd uintptr, name string) *os.File
// symlink provides os.Symlink.
symlink(oldname, newname string) error
// readlink provides [os.Readlink].
readlink(name string) (string, error)
// umask provides syscall.Umask.
umask(mask int) (oldmask int)
// sethostname provides syscall.Sethostname
sethostname(p []byte) (err error)
// chdir provides syscall.Chdir
chdir(path string) (err error)
// fchdir provides syscall.Fchdir
fchdir(fd int) (err error)
// open provides syscall.Open
open(path string, mode int, perm uint32) (fd int, err error)
// close provides syscall.Close
close(fd int) (err error)
// pivotRoot provides syscall.PivotRoot
pivotRoot(newroot, putold string) (err error)
// mount provides syscall.Mount
mount(source, target, fstype string, flags uintptr, data string) (err error)
// unmount provides syscall.Unmount
unmount(target string, flags int) (err error)
// wait4 provides syscall.Wait4
wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error)
// printf provides [log.Printf].
printf(format string, v ...any)
// fatal provides [log.Fatal]
fatal(v ...any)
// fatalf provides [log.Fatalf]
fatalf(format string, v ...any)
// verbose provides [Msg.Verbose].
verbose(v ...any)
// verbosef provides [Msg.Verbosef].
verbosef(format string, v ...any)
// suspend provides [Msg.Suspend].
suspend()
// resume provides [Msg.Resume].
resume() bool
// beforeExit provides [Msg.BeforeExit].
beforeExit()
// printBaseErr provides [Msg.PrintBaseErr].
printBaseErr(err error, fallback string)
}
// direct implements syscallDispatcher on the current kernel.
type direct struct{}
func (k direct) new(f func(k syscallDispatcher)) { go f(k) }
func (direct) lockOSThread() { runtime.LockOSThread() }
func (direct) setPtracer(pid uintptr) error { return SetPtracer(pid) }
func (direct) setDumpable(dumpable uintptr) error { return SetDumpable(dumpable) }
func (direct) setNoNewPrivs() error { return SetNoNewPrivs() }
func (direct) lastcap() uintptr { return LastCap() }
func (direct) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) }
func (direct) capBoundingSetDrop(cap uintptr) error { return capBoundingSetDrop(cap) }
func (direct) capAmbientClearAll() error { return capAmbientClearAll() }
func (direct) capAmbientRaise(cap uintptr) error { return capAmbientRaise(cap) }
func (direct) isatty(fd int) bool { return Isatty(fd) }
func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
return Receive(key, e, fdp)
}
func (direct) bindMount(source, target string, flags uintptr, eq bool) error {
return hostProc.bindMount(source, target, flags, eq)
}
func (direct) remount(target string, flags uintptr) error {
return hostProc.remount(target, flags)
}
func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
return mountTmpfs(k, fsname, target, flags, size, perm)
}
func (direct) ensureFile(name string, perm, pperm os.FileMode) error {
return ensureFile(name, perm, pperm)
}
func (direct) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error {
return seccomp.Load(rules, flags)
}
func (direct) notify(c chan<- os.Signal, sig ...os.Signal) { signal.Notify(c, sig...) }
func (direct) start(c *exec.Cmd) error { return c.Start() }
func (direct) signal(c *exec.Cmd, sig os.Signal) error { return c.Process.Signal(sig) }
func (direct) evalSymlinks(path string) (string, error) { return filepath.EvalSymlinks(path) }
func (direct) exit(code int) { os.Exit(code) }
func (direct) getpid() int { return os.Getpid() }
func (direct) stat(name string) (os.FileInfo, error) { return os.Stat(name) }
func (direct) mkdir(name string, perm os.FileMode) error { return os.Mkdir(name, perm) }
func (direct) mkdirTemp(dir, pattern string) (string, error) { return os.MkdirTemp(dir, pattern) }
func (direct) mkdirAll(path string, perm os.FileMode) error { return os.MkdirAll(path, perm) }
func (direct) readdir(name string) ([]os.DirEntry, error) { return os.ReadDir(name) }
func (direct) openNew(name string) (osFile, error) { return os.Open(name) }
func (direct) writeFile(name string, data []byte, perm os.FileMode) error {
return os.WriteFile(name, data, perm)
}
func (direct) createTemp(dir, pattern string) (osFile, error) {
return os.CreateTemp(dir, pattern)
}
func (direct) remove(name string) error {
return os.Remove(name)
}
func (direct) newFile(fd uintptr, name string) *os.File {
return os.NewFile(fd, name)
}
func (direct) symlink(oldname, newname string) error {
return os.Symlink(oldname, newname)
}
func (direct) readlink(name string) (string, error) {
return os.Readlink(name)
}
func (direct) umask(mask int) (oldmask int) { return syscall.Umask(mask) }
func (direct) sethostname(p []byte) (err error) { return syscall.Sethostname(p) }
func (direct) chdir(path string) (err error) { return syscall.Chdir(path) }
func (direct) fchdir(fd int) (err error) { return syscall.Fchdir(fd) }
func (direct) open(path string, mode int, perm uint32) (fd int, err error) {
return syscall.Open(path, mode, perm)
}
func (direct) close(fd int) (err error) {
return syscall.Close(fd)
}
func (direct) pivotRoot(newroot, putold string) (err error) {
return syscall.PivotRoot(newroot, putold)
}
func (direct) mount(source, target, fstype string, flags uintptr, data string) (err error) {
return syscall.Mount(source, target, fstype, flags, data)
}
func (direct) unmount(target string, flags int) (err error) {
return syscall.Unmount(target, flags)
}
func (direct) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error) {
return syscall.Wait4(pid, wstatus, options, rusage)
}
func (direct) printf(format string, v ...any) { log.Printf(format, v...) }
func (direct) fatal(v ...any) { log.Fatal(v...) }
func (direct) fatalf(format string, v ...any) { log.Fatalf(format, v...) }
func (direct) verbose(v ...any) { msg.Verbose(v...) }
func (direct) verbosef(format string, v ...any) { msg.Verbosef(format, v...) }
func (direct) suspend() { msg.Suspend() }
func (direct) resume() bool { return msg.Resume() }
func (direct) beforeExit() { msg.BeforeExit() }
func (direct) printBaseErr(err error, fallback string) { msg.PrintBaseErr(err, fallback) }
+748
View File
@@ -0,0 +1,748 @@
package container
import (
"bytes"
"errors"
"io"
"io/fs"
"os"
"os/exec"
"reflect"
"runtime"
"slices"
"strings"
"sync"
"syscall"
"testing"
"time"
"hakurei.app/container/seccomp"
)
var errUnique = errors.New("unique error injected by the test suite")
type opValidTestCase struct {
name string
op Op
want bool
}
func checkOpsValid(t *testing.T, testCases []opValidTestCase) {
t.Run("valid", func(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := tc.op.Valid(); got != tc.want {
t.Errorf("Valid: %v, want %v", got, tc.want)
}
})
}
})
}
type opsBuilderTestCase struct {
name string
ops *Ops
want Ops
}
func checkOpsBuilder(t *testing.T, testCases []opsBuilderTestCase) {
t.Run("build", func(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if !slices.EqualFunc(*tc.ops, tc.want, func(op Op, v Op) bool { return op.Is(v) }) {
t.Errorf("Ops: %#v, want %#v", tc.ops, tc.want)
}
})
}
})
}
type opIsTestCase struct {
name string
op, v Op
want bool
}
func checkOpIs(t *testing.T, testCases []opIsTestCase) {
t.Run("is", func(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := tc.op.Is(tc.v); got != tc.want {
t.Errorf("Is: %v, want %v", got, tc.want)
}
})
}
})
}
type opMetaTestCase struct {
name string
op Op
wantPrefix string
wantString string
}
func checkOpMeta(t *testing.T, testCases []opMetaTestCase) {
t.Run("meta", func(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("prefix", func(t *testing.T) {
if got := tc.op.prefix(); got != tc.wantPrefix {
t.Errorf("prefix: %q, want %q", got, tc.wantPrefix)
}
})
t.Run("string", func(t *testing.T) {
if got := tc.op.String(); got != tc.wantString {
t.Errorf("String: %s, want %s", got, tc.wantString)
}
})
})
}
})
}
type simpleTestCase struct {
name string
f func(k syscallDispatcher) error
want [][]kexpect
wantErr error
}
func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
defer handleExitStub()
k := &kstub{t: t, want: tc.want, wg: new(sync.WaitGroup)}
if err := tc.f(k); !errors.Is(err, tc.wantErr) {
t.Errorf("%s: error = %v, want %v", fname, err, tc.wantErr)
}
k.handleIncomplete(func(k *kstub) {
t.Errorf("%s: %d calls, want %d (track %d)", fname, k.pos, len(k.want[k.track]), k.track)
})
})
}
}
type opBehaviourTestCase struct {
name string
params *Params
op Op
early []kexpect
wantErrEarly error
apply []kexpect
wantErrApply error
}
func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
t.Run("behaviour", func(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
defer handleExitStub()
state := &setupState{Params: tc.params}
k := &kstub{t: t, want: [][]kexpect{slices.Concat(tc.early, []kexpect{{name: "\x00"}}, tc.apply)}, wg: new(sync.WaitGroup)}
errEarly := tc.op.early(state, k)
k.expect("\x00")
if !errors.Is(errEarly, tc.wantErrEarly) {
t.Errorf("early: error = %v, want %v", errEarly, tc.wantErrEarly)
}
if errEarly != nil {
goto out
}
if err := tc.op.apply(state, k); !errors.Is(err, tc.wantErrApply) {
t.Errorf("apply: error = %v, want %v", err, tc.wantErrApply)
}
out:
k.handleIncomplete(func(k *kstub) {
count := k.pos - 1 // separator
if count < len(tc.early) {
t.Errorf("early: %d calls, want %d", count, len(tc.early))
} else {
t.Errorf("apply: %d calls, want %d", count-len(tc.early), len(tc.apply))
}
})
})
}
})
}
func sliceAddr[S any](s []S) *[]S { return &s }
func newCheckedFile(t *testing.T, name, wantData string, closeErr error) osFile {
f := &checkedOsFile{t: t, name: name, want: wantData, closeErr: closeErr}
// check happens in Close, and cleanup is not guaranteed to run, so relying on it for sloppy implementations will cause sporadic test results
f.cleanup = runtime.AddCleanup(f, func(name string) { f.t.Fatalf("checkedOsFile %s became unreachable without a call to Close", name) }, f.name)
return f
}
type checkedOsFile struct {
t *testing.T
name string
want string
closeErr error
cleanup runtime.Cleanup
bytes.Buffer
}
func (f *checkedOsFile) Name() string { return f.name }
func (f *checkedOsFile) Stat() (fs.FileInfo, error) { panic("unreachable") }
func (f *checkedOsFile) Close() error {
defer f.cleanup.Stop()
if f.String() != f.want {
f.t.Errorf("checkedOsFile:\n%s\nwant\n%s", f.String(), f.want)
return syscall.ENOTRECOVERABLE
}
return f.closeErr
}
func newConstFile(s string) osFile { return &readerOsFile{Reader: strings.NewReader(s)} }
type readerOsFile struct {
closed bool
io.Reader
}
func (*readerOsFile) Name() string { panic("unreachable") }
func (*readerOsFile) Write([]byte) (int, error) { panic("unreachable") }
func (*readerOsFile) Stat() (fs.FileInfo, error) { panic("unreachable") }
func (r *readerOsFile) Close() error {
if r.closed {
return os.ErrClosed
}
r.closed = true
return nil
}
type writeErrOsFile struct{ err error }
func (writeErrOsFile) Name() string { panic("unreachable") }
func (f writeErrOsFile) Write([]byte) (int, error) { return 0, f.err }
func (writeErrOsFile) Stat() (fs.FileInfo, error) { panic("unreachable") }
func (writeErrOsFile) Read([]byte) (int, error) { panic("unreachable") }
func (writeErrOsFile) Close() error { panic("unreachable") }
type expectArgs = [5]any
type isDirFi bool
func (isDirFi) Name() string { panic("unreachable") }
func (isDirFi) Size() int64 { panic("unreachable") }
func (isDirFi) Mode() fs.FileMode { panic("unreachable") }
func (isDirFi) ModTime() time.Time { panic("unreachable") }
func (fi isDirFi) IsDir() bool { return bool(fi) }
func (isDirFi) Sys() any { panic("unreachable") }
func stubDir(names ...string) []os.DirEntry {
d := make([]os.DirEntry, len(names))
for i, name := range names {
d[i] = nameDentry(name)
}
return d
}
type nameDentry string
func (e nameDentry) Name() string { return string(e) }
func (nameDentry) IsDir() bool { panic("unreachable") }
func (nameDentry) Type() fs.FileMode { panic("unreachable") }
func (nameDentry) Info() (fs.FileInfo, error) { panic("unreachable") }
type kexpect struct {
name string
args expectArgs
ret any
err error
}
func (k *kexpect) error(ok ...bool) error {
if !slices.Contains(ok, false) {
return k.err
}
return syscall.ENOTRECOVERABLE
}
func handleExitStub() {
r := recover()
if r == 0xdeadbeef {
return
}
if r != nil {
panic(r)
}
}
type kstub struct {
t *testing.T
want [][]kexpect
// pos is the current position in want[track].
pos int
// track is the current active want.
track int
// sub stores addresses of kstub created by new.
sub []*kstub
// wg waits for all descendants to complete.
wg *sync.WaitGroup
}
// handleIncomplete calls f on an incomplete k and all its descendants.
func (k *kstub) handleIncomplete(f func(k *kstub)) {
k.wg.Wait()
if k.want != nil && len(k.want[k.track]) != k.pos {
f(k)
}
for _, sk := range k.sub {
sk.handleIncomplete(f)
}
}
// expect checks name and returns the current kexpect and advances pos.
func (k *kstub) expect(name string) (expect *kexpect) {
if len(k.want[k.track]) == k.pos {
k.t.Fatal("expect: want too short")
}
expect = &k.want[k.track][k.pos]
if name != expect.name {
if expect.name == "\x00" {
k.t.Fatalf("expect: func = %s, separator overrun", name)
}
if name == "\x00" {
k.t.Fatalf("expect: separator, want %s", expect.name)
}
k.t.Fatalf("expect: func = %s, want %s", name, expect.name)
}
k.pos++
return
}
// checkArg checks an argument comparable with the == operator. Avoid using this with pointers.
func checkArg[T comparable](k *kstub, arg string, got T, n int) bool {
if k.pos == 0 {
panic("invalid call to checkArg")
}
expect := k.want[k.track][k.pos-1]
want, ok := expect.args[n].(T)
if !ok || got != want {
k.t.Errorf("%s: %s = %#v, want %#v (%d)", expect.name, arg, got, want, k.pos-1)
return false
}
return true
}
// checkArgReflect checks an argument of any type.
func checkArgReflect(k *kstub, arg string, got any, n int) bool {
if k.pos == 0 {
panic("invalid call to checkArgReflect")
}
expect := k.want[k.track][k.pos-1]
want := expect.args[n]
if !reflect.DeepEqual(got, want) {
k.t.Errorf("%s: %s = %#v, want %#v (%d)", expect.name, arg, got, want, k.pos-1)
return false
}
return true
}
func (k *kstub) new(f func(k syscallDispatcher)) {
k.expect("new")
if len(k.want) <= k.track+1 {
k.t.Fatalf("new: track overrun")
}
sk := &kstub{t: k.t, want: k.want, track: len(k.sub) + 1, wg: k.wg}
k.sub = append(k.sub, sk)
k.wg.Add(1)
go func() {
defer k.wg.Done()
defer handleExitStub()
f(sk)
}()
}
func (k *kstub) lockOSThread() { k.expect("lockOSThread") }
func (k *kstub) setPtracer(pid uintptr) error {
return k.expect("setPtracer").error(
checkArg(k, "pid", pid, 0))
}
func (k *kstub) setDumpable(dumpable uintptr) error {
return k.expect("setDumpable").error(
checkArg(k, "dumpable", dumpable, 0))
}
func (k *kstub) setNoNewPrivs() error { return k.expect("setNoNewPrivs").err }
func (k *kstub) lastcap() uintptr { return k.expect("lastcap").ret.(uintptr) }
func (k *kstub) capset(hdrp *capHeader, datap *[2]capData) error {
return k.expect("capset").error(
checkArgReflect(k, "hdrp", hdrp, 0),
checkArgReflect(k, "datap", datap, 1))
}
func (k *kstub) capBoundingSetDrop(cap uintptr) error {
return k.expect("capBoundingSetDrop").error(
checkArg(k, "cap", cap, 0))
}
func (k *kstub) capAmbientClearAll() error { return k.expect("capAmbientClearAll").err }
func (k *kstub) capAmbientRaise(cap uintptr) error {
return k.expect("capAmbientRaise").error(
checkArg(k, "cap", cap, 0))
}
func (k *kstub) isatty(fd int) bool {
expect := k.expect("isatty")
if !checkArg(k, "fd", fd, 0) {
k.t.FailNow()
}
return expect.ret.(bool)
}
func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) {
expect := k.expect("receive")
var closed bool
closeFunc = func() error {
if closed {
k.t.Error("closeFunc called more than once")
return os.ErrClosed
}
closed = true
if expect.ret != nil {
// use return stored in kexpect for closeFunc instead
return expect.ret.(error)
}
return nil
}
err = expect.error(
checkArg(k, "key", key, 0),
checkArgReflect(k, "e", e, 1),
checkArgReflect(k, "fdp", fdp, 2))
// 3 is unused so stores params
if expect.args[3] != nil {
if v, ok := expect.args[3].(*initParams); ok && v != nil {
if p, ok0 := e.(*initParams); ok0 && p != nil {
*p = *v
}
}
}
// 4 is unused so stores fd
if expect.args[4] != nil {
if v, ok := expect.args[4].(uintptr); ok && v >= 3 {
if fdp != nil {
*fdp = v
}
}
}
return
}
func (k *kstub) bindMount(source, target string, flags uintptr, eq bool) error {
return k.expect("bindMount").error(
checkArg(k, "source", source, 0),
checkArg(k, "target", target, 1),
checkArg(k, "flags", flags, 2),
checkArg(k, "eq", eq, 3))
}
func (k *kstub) remount(target string, flags uintptr) error {
return k.expect("remount").error(
checkArg(k, "target", target, 0),
checkArg(k, "flags", flags, 1))
}
func (k *kstub) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
return k.expect("mountTmpfs").error(
checkArg(k, "fsname", fsname, 0),
checkArg(k, "target", target, 1),
checkArg(k, "flags", flags, 2),
checkArg(k, "size", size, 3),
checkArg(k, "perm", perm, 4))
}
func (k *kstub) ensureFile(name string, perm, pperm os.FileMode) error {
return k.expect("ensureFile").error(
checkArg(k, "name", name, 0),
checkArg(k, "perm", perm, 1),
checkArg(k, "pperm", pperm, 2))
}
func (k *kstub) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error {
return k.expect("seccompLoad").error(
checkArgReflect(k, "rules", rules, 0),
checkArg(k, "flags", flags, 1))
}
func (k *kstub) notify(c chan<- os.Signal, sig ...os.Signal) {
expect := k.expect("notify")
if c == nil || expect.error(
checkArgReflect(k, "sig", sig, 1)) != nil {
k.t.FailNow()
}
// export channel for external instrumentation
if chanf, ok := expect.args[0].(func(c chan<- os.Signal)); ok && chanf != nil {
chanf(c)
}
}
func (k *kstub) start(c *exec.Cmd) error {
expect := k.expect("start")
err := expect.error(
checkArg(k, "c.Path", c.Path, 0),
checkArgReflect(k, "c.Args", c.Args, 1),
checkArgReflect(k, "c.Env", c.Env, 2),
checkArg(k, "c.Dir", c.Dir, 3))
if process, ok := expect.ret.(*os.Process); ok && process != nil {
c.Process = process
}
return err
}
func (k *kstub) signal(c *exec.Cmd, sig os.Signal) error {
return k.expect("signal").error(
checkArg(k, "c.Path", c.Path, 0),
checkArgReflect(k, "c.Args", c.Args, 1),
checkArgReflect(k, "c.Env", c.Env, 2),
checkArg(k, "c.Dir", c.Dir, 3),
checkArg(k, "sig", sig, 4))
}
func (k *kstub) evalSymlinks(path string) (string, error) {
expect := k.expect("evalSymlinks")
return expect.ret.(string), expect.error(
checkArg(k, "path", path, 0))
}
func (k *kstub) exit(code int) {
k.expect("exit")
if !checkArg(k, "code", code, 0) {
k.t.FailNow()
}
panic(0xdeadbeef)
}
func (k *kstub) getpid() int { return k.expect("getpid").ret.(int) }
func (k *kstub) stat(name string) (os.FileInfo, error) {
expect := k.expect("stat")
return expect.ret.(os.FileInfo), expect.error(
checkArg(k, "name", name, 0))
}
func (k *kstub) mkdir(name string, perm os.FileMode) error {
return k.expect("mkdir").error(
checkArg(k, "name", name, 0),
checkArg(k, "perm", perm, 1))
}
func (k *kstub) mkdirTemp(dir, pattern string) (string, error) {
expect := k.expect("mkdirTemp")
return expect.ret.(string), expect.error(
checkArg(k, "dir", dir, 0),
checkArg(k, "pattern", pattern, 1))
}
func (k *kstub) mkdirAll(path string, perm os.FileMode) error {
return k.expect("mkdirAll").error(
checkArg(k, "path", path, 0),
checkArg(k, "perm", perm, 1))
}
func (k *kstub) readdir(name string) ([]os.DirEntry, error) {
expect := k.expect("readdir")
return expect.ret.([]os.DirEntry), expect.error(
checkArg(k, "name", name, 0))
}
func (k *kstub) openNew(name string) (osFile, error) {
expect := k.expect("openNew")
return expect.ret.(osFile), expect.error(
checkArg(k, "name", name, 0))
}
func (k *kstub) writeFile(name string, data []byte, perm os.FileMode) error {
return k.expect("writeFile").error(
checkArg(k, "name", name, 0),
checkArgReflect(k, "data", data, 1),
checkArg(k, "perm", perm, 2))
}
func (k *kstub) createTemp(dir, pattern string) (osFile, error) {
expect := k.expect("createTemp")
return expect.ret.(osFile), expect.error(
checkArg(k, "dir", dir, 0),
checkArg(k, "pattern", pattern, 1))
}
func (k *kstub) remove(name string) error {
return k.expect("remove").error(
checkArg(k, "name", name, 0))
}
func (k *kstub) newFile(fd uintptr, name string) *os.File {
expect := k.expect("newFile")
if expect.error(
checkArg(k, "fd", fd, 0),
checkArg(k, "name", name, 1)) != nil {
k.t.FailNow()
}
return expect.ret.(*os.File)
}
func (k *kstub) symlink(oldname, newname string) error {
return k.expect("symlink").error(
checkArg(k, "oldname", oldname, 0),
checkArg(k, "newname", newname, 1))
}
func (k *kstub) readlink(name string) (string, error) {
expect := k.expect("readlink")
return expect.ret.(string), expect.error(
checkArg(k, "name", name, 0))
}
func (k *kstub) umask(mask int) (oldmask int) {
expect := k.expect("umask")
if !checkArg(k, "mask", mask, 0) {
k.t.FailNow()
}
return expect.ret.(int)
}
func (k *kstub) sethostname(p []byte) (err error) {
return k.expect("sethostname").error(
checkArgReflect(k, "p", p, 0))
}
func (k *kstub) chdir(path string) (err error) {
return k.expect("chdir").error(
checkArg(k, "path", path, 0))
}
func (k *kstub) fchdir(fd int) (err error) {
return k.expect("fchdir").error(
checkArg(k, "fd", fd, 0))
}
func (k *kstub) open(path string, mode int, perm uint32) (fd int, err error) {
expect := k.expect("open")
return expect.ret.(int), expect.error(
checkArg(k, "path", path, 0),
checkArg(k, "mode", mode, 1),
checkArg(k, "perm", perm, 2))
}
func (k *kstub) close(fd int) (err error) {
return k.expect("close").error(
checkArg(k, "fd", fd, 0))
}
func (k *kstub) pivotRoot(newroot, putold string) (err error) {
return k.expect("pivotRoot").error(
checkArg(k, "newroot", newroot, 0),
checkArg(k, "putold", putold, 1))
}
func (k *kstub) mount(source, target, fstype string, flags uintptr, data string) (err error) {
return k.expect("mount").error(
checkArg(k, "source", source, 0),
checkArg(k, "target", target, 1),
checkArg(k, "fstype", fstype, 2),
checkArg(k, "flags", flags, 3),
checkArg(k, "data", data, 4))
}
func (k *kstub) unmount(target string, flags int) (err error) {
return k.expect("unmount").error(
checkArg(k, "target", target, 0),
checkArg(k, "flags", flags, 1))
}
func (k *kstub) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error) {
expect := k.expect("wait4")
// special case to prevent leaking the wait4 goroutine when testing initEntrypoint
if v, ok := expect.args[4].(int); ok && v == 0xdeadbeef {
k.t.Log("terminating current goroutine as requested by kexpect")
panic(0xdeadbeef)
}
wpid = expect.ret.(int)
err = expect.error(
checkArg(k, "pid", pid, 0),
checkArg(k, "options", options, 2))
if wstatusV, ok := expect.args[1].(syscall.WaitStatus); wstatus != nil && ok {
*wstatus = wstatusV
}
if rusageV, ok := expect.args[3].(syscall.Rusage); rusage != nil && ok {
*rusage = rusageV
}
return
}
func (k *kstub) printf(format string, v ...any) {
if k.expect("printf").error(
checkArg(k, "format", format, 0),
checkArgReflect(k, "v", v, 1)) != nil {
k.t.FailNow()
}
}
func (k *kstub) fatal(v ...any) {
if k.expect("fatal").error(
checkArgReflect(k, "v", v, 0)) != nil {
k.t.FailNow()
}
panic(0xdeadbeef)
}
func (k *kstub) fatalf(format string, v ...any) {
if k.expect("fatalf").error(
checkArg(k, "format", format, 0),
checkArgReflect(k, "v", v, 1)) != nil {
k.t.FailNow()
}
panic(0xdeadbeef)
}
func (k *kstub) verbose(v ...any) {
if k.expect("verbose").error(
checkArgReflect(k, "v", v, 0)) != nil {
k.t.FailNow()
}
}
func (k *kstub) verbosef(format string, v ...any) {
if k.expect("verbosef").error(
checkArg(k, "format", format, 0),
checkArgReflect(k, "v", v, 1)) != nil {
k.t.FailNow()
}
}
func (k *kstub) suspend() { k.expect("suspend") }
func (k *kstub) resume() bool { return k.expect("resume").ret.(bool) }
func (k *kstub) beforeExit() { k.expect("beforeExit") }
func (k *kstub) printBaseErr(err error, fallback string) {
if k.expect("printBaseErr").error(
checkArgReflect(k, "err", err, 0),
checkArg(k, "fallback", fallback, 1)) != nil {
k.t.FailNow()
}
}
+26
View File
@@ -0,0 +1,26 @@
package container
import (
"log"
"os"
"sync"
)
var (
executable string
executableOnce sync.Once
)
func copyExecutable() {
if name, err := os.Executable(); err != nil {
msg.BeforeExit()
log.Fatalf("cannot read executable path: %v", err)
} else {
executable = name
}
}
func MustExecutable() string {
executableOnce.Do(copyExecutable)
return executable
}
+17
View File
@@ -0,0 +1,17 @@
package container_test
import (
"os"
"testing"
"hakurei.app/container"
)
func TestExecutable(t *testing.T) {
for i := 0; i < 16; i++ {
if got := container.MustExecutable(); got != os.Args[0] {
t.Errorf("MustExecutable: %q, want %q",
got, os.Args[0])
}
}
}
+435
View File
@@ -0,0 +1,435 @@
package container
import (
"errors"
"fmt"
"os"
"os/exec"
"path"
"slices"
"strconv"
. "syscall"
"time"
"hakurei.app/container/seccomp"
)
const (
/* intermediate tmpfs mount point
this path might seem like a weird choice, however there are many good reasons to use it:
- the contents of this path is never exposed to the container:
the tmpfs root established here effectively becomes anonymous after pivot_root
- it is safe to assume this path exists and is a directory:
this program will not work correctly without a proper /proc and neither will most others
- this path belongs to the container init:
the container init is not any more privileged or trusted than the rest of the container
- this path is only accessible by init and root:
the container init sets SUID_DUMP_DISABLE and terminates if that fails;
it should be noted that none of this should become relevant at any point since the resulting
intermediate root tmpfs should be effectively anonymous */
intermediateHostPath = FHSProc + "self/fd"
// setup params file descriptor
setupEnv = "HAKUREI_SETUP"
)
type (
// Ops is a collection of [Op].
Ops []Op
// Op is a generic setup step ran inside the container init.
// Implementations of this interface are sent as a stream of gobs.
Op interface {
// early is called in host root.
early(state *setupState, k syscallDispatcher) error
// apply is called in intermediate root.
apply(state *setupState, k syscallDispatcher) error
prefix() string
Is(op Op) bool
Valid() bool
fmt.Stringer
}
// setupState persists context between Ops.
setupState struct {
nonrepeatable uintptr
*Params
}
)
// Grow grows the slice Ops points to using [slices.Grow].
func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) }
const (
nrAutoEtc = 1 << iota
nrAutoRoot
)
// initParams are params passed from parent.
type initParams struct {
Params
HostUid, HostGid int
// extra files count
Count int
// verbosity pass through
Verbose bool
}
func Init(prepareLogger func(prefix string), setVerbose func(verbose bool)) {
initEntrypoint(direct{}, prepareLogger, setVerbose)
}
func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setVerbose func(verbose bool)) {
k.lockOSThread()
prepareLogger("init")
if k.getpid() != 1 {
k.fatal("this process must run as pid 1")
}
if err := k.setPtracer(0); err != nil {
k.verbosef("cannot enable ptrace protection via Yama LSM: %v", err)
// not fatal: this program has no additional privileges at initial program start
}
var (
params initParams
closeSetup func() error
setupFd uintptr
offsetSetup int
)
if f, err := k.receive(setupEnv, &params, &setupFd); err != nil {
if errors.Is(err, EBADF) {
k.fatal("invalid setup descriptor")
}
if errors.Is(err, ErrNotSet) {
k.fatal("HAKUREI_SETUP not set")
}
k.fatalf("cannot decode init setup payload: %v", err)
} else {
if params.Ops == nil {
k.fatal("invalid setup parameters")
}
if params.ParentPerm == 0 {
params.ParentPerm = 0755
}
setVerbose(params.Verbose)
k.verbose("received setup parameters")
closeSetup = f
offsetSetup = int(setupFd + 1)
}
// write uid/gid map here so parent does not need to set dumpable
if err := k.setDumpable(SUID_DUMP_USER); err != nil {
k.fatalf("cannot set SUID_DUMP_USER: %v", err)
}
if err := k.writeFile(FHSProc+"self/uid_map",
append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...),
0); err != nil {
k.fatalf("%v", err)
}
if err := k.writeFile(FHSProc+"self/setgroups",
[]byte("deny\n"),
0); err != nil && !os.IsNotExist(err) {
k.fatalf("%v", err)
}
if err := k.writeFile(FHSProc+"self/gid_map",
append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...),
0); err != nil {
k.fatalf("%v", err)
}
if err := k.setDumpable(SUID_DUMP_DISABLE); err != nil {
k.fatalf("cannot set SUID_DUMP_DISABLE: %v", err)
}
oldmask := k.umask(0)
if params.Hostname != "" {
if err := k.sethostname([]byte(params.Hostname)); err != nil {
k.fatalf("cannot set hostname: %v", err)
}
}
// cache sysctl before pivot_root
lastcap := k.lastcap()
if err := k.mount(zeroString, FHSRoot, zeroString, MS_SILENT|MS_SLAVE|MS_REC, zeroString); err != nil {
k.fatalf("cannot make / rslave: %v", err)
}
state := &setupState{Params: &params.Params}
/* early is called right before pivot_root into intermediate root;
this step is mostly for gathering information that would otherwise be difficult to obtain
via library functions after pivot_root, and implementations are expected to avoid changing
the state of the mount namespace */
for i, op := range *params.Ops {
if op == nil || !op.Valid() {
k.fatalf("invalid op at index %d", i)
}
if err := op.early(state, k); err != nil {
k.printBaseErr(err,
fmt.Sprintf("cannot prepare op at index %d:", i))
k.beforeExit()
k.exit(1)
}
}
if err := k.mount(SourceTmpfsRootfs, intermediateHostPath, FstypeTmpfs, MS_NODEV|MS_NOSUID, zeroString); err != nil {
k.fatalf("cannot mount intermediate root: %v", err)
}
if err := k.chdir(intermediateHostPath); err != nil {
k.fatalf("cannot enter intermediate host path: %v", err)
}
if err := k.mkdir(sysrootDir, 0755); err != nil {
k.fatalf("%v", err)
}
if err := k.mount(sysrootDir, sysrootDir, zeroString, MS_SILENT|MS_BIND|MS_REC, zeroString); err != nil {
k.fatalf("cannot bind sysroot: %v", err)
}
if err := k.mkdir(hostDir, 0755); err != nil {
k.fatalf("%v", err)
}
// pivot_root uncovers intermediateHostPath in hostDir
if err := k.pivotRoot(intermediateHostPath, hostDir); err != nil {
k.fatalf("cannot pivot into intermediate root: %v", err)
}
if err := k.chdir(FHSRoot); err != nil {
k.fatalf("cannot enter intermediate root: %v", err)
}
/* apply is called right after pivot_root and entering the new root;
this step sets up the container filesystem, and implementations are expected to keep the host root
and sysroot mount points intact but otherwise can do whatever they need to;
chdir is allowed but discouraged */
for i, op := range *params.Ops {
// ops already checked during early setup
k.verbosef("%s %s", op.prefix(), op)
if err := op.apply(state, k); err != nil {
k.printBaseErr(err,
fmt.Sprintf("cannot apply op at index %d:", i))
k.beforeExit()
k.exit(1)
}
}
// setup requiring host root complete at this point
if err := k.mount(hostDir, hostDir, zeroString, MS_SILENT|MS_REC|MS_PRIVATE, zeroString); err != nil {
k.fatalf("cannot make host root rprivate: %v", err)
}
if err := k.unmount(hostDir, MNT_DETACH); err != nil {
k.fatalf("cannot unmount host root: %v", err)
}
{
var fd int
if err := IgnoringEINTR(func() (err error) {
fd, err = k.open(FHSRoot, O_DIRECTORY|O_RDONLY, 0)
return
}); err != nil {
k.fatalf("cannot open intermediate root: %v", err)
}
if err := k.chdir(sysrootPath); err != nil {
k.fatalf("cannot enter sysroot: %v", err)
}
if err := k.pivotRoot(".", "."); err != nil {
k.fatalf("cannot pivot into sysroot: %v", err)
}
if err := k.fchdir(fd); err != nil {
k.fatalf("cannot re-enter intermediate root: %v", err)
}
if err := k.unmount(".", MNT_DETACH); err != nil {
k.fatalf("cannot unmount intermediate root: %v", err)
}
if err := k.chdir(FHSRoot); err != nil {
k.fatalf("cannot enter root: %v", err)
}
if err := k.close(fd); err != nil {
k.fatalf("cannot close intermediate root: %v", err)
}
}
if err := k.capAmbientClearAll(); err != nil {
k.fatalf("cannot clear the ambient capability set: %v", err)
}
for i := uintptr(0); i <= lastcap; i++ {
if params.Privileged && i == CAP_SYS_ADMIN {
continue
}
if err := k.capBoundingSetDrop(i); err != nil {
k.fatalf("cannot drop capability from bounding set: %v", err)
}
}
var keep [2]uint32
if params.Privileged {
keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN)
if err := k.capAmbientRaise(CAP_SYS_ADMIN); err != nil {
k.fatalf("cannot raise CAP_SYS_ADMIN: %v", err)
}
}
if err := k.capset(
&capHeader{_LINUX_CAPABILITY_VERSION_3, 0},
&[2]capData{{0, keep[0], keep[0]}, {0, keep[1], keep[1]}},
); err != nil {
k.fatalf("cannot capset: %v", err)
}
if !params.SeccompDisable {
rules := params.SeccompRules
if len(rules) == 0 { // non-empty rules slice always overrides presets
k.verbosef("resolving presets %#x", params.SeccompPresets)
rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags)
}
if err := k.seccompLoad(rules, params.SeccompFlags); err != nil {
// this also indirectly asserts PR_SET_NO_NEW_PRIVS
k.fatalf("cannot load syscall filter: %v", err)
}
k.verbosef("%d filter rules loaded", len(rules))
} else {
k.verbose("syscall filter not configured")
}
extraFiles := make([]*os.File, params.Count)
for i := range extraFiles {
// setup fd is placed before all extra files
extraFiles[i] = k.newFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i))
}
k.umask(oldmask)
cmd := exec.Command(params.Path.String())
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Args = params.Args
cmd.Env = params.Env
cmd.ExtraFiles = extraFiles
cmd.Dir = params.Dir.String()
k.verbosef("starting initial program %s", params.Path)
if err := k.start(cmd); err != nil {
k.fatalf("%v", err)
}
k.suspend()
if err := closeSetup(); err != nil {
k.printf("cannot close setup pipe: %v", err)
// not fatal
}
type winfo struct {
wpid int
wstatus WaitStatus
}
info := make(chan winfo, 1)
done := make(chan struct{})
k.new(func(k syscallDispatcher) {
var (
err error
wpid = -2
wstatus WaitStatus
)
// keep going until no child process is left
for wpid != -1 {
if err != nil {
break
}
if wpid != -2 {
info <- winfo{wpid, wstatus}
}
err = EINTR
for errors.Is(err, EINTR) {
wpid, err = k.wait4(-1, &wstatus, 0, nil)
}
}
if !errors.Is(err, ECHILD) {
k.printf("unexpected wait4 response: %v", err)
}
close(done)
})
// handle signals to dump withheld messages
sig := make(chan os.Signal, 2)
k.notify(sig, os.Interrupt, CancelSignal)
// closed after residualProcessTimeout has elapsed after initial process death
timeout := make(chan struct{})
r := 2
for {
select {
case s := <-sig:
if k.resume() {
k.verbosef("%s after process start", s.String())
} else {
k.verbosef("got %s", s.String())
}
if s == CancelSignal && params.ForwardCancel && cmd.Process != nil {
k.verbose("forwarding context cancellation")
if err := k.signal(cmd, os.Interrupt); err != nil {
k.printf("cannot forward cancellation: %v", err)
}
continue
}
k.beforeExit()
k.exit(0)
case w := <-info:
if w.wpid == cmd.Process.Pid {
// initial process exited, output is most likely available again
k.resume()
switch {
case w.wstatus.Exited():
r = w.wstatus.ExitStatus()
k.verbosef("initial process exited with code %d", w.wstatus.ExitStatus())
case w.wstatus.Signaled():
r = 128 + int(w.wstatus.Signal())
k.verbosef("initial process exited with signal %s", w.wstatus.Signal())
default:
r = 255
k.verbosef("initial process exited with status %#x", w.wstatus)
}
go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
}
case <-done:
k.beforeExit()
k.exit(r)
case <-timeout:
k.printf("timeout exceeded waiting for lingering processes")
k.beforeExit()
k.exit(r)
}
}
}
const initName = "init"
// TryArgv0 calls [Init] if the last element of argv0 is "init".
func TryArgv0(v Msg, prepare func(prefix string), setVerbose func(verbose bool)) {
if len(os.Args) > 0 && path.Base(os.Args[0]) == initName {
msg = v
Init(prepare, setVerbose)
msg.BeforeExit()
os.Exit(0)
}
}
File diff suppressed because it is too large Load Diff
+113
View File
@@ -0,0 +1,113 @@
package container
import (
"encoding/gob"
"fmt"
"os"
"syscall"
)
func init() { gob.Register(new(BindMountOp)) }
// Bind appends an [Op] that bind mounts host path [BindMountOp.Source] on container path [BindMountOp.Target].
func (f *Ops) Bind(source, target *Absolute, flags int) *Ops {
*f = append(*f, &BindMountOp{nil, source, target, flags})
return f
}
// BindMountOp bind mounts host path Source on container path Target.
// Note that Flags uses bits declared in this package and should not be set with constants in [syscall].
type BindMountOp struct {
sourceFinal, Source, Target *Absolute
Flags int
}
const (
// BindOptional skips nonexistent host paths.
BindOptional = 1 << iota
// BindWritable mounts filesystem read-write.
BindWritable
// BindDevice allows access to devices (special files) on this filesystem.
BindDevice
// BindEnsure attempts to create the host path if it does not exist.
BindEnsure
)
func (b *BindMountOp) Valid() bool {
return b != nil &&
b.Source != nil && b.Target != nil &&
b.Flags&(BindOptional|BindEnsure) != (BindOptional|BindEnsure)
}
func (b *BindMountOp) early(_ *setupState, k syscallDispatcher) error {
if b.Flags&BindEnsure != 0 {
if err := k.mkdirAll(b.Source.String(), 0700); err != nil {
return wrapErrSelf(err)
}
}
if pathname, err := k.evalSymlinks(b.Source.String()); err != nil {
if os.IsNotExist(err) && b.Flags&BindOptional != 0 {
// leave sourceFinal as nil
return nil
}
return wrapErrSelf(err)
} else {
b.sourceFinal, err = NewAbs(pathname)
return err
}
}
func (b *BindMountOp) apply(_ *setupState, k syscallDispatcher) error {
if b.sourceFinal == nil {
if b.Flags&BindOptional == 0 {
// unreachable
return msg.WrapErr(os.ErrClosed, "impossible bind state reached")
}
return nil
}
source := toHost(b.sourceFinal.String())
target := toSysroot(b.Target.String())
// this perm value emulates bwrap behaviour as it clears bits from 0755 based on
// op->perms which is never set for any bind setup op so always results in 0700
if fi, err := k.stat(source); err != nil {
return wrapErrSelf(err)
} else if fi.IsDir() {
if err = k.mkdirAll(target, 0700); err != nil {
return wrapErrSelf(err)
}
} else if err = k.ensureFile(target, 0444, 0700); err != nil {
return err
}
var flags uintptr = syscall.MS_REC
if b.Flags&BindWritable == 0 {
flags |= syscall.MS_RDONLY
}
if b.Flags&BindDevice == 0 {
flags |= syscall.MS_NODEV
}
return k.bindMount(source, target, flags, b.sourceFinal == b.Target)
}
func (b *BindMountOp) Is(op Op) bool {
vb, ok := op.(*BindMountOp)
return ok && b.Valid() && vb.Valid() &&
b.Source.Is(vb.Source) &&
b.Target.Is(vb.Target) &&
b.Flags == vb.Flags
}
func (*BindMountOp) prefix() string { return "mounting" }
func (b *BindMountOp) String() string {
if b.Source == nil || b.Target == nil {
return "<invalid>"
}
if b.Source.String() == b.Target.String() {
return fmt.Sprintf("%q flags %#x", b.Source, b.Flags)
}
return fmt.Sprintf("%q on %q flags %#x", b.Source, b.Target, b.Flags)
}
+235
View File
@@ -0,0 +1,235 @@
package container
import (
"errors"
"os"
"syscall"
"testing"
)
func TestBindMountOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{
{"ENOENT not optional", new(Params), &BindMountOp{
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/bin/"}, "", syscall.ENOENT},
}, wrapErrSelf(syscall.ENOENT), nil, nil},
{"skip optional", new(Params), &BindMountOp{
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
Flags: BindOptional,
}, []kexpect{
{"evalSymlinks", expectArgs{"/bin/"}, "", syscall.ENOENT},
}, nil, nil, nil},
{"success optional", new(Params), &BindMountOp{
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
Flags: BindOptional,
}, []kexpect{
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
}, nil, []kexpect{
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil},
{"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil},
{"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil},
}, nil},
{"ensureFile device", new(Params), &BindMountOp{
Source: MustAbs("/dev/null"),
Target: MustAbs("/dev/null"),
Flags: BindWritable | BindDevice,
}, []kexpect{
{"evalSymlinks", expectArgs{"/dev/null"}, "/dev/null", nil},
}, nil, []kexpect{
{"stat", expectArgs{"/host/dev/null"}, isDirFi(false), nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, errUnique},
}, errUnique},
{"mkdirAll ensure", new(Params), &BindMountOp{
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
Flags: BindEnsure,
}, []kexpect{
{"mkdirAll", expectArgs{"/bin/", os.FileMode(0700)}, nil, errUnique},
}, wrapErrSelf(errUnique), nil, nil},
{"success ensure", new(Params), &BindMountOp{
Source: MustAbs("/bin/"),
Target: MustAbs("/usr/bin/"),
Flags: BindEnsure,
}, []kexpect{
{"mkdirAll", expectArgs{"/bin/", os.FileMode(0700)}, nil, nil},
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
}, nil, []kexpect{
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil},
{"mkdirAll", expectArgs{"/sysroot/usr/bin", os.FileMode(0700)}, nil, nil},
{"bindMount", expectArgs{"/host/usr/bin", "/sysroot/usr/bin", uintptr(0x4005), false}, nil, nil},
}, nil},
{"success device ro", new(Params), &BindMountOp{
Source: MustAbs("/dev/null"),
Target: MustAbs("/dev/null"),
Flags: BindDevice,
}, []kexpect{
{"evalSymlinks", expectArgs{"/dev/null"}, "/dev/null", nil},
}, nil, []kexpect{
{"stat", expectArgs{"/host/dev/null"}, isDirFi(false), nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4001), false}, nil, nil},
}, nil},
{"success device", new(Params), &BindMountOp{
Source: MustAbs("/dev/null"),
Target: MustAbs("/dev/null"),
Flags: BindWritable | BindDevice,
}, []kexpect{
{"evalSymlinks", expectArgs{"/dev/null"}, "/dev/null", nil},
}, nil, []kexpect{
{"stat", expectArgs{"/host/dev/null"}, isDirFi(false), nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4000), false}, nil, nil},
}, nil},
{"evalSymlinks", new(Params), &BindMountOp{
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", errUnique},
}, wrapErrSelf(errUnique), nil, nil},
{"stat", new(Params), &BindMountOp{
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
}, nil, []kexpect{
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), errUnique},
}, wrapErrSelf(errUnique)},
{"mkdirAll", new(Params), &BindMountOp{
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
}, nil, []kexpect{
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil},
{"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, errUnique},
}, wrapErrSelf(errUnique)},
{"bindMount", new(Params), &BindMountOp{
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
}, nil, []kexpect{
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil},
{"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil},
{"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, errUnique},
}, errUnique},
{"success", new(Params), &BindMountOp{
Source: MustAbs("/bin/"),
Target: MustAbs("/bin/"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil},
}, nil, []kexpect{
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil},
{"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil},
{"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil},
}, nil},
})
t.Run("unreachable", func(t *testing.T) {
t.Run("nil sourceFinal not optional", func(t *testing.T) {
wantErr := msg.WrapErr(os.ErrClosed, "impossible bind state reached")
if err := new(BindMountOp).apply(nil, nil); !errors.Is(err, wantErr) {
t.Errorf("apply: error = %v, want %v", err, wantErr)
}
})
})
checkOpsValid(t, []opValidTestCase{
{"nil", (*BindMountOp)(nil), false},
{"zero", new(BindMountOp), false},
{"nil source", &BindMountOp{Target: MustAbs("/")}, false},
{"nil target", &BindMountOp{Source: MustAbs("/")}, false},
{"flag optional ensure", &BindMountOp{Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindOptional | BindEnsure}, false},
{"valid", &BindMountOp{Source: MustAbs("/"), Target: MustAbs("/")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"autoetc", new(Ops).Bind(
MustAbs("/etc/"),
MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
0,
), Ops{
&BindMountOp{
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
},
}},
})
checkOpIs(t, []opIsTestCase{
{"zero", new(BindMountOp), new(BindMountOp), false},
{"internal ne", &BindMountOp{
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, &BindMountOp{
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
sourceFinal: MustAbs("/etc/"),
}, true},
{"flags differs", &BindMountOp{
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, &BindMountOp{
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
Flags: BindOptional,
}, false},
{"source differs", &BindMountOp{
Source: MustAbs("/.hakurei/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, &BindMountOp{
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, false},
{"target differs", &BindMountOp{
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, &BindMountOp{
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/"),
}, false},
{"equals", &BindMountOp{
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, &BindMountOp{
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"invalid", new(BindMountOp), "mounting", "<invalid>"},
{"autoetc", &BindMountOp{
Source: MustAbs("/etc/"),
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
}, "mounting", `"/etc/" on "/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659" flags 0x0`},
{"hostdev", &BindMountOp{
Source: MustAbs("/dev/"),
Target: MustAbs("/dev/"),
Flags: BindWritable | BindDevice,
}, "mounting", `"/dev/" flags 0x6`},
})
}
+142
View File
@@ -0,0 +1,142 @@
package container
import (
"encoding/gob"
"fmt"
"path"
. "syscall"
)
func init() { gob.Register(new(MountDevOp)) }
// Dev appends an [Op] that mounts a subset of host /dev.
func (f *Ops) Dev(target *Absolute, mqueue bool) *Ops {
*f = append(*f, &MountDevOp{target, mqueue, false})
return f
}
// DevWritable appends an [Op] that mounts a writable subset of host /dev.
// There is usually no good reason to write to /dev, so this should always be followed by a [RemountOp].
func (f *Ops) DevWritable(target *Absolute, mqueue bool) *Ops {
*f = append(*f, &MountDevOp{target, mqueue, true})
return f
}
// MountDevOp mounts a subset of host /dev on container path Target.
// If Mqueue is true, a private instance of [FstypeMqueue] is mounted.
// If Write is true, the resulting mount point is left writable.
type MountDevOp struct {
Target *Absolute
Mqueue bool
Write bool
}
func (d *MountDevOp) Valid() bool { return d != nil && d.Target != nil }
func (d *MountDevOp) early(*setupState, syscallDispatcher) error { return nil }
func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
target := toSysroot(d.Target.String())
if err := k.mountTmpfs(SourceTmpfsDevtmpfs, target, MS_NOSUID|MS_NODEV, 0, state.ParentPerm); err != nil {
return err
}
for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} {
targetPath := path.Join(target, name)
if err := k.ensureFile(targetPath, 0444, state.ParentPerm); err != nil {
return err
}
if err := k.bindMount(
toHost(FHSDev+name),
targetPath,
0,
true,
); err != nil {
return err
}
}
for i, name := range []string{"stdin", "stdout", "stderr"} {
if err := k.symlink(
FHSProc+"self/fd/"+string(rune(i+'0')),
path.Join(target, name),
); err != nil {
return wrapErrSelf(err)
}
}
for _, pair := range [][2]string{
{FHSProc + "self/fd", "fd"},
{FHSProc + "kcore", "core"},
{"pts/ptmx", "ptmx"},
} {
if err := k.symlink(pair[0], path.Join(target, pair[1])); err != nil {
return wrapErrSelf(err)
}
}
devShmPath := path.Join(target, "shm")
devPtsPath := path.Join(target, "pts")
for _, name := range []string{devShmPath, devPtsPath} {
if err := k.mkdir(name, state.ParentPerm); err != nil {
return wrapErrSelf(err)
}
}
if err := k.mount(SourceDevpts, devPtsPath, FstypeDevpts, MS_NOSUID|MS_NOEXEC,
"newinstance,ptmxmode=0666,mode=620"); err != nil {
return wrapErrSuffix(err,
fmt.Sprintf("cannot mount devpts on %q:", devPtsPath))
}
if state.RetainSession {
if k.isatty(Stdout) {
consolePath := path.Join(target, "console")
if err := k.ensureFile(consolePath, 0444, state.ParentPerm); err != nil {
return err
}
if name, err := k.readlink(hostProc.stdout()); err != nil {
return wrapErrSelf(err)
} else if err = k.bindMount(
toHost(name),
consolePath,
0,
false,
); err != nil {
return err
}
}
}
if d.Mqueue {
mqueueTarget := path.Join(target, "mqueue")
if err := k.mkdir(mqueueTarget, state.ParentPerm); err != nil {
return wrapErrSelf(err)
}
if err := k.mount(SourceMqueue, mqueueTarget, FstypeMqueue, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString); err != nil {
return wrapErrSuffix(err, "cannot mount mqueue:")
}
}
if d.Write {
return nil
}
if err := k.remount(target, MS_RDONLY); err != nil {
return wrapErrSuffix(k.remount(target, MS_RDONLY),
fmt.Sprintf("cannot remount %q:", target))
}
return k.mountTmpfs(SourceTmpfs, devShmPath, MS_NOSUID|MS_NODEV, 0, 01777)
}
func (d *MountDevOp) Is(op Op) bool {
vd, ok := op.(*MountDevOp)
return ok && d.Valid() && vd.Valid() &&
d.Target.Is(vd.Target) &&
d.Mqueue == vd.Mqueue &&
d.Write == vd.Write
}
func (*MountDevOp) prefix() string { return "mounting" }
func (d *MountDevOp) String() string {
if d.Mqueue {
return fmt.Sprintf("dev on %q with mqueue", d.Target)
}
return fmt.Sprintf("dev on %q", d.Target)
}
+791
View File
@@ -0,0 +1,791 @@
package container
import (
"os"
"testing"
)
func TestMountDevOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{
{"mountTmpfs", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, errUnique},
}, errUnique},
{"ensureFile null", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, errUnique},
}, errUnique},
{"bindMount null", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, errUnique},
}, errUnique},
{"ensureFile zero", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, errUnique},
}, errUnique},
{"bindMount zero", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, errUnique},
}, errUnique},
{"ensureFile full", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, errUnique},
}, errUnique},
{"bindMount full", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, errUnique},
}, errUnique},
{"ensureFile random", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, errUnique},
}, errUnique},
{"bindMount random", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, errUnique},
}, errUnique},
{"ensureFile urandom", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, errUnique},
}, errUnique},
{"bindMount urandom", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, errUnique},
}, errUnique},
{"ensureFile tty", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, errUnique},
}, errUnique},
{"bindMount tty", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, errUnique},
}, errUnique},
{"symlink stdin", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, errUnique},
}, wrapErrSelf(errUnique)},
{"symlink stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, errUnique},
}, wrapErrSelf(errUnique)},
{"symlink stderr", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, errUnique},
}, wrapErrSelf(errUnique)},
{"symlink fd", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, errUnique},
}, wrapErrSelf(errUnique)},
{"symlink kcore", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, errUnique},
}, wrapErrSelf(errUnique)},
{"symlink ptmx", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, errUnique},
}, wrapErrSelf(errUnique)},
{"mkdir shm", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, errUnique},
}, wrapErrSelf(errUnique)},
{"mkdir devpts", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, errUnique},
}, wrapErrSelf(errUnique)},
{"mount devpts", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil},
{"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, errUnique},
}, wrapErrSuffix(errUnique, `cannot mount devpts on "/sysroot/dev/pts":`)},
{"ensureFile stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil},
{"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil},
{"isatty", expectArgs{1}, true, nil},
{"ensureFile", expectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, errUnique},
}, errUnique},
{"readlink stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil},
{"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil},
{"isatty", expectArgs{1}, true, nil},
{"ensureFile", expectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"readlink", expectArgs{"/host/proc/self/fd/1"}, "", errUnique},
}, wrapErrSelf(errUnique)},
{"bindMount stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil},
{"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil},
{"isatty", expectArgs{1}, true, nil},
{"ensureFile", expectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"readlink", expectArgs{"/host/proc/self/fd/1"}, "/dev/pts/2", nil},
{"bindMount", expectArgs{"/host/dev/pts/2", "/sysroot/dev/console", uintptr(0), false}, nil, errUnique},
}, errUnique},
{"mkdir mqueue", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil},
{"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil},
{"isatty", expectArgs{1}, true, nil},
{"ensureFile", expectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"readlink", expectArgs{"/host/proc/self/fd/1"}, "/dev/pts/2", nil},
{"bindMount", expectArgs{"/host/dev/pts/2", "/sysroot/dev/console", uintptr(0), false}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/mqueue", os.FileMode(0750)}, nil, errUnique},
}, wrapErrSelf(errUnique)},
{"mount mqueue", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil},
{"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil},
{"isatty", expectArgs{1}, true, nil},
{"ensureFile", expectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"readlink", expectArgs{"/host/proc/self/fd/1"}, "/dev/pts/2", nil},
{"bindMount", expectArgs{"/host/dev/pts/2", "/sysroot/dev/console", uintptr(0), false}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/mqueue", os.FileMode(0750)}, nil, nil},
{"mount", expectArgs{"mqueue", "/sysroot/dev/mqueue", "mqueue", uintptr(0xe), ""}, nil, errUnique},
}, wrapErrSuffix(errUnique, "cannot mount mqueue:")},
{"success no session", &Params{ParentPerm: 0755}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
Write: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0755)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0755)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0755)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0755)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0755)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0755)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0755)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0755)}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0755)}, nil, nil},
{"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/mqueue", os.FileMode(0755)}, nil, nil},
{"mount", expectArgs{"mqueue", "/sysroot/dev/mqueue", "mqueue", uintptr(0xe), ""}, nil, nil},
}, nil},
{"success no tty", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
Write: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil},
{"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil},
{"isatty", expectArgs{1}, false, nil},
{"mkdir", expectArgs{"/sysroot/dev/mqueue", os.FileMode(0750)}, nil, nil},
{"mount", expectArgs{"mqueue", "/sysroot/dev/mqueue", "mqueue", uintptr(0xe), ""}, nil, nil},
}, nil},
{"success no mqueue", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil},
{"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil},
{"isatty", expectArgs{1}, true, nil},
{"ensureFile", expectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"readlink", expectArgs{"/host/proc/self/fd/1"}, "/dev/pts/2", nil},
{"bindMount", expectArgs{"/host/dev/pts/2", "/sysroot/dev/console", uintptr(0), false}, nil, nil},
{"remount", expectArgs{"/sysroot/dev", uintptr(1)}, nil, nil},
{"mountTmpfs", expectArgs{"tmpfs", "/sysroot/dev/shm", uintptr(0x6), 0, os.FileMode(01777)}, nil, nil},
}, nil},
{"success rw", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
Write: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil},
{"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil},
{"isatty", expectArgs{1}, true, nil},
{"ensureFile", expectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"readlink", expectArgs{"/host/proc/self/fd/1"}, "/dev/pts/2", nil},
{"bindMount", expectArgs{"/host/dev/pts/2", "/sysroot/dev/console", uintptr(0), false}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/mqueue", os.FileMode(0750)}, nil, nil},
{"mount", expectArgs{"mqueue", "/sysroot/dev/mqueue", "mqueue", uintptr(0xe), ""}, nil, nil},
}, nil},
{"success", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil},
{"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil},
{"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil},
{"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil},
{"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil},
{"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil},
{"isatty", expectArgs{1}, true, nil},
{"ensureFile", expectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil},
{"readlink", expectArgs{"/host/proc/self/fd/1"}, "/dev/pts/2", nil},
{"bindMount", expectArgs{"/host/dev/pts/2", "/sysroot/dev/console", uintptr(0), false}, nil, nil},
{"mkdir", expectArgs{"/sysroot/dev/mqueue", os.FileMode(0750)}, nil, nil},
{"mount", expectArgs{"mqueue", "/sysroot/dev/mqueue", "mqueue", uintptr(0xe), ""}, nil, nil},
{"remount", expectArgs{"/sysroot/dev", uintptr(1)}, nil, nil},
{"mountTmpfs", expectArgs{"tmpfs", "/sysroot/dev/shm", uintptr(0x6), 0, os.FileMode(01777)}, nil, nil},
}, nil},
})
checkOpsValid(t, []opValidTestCase{
{"nil", (*MountDevOp)(nil), false},
{"zero", new(MountDevOp), false},
{"valid", &MountDevOp{Target: MustAbs("/dev/")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"dev", new(Ops).Dev(MustAbs("/dev/"), true), Ops{
&MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
},
}},
{"dev writable", new(Ops).DevWritable(MustAbs("/.hakurei/dev/"), false), Ops{
&MountDevOp{
Target: MustAbs("/.hakurei/dev/"),
Write: true,
},
}},
})
checkOpIs(t, []opIsTestCase{
{"zero", new(MountDevOp), new(MountDevOp), false},
{"write differs", &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
Write: true,
}, false},
{"mqueue differs", &MountDevOp{
Target: MustAbs("/dev/"),
}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, false},
{"target differs", &MountDevOp{
Target: MustAbs("/"),
Mqueue: true,
}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, false},
{"equals", &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"mqueue", &MountDevOp{
Target: MustAbs("/dev/"),
Mqueue: true,
}, "mounting", `dev on "/dev/" with mqueue`},
{"dev", &MountDevOp{
Target: MustAbs("/dev/"),
}, "mounting", `dev on "/dev/"`},
})
}
+36
View File
@@ -0,0 +1,36 @@
package container
import (
"encoding/gob"
"fmt"
"os"
)
func init() { gob.Register(new(MkdirOp)) }
// Mkdir appends an [Op] that creates a directory in the container filesystem.
func (f *Ops) Mkdir(name *Absolute, perm os.FileMode) *Ops {
*f = append(*f, &MkdirOp{name, perm})
return f
}
// MkdirOp creates a directory at container Path with permission bits set to Perm.
type MkdirOp struct {
Path *Absolute
Perm os.FileMode
}
func (m *MkdirOp) Valid() bool { return m != nil && m.Path != nil }
func (m *MkdirOp) early(*setupState, syscallDispatcher) error { return nil }
func (m *MkdirOp) apply(_ *setupState, k syscallDispatcher) error {
return wrapErrSelf(k.mkdirAll(toSysroot(m.Path.String()), m.Perm))
}
func (m *MkdirOp) Is(op Op) bool {
vm, ok := op.(*MkdirOp)
return ok && m.Valid() && vm.Valid() &&
m.Path.Is(vm.Path) &&
m.Perm == vm.Perm
}
func (*MkdirOp) prefix() string { return "creating" }
func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) }
+42
View File
@@ -0,0 +1,42 @@
package container
import (
"os"
"testing"
)
func TestMkdirOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{
{"success", new(Params), &MkdirOp{
Path: MustAbs("/.hakurei"),
Perm: 0500,
}, nil, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/.hakurei", os.FileMode(0500)}, nil, nil},
}, nil},
})
checkOpsValid(t, []opValidTestCase{
{"nil", (*MkdirOp)(nil), false},
{"zero", new(MkdirOp), false},
{"valid", &MkdirOp{Path: MustAbs("/.hakurei")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"etc", new(Ops).Mkdir(MustAbs("/etc/"), 0), Ops{
&MkdirOp{Path: MustAbs("/etc/")},
}},
})
checkOpIs(t, []opIsTestCase{
{"zero", new(MkdirOp), new(MkdirOp), false},
{"path differs", &MkdirOp{Path: MustAbs("/"), Perm: 0755}, &MkdirOp{Path: MustAbs("/etc/"), Perm: 0755}, false},
{"perm differs", &MkdirOp{Path: MustAbs("/")}, &MkdirOp{Path: MustAbs("/"), Perm: 0755}, false},
{"equals", &MkdirOp{Path: MustAbs("/")}, &MkdirOp{Path: MustAbs("/")}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"etc", &MkdirOp{
Path: MustAbs("/etc/"),
}, "creating", `directory "/etc/" perm ----------`},
})
}
+184
View File
@@ -0,0 +1,184 @@
package container
import (
"encoding/gob"
"fmt"
"io/fs"
"slices"
"strings"
)
const (
// intermediate root file name pattern for [MountOverlayOp.Upper];
// remains after apply returns
intermediatePatternOverlayUpper = "overlay.upper.*"
// intermediate root file name pattern for [MountOverlayOp.Work];
// remains after apply returns
intermediatePatternOverlayWork = "overlay.work.*"
)
func init() { gob.Register(new(MountOverlayOp)) }
// Overlay appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target].
func (f *Ops) Overlay(target, state, work *Absolute, layers ...*Absolute) *Ops {
*f = append(*f, &MountOverlayOp{
Target: target,
Lower: layers,
Upper: state,
Work: work,
})
return f
}
// OverlayEphemeral appends an [Op] that mounts the overlay pseudo filesystem on [MountOverlayOp.Target]
// with an ephemeral upperdir and workdir.
func (f *Ops) OverlayEphemeral(target *Absolute, layers ...*Absolute) *Ops {
return f.Overlay(target, AbsFHSRoot, nil, layers...)
}
// OverlayReadonly appends an [Op] that mounts the overlay pseudo filesystem readonly on [MountOverlayOp.Target]
func (f *Ops) OverlayReadonly(target *Absolute, layers ...*Absolute) *Ops {
return f.Overlay(target, nil, nil, layers...)
}
// MountOverlayOp mounts [FstypeOverlay] on container path Target.
type MountOverlayOp struct {
Target *Absolute
// Any filesystem, does not need to be on a writable filesystem.
Lower []*Absolute
// formatted for [OptionOverlayLowerdir], resolved, prefixed and escaped during early
lower []string
// The upperdir is normally on a writable filesystem.
//
// If Work is nil and Upper holds the special value [AbsFHSRoot],
// an ephemeral upperdir and workdir will be set up.
//
// If both Work and Upper are nil, upperdir and workdir is omitted and the overlay is mounted readonly.
Upper *Absolute
// formatted for [OptionOverlayUpperdir], resolved, prefixed and escaped during early
upper string
// The workdir needs to be an empty directory on the same filesystem as upperdir.
Work *Absolute
// formatted for [OptionOverlayWorkdir], resolved, prefixed and escaped during early
work string
ephemeral bool
// used internally for mounting to the intermediate root
noPrefix bool
}
func (o *MountOverlayOp) Valid() bool {
if o == nil {
return false
}
if o.Work != nil && o.Upper == nil {
return false
}
if slices.Contains(o.Lower, nil) {
return false
}
return o.Target != nil
}
func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
if o.Work == nil && o.Upper != nil {
switch o.Upper.String() {
case FHSRoot: // ephemeral
o.ephemeral = true // intermediate root not yet available
default:
return msg.WrapErr(fs.ErrInvalid, fmt.Sprintf("upperdir has unexpected value %q", o.Upper))
}
}
// readonly handled in apply
if !o.ephemeral {
if o.Upper != o.Work && (o.Upper == nil || o.Work == nil) {
// unreachable
return msg.WrapErr(fs.ErrClosed, "impossible overlay state reached")
}
if o.Upper != nil {
if v, err := k.evalSymlinks(o.Upper.String()); err != nil {
return wrapErrSelf(err)
} else {
o.upper = EscapeOverlayDataSegment(toHost(v))
}
}
if o.Work != nil {
if v, err := k.evalSymlinks(o.Work.String()); err != nil {
return wrapErrSelf(err)
} else {
o.work = EscapeOverlayDataSegment(toHost(v))
}
}
}
o.lower = make([]string, len(o.Lower))
for i, a := range o.Lower { // nil checked in Valid
if v, err := k.evalSymlinks(a.String()); err != nil {
return wrapErrSelf(err)
} else {
o.lower[i] = EscapeOverlayDataSegment(toHost(v))
}
}
return nil
}
func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
target := o.Target.String()
if !o.noPrefix {
target = toSysroot(target)
}
if err := k.mkdirAll(target, state.ParentPerm); err != nil {
return wrapErrSelf(err)
}
if o.ephemeral {
var err error
// these directories are created internally, therefore early (absolute, symlink, prefix, escape) is bypassed
if o.upper, err = k.mkdirTemp(FHSRoot, intermediatePatternOverlayUpper); err != nil {
return wrapErrSelf(err)
}
if o.work, err = k.mkdirTemp(FHSRoot, intermediatePatternOverlayWork); err != nil {
return wrapErrSelf(err)
}
}
options := make([]string, 0, 4)
if o.upper == zeroString && o.work == zeroString { // readonly
if len(o.Lower) < 2 {
return msg.WrapErr(fs.ErrInvalid, "readonly overlay requires at least two lowerdir")
}
// "upperdir=" and "workdir=" may be omitted. In that case the overlay will be read-only
} else {
if len(o.Lower) == 0 {
return msg.WrapErr(fs.ErrInvalid, "overlay requires at least one lowerdir")
}
options = append(options,
OptionOverlayUpperdir+"="+o.upper,
OptionOverlayWorkdir+"="+o.work)
}
options = append(options,
OptionOverlayLowerdir+"="+strings.Join(o.lower, SpecialOverlayPath),
OptionOverlayUserxattr)
return wrapErrSuffix(k.mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, SpecialOverlayOption)),
fmt.Sprintf("cannot mount overlay on %q:", o.Target))
}
func (o *MountOverlayOp) Is(op Op) bool {
vo, ok := op.(*MountOverlayOp)
return ok && o.Valid() && vo.Valid() &&
o.Target.Is(vo.Target) &&
slices.EqualFunc(o.Lower, vo.Lower, func(a *Absolute, v *Absolute) bool { return a.Is(v) }) &&
o.Upper.Is(vo.Upper) && o.Work.Is(vo.Work)
}
func (*MountOverlayOp) prefix() string { return "mounting" }
func (o *MountOverlayOp) String() string {
return fmt.Sprintf("overlay on %q with %d layers", o.Target, len(o.Lower))
}
+368
View File
@@ -0,0 +1,368 @@
package container
import (
"errors"
"io/fs"
"os"
"testing"
)
func TestMountOverlayOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{
{"mkdirTemp invalid ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
Target: MustAbs("/"),
Lower: []*Absolute{
MustAbs("/var/lib/planterette/base/debian:f92c9052"),
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
},
Upper: MustAbs("/proc/"),
}, nil, msg.WrapErr(fs.ErrInvalid, `upperdir has unexpected value "/proc/"`), nil, nil},
{"mkdirTemp upper ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
Target: MustAbs("/"),
Lower: []*Absolute{
MustAbs("/var/lib/planterette/base/debian:f92c9052"),
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
},
Upper: MustAbs("/"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil},
}, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot", os.FileMode(0705)}, nil, nil},
{"mkdirTemp", expectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", errUnique},
}, wrapErrSelf(errUnique)},
{"mkdirTemp work ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
Target: MustAbs("/"),
Lower: []*Absolute{
MustAbs("/var/lib/planterette/base/debian:f92c9052"),
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
},
Upper: MustAbs("/"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil},
}, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot", os.FileMode(0705)}, nil, nil},
{"mkdirTemp", expectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil},
{"mkdirTemp", expectArgs{"/", "overlay.work.*"}, "overlay.work.32768", errUnique},
}, wrapErrSelf(errUnique)},
{"success ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{
Target: MustAbs("/"),
Lower: []*Absolute{
MustAbs("/var/lib/planterette/base/debian:f92c9052"),
MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"),
},
Upper: MustAbs("/"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil},
{"evalSymlinks", expectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil},
}, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot", os.FileMode(0705)}, nil, nil},
{"mkdirTemp", expectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil},
{"mkdirTemp", expectArgs{"/", "overlay.work.*"}, "overlay.work.32768", nil},
{"mount", expectArgs{"overlay", "/sysroot", "overlay", uintptr(0), "" +
"upperdir=overlay.upper.32768," +
"workdir=overlay.work.32768," +
"lowerdir=" +
`/host/var/lib/planterette/base/debian\:f92c9052:` +
`/host/var/lib/planterette/app/org.chromium.Chromium@debian\:f92c9052,` +
"userxattr"}, nil, nil},
}, nil},
{"short lower ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{
MustAbs("/mnt-root/nix/.ro-store"),
},
}, []kexpect{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil},
}, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil},
}, msg.WrapErr(fs.ErrInvalid, "readonly overlay requires at least two lowerdir")},
{"success ro noPrefix", &Params{ParentPerm: 0755}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{
MustAbs("/mnt-root/nix/.ro-store"),
MustAbs("/mnt-root/nix/.ro-store0"),
},
noPrefix: true,
}, []kexpect{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil},
}, nil, []kexpect{
{"mkdirAll", expectArgs{"/nix/store", os.FileMode(0755)}, nil, nil},
{"mount", expectArgs{"overlay", "/nix/store", "overlay", uintptr(0), "" +
"lowerdir=" +
"/host/mnt-root/nix/.ro-store:" +
"/host/mnt-root/nix/.ro-store0," +
"userxattr"}, nil, nil},
}, nil},
{"success ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{
MustAbs("/mnt-root/nix/.ro-store"),
MustAbs("/mnt-root/nix/.ro-store0"),
},
}, []kexpect{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil},
}, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil},
{"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
"lowerdir=" +
"/host/mnt-root/nix/.ro-store:" +
"/host/mnt-root/nix/.ro-store0," +
"userxattr"}, nil, nil},
}, nil},
{"nil lower", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
}, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil},
}, msg.WrapErr(fs.ErrInvalid, "overlay requires at least one lowerdir")},
{"evalSymlinks upper", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", errUnique},
}, wrapErrSelf(errUnique), nil, nil},
{"evalSymlinks work", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", errUnique},
}, wrapErrSelf(errUnique), nil, nil},
{"evalSymlinks lower", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", errUnique},
}, wrapErrSelf(errUnique), nil, nil},
{"mkdirAll", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil},
}, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, errUnique},
}, wrapErrSelf(errUnique)},
{"mount", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil},
}, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil},
{"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "upperdir=/host/mnt-root/nix/.rw-store/.upper,workdir=/host/mnt-root/nix/.rw-store/.work,lowerdir=/host/mnt-root/nix/ro-store,userxattr"}, nil, errUnique},
}, wrapErrSuffix(errUnique, `cannot mount overlay on "/nix/store":`)},
{"success single layer", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil},
}, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil},
{"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
"upperdir=/host/mnt-root/nix/.rw-store/.upper," +
"workdir=/host/mnt-root/nix/.rw-store/.work," +
"lowerdir=/host/mnt-root/nix/ro-store," +
"userxattr"}, nil, nil},
}, nil},
{"success", &Params{ParentPerm: 0700}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{
MustAbs("/mnt-root/nix/.ro-store"),
MustAbs("/mnt-root/nix/.ro-store0"),
MustAbs("/mnt-root/nix/.ro-store1"),
MustAbs("/mnt-root/nix/.ro-store2"),
MustAbs("/mnt-root/nix/.ro-store3"),
},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, []kexpect{
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/ro-store0", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store1"}, "/mnt-root/nix/ro-store1", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store2"}, "/mnt-root/nix/ro-store2", nil},
{"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store3"}, "/mnt-root/nix/ro-store3", nil},
}, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil},
{"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
"upperdir=/host/mnt-root/nix/.rw-store/.upper," +
"workdir=/host/mnt-root/nix/.rw-store/.work," +
"lowerdir=" +
"/host/mnt-root/nix/ro-store:" +
"/host/mnt-root/nix/ro-store0:" +
"/host/mnt-root/nix/ro-store1:" +
"/host/mnt-root/nix/ro-store2:" +
"/host/mnt-root/nix/ro-store3," +
"userxattr"}, nil, nil},
}, nil},
})
t.Run("unreachable", func(t *testing.T) {
t.Run("nil Upper non-nil Work not ephemeral", func(t *testing.T) {
wantErr := msg.WrapErr(fs.ErrClosed, "impossible overlay state reached")
if err := (&MountOverlayOp{
Work: MustAbs("/"),
}).early(nil, nil); !errors.Is(err, wantErr) {
t.Errorf("apply: error = %v, want %v", err, wantErr)
}
})
})
checkOpsValid(t, []opValidTestCase{
{"nil", (*MountOverlayOp)(nil), false},
{"zero", new(MountOverlayOp), false},
{"nil lower", &MountOverlayOp{Target: MustAbs("/"), Lower: []*Absolute{nil}}, false},
{"ro", &MountOverlayOp{Target: MustAbs("/"), Lower: []*Absolute{MustAbs("/")}}, true},
{"ro work", &MountOverlayOp{Target: MustAbs("/"), Work: MustAbs("/tmp/")}, false},
{"rw", &MountOverlayOp{Target: MustAbs("/"), Lower: []*Absolute{MustAbs("/")}, Upper: MustAbs("/"), Work: MustAbs("/")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"full", new(Ops).Overlay(
MustAbs("/nix/store"),
MustAbs("/mnt-root/nix/.rw-store/upper"),
MustAbs("/mnt-root/nix/.rw-store/work"),
MustAbs("/mnt-root/nix/.ro-store"),
), Ops{
&MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
},
}},
{"ephemeral", new(Ops).OverlayEphemeral(MustAbs("/nix/store"), MustAbs("/mnt-root/nix/.ro-store")), Ops{
&MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/"),
},
}},
{"readonly", new(Ops).OverlayReadonly(MustAbs("/nix/store"), MustAbs("/mnt-root/nix/.ro-store")), Ops{
&MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
},
}},
})
checkOpIs(t, []opIsTestCase{
{"zero", new(MountOverlayOp), new(MountOverlayOp), false},
{"differs target", &MountOverlayOp{
Target: MustAbs("/nix/store/differs"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work")}, false},
{"differs lower", &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store/differs")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work")}, false},
{"differs upper", &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper/differs"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work")}, false},
{"differs work", &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work/differs"),
}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work")}, false},
{"equals ro", &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")}}, true},
{"equals", &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work")}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"nix", &MountOverlayOp{
Target: MustAbs("/nix/store"),
Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")},
Upper: MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: MustAbs("/mnt-root/nix/.rw-store/work"),
}, "mounting", `overlay on "/nix/store" with 1 layers`},
})
}
+78
View File
@@ -0,0 +1,78 @@
package container
import (
"encoding/gob"
"fmt"
"syscall"
)
const (
// intermediate root file name pattern for [TmpfileOp]
intermediatePatternTmpfile = "tmp.*"
)
func init() { gob.Register(new(TmpfileOp)) }
// Place appends an [Op] that places a file in container path [TmpfileOp.Path] containing [TmpfileOp.Data].
func (f *Ops) Place(name *Absolute, data []byte) *Ops {
*f = append(*f, &TmpfileOp{name, data})
return f
}
// PlaceP is like Place but writes the address of [TmpfileOp.Data] to the pointer dataP points to.
func (f *Ops) PlaceP(name *Absolute, dataP **[]byte) *Ops {
t := &TmpfileOp{Path: name}
*dataP = &t.Data
*f = append(*f, t)
return f
}
// TmpfileOp places a file on container Path containing Data.
type TmpfileOp struct {
Path *Absolute
Data []byte
}
func (t *TmpfileOp) Valid() bool { return t != nil && t.Path != nil }
func (t *TmpfileOp) early(*setupState, syscallDispatcher) error { return nil }
func (t *TmpfileOp) apply(state *setupState, k syscallDispatcher) error {
var tmpPath string
if f, err := k.createTemp(FHSRoot, intermediatePatternTmpfile); err != nil {
return wrapErrSelf(err)
} else if _, err = f.Write(t.Data); err != nil {
return wrapErrSuffix(err,
"cannot write to intermediate file:")
} else if err = f.Close(); err != nil {
return wrapErrSuffix(err,
"cannot close intermediate file:")
} else {
tmpPath = f.Name()
}
target := toSysroot(t.Path.String())
if err := k.ensureFile(target, 0444, state.ParentPerm); err != nil {
return err
} else if err = k.bindMount(
tmpPath,
target,
syscall.MS_RDONLY|syscall.MS_NODEV,
false,
); err != nil {
return err
} else if err = k.remove(tmpPath); err != nil {
return wrapErrSelf(err)
}
return nil
}
func (t *TmpfileOp) Is(op Op) bool {
vt, ok := op.(*TmpfileOp)
return ok && t.Valid() && vt.Valid() &&
t.Path.Is(vt.Path) &&
string(t.Data) == string(vt.Data)
}
func (*TmpfileOp) prefix() string { return "placing" }
func (t *TmpfileOp) String() string {
return fmt.Sprintf("tmpfile %q (%d bytes)", t.Path, len(t.Data))
}
+131
View File
@@ -0,0 +1,131 @@
package container
import (
"os"
"testing"
)
func TestTmpfileOp(t *testing.T) {
const sampleDataString = `chronos:x:65534:65534:Hakurei:/var/empty:/bin/zsh`
var (
samplePath = MustAbs("/etc/passwd")
sampleData = []byte(sampleDataString)
)
checkOpBehaviour(t, []opBehaviourTestCase{
{"createTemp", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath,
Data: sampleData,
}, nil, nil, []kexpect{
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), errUnique},
}, wrapErrSelf(errUnique)},
{"Write", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath,
Data: sampleData,
}, nil, nil, []kexpect{
{"createTemp", expectArgs{"/", "tmp.*"}, writeErrOsFile{errUnique}, nil},
}, wrapErrSuffix(errUnique, "cannot write to intermediate file:")},
{"Close", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath,
Data: sampleData,
}, nil, nil, []kexpect{
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, errUnique), nil},
}, wrapErrSuffix(errUnique, "cannot close intermediate file:")},
{"ensureFile", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath,
Data: sampleData,
}, nil, nil, []kexpect{
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil},
{"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, errUnique},
}, errUnique},
{"bindMount", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath,
Data: sampleData,
}, nil, nil, []kexpect{
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil},
{"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil},
{"bindMount", expectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, errUnique},
}, errUnique},
{"remove", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath,
Data: sampleData,
}, nil, nil, []kexpect{
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil},
{"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil},
{"bindMount", expectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil},
{"remove", expectArgs{"tmp.32768"}, nil, errUnique},
}, wrapErrSelf(errUnique)},
{"success", &Params{ParentPerm: 0700}, &TmpfileOp{
Path: samplePath,
Data: sampleData,
}, nil, nil, []kexpect{
{"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil},
{"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil},
{"bindMount", expectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil},
{"remove", expectArgs{"tmp.32768"}, nil, nil},
}, nil},
})
checkOpsValid(t, []opValidTestCase{
{"nil", (*TmpfileOp)(nil), false},
{"zero", new(TmpfileOp), false},
{"valid", &TmpfileOp{Path: samplePath}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"noref", new(Ops).Place(samplePath, sampleData), Ops{
&TmpfileOp{
Path: samplePath,
Data: sampleData,
},
}},
{"ref", new(Ops).PlaceP(samplePath, new(*[]byte)), Ops{
&TmpfileOp{
Path: samplePath,
Data: []byte{},
},
}},
})
checkOpIs(t, []opIsTestCase{
{"zero", new(TmpfileOp), new(TmpfileOp), false},
{"differs path", &TmpfileOp{
Path: MustAbs("/etc/group"),
Data: sampleData,
}, &TmpfileOp{
Path: samplePath,
Data: sampleData,
}, false},
{"differs data", &TmpfileOp{
Path: samplePath,
Data: append(sampleData, 0),
}, &TmpfileOp{
Path: samplePath,
Data: sampleData,
}, false},
{"equals", &TmpfileOp{
Path: samplePath,
Data: sampleData,
}, &TmpfileOp{
Path: samplePath,
Data: sampleData,
}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"passwd", &TmpfileOp{
Path: samplePath,
Data: sampleData,
}, "placing", `tmpfile "/etc/passwd" (49 bytes)`},
})
}
+37
View File
@@ -0,0 +1,37 @@
package container
import (
"encoding/gob"
"fmt"
. "syscall"
)
func init() { gob.Register(new(MountProcOp)) }
// Proc appends an [Op] that mounts a private instance of proc.
func (f *Ops) Proc(target *Absolute) *Ops {
*f = append(*f, &MountProcOp{target})
return f
}
// MountProcOp mounts a new instance of [FstypeProc] on container path Target.
type MountProcOp struct{ Target *Absolute }
func (p *MountProcOp) Valid() bool { return p != nil && p.Target != nil }
func (p *MountProcOp) early(*setupState, syscallDispatcher) error { return nil }
func (p *MountProcOp) apply(state *setupState, k syscallDispatcher) error {
target := toSysroot(p.Target.String())
if err := k.mkdirAll(target, state.ParentPerm); err != nil {
return wrapErrSelf(err)
}
return wrapErrSuffix(k.mount(SourceProc, target, FstypeProc, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString),
fmt.Sprintf("cannot mount proc on %q:", p.Target.String()))
}
func (p *MountProcOp) Is(op Op) bool {
vp, ok := op.(*MountProcOp)
return ok && p.Valid() && vp.Valid() &&
p.Target.Is(vp.Target)
}
func (*MountProcOp) prefix() string { return "mounting" }
func (p *MountProcOp) String() string { return fmt.Sprintf("proc on %q", p.Target) }
+58
View File
@@ -0,0 +1,58 @@
package container
import (
"os"
"testing"
)
func TestMountProcOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{
{"mkdir", &Params{ParentPerm: 0755},
&MountProcOp{
Target: MustAbs("/proc/"),
}, nil, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, errUnique},
}, wrapErrSelf(errUnique)},
{"success", &Params{ParentPerm: 0700},
&MountProcOp{
Target: MustAbs("/proc/"),
}, nil, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/proc", os.FileMode(0700)}, nil, nil},
{"mount", expectArgs{"proc", "/sysroot/proc", "proc", uintptr(0xe), ""}, nil, nil},
}, nil},
})
checkOpsValid(t, []opValidTestCase{
{"nil", (*MountProcOp)(nil), false},
{"zero", new(MountProcOp), false},
{"valid", &MountProcOp{Target: MustAbs("/proc/")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"proc", new(Ops).Proc(MustAbs("/proc/")), Ops{
&MountProcOp{Target: MustAbs("/proc/")},
}},
})
checkOpIs(t, []opIsTestCase{
{"zero", new(MountProcOp), new(MountProcOp), false},
{"target differs", &MountProcOp{
Target: MustAbs("/proc/nonexistent"),
}, &MountProcOp{
Target: MustAbs("/proc/"),
}, false},
{"equals", &MountProcOp{
Target: MustAbs("/proc/"),
}, &MountProcOp{
Target: MustAbs("/proc/"),
}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"proc", &MountProcOp{Target: MustAbs("/proc/")},
"mounting", `proc on "/proc/"`},
})
}
+36
View File
@@ -0,0 +1,36 @@
package container
import (
"encoding/gob"
"fmt"
)
func init() { gob.Register(new(RemountOp)) }
// Remount appends an [Op] that applies [RemountOp.Flags] on container path [RemountOp.Target].
func (f *Ops) Remount(target *Absolute, flags uintptr) *Ops {
*f = append(*f, &RemountOp{target, flags})
return f
}
// RemountOp remounts Target with Flags.
type RemountOp struct {
Target *Absolute
Flags uintptr
}
func (r *RemountOp) Valid() bool { return r != nil && r.Target != nil }
func (*RemountOp) early(*setupState, syscallDispatcher) error { return nil }
func (r *RemountOp) apply(_ *setupState, k syscallDispatcher) error {
return wrapErrSuffix(k.remount(toSysroot(r.Target.String()), r.Flags),
fmt.Sprintf("cannot remount %q:", r.Target))
}
func (r *RemountOp) Is(op Op) bool {
vr, ok := op.(*RemountOp)
return ok && r.Valid() && vr.Valid() &&
r.Target.Is(vr.Target) &&
r.Flags == vr.Flags
}
func (*RemountOp) prefix() string { return "remounting" }
func (r *RemountOp) String() string { return fmt.Sprintf("%q flags %#x", r.Target, r.Flags) }
+67
View File
@@ -0,0 +1,67 @@
package container
import (
"syscall"
"testing"
)
func TestRemountOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{
{"success", new(Params), &RemountOp{
Target: MustAbs("/"),
Flags: syscall.MS_RDONLY,
}, nil, nil, []kexpect{
{"remount", expectArgs{"/sysroot", uintptr(1)}, nil, nil},
}, nil},
})
checkOpsValid(t, []opValidTestCase{
{"nil", (*RemountOp)(nil), false},
{"zero", new(RemountOp), false},
{"valid", &RemountOp{Target: MustAbs("/"), Flags: syscall.MS_RDONLY}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"root", new(Ops).Remount(MustAbs("/"), syscall.MS_RDONLY), Ops{
&RemountOp{
Target: MustAbs("/"),
Flags: syscall.MS_RDONLY,
},
}},
})
checkOpIs(t, []opIsTestCase{
{"zero", new(RemountOp), new(RemountOp), false},
{"target differs", &RemountOp{
Target: MustAbs("/dev/"),
Flags: syscall.MS_RDONLY,
}, &RemountOp{
Target: MustAbs("/"),
Flags: syscall.MS_RDONLY,
}, false},
{"flags differs", &RemountOp{
Target: MustAbs("/"),
Flags: syscall.MS_RDONLY | syscall.MS_NODEV,
}, &RemountOp{
Target: MustAbs("/"),
Flags: syscall.MS_RDONLY,
}, false},
{"equals", &RemountOp{
Target: MustAbs("/"),
Flags: syscall.MS_RDONLY,
}, &RemountOp{
Target: MustAbs("/"),
Flags: syscall.MS_RDONLY,
}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"root", &RemountOp{
Target: MustAbs("/"),
Flags: syscall.MS_RDONLY,
}, "remounting", `"/" flags 0x1`},
})
}
+62
View File
@@ -0,0 +1,62 @@
package container
import (
"encoding/gob"
"fmt"
"io/fs"
"path"
)
func init() { gob.Register(new(SymlinkOp)) }
// Link appends an [Op] that creates a symlink in the container filesystem.
func (f *Ops) Link(target *Absolute, linkName string, dereference bool) *Ops {
*f = append(*f, &SymlinkOp{target, linkName, dereference})
return f
}
// SymlinkOp optionally dereferences LinkName and creates a symlink at container path Target.
type SymlinkOp struct {
Target *Absolute
// LinkName is an arbitrary uninterpreted pathname.
LinkName string
// Dereference causes LinkName to be dereferenced during early.
Dereference bool
}
func (l *SymlinkOp) Valid() bool { return l != nil && l.Target != nil && l.LinkName != zeroString }
func (l *SymlinkOp) early(_ *setupState, k syscallDispatcher) error {
if l.Dereference {
if !isAbs(l.LinkName) {
return msg.WrapErr(fs.ErrInvalid, fmt.Sprintf("path %q is not absolute", l.LinkName))
}
if name, err := k.readlink(l.LinkName); err != nil {
return wrapErrSelf(err)
} else {
l.LinkName = name
}
}
return nil
}
func (l *SymlinkOp) apply(state *setupState, k syscallDispatcher) error {
target := toSysroot(l.Target.String())
if err := k.mkdirAll(path.Dir(target), state.ParentPerm); err != nil {
return wrapErrSelf(err)
}
return wrapErrSelf(k.symlink(l.LinkName, target))
}
func (l *SymlinkOp) Is(op Op) bool {
vl, ok := op.(*SymlinkOp)
return ok && l.Valid() && vl.Valid() &&
l.Target.Is(vl.Target) &&
l.LinkName == vl.LinkName &&
l.Dereference == vl.Dereference
}
func (*SymlinkOp) prefix() string { return "creating" }
func (l *SymlinkOp) String() string {
return fmt.Sprintf("symlink on %q linkname %q", l.Target, l.LinkName)
}
+124
View File
@@ -0,0 +1,124 @@
package container
import (
"io/fs"
"os"
"testing"
)
func TestSymlinkOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{
{"mkdir", &Params{ParentPerm: 0700}, &SymlinkOp{
Target: MustAbs("/etc/nixos"),
LinkName: "/etc/static/nixos",
}, nil, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, errUnique},
}, wrapErrSelf(errUnique)},
{"abs", &Params{ParentPerm: 0755}, &SymlinkOp{
Target: MustAbs("/etc/mtab"),
LinkName: "etc/mtab",
Dereference: true,
}, nil, msg.WrapErr(fs.ErrInvalid, `path "etc/mtab" is not absolute`), nil, nil},
{"readlink", &Params{ParentPerm: 0755}, &SymlinkOp{
Target: MustAbs("/etc/mtab"),
LinkName: "/etc/mtab",
Dereference: true,
}, []kexpect{
{"readlink", expectArgs{"/etc/mtab"}, "/proc/mounts", errUnique},
}, wrapErrSelf(errUnique), nil, nil},
{"success noderef", &Params{ParentPerm: 0700}, &SymlinkOp{
Target: MustAbs("/etc/nixos"),
LinkName: "/etc/static/nixos",
}, nil, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, nil},
{"symlink", expectArgs{"/etc/static/nixos", "/sysroot/etc/nixos"}, nil, nil},
}, nil},
{"success", &Params{ParentPerm: 0755}, &SymlinkOp{
Target: MustAbs("/etc/mtab"),
LinkName: "/etc/mtab",
Dereference: true,
}, []kexpect{
{"readlink", expectArgs{"/etc/mtab"}, "/proc/mounts", nil},
}, nil, []kexpect{
{"mkdirAll", expectArgs{"/sysroot/etc", os.FileMode(0755)}, nil, nil},
{"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil},
}, nil},
})
checkOpsValid(t, []opValidTestCase{
{"nil", (*SymlinkOp)(nil), false},
{"zero", new(SymlinkOp), false},
{"nil target", &SymlinkOp{LinkName: "/run/current-system"}, false},
{"zero linkname", &SymlinkOp{Target: MustAbs("/run/current-system")}, false},
{"valid", &SymlinkOp{Target: MustAbs("/run/current-system"), LinkName: "/run/current-system", Dereference: true}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"current-system", new(Ops).Link(
MustAbs("/run/current-system"),
"/run/current-system",
true,
), Ops{
&SymlinkOp{
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
},
}},
})
checkOpIs(t, []opIsTestCase{
{"zero", new(SymlinkOp), new(SymlinkOp), false},
{"target differs", &SymlinkOp{
Target: MustAbs("/run/current-system/differs"),
LinkName: "/run/current-system",
Dereference: true,
}, &SymlinkOp{
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
}, false},
{"linkname differs", &SymlinkOp{
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system/differs",
Dereference: true,
}, &SymlinkOp{
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
}, false},
{"dereference differs", &SymlinkOp{
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system",
}, &SymlinkOp{
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
}, false},
{"equals", &SymlinkOp{
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
}, &SymlinkOp{
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"current-system", &SymlinkOp{
Target: MustAbs("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
}, "creating", `symlink on "/run/current-system" linkname "/run/current-system"`},
})
}
+54
View File
@@ -0,0 +1,54 @@
package container
import (
"encoding/gob"
"fmt"
"io/fs"
"math"
"os"
. "syscall"
)
func init() { gob.Register(new(MountTmpfsOp)) }
// Tmpfs appends an [Op] that mounts tmpfs on container path [MountTmpfsOp.Path].
func (f *Ops) Tmpfs(target *Absolute, size int, perm os.FileMode) *Ops {
*f = append(*f, &MountTmpfsOp{SourceTmpfsEphemeral, target, MS_NOSUID | MS_NODEV, size, perm})
return f
}
// Readonly appends an [Op] that mounts read-only tmpfs on container path [MountTmpfsOp.Path].
func (f *Ops) Readonly(target *Absolute, perm os.FileMode) *Ops {
*f = append(*f, &MountTmpfsOp{SourceTmpfsReadonly, target, MS_RDONLY | MS_NOSUID | MS_NODEV, 0, perm})
return f
}
// MountTmpfsOp mounts [FstypeTmpfs] on container Path.
type MountTmpfsOp struct {
FSName string
Path *Absolute
Flags uintptr
Size int
Perm os.FileMode
}
func (t *MountTmpfsOp) Valid() bool { return t != nil && t.Path != nil && t.FSName != zeroString }
func (t *MountTmpfsOp) early(*setupState, syscallDispatcher) error { return nil }
func (t *MountTmpfsOp) apply(_ *setupState, k syscallDispatcher) error {
if t.Size < 0 || t.Size > math.MaxUint>>1 {
return msg.WrapErr(fs.ErrInvalid, fmt.Sprintf("size %d out of bounds", t.Size))
}
return k.mountTmpfs(t.FSName, toSysroot(t.Path.String()), t.Flags, t.Size, t.Perm)
}
func (t *MountTmpfsOp) Is(op Op) bool {
vt, ok := op.(*MountTmpfsOp)
return ok && t.Valid() && vt.Valid() &&
t.FSName == vt.FSName &&
t.Path.Is(vt.Path) &&
t.Flags == vt.Flags &&
t.Size == vt.Size &&
t.Perm == vt.Perm
}
func (*MountTmpfsOp) prefix() string { return "mounting" }
func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }
+165
View File
@@ -0,0 +1,165 @@
package container
import (
"io/fs"
"os"
"syscall"
"testing"
)
func TestMountTmpfsOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{
{"size oob", new(Params), &MountTmpfsOp{
Size: -1,
}, nil, nil, nil, msg.WrapErr(fs.ErrInvalid, "size -1 out of bounds")},
{"success", new(Params), &MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user/1000/"),
Size: 1 << 10,
Perm: 0700,
}, nil, nil, []kexpect{
{"mountTmpfs", expectArgs{
"ephemeral", // fsname
"/sysroot/run/user/1000", // target
uintptr(0), // flags
0x400, // size
os.FileMode(0700), // perm
}, nil, nil},
}, nil},
})
checkOpsValid(t, []opValidTestCase{
{"nil", (*MountTmpfsOp)(nil), false},
{"zero", new(MountTmpfsOp), false},
{"nil path", &MountTmpfsOp{FSName: "tmpfs"}, false},
{"zero fsname", &MountTmpfsOp{Path: MustAbs("/tmp/")}, false},
{"valid", &MountTmpfsOp{FSName: "tmpfs", Path: MustAbs("/tmp/")}, true},
})
checkOpsBuilder(t, []opsBuilderTestCase{
{"runtime", new(Ops).Tmpfs(
MustAbs("/run/user"),
1<<10,
0755,
), Ops{
&MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
},
}},
{"nscd", new(Ops).Readonly(
MustAbs("/var/run/nscd"),
0755,
), Ops{
&MountTmpfsOp{
FSName: "readonly",
Path: MustAbs("/var/run/nscd"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV | syscall.MS_RDONLY,
Perm: 0755,
},
}},
})
checkOpIs(t, []opIsTestCase{
{"zero", new(MountTmpfsOp), new(MountTmpfsOp), false},
{"fsname differs", &MountTmpfsOp{
FSName: "readonly",
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
}, &MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
}, false},
{"path differs", &MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user/differs"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
}, &MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
}, false},
{"flags differs", &MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV | syscall.MS_RDONLY,
Size: 1 << 10,
Perm: 0755,
}, &MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
}, false},
{"size differs", &MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1,
Perm: 0755,
}, &MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
}, false},
{"perm differs", &MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0700,
}, &MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
}, false},
{"equals", &MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
}, &MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
}, true},
})
checkOpMeta(t, []opMetaTestCase{
{"runtime", &MountTmpfsOp{
FSName: "ephemeral",
Path: MustAbs("/run/user"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0755,
}, "mounting", `tmpfs on "/run/user" size 1024`},
})
}
+239
View File
@@ -0,0 +1,239 @@
package container
import (
"strings"
"syscall"
"unsafe"
"hakurei.app/container/seccomp"
)
// include/uapi/linux/landlock.h
const (
LANDLOCK_CREATE_RULESET_VERSION = 1 << iota
)
type LandlockAccessFS uintptr
const (
LANDLOCK_ACCESS_FS_EXECUTE LandlockAccessFS = 1 << iota
LANDLOCK_ACCESS_FS_WRITE_FILE
LANDLOCK_ACCESS_FS_READ_FILE
LANDLOCK_ACCESS_FS_READ_DIR
LANDLOCK_ACCESS_FS_REMOVE_DIR
LANDLOCK_ACCESS_FS_REMOVE_FILE
LANDLOCK_ACCESS_FS_MAKE_CHAR
LANDLOCK_ACCESS_FS_MAKE_DIR
LANDLOCK_ACCESS_FS_MAKE_REG
LANDLOCK_ACCESS_FS_MAKE_SOCK
LANDLOCK_ACCESS_FS_MAKE_FIFO
LANDLOCK_ACCESS_FS_MAKE_BLOCK
LANDLOCK_ACCESS_FS_MAKE_SYM
LANDLOCK_ACCESS_FS_REFER
LANDLOCK_ACCESS_FS_TRUNCATE
LANDLOCK_ACCESS_FS_IOCTL_DEV
_LANDLOCK_ACCESS_FS_DELIM
)
func (f LandlockAccessFS) String() string {
switch f {
case LANDLOCK_ACCESS_FS_EXECUTE:
return "execute"
case LANDLOCK_ACCESS_FS_WRITE_FILE:
return "write_file"
case LANDLOCK_ACCESS_FS_READ_FILE:
return "read_file"
case LANDLOCK_ACCESS_FS_READ_DIR:
return "read_dir"
case LANDLOCK_ACCESS_FS_REMOVE_DIR:
return "remove_dir"
case LANDLOCK_ACCESS_FS_REMOVE_FILE:
return "remove_file"
case LANDLOCK_ACCESS_FS_MAKE_CHAR:
return "make_char"
case LANDLOCK_ACCESS_FS_MAKE_DIR:
return "make_dir"
case LANDLOCK_ACCESS_FS_MAKE_REG:
return "make_reg"
case LANDLOCK_ACCESS_FS_MAKE_SOCK:
return "make_sock"
case LANDLOCK_ACCESS_FS_MAKE_FIFO:
return "make_fifo"
case LANDLOCK_ACCESS_FS_MAKE_BLOCK:
return "make_block"
case LANDLOCK_ACCESS_FS_MAKE_SYM:
return "make_sym"
case LANDLOCK_ACCESS_FS_REFER:
return "fs_refer"
case LANDLOCK_ACCESS_FS_TRUNCATE:
return "fs_truncate"
case LANDLOCK_ACCESS_FS_IOCTL_DEV:
return "fs_ioctl_dev"
default:
var c []LandlockAccessFS
for i := LandlockAccessFS(1); i < _LANDLOCK_ACCESS_FS_DELIM; i <<= 1 {
if f&i != 0 {
c = append(c, i)
}
}
if len(c) == 0 {
return "NULL"
}
s := make([]string, len(c))
for i, v := range c {
s[i] = v.String()
}
return strings.Join(s, " ")
}
}
type LandlockAccessNet uintptr
const (
LANDLOCK_ACCESS_NET_BIND_TCP LandlockAccessNet = 1 << iota
LANDLOCK_ACCESS_NET_CONNECT_TCP
_LANDLOCK_ACCESS_NET_DELIM
)
func (f LandlockAccessNet) String() string {
switch f {
case LANDLOCK_ACCESS_NET_BIND_TCP:
return "bind_tcp"
case LANDLOCK_ACCESS_NET_CONNECT_TCP:
return "connect_tcp"
default:
var c []LandlockAccessNet
for i := LandlockAccessNet(1); i < _LANDLOCK_ACCESS_NET_DELIM; i <<= 1 {
if f&i != 0 {
c = append(c, i)
}
}
if len(c) == 0 {
return "NULL"
}
s := make([]string, len(c))
for i, v := range c {
s[i] = v.String()
}
return strings.Join(s, " ")
}
}
type LandlockScope uintptr
const (
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET LandlockScope = 1 << iota
LANDLOCK_SCOPE_SIGNAL
_LANDLOCK_SCOPE_DELIM
)
func (f LandlockScope) String() string {
switch f {
case LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET:
return "abstract_unix_socket"
case LANDLOCK_SCOPE_SIGNAL:
return "signal"
default:
var c []LandlockScope
for i := LandlockScope(1); i < _LANDLOCK_SCOPE_DELIM; i <<= 1 {
if f&i != 0 {
c = append(c, i)
}
}
if len(c) == 0 {
return "NULL"
}
s := make([]string, len(c))
for i, v := range c {
s[i] = v.String()
}
return strings.Join(s, " ")
}
}
type RulesetAttr struct {
// Bitmask of handled filesystem actions.
HandledAccessFS LandlockAccessFS
// Bitmask of handled network actions.
HandledAccessNet LandlockAccessNet
// Bitmask of scopes restricting a Landlock domain from accessing outside resources (e.g. IPCs).
Scoped LandlockScope
}
func (rulesetAttr *RulesetAttr) String() string {
if rulesetAttr == nil {
return "NULL"
}
elems := make([]string, 0, 3)
if rulesetAttr.HandledAccessFS > 0 {
elems = append(elems, "fs: "+rulesetAttr.HandledAccessFS.String())
}
if rulesetAttr.HandledAccessNet > 0 {
elems = append(elems, "net: "+rulesetAttr.HandledAccessNet.String())
}
if rulesetAttr.Scoped > 0 {
elems = append(elems, "scoped: "+rulesetAttr.Scoped.String())
}
if len(elems) == 0 {
return "0"
}
return strings.Join(elems, ", ")
}
func (rulesetAttr *RulesetAttr) Create(flags uintptr) (fd int, err error) {
var pointer, size uintptr
// NULL needed for abi version
if rulesetAttr != nil {
pointer = uintptr(unsafe.Pointer(rulesetAttr))
size = unsafe.Sizeof(*rulesetAttr)
}
rulesetFd, _, errno := syscall.Syscall(seccomp.SYS_LANDLOCK_CREATE_RULESET, pointer, size, flags)
fd = int(rulesetFd)
err = errno
if fd < 0 {
return
}
if rulesetAttr != nil { // not a fd otherwise
syscall.CloseOnExec(fd)
}
return fd, nil
}
func LandlockGetABI() (int, error) {
return (*RulesetAttr)(nil).Create(LANDLOCK_CREATE_RULESET_VERSION)
}
func LandlockRestrictSelf(rulesetFd int, flags uintptr) error {
r, _, errno := syscall.Syscall(seccomp.SYS_LANDLOCK_RESTRICT_SELF, uintptr(rulesetFd), flags, 0)
if r != 0 {
return errno
}
return nil
}
+61
View File
@@ -0,0 +1,61 @@
package container_test
import (
"testing"
"unsafe"
"hakurei.app/container"
)
func TestLandlockString(t *testing.T) {
testCases := []struct {
name string
rulesetAttr *container.RulesetAttr
want string
}{
{"nil", nil, "NULL"},
{"zero", new(container.RulesetAttr), "0"},
{"some", &container.RulesetAttr{Scoped: container.LANDLOCK_SCOPE_SIGNAL}, "scoped: signal"},
{"set", &container.RulesetAttr{
HandledAccessFS: container.LANDLOCK_ACCESS_FS_MAKE_SYM | container.LANDLOCK_ACCESS_FS_IOCTL_DEV | container.LANDLOCK_ACCESS_FS_WRITE_FILE,
HandledAccessNet: container.LANDLOCK_ACCESS_NET_BIND_TCP,
Scoped: container.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | container.LANDLOCK_SCOPE_SIGNAL,
}, "fs: write_file make_sym fs_ioctl_dev, net: bind_tcp, scoped: abstract_unix_socket signal"},
{"all", &container.RulesetAttr{
HandledAccessFS: container.LANDLOCK_ACCESS_FS_EXECUTE |
container.LANDLOCK_ACCESS_FS_WRITE_FILE |
container.LANDLOCK_ACCESS_FS_READ_FILE |
container.LANDLOCK_ACCESS_FS_READ_DIR |
container.LANDLOCK_ACCESS_FS_REMOVE_DIR |
container.LANDLOCK_ACCESS_FS_REMOVE_FILE |
container.LANDLOCK_ACCESS_FS_MAKE_CHAR |
container.LANDLOCK_ACCESS_FS_MAKE_DIR |
container.LANDLOCK_ACCESS_FS_MAKE_REG |
container.LANDLOCK_ACCESS_FS_MAKE_SOCK |
container.LANDLOCK_ACCESS_FS_MAKE_FIFO |
container.LANDLOCK_ACCESS_FS_MAKE_BLOCK |
container.LANDLOCK_ACCESS_FS_MAKE_SYM |
container.LANDLOCK_ACCESS_FS_REFER |
container.LANDLOCK_ACCESS_FS_TRUNCATE |
container.LANDLOCK_ACCESS_FS_IOCTL_DEV,
HandledAccessNet: container.LANDLOCK_ACCESS_NET_BIND_TCP |
container.LANDLOCK_ACCESS_NET_CONNECT_TCP,
Scoped: container.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
container.LANDLOCK_SCOPE_SIGNAL,
}, "fs: execute write_file read_file read_dir remove_dir remove_file make_char make_dir make_reg make_sock make_fifo make_block make_sym fs_refer fs_truncate fs_ioctl_dev, net: bind_tcp connect_tcp, scoped: abstract_unix_socket signal"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := tc.rulesetAttr.String(); got != tc.want {
t.Errorf("String: %s, want %s", got, tc.want)
}
})
}
}
func TestLandlockAttrSize(t *testing.T) {
want := 24
if got := unsafe.Sizeof(container.RulesetAttr{}); got != uintptr(want) {
t.Errorf("Sizeof: %d, want %d", got, want)
}
}
+247
View File
@@ -0,0 +1,247 @@
package container
import (
"errors"
"fmt"
"os"
"strings"
. "syscall"
"hakurei.app/container/vfs"
)
/*
Holding CAP_SYS_ADMIN within the user namespace that owns a process's mount namespace
allows that process to create bind mounts and mount the following types of filesystems:
- /proc (since Linux 3.8)
- /sys (since Linux 3.8)
- devpts (since Linux 3.9)
- tmpfs(5) (since Linux 3.9)
- ramfs (since Linux 3.9)
- mqueue (since Linux 3.9)
- bpf (since Linux 4.4)
- overlayfs (since Linux 5.11)
*/
const (
// zeroString is a zero value string, it represents NULL when passed to mount.
zeroString = ""
// SourceNone is used when the source value is ignored,
// such as when remounting.
SourceNone = "none"
// SourceProc is used when mounting proc.
// Note that any source value is allowed when fstype is [FstypeProc].
SourceProc = "proc"
// SourceDevpts is used when mounting devpts.
// Note that any source value is allowed when fstype is [FstypeDevpts].
SourceDevpts = "devpts"
// SourceMqueue is used when mounting mqueue.
// Note that any source value is allowed when fstype is [FstypeMqueue].
SourceMqueue = "mqueue"
// SourceOverlay is used when mounting overlay.
// Note that any source value is allowed when fstype is [FstypeOverlay].
SourceOverlay = "overlay"
// SourceTmpfs is used when mounting tmpfs.
SourceTmpfs = "tmpfs"
// SourceTmpfsRootfs is used when mounting the tmpfs instance backing the intermediate root.
SourceTmpfsRootfs = "rootfs"
// SourceTmpfsDevtmpfs is used when mounting tmpfs representing a subset of host devtmpfs.
SourceTmpfsDevtmpfs = "devtmpfs"
// SourceTmpfsEphemeral is used when mounting a writable instance of tmpfs.
SourceTmpfsEphemeral = "ephemeral"
// SourceTmpfsReadonly is used when mounting a readonly instance of tmpfs.
SourceTmpfsReadonly = "readonly"
// FstypeNULL is used when the fstype value is ignored,
// such as when bind mounting or remounting.
FstypeNULL = zeroString
// FstypeProc represents the proc pseudo-filesystem.
// A fully visible instance of proc must be available in the mount namespace for proc to be mounted.
// This filesystem type is usually mounted on [FHSProc].
FstypeProc = "proc"
// FstypeDevpts represents the devpts pseudo-filesystem.
// This type of filesystem is usually mounted on /dev/pts.
FstypeDevpts = "devpts"
// FstypeTmpfs represents the tmpfs filesystem.
// This filesystem type can be mounted anywhere in the container filesystem.
FstypeTmpfs = "tmpfs"
// FstypeMqueue represents the mqueue pseudo-filesystem.
// This filesystem type is usually mounted on /dev/mqueue.
FstypeMqueue = "mqueue"
// FstypeOverlay represents the overlay pseudo-filesystem.
// This filesystem type can be mounted anywhere in the container filesystem.
FstypeOverlay = "overlay"
// OptionOverlayLowerdir represents the lowerdir option of the overlay pseudo-filesystem.
// Any filesystem, does not need to be on a writable filesystem.
OptionOverlayLowerdir = "lowerdir"
// OptionOverlayUpperdir represents the upperdir option of the overlay pseudo-filesystem.
// The upperdir is normally on a writable filesystem.
OptionOverlayUpperdir = "upperdir"
// OptionOverlayWorkdir represents the workdir option of the overlay pseudo-filesystem.
// The workdir needs to be an empty directory on the same filesystem as upperdir.
OptionOverlayWorkdir = "workdir"
// OptionOverlayUserxattr represents the userxattr option of the overlay pseudo-filesystem.
// Use the "user.overlay." xattr namespace instead of "trusted.overlay.".
OptionOverlayUserxattr = "userxattr"
// SpecialOverlayEscape is the escape string for overlay mount options.
SpecialOverlayEscape = `\`
// SpecialOverlayOption is the separator string between overlay mount options.
SpecialOverlayOption = ","
// SpecialOverlayPath is the separator string between overlay paths.
SpecialOverlayPath = ":"
)
// bindMount mounts source on target and recursively applies flags if MS_REC is set.
func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) error {
// syscallDispatcher.bindMount and procPaths.remount must not be called from this function
if eq {
p.k.verbosef("resolved %q flags %#x", target, flags)
} else {
p.k.verbosef("resolved %q on %q flags %#x", source, target, flags)
}
if err := p.k.mount(source, target, FstypeNULL, MS_SILENT|MS_BIND|flags&MS_REC, zeroString); err != nil {
return wrapErrSuffix(err,
fmt.Sprintf("cannot mount %q on %q:", source, target))
}
return p.k.remount(target, flags)
}
// remount applies flags on target, recursively if MS_REC is set.
func (p *procPaths) remount(target string, flags uintptr) error {
// syscallDispatcher methods bindMount, remount must not be called from this function
var targetFinal string
if v, err := p.k.evalSymlinks(target); err != nil {
return wrapErrSelf(err)
} else {
targetFinal = v
if targetFinal != target {
p.k.verbosef("target resolves to %q", targetFinal)
}
}
// final target path according to the kernel through proc
var targetKFinal string
{
var destFd int
if err := IgnoringEINTR(func() (err error) {
destFd, err = p.k.open(targetFinal, O_PATH|O_CLOEXEC, 0)
return
}); err != nil {
return wrapErrSuffix(err,
fmt.Sprintf("cannot open %q:", targetFinal))
}
if v, err := p.k.readlink(p.fd(destFd)); err != nil {
return wrapErrSelf(err)
} else if err = p.k.close(destFd); err != nil {
return wrapErrSuffix(err,
fmt.Sprintf("cannot close %q:", targetFinal))
} else {
targetKFinal = v
}
}
mf := MS_NOSUID | flags&MS_NODEV | flags&MS_RDONLY
return p.mountinfo(func(d *vfs.MountInfoDecoder) error {
n, err := d.Unfold(targetKFinal)
if err != nil {
if errors.Is(err, ESTALE) {
return msg.WrapErr(err,
fmt.Sprintf("mount point %q never appeared in mountinfo", targetKFinal))
}
return wrapErrSuffix(err,
"cannot unfold mount hierarchy:")
}
if err = remountWithFlags(p.k, n, mf); err != nil {
return wrapErrSuffix(err,
fmt.Sprintf("cannot remount %q:", n.Clean))
}
if flags&MS_REC == 0 {
return nil
}
for cur := range n.Collective() {
// avoid remounting twice
if cur == n {
continue
}
err = remountWithFlags(p.k, cur, mf)
if err != nil && !errors.Is(err, EACCES) {
return wrapErrSuffix(err,
fmt.Sprintf("cannot propagate flags to %q:", cur.Clean))
}
}
return nil
})
}
// remountWithFlags remounts mount point described by [vfs.MountInfoNode].
func remountWithFlags(k syscallDispatcher, n *vfs.MountInfoNode, mf uintptr) error {
// syscallDispatcher methods bindMount, remount must not be called from this function
kf, unmatched := n.Flags()
if len(unmatched) != 0 {
k.verbosef("unmatched vfs options: %q", unmatched)
}
if kf&mf != mf {
return k.mount(SourceNone, n.Clean, FstypeNULL, MS_SILENT|MS_BIND|MS_REMOUNT|kf|mf, zeroString)
}
return nil
}
// mountTmpfs mounts tmpfs on target;
// callers who wish to mount to sysroot must pass the return value of toSysroot.
func mountTmpfs(k syscallDispatcher, fsname, target string, flags uintptr, size int, perm os.FileMode) error {
// syscallDispatcher.mountTmpfs must not be called from this function
if err := k.mkdirAll(target, parentPerm(perm)); err != nil {
return wrapErrSelf(err)
}
opt := fmt.Sprintf("mode=%#o", perm)
if size > 0 {
opt += fmt.Sprintf(",size=%d", size)
}
return wrapErrSuffix(
k.mount(fsname, target, FstypeTmpfs, flags, opt),
fmt.Sprintf("cannot mount tmpfs on %q:", target))
}
func parentPerm(perm os.FileMode) os.FileMode {
pperm := 0755
if perm&0070 == 0 {
pperm &= ^0050
}
if perm&0007 == 0 {
pperm &= ^0005
}
return os.FileMode(pperm)
}
// EscapeOverlayDataSegment escapes a string for formatting into the data argument of an overlay mount call.
func EscapeOverlayDataSegment(s string) string {
if s == zeroString {
return zeroString
}
if f := strings.SplitN(s, "\x00", 2); len(f) > 0 {
s = f[0]
}
return strings.NewReplacer(
SpecialOverlayEscape, SpecialOverlayEscape+SpecialOverlayEscape,
SpecialOverlayOption, SpecialOverlayEscape+SpecialOverlayOption,
SpecialOverlayPath, SpecialOverlayEscape+SpecialOverlayPath,
).Replace(s)
}
+305
View File
@@ -0,0 +1,305 @@
package container
import (
"os"
"syscall"
"testing"
"hakurei.app/container/vfs"
)
func TestBindMount(t *testing.T) {
checkSimple(t, "bindMount", []simpleTestCase{
{"mount", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY, true)
}, [][]kexpect{{
{"verbosef", expectArgs{"resolved %q flags %#x", []any{"/sysroot/nix", uintptr(1)}}, nil, nil},
{"mount", expectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, errUnique},
}}, wrapErrSuffix(errUnique, `cannot mount "/host/nix" on "/sysroot/nix":`)},
{"success ne", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/.host-nix", syscall.MS_RDONLY, false)
}, [][]kexpect{{
{"verbosef", expectArgs{"resolved %q on %q flags %#x", []any{"/host/nix", "/sysroot/.host-nix", uintptr(1)}}, nil, nil},
{"mount", expectArgs{"/host/nix", "/sysroot/.host-nix", "", uintptr(0x9000), ""}, nil, nil},
{"remount", expectArgs{"/sysroot/.host-nix", uintptr(1)}, nil, nil},
}}, nil},
{"success", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY, true)
}, [][]kexpect{{
{"verbosef", expectArgs{"resolved %q flags %#x", []any{"/sysroot/nix", uintptr(1)}}, nil, nil},
{"mount", expectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, nil},
{"remount", expectArgs{"/sysroot/nix", uintptr(1)}, nil, nil},
}}, nil},
})
}
func TestRemount(t *testing.T) {
const sampleMountinfoNix = `254 407 253:0 / /host rw,relatime master:1 - ext4 /dev/disk/by-label/nixos rw
255 254 0:28 / /host/mnt/.ro-cwd ro,noatime master:2 - 9p cwd ro,access=client,msize=16384,trans=virtio
256 254 0:29 / /host/nix/.ro-store rw,relatime master:3 - 9p nix-store rw,cache=f,access=client,msize=16384,trans=virtio
257 254 0:30 / /host/nix/store rw,relatime master:4 - overlay overlay rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work
258 257 0:30 / /host/nix/store ro,relatime master:5 - overlay overlay rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work
259 254 0:33 / /host/tmp/shared rw,relatime master:6 - 9p shared rw,access=client,msize=16384,trans=virtio
260 254 0:34 / /host/tmp/xchg rw,relatime master:7 - 9p xchg rw,access=client,msize=16384,trans=virtio
261 254 0:22 / /host/proc rw,nosuid,nodev,noexec,relatime master:8 - proc proc rw
262 254 0:25 / /host/sys rw,nosuid,nodev,noexec,relatime master:9 - sysfs sysfs rw
263 262 0:7 / /host/sys/kernel/security rw,nosuid,nodev,noexec,relatime master:10 - securityfs securityfs rw
264 262 0:35 /../../.. /host/sys/fs/cgroup rw,nosuid,nodev,noexec,relatime master:11 - cgroup2 cgroup2 rw,nsdelegate,memory_recursiveprot
265 262 0:36 / /host/sys/fs/pstore rw,nosuid,nodev,noexec,relatime master:12 - pstore pstore rw
266 262 0:37 / /host/sys/fs/bpf rw,nosuid,nodev,noexec,relatime master:13 - bpf bpf rw,mode=700
267 262 0:12 / /host/sys/kernel/tracing rw,nosuid,nodev,noexec,relatime master:20 - tracefs tracefs rw
268 262 0:8 / /host/sys/kernel/debug rw,nosuid,nodev,noexec,relatime master:21 - debugfs debugfs rw
269 262 0:44 / /host/sys/kernel/config rw,nosuid,nodev,noexec,relatime master:64 - configfs configfs rw
270 262 0:45 / /host/sys/fs/fuse/connections rw,nosuid,nodev,noexec,relatime master:66 - fusectl fusectl rw
271 254 0:6 / /host/dev rw,nosuid master:14 - devtmpfs devtmpfs rw,size=200532k,nr_inodes=498943,mode=755
324 271 0:20 / /host/dev/pts rw,nosuid,noexec,relatime master:15 - devpts devpts rw,gid=3,mode=620,ptmxmode=666
378 271 0:21 / /host/dev/shm rw,nosuid,nodev master:16 - tmpfs tmpfs rw
379 271 0:19 / /host/dev/mqueue rw,nosuid,nodev,noexec,relatime master:19 - mqueue mqueue rw
388 271 0:38 / /host/dev/hugepages rw,nosuid,nodev,relatime master:22 - hugetlbfs hugetlbfs rw,pagesize=2M
397 254 0:23 / /host/run rw,nosuid,nodev master:17 - tmpfs tmpfs rw,size=1002656k,mode=755
398 397 0:24 / /host/run/keys rw,nosuid,nodev,relatime master:18 - ramfs ramfs rw,mode=750
399 397 0:39 / /host/run/credentials/systemd-journald.service ro,nosuid,nodev,noexec,relatime,nosymfollow master:23 - tmpfs tmpfs rw,size=1024k,nr_inodes=1024,mode=700,noswap
400 397 0:43 / /host/run/wrappers rw,nodev,relatime master:93 - tmpfs tmpfs rw,mode=755
401 397 0:61 / /host/run/credentials/getty@tty1.service ro,nosuid,nodev,noexec,relatime,nosymfollow master:240 - tmpfs tmpfs rw,size=1024k,nr_inodes=1024,mode=700,noswap
402 397 0:62 / /host/run/credentials/serial-getty@ttyS0.service ro,nosuid,nodev,noexec,relatime,nosymfollow master:288 - tmpfs tmpfs rw,size=1024k,nr_inodes=1024,mode=700,noswap
403 397 0:63 / /host/run/user/1000 rw,nosuid,nodev,relatime master:295 - tmpfs tmpfs rw,size=401060k,nr_inodes=100265,mode=700,uid=1000,gid=100
404 254 0:46 / /host/mnt/cwd rw,relatime master:96 - overlay overlay rw,lowerdir=/mnt/.ro-cwd,upperdir=/tmp/.cwd/upper,workdir=/tmp/.cwd/work
405 254 0:47 / /host/mnt/src rw,relatime master:99 - overlay overlay rw,lowerdir=/nix/store/ihcrl3zwvp2002xyylri2wz0drwajx4z-ns0pa7q2b1jpx9pbf1l9352x6rniwxjn-source,upperdir=/tmp/.src/upper,workdir=/tmp/.src/work
407 253 0:65 / / rw,nosuid,nodev,relatime - tmpfs rootfs rw,uid=1000000,gid=1000000
408 407 0:65 /sysroot /sysroot rw,nosuid,nodev,relatime - tmpfs rootfs rw,uid=1000000,gid=1000000
409 408 253:0 /bin /sysroot/bin rw,nosuid,nodev,relatime master:1 - ext4 /dev/disk/by-label/nixos rw
410 408 253:0 /home /sysroot/home rw,nosuid,nodev,relatime master:1 - ext4 /dev/disk/by-label/nixos rw
411 408 253:0 /lib64 /sysroot/lib64 rw,nosuid,nodev,relatime master:1 - ext4 /dev/disk/by-label/nixos rw
412 408 253:0 /lost+found /sysroot/lost+found rw,nosuid,nodev,relatime master:1 - ext4 /dev/disk/by-label/nixos rw
413 408 253:0 /nix /sysroot/nix rw,relatime master:1 - ext4 /dev/disk/by-label/nixos rw
414 413 0:29 / /sysroot/nix/.ro-store rw,relatime master:3 - 9p nix-store rw,cache=f,access=client,msize=16384,trans=virtio
415 413 0:30 / /sysroot/nix/store rw,relatime master:4 - overlay overlay rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work
416 415 0:30 / /sysroot/nix/store ro,relatime master:5 - overlay overlay rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work`
checkSimple(t, "remount", []simpleTestCase{
{"evalSymlinks", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", errUnique},
}}, wrapErrSelf(errUnique)},
{"open", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, errUnique},
}}, wrapErrSuffix(errUnique, `cannot open "/sysroot/nix":`)},
{"readlink", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", errUnique},
}}, wrapErrSelf(errUnique)},
{"close", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
{"close", expectArgs{0xdeadbeef}, nil, errUnique},
}}, wrapErrSuffix(errUnique, `cannot close "/sysroot/nix":`)},
{"mountinfo stale", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/.hakurei", nil},
{"verbosef", expectArgs{"target resolves to %q", []any{"/sysroot/.hakurei"}}, nil, nil},
{"open", expectArgs{"/sysroot/.hakurei", 0x280000, uint32(0)}, 0xdeadbeef, nil},
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/.hakurei", nil},
{"close", expectArgs{0xdeadbeef}, nil, nil},
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
}}, msg.WrapErr(syscall.ESTALE, `mount point "/sysroot/.hakurei" never appeared in mountinfo`)},
{"mountinfo", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
{"close", expectArgs{0xdeadbeef}, nil, nil},
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile("\x00"), nil},
}}, wrapErrSuffix(vfs.ErrMountInfoFields, `cannot parse mountinfo:`)},
{"mount", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
{"close", expectArgs{0xdeadbeef}, nil, nil},
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, errUnique},
}}, wrapErrSuffix(errUnique, `cannot remount "/sysroot/nix":`)},
{"mount propagate", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
{"close", expectArgs{0xdeadbeef}, nil, nil},
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil},
{"mount", expectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, errUnique},
}}, wrapErrSuffix(errUnique, `cannot propagate flags to "/sysroot/nix/.ro-store":`)},
{"success toplevel", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/bin", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/bin"}, "/sysroot/bin", nil},
{"open", expectArgs{"/sysroot/bin", 0x280000, uint32(0)}, 0xbabe, nil},
{"readlink", expectArgs{"/host/proc/self/fd/47806"}, "/sysroot/bin", nil},
{"close", expectArgs{0xbabe}, nil, nil},
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
{"mount", expectArgs{"none", "/sysroot/bin", "", uintptr(0x209027), ""}, nil, nil},
}}, nil},
{"success EACCES", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
{"close", expectArgs{0xdeadbeef}, nil, nil},
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil},
{"mount", expectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, syscall.EACCES},
{"mount", expectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil},
}}, nil},
{"success no propagate", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
{"close", expectArgs{0xdeadbeef}, nil, nil},
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil},
}}, nil},
{"success case sensitive", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/nix"}, "/sysroot/nix", nil},
{"open", expectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil},
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
{"close", expectArgs{0xdeadbeef}, nil, nil},
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil},
{"mount", expectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, nil},
{"mount", expectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil},
}}, nil},
{"success", func(k syscallDispatcher) error {
return newProcPaths(k, hostPath).remount("/sysroot/.nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, [][]kexpect{{
{"evalSymlinks", expectArgs{"/sysroot/.nix"}, "/sysroot/NIX", nil},
{"verbosef", expectArgs{"target resolves to %q", []any{"/sysroot/NIX"}}, nil, nil},
{"open", expectArgs{"/sysroot/NIX", 0x280000, uint32(0)}, 0xdeadbeef, nil},
{"readlink", expectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil},
{"close", expectArgs{0xdeadbeef}, nil, nil},
{"openNew", expectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil},
{"mount", expectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil},
{"mount", expectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, nil},
{"mount", expectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil},
}}, nil},
})
}
func TestRemountWithFlags(t *testing.T) {
checkSimple(t, "remountWithFlags", []simpleTestCase{
{"noop unmatched", func(k syscallDispatcher) error {
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime,cat"}}, 0)
}, [][]kexpect{{
{"verbosef", expectArgs{"unmatched vfs options: %q", []any{[]string{"cat"}}}, nil, nil},
}}, nil},
{"noop", func(k syscallDispatcher) error {
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, 0)
}, nil, nil},
{"success", func(k syscallDispatcher) error {
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, syscall.MS_RDONLY)
}, [][]kexpect{{
{"mount", expectArgs{"none", "", "", uintptr(0x209021), ""}, nil, nil},
}}, nil},
})
}
func TestMountTmpfs(t *testing.T) {
checkSimple(t, "mountTmpfs", []simpleTestCase{
{"mkdirAll", func(k syscallDispatcher) error {
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700)
}, [][]kexpect{{
{"mkdirAll", expectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, errUnique},
}}, wrapErrSelf(errUnique)},
{"success no size", func(k syscallDispatcher) error {
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 0, 0710)
}, [][]kexpect{{
{"mkdirAll", expectArgs{"/sysroot/run/user/1000", os.FileMode(0750)}, nil, nil},
{"mount", expectArgs{"ephemeral", "/sysroot/run/user/1000", "tmpfs", uintptr(0), "mode=0710"}, nil, nil},
}}, nil},
{"success", func(k syscallDispatcher) error {
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700)
}, [][]kexpect{{
{"mkdirAll", expectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, nil},
{"mount", expectArgs{"ephemeral", "/sysroot/run/user/1000", "tmpfs", uintptr(0), "mode=0700,size=1024"}, nil, nil},
}}, nil},
})
}
func TestParentPerm(t *testing.T) {
testCases := []struct {
perm os.FileMode
want os.FileMode
}{
{0755, 0755},
{0750, 0750},
{0705, 0705},
{0700, 0700},
{050, 0750},
{05, 0705},
{0, 0700},
}
for _, tc := range testCases {
t.Run(tc.perm.String(), func(t *testing.T) {
if got := parentPerm(tc.perm); got != tc.want {
t.Errorf("parentPerm: %#o, want %#o", got, tc.want)
}
})
}
}
func TestEscapeOverlayDataSegment(t *testing.T) {
testCases := []struct {
name string
s string
want string
}{
{"zero", zeroString, zeroString},
{"multi", `\\\:,:,\\\`, `\\\\\\\:\,\:\,\\\\\\`},
{"bwrap", `/path :,\`, `/path \:\,\\`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := EscapeOverlayDataSegment(tc.s); got != tc.want {
t.Errorf("escapeOverlayDataSegment: %s, want %s", got, tc.want)
}
})
}
}
+68
View File
@@ -0,0 +1,68 @@
package container
import (
"errors"
"fmt"
"log"
"os"
"reflect"
"sync/atomic"
"testing"
)
type Msg interface {
IsVerbose() bool
Verbose(v ...any)
Verbosef(format string, v ...any)
WrapErr(err error, a ...any) error
PrintBaseErr(err error, fallback string)
Suspend()
Resume() bool
BeforeExit()
}
type DefaultMsg struct{ inactive atomic.Bool }
func (msg *DefaultMsg) IsVerbose() bool { return true }
func (msg *DefaultMsg) Verbose(v ...any) {
if !msg.inactive.Load() {
log.Println(v...)
}
}
func (msg *DefaultMsg) Verbosef(format string, v ...any) {
if !msg.inactive.Load() {
log.Printf(format, v...)
}
}
// checkedWrappedErr implements error with strict checks for wrapped values.
type checkedWrappedErr struct {
err error
a []any
}
func (c *checkedWrappedErr) Error() string { return fmt.Sprintf("%v, a = %s", c.err, c.a) }
func (c *checkedWrappedErr) Is(err error) bool {
var concreteErr *checkedWrappedErr
if !errors.As(err, &concreteErr) {
return false
}
return reflect.DeepEqual(c, concreteErr)
}
func (msg *DefaultMsg) WrapErr(err error, a ...any) error {
// provide a mostly bulletproof path to bypass this behaviour in tests
if testing.Testing() && os.Getenv("GOPATH") != Nonexistent {
return &checkedWrappedErr{err, a}
}
log.Println(a...)
return err
}
func (msg *DefaultMsg) PrintBaseErr(err error, fallback string) { log.Println(fallback, err) }
func (msg *DefaultMsg) Suspend() { msg.inactive.Store(true) }
func (msg *DefaultMsg) Resume() bool { return msg.inactive.CompareAndSwap(true, false) }
func (msg *DefaultMsg) BeforeExit() {}
+162
View File
@@ -0,0 +1,162 @@
package container_test
import (
"errors"
"log"
"strings"
"sync/atomic"
"syscall"
"testing"
"hakurei.app/container"
"hakurei.app/internal/hlog"
)
func TestDefaultMsg(t *testing.T) {
// bypass WrapErr testing behaviour
t.Setenv("GOPATH", container.Nonexistent)
{
w := log.Writer()
f := log.Flags()
t.Cleanup(func() { log.SetOutput(w); log.SetFlags(f) })
}
msg := new(container.DefaultMsg)
t.Run("is verbose", func(t *testing.T) {
if !msg.IsVerbose() {
t.Error("IsVerbose unexpected outcome")
}
})
t.Run("verbose", func(t *testing.T) {
log.SetOutput(panicWriter{})
msg.Suspend()
msg.Verbose()
msg.Verbosef("\x00")
msg.Resume()
buf := new(strings.Builder)
log.SetOutput(buf)
log.SetFlags(0)
msg.Verbose()
msg.Verbosef("\x00")
want := "\n\x00\n"
if buf.String() != want {
t.Errorf("Verbose: %q, want %q", buf.String(), want)
}
})
t.Run("wrapErr", func(t *testing.T) {
buf := new(strings.Builder)
log.SetOutput(buf)
log.SetFlags(0)
if err := msg.WrapErr(syscall.EBADE, "\x00", "\x00"); err != syscall.EBADE {
t.Errorf("WrapErr: %v", err)
}
msg.PrintBaseErr(syscall.ENOTRECOVERABLE, "cannot cuddle cat:")
want := "\x00 \x00\ncannot cuddle cat: state not recoverable\n"
if buf.String() != want {
t.Errorf("WrapErr: %q, want %q", buf.String(), want)
}
})
t.Run("inactive", func(t *testing.T) {
{
inactive := msg.Resume()
if inactive {
t.Cleanup(func() { msg.Suspend() })
}
}
if msg.Resume() {
t.Error("Resume unexpected outcome")
}
msg.Suspend()
if !msg.Resume() {
t.Error("Resume unexpected outcome")
}
})
// the function is a noop
t.Run("beforeExit", func(t *testing.T) { msg.BeforeExit() })
t.Run("checkedWrappedErr", func(t *testing.T) {
// temporarily re-enable testing behaviour
t.Setenv("GOPATH", "")
wrappedErr := msg.WrapErr(syscall.ENOTRECOVERABLE, "cannot cuddle cat:", syscall.ENOTRECOVERABLE)
t.Run("string", func(t *testing.T) {
want := "state not recoverable, a = [cannot cuddle cat: state not recoverable]"
if got := wrappedErr.Error(); got != want {
t.Errorf("Error: %q, want %q", got, want)
}
})
t.Run("bad concrete type", func(t *testing.T) {
if errors.Is(wrappedErr, syscall.ENOTRECOVERABLE) {
t.Error("incorrect type assertion")
}
})
})
}
type panicWriter struct{}
func (panicWriter) Write([]byte) (int, error) { panic("unreachable") }
func saveRestoreOutput(t *testing.T) {
out := container.GetOutput()
t.Cleanup(func() { container.SetOutput(out) })
}
func replaceOutput(t *testing.T) {
saveRestoreOutput(t)
container.SetOutput(&testOutput{t: t})
}
type testOutput struct {
t *testing.T
suspended atomic.Bool
}
func (out *testOutput) IsVerbose() bool { return testing.Verbose() }
func (out *testOutput) Verbose(v ...any) {
if !out.IsVerbose() {
return
}
out.t.Log(v...)
}
func (out *testOutput) Verbosef(format string, v ...any) {
if !out.IsVerbose() {
return
}
out.t.Logf(format, v...)
}
func (out *testOutput) WrapErr(err error, a ...any) error { return hlog.WrapErr(err, a...) }
func (out *testOutput) PrintBaseErr(err error, fallback string) { hlog.PrintBaseError(err, fallback) }
func (out *testOutput) Suspend() {
if out.suspended.CompareAndSwap(false, true) {
out.Verbose("suspend called")
return
}
out.Verbose("suspend called on suspended output")
}
func (out *testOutput) Resume() bool {
if out.suspended.CompareAndSwap(true, false) {
out.Verbose("resume called")
return true
}
out.Verbose("resume called on unsuspended output")
return false
}
func (out *testOutput) BeforeExit() { out.Verbose("beforeExit called") }
+26
View File
@@ -0,0 +1,26 @@
package container
var msg Msg = new(DefaultMsg)
func GetOutput() Msg { return msg }
func SetOutput(v Msg) {
if v == nil {
msg = new(DefaultMsg)
} else {
msg = v
}
}
func wrapErrSuffix(err error, a ...any) error {
if err == nil {
return nil
}
return msg.WrapErr(err, append(a, err)...)
}
func wrapErrSelf(err error) error {
if err == nil {
return nil
}
return msg.WrapErr(err, err.Error())
}
+110
View File
@@ -0,0 +1,110 @@
package container
import (
"reflect"
"syscall"
"testing"
)
func TestGetSetOutput(t *testing.T) {
{
out := GetOutput()
t.Cleanup(func() { SetOutput(out) })
}
t.Run("default", func(t *testing.T) {
SetOutput(new(stubOutput))
if v, ok := GetOutput().(*DefaultMsg); ok {
t.Fatalf("SetOutput: got unexpected output %#v", v)
}
SetOutput(nil)
if _, ok := GetOutput().(*DefaultMsg); !ok {
t.Fatalf("SetOutput: got unexpected output %#v", GetOutput())
}
})
t.Run("stub", func(t *testing.T) {
SetOutput(new(stubOutput))
if _, ok := GetOutput().(*stubOutput); !ok {
t.Fatalf("SetOutput: got unexpected output %#v", GetOutput())
}
})
}
func TestWrapErr(t *testing.T) {
{
out := GetOutput()
t.Cleanup(func() { SetOutput(out) })
}
var wrapFp *func(error, ...any) error
s := new(stubOutput)
SetOutput(s)
wrapFp = &s.wrapF
testCases := []struct {
name string
f func(t *testing.T)
wantErr error
wantA []any
}{
{"suffix nil", func(t *testing.T) {
if err := wrapErrSuffix(nil, "\x00"); err != nil {
t.Errorf("wrapErrSuffix: %v", err)
}
}, nil, nil},
{"suffix val", func(t *testing.T) {
if err := wrapErrSuffix(syscall.ENOTRECOVERABLE, "\x00\x00"); err != syscall.ENOTRECOVERABLE {
t.Errorf("wrapErrSuffix: %v", err)
}
}, syscall.ENOTRECOVERABLE, []any{"\x00\x00", syscall.ENOTRECOVERABLE}},
{"self nil", func(t *testing.T) {
if err := wrapErrSelf(nil); err != nil {
t.Errorf("wrapErrSelf: %v", err)
}
}, nil, nil},
{"self val", func(t *testing.T) {
if err := wrapErrSelf(syscall.ENOTRECOVERABLE); err != syscall.ENOTRECOVERABLE {
t.Errorf("wrapErrSelf: %v", err)
}
}, syscall.ENOTRECOVERABLE, []any{"state not recoverable"}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var (
gotErr error
gotA []any
)
*wrapFp = func(err error, a ...any) error { gotErr = err; gotA = a; return err }
tc.f(t)
if gotErr != tc.wantErr {
t.Errorf("WrapErr: err = %v, want %v", gotErr, tc.wantErr)
}
if !reflect.DeepEqual(gotA, tc.wantA) {
t.Errorf("WrapErr: a = %v, want %v", gotA, tc.wantA)
}
})
}
}
type stubOutput struct {
wrapF func(error, ...any) error
}
func (*stubOutput) IsVerbose() bool { panic("unreachable") }
func (*stubOutput) Verbose(...any) { panic("unreachable") }
func (*stubOutput) Verbosef(string, ...any) { panic("unreachable") }
func (*stubOutput) PrintBaseErr(error, string) { panic("unreachable") }
func (*stubOutput) Suspend() { panic("unreachable") }
func (*stubOutput) Resume() bool { panic("unreachable") }
func (*stubOutput) BeforeExit() { panic("unreachable") }
func (s *stubOutput) WrapErr(err error, v ...any) error {
if s.wrapF == nil {
panic("unreachable")
}
return s.wrapF(err, v...)
}
+48
View File
@@ -0,0 +1,48 @@
package container
import (
"encoding/gob"
"errors"
"os"
"strconv"
"syscall"
)
var (
ErrNotSet = errors.New("environment variable not set")
ErrFdFormat = errors.New("bad file descriptor representation")
)
// Setup appends the read end of a pipe for setup params transmission and returns its fd.
func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) {
if r, w, err := os.Pipe(); err != nil {
return -1, nil, err
} else {
fd := 3 + len(*extraFiles)
*extraFiles = append(*extraFiles, r)
return fd, gob.NewEncoder(w), nil
}
}
// Receive retrieves setup fd from the environment and receives params.
func Receive(key string, e any, fdp *uintptr) (func() error, error) {
var setup *os.File
if s, ok := os.LookupEnv(key); !ok {
return nil, ErrNotSet
} else {
if fd, err := strconv.Atoi(s); err != nil {
return nil, ErrFdFormat
} else {
setup = os.NewFile(uintptr(fd), "setup")
if setup == nil {
return nil, syscall.EBADF
}
if fdp != nil {
*fdp = setup.Fd()
}
}
}
return setup.Close, gob.NewDecoder(setup).Decode(e)
}
+120
View File
@@ -0,0 +1,120 @@
package container_test
import (
"errors"
"os"
"slices"
"strconv"
"syscall"
"testing"
"hakurei.app/container"
)
func TestSetupReceive(t *testing.T) {
t.Run("not set", func(t *testing.T) {
const key = "TEST_ENV_NOT_SET"
{
v, ok := os.LookupEnv(key)
t.Cleanup(func() {
if ok {
if err := os.Setenv(key, v); err != nil {
t.Fatalf("Setenv: error = %v", err)
}
} else {
if err := os.Unsetenv(key); err != nil {
t.Fatalf("Unsetenv: error = %v", err)
}
}
})
}
if _, err := container.Receive(key, nil, nil); !errors.Is(err, container.ErrNotSet) {
t.Errorf("Receive: error = %v, want %v", err, container.ErrNotSet)
}
})
t.Run("format", func(t *testing.T) {
const key = "TEST_ENV_FORMAT"
t.Setenv(key, "")
if _, err := container.Receive(key, nil, nil); !errors.Is(err, container.ErrFdFormat) {
t.Errorf("Receive: error = %v, want %v", err, container.ErrFdFormat)
}
})
t.Run("range", func(t *testing.T) {
const key = "TEST_ENV_RANGE"
t.Setenv(key, "-1")
if _, err := container.Receive(key, nil, nil); !errors.Is(err, syscall.EBADF) {
t.Errorf("Receive: error = %v, want %v", err, syscall.EBADF)
}
})
t.Run("setup receive", func(t *testing.T) {
check := func(t *testing.T, useNilFdp bool) {
const key = "TEST_SETUP_RECEIVE"
payload := []int{syscall.MS_MGC_VAL, syscall.MS_MGC_MSK, syscall.MS_ASYNC, syscall.MS_ACTIVE}
encoderDone := make(chan error, 1)
extraFiles := make([]*os.File, 0, 1)
if fd, encoder, err := container.Setup(&extraFiles); err != nil {
t.Fatalf("Setup: error = %v", err)
} else if fd != 3 {
t.Fatalf("Setup: fd = %d, want 3", fd)
} else {
go func() { encoderDone <- encoder.Encode(payload) }()
}
if len(extraFiles) != 1 {
t.Fatalf("extraFiles: len = %v, want 1", len(extraFiles))
}
var dupFd int
if fd, err := syscall.Dup(int(extraFiles[0].Fd())); err != nil {
t.Fatalf("Dup: error = %v", err)
} else {
syscall.CloseOnExec(fd)
dupFd = fd
t.Setenv(key, strconv.Itoa(fd))
}
var (
gotPayload []int
fdp *uintptr
)
if !useNilFdp {
fdp = new(uintptr)
}
var closeFile func() error
if f, err := container.Receive(key, &gotPayload, fdp); err != nil {
t.Fatalf("Receive: error = %v", err)
} else {
closeFile = f
if !slices.Equal(payload, gotPayload) {
t.Errorf("Receive: %#v, want %#v", gotPayload, payload)
}
}
if !useNilFdp {
if int(*fdp) != dupFd {
t.Errorf("Fd: %d, want %d", *fdp, dupFd)
}
}
if err := <-encoderDone; err != nil {
t.Errorf("Encode: error = %v", err)
}
if closeFile != nil {
if err := closeFile(); err != nil {
t.Errorf("Close: error = %v", err)
}
}
}
t.Run("fp", func(t *testing.T) { check(t, false) })
t.Run("nil", func(t *testing.T) { check(t, true) })
})
}
+162
View File
@@ -0,0 +1,162 @@
package container
import (
"errors"
"fmt"
"io/fs"
"os"
"path"
"strconv"
"strings"
"syscall"
"hakurei.app/container/vfs"
)
/* constants in this file bypass abs check, be extremely careful when changing them! */
const (
// FHSRoot points to the file system root.
FHSRoot = "/"
// FHSEtc points to the directory for system-specific configuration.
FHSEtc = "/etc/"
// FHSTmp points to the place for small temporary files.
FHSTmp = "/tmp/"
// FHSRun points to a "tmpfs" file system for system packages to place runtime data, socket files, and similar.
FHSRun = "/run/"
// FHSRunUser points to a directory containing per-user runtime directories,
// each usually individually mounted "tmpfs" instances.
FHSRunUser = FHSRun + "user/"
// FHSUsr points to vendor-supplied operating system resources.
FHSUsr = "/usr/"
// FHSUsrBin points to binaries and executables for user commands that shall appear in the $PATH search path.
FHSUsrBin = FHSUsr + "bin/"
// FHSVar points to persistent, variable system data. Writable during normal system operation.
FHSVar = "/var/"
// FHSVarLib points to persistent system data.
FHSVarLib = FHSVar + "lib/"
// FHSVarEmpty points to a nonstandard directory that is usually empty.
FHSVarEmpty = FHSVar + "empty/"
// FHSDev points to the root directory for device nodes.
FHSDev = "/dev/"
// FHSProc points to a virtual kernel file system exposing the process list and other functionality.
FHSProc = "/proc/"
// FHSProcSys points to a hierarchy below /proc/ that exposes a number of kernel tunables.
FHSProcSys = FHSProc + "sys/"
// FHSSys points to a virtual kernel file system exposing discovered devices and other functionality.
FHSSys = "/sys/"
)
var (
// AbsFHSRoot is [FHSRoot] as [Absolute].
AbsFHSRoot = &Absolute{FHSRoot}
// AbsFHSEtc is [FHSEtc] as [Absolute].
AbsFHSEtc = &Absolute{FHSEtc}
// AbsFHSTmp is [FHSTmp] as [Absolute].
AbsFHSTmp = &Absolute{FHSTmp}
// AbsFHSRun is [FHSRun] as [Absolute].
AbsFHSRun = &Absolute{FHSRun}
// AbsFHSRunUser is [FHSRunUser] as [Absolute].
AbsFHSRunUser = &Absolute{FHSRunUser}
// AbsFHSUsrBin is [FHSUsrBin] as [Absolute].
AbsFHSUsrBin = &Absolute{FHSUsrBin}
// AbsFHSVar is [FHSVar] as [Absolute].
AbsFHSVar = &Absolute{FHSVar}
// AbsFHSVarLib is [FHSVarLib] as [Absolute].
AbsFHSVarLib = &Absolute{FHSVarLib}
// AbsFHSDev is [FHSDev] as [Absolute].
AbsFHSDev = &Absolute{FHSDev}
// AbsFHSProc is [FHSProc] as [Absolute].
AbsFHSProc = &Absolute{FHSProc}
// AbsFHSSys is [FHSSys] as [Absolute].
AbsFHSSys = &Absolute{FHSSys}
)
const (
// Nonexistent is a path that cannot exist.
// /proc is chosen because a system with covered /proc is unsupported by this package.
Nonexistent = FHSProc + "nonexistent"
hostPath = FHSRoot + hostDir
hostDir = "host"
sysrootPath = FHSRoot + sysrootDir
sysrootDir = "sysroot"
)
func toSysroot(name string) string {
name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '/' })
return path.Join(sysrootPath, name)
}
func toHost(name string) string {
name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '/' })
return path.Join(hostPath, name)
}
func createFile(name string, perm, pperm os.FileMode, content []byte) error {
if err := os.MkdirAll(path.Dir(name), pperm); err != nil {
return wrapErrSelf(err)
}
f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm)
if err != nil {
return wrapErrSelf(err)
}
if content != nil {
_, err = f.Write(content)
}
return errors.Join(f.Close(), wrapErrSelf(err))
}
func ensureFile(name string, perm, pperm os.FileMode) error {
fi, err := os.Stat(name)
if err != nil {
if !os.IsNotExist(err) {
return wrapErrSelf(err)
}
return createFile(name, perm, pperm, nil)
}
if mode := fi.Mode(); mode&fs.ModeDir != 0 || mode&fs.ModeSymlink != 0 {
err = msg.WrapErr(syscall.EISDIR,
fmt.Sprintf("path %q is a directory", name))
}
return err
}
var hostProc = newProcPaths(direct{}, hostPath)
func newProcPaths(k syscallDispatcher, prefix string) *procPaths {
return &procPaths{k, prefix + "/proc", prefix + "/proc/self"}
}
type procPaths struct {
k syscallDispatcher
prefix string
self string
}
func (p *procPaths) stdout() string { return p.self + "/fd/1" }
func (p *procPaths) fd(fd int) string { return p.self + "/fd/" + strconv.Itoa(fd) }
func (p *procPaths) mountinfo(f func(d *vfs.MountInfoDecoder) error) error {
if r, err := p.k.openNew(p.self + "/mountinfo"); err != nil {
return wrapErrSelf(err)
} else {
d := vfs.NewMountInfoDecoder(r)
err0 := f(d)
if err = r.Close(); err != nil {
return wrapErrSelf(err)
} else if err = d.Err(); err != nil {
return wrapErrSuffix(err,
"cannot parse mountinfo:")
}
return err0
}
}
+252
View File
@@ -0,0 +1,252 @@
package container
import (
"errors"
"fmt"
"io"
"math"
"os"
"path"
"reflect"
"syscall"
"testing"
"unsafe"
"hakurei.app/container/vfs"
)
func TestToSysroot(t *testing.T) {
testCases := []struct {
name string
want string
}{
{"", "/sysroot"},
{"/", "/sysroot"},
{"//etc///", "/sysroot/etc"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := toSysroot(tc.name); got != tc.want {
t.Errorf("toSysroot: %q, want %q", got, tc.want)
}
})
}
}
func TestToHost(t *testing.T) {
testCases := []struct {
name string
want string
}{
{"", "/host"},
{"/", "/host"},
{"//etc///", "/host/etc"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := toHost(tc.name); got != tc.want {
t.Errorf("toHost: %q, want %q", got, tc.want)
}
})
}
}
// InternalToHostOvlEscape exports toHost passed to EscapeOverlayDataSegment.
func InternalToHostOvlEscape(s string) string { return EscapeOverlayDataSegment(toHost(s)) }
func TestCreateFile(t *testing.T) {
t.Run("nonexistent", func(t *testing.T) {
if err := createFile(path.Join(Nonexistent, ":3"), 0644, 0755, nil); !errors.Is(err, wrapErrSelf(&os.PathError{
Op: "mkdir",
Path: "/proc/nonexistent",
Err: syscall.ENOENT,
})) {
t.Errorf("createFile: error = %v", err)
}
if err := createFile(path.Join(Nonexistent), 0644, 0755, nil); !errors.Is(err, wrapErrSelf(&os.PathError{
Op: "open",
Path: "/proc/nonexistent",
Err: syscall.ENOENT,
})) {
t.Errorf("createFile: error = %v", err)
}
})
t.Run("touch", func(t *testing.T) {
tempDir := t.TempDir()
pathname := path.Join(tempDir, "empty")
if err := createFile(pathname, 0644, 0755, nil); err != nil {
t.Fatalf("createFile: error = %v", err)
}
if d, err := os.ReadFile(pathname); err != nil {
t.Fatalf("ReadFile: error = %v", err)
} else if len(d) != 0 {
t.Fatalf("createFile: %q", string(d))
}
})
t.Run("write", func(t *testing.T) {
tempDir := t.TempDir()
pathname := path.Join(tempDir, "zero")
if err := createFile(pathname, 0644, 0755, []byte{0}); err != nil {
t.Fatalf("createFile: error = %v", err)
}
if d, err := os.ReadFile(pathname); err != nil {
t.Fatalf("ReadFile: error = %v", err)
} else if string(d) != "\x00" {
t.Fatalf("createFile: %q, want %q", string(d), "\x00")
}
})
}
func TestEnsureFile(t *testing.T) {
t.Run("create", func(t *testing.T) {
if err := ensureFile(path.Join(t.TempDir(), "ensure"), 0644, 0755); err != nil {
t.Errorf("ensureFile: error = %v", err)
}
})
t.Run("stat", func(t *testing.T) {
t.Run("inaccessible", func(t *testing.T) {
tempDir := t.TempDir()
pathname := path.Join(tempDir, "inaccessible")
if f, err := os.Create(pathname); err != nil {
t.Fatalf("Create: error = %v", err)
} else {
_ = f.Close()
}
if err := os.Chmod(tempDir, 0); err != nil {
t.Fatalf("Chmod: error = %v", err)
}
wantErr := wrapErrSelf(&os.PathError{
Op: "stat",
Path: pathname,
Err: syscall.EACCES,
})
if err := ensureFile(pathname, 0644, 0755); !errors.Is(err, wantErr) {
t.Errorf("ensureFile: error = %v, want %v", err, wantErr)
}
if err := os.Chmod(tempDir, 0755); err != nil {
t.Fatalf("Chmod: error = %v", err)
}
})
t.Run("directory", func(t *testing.T) {
pathname := t.TempDir()
wantErr := msg.WrapErr(syscall.EISDIR, fmt.Sprintf("path %q is a directory", pathname))
if err := ensureFile(pathname, 0644, 0755); !errors.Is(err, wantErr) {
t.Errorf("ensureFile: error = %v, want %v", err, wantErr)
}
})
t.Run("ensure", func(t *testing.T) {
tempDir := t.TempDir()
pathname := path.Join(tempDir, "ensure")
if f, err := os.Create(pathname); err != nil {
t.Fatalf("Create: error = %v", err)
} else {
_ = f.Close()
}
if err := ensureFile(pathname, 0644, 0755); err != nil {
t.Errorf("ensureFile: error = %v", err)
}
})
})
}
func TestProcPaths(t *testing.T) {
t.Run("host", func(t *testing.T) {
t.Run("stdout", func(t *testing.T) {
want := "/host/proc/self/fd/1"
if got := hostProc.stdout(); got != want {
t.Errorf("stdout: %q, want %q", got, want)
}
})
t.Run("fd", func(t *testing.T) {
want := "/host/proc/self/fd/9223372036854775807"
if got := hostProc.fd(math.MaxInt64); got != want {
t.Errorf("stdout: %q, want %q", got, want)
}
})
})
t.Run("mountinfo", func(t *testing.T) {
t.Run("nonexistent", func(t *testing.T) {
nonexistentProc := newProcPaths(direct{}, t.TempDir())
wantErr := wrapErrSelf(&os.PathError{
Op: "open",
Path: nonexistentProc.self + "/mountinfo",
Err: syscall.ENOENT,
})
if err := nonexistentProc.mountinfo(func(*vfs.MountInfoDecoder) error { return syscall.EINVAL }); !errors.Is(err, wantErr) {
t.Errorf("mountinfo: error = %v, want %v", err, wantErr)
}
})
t.Run("sample", func(t *testing.T) {
tempDir := t.TempDir()
if err := os.MkdirAll(path.Join(tempDir, "proc/self"), 0755); err != nil {
t.Fatalf("MkdirAll: error = %v", err)
}
t.Run("clean", func(t *testing.T) {
if err := os.WriteFile(path.Join(tempDir, "proc/self/mountinfo"), []byte(`15 20 0:3 / /proc rw,relatime - proc /proc rw
16 20 0:15 / /sys rw,relatime - sysfs /sys rw
17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755`), 0644); err != nil {
t.Fatalf("WriteFile: error = %v", err)
}
var mountInfo *vfs.MountInfo
if err := newProcPaths(direct{}, tempDir).mountinfo(func(d *vfs.MountInfoDecoder) error { return d.Decode(&mountInfo) }); err != nil {
t.Fatalf("mountinfo: error = %v", err)
}
wantMountInfo := &vfs.MountInfo{Next: &vfs.MountInfo{Next: &vfs.MountInfo{
MountInfoEntry: vfs.MountInfoEntry{ID: 17, Parent: 20, Devno: vfs.DevT{0, 5}, Root: "/", Target: "/dev", VfsOptstr: "rw,relatime", OptFields: []string{}, FsType: "devtmpfs", Source: "udev", FsOptstr: "rw,size=1983516k,nr_inodes=495879,mode=755"}},
MountInfoEntry: vfs.MountInfoEntry{ID: 16, Parent: 20, Devno: vfs.DevT{0, 15}, Root: "/", Target: "/sys", VfsOptstr: "rw,relatime", OptFields: []string{}, FsType: "sysfs", Source: "/sys", FsOptstr: "rw"}},
MountInfoEntry: vfs.MountInfoEntry{ID: 15, Parent: 20, Devno: vfs.DevT{0, 3}, Root: "/", Target: "/proc", VfsOptstr: "rw,relatime", OptFields: []string{}, FsType: "proc", Source: "/proc", FsOptstr: "rw"},
}
if !reflect.DeepEqual(mountInfo, wantMountInfo) {
t.Errorf("Decode: %#v, want %#v", mountInfo, wantMountInfo)
}
})
t.Run("closed", func(t *testing.T) {
p := newProcPaths(direct{}, tempDir)
wantErr := wrapErrSelf(&os.PathError{
Op: "close",
Path: p.self + "/mountinfo",
Err: os.ErrClosed,
})
if err := p.mountinfo(func(d *vfs.MountInfoDecoder) error {
v := reflect.ValueOf(d).Elem().FieldByName("s").Elem().FieldByName("r")
v = reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr()))
if f, ok := v.Elem().Interface().(io.ReadCloser); !ok {
t.Fatal("implementation of bufio.Scanner no longer compatible with this fault injection")
return syscall.ENOTRECOVERABLE
} else {
return f.Close()
}
}); !errors.Is(err, wantErr) {
t.Errorf("mountinfo: error = %v, want %v", err, wantErr)
}
})
t.Run("malformed", func(t *testing.T) {
path.Join(tempDir, "proc/self/mountinfo")
if err := os.WriteFile(path.Join(tempDir, "proc/self/mountinfo"), []byte{0}, 0644); err != nil {
t.Fatalf("WriteFile: error = %v", err)
}
wantErr := wrapErrSuffix(vfs.ErrMountInfoFields, "cannot parse mountinfo:")
if err := newProcPaths(direct{}, tempDir).mountinfo(func(d *vfs.MountInfoDecoder) error { return d.Decode(new(*vfs.MountInfo)) }); !errors.Is(err, wantErr) {
t.Fatalf("mountinfo: error = %v, want %v", err, wantErr)
}
})
})
})
}
+24
View File
@@ -0,0 +1,24 @@
package seccomp_test
import . "hakurei.app/container/seccomp"
var bpfExpected = bpfLookup{
{AllowMultiarch | AllowCAN |
AllowBluetooth, PresetExt |
PresetDenyNS | PresetDenyTTY | PresetDenyDevel |
PresetLinux32}: toHash(
"e99dd345e195413473d3cbee07b4ed57b908bfa89ea2072fe93482847f50b5b758da17e74ca2bbc00813de49a2b9bf834c024ed48850be69b68a9a4c5f53a9db"),
{0, 0}: toHash(
"95ec69d017733e072160e0da80fdebecdf27ae8166f5e2a731270c98ea2d2946cb5231029063668af215879155da21aca79b070e04c0ee9acdf58f55cfa815a5"),
{0, PresetExt}: toHash(
"dc7f2e1c5e829b79ebb7efc759150f54a83a75c8df6fee4dce5dadc4736c585d4deebfeb3c7969af3a077e90b77bb4741db05d90997c8659b95891206ac9952d"),
{0, PresetStrict}: toHash(
"e880298df2bd6751d0040fc21bc0ed4c00f95dc0d7ba506c244d8b8cf6866dba8ef4a33296f287b66cccc1d78e97026597f84cc7dec1573e148960fbd35cd735"),
{0, PresetDenyNS | PresetDenyTTY | PresetDenyDevel}: toHash(
"39871b93ffafc8b979fcedc0b0c37b9e03922f5b02748dc5c3c17c92527f6e022ede1f48bff59246ea452c0d1de54827808b1a6f84f32bbde1aa02ae30eedcfa"),
{0, PresetExt | PresetDenyDevel}: toHash(
"c698b081ff957afe17a6d94374537d37f2a63f6f9dd75da7546542407a9e32476ebda3312ba7785d7f618542bcfaf27ca27dcc2dddba852069d28bcfe8cad39a"),
{0, PresetExt | PresetDenyNS | PresetDenyDevel}: toHash(
"0b76007476c1c9e25dbf674c29fdf609a1656a70063e49327654e1b5360ad3da06e1a3e32bf80e961c5516ad83d4b9e7e9bde876a93797e27627d2555c25858b"),
}
+24
View File
@@ -0,0 +1,24 @@
package seccomp_test
import . "hakurei.app/container/seccomp"
var bpfExpected = bpfLookup{
{AllowMultiarch | AllowCAN |
AllowBluetooth, PresetExt |
PresetDenyNS | PresetDenyTTY | PresetDenyDevel |
PresetLinux32}: toHash(
"1431c013f2ddac3adae577821cb5d351b1514e7c754d62346ddffd31f46ea02fb368e46e3f8104f81019617e721fe687ddd83f1e79580622ccc991da12622170"),
{0, 0}: toHash(
"450c21210dbf124dfa7ae56d0130f9c2e24b26f5bce8795ee75766c75850438ff9e7d91c5e73d63bbe51a5d4b06c2a0791c4de2903b2b9805f16265318183235"),
{0, PresetExt}: toHash(
"d971d0f2d30f54ac920fc6d84df2be279e9fd28cf2d48be775d7fdbd790b750e1369401cd3bb8bcf9ba3adb91874fe9792d9e3f62209b8ee59c9fdd2ddd10c7b"),
{0, PresetStrict}: toHash(
"79318538a3dc851314b6bd96f10d5861acb2aa7e13cb8de0619d0f6a76709d67f01ef3fd67e195862b02f9711e5b769bc4d1eb4fc0dfc41a723c89c968a93297"),
{0, PresetDenyNS | PresetDenyTTY | PresetDenyDevel}: toHash(
"228286c2f5df8e44463be0a57b91977b7f38b63b09e5d98dfabe5c61545b8f9ac3e5ea3d86df55d7edf2ce61875f0a5a85c0ab82800bef178c42533e8bdc9a6c"),
{0, PresetExt | PresetDenyDevel}: toHash(
"433ce9b911282d6dcc8029319fb79b816b60d5a795ec8fc94344dd027614d68f023166a91bb881faaeeedd26e3d89474e141e5a69a97e93b8984ca8f14999980"),
{0, PresetExt | PresetDenyNS | PresetDenyDevel}: toHash(
"cf1f4dc87436ba8ec95d268b663a6397bb0b4a5ac64d8557e6cc529d8b0f6f65dad3a92b62ed29d85eee9c6dde1267757a4d0f86032e8a45ca1bceadfa34cf5e"),
}
+28
View File
@@ -0,0 +1,28 @@
package seccomp_test
import (
"encoding/hex"
"hakurei.app/container/seccomp"
)
type (
bpfPreset = struct {
seccomp.ExportFlag
seccomp.FilterPreset
}
bpfLookup map[bpfPreset][]byte
)
func toHash(s string) []byte {
if len(s) != 128 {
panic("bad sha512 string length")
}
if v, err := hex.DecodeString(s); err != nil {
panic(err.Error())
} else if len(v) != 64 {
panic("unreachable")
} else {
return v
}
}
+130
View File
@@ -0,0 +1,130 @@
#ifndef _GNU_SOURCE
#define _GNU_SOURCE /* CLONE_NEWUSER */
#endif
#include "libseccomp-helper.h"
#include <assert.h>
#include <errno.h>
#include <sys/socket.h>
#define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch,
uint32_t multiarch,
struct hakurei_syscall_rule *rules,
size_t rules_sz, hakurei_export_flag flags) {
int i;
int last_allowed_family;
int disallowed;
struct hakurei_syscall_rule *rule;
int32_t res = 0; /* refer to resPrefix for message */
/* Blocklist all but unix, inet, inet6 and netlink */
struct {
int family;
hakurei_export_flag flags_mask;
} socket_family_allowlist[] = {
/* NOTE: Keep in numerical order */
{AF_UNSPEC, 0},
{AF_LOCAL, 0},
{AF_INET, 0},
{AF_INET6, 0},
{AF_NETLINK, 0},
{AF_CAN, HAKUREI_EXPORT_CAN},
{AF_BLUETOOTH, HAKUREI_EXPORT_BLUETOOTH},
};
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
if (ctx == NULL) {
res = 1;
goto out;
} else
errno = 0;
/* We only really need to handle arches on multiarch systems.
* If only one arch is supported the default is fine */
if (arch != 0) {
/* This *adds* the target arch, instead of replacing the
* native one. This is not ideal, because we'd like to only
* allow the target arch, but we can't really disallow the
* native arch at this point, because then bubblewrap
* couldn't continue running. */
*ret_p = seccomp_arch_add(ctx, arch);
if (*ret_p < 0 && *ret_p != -EEXIST) {
res = 2;
goto out;
}
if (flags & HAKUREI_EXPORT_MULTIARCH && multiarch != 0) {
*ret_p = seccomp_arch_add(ctx, multiarch);
if (*ret_p < 0 && *ret_p != -EEXIST) {
res = 3;
goto out;
}
}
}
for (i = 0; i < rules_sz; i++) {
rule = &rules[i];
assert(rule->m_errno == EPERM || rule->m_errno == ENOSYS);
if (rule->arg)
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno),
rule->syscall, 1, *rule->arg);
else
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno),
rule->syscall, 0);
if (*ret_p == -EFAULT) {
res = 4;
goto out;
} else if (*ret_p < 0) {
res = 5;
goto out;
}
}
/* Socket filtering doesn't work on e.g. i386, so ignore failures here
* However, we need to user seccomp_rule_add_exact to avoid libseccomp doing
* something else: https://github.com/seccomp/libseccomp/issues/8 */
last_allowed_family = -1;
for (i = 0; i < LEN(socket_family_allowlist); i++) {
if (socket_family_allowlist[i].flags_mask != 0 &&
(socket_family_allowlist[i].flags_mask & flags) !=
socket_family_allowlist[i].flags_mask)
continue;
for (disallowed = last_allowed_family + 1;
disallowed < socket_family_allowlist[i].family; disallowed++) {
/* Blocklist the in-between valid families */
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT),
SCMP_SYS(socket), 1,
SCMP_A0(SCMP_CMP_EQ, disallowed));
}
last_allowed_family = socket_family_allowlist[i].family;
}
/* Blocklist the rest */
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1,
SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1));
if (fd < 0) {
*ret_p = seccomp_load(ctx);
if (*ret_p != 0) {
res = 7;
goto out;
}
} else {
*ret_p = seccomp_export_bpf(ctx, fd);
if (*ret_p != 0) {
res = 6;
goto out;
}
}
out:
if (ctx)
seccomp_release(ctx);
return res;
}
+24
View File
@@ -0,0 +1,24 @@
#include <seccomp.h>
#include <stdint.h>
#if (SCMP_VER_MAJOR < 2) || (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR < 5) || \
(SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR == 5 && SCMP_VER_MICRO < 1)
#error This package requires libseccomp >= v2.5.1
#endif
typedef enum {
HAKUREI_EXPORT_MULTIARCH = 1 << 0,
HAKUREI_EXPORT_CAN = 1 << 1,
HAKUREI_EXPORT_BLUETOOTH = 1 << 2,
} hakurei_export_flag;
struct hakurei_syscall_rule {
int syscall;
int m_errno;
struct scmp_arg_cmp *arg;
};
int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch,
uint32_t multiarch,
struct hakurei_syscall_rule *rules,
size_t rules_sz, hakurei_export_flag flags);
+194
View File
@@ -0,0 +1,194 @@
package seccomp
/*
#cgo linux pkg-config: --static libseccomp
#include <libseccomp-helper.h>
#include <sys/personality.h>
*/
import "C"
import (
"errors"
"fmt"
"runtime"
"syscall"
"unsafe"
)
const (
PER_LINUX = C.PER_LINUX
PER_LINUX32 = C.PER_LINUX32
)
var (
ErrInvalidRules = errors.New("invalid native rules slice")
)
// LibraryError represents a libseccomp error.
type LibraryError struct {
Prefix string
Seccomp syscall.Errno
Errno error
}
func (e *LibraryError) Error() string {
if e.Seccomp == 0 {
if e.Errno == nil {
panic("invalid libseccomp error")
}
return fmt.Sprintf("%s: %s", e.Prefix, e.Errno)
}
if e.Errno == nil {
return fmt.Sprintf("%s: %s", e.Prefix, e.Seccomp)
}
return fmt.Sprintf("%s: %s (%s)", e.Prefix, e.Seccomp, e.Errno)
}
func (e *LibraryError) Is(err error) bool {
if e == nil {
return err == nil
}
if ef, ok := err.(*LibraryError); ok {
return *e == *ef
}
return (e.Seccomp != 0 && errors.Is(err, e.Seccomp)) ||
(e.Errno != nil && errors.Is(err, e.Errno))
}
type (
ScmpSyscall = C.int
ScmpErrno = C.int
)
// A NativeRule specifies an arch-specific action taken by seccomp under certain conditions.
type NativeRule struct {
// Syscall is the arch-dependent syscall number to act against.
Syscall ScmpSyscall
// Errno is the errno value to return when the condition is satisfied.
Errno ScmpErrno
// Arg is the optional struct scmp_arg_cmp passed to libseccomp.
Arg *ScmpArgCmp
}
type ExportFlag = C.hakurei_export_flag
const (
// AllowMultiarch allows multiarch/emulation.
AllowMultiarch ExportFlag = C.HAKUREI_EXPORT_MULTIARCH
// AllowCAN allows AF_CAN.
AllowCAN ExportFlag = C.HAKUREI_EXPORT_CAN
// AllowBluetooth allows AF_BLUETOOTH.
AllowBluetooth ExportFlag = C.HAKUREI_EXPORT_BLUETOOTH
)
var resPrefix = [...]string{
0: "",
1: "seccomp_init failed",
2: "seccomp_arch_add failed",
3: "seccomp_arch_add failed (multiarch)",
4: "internal libseccomp failure",
5: "seccomp_rule_add failed",
6: "seccomp_export_bpf failed",
7: "seccomp_load failed",
}
// Export streams filter contents to fd, or installs it to the current process if fd < 0.
func Export(fd int, rules []NativeRule, flags ExportFlag) error {
if len(rules) == 0 {
return ErrInvalidRules
}
var (
arch C.uint32_t = 0
multiarch C.uint32_t = 0
)
switch runtime.GOARCH {
case "386":
arch = C.SCMP_ARCH_X86
case "amd64":
arch = C.SCMP_ARCH_X86_64
multiarch = C.SCMP_ARCH_X86
case "arm":
arch = C.SCMP_ARCH_ARM
case "arm64":
arch = C.SCMP_ARCH_AARCH64
multiarch = C.SCMP_ARCH_ARM
}
var ret C.int
rulesPinner := new(runtime.Pinner)
for i := range rules {
rule := &rules[i]
rulesPinner.Pin(rule)
if rule.Arg != nil {
rulesPinner.Pin(rule.Arg)
}
}
res, err := C.hakurei_export_filter(
&ret, C.int(fd),
arch, multiarch,
(*C.struct_hakurei_syscall_rule)(unsafe.Pointer(&rules[0])),
C.size_t(len(rules)),
flags,
)
rulesPinner.Unpin()
if prefix := resPrefix[res]; prefix != "" {
return &LibraryError{
prefix,
-syscall.Errno(ret),
err,
}
}
return err
}
// ScmpCompare is the equivalent of scmp_compare;
// Comparison operators
type ScmpCompare = C.enum_scmp_compare
const (
_SCMP_CMP_MIN = C._SCMP_CMP_MIN
// not equal
SCMP_CMP_NE = C.SCMP_CMP_NE
// less than
SCMP_CMP_LT = C.SCMP_CMP_LT
// less than or equal
SCMP_CMP_LE = C.SCMP_CMP_LE
// equal
SCMP_CMP_EQ = C.SCMP_CMP_EQ
// greater than or equal
SCMP_CMP_GE = C.SCMP_CMP_GE
// greater than
SCMP_CMP_GT = C.SCMP_CMP_GT
// masked equality
SCMP_CMP_MASKED_EQ = C.SCMP_CMP_MASKED_EQ
_SCMP_CMP_MAX = C._SCMP_CMP_MAX
)
// ScmpDatum is the equivalent of scmp_datum_t;
// Argument datum
type ScmpDatum uint64
// ScmpArgCmp is the equivalent of struct scmp_arg_cmp;
// Argument / Value comparison definition
type ScmpArgCmp struct {
// argument number, starting at 0
Arg C.uint
// the comparison op, e.g. SCMP_CMP_*
Op ScmpCompare
DatumA, DatumB ScmpDatum
}
// only used for testing
func syscallResolveName(s string) (trap int) {
v := C.CString(s)
trap = int(C.seccomp_syscall_resolve_name(v))
C.free(unsafe.Pointer(v))
return
}
+94
View File
@@ -0,0 +1,94 @@
package seccomp_test
import (
"crypto/sha512"
"errors"
"io"
"slices"
"syscall"
"testing"
. "hakurei.app/container/seccomp"
)
func TestExport(t *testing.T) {
testCases := []struct {
name string
flags ExportFlag
presets FilterPreset
wantErr bool
}{
{"everything", AllowMultiarch | AllowCAN |
AllowBluetooth, PresetExt |
PresetDenyNS | PresetDenyTTY | PresetDenyDevel |
PresetLinux32, false},
{"compat", 0, 0, false},
{"base", 0, PresetExt, false},
{"strict", 0, PresetStrict, false},
{"strict compat", 0, PresetDenyNS | PresetDenyTTY | PresetDenyDevel, false},
{"hakurei default", 0, PresetExt | PresetDenyDevel, false},
{"hakurei tty", 0, PresetExt | PresetDenyNS | PresetDenyDevel, false},
}
buf := make([]byte, 8)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
e := New(Preset(tc.presets, tc.flags), tc.flags)
want := bpfExpected[bpfPreset{tc.flags, tc.presets}]
digest := sha512.New()
if _, err := io.CopyBuffer(digest, e, buf); (err != nil) != tc.wantErr {
t.Errorf("Exporter: error = %v, wantErr %v", err, tc.wantErr)
return
}
if err := e.Close(); err != nil {
t.Errorf("Close: error = %v", err)
}
if got := digest.Sum(nil); !slices.Equal(got, want) {
t.Fatalf("Export() hash = %x, want %x",
got, want)
return
}
})
}
t.Run("close without use", func(t *testing.T) {
e := New(Preset(0, 0), 0)
if err := e.Close(); !errors.Is(err, syscall.EINVAL) {
t.Errorf("Close: error = %v", err)
return
}
})
t.Run("close partial read", func(t *testing.T) {
e := New(Preset(0, 0), 0)
if _, err := e.Read(nil); err != nil {
t.Errorf("Read: error = %v", err)
return
}
// the underlying implementation uses buffered io, so the outcome of this is nondeterministic;
// that is not harmful however, so both outcomes are checked for here
if err := e.Close(); err != nil &&
(!errors.Is(err, syscall.ECANCELED) || !errors.Is(err, syscall.EBADF)) {
t.Errorf("Close: error = %v", err)
return
}
})
}
func BenchmarkExport(b *testing.B) {
buf := make([]byte, 8)
for b.Loop() {
e := New(
Preset(PresetExt|PresetDenyNS|PresetDenyTTY|PresetDenyDevel|PresetLinux32,
AllowMultiarch|AllowCAN|AllowBluetooth),
AllowMultiarch|AllowCAN|AllowBluetooth)
if _, err := io.CopyBuffer(io.Discard, e, buf); err != nil {
b.Fatalf("cannot export: %v", err)
}
if err := e.Close(); err != nil {
b.Fatalf("cannot close exporter: %v", err)
}
}
}
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env perl
# Copyright 2009 The Go Authors. All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
use strict;
use POSIX ();
my $command = "mksysnum_linux.pl ". join(' ', @ARGV);
my $uname_arch = (POSIX::uname)[4];
my %syscall_cutoff_arch = (
"x86_64" => 302,
"aarch64" => 281,
);
print <<EOF;
// $command
// Code generated by the command above; DO NOT EDIT.
package seccomp
import . "syscall"
var syscallNum = map[string]int{
EOF
my $offset = 0;
my $state = -1;
sub fmt {
my ($name, $num) = @_;
if($num > 999){
# ignore deprecated syscalls that are no longer implemented
# https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/tree/include/uapi/asm-generic/unistd.h?id=refs/heads/master#n716
return;
}
(my $name_upper = $name) =~ y/a-z/A-Z/;
$num = $num + $offset;
if($num > $syscall_cutoff_arch{$uname_arch}){ # not wired in Go standard library
if($state < 0){
print " \"$name\": SYS_$name_upper,\n";
}
else{
print " SYS_$name_upper = $num;\n";
}
}
elsif($state < 0){
print " \"$name\": SYS_$name_upper,\n";
}
else{
return;
}
}
GENERATE:
my $prev;
open(GCC, "gcc -E -dD $ARGV[0] |") || die "can't run gcc";
while(<GCC>){
if(/^#define __NR_Linux\s+([0-9]+)/){
# mips/mips64: extract offset
$offset = $1;
}
elsif(/^#define __NR_syscalls\s+/) {
# ignore redefinitions of __NR_syscalls
}
elsif(/^#define __NR_(\w+)\s+([0-9]+)/){
$prev = $2;
fmt($1, $2);
}
elsif(/^#define __NR3264_(\w+)\s+([0-9]+)/){
$prev = $2;
fmt($1, $2);
}
elsif(/^#define __NR_(\w+)\s+\(\w+\+\s*([0-9]+)\)/){
fmt($1, $prev+$2)
}
elsif(/^#define __NR_(\w+)\s+\(__NR_Linux \+ ([0-9]+)/){
fmt($1, $2);
}
}
if($state < 0){
$state = $state + 1;
print "}\n\nconst (\n";
goto GENERATE;
}
print ")";
+229
View File
@@ -0,0 +1,229 @@
package seccomp
/* flatpak commit 4c3bf179e2e4a2a298cd1db1d045adaf3f564532 */
import (
. "syscall"
)
type FilterPreset int
const (
// PresetExt are project-specific extensions.
PresetExt FilterPreset = 1 << iota
// PresetDenyNS denies namespace setup syscalls.
PresetDenyNS
// PresetDenyTTY denies faking input.
PresetDenyTTY
// PresetDenyDevel denies development-related syscalls.
PresetDenyDevel
// PresetLinux32 sets PER_LINUX32.
PresetLinux32
)
func Preset(presets FilterPreset, flags ExportFlag) (rules []NativeRule) {
allowedPersonality := PER_LINUX
if presets&PresetLinux32 != 0 {
allowedPersonality = PER_LINUX32
}
presetDevelFinal := presetDevel(ScmpDatum(allowedPersonality))
l := len(presetCommon)
if presets&PresetDenyNS != 0 {
l += len(presetNamespace)
}
if presets&PresetDenyTTY != 0 {
l += len(presetTTY)
}
if presets&PresetDenyDevel != 0 {
l += len(presetDevelFinal)
}
if flags&AllowMultiarch == 0 {
l += len(presetEmu)
}
if presets&PresetExt != 0 {
l += len(presetCommonExt)
if presets&PresetDenyNS != 0 {
l += len(presetNamespaceExt)
}
if flags&AllowMultiarch == 0 {
l += len(presetEmuExt)
}
}
rules = make([]NativeRule, 0, l)
rules = append(rules, presetCommon...)
if presets&PresetDenyNS != 0 {
rules = append(rules, presetNamespace...)
}
if presets&PresetDenyTTY != 0 {
rules = append(rules, presetTTY...)
}
if presets&PresetDenyDevel != 0 {
rules = append(rules, presetDevelFinal...)
}
if flags&AllowMultiarch == 0 {
rules = append(rules, presetEmu...)
}
if presets&PresetExt != 0 {
rules = append(rules, presetCommonExt...)
if presets&PresetDenyNS != 0 {
rules = append(rules, presetNamespaceExt...)
}
if flags&AllowMultiarch == 0 {
rules = append(rules, presetEmuExt...)
}
}
return
}
var (
presetCommon = []NativeRule{
/* Block dmesg */
{ScmpSyscall(SYS_SYSLOG), ScmpErrno(EPERM), nil},
/* Useless old syscall */
{ScmpSyscall(SYS_USELIB), ScmpErrno(EPERM), nil},
/* Don't allow disabling accounting */
{ScmpSyscall(SYS_ACCT), ScmpErrno(EPERM), nil},
/* Don't allow reading current quota use */
{ScmpSyscall(SYS_QUOTACTL), ScmpErrno(EPERM), nil},
/* Don't allow access to the kernel keyring */
{ScmpSyscall(SYS_ADD_KEY), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_KEYCTL), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_REQUEST_KEY), ScmpErrno(EPERM), nil},
/* Scary VM/NUMA ops */
{ScmpSyscall(SYS_MOVE_PAGES), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_MBIND), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_GET_MEMPOLICY), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SET_MEMPOLICY), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_MIGRATE_PAGES), ScmpErrno(EPERM), nil},
}
/* hakurei: project-specific extensions */
presetCommonExt = []NativeRule{
/* system calls for changing the system clock */
{ScmpSyscall(SYS_ADJTIMEX), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_CLOCK_ADJTIME), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_CLOCK_ADJTIME64), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_CLOCK_SETTIME), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_CLOCK_SETTIME64), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETTIMEOFDAY), ScmpErrno(EPERM), nil},
/* loading and unloading of kernel modules */
{ScmpSyscall(SYS_DELETE_MODULE), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_FINIT_MODULE), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_INIT_MODULE), ScmpErrno(EPERM), nil},
/* system calls for rebooting and reboot preparation */
{ScmpSyscall(SYS_KEXEC_FILE_LOAD), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_KEXEC_LOAD), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_REBOOT), ScmpErrno(EPERM), nil},
/* system calls for enabling/disabling swap devices */
{ScmpSyscall(SYS_SWAPOFF), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SWAPON), ScmpErrno(EPERM), nil},
}
presetNamespace = []NativeRule{
/* Don't allow subnamespace setups: */
{ScmpSyscall(SYS_UNSHARE), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETNS), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_MOUNT), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_UMOUNT), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_UMOUNT2), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_PIVOT_ROOT), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_CHROOT), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_CLONE), ScmpErrno(EPERM),
&ScmpArgCmp{cloneArg, SCMP_CMP_MASKED_EQ, CLONE_NEWUSER, CLONE_NEWUSER}},
/* seccomp can't look into clone3()'s struct clone_args to check whether
* the flags are OK, so we have no choice but to block clone3().
* Return ENOSYS so user-space will fall back to clone().
* (CVE-2021-41133; see also https://github.com/moby/moby/commit/9f6b562d)
*/
{ScmpSyscall(SYS_CLONE3), ScmpErrno(ENOSYS), nil},
/* New mount manipulation APIs can also change our VFS. There's no
* legitimate reason to do these in the sandbox, so block all of them
* rather than thinking about which ones might be dangerous.
* (CVE-2021-41133) */
{ScmpSyscall(SYS_OPEN_TREE), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_MOVE_MOUNT), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_FSOPEN), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_FSCONFIG), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_FSMOUNT), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_FSPICK), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_MOUNT_SETATTR), ScmpErrno(ENOSYS), nil},
}
/* hakurei: project-specific extensions */
presetNamespaceExt = []NativeRule{
/* changing file ownership */
{ScmpSyscall(SYS_CHOWN), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_CHOWN32), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_FCHOWN), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_FCHOWN32), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_FCHOWNAT), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_LCHOWN), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_LCHOWN32), ScmpErrno(EPERM), nil},
/* system calls for changing user ID and group ID credentials */
{ScmpSyscall(SYS_SETGID), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETGID32), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETGROUPS), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETGROUPS32), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETREGID), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETREGID32), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETRESGID), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETRESGID32), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETRESUID), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETRESUID32), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETREUID), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETREUID32), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETUID), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETUID32), ScmpErrno(EPERM), nil},
}
presetTTY = []NativeRule{
/* Don't allow faking input to the controlling tty (CVE-2017-5226) */
{ScmpSyscall(SYS_IOCTL), ScmpErrno(EPERM),
&ScmpArgCmp{1, SCMP_CMP_MASKED_EQ, 0xFFFFFFFF, TIOCSTI}},
/* In the unlikely event that the controlling tty is a Linux virtual
* console (/dev/tty2 or similar), copy/paste operations have an effect
* similar to TIOCSTI (CVE-2023-28100) */
{ScmpSyscall(SYS_IOCTL), ScmpErrno(EPERM),
&ScmpArgCmp{1, SCMP_CMP_MASKED_EQ, 0xFFFFFFFF, TIOCLINUX}},
}
presetEmu = []NativeRule{
/* modify_ldt is a historic source of interesting information leaks,
* so it's disabled as a hardening measure.
* However, it is required to run old 16-bit applications
* as well as some Wine patches, so it's allowed in multiarch. */
{ScmpSyscall(SYS_MODIFY_LDT), ScmpErrno(EPERM), nil},
}
/* hakurei: project-specific extensions */
presetEmuExt = []NativeRule{
{ScmpSyscall(SYS_SUBPAGE_PROT), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_SWITCH_ENDIAN), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_VM86), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_VM86OLD), ScmpErrno(ENOSYS), nil},
}
)
func presetDevel(allowedPersonality ScmpDatum) []NativeRule {
return []NativeRule{
/* Profiling operations; we expect these to be done by tools from outside
* the sandbox. In particular perf has been the source of many CVEs. */
{ScmpSyscall(SYS_PERF_EVENT_OPEN), ScmpErrno(EPERM), nil},
/* Don't allow you to switch to bsd emulation or whatnot */
{ScmpSyscall(SYS_PERSONALITY), ScmpErrno(EPERM),
&ScmpArgCmp{0, SCMP_CMP_NE, allowedPersonality, 0}},
{ScmpSyscall(SYS_PTRACE), ScmpErrno(EPERM), nil},
}
}
@@ -0,0 +1,7 @@
//go:build s390 || s390x
package seccomp
/* Architectures with CONFIG_CLONE_BACKWARDS2: the child stack
* and flags arguments are reversed so the flags come second */
const cloneArg = 1
@@ -0,0 +1,6 @@
//go:build !s390 && !s390x
package seccomp
/* Normally the flags come first */
const cloneArg = 0

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