Compare commits

...

115 Commits

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

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-26 03:27:07 +09:00
07194c74cb
release: 0.2.0
All checks were successful
Release / Create release (push) Successful in 39s
Test / Sandbox (push) Successful in 41s
Test / Hakurei (push) Successful in 1m9s
Test / Create distribution (push) Successful in 24s
Test / Hpkg (push) Successful in 1m10s
Test / Sandbox (race detector) (push) Successful in 4m5s
Test / Hakurei (race detector) (push) Successful in 5m12s
Test / Flake checks (push) Successful in 1m31s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-26 02:23:59 +09:00
4cf694d2b3
hst: use hsu userid for share path suffix
All checks were successful
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
c9facb746b
hst/config: remove data field, rename dir to home
All checks were successful
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
878b66022e
hst/fsbind: optional ensure source
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m18s
Test / Hakurei (push) Successful in 3m22s
Test / Hpkg (push) Successful in 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
2e0a4795f6
container/initbind: optional ensure host directory
All checks were successful
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
c328b584c0
hst/fslink: improve string representation
All checks were successful
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
9585b35d5b
hst/config: remove symlink field
All checks were successful
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
26cafe3e80
hst/fs: implement link fstype
All checks were successful
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
125f150784
hst/fs: update doc comments
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m18s
Test / Hakurei (push) Successful in 3m22s
Test / Hpkg (push) Successful in 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
0dcac55a0c
hst/config: remove container etc field
All checks were successful
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
6d202d73b4
hst/fsbind: optional autoetc behaviour
All checks were successful
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
1438096339
hst/config: handle filesystem entry targeting root
All checks were successful
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
059164d4fa
hst/fsbind: optional autoroot behaviour
All checks were successful
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
8db906ee64
container/dispatcher: remove exit stub test log
All checks were successful
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
cedfceded5
container/autoroot: remove prefix field
All checks were successful
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
33d2dcce1b
container/initoverlay: internal bypass sysroot prefix
All checks were successful
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
2baa2d7063
container/init: measure init behaviour
All checks were successful
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
0166833431
container/dispatcher: start goroutine in dispatcher
All checks were successful
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
b3da3da525
container/init: avoid multiple lastcap calls
All checks were successful
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
1b3902df78
container/dispatcher: instrument each goroutine individually
All checks were successful
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
ea1e3ebae9
container/params: pass fd instead of file
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 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
1c692bfb79
container/init: call lockOSThread through dispatcher
All checks were successful
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
141a18999f
container: move integration test helpers
All checks were successful
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
afe23600d2
container/path: use syscall dispatcher
All checks were successful
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
09d2844981
container/init: wrap syscall helper functions
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m7s
Test / Hakurei (push) Successful in 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
d500d6e559
system/dbus: share host net ns for abstract
All checks were successful
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
5b73316ae0
container/syscall: doc comments from manpages
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 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
5d8a2199b6
container/init: op interface valid method
All checks were successful
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
a1482ecdd0
container/inittmpfs: check path equivalence by value
All checks were successful
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
a07f9ed84c
container/initsymlink: check path equivalence by value
All checks were successful
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
51304b03af
container/initremount: check path equivalence by value
All checks were successful
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
c6397b941f
container/initproc: check path equivalence by value
All checks were successful
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
d65e5f817a
container/initplace: check path equivalence by value
All checks were successful
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
696e593898
container/initoverlay: check path equivalence by value
All checks were successful
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
97ab24feef
container/init: use absolute compare method
All checks were successful
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
31f0dd36df
absolute: efficient equivalence check method
All checks were successful
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
9aec2f46fe
container/initdev: check path equivalence by value
All checks were successful
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
022cc26b2e
container/capability: check CAP_TO_INDEX and CAP_TO_MASK
All checks were successful
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
b4c018da8f
container/autoetc: do not bypass absolute check
All checks were successful
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
66f52407d3
container/initmkdir: check path equivalence by value
All checks were successful
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
e463faf649
container/initbind: check path equivalence by value
All checks were successful
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
375acb476d
container/autoroot: check host path equivalence by value
All checks were successful
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
c81c9a9d75
container/init: split setup ops into individual files
All checks were successful
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
339e4080dc
container/ops: move Op type to init file
All checks were successful
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
e0533aaa68
container/autoroot: filter dentry with empty name
All checks were successful
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
13c7083bc0
container: ptrace protection via Yama LSM
All checks were successful
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
6947ff04e0
system/dbus/proc: host abstract only when not binding
All checks were successful
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
140fe21237
container/params: check setup/receive behaviour
All checks were successful
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
f52d2c7db6
container/path: check create and mountinfo helpers
All checks were successful
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
3c9e547c4a
cmd/hpkg: add deprecation notice
All checks were successful
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
a3988c1a77
hst: rename net and abstract fields
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 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
5db0714072
container: optionally isolate host abstract UNIX domain sockets via landlock
All checks were successful
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
69a4ab8105
container: move PR_SET_NO_NEW_PRIVS to parent
All checks were successful
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
22d577ab49
test/sandbox: do not discard stderr getting hash
All checks were successful
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
83a1c75f1a
app: set up acl on X11 socket
All checks were successful
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 #1.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-18 11:30:58 +09:00
0ac6e99818
container: start from locked thread
All checks were successful
Test / Hpkg (push) Successful in 4m14s
Test / Create distribution (push) Successful in 33s
Test / Sandbox (race detector) (push) Successful in 4m28s
Test / Hakurei (race detector) (push) Successful in 5m12s
Test / Flake checks (push) Successful in 1m33s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m17s
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
f35733810e
container: check output helper functions
All checks were successful
Test / Hakurei (race detector) (push) Successful in 5m17s
Test / Flake checks (push) Successful in 1m46s
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
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
9c1a5d43ba
container: enforce nonrepeatable autoetc and autoroot
All checks were successful
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
8aa65f28c6
container: allow additional state between ops
All checks were successful
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
f9edec7e41
hst: merge miscellaneous files
All checks were successful
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
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m10s
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
305c600cf5
hst: move container type to config
All checks were successful
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
8dd3e1ee5d
hst/fs: rename method Target to Path
All checks were successful
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
4ffeec3004
hst/enablement: editor friendly enablement adaptor
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Hakurei (push) Successful in 45s
Test / Hpkg (push) Successful in 3m17s
Test / Sandbox (push) Successful in 43s
Test / Hakurei (race detector) (push) Successful in 45s
Test / Sandbox (race detector) (push) Successful in 43s
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
9ed3ba85ea
hst/fs: implement overlay fstype
All checks were successful
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
4433c993fa
nix: check config via hakurei
All checks were successful
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
430991c39b
hst/fs: remove type method
All checks were successful
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
ba3227bf15
container: export overlay escape
All checks were successful
Test / Sandbox (push) Successful in 2m21s
Test / Hakurei (push) Successful in 3m23s
Test / Sandbox (race detector) (push) Successful in 4m22s
Test / Hpkg (push) Successful in 4m14s
Test / Hakurei (race detector) (push) Successful in 5m8s
Test / Flake checks (push) Successful in 1m22s
Test / Create distribution (push) Successful in 37s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-14 23:44:11 +09:00
0e543a58b3
hst/fs: valid method on underlying interface
All checks were successful
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
c989e7785a
hst/info: include extra information
All checks were successful
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
332d90d6c7
container/path: remove unused path
All checks were successful
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
99ac96511b
hst/fs: interface filesystem config
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m37s
Test / Hpkg (push) Successful in 4m27s
Test / Sandbox (race detector) (push) Successful in 4m23s
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
e99d7affb0
container: use absolute for pathname
All checks were successful
Test / Flake checks (push) Successful in 1m26s
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
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
41ac2be965
container/absolute: wrap safe stdlib functions
All checks were successful
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
02271583fb
container: remove PATH lookup behaviour
All checks were successful
Test / Hakurei (race detector) (push) Successful in 2m42s
Test / Flake checks (push) Successful in 1m25s
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
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
ef54b2cd08
container/absolute: early absolute pathname check
All checks were successful
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
82608164f6
container/params: remove confusingly named error
All checks were successful
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 / Flake checks (push) Successful in 1m19s
Test / Sandbox (race detector) (push) Successful in 4m16s
Test / Hakurei (race detector) (push) Successful in 4m49s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-09 17:37:46 +09:00
edd6f2cfa9
container: document ambient capabilities
All checks were successful
Test / Hakurei (push) Successful in 2m3s
Test / Flake checks (push) Successful in 1m22s
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
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-08 02:11:55 +09:00
acffa76812
container/ops: implement overlay op
All checks were successful
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
8da76483e6
container/path: fix typo "paths"
All checks were successful
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
534c932906
container: test case runtime initialisation
All checks were successful
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
fee10fed4d
container: test bypass output buffer on verbose
All checks were successful
Test / Flake checks (push) Successful in 1m25s
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
This restores verbose behaviour.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-08 00:57:27 +09:00
a4f7e92e1c
test/interactive: helper scripts for tracing
All checks were successful
Test / Hakurei (push) Successful in 41s
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 39s
Test / Hpkg (push) Successful in 40s
Test / Hakurei (race detector) (push) Successful in 41s
Test / Sandbox (race detector) (push) Successful in 39s
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
f1a53d6116
container: raise CAP_DAC_OVERRIDE
All checks were successful
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
b353c3deea
nix: make src overlay writable
All checks were successful
Test / Hakurei (push) Successful in 42s
Test / Create distribution (push) Successful in 33s
Test / Hakurei (race detector) (push) Successful in 42s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Sandbox (push) Successful in 40s
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
fde5f1ca64
container: buffer test output
All checks were successful
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 / Hakurei (race detector) (push) Successful in 4m37s
Test / Sandbox (race detector) (push) Successful in 4m7s
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
4d0bdd84b5
container: test respect verbose flag
All checks were successful
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
72a931a71a
nix: interactive nixos vm
All checks were successful
Test / Hakurei (push) Successful in 41s
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Hpkg (push) Successful in 40s
Test / Hakurei (race detector) (push) Successful in 41s
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
9a25542c6d
container/init: use mount string constants
All checks were successful
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
c6be82bcf9
container/path: fhs path constants
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m6s
Test / Hakurei (push) Successful in 3m6s
Test / Sandbox (race detector) (push) Successful in 4m14s
Test / Hpkg (push) Successful in 4m11s
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
38245559dc
container/ops: mount dev readonly
All checks were successful
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
7b416d47dc
container/ops: merge mqueue and dev Ops
All checks were successful
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
15170735ba
container/mount: move tmpfs sysroot prefixing to caller
All checks were successful
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
6a3886e9db
container/op: unexport bind resolved source field
All checks were successful
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
ff66296378
container/mount: mount data escape helper function
All checks were successful
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
347a79df72
container: improve clone flags readability
All checks were successful
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
0f78864a67
container/mount: export mount string constants
All checks were successful
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
b32b1975a8
hst/container: remove cover
All checks were successful
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
2b1eaa62f1
update github notice
All checks were successful
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
f13dca184c
release: 0.1.3
All checks were successful
Test / Create distribution (push) Successful in 26s
Release / Create release (push) Successful in 41s
Test / Sandbox (push) Successful in 41s
Test / Hakurei (push) Successful in 44s
Test / Hakurei (race detector) (push) Successful in 44s
Test / Sandbox (race detector) (push) Successful in 42s
Test / Hpkg (push) Successful in 42s
Test / Flake checks (push) Successful in 1m17s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-02 00:02:54 +09:00
3b8a3d3b00
app: remount root readonly
All checks were successful
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
c5d24979f5
container/ops: expose remount as Op
All checks were successful
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
1dc780bca7
container/mount: separate remount from bind
All checks were successful
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
ec33061c92
nix: remove nscd cover
All checks were successful
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
af0899de96
hst/container: mount tmpfs via magic src string
All checks were successful
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
547a2adaa4
container/mount: pass tmpfs flags
All checks were successful
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
c02948e155
cmd/hakurei: print autoroot configuration
All checks were successful
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
387b86bcdd
app: integrate container autoroot
All checks were successful
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
4e85643865
container: implement autoroot as setup op
All checks were successful
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
987981df73
test/sandbox: check pd behaviour
All checks were successful
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
f14e7255be
container/ops: use correct flags value in bind string
All checks were successful
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
a8a79a8664
cmd/hpkg: rename from planterette
All checks were successful
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
3ae0cec000
test: increase vm memory
All checks were successful
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
4e518f11d8
container/ops: autoetc implementation to separate file
All checks were successful
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
126 changed files with 13027 additions and 2338 deletions

View File

@ -73,20 +73,20 @@ jobs:
path: result/* path: result/*
retention-days: 1 retention-days: 1
planterette: hpkg:
name: Planterette name: Hpkg
runs-on: nix runs-on: nix
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Run NixOS test - name: Run NixOS test
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.planterette run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.hpkg
- name: Upload test output - name: Upload test output
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: "planterette-vm-output" name: "hpkg-vm-output"
path: result/* path: result/*
retention-days: 1 retention-days: 1
@ -97,7 +97,7 @@ jobs:
- race - race
- sandbox - sandbox
- sandbox-race - sandbox-race
- planterette - hpkg
runs-on: nix runs-on: nix
steps: steps:
- name: Checkout - name: Checkout

View File

@ -1 +1,5 @@
DO NOT ADD NEW ACTIONS HERE
This port is solely for releasing to the github mirror and serves no purpose during development. 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.

3
.gitignore vendored
View File

@ -30,3 +30,6 @@ go.work.sum
# release # release
/dist/hakurei-* /dist/hakurei-*
# interactive nixos vm
nixos.qcow2

View File

@ -16,7 +16,8 @@
</p> </p>
Hakurei is a tool for running sandboxed graphical applications as dedicated subordinate users on the Linux kernel. Hakurei is a tool for running sandboxed graphical applications as dedicated subordinate users on the Linux kernel.
It also implements [planterette (WIP)](cmd/planterette), a self-contained Android-like package manager with modern security features. It implements the application container of [planterette (WIP)](https://git.gensokyo.uk/security/planterette),
a self-contained Android-like package manager with modern security features.
## NixOS Module usage ## NixOS Module usage

View File

@ -14,6 +14,7 @@ import (
"time" "time"
"hakurei.app/command" "hakurei.app/command"
"hakurei.app/container"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal" "hakurei.app/internal"
"hakurei.app/internal/app" "hakurei.app/internal/app"
@ -94,7 +95,7 @@ func buildCommand(out io.Writer) command.Command {
Gid: us, Gid: us,
Username: "chronos", Username: "chronos",
Name: "Hakurei Permissive Default", Name: "Hakurei Permissive Default",
HomeDir: "/var/empty", HomeDir: container.FHSVarEmpty,
} }
} else { } else {
passwd = u passwd = u
@ -114,21 +115,29 @@ func buildCommand(out io.Writer) command.Command {
config.Identity = aid config.Identity = aid
config.Groups = groups config.Groups = groups
config.Data = homeDir
config.Username = userName 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 { if wayland {
config.Enablements |= system.EWayland e |= system.EWayland
} }
if x11 { if x11 {
config.Enablements |= system.EX11 e |= system.EX11
} }
if dBus { if dBus {
config.Enablements |= system.EDBus e |= system.EDBus
} }
if pulse { if pulse {
config.Enablements |= system.EPulse e |= system.EPulse
} }
config.Enablements = hst.NewEnablements(e)
// parse D-Bus config file from flags if applicable // parse D-Bus config file from flags if applicable
if dBus { if dBus {
@ -212,7 +221,7 @@ func buildCommand(out io.Writer) command.Command {
var psFlagShort bool var psFlagShort bool
c.NewCommand("ps", "List active instances", func(args []string) error { c.NewCommand("ps", "List active instances", func(args []string) error {
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(std.Paths().RunDirPath), psFlagShort, flagJSON) printPs(os.Stdout, time.Now().UTC(), state.NewMulti(std.Paths().RunDirPath.String()), psFlagShort, flagJSON)
return errSuccess return errSuccess
}).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id") }).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id")

View File

@ -30,6 +30,11 @@ func main() {
// early init path, skips root check and duplicate PR_SET_DUMPABLE // early init path, skips root check and duplicate PR_SET_DUMPABLE
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput) container.TryArgv0(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 { if err := container.SetDumpable(container.SUID_DUMP_DISABLE); err != nil {
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err) log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
// not fatal: this program runs as the privileged user // not fatal: this program runs as the privileged user

View File

@ -87,7 +87,7 @@ func tryShort(name string) (config *hst.Config, entry *state.State) {
if likePrefix && len(name) >= 8 { if likePrefix && len(name) >= 8 {
hlog.Verbose("argument looks like prefix") hlog.Verbose("argument looks like prefix")
s := state.NewMulti(std.Paths().RunDirPath) s := state.NewMulti(std.Paths().RunDirPath.String())
if entries, err := state.Join(s); err != nil { if entries, err := state.Join(s); err != nil {
log.Printf("cannot join store: %v", err) log.Printf("cannot join store: %v", err)
// drop to fetch from file // drop to fetch from file

View File

@ -22,9 +22,9 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
t := newPrinter(output) t := newPrinter(output)
defer t.MustFlush() defer t.MustFlush()
info := new(hst.Info) info := &hst.Info{Paths: std.Paths()}
// get fid by querying uid of aid 0 // get hid by querying uid of identity 0
if uid, err := std.Uid(0); err != nil { if uid, err := std.Uid(0); err != nil {
hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:") hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:")
os.Exit(1) os.Exit(1)
@ -38,6 +38,10 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
} }
t.Printf("User:\t%d\n", info.User) t.Printf("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( func printShowInstance(
@ -73,17 +77,17 @@ func printShowInstance(
} else { } else {
t.Printf(" Identity:\t%d\n", config.Identity) t.Printf(" Identity:\t%d\n", config.Identity)
} }
t.Printf(" Enablements:\t%s\n", config.Enablements.String()) t.Printf(" Enablements:\t%s\n", config.Enablements.Unwrap().String())
if len(config.Groups) > 0 { if len(config.Groups) > 0 {
t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", ")) t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", "))
} }
if config.Data != "" { if config.Home != nil {
t.Printf(" Data:\t%s\n", config.Data) t.Printf(" Home:\t%s\n", config.Home)
} }
if config.Container != nil { if config.Container != nil {
container := config.Container params := config.Container
if container.Hostname != "" { if params.Hostname != "" {
t.Printf(" Hostname:\t%s\n", container.Hostname) t.Printf(" Hostname:\t%s\n", params.Hostname)
} }
flags := make([]string, 0, 7) flags := make([]string, 0, 7)
writeFlag := func(name string, value bool) { writeFlag := func(name string, value bool) {
@ -91,31 +95,23 @@ func printShowInstance(
flags = append(flags, name) flags = append(flags, name)
} }
} }
writeFlag("userns", container.Userns) writeFlag("userns", params.Userns)
writeFlag("devel", container.Devel) writeFlag("devel", params.Devel)
writeFlag("net", container.Net) writeFlag("net", params.HostNet)
writeFlag("device", container.Device) writeFlag("abstract", params.HostAbstract)
writeFlag("tty", container.Tty) writeFlag("device", params.Device)
writeFlag("mapuid", container.MapRealUID) writeFlag("tty", params.Tty)
writeFlag("mapuid", params.MapRealUID)
writeFlag("directwl", config.DirectWayland) writeFlag("directwl", config.DirectWayland)
writeFlag("autoetc", container.AutoEtc)
if len(flags) == 0 { if len(flags) == 0 {
flags = append(flags, "none") flags = append(flags, "none")
} }
t.Printf(" Flags:\t%s\n", strings.Join(flags, " ")) t.Printf(" Flags:\t%s\n", strings.Join(flags, " "))
etc := container.Etc if config.Path != nil {
if etc == "" {
etc = "/etc"
}
t.Printf(" Etc:\t%s\n", etc)
if len(container.Cover) > 0 {
t.Printf(" Cover:\t%s\n", strings.Join(container.Cover, " "))
}
t.Printf(" Path:\t%s\n", config.Path) t.Printf(" Path:\t%s\n", config.Path)
} }
}
if len(config.Args) > 0 { if len(config.Args) > 0 {
t.Printf(" Arguments:\t%s\n", strings.Join(config.Args, " ")) t.Printf(" Arguments:\t%s\n", strings.Join(config.Args, " "))
} }
@ -125,30 +121,11 @@ func printShowInstance(
if config.Container != nil && len(config.Container.Filesystem) > 0 { if config.Container != nil && len(config.Container.Filesystem) > 0 {
t.Printf("Filesystem\n") t.Printf("Filesystem\n")
for _, f := range config.Container.Filesystem { for _, f := range config.Container.Filesystem {
if f == nil { if !f.Valid() {
t.Println(" <invalid>")
continue continue
} }
t.Printf(" %s\n", f)
expr := new(strings.Builder)
expr.Grow(3 + len(f.Src) + 1 + len(f.Dst))
if f.Device {
expr.WriteString(" d")
} else if f.Write {
expr.WriteString(" w")
} else {
expr.WriteString(" ")
}
if f.Must {
expr.WriteString("*")
} else {
expr.WriteString("+")
}
expr.WriteString(f.Src)
if f.Dst != "" {
expr.WriteString(":" + f.Dst)
}
t.Printf("%s\n", expr.String())
} }
t.Printf("\n") t.Printf("\n")
} }

View File

@ -39,20 +39,21 @@ func Test_printShowInstance(t *testing.T) {
Identity: 9 (org.chromium.Chromium) Identity: 9 (org.chromium.Chromium)
Enablements: wayland, dbus, pulseaudio Enablements: wayland, dbus, pulseaudio
Groups: video, dialout, plugdev Groups: video, dialout, plugdev
Data: /var/lib/hakurei/u0/org.chromium.Chromium Home: /data/data/org.chromium.Chromium
Hostname: localhost Hostname: localhost
Flags: userns devel net device tty mapuid autoetc Flags: userns devel net abstract device tty mapuid
Etc: /etc
Cover: /var/run/nscd
Path: /run/current-system/sw/bin/chromium Path: /run/current-system/sw/bin/chromium
Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
Filesystem Filesystem
+/nix/store autoroot:w:/var/lib/hakurei/base/org.debian
+/run/current-system autoetc:/etc/
+/run/opengl-driver w+ephemeral(-rwxr-xr-x):/tmp/
+/var/db/nix-channels w*/nix/store:/mnt-root/nix/.rw-store/upper:/mnt-root/nix/.rw-store/work:/mnt-root/nix/.ro-store
w*/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium */nix/store
/run/current-system@
/run/opengl-driver@
w-/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
d+/dev/dri d+/dev/dri
Extra ACL Extra ACL
@ -82,18 +83,15 @@ App
Identity: 0 Identity: 0
Enablements: (no enablements) Enablements: (no enablements)
Flags: none Flags: none
Etc: /etc
Path:
`}, `},
{"config nil entries", nil, &hst.Config{Container: &hst.ContainerConfig{Filesystem: make([]*hst.FilesystemConfig, 1)}, ExtraPerms: make([]*hst.ExtraPermConfig, 1)}, false, false, `App {"config nil entries", nil, &hst.Config{Container: &hst.ContainerConfig{Filesystem: make([]hst.FilesystemConfigJSON, 1)}, ExtraPerms: make([]*hst.ExtraPermConfig, 1)}, false, false, `App
Identity: 0 Identity: 0
Enablements: (no enablements) Enablements: (no enablements)
Flags: none Flags: none
Etc: /etc
Path:
Filesystem Filesystem
<invalid>
Extra ACL Extra ACL
@ -118,20 +116,21 @@ App
Identity: 9 (org.chromium.Chromium) Identity: 9 (org.chromium.Chromium)
Enablements: wayland, dbus, pulseaudio Enablements: wayland, dbus, pulseaudio
Groups: video, dialout, plugdev Groups: video, dialout, plugdev
Data: /var/lib/hakurei/u0/org.chromium.Chromium Home: /data/data/org.chromium.Chromium
Hostname: localhost Hostname: localhost
Flags: userns devel net device tty mapuid autoetc Flags: userns devel net abstract device tty mapuid
Etc: /etc
Cover: /var/run/nscd
Path: /run/current-system/sw/bin/chromium Path: /run/current-system/sw/bin/chromium
Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
Filesystem Filesystem
+/nix/store autoroot:w:/var/lib/hakurei/base/org.debian
+/run/current-system autoetc:/etc/
+/run/opengl-driver w+ephemeral(-rwxr-xr-x):/tmp/
+/var/db/nix-channels w*/nix/store:/mnt-root/nix/.rw-store/upper:/mnt-root/nix/.rw-store/work:/mnt-root/nix/.ro-store
w*/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium */nix/store
/run/current-system@
/run/opengl-driver@
w-/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
d+/dev/dri d+/dev/dri
Extra ACL Extra ACL
@ -194,7 +193,11 @@ App
"--enable-features=UseOzonePlatform", "--enable-features=UseOzonePlatform",
"--ozone-platform=wayland" "--ozone-platform=wayland"
], ],
"enablements": 13, "enablements": {
"wayland": true,
"dbus": true,
"pulse": true
},
"session_bus": { "session_bus": {
"see": null, "see": null,
"talk": [ "talk": [
@ -233,8 +236,7 @@ App
}, },
"username": "chronos", "username": "chronos",
"shell": "/run/current-system/sw/bin/zsh", "shell": "/run/current-system/sw/bin/zsh",
"data": "/var/lib/hakurei/u0/org.chromium.Chromium", "home": "/data/data/org.chromium.Chromium",
"dir": "/data/data/org.chromium.Chromium",
"extra_perms": [ "extra_perms": [
{ {
"ensure": true, "ensure": true,
@ -262,7 +264,8 @@ App
"seccomp_compat": true, "seccomp_compat": true,
"devel": true, "devel": true,
"userns": true, "userns": true,
"net": true, "host_net": true,
"host_abstract": true,
"tty": true, "tty": true,
"multiarch": true, "multiarch": true,
"env": { "env": {
@ -274,38 +277,62 @@ App
"device": true, "device": true,
"filesystem": [ "filesystem": [
{ {
"type": "bind",
"dst": "/",
"src": "/var/lib/hakurei/base/org.debian",
"write": true,
"special": true
},
{
"type": "bind",
"dst": "/etc/",
"src": "/etc/",
"special": true
},
{
"type": "ephemeral",
"dst": "/tmp/",
"write": true,
"perm": 493
},
{
"type": "overlay",
"dst": "/nix/store",
"lower": [
"/mnt-root/nix/.ro-store"
],
"upper": "/mnt-root/nix/.rw-store/upper",
"work": "/mnt-root/nix/.rw-store/work"
},
{
"type": "bind",
"src": "/nix/store" "src": "/nix/store"
}, },
{ {
"src": "/run/current-system" "type": "link",
"dst": "/run/current-system",
"linkname": "/run/current-system",
"dereference": true
}, },
{ {
"src": "/run/opengl-driver" "type": "link",
}, "dst": "/run/opengl-driver",
{ "linkname": "/run/opengl-driver",
"src": "/var/db/nix-channels" "dereference": true
}, },
{ {
"type": "bind",
"dst": "/data/data/org.chromium.Chromium", "dst": "/data/data/org.chromium.Chromium",
"src": "/var/lib/hakurei/u0/org.chromium.Chromium", "src": "/var/lib/hakurei/u0/org.chromium.Chromium",
"write": true, "write": true,
"require": true "ensure": true
}, },
{ {
"type": "bind",
"src": "/dev/dri", "src": "/dev/dri",
"dev": true "dev": true,
"optional": true
} }
],
"symlink": [
[
"/run/user/65534",
"/run/user/150"
]
],
"etc": "/etc",
"auto_etc": true,
"cover": [
"/var/run/nscd"
] ]
} }
}, },
@ -322,7 +349,11 @@ App
"--enable-features=UseOzonePlatform", "--enable-features=UseOzonePlatform",
"--ozone-platform=wayland" "--ozone-platform=wayland"
], ],
"enablements": 13, "enablements": {
"wayland": true,
"dbus": true,
"pulse": true
},
"session_bus": { "session_bus": {
"see": null, "see": null,
"talk": [ "talk": [
@ -361,8 +392,7 @@ App
}, },
"username": "chronos", "username": "chronos",
"shell": "/run/current-system/sw/bin/zsh", "shell": "/run/current-system/sw/bin/zsh",
"data": "/var/lib/hakurei/u0/org.chromium.Chromium", "home": "/data/data/org.chromium.Chromium",
"dir": "/data/data/org.chromium.Chromium",
"extra_perms": [ "extra_perms": [
{ {
"ensure": true, "ensure": true,
@ -390,7 +420,8 @@ App
"seccomp_compat": true, "seccomp_compat": true,
"devel": true, "devel": true,
"userns": true, "userns": true,
"net": true, "host_net": true,
"host_abstract": true,
"tty": true, "tty": true,
"multiarch": true, "multiarch": true,
"env": { "env": {
@ -402,38 +433,62 @@ App
"device": true, "device": true,
"filesystem": [ "filesystem": [
{ {
"type": "bind",
"dst": "/",
"src": "/var/lib/hakurei/base/org.debian",
"write": true,
"special": true
},
{
"type": "bind",
"dst": "/etc/",
"src": "/etc/",
"special": true
},
{
"type": "ephemeral",
"dst": "/tmp/",
"write": true,
"perm": 493
},
{
"type": "overlay",
"dst": "/nix/store",
"lower": [
"/mnt-root/nix/.ro-store"
],
"upper": "/mnt-root/nix/.rw-store/upper",
"work": "/mnt-root/nix/.rw-store/work"
},
{
"type": "bind",
"src": "/nix/store" "src": "/nix/store"
}, },
{ {
"src": "/run/current-system" "type": "link",
"dst": "/run/current-system",
"linkname": "/run/current-system",
"dereference": true
}, },
{ {
"src": "/run/opengl-driver" "type": "link",
}, "dst": "/run/opengl-driver",
{ "linkname": "/run/opengl-driver",
"src": "/var/db/nix-channels" "dereference": true
}, },
{ {
"type": "bind",
"dst": "/data/data/org.chromium.Chromium", "dst": "/data/data/org.chromium.Chromium",
"src": "/var/lib/hakurei/u0/org.chromium.Chromium", "src": "/var/lib/hakurei/u0/org.chromium.Chromium",
"write": true, "write": true,
"require": true "ensure": true
}, },
{ {
"type": "bind",
"src": "/dev/dri", "src": "/dev/dri",
"dev": true "dev": true,
"optional": true
} }
],
"symlink": [
[
"/run/user/65534",
"/run/user/150"
]
],
"etc": "/etc",
"auto_etc": true,
"cover": [
"/var/run/nscd"
] ]
} }
} }
@ -504,7 +559,11 @@ func Test_printPs(t *testing.T) {
"--enable-features=UseOzonePlatform", "--enable-features=UseOzonePlatform",
"--ozone-platform=wayland" "--ozone-platform=wayland"
], ],
"enablements": 13, "enablements": {
"wayland": true,
"dbus": true,
"pulse": true
},
"session_bus": { "session_bus": {
"see": null, "see": null,
"talk": [ "talk": [
@ -543,8 +602,7 @@ func Test_printPs(t *testing.T) {
}, },
"username": "chronos", "username": "chronos",
"shell": "/run/current-system/sw/bin/zsh", "shell": "/run/current-system/sw/bin/zsh",
"data": "/var/lib/hakurei/u0/org.chromium.Chromium", "home": "/data/data/org.chromium.Chromium",
"dir": "/data/data/org.chromium.Chromium",
"extra_perms": [ "extra_perms": [
{ {
"ensure": true, "ensure": true,
@ -572,7 +630,8 @@ func Test_printPs(t *testing.T) {
"seccomp_compat": true, "seccomp_compat": true,
"devel": true, "devel": true,
"userns": true, "userns": true,
"net": true, "host_net": true,
"host_abstract": true,
"tty": true, "tty": true,
"multiarch": true, "multiarch": true,
"env": { "env": {
@ -584,38 +643,62 @@ func Test_printPs(t *testing.T) {
"device": true, "device": true,
"filesystem": [ "filesystem": [
{ {
"type": "bind",
"dst": "/",
"src": "/var/lib/hakurei/base/org.debian",
"write": true,
"special": true
},
{
"type": "bind",
"dst": "/etc/",
"src": "/etc/",
"special": true
},
{
"type": "ephemeral",
"dst": "/tmp/",
"write": true,
"perm": 493
},
{
"type": "overlay",
"dst": "/nix/store",
"lower": [
"/mnt-root/nix/.ro-store"
],
"upper": "/mnt-root/nix/.rw-store/upper",
"work": "/mnt-root/nix/.rw-store/work"
},
{
"type": "bind",
"src": "/nix/store" "src": "/nix/store"
}, },
{ {
"src": "/run/current-system" "type": "link",
"dst": "/run/current-system",
"linkname": "/run/current-system",
"dereference": true
}, },
{ {
"src": "/run/opengl-driver" "type": "link",
}, "dst": "/run/opengl-driver",
{ "linkname": "/run/opengl-driver",
"src": "/var/db/nix-channels" "dereference": true
}, },
{ {
"type": "bind",
"dst": "/data/data/org.chromium.Chromium", "dst": "/data/data/org.chromium.Chromium",
"src": "/var/lib/hakurei/u0/org.chromium.Chromium", "src": "/var/lib/hakurei/u0/org.chromium.Chromium",
"write": true, "write": true,
"require": true "ensure": true
}, },
{ {
"type": "bind",
"src": "/dev/dri", "src": "/dev/dri",
"dev": true "dev": true,
"optional": true
} }
],
"symlink": [
[
"/run/user/65534",
"/run/user/150"
]
],
"etc": "/etc",
"auto_etc": true,
"cover": [
"/var/run/nscd"
] ]
} }
}, },

7
cmd/hpkg/README Normal file
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.

View File

@ -4,11 +4,10 @@ import (
"encoding/json" "encoding/json"
"log" "log"
"os" "os"
"path"
"hakurei.app/container"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/system"
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
) )
@ -27,7 +26,9 @@ type appInfo struct {
// passed through to [hst.Config] // passed through to [hst.Config]
Userns bool `json:"userns,omitempty"` Userns bool `json:"userns,omitempty"`
// passed through to [hst.Config] // passed through to [hst.Config]
Net bool `json:"net,omitempty"` HostNet bool `json:"net,omitempty"`
// passed through to [hst.Config]
HostAbstract bool `json:"abstract,omitempty"`
// passed through to [hst.Config] // passed through to [hst.Config]
Device bool `json:"dev,omitempty"` Device bool `json:"dev,omitempty"`
// passed through to [hst.Config] // passed through to [hst.Config]
@ -41,7 +42,7 @@ type appInfo struct {
// passed through to [hst.Config] // passed through to [hst.Config]
SessionBus *dbus.Config `json:"session_bus,omitempty"` SessionBus *dbus.Config `json:"session_bus,omitempty"`
// passed through to [hst.Config] // passed through to [hst.Config]
Enablements system.Enablement `json:"enablements"` Enablements *hst.Enablements `json:"enablements,omitempty"`
// passed through to [hst.Config] // passed through to [hst.Config]
Multiarch bool `json:"multiarch,omitempty"` Multiarch bool `json:"multiarch,omitempty"`
@ -55,18 +56,18 @@ type appInfo struct {
// store path to nixGL source // store path to nixGL source
NixGL string `json:"nix_gl,omitempty"` NixGL string `json:"nix_gl,omitempty"`
// store path to activate-and-exec script // store path to activate-and-exec script
Launcher string `json:"launcher"` Launcher *container.Absolute `json:"launcher"`
// store path to /run/current-system // store path to /run/current-system
CurrentSystem string `json:"current_system"` CurrentSystem *container.Absolute `json:"current_system"`
// store path to home-manager activation package // store path to home-manager activation package
ActivationPackage string `json:"activation_package"` ActivationPackage string `json:"activation_package"`
} }
func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool) *hst.Config { func (app *appInfo) toHst(pathSet *appPathSet, pathname *container.Absolute, argv []string, flagDropShell bool) *hst.Config {
config := &hst.Config{ config := &hst.Config{
ID: app.ID, ID: app.ID,
Path: argv[0], Path: pathname,
Args: argv, Args: argv,
Enablements: app.Enablements, Enablements: app.Enablements,
@ -76,9 +77,8 @@ func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool
DirectWayland: app.DirectWayland, DirectWayland: app.DirectWayland,
Username: "hakurei", Username: "hakurei",
Shell: shellPath, Shell: pathShell,
Data: pathSet.homeDir, Home: pathDataData.Append(app.ID),
Dir: path.Join("/data/data", app.ID),
Identity: app.Identity, Identity: app.Identity,
Groups: app.Groups, Groups: app.Groups,
@ -87,27 +87,26 @@ func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool
Hostname: formatHostname(app.Name), Hostname: formatHostname(app.Name),
Devel: app.Devel, Devel: app.Devel,
Userns: app.Userns, Userns: app.Userns,
Net: app.Net, HostNet: app.HostNet,
HostAbstract: app.HostAbstract,
Device: app.Device, Device: app.Device,
Tty: app.Tty || flagDropShell, Tty: app.Tty || flagDropShell,
MapRealUID: app.MapRealUID, MapRealUID: app.MapRealUID,
Filesystem: []*hst.FilesystemConfig{ Filesystem: []hst.FilesystemConfigJSON{
{Src: path.Join(pathSet.nixPath, "store"), Dst: "/nix/store", Must: true}, {FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
{Src: pathSet.metaPath, Dst: path.Join(hst.Tmp, "app"), Must: true}, {FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append("store"), Target: pathNixStore}},
{Src: "/etc/resolv.conf"}, {FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{Src: "/sys/block"}, {FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{Src: "/sys/bus"}, {FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
{Src: "/sys/class"}, {FilesystemConfig: &hst.FSBind{Source: pathSet.metaPath, Target: hst.AbsTmp.Append("app")}},
{Src: "/sys/dev"}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSEtc.Append("resolv.conf"), Optional: true}},
{Src: "/sys/devices"}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("block"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("bus"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("class"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("dev"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("devices"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID), Source: pathSet.homeDir, Write: true, Ensure: true}},
}, },
Link: [][2]string{
{app.CurrentSystem, "/run/current-system"},
{"/run/current-system/sw/bin", "/bin"},
{"/run/current-system/sw/bin", "/usr/bin"},
},
Etc: path.Join(pathSet.cacheDir, "etc"),
AutoEtc: true,
}, },
ExtraPerms: []*hst.ExtraPermConfig{ ExtraPerms: []*hst.ExtraPermConfig{
{Path: dataHome, Execute: true}, {Path: dataHome, Execute: true},
@ -140,6 +139,14 @@ func loadAppInfo(name string, beforeFail func()) *appInfo {
beforeFail() beforeFail()
log.Fatal("application identifier must not be empty") 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 return bundle
} }

View File

@ -171,7 +171,12 @@ let
broadcast = { }; broadcast = { };
}); });
enablements = (if allow_wayland then 1 else 0) + (if allow_x11 then 2 else 0) + (if allow_dbus then 4 else 0) + (if allow_pulse then 8 else 0); enablements = {
wayland = allow_wayland;
x11 = allow_x11;
dbus = allow_dbus;
pulse = allow_pulse;
};
mesa = if gpu then mesaWrappers else null; mesa = if gpu then mesaWrappers else null;
nix_gl = if gpu then nixGL else null; nix_gl = if gpu then nixGL else null;

View File

@ -11,20 +11,19 @@ import (
"syscall" "syscall"
"hakurei.app/command" "hakurei.app/command"
"hakurei.app/container"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal" "hakurei.app/internal"
"hakurei.app/internal/hlog" "hakurei.app/internal/hlog"
) )
const shellPath = "/run/current-system/sw/bin/bash"
var ( var (
errSuccess = errors.New("success") errSuccess = errors.New("success")
) )
func init() { func init() {
hlog.Prepare("planterette") hlog.Prepare("hpkg")
if err := os.Setenv("SHELL", shellPath); err != nil { if err := os.Setenv("SHELL", pathShell.String()); err != nil {
log.Fatalf("cannot set $SHELL: %v", err) log.Fatalf("cannot set $SHELL: %v", err)
} }
} }
@ -42,7 +41,7 @@ func main() {
flagVerbose bool flagVerbose bool
flagDropShell bool flagDropShell bool
) )
c := command.New(os.Stderr, log.Printf, "planterette", func([]string) error { internal.InstallOutput(flagVerbose); return nil }). c := command.New(os.Stderr, log.Printf, "hpkg", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console"). Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next hakurei action") Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next hakurei action")
@ -66,7 +65,7 @@ func main() {
} }
/* /*
Look up paths to programs started by planterette. Look up paths to programs started by hpkg.
This is done here to ease error handling as cleanup is not yet required. This is done here to ease error handling as cleanup is not yet required.
*/ */
@ -81,31 +80,32 @@ func main() {
Extract package and set up for cleanup. Extract package and set up for cleanup.
*/ */
var workDir string var workDir *container.Absolute
if p, err := os.MkdirTemp("", "planterette.*"); err != nil { if p, err := os.MkdirTemp("", "hpkg.*"); err != nil {
log.Printf("cannot create temporary directory: %v", err) log.Printf("cannot create temporary directory: %v", err)
return err return err
} else { } else if workDir, err = container.NewAbs(p); err != nil {
workDir = p log.Printf("invalid temporary directory: %v", err)
return err
} }
cleanup := func() { cleanup := func() {
// should be faster than a native implementation // should be faster than a native implementation
mustRun(chmod, "-R", "+w", workDir) mustRun(chmod, "-R", "+w", workDir.String())
mustRun(rm, "-rf", workDir) mustRun(rm, "-rf", workDir.String())
} }
beforeRunFail.Store(&cleanup) beforeRunFail.Store(&cleanup)
mustRun(tar, "-C", workDir, "-xf", pkgPath) mustRun(tar, "-C", workDir.String(), "-xf", pkgPath)
/* /*
Parse bundle and app metadata, do pre-install checks. Parse bundle and app metadata, do pre-install checks.
*/ */
bundle := loadAppInfo(path.Join(workDir, "bundle.json"), cleanup) bundle := loadAppInfo(path.Join(workDir.String(), "bundle.json"), cleanup)
pathSet := pathSetByApp(bundle.ID) pathSet := pathSetByApp(bundle.ID)
a := bundle a := bundle
if s, err := os.Stat(pathSet.metaPath); err != nil { if s, err := os.Stat(pathSet.metaPath.String()); err != nil {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
cleanup() cleanup()
log.Printf("cannot access %q: %v", pathSet.metaPath, err) log.Printf("cannot access %q: %v", pathSet.metaPath, err)
@ -117,7 +117,7 @@ func main() {
log.Printf("metadata path %q is not a file", pathSet.metaPath) log.Printf("metadata path %q is not a file", pathSet.metaPath)
return syscall.EBADMSG return syscall.EBADMSG
} else { } else {
a = loadAppInfo(pathSet.metaPath, cleanup) a = loadAppInfo(pathSet.metaPath.String(), cleanup)
if a.ID != bundle.ID { if a.ID != bundle.ID {
cleanup() cleanup()
log.Printf("app %q claims to have identifier %q", log.Printf("app %q claims to have identifier %q",
@ -208,7 +208,7 @@ func main() {
*/ */
// serialise metadata to ensure consistency // serialise metadata to ensure consistency
if f, err := os.OpenFile(pathSet.metaPath+"~", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil { if f, err := os.OpenFile(pathSet.metaPath.String()+"~", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil {
cleanup() cleanup()
log.Printf("cannot create metadata file: %v", err) log.Printf("cannot create metadata file: %v", err)
return err return err
@ -221,7 +221,7 @@ func main() {
// not fatal // not fatal
} }
if err := os.Rename(pathSet.metaPath+"~", pathSet.metaPath); err != nil { if err := os.Rename(pathSet.metaPath.String()+"~", pathSet.metaPath.String()); err != nil {
cleanup() cleanup()
log.Printf("cannot rename metadata file: %v", err) log.Printf("cannot rename metadata file: %v", err)
return err return err
@ -250,7 +250,7 @@ func main() {
id := args[0] id := args[0]
pathSet := pathSetByApp(id) pathSet := pathSetByApp(id)
a := loadAppInfo(pathSet.metaPath, func() {}) a := loadAppInfo(pathSet.metaPath.String(), func() {})
if a.ID != id { if a.ID != id {
log.Printf("app %q claims to have identifier %q", id, a.ID) log.Printf("app %q claims to have identifier %q", id, a.ID)
return syscall.EBADE return syscall.EBADE
@ -274,13 +274,13 @@ func main() {
"--override-input nixpkgs path:/etc/nixpkgs " + "--override-input nixpkgs path:/etc/nixpkgs " +
"path:" + a.NixGL + "#nixVulkanNvidia", "path:" + a.NixGL + "#nixVulkanNvidia",
}, true, func(config *hst.Config) *hst.Config { }, true, func(config *hst.Config) *hst.Config {
config.Container.Filesystem = append(config.Container.Filesystem, []*hst.FilesystemConfig{ config.Container.Filesystem = append(config.Container.Filesystem, []hst.FilesystemConfigJSON{
{Src: "/etc/resolv.conf"}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSEtc.Append("resolv.conf"), Optional: true}},
{Src: "/sys/block"}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("block"), Optional: true}},
{Src: "/sys/bus"}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("bus"), Optional: true}},
{Src: "/sys/class"}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("class"), Optional: true}},
{Src: "/sys/dev"}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("dev"), Optional: true}},
{Src: "/sys/devices"}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("devices"), Optional: true}},
}...) }...)
appendGPUFilesystem(config) appendGPUFilesystem(config)
return config return config
@ -291,15 +291,16 @@ func main() {
Create app configuration. Create app configuration.
*/ */
pathname := a.Launcher
argv := make([]string, 1, len(args)) argv := make([]string, 1, len(args))
if !flagDropShell { if flagDropShell {
argv[0] = a.Launcher pathname = pathShell
argv[0] = bash
} else { } else {
argv[0] = shellPath argv[0] = a.Launcher.String()
} }
argv = append(argv, args[1:]...) argv = append(argv, args[1:]...)
config := a.toHst(pathSet, pathname, argv, flagDropShell)
config := a.toFst(pathSet, argv, flagDropShell)
/* /*
Expose GPU devices. Expose GPU devices.
@ -307,7 +308,7 @@ func main() {
if a.GPU { if a.GPU {
config.Container.Filesystem = append(config.Container.Filesystem, config.Container.Filesystem = append(config.Container.Filesystem,
&hst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(hst.Tmp, "nixGL")}) hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append(".nixGL"), Target: hst.AbsTmp.Append("nixGL")}})
appendGPUFilesystem(config) appendGPUFilesystem(config)
} }

116
cmd/hpkg/paths.go Normal file
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}},
}...)
}

View File

@ -9,7 +9,7 @@ let
buildPackage = self.buildPackage.${system}; buildPackage = self.buildPackage.${system};
in in
nixosTest { nixosTest {
name = "planterette"; name = "hpkg";
nodes.machine = { nodes.machine = {
environment.etc = { environment.etc = {
"foot.pkg".source = callPackage ./foot.nix { inherit buildPackage; }; "foot.pkg".source = callPackage ./foot.nix { inherit buildPackage; };

View File

@ -79,20 +79,20 @@ print(machine.succeed("sudo -u alice -i hakurei version"))
machine.wait_for_file("/run/user/1000/wayland-1") machine.wait_for_file("/run/user/1000/wayland-1")
machine.wait_for_file("/tmp/sway-ipc.sock") machine.wait_for_file("/tmp/sway-ipc.sock")
# Prepare planterette directory: # Prepare hpkg directory:
machine.succeed("install -dm 0700 -o alice -g users /var/lib/hakurei/1000") machine.succeed("install -dm 0700 -o alice -g users /var/lib/hakurei/1000")
# Install planterette app: # Install hpkg app:
swaymsg("exec planterette -v install /etc/foot.pkg && touch /tmp/planterette-install-ok") swaymsg("exec hpkg -v install /etc/foot.pkg && touch /tmp/hpkg-install-ok")
machine.wait_for_file("/tmp/planterette-install-ok") machine.wait_for_file("/tmp/hpkg-install-ok")
# Start app (foot) with Wayland enablement: # Start app (foot) with Wayland enablement:
swaymsg("exec planterette -v start org.codeberg.dnkl.foot") swaymsg("exec hpkg -v start org.codeberg.dnkl.foot")
wait_for_window("hakurei@machine-foot") wait_for_window("hakurei@machine-foot")
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n") machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
machine.wait_for_file("/tmp/hakurei.1000/tmpdir/2/success-client") machine.wait_for_file("/tmp/hakurei.0/tmpdir/2/success-client")
collect_state_ui("app_wayland") collect_state_ui("app_wayland")
check_state("foot", 13) check_state("foot", {"wayland": True, "dbus": True, "pulse": True})
# Verify acl on XDG_RUNTIME_DIR: # Verify acl on XDG_RUNTIME_DIR:
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002")) print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002"))
machine.send_chars("exit\n") machine.send_chars("exit\n")

View File

@ -2,9 +2,9 @@ package main
import ( import (
"context" "context"
"path"
"strings" "strings"
"hakurei.app/container"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal" "hakurei.app/internal"
@ -18,8 +18,8 @@ func withNixDaemon(
mustRunAppDropShell(ctx, updateConfig(&hst.Config{ mustRunAppDropShell(ctx, updateConfig(&hst.Config{
ID: app.ID, ID: app.ID,
Path: shellPath, Path: pathShell,
Args: []string{shellPath, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " + Args: []string{bash, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
// start nix-daemon // start nix-daemon
"nix-daemon --store / & " + "nix-daemon --store / & " +
// wait for socket to appear // wait for socket to appear
@ -32,9 +32,8 @@ func withNixDaemon(
}, },
Username: "hakurei", Username: "hakurei",
Shell: shellPath, Shell: pathShell,
Data: pathSet.homeDir, Home: pathDataData.Append(app.ID),
Dir: path.Join("/data/data", app.ID),
ExtraPerms: []*hst.ExtraPermConfig{ ExtraPerms: []*hst.ExtraPermConfig{
{Path: dataHome, Execute: true}, {Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true}, {Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
@ -45,37 +44,34 @@ func withNixDaemon(
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action, Hostname: formatHostname(app.Name) + "-" + action,
Userns: true, // nix sandbox requires userns Userns: true, // nix sandbox requires userns
Net: net, HostNet: net,
SeccompFlags: seccomp.AllowMultiarch, SeccompFlags: seccomp.AllowMultiarch,
Tty: dropShell, Tty: dropShell,
Filesystem: []*hst.FilesystemConfig{ Filesystem: []hst.FilesystemConfigJSON{
{Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true}, {FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath, Target: pathNix, Write: true}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID), Source: pathSet.homeDir, Write: true, Ensure: true}},
}, },
Link: [][2]string{
{app.CurrentSystem, "/run/current-system"},
{"/run/current-system/sw/bin", "/bin"},
{"/run/current-system/sw/bin", "/usr/bin"},
},
Etc: path.Join(pathSet.cacheDir, "etc"),
AutoEtc: true,
}, },
}), dropShell, beforeFail) }), dropShell, beforeFail)
} }
func withCacheDir( func withCacheDir(
ctx context.Context, ctx context.Context,
action string, command []string, workDir string, action string, command []string, workDir *container.Absolute,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) { app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
mustRunAppDropShell(ctx, &hst.Config{ mustRunAppDropShell(ctx, &hst.Config{
ID: app.ID, ID: app.ID,
Path: shellPath, Path: pathShell,
Args: []string{shellPath, "-lc", strings.Join(command, " && ")}, Args: []string{bash, "-lc", strings.Join(command, " && ")},
Username: "nixos", Username: "nixos",
Shell: shellPath, Shell: pathShell,
Data: pathSet.cacheDir, // this also ensures cacheDir via shim Home: pathDataData.Append(app.ID, "cache"),
Dir: path.Join("/data/data", app.ID, "cache"),
ExtraPerms: []*hst.ExtraPermConfig{ ExtraPerms: []*hst.ExtraPermConfig{
{Path: dataHome, Execute: true}, {Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true}, {Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
@ -88,24 +84,22 @@ func withCacheDir(
Hostname: formatHostname(app.Name) + "-" + action, Hostname: formatHostname(app.Name) + "-" + action,
SeccompFlags: seccomp.AllowMultiarch, SeccompFlags: seccomp.AllowMultiarch,
Tty: dropShell, Tty: dropShell,
Filesystem: []*hst.FilesystemConfig{ Filesystem: []hst.FilesystemConfigJSON{
{Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true}, {FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: workDir.Append(container.FHSEtc), Special: true}},
{Src: workDir, Dst: path.Join(hst.Tmp, "bundle"), Must: true}, {FilesystemConfig: &hst.FSBind{Source: workDir.Append("nix"), Target: pathNix}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Source: workDir, Target: hst.AbsTmp.Append("bundle")}},
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID, "cache"), Source: pathSet.cacheDir, Write: true, Ensure: true}},
}, },
Link: [][2]string{
{app.CurrentSystem, "/run/current-system"},
{"/run/current-system/sw/bin", "/bin"},
{"/run/current-system/sw/bin", "/usr/bin"},
},
Etc: path.Join(workDir, "etc"),
AutoEtc: true,
}, },
}, dropShell, beforeFail) }, dropShell, beforeFail)
} }
func mustRunAppDropShell(ctx context.Context, config *hst.Config, dropShell bool, beforeFail func()) { func mustRunAppDropShell(ctx context.Context, config *hst.Config, dropShell bool, beforeFail func()) {
if dropShell { if dropShell {
config.Args = []string{shellPath, "-l"} config.Args = []string{bash, "-l"}
mustRunApp(ctx, config, beforeFail) mustRunApp(ctx, config, beforeFail)
beforeFail() beforeFail()
internal.Exit(0) internal.Exit(0)

View File

@ -1,101 +0,0 @@
package main
import (
"log"
"os"
"os/exec"
"path"
"strconv"
"sync/atomic"
"hakurei.app/hst"
"hakurei.app/internal/hlog"
)
var (
dataHome string
)
func init() {
// dataHome
if p, ok := os.LookupEnv("HAKUREI_DATA_HOME"); ok {
dataHome = p
} else {
dataHome = "/var/lib/hakurei/" + strconv.Itoa(os.Getuid())
}
}
func lookPath(file string) string {
if p, err := exec.LookPath(file); err != nil {
log.Fatalf("%s: command not found", file)
return ""
} else {
return p
}
}
var beforeRunFail = new(atomic.Pointer[func()])
func mustRun(name string, arg ...string) {
hlog.Verbosef("spawning process: %q %q", name, arg)
cmd := exec.Command(name, arg...)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
if f := beforeRunFail.Swap(nil); f != nil {
(*f)()
}
log.Fatalf("%s: %v", name, err)
}
}
type appPathSet struct {
// ${dataHome}/${id}
baseDir string
// ${baseDir}/app
metaPath string
// ${baseDir}/files
homeDir string
// ${baseDir}/cache
cacheDir string
// ${baseDir}/cache/nix
nixPath string
}
func pathSetByApp(id string) *appPathSet {
pathSet := new(appPathSet)
pathSet.baseDir = path.Join(dataHome, id)
pathSet.metaPath = path.Join(pathSet.baseDir, "app")
pathSet.homeDir = path.Join(pathSet.baseDir, "files")
pathSet.cacheDir = path.Join(pathSet.baseDir, "cache")
pathSet.nixPath = path.Join(pathSet.cacheDir, "nix")
return pathSet
}
func appendGPUFilesystem(config *hst.Config) {
config.Container.Filesystem = append(config.Container.Filesystem, []*hst.FilesystemConfig{
// flatpak commit 763a686d874dd668f0236f911de00b80766ffe79
{Src: "/dev/dri", Device: true},
// mali
{Src: "/dev/mali", Device: true},
{Src: "/dev/mali0", Device: true},
{Src: "/dev/umplock", Device: true},
// nvidia
{Src: "/dev/nvidiactl", Device: true},
{Src: "/dev/nvidia-modeset", Device: true},
// nvidia OpenCL/CUDA
{Src: "/dev/nvidia-uvm", Device: true},
{Src: "/dev/nvidia-uvm-tools", Device: true},
// flatpak commit d2dff2875bb3b7e2cd92d8204088d743fd07f3ff
{Src: "/dev/nvidia0", Device: true}, {Src: "/dev/nvidia1", Device: true},
{Src: "/dev/nvidia2", Device: true}, {Src: "/dev/nvidia3", Device: true},
{Src: "/dev/nvidia4", Device: true}, {Src: "/dev/nvidia5", Device: true},
{Src: "/dev/nvidia6", Device: true}, {Src: "/dev/nvidia7", Device: true},
{Src: "/dev/nvidia8", Device: true}, {Src: "/dev/nvidia9", Device: true},
{Src: "/dev/nvidia10", Device: true}, {Src: "/dev/nvidia11", Device: true},
{Src: "/dev/nvidia12", Device: true}, {Src: "/dev/nvidia13", Device: true},
{Src: "/dev/nvidia14", Device: true}, {Src: "/dev/nvidia15", Device: true},
{Src: "/dev/nvidia16", Device: true}, {Src: "/dev/nvidia17", Device: true},
{Src: "/dev/nvidia18", Device: true}, {Src: "/dev/nvidia19", Device: true},
}...)
}

107
container/absolute.go Normal file
View File

@ -0,0 +1,107 @@
package container
import (
"encoding/json"
"errors"
"fmt"
"path"
"slices"
"strings"
"syscall"
)
// AbsoluteError is returned by [NewAbs] and holds the invalid pathname.
type AbsoluteError struct {
Pathname string
}
func (e *AbsoluteError) Error() string { return fmt.Sprintf("path %q is not absolute", e.Pathname) }
func (e *AbsoluteError) Is(target error) bool {
var ce *AbsoluteError
if !errors.As(target, &ce) {
return errors.Is(target, syscall.EINVAL)
}
return *e == *ce
}
// Absolute holds a pathname checked to be absolute.
type Absolute struct {
pathname string
}
// isAbs wraps [path.IsAbs] in case additional checks are added in the future.
func isAbs(pathname string) bool { return path.IsAbs(pathname) }
func (a *Absolute) String() string {
if a.pathname == zeroString {
panic("attempted use of zero Absolute")
}
return a.pathname
}
func (a *Absolute) Is(v *Absolute) bool {
if a == nil && v == nil {
return true
}
return a != nil && v != nil &&
a.pathname != zeroString && v.pathname != zeroString &&
a.pathname == v.pathname
}
// NewAbs checks pathname and returns a new [Absolute] if pathname is absolute.
func NewAbs(pathname string) (*Absolute, error) {
if !isAbs(pathname) {
return nil, &AbsoluteError{pathname}
}
return &Absolute{pathname}, nil
}
// MustAbs calls [NewAbs] and panics on error.
func MustAbs(pathname string) *Absolute {
if a, err := NewAbs(pathname); err != nil {
panic(err.Error())
} else {
return a
}
}
// Append calls [path.Join] with [Absolute] as the first element.
func (a *Absolute) Append(elem ...string) *Absolute {
return &Absolute{path.Join(append([]string{a.String()}, elem...)...)}
}
// Dir calls [path.Dir] with [Absolute] as its argument.
func (a *Absolute) Dir() *Absolute { return &Absolute{path.Dir(a.String())} }
func (a *Absolute) GobEncode() ([]byte, error) { return []byte(a.String()), nil }
func (a *Absolute) GobDecode(data []byte) error {
pathname := string(data)
if !isAbs(pathname) {
return &AbsoluteError{pathname}
}
a.pathname = pathname
return nil
}
func (a *Absolute) MarshalJSON() ([]byte, error) { return json.Marshal(a.String()) }
func (a *Absolute) UnmarshalJSON(data []byte) error {
var pathname string
if err := json.Unmarshal(data, &pathname); err != nil {
return err
}
if !isAbs(pathname) {
return &AbsoluteError{pathname}
}
a.pathname = pathname
return nil
}
// SortAbs calls [slices.SortFunc] for a slice of [Absolute].
func SortAbs(x []*Absolute) {
slices.SortFunc(x, func(a, b *Absolute) int { return strings.Compare(a.String(), b.String()) })
}
// CompactAbs calls [slices.CompactFunc] for a slice of [Absolute].
func CompactAbs(s []*Absolute) []*Absolute {
return slices.CompactFunc(s, func(a *Absolute, b *Absolute) bool { return a.String() == b.String() })
}

348
container/absolute_test.go Normal file
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
container/autoetc.go Normal file
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
container/autoetc_test.go Normal file
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
container/autoroot.go Normal file
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
container/autoroot_test.go Normal file
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)
}
})
}
}

View File

@ -14,6 +14,7 @@ const (
CAP_SYS_ADMIN = 0x15 CAP_SYS_ADMIN = 0x15
CAP_SETPCAP = 0x8 CAP_SETPCAP = 0x8
CAP_DAC_OVERRIDE = 0x1
) )
type ( type (
@ -36,9 +37,52 @@ func capToIndex(cap uintptr) uintptr { return cap >> 5 }
func capToMask(cap uintptr) uint32 { return 1 << uint(cap&31) } func capToMask(cap uintptr) uint32 { return 1 << uint(cap&31) }
func capset(hdrp *capHeader, datap *[2]capData) error { func capset(hdrp *capHeader, datap *[2]capData) error {
if _, _, errno := syscall.Syscall(syscall.SYS_CAPSET, r, _, errno := syscall.Syscall(
syscall.SYS_CAPSET,
uintptr(unsafe.Pointer(hdrp)), uintptr(unsafe.Pointer(hdrp)),
uintptr(unsafe.Pointer(&datap[0])), 0); errno != 0 { 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 errno
} }
return nil return nil

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)
}
})
}
}

View File

@ -9,7 +9,7 @@ import (
"io" "io"
"os" "os"
"os/exec" "os/exec"
"path" "runtime"
"strconv" "strconv"
. "syscall" . "syscall"
"time" "time"
@ -18,10 +18,6 @@ import (
) )
const ( const (
// Nonexistent is a path that cannot exist.
// /proc is chosen because a system with covered /proc is unsupported by this package.
Nonexistent = "/proc/nonexistent"
// CancelSignal is the signal expected by container init on context cancel. // CancelSignal is the signal expected by container init on context cancel.
// A custom [Container.Cancel] function must eventually deliver this signal. // A custom [Container.Cancel] function must eventually deliver this signal.
CancelSignal = SIGTERM CancelSignal = SIGTERM
@ -31,8 +27,6 @@ type (
// Container represents a container environment being prepared or run. // Container represents a container environment being prepared or run.
// None of [Container] methods are safe for concurrent use. // None of [Container] methods are safe for concurrent use.
Container struct { Container struct {
// Name of initial process in the container.
name string
// Cgroup fd, nil to disable. // Cgroup fd, nil to disable.
Cgroup *int Cgroup *int
// ExtraFiles passed through to initial process in the container, // ExtraFiles passed through to initial process in the container,
@ -43,6 +37,8 @@ type (
setup *gob.Encoder setup *gob.Encoder
// cancels cmd // cancels cmd
cancel context.CancelFunc cancel context.CancelFunc
// closed after Wait returns
wait chan struct{}
Stdin io.Reader Stdin io.Reader
Stdout io.Writer Stdout io.Writer
@ -59,11 +55,11 @@ type (
// Params holds container configuration and is safe to serialise. // Params holds container configuration and is safe to serialise.
Params struct { Params struct {
// Working directory in the container. // Working directory in the container.
Dir string Dir *Absolute
// Initial process environment. // Initial process environment.
Env []string Env []string
// Absolute path of initial process in the container. Overrides name. // Pathname of initial process in the container.
Path string Path *Absolute
// Initial process argv. // Initial process argv.
Args []string Args []string
// Deliver SIGINT to the initial process on context cancellation. // Deliver SIGINT to the initial process on context cancellation.
@ -96,6 +92,8 @@ type (
RetainSession bool RetainSession bool
// Do not [syscall.CLONE_NEWNET]. // Do not [syscall.CLONE_NEWNET].
HostNet bool HostNet bool
// Do not [LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET].
HostAbstract bool
// Retain CAP_SYS_ADMIN. // Retain CAP_SYS_ADMIN.
Privileged bool Privileged bool
} }
@ -113,11 +111,6 @@ func (p *Container) Start() error {
ctx, cancel := context.WithCancel(p.ctx) ctx, cancel := context.WithCancel(p.ctx)
p.cancel = cancel p.cancel = cancel
var cloneFlags uintptr = CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP
if !p.HostNet {
cloneFlags |= CLONE_NEWNET
}
// map to overflow id to work around ownership checks // map to overflow id to work around ownership checks
if p.Uid < 1 { if p.Uid < 1 {
p.Uid = OverflowUid() p.Uid = OverflowUid()
@ -147,20 +140,30 @@ func (p *Container) Start() error {
} else { } else {
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(CancelSignal) } p.cmd.Cancel = func() error { return p.cmd.Process.Signal(CancelSignal) }
} }
p.cmd.Dir = "/" p.cmd.Dir = FHSRoot
p.cmd.SysProcAttr = &SysProcAttr{ p.cmd.SysProcAttr = &SysProcAttr{
Setsid: !p.RetainSession, Setsid: !p.RetainSession,
Pdeathsig: SIGKILL, Pdeathsig: SIGKILL,
Cloneflags: cloneFlags | CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS, Cloneflags: CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS |
CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP,
// remain privileged for setup AmbientCaps: []uintptr{
AmbientCaps: []uintptr{CAP_SYS_ADMIN, CAP_SETPCAP}, // general container setup
CAP_SYS_ADMIN,
// drop capabilities
CAP_SETPCAP,
// overlay access to upperdir and workdir
CAP_DAC_OVERRIDE,
},
UseCgroupFD: p.Cgroup != nil, UseCgroupFD: p.Cgroup != nil,
} }
if p.cmd.SysProcAttr.UseCgroupFD { if p.cmd.SysProcAttr.UseCgroupFD {
p.cmd.SysProcAttr.CgroupFD = *p.Cgroup 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 // place setup pipe before user supplied extra files, this is later restored by init
if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil { if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil {
@ -172,11 +175,74 @@ func (p *Container) Start() error {
} }
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...) 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") msg.Verbose("starting container init")
if err := p.cmd.Start(); err != nil { if err := p.cmd.Start(); err != nil {
return msg.WrapErr(err, err.Error()) return msg.WrapErr(err, err.Error())
} }
return nil return nil
}()
// keep this thread alive until Wait returns for cancel
<-p.wait
}()
return <-done
} }
// Serve serves [Container.Params] to the container init. // Serve serves [Container.Params] to the container init.
@ -189,33 +255,16 @@ func (p *Container) Serve() error {
setup := p.setup setup := p.setup
p.setup = nil p.setup = nil
if p.Path != "" && !path.IsAbs(p.Path) { if p.Path == nil {
p.cancel() p.cancel()
return msg.WrapErr(EINVAL, return msg.WrapErr(EINVAL, "invalid executable pathname")
fmt.Sprintf("invalid executable path %q", p.Path))
} }
if p.Path == "" {
if p.name == "" {
p.Path = os.Getenv("SHELL")
if !path.IsAbs(p.Path) {
p.cancel()
return msg.WrapErr(EBADE,
"no command specified and $SHELL is invalid")
}
p.name = path.Base(p.Path)
} else if path.IsAbs(p.name) {
p.Path = p.name
} else if v, err := exec.LookPath(p.name); err != nil {
p.cancel()
return msg.WrapErr(err, err.Error())
} else {
p.Path = v
}
}
if p.SeccompRules == nil {
// do not transmit nil // do not transmit nil
if p.Dir == nil {
p.Dir = AbsFHSRoot
}
if p.SeccompRules == nil {
p.SeccompRules = make([]seccomp.NativeRule, 0) p.SeccompRules = make([]seccomp.NativeRule, 0)
} }
@ -234,8 +283,19 @@ func (p *Container) Serve() error {
return err return err
} }
// Wait waits for the container init process to exit. // Wait waits for the container init process to exit and releases any resources associated with the [Container].
func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() } 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 { func (p *Container) String() string {
return fmt.Sprintf("argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x", return fmt.Sprintf("argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x",
@ -250,8 +310,15 @@ func (p *Container) ProcessState() *os.ProcessState {
return p.cmd.ProcessState return p.cmd.ProcessState
} }
func New(ctx context.Context, name string, args ...string) *Container { // New returns the address to a new instance of [Container] that requires further initialisation before use.
return &Container{name: name, ctx: ctx, func New(ctx context.Context) *Container {
Params: Params{Args: append([]string{name}, args...), Dir: "/", Ops: new(Ops)}, 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
} }

View File

@ -14,6 +14,7 @@ import (
"strings" "strings"
"syscall" "syscall"
"testing" "testing"
"time"
"hakurei.app/command" "hakurei.app/command"
"hakurei.app/container" "hakurei.app/container"
@ -22,23 +23,40 @@ import (
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal" "hakurei.app/internal"
"hakurei.app/internal/hlog" "hakurei.app/internal/hlog"
"hakurei.app/ldd"
) )
const ( const (
ignore = "\x00" ignore = "\x00"
ignoreV = -1 ignoreV = -1
pathWantMnt = "/etc/hakurei/want-mnt" 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 { var containerTestCases = []struct {
name string name string
filter bool filter bool
session bool session bool
net bool net bool
ops *container.Ops ro bool
ops func(t *testing.T) (*container.Ops, context.Context)
mnt func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry
mnt []*vfs.MountInfoEntry
uid int uid int
gid int gid int
@ -46,31 +64,34 @@ var containerTestCases = []struct {
flags seccomp.ExportFlag flags seccomp.ExportFlag
presets seccomp.FilterPreset presets seccomp.FilterPreset
}{ }{
{"minimal", true, false, false, {"minimal", true, false, false, true,
new(container.Ops), nil, emptyOps, emptyMnt,
1000, 100, nil, 0, seccomp.PresetStrict}, 1000, 100, nil, 0, seccomp.PresetStrict},
{"allow", true, true, true, {"allow", true, true, true, false,
new(container.Ops), nil, emptyOps, emptyMnt,
1000, 100, nil, 0, seccomp.PresetExt | seccomp.PresetDenyDevel}, 1000, 100, nil, 0, seccomp.PresetExt | seccomp.PresetDenyDevel},
{"no filter", false, true, true, {"no filter", false, true, true, true,
new(container.Ops), nil, emptyOps, emptyMnt,
1000, 100, nil, 0, seccomp.PresetExt}, 1000, 100, nil, 0, seccomp.PresetExt},
{"custom rules", true, true, true, {"custom rules", true, true, true, false,
new(container.Ops), nil, emptyOps, emptyMnt,
1, 31, []seccomp.NativeRule{{seccomp.ScmpSyscall(syscall.SYS_SETUID), seccomp.ScmpErrno(syscall.EPERM), nil}}, 0, seccomp.PresetExt}, 1, 31, []seccomp.NativeRule{{seccomp.ScmpSyscall(syscall.SYS_SETUID), seccomp.ScmpErrno(syscall.EPERM), nil}}, 0, seccomp.PresetExt},
{"tmpfs", true, false, false,
new(container.Ops). {"tmpfs", true, false, false, true,
Tmpfs(hst.Tmp, 0, 0755), earlyOps(new(container.Ops).
[]*vfs.MountInfoEntry{ Tmpfs(hst.AbsTmp, 0, 0755),
ent("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore), ),
}, earlyMnt(
ent("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", ignore),
),
9, 9, nil, 0, seccomp.PresetStrict}, 9, 9, nil, 0, seccomp.PresetStrict},
{"dev", true, true /* go test output is not a tty */, false,
new(container.Ops). {"dev", true, true /* go test output is not a tty */, false, false,
Dev("/dev"). earlyOps(new(container.Ops).
Mqueue("/dev/mqueue"), Dev(container.MustAbs("/dev"), true),
[]*vfs.MountInfoEntry{ ),
ent("/", "/dev", "rw,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore), earlyMnt(
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
ent("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), ent("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), ent("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), ent("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
@ -79,24 +100,125 @@ var containerTestCases = []struct {
ent("/tty", "/dev/tty", "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/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
ent("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"), ent("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
}, ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
),
1971, 100, nil, 0, seccomp.PresetStrict}, 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) { func TestContainer(t *testing.T) {
{ replaceOutput(t)
oldVerbose := hlog.Load()
oldOutput := container.GetOutput()
internal.InstallOutput(true)
t.Cleanup(func() { hlog.Store(oldVerbose) })
t.Cleanup(func() { container.SetOutput(oldOutput) })
}
t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) { t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) {
wantErr := context.Canceled wantErr := context.Canceled
wantExitCode := 0 wantExitCode := 0
if err := c.Wait(); !errors.Is(err, wantErr) { if err := c.Wait(); !errors.Is(err, wantErr) {
hlog.PrintBaseError(err, "wait:") container.GetOutput().PrintBaseErr(err, "wait:")
t.Errorf("Wait: error = %v, want %v", err, wantErr) t.Errorf("Wait: error = %v, want %v", err, wantErr)
} }
if ps := c.ProcessState(); ps == nil { if ps := c.ProcessState(); ps == nil {
@ -111,7 +233,7 @@ func TestContainer(t *testing.T) {
}, func(t *testing.T, c *container.Container) { }, func(t *testing.T, c *container.Container) {
var exitError *exec.ExitError var exitError *exec.ExitError
if err := c.Wait(); !errors.As(err, &exitError) { if err := c.Wait(); !errors.As(err, &exitError) {
hlog.PrintBaseError(err, "wait:") container.GetOutput().PrintBaseErr(err, "wait:")
t.Errorf("Wait: error = %v", err) t.Errorf("Wait: error = %v", err)
} }
if code := exitError.ExitCode(); code != blockExitCodeInterrupt { if code := exitError.ExitCode(); code != blockExitCodeInterrupt {
@ -121,17 +243,25 @@ func TestContainer(t *testing.T) {
for i, tc := range containerTestCases { for i, tc := range containerTestCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
wantOps, wantOpsCtx := tc.ops(t)
wantMnt := tc.mnt(t, wantOpsCtx)
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout) ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
defer cancel() defer cancel()
var libPaths []string var libPaths []*container.Absolute
c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i)) c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i))
c.Uid = tc.uid c.Uid = tc.uid
c.Gid = tc.gid c.Gid = tc.gid
c.Hostname = hostnameFromTestCase(tc.name) 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.Stdout, c.Stderr = os.Stdout, os.Stderr
}
c.WaitDelay = helperDefaultTimeout c.WaitDelay = helperDefaultTimeout
*c.Ops = append(*c.Ops, *tc.ops...) *c.Ops = append(*c.Ops, *wantOps...)
c.SeccompRules = tc.rules c.SeccompRules = tc.rules
c.SeccompFlags = tc.flags | seccomp.AllowMultiarch c.SeccompFlags = tc.flags | seccomp.AllowMultiarch
c.SeccompPresets = tc.presets c.SeccompPresets = tc.presets
@ -140,10 +270,11 @@ func TestContainer(t *testing.T) {
c.HostNet = tc.net c.HostNet = tc.net
c. c.
Tmpfs("/tmp", 0, 0755). Readonly(container.MustAbs(pathReadonly), 0755).
Place("/etc/hostname", []byte(c.Hostname)) Tmpfs(container.MustAbs("/tmp"), 0, 0755).
Place(container.MustAbs("/etc/hostname"), []byte(c.Hostname))
// needs /proc to check mountinfo // needs /proc to check mountinfo
c.Proc("/proc") c.Proc(container.MustAbs("/proc"))
// mountinfo cannot be resolved directly by helper due to libPaths nondeterminism // mountinfo cannot be resolved directly by helper due to libPaths nondeterminism
mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths)) mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
@ -152,14 +283,16 @@ func TestContainer(t *testing.T) {
// Bind(os.Args[0], helperInnerPath, 0) // Bind(os.Args[0], helperInnerPath, 0)
ent(ignore, helperInnerPath, "ro,nosuid,nodev,relatime", ignore, ignore, ignore), ent(ignore, helperInnerPath, "ro,nosuid,nodev,relatime", ignore, ignore, ignore),
) )
for _, name := range libPaths { for _, a := range libPaths {
// Bind(name, name, 0) // Bind(name, name, 0)
mnt = append(mnt, ent(ignore, name, "ro,nosuid,nodev,relatime", ignore, ignore, ignore)) mnt = append(mnt, ent(ignore, a.String(), "ro,nosuid,nodev,relatime", ignore, ignore, ignore))
} }
mnt = append(mnt, tc.mnt...) mnt = append(mnt, wantMnt...)
mnt = append(mnt, mnt = append(mnt,
// Readonly(pathReadonly, 0755)
ent("/", pathReadonly, "ro,nosuid,nodev", "tmpfs", "readonly", ignore),
// Tmpfs("/tmp", 0, 0755) // Tmpfs("/tmp", 0, 0755)
ent("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore), ent("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", ignore),
// Place("/etc/hostname", []byte(hostname)) // Place("/etc/hostname", []byte(hostname))
ent(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore), ent(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
// Proc("/proc") // Proc("/proc")
@ -169,19 +302,27 @@ func TestContainer(t *testing.T) {
) )
want := new(bytes.Buffer) want := new(bytes.Buffer)
if err := gob.NewEncoder(want).Encode(mnt); err != nil { if err := gob.NewEncoder(want).Encode(mnt); err != nil {
_, _ = output.WriteTo(os.Stdout)
t.Fatalf("cannot serialise expected mount points: %v", err) t.Fatalf("cannot serialise expected mount points: %v", err)
} }
c.Place(pathWantMnt, want.Bytes()) c.Place(container.MustAbs(pathWantMnt), want.Bytes())
if tc.ro {
c.Remount(container.MustAbs("/"), syscall.MS_RDONLY)
}
if err := c.Start(); err != nil { if err := c.Start(); err != nil {
hlog.PrintBaseError(err, "start:") _, _ = output.WriteTo(os.Stdout)
container.GetOutput().PrintBaseErr(err, "start:")
t.Fatalf("cannot start container: %v", err) t.Fatalf("cannot start container: %v", err)
} else if err = c.Serve(); err != nil { } else if err = c.Serve(); err != nil {
hlog.PrintBaseError(err, "serve:") _, _ = output.WriteTo(os.Stdout)
container.GetOutput().PrintBaseErr(err, "serve:")
t.Errorf("cannot serve setup params: %v", err) t.Errorf("cannot serve setup params: %v", err)
} }
if err := c.Wait(); err != nil { if err := c.Wait(); err != nil {
hlog.PrintBaseError(err, "wait:") _, _ = output.WriteTo(os.Stdout)
container.GetOutput().PrintBaseErr(err, "wait:")
t.Fatalf("wait: %v", err) t.Fatalf("wait: %v", err)
} }
}) })
@ -235,10 +376,10 @@ func testContainerCancel(
} }
if err := c.Start(); err != nil { if err := c.Start(); err != nil {
hlog.PrintBaseError(err, "start:") container.GetOutput().PrintBaseErr(err, "start:")
t.Fatalf("cannot start container: %v", err) t.Fatalf("cannot start container: %v", err)
} else if err = c.Serve(); err != nil { } else if err = c.Serve(); err != nil {
hlog.PrintBaseError(err, "serve:") container.GetOutput().PrintBaseErr(err, "serve:")
t.Errorf("cannot serve setup params: %v", err) t.Errorf("cannot serve setup params: %v", err)
} }
<-ready <-ready
@ -248,7 +389,7 @@ func testContainerCancel(
} }
func TestContainerString(t *testing.T) { func TestContainerString(t *testing.T) {
c := container.New(t.Context(), "ldd", "/usr/bin/env") c := container.NewCommand(t.Context(), container.MustAbs("/run/current-system/sw/bin/ldd"), "ldd", "/usr/bin/env")
c.SeccompFlags |= seccomp.AllowMultiarch c.SeccompFlags |= seccomp.AllowMultiarch
c.SeccompRules = seccomp.Preset( c.SeccompRules = seccomp.Preset(
seccomp.PresetExt|seccomp.PresetDenyNS|seccomp.PresetDenyTTY, seccomp.PresetExt|seccomp.PresetDenyNS|seccomp.PresetDenyTTY,
@ -309,6 +450,10 @@ func init() {
return fmt.Errorf("/etc/hostname: %q, want %q", 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 fail bool
@ -321,6 +466,11 @@ func init() {
return fmt.Errorf("cannot close expected mount points: %v", err) 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 var d *vfs.MountInfoDecoder
if f, err := os.Open("/proc/self/mountinfo"); err != nil { if f, err := os.Open("/proc/self/mountinfo"); err != nil {
return fmt.Errorf("cannot open mountinfo: %v", err) return fmt.Errorf("cannot open mountinfo: %v", err)
@ -366,3 +516,61 @@ func init() {
}) })
}) })
} }
const (
envDoCheck = "HAKUREI_TEST_DO_CHECK"
helperDefaultTimeout = 5 * time.Second
helperInnerPath = "/usr/bin/helper"
)
var (
absHelperInnerPath = container.MustAbs(helperInnerPath)
)
var helperCommands []func(c command.Command)
func TestMain(m *testing.M) {
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
if os.Getenv(envDoCheck) == "1" {
c := command.New(os.Stderr, log.Printf, "helper", func(args []string) error {
log.SetFlags(0)
log.SetPrefix("helper: ")
return nil
})
for _, f := range helperCommands {
f(c)
}
c.MustParse(os.Args[1:], func(err error) {
if err != nil {
log.Fatal(err.Error())
}
})
return
}
os.Exit(m.Run())
}
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*container.Absolute, args ...string) (c *container.Container) {
c = container.NewCommand(ctx, absHelperInnerPath, "helper", args...)
c.Env = append(c.Env, envDoCheck+"=1")
c.Bind(container.MustAbs(os.Args[0]), absHelperInnerPath, 0)
// in case test has cgo enabled
if entries, err := ldd.Exec(ctx, os.Args[0]); err != nil {
log.Fatalf("ldd: %v", err)
} else {
*libPaths = ldd.Path(entries)
}
for _, name := range *libPaths {
c.Bind(name, name, 0)
}
return
}
func helperNewContainer(ctx context.Context, args ...string) (c *container.Container) {
return helperNewContainerLibPaths(ctx, new([]*container.Absolute), args...)
}

245
container/dispatcher.go Normal file
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) }

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()
}
}

View File

@ -3,12 +3,10 @@ package container
import ( import (
"errors" "errors"
"fmt" "fmt"
"log"
"os" "os"
"os/exec" "os/exec"
"os/signal"
"path" "path"
"runtime" "slices"
"strconv" "strconv"
. "syscall" . "syscall"
"time" "time"
@ -31,12 +29,46 @@ const (
it should be noted that none of this should become relevant at any point since the resulting it should be noted that none of this should become relevant at any point since the resulting
intermediate root tmpfs should be effectively anonymous */ intermediate root tmpfs should be effectively anonymous */
intermediateHostPath = "/proc/self/fd" intermediateHostPath = FHSProc + "self/fd"
// setup params file descriptor // setup params file descriptor
setupEnv = "HAKUREI_SETUP" 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 { type initParams struct {
Params Params
@ -47,180 +79,195 @@ type initParams struct {
Verbose bool Verbose bool
} }
func Init(prepare func(prefix string), setVerbose func(verbose bool)) { func Init(prepareLogger func(prefix string), setVerbose func(verbose bool)) {
runtime.LockOSThread() initEntrypoint(direct{}, prepareLogger, setVerbose)
prepare("init") }
if os.Getpid() != 1 { func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setVerbose func(verbose bool)) {
log.Fatal("this process must run as pid 1") 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 ( var (
params initParams params initParams
closeSetup func() error closeSetup func() error
setupFile *os.File setupFd uintptr
offsetSetup int offsetSetup int
) )
if f, err := Receive(setupEnv, &params, &setupFile); err != nil { if f, err := k.receive(setupEnv, &params, &setupFd); err != nil {
if errors.Is(err, ErrInvalid) { if errors.Is(err, EBADF) {
log.Fatal("invalid setup descriptor") k.fatal("invalid setup descriptor")
} }
if errors.Is(err, ErrNotSet) { if errors.Is(err, ErrNotSet) {
log.Fatal("HAKUREI_SETUP not set") k.fatal("HAKUREI_SETUP not set")
} }
log.Fatalf("cannot decode init setup payload: %v", err) k.fatalf("cannot decode init setup payload: %v", err)
} else { } else {
if params.Ops == nil { if params.Ops == nil {
log.Fatal("invalid setup parameters") k.fatal("invalid setup parameters")
} }
if params.ParentPerm == 0 { if params.ParentPerm == 0 {
params.ParentPerm = 0755 params.ParentPerm = 0755
} }
setVerbose(params.Verbose) setVerbose(params.Verbose)
msg.Verbose("received setup parameters") k.verbose("received setup parameters")
closeSetup = f closeSetup = f
offsetSetup = int(setupFile.Fd() + 1) offsetSetup = int(setupFd + 1)
} }
// write uid/gid map here so parent does not need to set dumpable // write uid/gid map here so parent does not need to set dumpable
if err := SetDumpable(SUID_DUMP_USER); err != nil { if err := k.setDumpable(SUID_DUMP_USER); err != nil {
log.Fatalf("cannot set SUID_DUMP_USER: %s", err) k.fatalf("cannot set SUID_DUMP_USER: %v", err)
} }
if err := os.WriteFile("/proc/self/uid_map", if err := k.writeFile(FHSProc+"self/uid_map",
append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...), append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...),
0); err != nil { 0); err != nil {
log.Fatalf("%v", err) k.fatalf("%v", err)
} }
if err := os.WriteFile("/proc/self/setgroups", if err := k.writeFile(FHSProc+"self/setgroups",
[]byte("deny\n"), []byte("deny\n"),
0); err != nil && !os.IsNotExist(err) { 0); err != nil && !os.IsNotExist(err) {
log.Fatalf("%v", err) k.fatalf("%v", err)
} }
if err := os.WriteFile("/proc/self/gid_map", if err := k.writeFile(FHSProc+"self/gid_map",
append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...), append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...),
0); err != nil { 0); err != nil {
log.Fatalf("%v", err) k.fatalf("%v", err)
} }
if err := SetDumpable(SUID_DUMP_DISABLE); err != nil { if err := k.setDumpable(SUID_DUMP_DISABLE); err != nil {
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err) k.fatalf("cannot set SUID_DUMP_DISABLE: %v", err)
} }
oldmask := Umask(0) oldmask := k.umask(0)
if params.Hostname != "" { if params.Hostname != "" {
if err := Sethostname([]byte(params.Hostname)); err != nil { if err := k.sethostname([]byte(params.Hostname)); err != nil {
log.Fatalf("cannot set hostname: %v", err) k.fatalf("cannot set hostname: %v", err)
} }
} }
// cache sysctl before pivot_root // cache sysctl before pivot_root
LastCap() lastcap := k.lastcap()
if err := Mount("", "/", "", MS_SILENT|MS_SLAVE|MS_REC, ""); err != nil { if err := k.mount(zeroString, FHSRoot, zeroString, MS_SILENT|MS_SLAVE|MS_REC, zeroString); err != nil {
log.Fatalf("cannot make / rslave: %v", err) 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 { for i, op := range *params.Ops {
if op == nil { if op == nil || !op.Valid() {
log.Fatalf("invalid op %d", i) k.fatalf("invalid op at index %d", i)
} }
if err := op.early(&params.Params); err != nil { if err := op.early(state, k); err != nil {
msg.PrintBaseErr(err, k.printBaseErr(err,
fmt.Sprintf("cannot prepare op %d:", i)) fmt.Sprintf("cannot prepare op at index %d:", i))
msg.BeforeExit() k.beforeExit()
os.Exit(1) k.exit(1)
} }
} }
if err := Mount("rootfs", intermediateHostPath, "tmpfs", MS_NODEV|MS_NOSUID, ""); err != nil { if err := k.mount(SourceTmpfsRootfs, intermediateHostPath, FstypeTmpfs, MS_NODEV|MS_NOSUID, zeroString); err != nil {
log.Fatalf("cannot mount intermediate root: %v", err) k.fatalf("cannot mount intermediate root: %v", err)
} }
if err := os.Chdir(intermediateHostPath); err != nil { if err := k.chdir(intermediateHostPath); err != nil {
log.Fatalf("cannot enter base path: %v", err) k.fatalf("cannot enter intermediate host path: %v", err)
} }
if err := os.Mkdir(sysrootDir, 0755); err != nil { if err := k.mkdir(sysrootDir, 0755); err != nil {
log.Fatalf("%v", err) k.fatalf("%v", err)
} }
if err := Mount(sysrootDir, sysrootDir, "", MS_SILENT|MS_MGC_VAL|MS_BIND|MS_REC, ""); err != nil { if err := k.mount(sysrootDir, sysrootDir, zeroString, MS_SILENT|MS_BIND|MS_REC, zeroString); err != nil {
log.Fatalf("cannot bind sysroot: %v", err) k.fatalf("cannot bind sysroot: %v", err)
} }
if err := os.Mkdir(hostDir, 0755); err != nil { if err := k.mkdir(hostDir, 0755); err != nil {
log.Fatalf("%v", err) k.fatalf("%v", err)
} }
// pivot_root uncovers intermediateHostPath in hostDir // pivot_root uncovers intermediateHostPath in hostDir
if err := PivotRoot(intermediateHostPath, hostDir); err != nil { if err := k.pivotRoot(intermediateHostPath, hostDir); err != nil {
log.Fatalf("cannot pivot into intermediate root: %v", err) k.fatalf("cannot pivot into intermediate root: %v", err)
} }
if err := os.Chdir("/"); err != nil { if err := k.chdir(FHSRoot); err != nil {
log.Fatalf("%v", err) 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 { for i, op := range *params.Ops {
// ops already checked during early setup // ops already checked during early setup
msg.Verbosef("%s %s", op.prefix(), op) k.verbosef("%s %s", op.prefix(), op)
if err := op.apply(&params.Params); err != nil { if err := op.apply(state, k); err != nil {
msg.PrintBaseErr(err, k.printBaseErr(err,
fmt.Sprintf("cannot apply op %d:", i)) fmt.Sprintf("cannot apply op at index %d:", i))
msg.BeforeExit() k.beforeExit()
os.Exit(1) k.exit(1)
} }
} }
// setup requiring host root complete at this point // setup requiring host root complete at this point
if err := Mount(hostDir, hostDir, "", MS_SILENT|MS_REC|MS_PRIVATE, ""); err != nil { if err := k.mount(hostDir, hostDir, zeroString, MS_SILENT|MS_REC|MS_PRIVATE, zeroString); err != nil {
log.Fatalf("cannot make host root rprivate: %v", err) k.fatalf("cannot make host root rprivate: %v", err)
} }
if err := Unmount(hostDir, MNT_DETACH); err != nil { if err := k.unmount(hostDir, MNT_DETACH); err != nil {
log.Fatalf("cannot unmount host root: %v", err) k.fatalf("cannot unmount host root: %v", err)
} }
{ {
var fd int var fd int
if err := IgnoringEINTR(func() (err error) { if err := IgnoringEINTR(func() (err error) {
fd, err = Open("/", O_DIRECTORY|O_RDONLY, 0) fd, err = k.open(FHSRoot, O_DIRECTORY|O_RDONLY, 0)
return return
}); err != nil { }); err != nil {
log.Fatalf("cannot open intermediate root: %v", err) k.fatalf("cannot open intermediate root: %v", err)
} }
if err := os.Chdir(sysrootPath); err != nil { if err := k.chdir(sysrootPath); err != nil {
log.Fatalf("%v", err) k.fatalf("cannot enter sysroot: %v", err)
} }
if err := PivotRoot(".", "."); err != nil { if err := k.pivotRoot(".", "."); err != nil {
log.Fatalf("cannot pivot into sysroot: %v", err) k.fatalf("cannot pivot into sysroot: %v", err)
} }
if err := Fchdir(fd); err != nil { if err := k.fchdir(fd); err != nil {
log.Fatalf("cannot re-enter intermediate root: %v", err) k.fatalf("cannot re-enter intermediate root: %v", err)
} }
if err := Unmount(".", MNT_DETACH); err != nil { if err := k.unmount(".", MNT_DETACH); err != nil {
log.Fatalf("cannot unmount intemediate root: %v", err) k.fatalf("cannot unmount intermediate root: %v", err)
} }
if err := os.Chdir("/"); err != nil { if err := k.chdir(FHSRoot); err != nil {
log.Fatalf("%v", err) k.fatalf("cannot enter root: %v", err)
} }
if err := Close(fd); err != nil { if err := k.close(fd); err != nil {
log.Fatalf("cannot close intermediate root: %v", err) k.fatalf("cannot close intermediate root: %v", err)
} }
} }
if _, _, errno := Syscall(SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 { if err := k.capAmbientClearAll(); err != nil {
log.Fatalf("prctl(PR_SET_NO_NEW_PRIVS): %v", errno) k.fatalf("cannot clear the ambient capability set: %v", err)
} }
for i := uintptr(0); i <= lastcap; i++ {
if _, _, errno := Syscall(SYS_PRCTL, PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0); errno != 0 {
log.Fatalf("cannot clear the ambient capability set: %v", errno)
}
for i := uintptr(0); i <= LastCap(); i++ {
if params.Privileged && i == CAP_SYS_ADMIN { if params.Privileged && i == CAP_SYS_ADMIN {
continue continue
} }
if _, _, errno := Syscall(SYS_PRCTL, PR_CAPBSET_DROP, i, 0); errno != 0 { if err := k.capBoundingSetDrop(i); err != nil {
log.Fatalf("cannot drop capability from bonding set: %v", errno) k.fatalf("cannot drop capability from bounding set: %v", err)
} }
} }
@ -228,53 +275,54 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
if params.Privileged { if params.Privileged {
keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN) keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN)
if _, _, errno := Syscall(SYS_PRCTL, PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, CAP_SYS_ADMIN); errno != 0 { if err := k.capAmbientRaise(CAP_SYS_ADMIN); err != nil {
log.Fatalf("cannot raise CAP_SYS_ADMIN: %v", errno) k.fatalf("cannot raise CAP_SYS_ADMIN: %v", err)
} }
} }
if err := capset( if err := k.capset(
&capHeader{_LINUX_CAPABILITY_VERSION_3, 0}, &capHeader{_LINUX_CAPABILITY_VERSION_3, 0},
&[2]capData{{0, keep[0], keep[0]}, {0, keep[1], keep[1]}}, &[2]capData{{0, keep[0], keep[0]}, {0, keep[1], keep[1]}},
); err != nil { ); err != nil {
log.Fatalf("cannot capset: %v", err) k.fatalf("cannot capset: %v", err)
} }
if !params.SeccompDisable { if !params.SeccompDisable {
rules := params.SeccompRules rules := params.SeccompRules
if len(rules) == 0 { // non-empty rules slice always overrides presets if len(rules) == 0 { // non-empty rules slice always overrides presets
msg.Verbosef("resolving presets %#x", params.SeccompPresets) k.verbosef("resolving presets %#x", params.SeccompPresets)
rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags) rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags)
} }
if err := seccomp.Load(rules, params.SeccompFlags); err != nil { if err := k.seccompLoad(rules, params.SeccompFlags); err != nil {
log.Fatalf("cannot load syscall filter: %v", err) // this also indirectly asserts PR_SET_NO_NEW_PRIVS
k.fatalf("cannot load syscall filter: %v", err)
} }
msg.Verbosef("%d filter rules loaded", len(rules)) k.verbosef("%d filter rules loaded", len(rules))
} else { } else {
msg.Verbose("syscall filter not configured") k.verbose("syscall filter not configured")
} }
extraFiles := make([]*os.File, params.Count) extraFiles := make([]*os.File, params.Count)
for i := range extraFiles { for i := range extraFiles {
// setup fd is placed before all extra files // setup fd is placed before all extra files
extraFiles[i] = os.NewFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i)) extraFiles[i] = k.newFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i))
} }
Umask(oldmask) k.umask(oldmask)
cmd := exec.Command(params.Path) cmd := exec.Command(params.Path.String())
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Args = params.Args cmd.Args = params.Args
cmd.Env = params.Env cmd.Env = params.Env
cmd.ExtraFiles = extraFiles cmd.ExtraFiles = extraFiles
cmd.Dir = params.Dir cmd.Dir = params.Dir.String()
msg.Verbosef("starting initial program %s", params.Path) k.verbosef("starting initial program %s", params.Path)
if err := cmd.Start(); err != nil { if err := k.start(cmd); err != nil {
log.Fatalf("%v", err) k.fatalf("%v", err)
} }
msg.Suspend() k.suspend()
if err := closeSetup(); err != nil { if err := closeSetup(); err != nil {
log.Printf("cannot close setup pipe: %v", err) k.printf("cannot close setup pipe: %v", err)
// not fatal // not fatal
} }
@ -285,7 +333,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
info := make(chan winfo, 1) info := make(chan winfo, 1)
done := make(chan struct{}) done := make(chan struct{})
go func() { k.new(func(k syscallDispatcher) {
var ( var (
err error err error
wpid = -2 wpid = -2
@ -304,19 +352,19 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
err = EINTR err = EINTR
for errors.Is(err, EINTR) { for errors.Is(err, EINTR) {
wpid, err = Wait4(-1, &wstatus, 0, nil) wpid, err = k.wait4(-1, &wstatus, 0, nil)
} }
} }
if !errors.Is(err, ECHILD) { if !errors.Is(err, ECHILD) {
log.Printf("unexpected wait4 response: %v", err) k.printf("unexpected wait4 response: %v", err)
} }
close(done) close(done)
}() })
// handle signals to dump withheld messages // handle signals to dump withheld messages
sig := make(chan os.Signal, 2) sig := make(chan os.Signal, 2)
signal.Notify(sig, os.Interrupt, CancelSignal) k.notify(sig, os.Interrupt, CancelSignal)
// closed after residualProcessTimeout has elapsed after initial process death // closed after residualProcessTimeout has elapsed after initial process death
timeout := make(chan struct{}) timeout := make(chan struct{})
@ -325,45 +373,51 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
for { for {
select { select {
case s := <-sig: case s := <-sig:
if msg.Resume() { if k.resume() {
msg.Verbosef("%s after process start", s.String()) k.verbosef("%s after process start", s.String())
} else { } else {
msg.Verbosef("got %s", s.String()) k.verbosef("got %s", s.String())
} }
if s == CancelSignal && params.ForwardCancel && cmd.Process != nil { if s == CancelSignal && params.ForwardCancel && cmd.Process != nil {
msg.Verbose("forwarding context cancellation") k.verbose("forwarding context cancellation")
if err := cmd.Process.Signal(os.Interrupt); err != nil { if err := k.signal(cmd, os.Interrupt); err != nil {
log.Printf("cannot forward cancellation: %v", err) k.printf("cannot forward cancellation: %v", err)
} }
continue continue
} }
os.Exit(0) k.beforeExit()
k.exit(0)
case w := <-info: case w := <-info:
if w.wpid == cmd.Process.Pid { if w.wpid == cmd.Process.Pid {
// initial process exited, output is most likely available again // initial process exited, output is most likely available again
msg.Resume() k.resume()
switch { switch {
case w.wstatus.Exited(): case w.wstatus.Exited():
r = w.wstatus.ExitStatus() r = w.wstatus.ExitStatus()
msg.Verbosef("initial process exited with code %d", w.wstatus.ExitStatus()) k.verbosef("initial process exited with code %d", w.wstatus.ExitStatus())
case w.wstatus.Signaled(): case w.wstatus.Signaled():
r = 128 + int(w.wstatus.Signal()) r = 128 + int(w.wstatus.Signal())
msg.Verbosef("initial process exited with signal %s", w.wstatus.Signal()) k.verbosef("initial process exited with signal %s", w.wstatus.Signal())
default: default:
r = 255 r = 255
msg.Verbosef("initial process exited with status %#x", w.wstatus) k.verbosef("initial process exited with status %#x", w.wstatus)
} }
go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }() go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
} }
case <-done: case <-done:
msg.BeforeExit() k.beforeExit()
os.Exit(r) k.exit(r)
case <-timeout: case <-timeout:
log.Println("timeout exceeded waiting for lingering processes") k.printf("timeout exceeded waiting for lingering processes")
msg.BeforeExit() k.beforeExit()
os.Exit(r) k.exit(r)
} }
} }
} }

File diff suppressed because it is too large Load Diff

113
container/initbind.go Normal file
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
container/initbind_test.go Normal file
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
container/initdev.go Normal file
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
container/initdev_test.go Normal file
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
container/initmkdir.go Normal file
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) }

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
container/initoverlay.go Normal file
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))
}

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
container/initplace.go Normal file
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
container/initplace_test.go Normal file
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
container/initproc.go Normal file
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) }

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
container/initremount.go Normal file
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) }

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
container/initsymlink.go Normal file
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)
}

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
container/inittmpfs.go Normal file
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
container/inittmpfs_test.go Normal file
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
container/landlock.go Normal file
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
}

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)
}
}

View File

@ -4,31 +4,126 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "strings"
. "syscall" . "syscall"
"hakurei.app/container/vfs" "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 { 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 { if eq {
msg.Verbosef("resolved %q flags %#x", target, flags) p.k.verbosef("resolved %q flags %#x", target, flags)
} else { } else {
msg.Verbosef("resolved %q on %q flags %#x", source, target, flags) p.k.verbosef("resolved %q on %q flags %#x", source, target, flags)
} }
if err := Mount(source, target, "", MS_SILENT|MS_BIND|flags&MS_REC, ""); err != nil { if err := p.k.mount(source, target, FstypeNULL, MS_SILENT|MS_BIND|flags&MS_REC, zeroString); err != nil {
return wrapErrSuffix(err, return wrapErrSuffix(err,
fmt.Sprintf("cannot mount %q on %q:", source, target)) 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 var targetFinal string
if v, err := filepath.EvalSymlinks(target); err != nil { if v, err := p.k.evalSymlinks(target); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
} else { } else {
targetFinal = v targetFinal = v
if targetFinal != target { if targetFinal != target {
msg.Verbosef("target resolves to %q", targetFinal) p.k.verbosef("target resolves to %q", targetFinal)
} }
} }
@ -37,15 +132,15 @@ func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) err
{ {
var destFd int var destFd int
if err := IgnoringEINTR(func() (err error) { if err := IgnoringEINTR(func() (err error) {
destFd, err = Open(targetFinal, O_PATH|O_CLOEXEC, 0) destFd, err = p.k.open(targetFinal, O_PATH|O_CLOEXEC, 0)
return return
}); err != nil { }); err != nil {
return wrapErrSuffix(err, return wrapErrSuffix(err,
fmt.Sprintf("cannot open %q:", targetFinal)) fmt.Sprintf("cannot open %q:", targetFinal))
} }
if v, err := os.Readlink(p.fd(destFd)); err != nil { if v, err := p.k.readlink(p.fd(destFd)); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
} else if err = Close(destFd); err != nil { } else if err = p.k.close(destFd); err != nil {
return wrapErrSuffix(err, return wrapErrSuffix(err,
fmt.Sprintf("cannot close %q:", targetFinal)) fmt.Sprintf("cannot close %q:", targetFinal))
} else { } else {
@ -54,7 +149,7 @@ func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) err
} }
mf := MS_NOSUID | flags&MS_NODEV | flags&MS_RDONLY mf := MS_NOSUID | flags&MS_NODEV | flags&MS_RDONLY
return hostProc.mountinfo(func(d *vfs.MountInfoDecoder) error { return p.mountinfo(func(d *vfs.MountInfoDecoder) error {
n, err := d.Unfold(targetKFinal) n, err := d.Unfold(targetKFinal)
if err != nil { if err != nil {
if errors.Is(err, ESTALE) { if errors.Is(err, ESTALE) {
@ -65,17 +160,25 @@ func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) err
"cannot unfold mount hierarchy:") "cannot unfold mount hierarchy:")
} }
if err = remountWithFlags(n, mf); err != nil { if err = remountWithFlags(p.k, n, mf); err != nil {
return err return wrapErrSuffix(err,
fmt.Sprintf("cannot remount %q:", n.Clean))
} }
if flags&MS_REC == 0 { if flags&MS_REC == 0 {
return nil return nil
} }
for cur := range n.Collective() { for cur := range n.Collective() {
err = remountWithFlags(cur, mf) // avoid remounting twice
if cur == n {
continue
}
err = remountWithFlags(p.k, cur, mf)
if err != nil && !errors.Is(err, EACCES) { if err != nil && !errors.Is(err, EACCES) {
return err return wrapErrSuffix(err,
fmt.Sprintf("cannot propagate flags to %q:", cur.Clean))
} }
} }
@ -83,23 +186,27 @@ func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) err
}) })
} }
func remountWithFlags(n *vfs.MountInfoNode, mf uintptr) error { // 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() kf, unmatched := n.Flags()
if len(unmatched) != 0 { if len(unmatched) != 0 {
msg.Verbosef("unmatched vfs options: %q", unmatched) k.verbosef("unmatched vfs options: %q", unmatched)
} }
if kf&mf != mf { if kf&mf != mf {
return wrapErrSuffix( return k.mount(SourceNone, n.Clean, FstypeNULL, MS_SILENT|MS_BIND|MS_REMOUNT|kf|mf, zeroString)
Mount("none", n.Clean, "", MS_SILENT|MS_BIND|MS_REMOUNT|kf|mf, ""),
fmt.Sprintf("cannot remount %q:", n.Clean))
} }
return nil return nil
} }
func mountTmpfs(fsname, name string, size int, perm os.FileMode) error { // mountTmpfs mounts tmpfs on target;
target := toSysroot(name) // callers who wish to mount to sysroot must pass the return value of toSysroot.
if err := os.MkdirAll(target, parentPerm(perm)); err != nil { 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) return wrapErrSelf(err)
} }
opt := fmt.Sprintf("mode=%#o", perm) opt := fmt.Sprintf("mode=%#o", perm)
@ -107,8 +214,8 @@ func mountTmpfs(fsname, name string, size int, perm os.FileMode) error {
opt += fmt.Sprintf(",size=%d", size) opt += fmt.Sprintf(",size=%d", size)
} }
return wrapErrSuffix( return wrapErrSuffix(
Mount(fsname, target, "tmpfs", MS_NOSUID|MS_NODEV, opt), k.mount(fsname, target, FstypeTmpfs, flags, opt),
fmt.Sprintf("cannot mount tmpfs on %q:", name)) fmt.Sprintf("cannot mount tmpfs on %q:", target))
} }
func parentPerm(perm os.FileMode) os.FileMode { func parentPerm(perm os.FileMode) os.FileMode {
@ -121,3 +228,20 @@ func parentPerm(perm os.FileMode) os.FileMode {
} }
return os.FileMode(pperm) return os.FileMode(pperm)
} }
// EscapeOverlayDataSegment escapes a string for formatting into the data argument of an overlay mount call.
func EscapeOverlayDataSegment(s string) string {
if s == zeroString {
return zeroString
}
if f := strings.SplitN(s, "\x00", 2); len(f) > 0 {
s = f[0]
}
return strings.NewReplacer(
SpecialOverlayEscape, SpecialOverlayEscape+SpecialOverlayEscape,
SpecialOverlayOption, SpecialOverlayEscape+SpecialOverlayOption,
SpecialOverlayPath, SpecialOverlayEscape+SpecialOverlayPath,
).Replace(s)
}

305
container/mount_test.go Normal file
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)
}
})
}
}

View File

@ -1,8 +1,13 @@
package container package container
import ( import (
"errors"
"fmt"
"log" "log"
"os"
"reflect"
"sync/atomic" "sync/atomic"
"testing"
) )
type Msg interface { type Msg interface {
@ -32,7 +37,27 @@ func (msg *DefaultMsg) Verbosef(format string, v ...any) {
} }
} }
// 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 { 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...) log.Println(a...)
return err return err
} }

162
container/msg_test.go Normal file
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") }

View File

@ -1,500 +0,0 @@
package container
import (
"encoding/gob"
"fmt"
"math"
"os"
"path"
"path/filepath"
"slices"
"strings"
. "syscall"
"unsafe"
)
type (
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(params *Params) error
// apply is called in intermediate root.
apply(params *Params) error
prefix() string
Is(op Op) bool
fmt.Stringer
}
)
// Grow grows the slice Ops points to using [slices.Grow].
func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) }
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 string, flags int) *Ops {
*f = append(*f, &BindMountOp{source, "", target, flags})
return f
}
type BindMountOp struct {
Source, SourceFinal, Target string
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
)
func (b *BindMountOp) early(*Params) error {
if !path.IsAbs(b.Source) {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", b.Source))
}
if v, err := filepath.EvalSymlinks(b.Source); err != nil {
if os.IsNotExist(err) && b.Flags&BindOptional != 0 {
b.SourceFinal = "\x00"
return nil
}
return wrapErrSelf(err)
} else {
b.SourceFinal = v
return nil
}
}
func (b *BindMountOp) apply(*Params) error {
if b.SourceFinal == "\x00" {
if b.Flags&BindOptional == 0 {
// unreachable
return EBADE
}
return nil
}
if !path.IsAbs(b.SourceFinal) || !path.IsAbs(b.Target) {
return msg.WrapErr(EBADE, "path is not absolute")
}
source := toHost(b.SourceFinal)
target := toSysroot(b.Target)
// 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 := os.Stat(source); err != nil {
return wrapErrSelf(err)
} else if fi.IsDir() {
if err = os.MkdirAll(target, 0700); err != nil {
return wrapErrSelf(err)
}
} else if err = ensureFile(target, 0444, 0700); err != nil {
return err
}
var flags uintptr = MS_REC
if b.Flags&BindWritable == 0 {
flags |= MS_RDONLY
}
if b.Flags&BindDevice == 0 {
flags |= MS_NODEV
}
return hostProc.bindMount(source, target, flags, b.SourceFinal == b.Target)
}
func (b *BindMountOp) Is(op Op) bool { vb, ok := op.(*BindMountOp); return ok && *b == *vb }
func (*BindMountOp) prefix() string { return "mounting" }
func (b *BindMountOp) String() string {
if b.Source == b.Target {
return fmt.Sprintf("%q flags %#x", b.Source, b.Flags)
}
return fmt.Sprintf("%q on %q flags %#x", b.Source, b.Target, b.Flags&BindWritable)
}
func init() { gob.Register(new(MountProcOp)) }
// Proc appends an [Op] that mounts a private instance of proc.
func (f *Ops) Proc(dest string) *Ops {
*f = append(*f, MountProcOp(dest))
return f
}
type MountProcOp string
func (p MountProcOp) early(*Params) error { return nil }
func (p MountProcOp) apply(params *Params) error {
v := string(p)
if !path.IsAbs(v) {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", v))
}
target := toSysroot(v)
if err := os.MkdirAll(target, params.ParentPerm); err != nil {
return wrapErrSelf(err)
}
return wrapErrSuffix(Mount("proc", target, "proc", MS_NOSUID|MS_NOEXEC|MS_NODEV, ""),
fmt.Sprintf("cannot mount proc on %q:", v))
}
func (p MountProcOp) Is(op Op) bool { vp, ok := op.(MountProcOp); return ok && p == vp }
func (MountProcOp) prefix() string { return "mounting" }
func (p MountProcOp) String() string { return fmt.Sprintf("proc on %q", string(p)) }
func init() { gob.Register(new(MountDevOp)) }
// Dev appends an [Op] that mounts a subset of host /dev.
func (f *Ops) Dev(dest string) *Ops {
*f = append(*f, MountDevOp(dest))
return f
}
type MountDevOp string
func (d MountDevOp) early(*Params) error { return nil }
func (d MountDevOp) apply(params *Params) error {
v := string(d)
if !path.IsAbs(v) {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", v))
}
target := toSysroot(v)
if err := mountTmpfs("devtmpfs", v, 0, params.ParentPerm); err != nil {
return err
}
for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} {
targetPath := toSysroot(path.Join(v, name))
if err := ensureFile(targetPath, 0444, params.ParentPerm); err != nil {
return err
}
if err := hostProc.bindMount(
toHost("/dev/"+name),
targetPath,
0,
true,
); err != nil {
return err
}
}
for i, name := range []string{"stdin", "stdout", "stderr"} {
if err := os.Symlink(
"/proc/self/fd/"+string(rune(i+'0')),
path.Join(target, name),
); err != nil {
return wrapErrSelf(err)
}
}
for _, pair := range [][2]string{
{"/proc/self/fd", "fd"},
{"/proc/kcore", "core"},
{"pts/ptmx", "ptmx"},
} {
if err := os.Symlink(pair[0], path.Join(target, pair[1])); err != nil {
return wrapErrSelf(err)
}
}
devPtsPath := path.Join(target, "pts")
for _, name := range []string{path.Join(target, "shm"), devPtsPath} {
if err := os.Mkdir(name, params.ParentPerm); err != nil {
return wrapErrSelf(err)
}
}
if err := Mount("devpts", devPtsPath, "devpts", MS_NOSUID|MS_NOEXEC,
"newinstance,ptmxmode=0666,mode=620"); err != nil {
return wrapErrSuffix(err,
fmt.Sprintf("cannot mount devpts on %q:", devPtsPath))
}
if params.RetainSession {
var buf [8]byte
if _, _, errno := Syscall(SYS_IOCTL, 1, TIOCGWINSZ, uintptr(unsafe.Pointer(&buf[0]))); errno == 0 {
consolePath := toSysroot(path.Join(v, "console"))
if err := ensureFile(consolePath, 0444, params.ParentPerm); err != nil {
return err
}
if name, err := os.Readlink(hostProc.stdout()); err != nil {
return wrapErrSelf(err)
} else if err = hostProc.bindMount(
toHost(name),
consolePath,
0,
false,
); err != nil {
return err
}
}
}
return nil
}
func (d MountDevOp) Is(op Op) bool { vd, ok := op.(MountDevOp); return ok && d == vd }
func (MountDevOp) prefix() string { return "mounting" }
func (d MountDevOp) String() string { return fmt.Sprintf("dev on %q", string(d)) }
func init() { gob.Register(new(MountMqueueOp)) }
// Mqueue appends an [Op] that mounts a private instance of mqueue.
func (f *Ops) Mqueue(dest string) *Ops {
*f = append(*f, MountMqueueOp(dest))
return f
}
type MountMqueueOp string
func (m MountMqueueOp) early(*Params) error { return nil }
func (m MountMqueueOp) apply(params *Params) error {
v := string(m)
if !path.IsAbs(v) {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", v))
}
target := toSysroot(v)
if err := os.MkdirAll(target, params.ParentPerm); err != nil {
return wrapErrSelf(err)
}
return wrapErrSuffix(Mount("mqueue", target, "mqueue", MS_NOSUID|MS_NOEXEC|MS_NODEV, ""),
fmt.Sprintf("cannot mount mqueue on %q:", v))
}
func (m MountMqueueOp) Is(op Op) bool { vm, ok := op.(MountMqueueOp); return ok && m == vm }
func (MountMqueueOp) prefix() string { return "mounting" }
func (m MountMqueueOp) String() string { return fmt.Sprintf("mqueue on %q", string(m)) }
func init() { gob.Register(new(MountTmpfsOp)) }
// Tmpfs appends an [Op] that mounts tmpfs on container path [MountTmpfsOp.Path].
func (f *Ops) Tmpfs(dest string, size int, perm os.FileMode) *Ops {
*f = append(*f, &MountTmpfsOp{dest, size, perm})
return f
}
type MountTmpfsOp struct {
Path string
Size int
Perm os.FileMode
}
func (t *MountTmpfsOp) early(*Params) error { return nil }
func (t *MountTmpfsOp) apply(*Params) error {
if !path.IsAbs(t.Path) {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", t.Path))
}
if t.Size < 0 || t.Size > math.MaxUint>>1 {
return msg.WrapErr(EBADE, fmt.Sprintf("size %d out of bounds", t.Size))
}
return mountTmpfs("tmpfs", t.Path, t.Size, t.Perm)
}
func (t *MountTmpfsOp) Is(op Op) bool { vt, ok := op.(*MountTmpfsOp); return ok && *t == *vt }
func (*MountTmpfsOp) prefix() string { return "mounting" }
func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }
func init() { gob.Register(new(SymlinkOp)) }
// Link appends an [Op] that creates a symlink in the container filesystem.
func (f *Ops) Link(target, linkName string) *Ops {
*f = append(*f, &SymlinkOp{target, linkName})
return f
}
type SymlinkOp [2]string
func (l *SymlinkOp) early(*Params) error {
if strings.HasPrefix(l[0], "*") {
l[0] = l[0][1:]
if !path.IsAbs(l[0]) {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", l[0]))
}
if name, err := os.Readlink(l[0]); err != nil {
return wrapErrSelf(err)
} else {
l[0] = name
}
}
return nil
}
func (l *SymlinkOp) apply(params *Params) error {
// symlink target is an arbitrary path value, so only validate link name here
if !path.IsAbs(l[1]) {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", l[1]))
}
target := toSysroot(l[1])
if err := os.MkdirAll(path.Dir(target), params.ParentPerm); err != nil {
return wrapErrSelf(err)
}
if err := os.Symlink(l[0], target); err != nil {
return wrapErrSelf(err)
}
return nil
}
func (l *SymlinkOp) Is(op Op) bool { vl, ok := op.(*SymlinkOp); return ok && *l == *vl }
func (*SymlinkOp) prefix() string { return "creating" }
func (l *SymlinkOp) String() string { return fmt.Sprintf("symlink on %q target %q", l[1], l[0]) }
func init() { gob.Register(new(MkdirOp)) }
// Mkdir appends an [Op] that creates a directory in the container filesystem.
func (f *Ops) Mkdir(dest string, perm os.FileMode) *Ops {
*f = append(*f, &MkdirOp{dest, perm})
return f
}
type MkdirOp struct {
Path string
Perm os.FileMode
}
func (m *MkdirOp) early(*Params) error { return nil }
func (m *MkdirOp) apply(*Params) error {
if !path.IsAbs(m.Path) {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", m.Path))
}
if err := os.MkdirAll(toSysroot(m.Path), m.Perm); err != nil {
return wrapErrSelf(err)
}
return nil
}
func (m *MkdirOp) Is(op Op) bool { vm, ok := op.(*MkdirOp); return ok && m == vm }
func (*MkdirOp) prefix() string { return "creating" }
func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) }
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 string, 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 string, dataP **[]byte) *Ops {
t := &TmpfileOp{Path: name}
*dataP = &t.Data
*f = append(*f, t)
return f
}
type TmpfileOp struct {
Path string
Data []byte
}
func (t *TmpfileOp) early(*Params) error { return nil }
func (t *TmpfileOp) apply(params *Params) error {
if !path.IsAbs(t.Path) {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", t.Path))
}
var tmpPath string
if f, err := os.CreateTemp("/", "tmp.*"); 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)
if err := ensureFile(target, 0444, params.ParentPerm); err != nil {
return err
} else if err = hostProc.bindMount(
tmpPath,
target,
MS_RDONLY|MS_NODEV,
false,
); err != nil {
return err
} else if err = os.Remove(tmpPath); err != nil {
return wrapErrSelf(err)
}
return nil
}
func (t *TmpfileOp) Is(op Op) bool {
vt, ok := op.(*TmpfileOp)
return ok && t.Path == vt.Path && slices.Equal(t.Data, 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))
}
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, prefix string) *Ops {
e := &AutoEtcOp{prefix}
f.Mkdir("/etc", 0755)
f.Bind(host, e.hostPath(), 0)
*f = append(*f, e)
return f
}
type AutoEtcOp struct{ Prefix string }
func (e *AutoEtcOp) early(*Params) error { return nil }
func (e *AutoEtcOp) apply(*Params) error {
const target = sysrootPath + "/etc/"
rel := e.hostRel() + "/"
if err := os.MkdirAll(target, 0755); err != nil {
return wrapErrSelf(err)
}
if d, err := os.ReadDir(toSysroot(e.hostPath())); err != nil {
return wrapErrSelf(err)
} else {
for _, ent := range d {
n := ent.Name()
switch n {
case ".host":
case "passwd":
case "group":
case "mtab":
if err = os.Symlink("/proc/mounts", target+n); err != nil {
return wrapErrSelf(err)
}
default:
if err = os.Symlink(rel+n, target+n); err != nil {
return wrapErrSelf(err)
}
}
}
}
return nil
}
func (e *AutoEtcOp) hostPath() string { return "/etc/" + 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 == nil && ve == nil) || (e != nil && ve != nil && *e == *ve))
}
func (*AutoEtcOp) prefix() string { return "setting up" }
func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }

110
container/output_test.go Normal file
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...)
}

View File

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

120
container/params_test.go Normal file
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) })
})
}

View File

@ -13,10 +13,81 @@ import (
"hakurei.app/container/vfs" "hakurei.app/container/vfs"
) )
/* constants in this file bypass abs check, be extremely careful when changing them! */
const ( const (
hostPath = "/" + hostDir // 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" hostDir = "host"
sysrootPath = "/" + sysrootDir sysrootPath = FHSRoot + sysrootDir
sysrootDir = "sysroot" sysrootDir = "sysroot"
) )
@ -40,18 +111,15 @@ func createFile(name string, perm, pperm os.FileMode, content []byte) error {
} }
if content != nil { if content != nil {
_, err = f.Write(content) _, err = f.Write(content)
if err != nil {
err = wrapErrSelf(err)
} }
} return errors.Join(f.Close(), wrapErrSelf(err))
return errors.Join(f.Close(), err)
} }
func ensureFile(name string, perm, pperm os.FileMode) error { func ensureFile(name string, perm, pperm os.FileMode) error {
fi, err := os.Stat(name) fi, err := os.Stat(name)
if err != nil { if err != nil {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
return err return wrapErrSelf(err)
} }
return createFile(name, perm, pperm, nil) return createFile(name, perm, pperm, nil)
} }
@ -63,13 +131,14 @@ func ensureFile(name string, perm, pperm os.FileMode) error {
return err return err
} }
var hostProc = newProcPats(hostPath) var hostProc = newProcPaths(direct{}, hostPath)
func newProcPats(prefix string) *procPaths { func newProcPaths(k syscallDispatcher, prefix string) *procPaths {
return &procPaths{prefix + "/proc", prefix + "/proc/self"} return &procPaths{k, prefix + "/proc", prefix + "/proc/self"}
} }
type procPaths struct { type procPaths struct {
k syscallDispatcher
prefix string prefix string
self string self string
} }
@ -77,14 +146,13 @@ type procPaths struct {
func (p *procPaths) stdout() string { return p.self + "/fd/1" } 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) fd(fd int) string { return p.self + "/fd/" + strconv.Itoa(fd) }
func (p *procPaths) mountinfo(f func(d *vfs.MountInfoDecoder) error) error { func (p *procPaths) mountinfo(f func(d *vfs.MountInfoDecoder) error) error {
if r, err := os.Open(p.self + "/mountinfo"); err != nil { if r, err := p.k.openNew(p.self + "/mountinfo"); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
} else { } else {
d := vfs.NewMountInfoDecoder(r) d := vfs.NewMountInfoDecoder(r)
err0 := f(d) err0 := f(d)
if err = r.Close(); err != nil { if err = r.Close(); err != nil {
return wrapErrSuffix(err, return wrapErrSelf(err)
"cannot close mountinfo:")
} else if err = d.Err(); err != nil { } else if err = d.Err(); err != nil {
return wrapErrSuffix(err, return wrapErrSuffix(err,
"cannot parse mountinfo:") "cannot parse mountinfo:")

252
container/path_test.go Normal file
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)
}
})
})
})
}

View File

@ -2,13 +2,24 @@ package container
import ( import (
"syscall" "syscall"
"unsafe"
) )
// SetPtracer allows processes to ptrace(2) the calling process.
func SetPtracer(pid uintptr) error {
_, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_SET_PTRACER, pid, 0)
if errno == 0 {
return nil
}
return errno
}
const ( const (
SUID_DUMP_DISABLE = iota SUID_DUMP_DISABLE = iota
SUID_DUMP_USER SUID_DUMP_USER
) )
// SetDumpable sets the "dumpable" attribute of the calling process.
func SetDumpable(dumpable uintptr) error { func SetDumpable(dumpable uintptr) error {
// linux/sched/coredump.h // linux/sched/coredump.h
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, dumpable, 0); errno != 0 { if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, dumpable, 0); errno != 0 {
@ -18,6 +29,27 @@ func SetDumpable(dumpable uintptr) error {
return nil return nil
} }
// SetNoNewPrivs sets the calling thread's no_new_privs attribute.
func SetNoNewPrivs() error {
_, _, errno := syscall.Syscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0)
if errno == 0 {
return nil
}
return errno
}
// Isatty tests whether a file descriptor refers to a terminal.
func Isatty(fd int) bool {
var buf [8]byte
r, _, _ := syscall.Syscall(
syscall.SYS_IOCTL,
uintptr(fd),
syscall.TIOCGWINSZ,
uintptr(unsafe.Pointer(&buf[0])),
)
return r == 0
}
// IgnoringEINTR makes a function call and repeats it if it returns an // IgnoringEINTR makes a function call and repeats it if it returns an
// EINTR error. This appears to be required even though we install all // EINTR error. This appears to be required even though we install all
// signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846. // signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846.

View File

@ -17,9 +17,9 @@ var (
) )
const ( const (
kernelOverflowuidPath = "/proc/sys/kernel/overflowuid" kernelOverflowuidPath = FHSProcSys + "kernel/overflowuid"
kernelOverflowgidPath = "/proc/sys/kernel/overflowgid" kernelOverflowgidPath = FHSProcSys + "kernel/overflowgid"
kernelCapLastCapPath = "/proc/sys/kernel/cap_last_cap" kernelCapLastCapPath = FHSProcSys + "kernel/cap_last_cap"
) )
func mustReadSysctl() { func mustReadSysctl() {

2
dist/install.sh vendored
View File

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

View File

@ -32,7 +32,7 @@
buildPackage = forAllSystems ( buildPackage = forAllSystems (
system: system:
nixpkgsFor.${system}.callPackage ( nixpkgsFor.${system}.callPackage (
import ./cmd/planterette/build.nix { import ./cmd/hpkg/build.nix {
inherit inherit
nixpkgsFor nixpkgsFor
system system
@ -69,7 +69,7 @@
withRace = true; withRace = true;
}; };
planterette = callPackage ./cmd/planterette/test { inherit system self; }; hpkg = callPackage ./cmd/hpkg/test { inherit system self; };
formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } '' formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } ''
cd ${./.} cd ${./.}
@ -125,7 +125,7 @@
glibc glibc
xdg-dbus-proxy xdg-dbus-proxy
# planterette # hpkg
zstd zstd
gnutar gnutar
coreutils coreutils
@ -159,6 +159,54 @@
default = pkgs.mkShell { buildInputs = hakurei.targetPkgs; }; default = pkgs.mkShell { buildInputs = hakurei.targetPkgs; };
withPackage = pkgs.mkShell { buildInputs = [ hakurei ] ++ hakurei.targetPkgs; }; withPackage = pkgs.mkShell { buildInputs = [ hakurei ] ++ hakurei.targetPkgs; };
vm =
let
nixos = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
{
environment = {
systemPackages = [
(pkgs.buildFHSEnv {
pname = "hakurei-fhs";
inherit (hakurei) version;
targetPkgs = _: hakurei.targetPkgs;
extraOutputsToInstall = [ "dev" ];
profile = ''
export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH"
'';
})
];
hakurei =
let
# this is used for interactive vm testing during development, where tests might be broken
package = self.packages.${pkgs.system}.hakurei.override {
buildGoModule = previousArgs: pkgs.pkgsStatic.buildGoModule (previousArgs // { doCheck = false; });
};
in
{
inherit package;
hsuPackage = self.packages.${pkgs.system}.hsu.override { hakurei = package; };
};
};
}
./test/interactive/configuration.nix
./test/interactive/vm.nix
./test/interactive/hakurei.nix
./test/interactive/trace.nix
self.nixosModules.hakurei
self.inputs.home-manager.nixosModules.home-manager
];
};
in
pkgs.mkShell {
buildInputs = [ nixos.config.system.build.vm ];
shellHook = "exec run-nixos-vm $@";
};
generateDoc = generateDoc =
let let
inherit (pkgs) lib; inherit (pkgs) lib;

View File

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

View File

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

View File

@ -1,27 +1,32 @@
// Package hst exports shared types for invoking hakurei.
package hst package hst
import ( import (
"hakurei.app/system" "time"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
) )
const Tmp = "/.hakurei" const Tmp = "/.hakurei"
var AbsTmp = container.MustAbs(Tmp)
// Config is used to seal an app implementation. // Config is used to seal an app implementation.
type Config struct { type (
Config struct {
// reverse-DNS style arbitrary identifier string from config; // reverse-DNS style arbitrary identifier string from config;
// passed to wayland security-context-v1 as application ID // passed to wayland security-context-v1 as application ID
// and used as part of defaults in dbus session proxy // and used as part of defaults in dbus session proxy
ID string `json:"id"` ID string `json:"id"`
// absolute path to executable file // absolute path to executable file
Path string `json:"path,omitempty"` Path *container.Absolute `json:"path,omitempty"`
// final args passed to container init // final args passed to container init
Args []string `json:"args"` Args []string `json:"args"`
// system services to make available in the container // system services to make available in the container
Enablements system.Enablement `json:"enablements"` Enablements *Enablements `json:"enablements,omitempty"`
// session D-Bus proxy configuration; // session D-Bus proxy configuration;
// nil makes session bus proxy assume built-in defaults // nil makes session bus proxy assume built-in defaults
@ -35,13 +40,12 @@ type Config struct {
// passwd username in container, defaults to passwd name of target uid or chronos // passwd username in container, defaults to passwd name of target uid or chronos
Username string `json:"username,omitempty"` Username string `json:"username,omitempty"`
// absolute path to shell, empty for host shell // absolute path to shell
Shell string `json:"shell,omitempty"` Shell *container.Absolute `json:"shell"`
// absolute path to home directory in the init mount namespace // directory to enter and use as home in the container mount namespace
Data string `json:"data"` Home *container.Absolute `json:"home"`
// directory to enter and use as home in the container mount namespace, empty for Data
Dir string `json:"dir"` // extra acl ops to perform before setuid
// extra acl ops, dispatches before container init
ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"` ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
// numerical application id, used for init user namespace credentials // numerical application id, used for init user namespace credentials
@ -53,23 +57,67 @@ type Config struct {
Container *ContainerConfig `json:"container"` Container *ContainerConfig `json:"container"`
} }
// ContainerConfig describes the container configuration baseline to which the app implementation adds upon.
ContainerConfig struct {
// container hostname
Hostname string `json:"hostname,omitempty"`
// duration to wait for after interrupting a container's initial process in nanoseconds;
// a negative value causes the container to be terminated immediately on cancellation
WaitDelay time.Duration `json:"wait_delay,omitempty"`
// extra seccomp flags
SeccompFlags seccomp.ExportFlag `json:"seccomp_flags"`
// extra seccomp presets
SeccompPresets seccomp.FilterPreset `json:"seccomp_presets"`
// disable project-specific filter extensions
SeccompCompat bool `json:"seccomp_compat,omitempty"`
// allow ptrace and friends
Devel bool `json:"devel,omitempty"`
// allow userns creation in container
Userns bool `json:"userns,omitempty"`
// share host net namespace
HostNet bool `json:"host_net,omitempty"`
// share abstract unix socket scope
HostAbstract bool `json:"host_abstract,omitempty"`
// allow dangerous terminal I/O
Tty bool `json:"tty,omitempty"`
// allow multiarch
Multiarch bool `json:"multiarch,omitempty"`
// initial process environment variables
Env map[string]string `json:"env"`
// map target user uid to privileged user uid in the user namespace
MapRealUID bool `json:"map_real_uid"`
// pass through all devices
Device bool `json:"device,omitempty"`
// container mount points;
// if the first element targets /, it is inserted early and excluded from path hiding
Filesystem []FilesystemConfigJSON `json:"filesystem"`
}
)
// ExtraPermConfig describes an acl update op. // ExtraPermConfig describes an acl update op.
type ExtraPermConfig struct { type ExtraPermConfig struct {
Ensure bool `json:"ensure,omitempty"` Ensure bool `json:"ensure,omitempty"`
Path string `json:"path"` Path *container.Absolute `json:"path"`
Read bool `json:"r,omitempty"` Read bool `json:"r,omitempty"`
Write bool `json:"w,omitempty"` Write bool `json:"w,omitempty"`
Execute bool `json:"x,omitempty"` Execute bool `json:"x,omitempty"`
} }
func (e *ExtraPermConfig) String() string { func (e *ExtraPermConfig) String() string {
buf := make([]byte, 0, 5+len(e.Path)) if e == nil || e.Path == nil {
return "<invalid>"
}
buf := make([]byte, 0, 5+len(e.Path.String()))
buf = append(buf, '-', '-', '-') buf = append(buf, '-', '-', '-')
if e.Ensure { if e.Ensure {
buf = append(buf, '+') buf = append(buf, '+')
} }
buf = append(buf, ':') buf = append(buf, ':')
buf = append(buf, []byte(e.Path)...) buf = append(buf, []byte(e.Path.String())...)
if e.Read { if e.Read {
buf[0] = 'r' buf[0] = 'r'
} }

35
hst/config_test.go Normal file
View File

@ -0,0 +1,35 @@
package hst_test
import (
"testing"
"hakurei.app/container"
"hakurei.app/hst"
)
func TestExtraPermConfig(t *testing.T) {
testCases := []struct {
name string
config *hst.ExtraPermConfig
want string
}{
{"nil", nil, "<invalid>"},
{"nil path", &hst.ExtraPermConfig{Path: nil}, "<invalid>"},
{"r", &hst.ExtraPermConfig{Path: container.AbsFHSRoot, Read: true}, "r--:/"},
{"r+", &hst.ExtraPermConfig{Ensure: true, Path: container.AbsFHSRoot, Read: true}, "r--+:/"},
{"w", &hst.ExtraPermConfig{Path: hst.AbsTmp, Write: true}, "-w-:/.hakurei"},
{"w+", &hst.ExtraPermConfig{Ensure: true, Path: hst.AbsTmp, Write: true}, "-w-+:/.hakurei"},
{"x", &hst.ExtraPermConfig{Path: container.AbsFHSRunUser, Execute: true}, "--x:/run/user/"},
{"x+", &hst.ExtraPermConfig{Ensure: true, Path: container.AbsFHSRunUser, Execute: true}, "--x+:/run/user/"},
{"rwx", &hst.ExtraPermConfig{Path: container.AbsFHSTmp, Read: true, Write: true, Execute: true}, "rwx:/tmp/"},
{"rwx+", &hst.ExtraPermConfig{Ensure: true, Path: container.AbsFHSTmp, Read: true, Write: true, Execute: true}, "rwx+:/tmp/"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := tc.config.String(); got != tc.want {
t.Errorf("String: %q, want %q", got, tc.want)
}
})
}
}

View File

@ -1,69 +0,0 @@
package hst
import (
"time"
"hakurei.app/container/seccomp"
)
type (
// ContainerConfig describes the container configuration baseline to which the app implementation adds upon.
ContainerConfig struct {
// container hostname
Hostname string `json:"hostname,omitempty"`
// duration to wait for after interrupting a container's initial process in nanoseconds;
// a negative value causes the container to be terminated immediately on cancellation
WaitDelay time.Duration `json:"wait_delay,omitempty"`
// extra seccomp flags
SeccompFlags seccomp.ExportFlag `json:"seccomp_flags"`
// extra seccomp presets
SeccompPresets seccomp.FilterPreset `json:"seccomp_presets"`
// disable project-specific filter extensions
SeccompCompat bool `json:"seccomp_compat,omitempty"`
// allow ptrace and friends
Devel bool `json:"devel,omitempty"`
// allow userns creation in container
Userns bool `json:"userns,omitempty"`
// share host net namespace
Net bool `json:"net,omitempty"`
// allow dangerous terminal I/O
Tty bool `json:"tty,omitempty"`
// allow multiarch
Multiarch bool `json:"multiarch,omitempty"`
// initial process environment variables
Env map[string]string `json:"env"`
// map target user uid to privileged user uid in the user namespace
MapRealUID bool `json:"map_real_uid"`
// pass through all devices
Device bool `json:"device,omitempty"`
// container host filesystem bind mounts
Filesystem []*FilesystemConfig `json:"filesystem"`
// create symlinks inside container filesystem
Link [][2]string `json:"symlink"`
// read-only /etc directory
Etc string `json:"etc,omitempty"`
// automatically set up /etc symlinks
AutoEtc bool `json:"auto_etc"`
// cover these paths or create them if they do not already exist
Cover []string `json:"cover"`
}
// FilesystemConfig is an abstract representation of a bind mount.
FilesystemConfig struct {
// mount point in container, same as src if empty
Dst string `json:"dst,omitempty"`
// host filesystem path to make available to the container
Src string `json:"src"`
// do not mount filesystem read-only
Write bool `json:"write,omitempty"`
// do not disable device files
Device bool `json:"dev,omitempty"`
// fail if the bind mount cannot be established for any reason
Must bool `json:"require,omitempty"`
}
)

69
hst/enablement.go Normal file
View File

@ -0,0 +1,69 @@
package hst
import (
"encoding/json"
"syscall"
"hakurei.app/system"
)
// NewEnablements returns the address of [system.Enablement] as [Enablements].
func NewEnablements(e system.Enablement) *Enablements { return (*Enablements)(&e) }
// enablementsJSON is the [json] representation of the [system.Enablement] bit field.
type enablementsJSON struct {
Wayland bool `json:"wayland,omitempty"`
X11 bool `json:"x11,omitempty"`
DBus bool `json:"dbus,omitempty"`
Pulse bool `json:"pulse,omitempty"`
}
// Enablements is the [json] adapter for [system.Enablement].
type Enablements system.Enablement
// Unwrap returns the underlying [system.Enablement].
func (e *Enablements) Unwrap() system.Enablement {
if e == nil {
return 0
}
return system.Enablement(*e)
}
func (e *Enablements) MarshalJSON() ([]byte, error) {
if e == nil {
return nil, syscall.EINVAL
}
return json.Marshal(&enablementsJSON{
Wayland: system.Enablement(*e)&system.EWayland != 0,
X11: system.Enablement(*e)&system.EX11 != 0,
DBus: system.Enablement(*e)&system.EDBus != 0,
Pulse: system.Enablement(*e)&system.EPulse != 0,
})
}
func (e *Enablements) UnmarshalJSON(data []byte) error {
if e == nil {
return syscall.EINVAL
}
v := new(enablementsJSON)
if err := json.Unmarshal(data, &v); err != nil {
return err
}
var ve system.Enablement
if v.Wayland {
ve |= system.EWayland
}
if v.X11 {
ve |= system.EX11
}
if v.DBus {
ve |= system.EDBus
}
if v.Pulse {
ve |= system.EPulse
}
*e = Enablements(ve)
return nil
}

108
hst/enablement_test.go Normal file
View File

@ -0,0 +1,108 @@
package hst_test
import (
"encoding/json"
"errors"
"syscall"
"testing"
"hakurei.app/hst"
"hakurei.app/system"
)
func TestEnablements(t *testing.T) {
testCases := []struct {
name string
e *hst.Enablements
data string
sData string
}{
{"nil", nil, "null", `{"value":null,"magic":3236757504}`},
{"zero", hst.NewEnablements(0), `{}`, `{"value":{},"magic":3236757504}`},
{"wayland", hst.NewEnablements(system.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`},
{"x11", hst.NewEnablements(system.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`},
{"dbus", hst.NewEnablements(system.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`},
{"pulse", hst.NewEnablements(system.EPulse), `{"pulse":true}`, `{"value":{"pulse":true},"magic":3236757504}`},
{"all", hst.NewEnablements(system.EWayland | system.EX11 | system.EDBus | system.EPulse), `{"wayland":true,"x11":true,"dbus":true,"pulse":true}`, `{"value":{"wayland":true,"x11":true,"dbus":true,"pulse":true},"magic":3236757504}`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("marshal", func(t *testing.T) {
if got, err := json.Marshal(tc.e); err != nil {
t.Fatalf("Marshal: error = %v", err)
} else if string(got) != tc.data {
t.Errorf("Marshal:\n%s, want\n%s", string(got), tc.data)
}
if got, err := json.Marshal(struct {
Value *hst.Enablements `json:"value"`
Magic int `json:"magic"`
}{tc.e, syscall.MS_MGC_VAL}); err != nil {
t.Fatalf("Marshal: error = %v", err)
} else if string(got) != tc.sData {
t.Errorf("Marshal:\n%s, want\n%s", string(got), tc.sData)
}
})
t.Run("unmarshal", func(t *testing.T) {
{
got := new(hst.Enablements)
if err := json.Unmarshal([]byte(tc.data), &got); err != nil {
t.Fatalf("Unmarshal: error = %v", err)
}
if tc.e == nil {
if got != nil {
t.Errorf("Unmarshal: %v", got)
}
} else if *got != *tc.e {
t.Errorf("Unmarshal: %v, want %v", got, tc.e)
}
}
{
got := *(new(struct {
Value *hst.Enablements `json:"value"`
Magic int `json:"magic"`
}))
if err := json.Unmarshal([]byte(tc.sData), &got); err != nil {
t.Fatalf("Unmarshal: error = %v", err)
}
if tc.e == nil {
if got.Value != nil {
t.Errorf("Unmarshal: %v", got)
}
} else if *got.Value != *tc.e {
t.Errorf("Unmarshal: %v, want %v", got.Value, tc.e)
}
}
})
})
}
t.Run("unwrap", func(t *testing.T) {
t.Run("nil", func(t *testing.T) {
if got := (*hst.Enablements)(nil).Unwrap(); got != 0 {
t.Errorf("Unwrap: %v", got)
}
})
t.Run("val", func(t *testing.T) {
if got := hst.NewEnablements(system.EWayland | system.EPulse).Unwrap(); got != system.EWayland|system.EPulse {
t.Errorf("Unwrap: %v", got)
}
})
})
t.Run("passthrough", func(t *testing.T) {
if _, err := (*hst.Enablements)(nil).MarshalJSON(); !errors.Is(err, syscall.EINVAL) {
t.Errorf("MarshalJSON: error = %v", err)
}
if err := (*hst.Enablements)(nil).UnmarshalJSON(nil); !errors.Is(err, syscall.EINVAL) {
t.Errorf("UnmarshalJSON: error = %v", err)
}
if err := new(hst.Enablements).UnmarshalJSON([]byte{}); err == nil {
t.Errorf("UnmarshalJSON: error = %v", err)
}
})
}

137
hst/fs.go Normal file
View File

@ -0,0 +1,137 @@
package hst
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"hakurei.app/container"
)
// FilesystemConfig is an abstract representation of a mount point.
type FilesystemConfig interface {
// Valid returns whether the configuration is valid.
Valid() bool
// Path returns the target path in the container.
Path() *container.Absolute
// Host returns a slice of all host paths used by this operation.
Host() []*container.Absolute
// Apply appends the [container.Op] implementing this operation.
Apply(z *ApplyState)
fmt.Stringer
}
// ApplyState holds the address of [container.Ops] and any relevant application state.
type ApplyState struct {
// AutoEtcPrefix is the prefix for [container.AutoEtcOp].
AutoEtcPrefix string
*container.Ops
}
var (
ErrFSNull = errors.New("unexpected null in mount point")
)
// FSTypeError is returned when [ContainerConfig.Filesystem] contains an entry with invalid type.
type FSTypeError string
func (f FSTypeError) Error() string { return fmt.Sprintf("invalid filesystem type %q", string(f)) }
// FSImplError is returned for unsupported implementations of [FilesystemConfig].
type FSImplError struct{ Value FilesystemConfig }
func (f FSImplError) Error() string {
implType := reflect.TypeOf(f.Value)
var name string
for implType != nil && implType.Kind() == reflect.Ptr {
name += "*"
implType = implType.Elem()
}
if implType != nil {
name += implType.Name()
} else {
name += "nil"
}
return fmt.Sprintf("implementation %s not supported", name)
}
// FilesystemConfigJSON is the [json] adapter for [FilesystemConfig].
type FilesystemConfigJSON struct{ FilesystemConfig }
// Valid returns whether the [FilesystemConfigJSON] is valid.
func (f *FilesystemConfigJSON) Valid() bool {
return f != nil && f.FilesystemConfig != nil && f.FilesystemConfig.Valid()
}
// fsType holds the string representation of a [FilesystemConfig]'s concrete type.
type fsType struct {
Type string `json:"type"`
}
func (f *FilesystemConfigJSON) MarshalJSON() ([]byte, error) {
if f == nil || f.FilesystemConfig == nil {
return nil, ErrFSNull
}
var v any
switch cv := f.FilesystemConfig.(type) {
case *FSBind:
v = &struct {
fsType
*FSBind
}{fsType{FilesystemBind}, cv}
case *FSEphemeral:
v = &struct {
fsType
*FSEphemeral
}{fsType{FilesystemEphemeral}, cv}
case *FSOverlay:
v = &struct {
fsType
*FSOverlay
}{fsType{FilesystemOverlay}, cv}
case *FSLink:
v = &struct {
fsType
*FSLink
}{fsType{FilesystemLink}, cv}
default:
return nil, FSImplError{f.FilesystemConfig}
}
return json.Marshal(v)
}
func (f *FilesystemConfigJSON) UnmarshalJSON(data []byte) error {
t := new(fsType)
if err := json.Unmarshal(data, &t); err != nil {
return err
}
if t == nil {
return ErrFSNull
}
switch t.Type {
case FilesystemBind:
*f = FilesystemConfigJSON{new(FSBind)}
case FilesystemEphemeral:
*f = FilesystemConfigJSON{new(FSEphemeral)}
case FilesystemOverlay:
*f = FilesystemConfigJSON{new(FSOverlay)}
case FilesystemLink:
*f = FilesystemConfigJSON{new(FSLink)}
default:
return FSTypeError(t.Type)
}
return json.Unmarshal(data, f.FilesystemConfig)
}

297
hst/fs_test.go Normal file
View File

@ -0,0 +1,297 @@
package hst_test
import (
"encoding/json"
"errors"
"reflect"
"strings"
"syscall"
"testing"
"hakurei.app/container"
"hakurei.app/hst"
)
func TestFilesystemConfigJSON(t *testing.T) {
testCases := []struct {
name string
want hst.FilesystemConfigJSON
wantErr error
data, sData string
}{
{"nil", hst.FilesystemConfigJSON{FilesystemConfig: nil}, hst.ErrFSNull,
`null`, `{"fs":null,"magic":3236757504}`},
{"bad type", hst.FilesystemConfigJSON{FilesystemConfig: stubFS{"cat"}},
hst.FSTypeError("cat"),
`{"type":"cat","meow":true}`, `{"fs":{"type":"cat","meow":true},"magic":3236757504}`},
{"bad impl bind", hst.FilesystemConfigJSON{FilesystemConfig: stubFS{"bind"}},
hst.FSImplError{Value: stubFS{"bind"}},
"\x00", "\x00"},
{"bad impl ephemeral", hst.FilesystemConfigJSON{FilesystemConfig: stubFS{"ephemeral"}},
hst.FSImplError{Value: stubFS{"ephemeral"}},
"\x00", "\x00"},
{"bad impl overlay", hst.FilesystemConfigJSON{FilesystemConfig: stubFS{"overlay"}},
hst.FSImplError{Value: stubFS{"overlay"}},
"\x00", "\x00"},
{"bind", hst.FilesystemConfigJSON{
FilesystemConfig: &hst.FSBind{
Target: m("/etc"),
Source: m("/mnt/etc"),
Optional: true,
},
}, nil,
`{"type":"bind","dst":"/etc","src":"/mnt/etc","optional":true}`,
`{"fs":{"type":"bind","dst":"/etc","src":"/mnt/etc","optional":true},"magic":3236757504}`},
{"ephemeral", hst.FilesystemConfigJSON{
FilesystemConfig: &hst.FSEphemeral{
Target: m("/run/user/65534"),
Write: true,
Size: 1 << 10,
Perm: 0700,
},
}, nil,
`{"type":"ephemeral","dst":"/run/user/65534","write":true,"size":1024,"perm":448}`,
`{"fs":{"type":"ephemeral","dst":"/run/user/65534","write":true,"size":1024,"perm":448},"magic":3236757504}`},
{"overlay", hst.FilesystemConfigJSON{
FilesystemConfig: &hst.FSOverlay{
Target: m("/nix/store"),
Lower: ms("/mnt-root/nix/.ro-store"),
Upper: m("/mnt-root/nix/.rw-store/upper"),
Work: m("/mnt-root/nix/.rw-store/work"),
},
}, nil,
`{"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"}`,
`{"fs":{"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"},"magic":3236757504}`},
{"link", hst.FilesystemConfigJSON{
FilesystemConfig: &hst.FSLink{
Target: m("/run/current-system"),
Linkname: "/run/current-system",
Dereference: true,
},
}, nil,
`{"type":"link","dst":"/run/current-system","linkname":"/run/current-system","dereference":true}`,
`{"fs":{"type":"link","dst":"/run/current-system","linkname":"/run/current-system","dereference":true},"magic":3236757504}`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("marshal", func(t *testing.T) {
wantErr := tc.wantErr
if errors.As(wantErr, new(hst.FSTypeError)) {
// for unsupported implementation tc
wantErr = hst.FSImplError{Value: stubFS{"cat"}}
}
{
d, err := json.Marshal(&tc.want)
if !errors.Is(err, wantErr) {
t.Errorf("Marshal: error = %v, want %v", err, wantErr)
}
if wantErr != nil {
goto checkSMarshal
}
if string(d) != tc.data {
t.Errorf("Marshal:\n%s\nwant:\n%s", string(d), tc.data)
}
}
checkSMarshal:
{
d, err := json.Marshal(&sCheck{tc.want, syscall.MS_MGC_VAL})
if !errors.Is(err, wantErr) {
t.Errorf("Marshal: error = %v, want %v", err, wantErr)
}
if wantErr != nil {
return
}
if string(d) != tc.sData {
t.Errorf("Marshal:\n%s\nwant:\n%s", string(d), tc.sData)
}
}
})
t.Run("unmarshal", func(t *testing.T) {
if tc.data == "\x00" && tc.sData == "\x00" {
if errors.As(tc.wantErr, new(hst.FSImplError)) {
// this error is only returned on marshal
return
}
}
{
var got hst.FilesystemConfigJSON
err := json.Unmarshal([]byte(tc.data), &got)
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.want, &got) {
t.Errorf("Unmarshal: %#v, want %#v", &tc.want, &got)
}
}
checkSUnmarshal:
{
var got sCheck
err := json.Unmarshal([]byte(tc.sData), &got)
if !errors.Is(err, tc.wantErr) {
t.Errorf("Unmarshal: error = %v, want %v", err, tc.wantErr)
}
if tc.wantErr != nil {
return
}
want := sCheck{tc.want, syscall.MS_MGC_VAL}
if !reflect.DeepEqual(&got, &want) {
t.Errorf("Unmarshal: %#v, want %#v", &got, &want)
}
}
})
})
}
t.Run("valid", func(t *testing.T) {
if got := (*hst.FilesystemConfigJSON).Valid(nil); got {
t.Errorf("Valid: %v, want false", got)
}
if got := new(hst.FilesystemConfigJSON).Valid(); got {
t.Errorf("Valid: %v, want false", got)
}
if got := (&hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Source: m("/etc")}}).Valid(); !got {
t.Errorf("Valid: %v, want true", got)
}
})
t.Run("passthrough", func(t *testing.T) {
if err := new(hst.FilesystemConfigJSON).UnmarshalJSON(make([]byte, 0)); err == nil {
t.Errorf("UnmarshalJSON: error = %v", err)
}
})
}
func TestFSErrors(t *testing.T) {
t.Run("type", func(t *testing.T) {
want := `invalid filesystem type "cat"`
if got := hst.FSTypeError("cat").Error(); got != want {
t.Errorf("Error: %q, want %q", got, want)
}
})
t.Run("impl", func(t *testing.T) {
testCases := []struct {
name string
val hst.FilesystemConfig
want string
}{
{"nil", nil, "implementation nil not supported"},
{"stub", stubFS{"cat"}, "implementation stubFS not supported"},
{"*stub", &stubFS{"cat"}, "implementation *stubFS not supported"},
{"(*stub)(nil)", (*stubFS)(nil), "implementation *stubFS not supported"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := hst.FSImplError{Value: tc.val}
if got := err.Error(); got != tc.want {
t.Errorf("Error: %q, want %q", got, tc.want)
}
})
}
})
}
type stubFS struct {
typeName string
}
func (s stubFS) Valid() bool { return false }
func (s stubFS) Path() *container.Absolute { panic("unreachable") }
func (s stubFS) Host() []*container.Absolute { panic("unreachable") }
func (s stubFS) Apply(*hst.ApplyState) { panic("unreachable") }
func (s stubFS) String() string { return "<invalid " + s.typeName + ">" }
type sCheck struct {
FS hst.FilesystemConfigJSON `json:"fs"`
Magic int `json:"magic"`
}
type fsTestCase struct {
name string
fs hst.FilesystemConfig
valid bool
ops container.Ops
path *container.Absolute
host []*container.Absolute
str string
}
func checkFs(t *testing.T, testCases []fsTestCase) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("valid", func(t *testing.T) {
if got := tc.fs.Valid(); got != tc.valid {
t.Errorf("Valid: %v, want %v", got, tc.valid)
}
})
t.Run("ops", func(t *testing.T) {
ops := new(container.Ops)
tc.fs.Apply(&hst.ApplyState{AutoEtcPrefix: ":3", Ops: ops})
if !reflect.DeepEqual(ops, &tc.ops) {
gotString := new(strings.Builder)
for _, op := range *ops {
gotString.WriteString("\n" + op.String())
}
wantString := new(strings.Builder)
for _, op := range tc.ops {
wantString.WriteString("\n" + op.String())
}
t.Errorf("Apply: %s, want %s", gotString, wantString)
}
})
t.Run("path", func(t *testing.T) {
if got := tc.fs.Path(); !reflect.DeepEqual(got, tc.path) {
t.Errorf("Target: %q, want %q", got, tc.path)
}
})
t.Run("host", func(t *testing.T) {
if got := tc.fs.Host(); !reflect.DeepEqual(got, tc.host) {
t.Errorf("Host: %q, want %q", got, tc.host)
}
})
t.Run("string", func(t *testing.T) {
if tc.str == "\x00" {
return
}
if got := tc.fs.String(); got != tc.str {
t.Errorf("String: %q, want %q", got, tc.str)
}
})
})
}
}
func m(pathname string) *container.Absolute { return container.MustAbs(pathname) }
func ms(pathnames ...string) []*container.Absolute {
as := make([]*container.Absolute, len(pathnames))
for i, pathname := range pathnames {
as[i] = container.MustAbs(pathname)
}
return as
}

176
hst/fsbind.go Normal file
View File

@ -0,0 +1,176 @@
package hst
import (
"encoding/gob"
"strings"
"hakurei.app/container"
)
func init() { gob.Register(new(FSBind)) }
// FilesystemBind is the type string of a bind mount point.
const FilesystemBind = "bind"
// FSBind represents a host to container bind mount.
type FSBind struct {
// mount point in container, same as Source if empty
Target *container.Absolute `json:"dst,omitempty"`
// host filesystem path to make available to the container
Source *container.Absolute `json:"src"`
// do not mount Target read-only
Write bool `json:"write,omitempty"`
// do not disable device files on Target, implies Write
Device bool `json:"dev,omitempty"`
// create Source as a directory if it does not exist
Ensure bool `json:"ensure,omitempty"`
// skip this mount point if Source does not exist
Optional bool `json:"optional,omitempty"`
// enable special behaviour:
// for autoroot, Target must be set to [container.AbsFHSRoot];
// for autoetc, Target must be set to [container.AbsFHSEtc]
Special bool `json:"special,omitempty"`
}
// IsAutoRoot returns whether this FSBind has autoroot behaviour enabled.
func (b *FSBind) IsAutoRoot() bool {
return b.Valid() && b.Special && b.Target.String() == container.FHSRoot
}
// IsAutoEtc returns whether this FSBind has autoetc behaviour enabled.
func (b *FSBind) IsAutoEtc() bool {
return b.Valid() && b.Special && b.Target.String() == container.FHSEtc
}
func (b *FSBind) Valid() bool {
if b == nil || b.Source == nil {
return false
}
if b.Ensure && b.Optional {
return false
}
if b.Special {
if b.Target == nil {
return false
} else {
switch b.Target.String() {
case container.FHSRoot, container.FHSEtc:
break
default:
return false
}
}
}
return true
}
func (b *FSBind) Path() *container.Absolute {
if !b.Valid() {
return nil
}
if b.Target == nil {
return b.Source
}
return b.Target
}
func (b *FSBind) Host() []*container.Absolute {
if !b.Valid() {
return nil
}
return []*container.Absolute{b.Source}
}
func (b *FSBind) Apply(z *ApplyState) {
if !b.Valid() {
return
}
target := b.Target
if target == nil {
target = b.Source
}
var flags int
if b.Write {
flags |= container.BindWritable
}
if b.Device {
flags |= container.BindDevice | container.BindWritable
}
if b.Ensure {
flags |= container.BindEnsure
}
if b.Optional {
flags |= container.BindOptional
}
switch {
case b.IsAutoRoot():
z.Root(b.Source, flags)
case b.IsAutoEtc():
z.Etc(b.Source, z.AutoEtcPrefix)
default:
z.Bind(b.Source, target, flags)
}
}
func (b *FSBind) String() string {
if !b.Valid() {
return "<invalid>"
}
var flagSym string
if b.Device {
flagSym = "d"
} else if b.Write {
flagSym = "w"
}
if b.Special {
switch {
case b.IsAutoRoot():
prefix := "autoroot"
if flagSym != "" {
prefix += ":" + flagSym
}
if b.Source.String() != container.FHSRoot {
return prefix + ":" + b.Source.String()
}
return prefix
case b.IsAutoEtc():
return "autoetc:" + b.Source.String()
}
}
g := 4 + len(b.Source.String())
if b.Target != nil {
g += len(b.Target.String())
}
expr := new(strings.Builder)
expr.Grow(g)
expr.WriteString(flagSym)
switch {
case b.Ensure:
expr.WriteString("-")
case b.Optional:
expr.WriteString("+")
default:
expr.WriteString("*")
}
expr.WriteString(b.Source.String())
if b.Target != nil {
expr.WriteString(":" + b.Target.String())
}
return expr.String()
}

121
hst/fsbind_test.go Normal file
View File

@ -0,0 +1,121 @@
package hst_test
import (
"testing"
"hakurei.app/container"
"hakurei.app/hst"
)
func TestFSBind(t *testing.T) {
checkFs(t, []fsTestCase{
{"nil", (*hst.FSBind)(nil), false, nil, nil, nil, "<invalid>"},
{"ensure optional", &hst.FSBind{Source: m("/"), Ensure: true, Optional: true},
false, nil, nil, nil, "<invalid>"},
{"full", &hst.FSBind{
Target: m("/dev"),
Source: m("/mnt/dev"),
Optional: true,
Device: true,
}, true, container.Ops{&container.BindMountOp{
Source: m("/mnt/dev"),
Target: m("/dev"),
Flags: container.BindWritable | container.BindDevice | container.BindOptional,
}}, m("/dev"), ms("/mnt/dev"),
"d+/mnt/dev:/dev"},
{"full ensure", &hst.FSBind{
Target: m("/dev"),
Source: m("/mnt/dev"),
Ensure: true,
Device: true,
}, true, container.Ops{&container.BindMountOp{
Source: m("/mnt/dev"),
Target: m("/dev"),
Flags: container.BindWritable | container.BindDevice | container.BindEnsure,
}}, m("/dev"), ms("/mnt/dev"),
"d-/mnt/dev:/dev"},
{"full write dev", &hst.FSBind{
Target: m("/dev"),
Source: m("/mnt/dev"),
Write: true,
Device: true,
}, true, container.Ops{&container.BindMountOp{
Source: m("/mnt/dev"),
Target: m("/dev"),
Flags: container.BindWritable | container.BindDevice,
}}, m("/dev"), ms("/mnt/dev"),
"d*/mnt/dev:/dev"},
{"full write", &hst.FSBind{
Target: m("/tmp"),
Source: m("/mnt/tmp"),
Write: true,
}, true, container.Ops{&container.BindMountOp{
Source: m("/mnt/tmp"),
Target: m("/tmp"),
Flags: container.BindWritable,
}}, m("/tmp"), ms("/mnt/tmp"),
"w*/mnt/tmp:/tmp"},
{"full no flags", &hst.FSBind{
Target: m("/etc"),
Source: m("/mnt/etc"),
}, true, container.Ops{&container.BindMountOp{
Source: m("/mnt/etc"),
Target: m("/etc"),
}}, m("/etc"), ms("/mnt/etc"),
"*/mnt/etc:/etc"},
{"nil dst", &hst.FSBind{
Source: m("/"),
}, true, container.Ops{&container.BindMountOp{
Source: m("/"),
Target: m("/"),
}}, m("/"), ms("/"),
"*/"},
{"special nil target", &hst.FSBind{
Source: m("/"),
Special: true,
}, false, nil, nil, nil, "<invalid>"},
{"special bad target", &hst.FSBind{
Source: m("/"),
Target: m("/var/"),
Special: true,
}, false, nil, nil, nil, "<invalid>"},
{"autoroot pd", &hst.FSBind{
Target: m("/"),
Source: m("/"),
Write: true,
Special: true,
}, true, container.Ops{&container.AutoRootOp{
Host: m("/"),
Flags: container.BindWritable,
}}, m("/"), ms("/"), "autoroot:w"},
{"autoroot silly", &hst.FSBind{
Target: m("/"),
Source: m("/etc"),
Write: true,
Special: true,
}, true, container.Ops{&container.AutoRootOp{
Host: m("/etc"),
Flags: container.BindWritable,
}}, m("/"), ms("/etc"), "autoroot:w:/etc"},
{"autoetc", &hst.FSBind{
Target: m("/etc/"),
Source: m("/etc/"),
Special: true,
}, true, container.Ops{
&container.MkdirOp{Path: m("/etc/"), Perm: 0755},
&container.BindMountOp{Source: m("/etc/"), Target: m("/etc/.host/:3")},
&container.AutoEtcOp{Prefix: ":3"},
}, m("/etc/"), ms("/etc/"), "autoetc:/etc/"},
})
}

83
hst/fsephemeral.go Normal file
View File

@ -0,0 +1,83 @@
package hst
import (
"encoding/gob"
"os"
"strings"
"hakurei.app/container"
)
func init() { gob.Register(new(FSEphemeral)) }
// FilesystemEphemeral is the type string of a mount point with ephemeral state.
const FilesystemEphemeral = "ephemeral"
// FSEphemeral represents an ephemeral container mount point.
type FSEphemeral struct {
// mount point in container
Target *container.Absolute `json:"dst,omitempty"`
// do not mount filesystem read-only
Write bool `json:"write,omitempty"`
// upper limit on the size of the filesystem
Size int `json:"size,omitempty"`
// initial permission bits of the new filesystem
Perm os.FileMode `json:"perm,omitempty"`
}
func (e *FSEphemeral) Valid() bool { return e != nil && e.Target != nil }
func (e *FSEphemeral) Path() *container.Absolute {
if !e.Valid() {
return nil
}
return e.Target
}
func (e *FSEphemeral) Host() []*container.Absolute { return nil }
const fsEphemeralDefaultPerm = os.FileMode(0755)
func (e *FSEphemeral) Apply(z *ApplyState) {
if !e.Valid() {
return
}
size := e.Size
if size < 0 {
size = 0
}
perm := e.Perm
if perm == 0 {
perm = fsEphemeralDefaultPerm
}
if e.Write {
z.Tmpfs(e.Target, size, perm)
} else {
z.Readonly(e.Target, perm)
}
}
func (e *FSEphemeral) String() string {
if !e.Valid() {
return "<invalid>"
}
expr := new(strings.Builder)
expr.Grow(15 + len(FilesystemEphemeral) + len(e.Target.String()))
if e.Write {
expr.WriteString("w")
}
expr.WriteString("+" + FilesystemEphemeral + "(")
if e.Perm != 0 {
expr.WriteString(e.Perm.String())
} else {
expr.WriteString(fsEphemeralDefaultPerm.String())
}
expr.WriteString("):" + e.Target.String())
return expr.String()
}

50
hst/fsephemeral_test.go Normal file
View File

@ -0,0 +1,50 @@
package hst_test
import (
"syscall"
"testing"
"hakurei.app/container"
"hakurei.app/hst"
)
func TestFSEphemeral(t *testing.T) {
checkFs(t, []fsTestCase{
{"nil", (*hst.FSEphemeral)(nil), false, nil, nil, nil, "<invalid>"},
{"full", &hst.FSEphemeral{
Target: m("/run/user/65534"),
Write: true,
Size: 1 << 10,
Perm: 0700,
}, true, container.Ops{&container.MountTmpfsOp{
FSName: "ephemeral",
Path: m("/run/user/65534"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Size: 1 << 10,
Perm: 0700,
}}, m("/run/user/65534"), nil,
"w+ephemeral(-rwx------):/run/user/65534"},
{"cover ro", &hst.FSEphemeral{Target: m("/run/nscd")}, true,
container.Ops{&container.MountTmpfsOp{
FSName: "readonly",
Path: m("/run/nscd"),
Flags: syscall.MS_NOSUID | syscall.MS_NODEV | syscall.MS_RDONLY,
Perm: 0755,
}}, m("/run/nscd"), nil,
"+ephemeral(-rwxr-xr-x):/run/nscd"},
{"negative size", &hst.FSEphemeral{
Target: hst.AbsTmp,
Write: true,
Size: -1,
}, true, container.Ops{&container.MountTmpfsOp{
FSName: "ephemeral",
Path: hst.AbsTmp,
Flags: syscall.MS_NOSUID | syscall.MS_NODEV,
Perm: 0755,
}}, hst.AbsTmp, nil,
"w+ephemeral(-rwxr-xr-x):/.hakurei"},
})
}

61
hst/fslink.go Normal file
View File

@ -0,0 +1,61 @@
package hst
import (
"encoding/gob"
"path"
"hakurei.app/container"
)
func init() { gob.Register(new(FSLink)) }
// FilesystemLink is the type string of a symbolic link.
const FilesystemLink = "link"
// FSLink represents a symlink in the container filesystem.
type FSLink struct {
// link path in container
Target *container.Absolute `json:"dst"`
// linkname the symlink points to
Linkname string `json:"linkname"`
// whether to dereference linkname before creating the link
Dereference bool `json:"dereference,omitempty"`
}
func (l *FSLink) Valid() bool {
if l == nil || l.Target == nil || l.Linkname == "" {
return false
}
return !l.Dereference || path.IsAbs(l.Linkname)
}
func (l *FSLink) Path() *container.Absolute {
if !l.Valid() {
return nil
}
return l.Target
}
func (l *FSLink) Host() []*container.Absolute { return nil }
func (l *FSLink) Apply(z *ApplyState) {
if !l.Valid() {
return
}
z.Link(l.Target, l.Linkname, l.Dereference)
}
func (l *FSLink) String() string {
if !l.Valid() {
return "<invalid>"
}
var dereference string
if l.Dereference {
if l.Target.String() == l.Linkname {
return l.Target.String() + "@"
}
dereference = "*"
}
return l.Target.String() + " -> " + dereference + l.Linkname
}

62
hst/fslink_test.go Normal file
View File

@ -0,0 +1,62 @@
package hst_test
import (
"testing"
"hakurei.app/container"
"hakurei.app/hst"
)
func TestFSLink(t *testing.T) {
checkFs(t, []fsTestCase{
{"nil", (*hst.FSLink)(nil), false, nil, nil, nil, "<invalid>"},
{"zero", new(hst.FSLink), false, nil, nil, nil, "<invalid>"},
{"deref rel", &hst.FSLink{Target: m("/"), Linkname: ":3", Dereference: true},
false, nil, nil, nil, "<invalid>"},
{"deref differs", &hst.FSLink{
Target: m("/.hakurei/etc"),
Linkname: "/etc/static",
Dereference: true,
}, true, container.Ops{
&container.SymlinkOp{
Target: m("/.hakurei/etc"),
LinkName: "/etc/static",
Dereference: true,
},
}, m("/.hakurei/etc"), nil,
"/.hakurei/etc -> */etc/static"},
{"deref", &hst.FSLink{
Target: m("/run/current-system"),
Linkname: "/run/current-system",
Dereference: true,
}, true, container.Ops{
&container.SymlinkOp{
Target: m("/run/current-system"),
LinkName: "/run/current-system",
Dereference: true,
},
}, m("/run/current-system"), nil,
"/run/current-system@"},
{"direct", &hst.FSLink{
Target: m("/etc/mtab"),
Linkname: "/proc/mounts",
}, true, container.Ops{
&container.SymlinkOp{
Target: m("/etc/mtab"),
LinkName: "/proc/mounts",
},
}, m("/etc/mtab"), nil, "/etc/mtab -> /proc/mounts"},
{"direct rel", &hst.FSLink{
Target: m("/etc/mtab"),
Linkname: "../proc/mounts",
}, true, container.Ops{
&container.SymlinkOp{
Target: m("/etc/mtab"),
LinkName: "../proc/mounts",
},
}, m("/etc/mtab"), nil, "/etc/mtab -> ../proc/mounts"},
})
}

98
hst/fsoverlay.go Normal file
View File

@ -0,0 +1,98 @@
package hst
import (
"encoding/gob"
"strings"
"hakurei.app/container"
)
func init() { gob.Register(new(FSOverlay)) }
// FilesystemOverlay is the type string of an overlay mount point.
const FilesystemOverlay = "overlay"
// FSOverlay represents an overlay mount point.
type FSOverlay struct {
// mount point in container
Target *container.Absolute `json:"dst"`
// any filesystem, does not need to be on a writable filesystem, must not be nil
Lower []*container.Absolute `json:"lower"`
// the upperdir is normally on a writable filesystem, leave as nil to mount Lower readonly
Upper *container.Absolute `json:"upper,omitempty"`
// the workdir needs to be an empty directory on the same filesystem as Upper, must not be nil if Upper is populated
Work *container.Absolute `json:"work,omitempty"`
}
func (o *FSOverlay) Valid() bool {
if o == nil || o.Target == nil {
return false
}
for _, a := range o.Lower {
if a == nil {
return false
}
}
if o.Upper != nil { // rw
return o.Work != nil && len(o.Lower) > 0
} else { // ro
return len(o.Lower) >= 2
}
}
func (o *FSOverlay) Path() *container.Absolute {
if !o.Valid() {
return nil
}
return o.Target
}
func (o *FSOverlay) Host() []*container.Absolute {
if !o.Valid() {
return nil
}
p := make([]*container.Absolute, 0, 2+len(o.Lower))
if o.Upper != nil && o.Work != nil {
p = append(p, o.Upper, o.Work)
}
p = append(p, o.Lower...)
return p
}
func (o *FSOverlay) Apply(z *ApplyState) {
if !o.Valid() {
return
}
if o.Upper != nil && o.Work != nil { // rw
z.Overlay(o.Target, o.Upper, o.Work, o.Lower...)
} else { // ro
z.OverlayReadonly(o.Target, o.Lower...)
}
}
func (o *FSOverlay) String() string {
if !o.Valid() {
return "<invalid>"
}
lower := make([]string, len(o.Lower))
for i, a := range o.Lower {
lower[i] = container.EscapeOverlayDataSegment(a.String())
}
if o.Upper != nil && o.Work != nil {
return "w*" + strings.Join(append([]string{
container.EscapeOverlayDataSegment(o.Target.String()),
container.EscapeOverlayDataSegment(o.Upper.String()),
container.EscapeOverlayDataSegment(o.Work.String())},
lower...), container.SpecialOverlayPath)
} else {
return "*" + strings.Join(append([]string{
container.EscapeOverlayDataSegment(o.Target.String())},
lower...), container.SpecialOverlayPath)
}
}

50
hst/fsoverlay_test.go Normal file
View File

@ -0,0 +1,50 @@
package hst_test
import (
"testing"
"hakurei.app/container"
"hakurei.app/hst"
)
func TestFSOverlay(t *testing.T) {
checkFs(t, []fsTestCase{
{"nil", (*hst.FSOverlay)(nil), false, nil, nil, nil, "<invalid>"},
{"nil lower", &hst.FSOverlay{Target: m("/etc"), Lower: []*container.Absolute{nil}}, false, nil, nil, nil, "<invalid>"},
{"zero lower", &hst.FSOverlay{Target: m("/etc"), Upper: m("/"), Work: m("/")}, false, nil, nil, nil, "<invalid>"},
{"zero lower ro", &hst.FSOverlay{Target: m("/etc")}, false, nil, nil, nil, "<invalid>"},
{"short lower", &hst.FSOverlay{Target: m("/etc"), Lower: ms("/etc")}, false, nil, nil, nil, "<invalid>"},
{"full", &hst.FSOverlay{
Target: m("/nix/store"),
Lower: ms("/mnt-root/nix/.ro-store"),
Upper: m("/mnt-root/nix/.rw-store/upper"),
Work: m("/mnt-root/nix/.rw-store/work"),
}, true, container.Ops{&container.MountOverlayOp{
Target: m("/nix/store"),
Lower: ms("/mnt-root/nix/.ro-store"),
Upper: m("/mnt-root/nix/.rw-store/upper"),
Work: m("/mnt-root/nix/.rw-store/work"),
}}, m("/nix/store"), ms("/mnt-root/nix/.rw-store/upper", "/mnt-root/nix/.rw-store/work", "/mnt-root/nix/.ro-store"),
"w*/nix/store:/mnt-root/nix/.rw-store/upper:/mnt-root/nix/.rw-store/work:/mnt-root/nix/.ro-store"},
{"ro", &hst.FSOverlay{
Target: m("/mnt/src"),
Lower: ms("/tmp/.src0", "/tmp/.src1"),
}, true, container.Ops{&container.MountOverlayOp{
Target: m("/mnt/src"),
Lower: ms("/tmp/.src0", "/tmp/.src1"),
}}, m("/mnt/src"), ms("/tmp/.src0", "/tmp/.src1"),
"*/mnt/src:/tmp/.src0:/tmp/.src1"},
{"ro work", &hst.FSOverlay{
Target: m("/mnt/src"),
Lower: ms("/tmp/.src0", "/tmp/.src1"),
Work: m("/tmp"),
}, true, container.Ops{&container.MountOverlayOp{
Target: m("/mnt/src"),
Lower: ms("/tmp/.src0", "/tmp/.src1"),
}}, m("/mnt/src"), ms("/tmp/.src0", "/tmp/.src1"),
"*/mnt/src:/tmp/.src0:/tmp/.src1"},
})
}

119
hst/hst.go Normal file
View File

@ -0,0 +1,119 @@
// Package hst exports stable shared types for interacting with hakurei.
package hst
import (
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/system"
"hakurei.app/system/dbus"
)
// Paths contains environment-dependent paths used by hakurei.
type Paths struct {
// temporary directory returned by [os.TempDir] (usually `/tmp`)
TempDir *container.Absolute `json:"temp_dir"`
// path to shared directory (usually `/tmp/hakurei.%d`, [Info.User])
SharePath *container.Absolute `json:"share_path"`
// XDG_RUNTIME_DIR value (usually `/run/user/%d`, uid)
RuntimePath *container.Absolute `json:"runtime_path"`
// application runtime directory (usually `/run/user/%d/hakurei`)
RunDirPath *container.Absolute `json:"run_dir_path"`
}
type Info struct {
// User is the userid according to hsu.
User int `json:"user"`
Paths
}
// Template returns a fully populated instance of Config.
func Template() *Config {
return &Config{
ID: "org.chromium.Chromium",
Path: container.AbsFHSRun.Append("current-system/sw/bin/chromium"),
Args: []string{
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland",
},
Enablements: NewEnablements(system.EWayland | system.EDBus | system.EPulse),
SessionBus: &dbus.Config{
See: nil,
Talk: []string{"org.freedesktop.Notifications", "org.freedesktop.FileManager1", "org.freedesktop.ScreenSaver",
"org.freedesktop.secrets", "org.kde.kwalletd5", "org.kde.kwalletd6", "org.gnome.SessionManager"},
Own: []string{"org.chromium.Chromium.*", "org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"},
Call: map[string]string{"org.freedesktop.portal.*": "*"},
Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"},
Log: false,
Filter: true,
},
SystemBus: &dbus.Config{
See: nil,
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
Own: nil,
Call: nil,
Broadcast: nil,
Log: false,
Filter: true,
},
DirectWayland: false,
Username: "chronos",
Shell: container.AbsFHSRun.Append("current-system/sw/bin/zsh"),
Home: container.MustAbs("/data/data/org.chromium.Chromium"),
ExtraPerms: []*ExtraPermConfig{
{Path: container.AbsFHSVarLib.Append("hakurei/u0"), Ensure: true, Execute: true},
{Path: container.AbsFHSVarLib.Append("hakurei/u0/org.chromium.Chromium"), Read: true, Write: true, Execute: true},
},
Identity: 9,
Groups: []string{"video", "dialout", "plugdev"},
Container: &ContainerConfig{
Hostname: "localhost",
Devel: true,
Userns: true,
HostNet: true,
HostAbstract: true,
Device: true,
WaitDelay: -1,
SeccompFlags: seccomp.AllowMultiarch,
SeccompPresets: seccomp.PresetExt,
SeccompCompat: true,
Tty: true,
Multiarch: true,
MapRealUID: true,
// example API credentials pulled from Google Chrome
// DO NOT USE THESE IN A REAL BROWSER
Env: map[string]string{
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT",
},
Filesystem: []FilesystemConfigJSON{
{&FSBind{Target: container.AbsFHSRoot, Source: container.AbsFHSVarLib.Append("hakurei/base/org.debian"), Write: true, Special: true}},
{&FSBind{Target: container.AbsFHSEtc, Source: container.AbsFHSEtc, Special: true}},
{&FSEphemeral{Target: container.AbsFHSTmp, Write: true, Perm: 0755}},
{&FSOverlay{
Target: container.MustAbs("/nix/store"),
Lower: []*container.Absolute{container.MustAbs("/mnt-root/nix/.ro-store")},
Upper: container.MustAbs("/mnt-root/nix/.rw-store/upper"),
Work: container.MustAbs("/mnt-root/nix/.rw-store/work"),
}},
{&FSBind{Source: container.MustAbs("/nix/store")}},
{&FSLink{Target: container.AbsFHSRun.Append("current-system"), Linkname: "/run/current-system", Dereference: true}},
{&FSLink{Target: container.AbsFHSRun.Append("opengl-driver"), Linkname: "/run/opengl-driver", Dereference: true}},
{&FSBind{Source: container.AbsFHSVarLib.Append("hakurei/u0/org.chromium.Chromium"),
Target: container.MustAbs("/data/data/org.chromium.Chromium"), Write: true, Ensure: true}},
{&FSBind{Source: container.AbsFHSDev.Append("dri"), Device: true, Optional: true}},
},
},
}
}

View File

@ -18,7 +18,11 @@ func TestTemplate(t *testing.T) {
"--enable-features=UseOzonePlatform", "--enable-features=UseOzonePlatform",
"--ozone-platform=wayland" "--ozone-platform=wayland"
], ],
"enablements": 13, "enablements": {
"wayland": true,
"dbus": true,
"pulse": true
},
"session_bus": { "session_bus": {
"see": null, "see": null,
"talk": [ "talk": [
@ -57,8 +61,7 @@ func TestTemplate(t *testing.T) {
}, },
"username": "chronos", "username": "chronos",
"shell": "/run/current-system/sw/bin/zsh", "shell": "/run/current-system/sw/bin/zsh",
"data": "/var/lib/hakurei/u0/org.chromium.Chromium", "home": "/data/data/org.chromium.Chromium",
"dir": "/data/data/org.chromium.Chromium",
"extra_perms": [ "extra_perms": [
{ {
"ensure": true, "ensure": true,
@ -86,7 +89,8 @@ func TestTemplate(t *testing.T) {
"seccomp_compat": true, "seccomp_compat": true,
"devel": true, "devel": true,
"userns": true, "userns": true,
"net": true, "host_net": true,
"host_abstract": true,
"tty": true, "tty": true,
"multiarch": true, "multiarch": true,
"env": { "env": {
@ -98,38 +102,62 @@ func TestTemplate(t *testing.T) {
"device": true, "device": true,
"filesystem": [ "filesystem": [
{ {
"type": "bind",
"dst": "/",
"src": "/var/lib/hakurei/base/org.debian",
"write": true,
"special": true
},
{
"type": "bind",
"dst": "/etc/",
"src": "/etc/",
"special": true
},
{
"type": "ephemeral",
"dst": "/tmp/",
"write": true,
"perm": 493
},
{
"type": "overlay",
"dst": "/nix/store",
"lower": [
"/mnt-root/nix/.ro-store"
],
"upper": "/mnt-root/nix/.rw-store/upper",
"work": "/mnt-root/nix/.rw-store/work"
},
{
"type": "bind",
"src": "/nix/store" "src": "/nix/store"
}, },
{ {
"src": "/run/current-system" "type": "link",
"dst": "/run/current-system",
"linkname": "/run/current-system",
"dereference": true
}, },
{ {
"src": "/run/opengl-driver" "type": "link",
}, "dst": "/run/opengl-driver",
{ "linkname": "/run/opengl-driver",
"src": "/var/db/nix-channels" "dereference": true
}, },
{ {
"type": "bind",
"dst": "/data/data/org.chromium.Chromium", "dst": "/data/data/org.chromium.Chromium",
"src": "/var/lib/hakurei/u0/org.chromium.Chromium", "src": "/var/lib/hakurei/u0/org.chromium.Chromium",
"write": true, "write": true,
"require": true "ensure": true
}, },
{ {
"type": "bind",
"src": "/dev/dri", "src": "/dev/dri",
"dev": true "dev": true,
"optional": true
} }
],
"symlink": [
[
"/run/user/65534",
"/run/user/150"
]
],
"etc": "/etc",
"auto_etc": true,
"cover": [
"/var/run/nscd"
] ]
} }
}` }`

View File

@ -1,5 +0,0 @@
package hst
type Info struct {
User int `json:"user"`
}

View File

@ -1,11 +0,0 @@
package hst
// Paths contains environment-dependent paths used by hakurei.
type Paths struct {
// path to shared directory (usually `/tmp/hakurei.%d`)
SharePath string `json:"share_path"`
// XDG_RUNTIME_DIR value (usually `/run/user/%d`)
RuntimePath string `json:"runtime_path"`
// application runtime directory (usually `/run/user/%d/hakurei`)
RunDirPath string `json:"run_dir_path"`
}

View File

@ -1,94 +0,0 @@
package hst
import (
"hakurei.app/container/seccomp"
"hakurei.app/system"
"hakurei.app/system/dbus"
)
// Template returns a fully populated instance of Config.
func Template() *Config {
return &Config{
ID: "org.chromium.Chromium",
Path: "/run/current-system/sw/bin/chromium",
Args: []string{
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland",
},
Enablements: system.EWayland | system.EDBus | system.EPulse,
SessionBus: &dbus.Config{
See: nil,
Talk: []string{"org.freedesktop.Notifications", "org.freedesktop.FileManager1", "org.freedesktop.ScreenSaver",
"org.freedesktop.secrets", "org.kde.kwalletd5", "org.kde.kwalletd6", "org.gnome.SessionManager"},
Own: []string{"org.chromium.Chromium.*", "org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"},
Call: map[string]string{"org.freedesktop.portal.*": "*"},
Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"},
Log: false,
Filter: true,
},
SystemBus: &dbus.Config{
See: nil,
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
Own: nil,
Call: nil,
Broadcast: nil,
Log: false,
Filter: true,
},
DirectWayland: false,
Username: "chronos",
Shell: "/run/current-system/sw/bin/zsh",
Data: "/var/lib/hakurei/u0/org.chromium.Chromium",
Dir: "/data/data/org.chromium.Chromium",
ExtraPerms: []*ExtraPermConfig{
{Path: "/var/lib/hakurei/u0", Ensure: true, Execute: true},
{Path: "/var/lib/hakurei/u0/org.chromium.Chromium", Read: true, Write: true, Execute: true},
},
Identity: 9,
Groups: []string{"video", "dialout", "plugdev"},
Container: &ContainerConfig{
Hostname: "localhost",
Devel: true,
Userns: true,
Net: true,
Device: true,
WaitDelay: -1,
SeccompFlags: seccomp.AllowMultiarch,
SeccompPresets: seccomp.PresetExt,
SeccompCompat: true,
Tty: true,
Multiarch: true,
MapRealUID: true,
// example API credentials pulled from Google Chrome
// DO NOT USE THESE IN A REAL BROWSER
Env: map[string]string{
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT",
},
Filesystem: []*FilesystemConfig{
{Src: "/nix/store"},
{Src: "/run/current-system"},
{Src: "/run/opengl-driver"},
{Src: "/var/db/nix-channels"},
{Src: "/var/lib/hakurei/u0/org.chromium.Chromium",
Dst: "/data/data/org.chromium.Chromium", Write: true, Must: true},
{Src: "/dev/dri", Device: true},
},
Link: [][2]string{{"/run/user/65534", "/run/user/150"}},
Etc: "/etc",
AutoEtc: true,
Cover: []string{"/var/run/nscd"},
},
}
}

View File

@ -7,7 +7,6 @@ import (
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/app/state" "hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/internal/sys" "hakurei.app/internal/sys"
) )
@ -59,10 +58,6 @@ func (a *app) Seal(config *hst.Config) (SealedApp, error) {
if a.outcome != nil { if a.outcome != nil {
panic("app sealed twice") panic("app sealed twice")
} }
if config == nil {
return nil, hlog.WrapErr(ErrConfig,
"attempted to seal app with nil config")
}
seal := new(outcome) seal := new(outcome)
seal.id = a.id seal.id = a.id

View File

@ -11,6 +11,7 @@ import (
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/app" "hakurei.app/internal/app"
"hakurei.app/internal/app/state" "hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/internal/sys" "hakurei.app/internal/sys"
"hakurei.app/system" "hakurei.app/system"
) )
@ -36,6 +37,7 @@ func TestApp(t *testing.T) {
) )
if !t.Run("seal", func(t *testing.T) { if !t.Run("seal", func(t *testing.T) {
if sa, err := a.Seal(tc.config); err != nil { if sa, err := a.Seal(tc.config); err != nil {
hlog.PrintBaseError(err, "got generic error:")
t.Errorf("Seal: error = %v", err) t.Errorf("Seal: error = %v", err)
return return
} else { } else {

View File

@ -1,6 +1,8 @@
package app_test package app_test
import ( import (
"syscall"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/hst" "hakurei.app/hst"
@ -10,23 +12,37 @@ import (
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
) )
func m(pathname string) *container.Absolute { return container.MustAbs(pathname) }
func f(c hst.FilesystemConfig) hst.FilesystemConfigJSON {
return hst.FilesystemConfigJSON{FilesystemConfig: c}
}
var testCasesNixos = []sealTestCase{ var testCasesNixos = []sealTestCase{
{ {
"nixos chromium direct wayland", new(stubNixOS), "nixos chromium direct wayland", new(stubNixOS),
&hst.Config{ &hst.Config{
ID: "org.chromium.Chromium", ID: "org.chromium.Chromium",
Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start", Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"),
Enablements: system.EWayland | system.EDBus | system.EPulse, Enablements: hst.NewEnablements(system.EWayland | system.EDBus | system.EPulse),
Shell: m("/run/current-system/sw/bin/zsh"),
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Userns: true, Net: true, MapRealUID: true, Env: nil, AutoEtc: true, Userns: true, HostNet: true, MapRealUID: true, Env: nil,
Filesystem: []*hst.FilesystemConfig{ Filesystem: []hst.FilesystemConfigJSON{
{Src: "/bin", Must: true}, {Src: "/usr/bin", Must: true}, f(&hst.FSBind{Source: m("/bin")}),
{Src: "/nix/store", Must: true}, {Src: "/run/current-system", Must: true}, f(&hst.FSBind{Source: m("/usr/bin/")}),
{Src: "/sys/block"}, {Src: "/sys/bus"}, {Src: "/sys/class"}, {Src: "/sys/dev"}, {Src: "/sys/devices"}, f(&hst.FSBind{Source: m("/nix/store")}),
{Src: "/run/opengl-driver", Must: true}, {Src: "/dev/dri", Device: true}, f(&hst.FSBind{Source: m("/run/current-system")}),
f(&hst.FSBind{Source: m("/sys/block"), Optional: true}),
f(&hst.FSBind{Source: m("/sys/bus"), Optional: true}),
f(&hst.FSBind{Source: m("/sys/class"), Optional: true}),
f(&hst.FSBind{Source: m("/sys/dev"), Optional: true}),
f(&hst.FSBind{Source: m("/sys/devices"), Optional: true}),
f(&hst.FSBind{Source: m("/run/opengl-driver")}),
f(&hst.FSBind{Source: m("/dev/dri"), Device: true, Optional: true}),
f(&hst.FSBind{Source: m("/etc/"), Target: m("/etc/"), Special: true}),
f(&hst.FSBind{Source: m("/var/lib/persist/module/hakurei/0/1"), Write: true, Ensure: true}),
}, },
Cover: []string{"/var/run/nscd"},
}, },
SystemBus: &dbus.Config{ SystemBus: &dbus.Config{
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"}, Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
@ -49,7 +65,7 @@ var testCasesNixos = []sealTestCase{
DirectWayland: true, DirectWayland: true,
Username: "u0_a1", Username: "u0_a1",
Data: "/var/lib/persist/module/hakurei/0/1", Home: m("/var/lib/persist/module/hakurei/0/1"),
Identity: 1, Groups: []string{}, Identity: 1, Groups: []string{},
}, },
state.ID{ state.ID{
@ -97,8 +113,8 @@ var testCasesNixos = []sealTestCase{
&container.Params{ &container.Params{
Uid: 1971, Uid: 1971,
Gid: 100, Gid: 100,
Dir: "/var/lib/persist/module/hakurei/0/1", Dir: m("/var/lib/persist/module/hakurei/0/1"),
Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start", Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"),
Args: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"}, Args: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
Env: []string{ Env: []string{
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus", "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus",
@ -115,33 +131,35 @@ var testCasesNixos = []sealTestCase{
"XDG_SESSION_TYPE=tty", "XDG_SESSION_TYPE=tty",
}, },
Ops: new(container.Ops). Ops: new(container.Ops).
Proc("/proc"). Proc(m("/proc/")).
Tmpfs(hst.Tmp, 4096, 0755). Tmpfs(hst.AbsTmp, 4096, 0755).
Dev("/dev").Mqueue("/dev/mqueue"). DevWritable(m("/dev/"), true).
Bind("/bin", "/bin", 0). Bind(m("/bin"), m("/bin"), 0).
Bind("/usr/bin", "/usr/bin", 0). Bind(m("/usr/bin/"), m("/usr/bin/"), 0).
Bind("/nix/store", "/nix/store", 0). Bind(m("/nix/store"), m("/nix/store"), 0).
Bind("/run/current-system", "/run/current-system", 0). Bind(m("/run/current-system"), m("/run/current-system"), 0).
Bind("/sys/block", "/sys/block", container.BindOptional). Bind(m("/sys/block"), m("/sys/block"), container.BindOptional).
Bind("/sys/bus", "/sys/bus", container.BindOptional). Bind(m("/sys/bus"), m("/sys/bus"), container.BindOptional).
Bind("/sys/class", "/sys/class", container.BindOptional). Bind(m("/sys/class"), m("/sys/class"), container.BindOptional).
Bind("/sys/dev", "/sys/dev", container.BindOptional). Bind(m("/sys/dev"), m("/sys/dev"), container.BindOptional).
Bind("/sys/devices", "/sys/devices", container.BindOptional). Bind(m("/sys/devices"), m("/sys/devices"), container.BindOptional).
Bind("/run/opengl-driver", "/run/opengl-driver", 0). Bind(m("/run/opengl-driver"), m("/run/opengl-driver"), 0).
Bind("/dev/dri", "/dev/dri", container.BindDevice|container.BindWritable|container.BindOptional). Bind(m("/dev/dri"), m("/dev/dri"), container.BindDevice|container.BindWritable|container.BindOptional).
Etc("/etc", "8e2c76b066dabe574cf073bdb46eb5c1"). Etc(m("/etc/"), "8e2c76b066dabe574cf073bdb46eb5c1").
Tmpfs("/run/user", 4096, 0755). Bind(m("/var/lib/persist/module/hakurei/0/1"), m("/var/lib/persist/module/hakurei/0/1"), container.BindWritable|container.BindEnsure).
Bind("/tmp/hakurei.1971/runtime/1", "/run/user/1971", container.BindWritable). Remount(m("/dev/"), syscall.MS_RDONLY).
Bind("/tmp/hakurei.1971/tmpdir/1", "/tmp", container.BindWritable). Tmpfs(m("/dev/shm"), 0, 01777).
Bind("/var/lib/persist/module/hakurei/0/1", "/var/lib/persist/module/hakurei/0/1", container.BindWritable). Tmpfs(m("/run/user/"), 4096, 0755).
Place("/etc/passwd", []byte("u0_a1:x:1971:100:Hakurei:/var/lib/persist/module/hakurei/0/1:/run/current-system/sw/bin/zsh\n")). Bind(m("/tmp/hakurei.1971/runtime/1"), m("/run/user/1971"), container.BindWritable).
Place("/etc/group", []byte("hakurei:x:100:\n")). Bind(m("/tmp/hakurei.1971/tmpdir/1"), m("/tmp/"), container.BindWritable).
Bind("/run/user/1971/wayland-0", "/run/user/1971/wayland-0", 0). Place(m("/etc/passwd"), []byte("u0_a1:x:1971:100:Hakurei:/var/lib/persist/module/hakurei/0/1:/run/current-system/sw/bin/zsh\n")).
Bind("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native", 0). Place(m("/etc/group"), []byte("hakurei:x:100:\n")).
Place(hst.Tmp+"/pulse-cookie", nil). Bind(m("/run/user/1971/wayland-0"), m("/run/user/1971/wayland-0"), 0).
Bind("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus", 0). Bind(m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse"), m("/run/user/1971/pulse/native"), 0).
Bind("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket", 0). Place(m(hst.Tmp+"/pulse-cookie"), nil).
Tmpfs("/var/run/nscd", 8192, 0755), Bind(m("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus"), m("/run/user/1971/bus"), 0).
Bind(m("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), m("/run/dbus/system_bus_socket"), 0).
Remount(m("/"), syscall.MS_RDONLY),
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyTTY | seccomp.PresetDenyDevel, SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyTTY | seccomp.PresetDenyDevel,
HostNet: true, HostNet: true,
ForwardCancel: true, ForwardCancel: true,

View File

@ -2,6 +2,7 @@ package app_test
import ( import (
"os" "os"
"syscall"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
@ -15,7 +16,7 @@ import (
var testCasesPd = []sealTestCase{ var testCasesPd = []sealTestCase{
{ {
"nixos permissive defaults no enablements", new(stubNixOS), "nixos permissive defaults no enablements", new(stubNixOS),
&hst.Config{Username: "chronos", Data: "/home/chronos"}, &hst.Config{Username: "chronos", Home: m("/home/chronos")},
state.ID{ state.ID{
0x4a, 0x45, 0x0b, 0x65, 0x4a, 0x45, 0x0b, 0x65,
0x96, 0xd7, 0xbc, 0x15, 0x96, 0xd7, 0xbc, 0x15,
@ -29,8 +30,8 @@ var testCasesPd = []sealTestCase{
Ensure("/tmp/hakurei.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir", acl.Execute). Ensure("/tmp/hakurei.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir", acl.Execute).
Ensure("/tmp/hakurei.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute), Ensure("/tmp/hakurei.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute),
&container.Params{ &container.Params{
Dir: "/home/chronos", Dir: m("/home/chronos"),
Path: "/run/current-system/sw/bin/zsh", Path: m("/run/current-system/sw/bin/zsh"),
Args: []string{"/run/current-system/sw/bin/zsh"}, Args: []string{"/run/current-system/sw/bin/zsh"},
Env: []string{ Env: []string{
"HOME=/home/chronos", "HOME=/home/chronos",
@ -42,34 +43,26 @@ var testCasesPd = []sealTestCase{
"XDG_SESSION_TYPE=tty", "XDG_SESSION_TYPE=tty",
}, },
Ops: new(container.Ops). Ops: new(container.Ops).
Proc("/proc"). Root(m("/"), container.BindWritable).
Tmpfs(hst.Tmp, 4096, 0755). Proc(m("/proc/")).
Dev("/dev").Mqueue("/dev/mqueue"). Tmpfs(hst.AbsTmp, 4096, 0755).
Bind("/bin", "/bin", container.BindWritable). DevWritable(m("/dev/"), true).
Bind("/boot", "/boot", container.BindWritable). Bind(m("/dev/kvm"), m("/dev/kvm"), container.BindWritable|container.BindDevice|container.BindOptional).
Bind("/home", "/home", container.BindWritable). Readonly(m("/var/run/nscd"), 0755).
Bind("/lib", "/lib", container.BindWritable). Etc(m("/etc/"), "4a450b6596d7bc15bd01780eb9a607ac").
Bind("/lib64", "/lib64", container.BindWritable). Tmpfs(m("/run/user/1971"), 8192, 0755).
Bind("/nix", "/nix", container.BindWritable). Tmpfs(m("/run/dbus"), 8192, 0755).
Bind("/root", "/root", container.BindWritable). Remount(m("/dev/"), syscall.MS_RDONLY).
Bind("/run", "/run", container.BindWritable). Tmpfs(m("/dev/shm"), 0, 01777).
Bind("/srv", "/srv", container.BindWritable). Tmpfs(m("/run/user/"), 4096, 0755).
Bind("/sys", "/sys", container.BindWritable). Bind(m("/tmp/hakurei.1971/runtime/0"), m("/run/user/65534"), container.BindWritable).
Bind("/usr", "/usr", container.BindWritable). Bind(m("/tmp/hakurei.1971/tmpdir/0"), m("/tmp/"), container.BindWritable).
Bind("/var", "/var", container.BindWritable). Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")).
Bind("/dev/kvm", "/dev/kvm", container.BindWritable|container.BindDevice|container.BindOptional). Place(m("/etc/group"), []byte("hakurei:x:65534:\n")).
Tmpfs("/run/user/1971", 8192, 0755). Remount(m("/"), syscall.MS_RDONLY),
Tmpfs("/run/dbus", 8192, 0755).
Etc("/etc", "4a450b6596d7bc15bd01780eb9a607ac").
Tmpfs("/run/user", 4096, 0755).
Bind("/tmp/hakurei.1971/runtime/0", "/run/user/65534", container.BindWritable).
Bind("/tmp/hakurei.1971/tmpdir/0", "/tmp", container.BindWritable).
Bind("/home/chronos", "/home/chronos", container.BindWritable).
Place("/etc/passwd", []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")).
Place("/etc/group", []byte("hakurei:x:65534:\n")).
Tmpfs("/var/run/nscd", 8192, 0755),
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel, SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
HostNet: true, HostNet: true,
HostAbstract: true,
RetainSession: true, RetainSession: true,
ForwardCancel: true, ForwardCancel: true,
}, },
@ -82,7 +75,7 @@ var testCasesPd = []sealTestCase{
Identity: 9, Identity: 9,
Groups: []string{"video"}, Groups: []string{"video"},
Username: "chronos", Username: "chronos",
Data: "/home/chronos", Home: m("/home/chronos"),
SessionBus: &dbus.Config{ SessionBus: &dbus.Config{
Talk: []string{ Talk: []string{
"org.freedesktop.Notifications", "org.freedesktop.Notifications",
@ -114,7 +107,7 @@ var testCasesPd = []sealTestCase{
}, },
Filter: true, Filter: true,
}, },
Enablements: system.EWayland | system.EDBus | system.EPulse, Enablements: hst.NewEnablements(system.EWayland | system.EDBus | system.EPulse),
}, },
state.ID{ state.ID{
0xeb, 0xf0, 0x83, 0xd1, 0xeb, 0xf0, 0x83, 0xd1,
@ -168,8 +161,8 @@ var testCasesPd = []sealTestCase{
UpdatePerm("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write). UpdatePerm("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write).
UpdatePerm("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write), UpdatePerm("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write),
&container.Params{ &container.Params{
Dir: "/home/chronos", Dir: m("/home/chronos"),
Path: "/run/current-system/sw/bin/zsh", Path: m("/run/current-system/sw/bin/zsh"),
Args: []string{"zsh", "-c", "exec chromium "}, Args: []string{"zsh", "-c", "exec chromium "},
Env: []string{ Env: []string{
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus", "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus",
@ -186,40 +179,32 @@ var testCasesPd = []sealTestCase{
"XDG_SESSION_TYPE=tty", "XDG_SESSION_TYPE=tty",
}, },
Ops: new(container.Ops). Ops: new(container.Ops).
Proc("/proc"). Root(m("/"), container.BindWritable).
Tmpfs(hst.Tmp, 4096, 0755). Proc(m("/proc/")).
Dev("/dev").Mqueue("/dev/mqueue"). Tmpfs(hst.AbsTmp, 4096, 0755).
Bind("/bin", "/bin", container.BindWritable). DevWritable(m("/dev/"), true).
Bind("/boot", "/boot", container.BindWritable). Bind(m("/dev/dri"), m("/dev/dri"), container.BindWritable|container.BindDevice|container.BindOptional).
Bind("/home", "/home", container.BindWritable). Bind(m("/dev/kvm"), m("/dev/kvm"), container.BindWritable|container.BindDevice|container.BindOptional).
Bind("/lib", "/lib", container.BindWritable). Readonly(m("/var/run/nscd"), 0755).
Bind("/lib64", "/lib64", container.BindWritable). Etc(m("/etc/"), "ebf083d1b175911782d413369b64ce7c").
Bind("/nix", "/nix", container.BindWritable). Tmpfs(m("/run/user/1971"), 8192, 0755).
Bind("/root", "/root", container.BindWritable). Tmpfs(m("/run/dbus"), 8192, 0755).
Bind("/run", "/run", container.BindWritable). Remount(m("/dev/"), syscall.MS_RDONLY).
Bind("/srv", "/srv", container.BindWritable). Tmpfs(m("/dev/shm"), 0, 01777).
Bind("/sys", "/sys", container.BindWritable). Tmpfs(m("/run/user/"), 4096, 0755).
Bind("/usr", "/usr", container.BindWritable). Bind(m("/tmp/hakurei.1971/runtime/9"), m("/run/user/65534"), container.BindWritable).
Bind("/var", "/var", container.BindWritable). Bind(m("/tmp/hakurei.1971/tmpdir/9"), m("/tmp/"), container.BindWritable).
Bind("/dev/dri", "/dev/dri", container.BindWritable|container.BindDevice|container.BindOptional). Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")).
Bind("/dev/kvm", "/dev/kvm", container.BindWritable|container.BindDevice|container.BindOptional). Place(m("/etc/group"), []byte("hakurei:x:65534:\n")).
Tmpfs("/run/user/1971", 8192, 0755). Bind(m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/65534/wayland-0"), 0).
Tmpfs("/run/dbus", 8192, 0755). Bind(m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse"), m("/run/user/65534/pulse/native"), 0).
Etc("/etc", "ebf083d1b175911782d413369b64ce7c"). Place(m(hst.Tmp+"/pulse-cookie"), nil).
Tmpfs("/run/user", 4096, 0755). Bind(m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus"), m("/run/user/65534/bus"), 0).
Bind("/tmp/hakurei.1971/runtime/9", "/run/user/65534", container.BindWritable). Bind(m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), m("/run/dbus/system_bus_socket"), 0).
Bind("/tmp/hakurei.1971/tmpdir/9", "/tmp", container.BindWritable). Remount(m("/"), syscall.MS_RDONLY),
Bind("/home/chronos", "/home/chronos", container.BindWritable).
Place("/etc/passwd", []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")).
Place("/etc/group", []byte("hakurei:x:65534:\n")).
Bind("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/65534/wayland-0", 0).
Bind("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native", 0).
Place(hst.Tmp+"/pulse-cookie", nil).
Bind("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus", 0).
Bind("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket", 0).
Tmpfs("/var/run/nscd", 8192, 0755),
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel, SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
HostNet: true, HostNet: true,
HostAbstract: true,
RetainSession: true, RetainSession: true,
ForwardCancel: true, ForwardCancel: true,
}, },

View File

@ -127,8 +127,8 @@ func (s *stubNixOS) Open(name string) (fs.File, error) {
func (s *stubNixOS) Paths() hst.Paths { func (s *stubNixOS) Paths() hst.Paths {
return hst.Paths{ return hst.Paths{
SharePath: "/tmp/hakurei.1971", SharePath: m("/tmp/hakurei.1971"),
RuntimePath: "/run/user/1971", RuntimePath: m("/run/user/1971"),
RunDirPath: "/run/user/1971/hakurei", RunDirPath: m("/run/user/1971/hakurei"),
} }
} }

View File

@ -11,6 +11,7 @@ import (
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/hlog"
"hakurei.app/internal/sys" "hakurei.app/internal/sys"
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
) )
@ -21,9 +22,9 @@ const preallocateOpsCount = 1 << 5
// newContainer initialises [container.Params] via [hst.ContainerConfig]. // newContainer initialises [container.Params] via [hst.ContainerConfig].
// Note that remaining container setup must be queued by the caller. // Note that remaining container setup must be queued by the caller.
func newContainer(s *hst.ContainerConfig, os sys.State, uid, gid *int) (*container.Params, map[string]string, error) { func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid *int) (*container.Params, map[string]string, error) {
if s == nil { if s == nil {
return nil, nil, syscall.EBADE return nil, nil, hlog.WrapErr(syscall.EBADE, "invalid container configuration")
} }
params := &container.Params{ params := &container.Params{
@ -31,16 +32,21 @@ func newContainer(s *hst.ContainerConfig, os sys.State, uid, gid *int) (*contain
SeccompFlags: s.SeccompFlags, SeccompFlags: s.SeccompFlags,
SeccompPresets: s.SeccompPresets, SeccompPresets: s.SeccompPresets,
RetainSession: s.Tty, RetainSession: s.Tty,
HostNet: s.Net, HostNet: s.HostNet,
HostAbstract: s.HostAbstract,
// the container is canceled when shim is requested to exit or receives an interrupt or termination signal; // the container is canceled when shim is requested to exit or receives an interrupt or termination signal;
// this behaviour is implemented in the shim // this behaviour is implemented in the shim
ForwardCancel: s.WaitDelay >= 0, ForwardCancel: s.WaitDelay >= 0,
} }
as := &hst.ApplyState{
AutoEtcPrefix: prefix,
}
{ {
ops := make(container.Ops, 0, preallocateOpsCount+len(s.Filesystem)+len(s.Link)+len(s.Cover)) ops := make(container.Ops, 0, preallocateOpsCount+len(s.Filesystem))
params.Ops = &ops params.Ops = &ops
as.Ops = &ops
} }
if s.Multiarch { if s.Multiarch {
@ -72,14 +78,29 @@ func newContainer(s *hst.ContainerConfig, os sys.State, uid, gid *int) (*contain
*gid = container.OverflowGid() *gid = container.OverflowGid()
} }
filesystem := s.Filesystem
var autoroot *hst.FSBind
// valid happens late, so root mount gets it here
if len(filesystem) > 0 && filesystem[0].Valid() && filesystem[0].Path().String() == container.FHSRoot {
// if the first element targets /, it is inserted early and excluded from path hiding
rootfs := filesystem[0].FilesystemConfig
filesystem = filesystem[1:]
rootfs.Apply(as)
// autoroot requires special handling during path hiding
if b, ok := rootfs.(*hst.FSBind); ok && b.IsAutoRoot() {
autoroot = b
}
}
params. params.
Proc("/proc"). Proc(container.AbsFHSProc).
Tmpfs(hst.Tmp, 1<<12, 0755) Tmpfs(hst.AbsTmp, 1<<12, 0755)
if !s.Device { if !s.Device {
params.Dev("/dev").Mqueue("/dev/mqueue") params.DevWritable(container.AbsFHSDev, true)
} else { } else {
params.Bind("/dev", "/dev", container.BindWritable|container.BindDevice) params.Bind(container.AbsFHSDev, container.AbsFHSDev, container.BindWritable|container.BindDevice)
} }
/* retrieve paths and hide them if they're made available in the sandbox; /* retrieve paths and hide them if they're made available in the sandbox;
@ -88,7 +109,7 @@ func newContainer(s *hst.ContainerConfig, os sys.State, uid, gid *int) (*contain
and should not be treated as such, ALWAYS be careful with what you bind */ and should not be treated as such, ALWAYS be careful with what you bind */
var hidePaths []string var hidePaths []string
sc := os.Paths() sc := os.Paths()
hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath) hidePaths = append(hidePaths, sc.RuntimePath.String(), sc.SharePath.String())
_, systemBusAddr := dbus.Address() _, systemBusAddr := dbus.Address()
if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil { if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
return nil, nil, err return nil, nil, err
@ -103,7 +124,7 @@ func newContainer(s *hst.ContainerConfig, os sys.State, uid, gid *int) (*contain
if path.IsAbs(pair[1]) { if path.IsAbs(pair[1]) {
// get parent dir of socket // get parent dir of socket
dir := path.Dir(pair[1]) dir := path.Dir(pair[1])
if dir == "." || dir == "/" { if dir == "." || dir == container.FHSRoot {
os.Printf("dbus socket %q is in an unusual location", pair[1]) os.Printf("dbus socket %q is in an unusual location", pair[1])
} }
hidePaths = append(hidePaths, dir) hidePaths = append(hidePaths, dir)
@ -121,63 +142,100 @@ func newContainer(s *hst.ContainerConfig, os sys.State, uid, gid *int) (*contain
} }
} }
for _, c := range s.Filesystem { var hidePathSourceCount int
if c == nil { for i, c := range filesystem {
continue if !c.Valid() {
return nil, nil, fmt.Errorf("invalid filesystem at index %d", i)
}
c.Apply(as)
// fs counter
hidePathSourceCount += len(c.Host())
} }
if !path.IsAbs(c.Src) { // AutoRootOp is a collection of many BindMountOp internally
return nil, nil, fmt.Errorf("src path %q is not absolute", c.Src) var autoRootEntries []fs.DirEntry
if autoroot != nil {
if d, err := os.ReadDir(autoroot.Source.String()); err != nil {
return nil, nil, err
} else {
// autoroot counter
hidePathSourceCount += len(d)
autoRootEntries = d
}
} }
dest := c.Dst hidePathSource := make([]*container.Absolute, 0, hidePathSourceCount)
if c.Dst == "" {
dest = c.Src // fs append
} else if !path.IsAbs(dest) { for _, c := range filesystem {
return nil, nil, fmt.Errorf("dst path %q is not absolute", dest) // all entries already checked above
hidePathSource = append(hidePathSource, c.Host()...)
} }
srcH := c.Src // autoroot append
if err := evalSymlinks(os, &srcH); err != nil { if autoroot != nil {
for _, ent := range autoRootEntries {
name := ent.Name()
if container.IsAutoRootBindable(name) {
hidePathSource = append(hidePathSource, autoroot.Source.Append(name))
}
}
}
// evaluated path, input path
hidePathSourceEval := make([][2]string, len(hidePathSource))
for i, a := range hidePathSource {
if a == nil {
// unreachable
return nil, nil, syscall.ENOTRECOVERABLE
}
hidePathSourceEval[i] = [2]string{a.String(), a.String()}
if err := evalSymlinks(os, &hidePathSourceEval[i][0]); err != nil {
return nil, nil, err return nil, nil, err
} }
}
for _, p := range hidePathSourceEval {
for i := range hidePaths { for i := range hidePaths {
// skip matched entries // skip matched entries
if hidePathMatch[i] { if hidePathMatch[i] {
continue continue
} }
if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil { if ok, err := deepContainsH(p[0], hidePaths[i]); err != nil {
return nil, nil, err return nil, nil, err
} else if ok { } else if ok {
hidePathMatch[i] = true hidePathMatch[i] = true
os.Printf("hiding paths from %q", c.Src) os.Printf("hiding path %q from %q", hidePaths[i], p[1])
} }
} }
var flags int
if c.Write {
flags |= container.BindWritable
}
if c.Device {
flags |= container.BindDevice | container.BindWritable
}
if !c.Must {
flags |= container.BindOptional
}
params.Bind(c.Src, dest, flags)
} }
// cover matched paths // cover matched paths
for i, ok := range hidePathMatch { for i, ok := range hidePathMatch {
if ok { if ok {
params.Tmpfs(hidePaths[i], 1<<13, 0755) if a, err := container.NewAbs(hidePaths[i]); err != nil {
var absoluteError *container.AbsoluteError
if !errors.As(err, &absoluteError) {
return nil, nil, err
}
if absoluteError == nil {
return nil, nil, syscall.ENOTRECOVERABLE
}
return nil, nil, fmt.Errorf("invalid path hiding candidate %q", absoluteError.Pathname)
} else {
params.Tmpfs(a, 1<<13, 0755)
}
} }
} }
for _, l := range s.Link { // no more ContainerConfig paths beyond this point
params.Link(l[0], l[1]) if !s.Device {
params.
Remount(container.AbsFHSDev, syscall.MS_RDONLY).
Tmpfs(container.AbsFHSDev.Append("shm"), 0, 01777)
} }
return params, maps.Clone(s.Env), nil return params, maps.Clone(s.Env), nil

View File

@ -39,7 +39,7 @@ func (seal *outcome) Run(rs *RunState) error {
if err := seal.sys.Commit(seal.ctx); err != nil { if err := seal.sys.Commit(seal.ctx); err != nil {
return err return err
} }
store := state.NewMulti(seal.runDirPath) store := state.NewMulti(seal.runDirPath.String())
deferredStoreFunc := func(c state.Cursor) error { return nil } // noop until state in store deferredStoreFunc := func(c state.Cursor) error { return nil } // noop until state in store
defer func() { defer func() {
var revertErr error var revertErr error
@ -64,7 +64,7 @@ func (seal *outcome) Run(rs *RunState) error {
// accumulate enablements of remaining launchers // accumulate enablements of remaining launchers
for i, s := range states { for i, s := range states {
if s.Config != nil { if s.Config != nil {
rt |= s.Config.Enablements rt |= s.Config.Enablements.Unwrap()
} else { } else {
log.Printf("state entry %d does not contain config", i) log.Printf("state entry %d does not contain config", i)
} }
@ -88,7 +88,7 @@ func (seal *outcome) Run(rs *RunState) error {
defer cancel() defer cancel()
cmd := exec.CommandContext(ctx, hsuPath) cmd := exec.CommandContext(ctx, hsuPath)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Dir = "/" // container init enters final working directory cmd.Dir = container.FHSRoot // container init enters final working directory
// shim runs in the same session as monitor; see shim.go for behaviour // shim runs in the same session as monitor; see shim.go for behaviour
cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGCONT) } cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGCONT) }
@ -128,7 +128,6 @@ func (seal *outcome) Run(rs *RunState) error {
os.Getpid(), os.Getpid(),
seal.waitDelay, seal.waitDelay,
seal.container, seal.container,
seal.user.data,
hlog.Load(), hlog.Load(),
}) })
}() }()

View File

@ -12,6 +12,7 @@ import (
"path" "path"
"regexp" "regexp"
"slices" "slices"
"strconv"
"strings" "strings"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
@ -49,9 +50,7 @@ const (
) )
var ( var (
ErrConfig = errors.New("no configuration to seal") ErrIdent = errors.New("invalid identity")
ErrUser = errors.New("invalid aid")
ErrHome = errors.New("invalid home directory")
ErrName = errors.New("invalid username") ErrName = errors.New("invalid username")
ErrXDisplay = errors.New(display + " unset") ErrXDisplay = errors.New(display + " unset")
@ -67,8 +66,8 @@ var posixUsername = regexp.MustCompilePOSIX("^[a-z_]([A-Za-z0-9_-]{0,31}|[A-Za-z
type outcome struct { type outcome struct {
// copied from initialising [app] // copied from initialising [app]
id *stringPair[state.ID] id *stringPair[state.ID]
// copied from [sys.State] response // copied from [sys.State]
runDirPath string runDirPath *container.Absolute
// initial [hst.Config] gob stream for state data; // initial [hst.Config] gob stream for state data;
// this is prepared ahead of time as config is clobbered during seal creation // this is prepared ahead of time as config is clobbered during seal creation
@ -93,9 +92,9 @@ type shareHost struct {
// whether XDG_RUNTIME_DIR is used post hsu // whether XDG_RUNTIME_DIR is used post hsu
useRuntimeDir bool useRuntimeDir bool
// process-specific directory in tmpdir, empty if unused // process-specific directory in tmpdir, empty if unused
sharePath string sharePath *container.Absolute
// process-specific directory in XDG_RUNTIME_DIR, empty if unused // process-specific directory in XDG_RUNTIME_DIR, empty if unused
runtimeSharePath string runtimeSharePath *container.Absolute
seal *outcome seal *outcome
sc hst.Paths sc hst.Paths
@ -107,48 +106,46 @@ func (share *shareHost) ensureRuntimeDir() {
return return
} }
share.useRuntimeDir = true share.useRuntimeDir = true
share.seal.sys.Ensure(share.sc.RunDirPath, 0700) share.seal.sys.Ensure(share.sc.RunDirPath.String(), 0700)
share.seal.sys.UpdatePermType(system.User, share.sc.RunDirPath, acl.Execute) share.seal.sys.UpdatePermType(system.User, share.sc.RunDirPath.String(), acl.Execute)
share.seal.sys.Ensure(share.sc.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset share.seal.sys.Ensure(share.sc.RuntimePath.String(), 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset
share.seal.sys.UpdatePermType(system.User, share.sc.RuntimePath, acl.Execute) share.seal.sys.UpdatePermType(system.User, share.sc.RuntimePath.String(), acl.Execute)
} }
// instance returns a process-specific share path within tmpdir // instance returns a process-specific share path within tmpdir
func (share *shareHost) instance() string { func (share *shareHost) instance() *container.Absolute {
if share.sharePath != "" { if share.sharePath != nil {
return share.sharePath return share.sharePath
} }
share.sharePath = path.Join(share.sc.SharePath, share.seal.id.String()) share.sharePath = share.sc.SharePath.Append(share.seal.id.String())
share.seal.sys.Ephemeral(system.Process, share.sharePath, 0711) share.seal.sys.Ephemeral(system.Process, share.sharePath.String(), 0711)
return share.sharePath return share.sharePath
} }
// runtime returns a process-specific share path within XDG_RUNTIME_DIR // runtime returns a process-specific share path within XDG_RUNTIME_DIR
func (share *shareHost) runtime() string { func (share *shareHost) runtime() *container.Absolute {
if share.runtimeSharePath != "" { if share.runtimeSharePath != nil {
return share.runtimeSharePath return share.runtimeSharePath
} }
share.ensureRuntimeDir() share.ensureRuntimeDir()
share.runtimeSharePath = path.Join(share.sc.RunDirPath, share.seal.id.String()) share.runtimeSharePath = share.sc.RunDirPath.Append(share.seal.id.String())
share.seal.sys.Ephemeral(system.Process, share.runtimeSharePath, 0700) share.seal.sys.Ephemeral(system.Process, share.runtimeSharePath.String(), 0700)
share.seal.sys.UpdatePerm(share.runtimeSharePath, acl.Execute) share.seal.sys.UpdatePerm(share.runtimeSharePath.String(), acl.Execute)
return share.runtimeSharePath return share.runtimeSharePath
} }
// hsuUser stores post-hsu credentials and metadata // hsuUser stores post-hsu credentials and metadata
type hsuUser struct { type hsuUser struct {
// application id // identity
aid *stringPair[int] aid *stringPair[int]
// target uid resolved by fid:aid // target uid resolved by hid:aid
uid *stringPair[int] uid *stringPair[int]
// supplementary group ids // supplementary group ids
supp []string supp []string
// home directory host path
data string
// app user home directory // app user home directory
home string home *container.Absolute
// passwd database username // passwd database username
username string username string
} }
@ -159,6 +156,13 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
} }
seal.ctx = ctx seal.ctx = ctx
if config == nil {
return hlog.WrapErr(syscall.EINVAL, syscall.EINVAL.Error())
}
if config.Home == nil {
return hlog.WrapErr(os.ErrInvalid, "invalid path to home directory")
}
{ {
// encode initial configuration for state tracking // encode initial configuration for state tracking
ct := new(bytes.Buffer) ct := new(bytes.Buffer)
@ -171,14 +175,13 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
// allowed aid range 0 to 9999, this is checked again in hsu // allowed aid range 0 to 9999, this is checked again in hsu
if config.Identity < 0 || config.Identity > 9999 { if config.Identity < 0 || config.Identity > 9999 {
return hlog.WrapErr(ErrUser, return hlog.WrapErr(ErrIdent,
fmt.Sprintf("identity %d out of range", config.Identity)) fmt.Sprintf("identity %d out of range", config.Identity))
} }
seal.user = hsuUser{ seal.user = hsuUser{
aid: newInt(config.Identity), aid: newInt(config.Identity),
data: config.Data, home: config.Home,
home: config.Dir,
username: config.Username, username: config.Username,
} }
if seal.user.username == "" { if seal.user.username == "" {
@ -188,13 +191,6 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
return hlog.WrapErr(ErrName, return hlog.WrapErr(ErrName,
fmt.Sprintf("invalid user name %q", seal.user.username)) fmt.Sprintf("invalid user name %q", seal.user.username))
} }
if seal.user.data == "" || !path.IsAbs(seal.user.data) {
return hlog.WrapErr(ErrHome,
fmt.Sprintf("invalid home directory %q", seal.user.data))
}
if seal.user.home == "" {
seal.user.home = seal.user.data
}
if u, err := sys.Uid(seal.user.aid.unwrap()); err != nil { if u, err := sys.Uid(seal.user.aid.unwrap()); err != nil {
return err return err
} else { } else {
@ -210,26 +206,25 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
} }
} }
// this also falls back to host path if encountering an invalid path
if !path.IsAbs(config.Shell) {
config.Shell = "/bin/sh"
if s, ok := sys.LookupEnv(shell); ok && path.IsAbs(s) {
config.Shell = s
}
}
// do not use the value of shell before this point
// permissive defaults // permissive defaults
if config.Container == nil { if config.Container == nil {
hlog.Verbose("container configuration not supplied, PROCEED WITH CAUTION") hlog.Verbose("container configuration not supplied, PROCEED WITH CAUTION")
if config.Shell == nil {
config.Shell = container.AbsFHSRoot.Append("bin", "sh")
s, _ := sys.LookupEnv(shell)
if a, err := container.NewAbs(s); err == nil {
config.Shell = a
}
}
// hsu clears the environment so resolve paths early // hsu clears the environment so resolve paths early
if !path.IsAbs(config.Path) { if config.Path == nil {
if len(config.Args) > 0 { if len(config.Args) > 0 {
if p, err := sys.LookPath(config.Args[0]); err != nil { if p, err := sys.LookPath(config.Args[0]); err != nil {
return hlog.WrapErr(err, err.Error()) return hlog.WrapErr(err, err.Error())
} else { } else if config.Path, err = container.NewAbs(p); err != nil {
config.Path = p return hlog.WrapErr(err, err.Error())
} }
} else { } else {
config.Path = config.Shell config.Path = config.Shell
@ -238,62 +233,66 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
conf := &hst.ContainerConfig{ conf := &hst.ContainerConfig{
Userns: true, Userns: true,
Net: true, HostNet: true,
HostAbstract: true,
Tty: true, Tty: true,
AutoEtc: true,
}
// bind entries in /
if d, err := sys.ReadDir("/"); err != nil {
return err
} else {
b := make([]*hst.FilesystemConfig, 0, len(d))
for _, ent := range d {
p := "/" + ent.Name()
switch p {
case "/proc":
case "/dev":
case "/tmp":
case "/mnt":
case "/etc":
default: Filesystem: []hst.FilesystemConfigJSON{
b = append(b, &hst.FilesystemConfig{Src: p, Write: true, Must: true}) // autoroot, includes the home directory
} {&hst.FSBind{
} Target: container.AbsFHSRoot,
conf.Filesystem = append(conf.Filesystem, b...) Source: container.AbsFHSRoot,
Write: true,
Special: true,
}},
},
} }
// hide nscd from sandbox if present
nscd := "/var/run/nscd"
if _, err := sys.Stat(nscd); !errors.Is(err, fs.ErrNotExist) {
conf.Cover = append(conf.Cover, nscd)
}
// bind GPU stuff // bind GPU stuff
if config.Enablements&(system.EX11|system.EWayland) != 0 { if config.Enablements.Unwrap()&(system.EX11|system.EWayland) != 0 {
conf.Filesystem = append(conf.Filesystem, &hst.FilesystemConfig{Src: "/dev/dri", Device: true}) conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("dri"), Device: true, Optional: true}})
} }
// opportunistically bind kvm // opportunistically bind kvm
conf.Filesystem = append(conf.Filesystem, &hst.FilesystemConfig{Src: "/dev/kvm", Device: true}) conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("kvm"), Device: true, Optional: true}})
// hide nscd from container if present
nscd := container.AbsFHSVar.Append("run/nscd")
if _, err := sys.Stat(nscd.String()); !errors.Is(err, fs.ErrNotExist) {
conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSEphemeral{Target: nscd}})
}
// do autoetc last
conf.Filesystem = append(conf.Filesystem,
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
Target: container.AbsFHSEtc,
Source: container.AbsFHSEtc,
Special: true,
}},
)
config.Container = conf config.Container = conf
} }
// late nil checks for pd behaviour
if config.Shell == nil {
return hlog.WrapErr(syscall.EINVAL, "invalid shell path")
}
if config.Path == nil {
return hlog.WrapErr(syscall.EINVAL, "invalid program path")
}
var mapuid, mapgid *stringPair[int] var mapuid, mapgid *stringPair[int]
{ {
var uid, gid int var uid, gid int
var err error var err error
seal.container, seal.env, err = newContainer(config.Container, sys, &uid, &gid) seal.container, seal.env, err = newContainer(config.Container, sys, seal.id.String(), &uid, &gid)
seal.waitDelay = config.Container.WaitDelay seal.waitDelay = config.Container.WaitDelay
if err != nil { if err != nil {
return hlog.WrapErrSuffix(err, return hlog.WrapErrSuffix(err,
"cannot initialise container configuration:") "cannot initialise container configuration:")
} }
if !path.IsAbs(config.Path) {
return hlog.WrapErr(syscall.EINVAL,
"invalid program path")
}
if len(config.Args) == 0 { if len(config.Args) == 0 {
config.Args = []string{config.Path} config.Args = []string{config.Path.String()}
} }
seal.container.Path = config.Path seal.container.Path = config.Path
seal.container.Args = config.Args seal.container.Args = config.Args
@ -305,69 +304,52 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
} }
} }
if !config.Container.AutoEtc {
if config.Container.Etc != "" {
seal.container.Bind(config.Container.Etc, "/etc", 0)
}
} else {
etcPath := config.Container.Etc
if etcPath == "" {
etcPath = "/etc"
}
seal.container.Etc(etcPath, seal.id.String())
}
// inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as mapped uid // inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as mapped uid
innerRuntimeDir := path.Join("/run/user", mapuid.String()) innerRuntimeDir := container.AbsFHSRunUser.Append(mapuid.String())
seal.env[xdgRuntimeDir] = innerRuntimeDir seal.env[xdgRuntimeDir] = innerRuntimeDir.String()
seal.env[xdgSessionClass] = "user" seal.env[xdgSessionClass] = "user"
seal.env[xdgSessionType] = "tty" seal.env[xdgSessionType] = "tty"
share := &shareHost{seal: seal, sc: sys.Paths()} share := &shareHost{seal: seal, sc: sys.Paths()}
seal.runDirPath = share.sc.RunDirPath seal.runDirPath = share.sc.RunDirPath
seal.sys = system.New(seal.user.uid.unwrap()) seal.sys = system.New(seal.user.uid.unwrap())
seal.sys.Ensure(share.sc.SharePath, 0711) seal.sys.Ensure(share.sc.SharePath.String(), 0711)
{ {
runtimeDir := path.Join(share.sc.SharePath, "runtime") runtimeDir := share.sc.SharePath.Append("runtime")
seal.sys.Ensure(runtimeDir, 0700) seal.sys.Ensure(runtimeDir.String(), 0700)
seal.sys.UpdatePermType(system.User, runtimeDir, acl.Execute) seal.sys.UpdatePermType(system.User, runtimeDir.String(), acl.Execute)
runtimeDirInst := path.Join(runtimeDir, seal.user.aid.String()) runtimeDirInst := runtimeDir.Append(seal.user.aid.String())
seal.sys.Ensure(runtimeDirInst, 0700) seal.sys.Ensure(runtimeDirInst.String(), 0700)
seal.sys.UpdatePermType(system.User, runtimeDirInst, acl.Read, acl.Write, acl.Execute) seal.sys.UpdatePermType(system.User, runtimeDirInst.String(), acl.Read, acl.Write, acl.Execute)
seal.container.Tmpfs("/run/user", 1<<12, 0755) seal.container.Tmpfs(container.AbsFHSRunUser, 1<<12, 0755)
seal.container.Bind(runtimeDirInst, innerRuntimeDir, container.BindWritable) seal.container.Bind(runtimeDirInst, innerRuntimeDir, container.BindWritable)
} }
{ {
tmpdir := path.Join(share.sc.SharePath, "tmpdir") tmpdir := share.sc.SharePath.Append("tmpdir")
seal.sys.Ensure(tmpdir, 0700) seal.sys.Ensure(tmpdir.String(), 0700)
seal.sys.UpdatePermType(system.User, tmpdir, acl.Execute) seal.sys.UpdatePermType(system.User, tmpdir.String(), acl.Execute)
tmpdirInst := path.Join(tmpdir, seal.user.aid.String()) tmpdirInst := tmpdir.Append(seal.user.aid.String())
seal.sys.Ensure(tmpdirInst, 01700) seal.sys.Ensure(tmpdirInst.String(), 01700)
seal.sys.UpdatePermType(system.User, tmpdirInst, acl.Read, acl.Write, acl.Execute) seal.sys.UpdatePermType(system.User, tmpdirInst.String(), acl.Read, acl.Write, acl.Execute)
// mount inner /tmp from share so it shares persistence and storage behaviour of host /tmp // mount inner /tmp from share so it shares persistence and storage behaviour of host /tmp
seal.container.Bind(tmpdirInst, "/tmp", container.BindWritable) seal.container.Bind(tmpdirInst, container.AbsFHSTmp, container.BindWritable)
} }
{ {
homeDir := "/var/empty"
if seal.user.home != "" {
homeDir = seal.user.home
}
username := "chronos" username := "chronos"
if seal.user.username != "" { if seal.user.username != "" {
username = seal.user.username username = seal.user.username
} }
seal.container.Bind(seal.user.data, homeDir, container.BindWritable) seal.container.Dir = seal.user.home
seal.container.Dir = homeDir seal.env["HOME"] = seal.user.home.String()
seal.env["HOME"] = homeDir
seal.env["USER"] = username seal.env["USER"] = username
seal.env[shell] = config.Shell seal.env[shell] = config.Shell.String()
seal.container.Place("/etc/passwd", seal.container.Place(container.AbsFHSEtc.Append("passwd"),
[]byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Hakurei:"+homeDir+":"+config.Shell+"\n")) []byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Hakurei:"+seal.user.home.String()+":"+config.Shell.String()+"\n"))
seal.container.Place("/etc/group", seal.container.Place(container.AbsFHSEtc.Append("group"),
[]byte("hakurei:x:"+mapgid.String()+":\n")) []byte("hakurei:x:"+mapgid.String()+":\n"))
} }
@ -376,19 +358,19 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
seal.env[term] = t seal.env[term] = t
} }
if config.Enablements&system.EWayland != 0 { if config.Enablements.Unwrap()&system.EWayland != 0 {
// outer wayland socket (usually `/run/user/%d/wayland-%d`) // outer wayland socket (usually `/run/user/%d/wayland-%d`)
var socketPath string var socketPath *container.Absolute
if name, ok := sys.LookupEnv(wayland.WaylandDisplay); !ok { if name, ok := sys.LookupEnv(wayland.WaylandDisplay); !ok {
hlog.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName) hlog.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName)
socketPath = path.Join(share.sc.RuntimePath, wayland.FallbackName) socketPath = share.sc.RuntimePath.Append(wayland.FallbackName)
} else if !path.IsAbs(name) { } else if a, err := container.NewAbs(name); err != nil {
socketPath = path.Join(share.sc.RuntimePath, name) socketPath = share.sc.RuntimePath.Append(name)
} else { } else {
socketPath = name socketPath = a
} }
innerPath := path.Join(innerRuntimeDir, wayland.FallbackName) innerPath := innerRuntimeDir.Append(wayland.FallbackName)
seal.env[wayland.WaylandDisplay] = wayland.FallbackName seal.env[wayland.WaylandDisplay] = wayland.FallbackName
if !config.DirectWayland { // set up security-context-v1 if !config.DirectWayland { // set up security-context-v1
@ -398,35 +380,61 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
appID = "app.hakurei." + seal.id.String() appID = "app.hakurei." + seal.id.String()
} }
// downstream socket paths // downstream socket paths
outerPath := path.Join(share.instance(), "wayland") outerPath := share.instance().Append("wayland")
seal.sys.Wayland(&seal.sync, outerPath, socketPath, appID, seal.id.String()) seal.sys.Wayland(&seal.sync, outerPath.String(), socketPath.String(), appID, seal.id.String())
seal.container.Bind(outerPath, innerPath, 0) seal.container.Bind(outerPath, innerPath, 0)
} else { // bind mount wayland socket (insecure) } else { // bind mount wayland socket (insecure)
hlog.Verbose("direct wayland access, PROCEED WITH CAUTION") hlog.Verbose("direct wayland access, PROCEED WITH CAUTION")
share.ensureRuntimeDir() share.ensureRuntimeDir()
seal.container.Bind(socketPath, innerPath, 0) seal.container.Bind(socketPath, innerPath, 0)
seal.sys.UpdatePermType(system.EWayland, socketPath, acl.Read, acl.Write, acl.Execute) seal.sys.UpdatePermType(system.EWayland, socketPath.String(), acl.Read, acl.Write, acl.Execute)
} }
} }
if config.Enablements&system.EX11 != 0 { if config.Enablements.Unwrap()&system.EX11 != 0 {
if d, ok := sys.LookupEnv(display); !ok { if d, ok := sys.LookupEnv(display); !ok {
return hlog.WrapErr(ErrXDisplay, return hlog.WrapErr(ErrXDisplay,
"DISPLAY is not set") "DISPLAY is not set")
} else { } else {
socketDir := container.AbsFHSTmp.Append(".X11-unix")
// the socket file at `/tmp/.X11-unix/X%d` is typically owned by the priv user
// and not accessible by the target user
var socketPath *container.Absolute
if len(d) > 1 && d[0] == ':' { // `:%d`
if n, err := strconv.Atoi(d[1:]); err == nil && n >= 0 {
socketPath = socketDir.Append("X" + strconv.Itoa(n))
}
} else if len(d) > 5 && strings.HasPrefix(d, "unix:") { // `unix:%s`
if a, err := container.NewAbs(d[5:]); err == nil {
socketPath = a
}
}
if socketPath != nil {
if _, err := sys.Stat(socketPath.String()); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return hlog.WrapErrSuffix(err,
fmt.Sprintf("cannot access X11 socket %q:", socketPath))
}
} else {
seal.sys.UpdatePermType(system.EX11, socketPath.String(), acl.Read, acl.Write, acl.Execute)
d = "unix:" + socketPath.String()
}
}
seal.sys.ChangeHosts("#" + seal.user.uid.String()) seal.sys.ChangeHosts("#" + seal.user.uid.String())
seal.env[display] = d seal.env[display] = d
seal.container.Bind("/tmp/.X11-unix", "/tmp/.X11-unix", 0) seal.container.Bind(socketDir, socketDir, 0)
} }
} }
if config.Enablements&system.EPulse != 0 { if config.Enablements.Unwrap()&system.EPulse != 0 {
// PulseAudio runtime directory (usually `/run/user/%d/pulse`) // PulseAudio runtime directory (usually `/run/user/%d/pulse`)
pulseRuntimeDir := path.Join(share.sc.RuntimePath, "pulse") pulseRuntimeDir := share.sc.RuntimePath.Append("pulse")
// PulseAudio socket (usually `/run/user/%d/pulse/native`) // PulseAudio socket (usually `/run/user/%d/pulse/native`)
pulseSocket := path.Join(pulseRuntimeDir, "native") pulseSocket := pulseRuntimeDir.Append("native")
if _, err := sys.Stat(pulseRuntimeDir); err != nil { if _, err := sys.Stat(pulseRuntimeDir.String()); err != nil {
if !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
return hlog.WrapErrSuffix(err, return hlog.WrapErrSuffix(err,
fmt.Sprintf("cannot access PulseAudio directory %q:", pulseRuntimeDir)) fmt.Sprintf("cannot access PulseAudio directory %q:", pulseRuntimeDir))
@ -435,7 +443,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir)) fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir))
} }
if s, err := sys.Stat(pulseSocket); err != nil { if s, err := sys.Stat(pulseSocket.String()); err != nil {
if !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
return hlog.WrapErrSuffix(err, return hlog.WrapErrSuffix(err,
fmt.Sprintf("cannot access PulseAudio socket %q:", pulseSocket)) fmt.Sprintf("cannot access PulseAudio socket %q:", pulseSocket))
@ -450,39 +458,38 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
} }
// hard link pulse socket into target-executable share // hard link pulse socket into target-executable share
innerPulseRuntimeDir := path.Join(share.runtime(), "pulse") innerPulseRuntimeDir := share.runtime().Append("pulse")
innerPulseSocket := path.Join(innerRuntimeDir, "pulse", "native") innerPulseSocket := innerRuntimeDir.Append("pulse", "native")
seal.sys.Link(pulseSocket, innerPulseRuntimeDir) seal.sys.Link(pulseSocket.String(), innerPulseRuntimeDir.String())
seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0) seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0)
seal.env[pulseServer] = "unix:" + innerPulseSocket seal.env[pulseServer] = "unix:" + innerPulseSocket.String()
// publish current user's pulse cookie for target user // publish current user's pulse cookie for target user
if src, err := discoverPulseCookie(sys); err != nil { if src, err := discoverPulseCookie(sys); err != nil {
// not fatal // not fatal
hlog.Verbose(strings.TrimSpace(err.(*hlog.BaseError).Message())) hlog.Verbose(strings.TrimSpace(err.(*hlog.BaseError).Message()))
} else { } else {
innerDst := hst.Tmp + "/pulse-cookie" innerDst := hst.AbsTmp.Append("/pulse-cookie")
seal.env[pulseCookie] = innerDst seal.env[pulseCookie] = innerDst.String()
var payload *[]byte var payload *[]byte
seal.container.PlaceP(innerDst, &payload) seal.container.PlaceP(innerDst, &payload)
seal.sys.CopyFile(payload, src, 256, 256) seal.sys.CopyFile(payload, src, 256, 256)
} }
} }
if config.Enablements&system.EDBus != 0 { if config.Enablements.Unwrap()&system.EDBus != 0 {
// ensure dbus session bus defaults // ensure dbus session bus defaults
if config.SessionBus == nil { if config.SessionBus == nil {
config.SessionBus = dbus.NewConfig(config.ID, true, true) config.SessionBus = dbus.NewConfig(config.ID, true, true)
} }
// downstream socket paths // downstream socket paths
sharePath := share.instance() sessionPath, systemPath := share.instance().Append("bus"), share.instance().Append("system_bus_socket")
sessionPath, systemPath := path.Join(sharePath, "bus"), path.Join(sharePath, "system_bus_socket")
// configure dbus proxy // configure dbus proxy
if f, err := seal.sys.ProxyDBus( if f, err := seal.sys.ProxyDBus(
config.SessionBus, config.SystemBus, config.SessionBus, config.SystemBus,
sessionPath, systemPath, sessionPath.String(), systemPath.String(),
); err != nil { ); err != nil {
return err return err
} else { } else {
@ -490,30 +497,29 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
} }
// share proxy sockets // share proxy sockets
sessionInner := path.Join(innerRuntimeDir, "bus") sessionInner := innerRuntimeDir.Append("bus")
seal.env[dbusSessionBusAddress] = "unix:path=" + sessionInner seal.env[dbusSessionBusAddress] = "unix:path=" + sessionInner.String()
seal.container.Bind(sessionPath, sessionInner, 0) seal.container.Bind(sessionPath, sessionInner, 0)
seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write) seal.sys.UpdatePerm(sessionPath.String(), acl.Read, acl.Write)
if config.SystemBus != nil { if config.SystemBus != nil {
systemInner := "/run/dbus/system_bus_socket" systemInner := container.AbsFHSRun.Append("dbus/system_bus_socket")
seal.env[dbusSystemBusAddress] = "unix:path=" + systemInner seal.env[dbusSystemBusAddress] = "unix:path=" + systemInner.String()
seal.container.Bind(systemPath, systemInner, 0) seal.container.Bind(systemPath, systemInner, 0)
seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write) seal.sys.UpdatePerm(systemPath.String(), acl.Read, acl.Write)
} }
} }
for _, dest := range config.Container.Cover { // mount root read-only as the final setup Op
seal.container.Tmpfs(dest, 1<<13, 0755) seal.container.Remount(container.AbsFHSRoot, syscall.MS_RDONLY)
}
// append ExtraPerms last // append ExtraPerms last
for _, p := range config.ExtraPerms { for _, p := range config.ExtraPerms {
if p == nil { if p == nil || p.Path == nil {
continue continue
} }
if p.Ensure { if p.Ensure {
seal.sys.Ensure(p.Path, 0700) seal.sys.Ensure(p.Path.String(), 0700)
} }
perms := make(acl.Perms, 0, 3) perms := make(acl.Perms, 0, 3)
@ -526,7 +532,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
if p.Execute { if p.Execute {
perms = append(perms, acl.Execute) perms = append(perms, acl.Execute)
} }
seal.sys.UpdatePermType(system.User, p.Path, perms...) seal.sys.UpdatePermType(system.User, p.Path.String(), perms...)
} }
// flatten and sort env for deterministic behaviour // flatten and sort env for deterministic behaviour

View File

@ -34,8 +34,6 @@ type shimParams struct {
// finalised container params // finalised container params
Container *container.Params Container *container.Params
// path to outer home directory
Home string
// verbosity pass through // verbosity pass through
Verbose bool Verbose bool
@ -64,7 +62,7 @@ func ShimMain() {
closeSetup func() error closeSetup func() error
) )
if f, err := container.Receive(shimEnv, &params, nil); err != nil { if f, err := container.Receive(shimEnv, &params, nil); err != nil {
if errors.Is(err, container.ErrInvalid) { if errors.Is(err, syscall.EBADF) {
log.Fatal("invalid config descriptor") log.Fatal("invalid config descriptor")
} }
if errors.Is(err, container.ErrNotSet) { if errors.Is(err, container.ErrNotSet) {
@ -142,28 +140,9 @@ func ShimMain() {
// not fatal // not fatal
} }
// ensure home directory as target user
if s, err := os.Stat(params.Home); err != nil {
if os.IsNotExist(err) {
if err = os.Mkdir(params.Home, 0700); err != nil {
log.Fatalf("cannot create home directory: %v", err)
}
} else {
log.Fatalf("cannot access home directory: %v", err)
}
// home directory is created, proceed
} else if !s.IsDir() {
log.Fatalf("path %q is not a directory", params.Home)
}
var name string
if len(params.Container.Args) > 0 {
name = params.Container.Args[0]
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
cancelContainer.Store(&stop) cancelContainer.Store(&stop)
z := container.New(ctx, name) z := container.New(ctx)
z.Params = *params.Container z.Params = *params.Container
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr

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