Compare commits

..

71 Commits

Author SHA1 Message Date
2de7c2d07d
container: optionally isolate host abstract UNIX domain sockets via landlock
Some checks failed
Test / Sandbox (push) Successful in 2m17s
Test / Hakurei (race detector) (pull_request) Failing after 1h0m59s
Test / Flake checks (pull_request) Has been skipped
Test / Sandbox (pull_request) Successful in 1m29s
Test / Hpkg (push) Successful in 4m12s
Test / Hpkg (pull_request) Successful in 1m56s
Test / Sandbox (race detector) (push) Successful in 4m17s
Test / Sandbox (race detector) (pull_request) Successful in 2m5s
Test / Create distribution (pull_request) Failing after 28s
Test / Hakurei (pull_request) Failing after 20m30s
Test / Hakurei (race detector) (push) Failing after 22m21s
Test / Hakurei (push) Failing after 39m46s
Test / Flake checks (push) Has been skipped
Test / Create distribution (push) Failing after 46s
2025-08-17 16:05:49 +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 / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m57s
Test / Hakurei (push) Successful in 2m57s
Test / Hpkg (push) Successful in 3m58s
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 2m42s
Test / Flake checks (push) Successful in 1m25s
This is way higher level than the container package and does not even work unless every path is mounted in the exact same location.

This behaviour causes nothing but confusion and problems,

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-09 19:08:54 +09:00
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
cb513bb1cd
release: 0.1.2
All checks were successful
Release / Create release (push) Successful in 41s
Test / Sandbox (push) Successful in 40s
Test / Hakurei (push) Successful in 2m37s
Test / Create distribution (push) Successful in 24s
Test / Sandbox (race detector) (push) Successful in 3m29s
Test / Planterette (push) Successful in 3m5s
Test / Hakurei (race detector) (push) Successful in 2m27s
Test / Flake checks (push) Successful in 1m19s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-29 03:11:33 +09:00
f7bd28118c
hst: configurable wait delay
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m58s
Test / Hakurei (push) Successful in 2m47s
Test / Sandbox (race detector) (push) Successful in 3m56s
Test / Planterette (push) Successful in 3m58s
Test / Hakurei (race detector) (push) Successful in 4m31s
Test / Flake checks (push) Successful in 1m17s
This is useful for programs that take a long time to clean up.

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

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

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

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

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

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

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

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-25 00:45:10 +09:00
9d7a19d162
container: use more reliable nonexistence
All checks were successful
Test / Create distribution (push) Successful in 45s
Test / Sandbox (push) Successful in 2m21s
Test / Hakurei (push) Successful in 3m8s
Test / Planterette (push) Successful in 3m55s
Test / Sandbox (race detector) (push) Successful in 4m6s
Test / Hakurei (race detector) (push) Successful in 4m41s
Test / Flake checks (push) Successful in 1m18s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-18 23:18:26 +09:00
107 changed files with 5213 additions and 1820 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.Data = 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

@ -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

@ -12,6 +12,7 @@ import (
"text/tabwriter" "text/tabwriter"
"time" "time"
"hakurei.app/container"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/app/state" "hakurei.app/internal/app/state"
"hakurei.app/internal/hlog" "hakurei.app/internal/hlog"
@ -22,9 +23,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 +39,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 +78,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.Data != nil {
t.Printf(" Data:\t%s\n", config.Data) t.Printf(" Data:\t%s\n", config.Data)
} }
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 +96,33 @@ 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.Net)
writeFlag("device", container.Device) writeFlag("device", params.Device)
writeFlag("tty", container.Tty) writeFlag("tty", params.Tty)
writeFlag("mapuid", container.MapRealUID) writeFlag("mapuid", params.MapRealUID)
writeFlag("directwl", config.DirectWayland) writeFlag("directwl", config.DirectWayland)
writeFlag("autoetc", container.AutoEtc) writeFlag("autoetc", params.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 params.AutoRoot != nil {
if etc == "" { t.Printf(" Root:\t%s (%d)\n", params.AutoRoot, params.RootFlags)
etc = "/etc" }
etc := params.Etc
if etc == nil {
etc = container.AbsFHSEtc
} }
t.Printf(" Etc:\t%s\n", etc) t.Printf(" Etc:\t%s\n", etc)
if len(container.Cover) > 0 { if config.Path != nil {
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 +132,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

@ -42,16 +42,17 @@ func Test_printShowInstance(t *testing.T) {
Data: /var/lib/hakurei/u0/org.chromium.Chromium Data: /var/lib/hakurei/u0/org.chromium.Chromium
Hostname: localhost Hostname: localhost
Flags: userns devel net device tty mapuid autoetc Flags: userns devel net device tty mapuid autoetc
Etc: /etc Root: /var/lib/hakurei/base/org.debian (2)
Cover: /var/run/nscd Etc: /etc/
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 w+ephemeral(-rwxr-xr-x):/tmp/
+/run/current-system w*/nix/store:/mnt-root/nix/.rw-store/upper:/mnt-root/nix/.rw-store/work:/mnt-root/nix/.ro-store
+/run/opengl-driver */nix/store
+/var/db/nix-channels */run/current-system
*/run/opengl-driver
w*/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium w*/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
d+/dev/dri d+/dev/dri
@ -82,18 +83,17 @@ App
Identity: 0 Identity: 0
Enablements: (no enablements) Enablements: (no enablements)
Flags: none Flags: none
Etc: /etc 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 Etc: /etc/
Path:
Filesystem Filesystem
<invalid>
Extra ACL Extra ACL
@ -121,16 +121,17 @@ App
Data: /var/lib/hakurei/u0/org.chromium.Chromium Data: /var/lib/hakurei/u0/org.chromium.Chromium
Hostname: localhost Hostname: localhost
Flags: userns devel net device tty mapuid autoetc Flags: userns devel net device tty mapuid autoetc
Etc: /etc Root: /var/lib/hakurei/base/org.debian (2)
Cover: /var/run/nscd Etc: /etc/
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 w+ephemeral(-rwxr-xr-x):/tmp/
+/run/current-system w*/nix/store:/mnt-root/nix/.rw-store/upper:/mnt-root/nix/.rw-store/work:/mnt-root/nix/.ro-store
+/run/opengl-driver */nix/store
+/var/db/nix-channels */run/current-system
*/run/opengl-driver
w*/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium w*/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
d+/dev/dri d+/dev/dri
@ -194,7 +195,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": [
@ -256,8 +261,10 @@ App
], ],
"container": { "container": {
"hostname": "localhost", "hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1, "seccomp_flags": 1,
"seccomp_presets": 1, "seccomp_presets": 1,
"seccomp_compat": true,
"devel": true, "devel": true,
"userns": true, "userns": true,
"net": true, "net": true,
@ -272,39 +279,55 @@ App
"device": true, "device": true,
"filesystem": [ "filesystem": [
{ {
"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"
}, },
{ {
"type": "bind",
"src": "/run/current-system" "src": "/run/current-system"
}, },
{ {
"type": "bind",
"src": "/run/opengl-driver" "src": "/run/opengl-driver"
}, },
{ {
"src": "/var/db/nix-channels" "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
}, },
{ {
"type": "bind",
"src": "/dev/dri", "src": "/dev/dri",
"dev": true "dev": true,
"optional": true
} }
], ],
"symlink": [ "symlink": [
[ {
"/run/user/65534", "target": "/run/user/65534",
"/run/user/150" "linkname": "/run/user/150"
] }
], ],
"etc": "/etc", "auto_root": "/var/lib/hakurei/base/org.debian",
"auto_etc": true, "root_flags": 2,
"cover": [ "etc": "/etc/",
"/var/run/nscd" "auto_etc": true
]
} }
}, },
"time": "1970-01-01T00:00:00.000000009Z" "time": "1970-01-01T00:00:00.000000009Z"
@ -320,7 +343,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": [
@ -382,8 +409,10 @@ App
], ],
"container": { "container": {
"hostname": "localhost", "hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1, "seccomp_flags": 1,
"seccomp_presets": 1, "seccomp_presets": 1,
"seccomp_compat": true,
"devel": true, "devel": true,
"userns": true, "userns": true,
"net": true, "net": true,
@ -398,39 +427,55 @@ App
"device": true, "device": true,
"filesystem": [ "filesystem": [
{ {
"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"
}, },
{ {
"type": "bind",
"src": "/run/current-system" "src": "/run/current-system"
}, },
{ {
"type": "bind",
"src": "/run/opengl-driver" "src": "/run/opengl-driver"
}, },
{ {
"src": "/var/db/nix-channels" "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
}, },
{ {
"type": "bind",
"src": "/dev/dri", "src": "/dev/dri",
"dev": true "dev": true,
"optional": true
} }
], ],
"symlink": [ "symlink": [
[ {
"/run/user/65534", "target": "/run/user/65534",
"/run/user/150" "linkname": "/run/user/150"
] }
], ],
"etc": "/etc", "auto_root": "/var/lib/hakurei/base/org.debian",
"auto_etc": true, "root_flags": 2,
"cover": [ "etc": "/etc/",
"/var/run/nscd" "auto_etc": true
]
} }
} }
`}, `},
@ -500,7 +545,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": [
@ -562,8 +611,10 @@ func Test_printPs(t *testing.T) {
], ],
"container": { "container": {
"hostname": "localhost", "hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1, "seccomp_flags": 1,
"seccomp_presets": 1, "seccomp_presets": 1,
"seccomp_compat": true,
"devel": true, "devel": true,
"userns": true, "userns": true,
"net": true, "net": true,
@ -578,39 +629,55 @@ func Test_printPs(t *testing.T) {
"device": true, "device": true,
"filesystem": [ "filesystem": [
{ {
"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"
}, },
{ {
"type": "bind",
"src": "/run/current-system" "src": "/run/current-system"
}, },
{ {
"type": "bind",
"src": "/run/opengl-driver" "src": "/run/opengl-driver"
}, },
{ {
"src": "/var/db/nix-channels" "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
}, },
{ {
"type": "bind",
"src": "/dev/dri", "src": "/dev/dri",
"dev": true "dev": true,
"optional": true
} }
], ],
"symlink": [ "symlink": [
[ {
"/run/user/65534", "target": "/run/user/65534",
"/run/user/150" "linkname": "/run/user/150"
] }
], ],
"etc": "/etc", "auto_root": "/var/lib/hakurei/base/org.debian",
"auto_etc": true, "root_flags": 2,
"cover": [ "etc": "/etc/",
"/var/run/nscd" "auto_etc": true
]
} }
}, },
"time": "1970-01-01T00:00:00.000000009Z" "time": "1970-01-01T00:00:00.000000009Z"

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"
) )
@ -43,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"`
@ -57,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,
@ -78,9 +77,9 @@ 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, Data: pathSet.homeDir,
Dir: path.Join("/data/data", app.ID), Dir: pathDataData.Append(app.ID),
Identity: app.Identity, Identity: app.Identity,
Groups: app.Groups, Groups: app.Groups,
@ -93,22 +92,22 @@ func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool
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{Source: pathSet.nixPath.Append("store"), Target: pathNixStore}},
{Src: pathSet.metaPath, Dst: path.Join(hst.Tmp, "app"), Must: true}, {FilesystemConfig: &hst.FSBind{Source: pathSet.metaPath, Target: hst.AbsTmp.Append("app")}},
{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}},
}, },
Link: [][2]string{ Link: []hst.LinkConfig{
{app.CurrentSystem, "/run/current-system"}, {pathCurrentSystem, app.CurrentSystem.String()},
{"/run/current-system/sw/bin", "/bin"}, {pathBin, pathSwBin.String()},
{"/run/current-system/sw/bin", "/usr/bin"}, {container.AbsFHSUsrBin, pathSwBin.String()},
}, },
Etc: path.Join(pathSet.cacheDir, "etc"), Etc: pathSet.cacheDir.Append("etc"),
AutoEtc: true, AutoEtc: true,
}, },
ExtraPerms: []*hst.ExtraPermConfig{ ExtraPerms: []*hst.ExtraPermConfig{
@ -142,6 +141,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;
@ -215,8 +220,7 @@ stdenv.mkDerivation {
# create binary cache # create binary cache
closureInfo="${ closureInfo="${
closureInfo { closureInfo {
rootPaths = rootPaths = [
[
homeManagerConfiguration.activationPackage homeManagerConfiguration.activationPackage
launcher launcher
] ]

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.1000/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,9 @@ func withNixDaemon(
}, },
Username: "hakurei", Username: "hakurei",
Shell: shellPath, Shell: pathShell,
Data: pathSet.homeDir, Data: pathSet.homeDir,
Dir: path.Join("/data/data", app.ID), Dir: pathDataData.Append(app.ID),
ExtraPerms: []*hst.ExtraPermConfig{ ExtraPerms: []*hst.ExtraPermConfig{
{Path: dataHome, Execute: true}, {Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true}, {Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
@ -48,15 +48,15 @@ func withNixDaemon(
Net: net, Net: 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{Source: pathSet.nixPath, Target: pathNix, Write: true}},
}, },
Link: [][2]string{ Link: []hst.LinkConfig{
{app.CurrentSystem, "/run/current-system"}, {pathCurrentSystem, app.CurrentSystem.String()},
{"/run/current-system/sw/bin", "/bin"}, {pathBin, pathSwBin.String()},
{"/run/current-system/sw/bin", "/usr/bin"}, {container.AbsFHSUsrBin, pathSwBin.String()},
}, },
Etc: path.Join(pathSet.cacheDir, "etc"), Etc: pathSet.cacheDir.Append("etc"),
AutoEtc: true, AutoEtc: true,
}, },
}), dropShell, beforeFail) }), dropShell, beforeFail)
@ -64,18 +64,18 @@ func withNixDaemon(
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 Data: pathSet.cacheDir, // this also ensures cacheDir via shim
Dir: path.Join("/data/data", app.ID, "cache"), Dir: pathDataData.Append(app.ID, "cache"),
ExtraPerms: []*hst.ExtraPermConfig{ ExtraPerms: []*hst.ExtraPermConfig{
{Path: dataHome, Execute: true}, {Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true}, {Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
@ -88,16 +88,16 @@ 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{Source: workDir.Append("nix"), Target: pathNix}},
{Src: workDir, Dst: path.Join(hst.Tmp, "bundle"), Must: true}, {FilesystemConfig: &hst.FSBind{Source: workDir, Target: hst.AbsTmp.Append("bundle")}},
}, },
Link: [][2]string{ Link: []hst.LinkConfig{
{app.CurrentSystem, "/run/current-system"}, {pathCurrentSystem, app.CurrentSystem.String()},
{"/run/current-system/sw/bin", "/bin"}, {pathBin, pathSwBin.String()},
{"/run/current-system/sw/bin", "/usr/bin"}, {container.AbsFHSUsrBin, pathSwBin.String()},
}, },
Etc: path.Join(workDir, "etc"), Etc: workDir.Append(container.FHSEtc),
AutoEtc: true, AutoEtc: true,
}, },
}, dropShell, beforeFail) }, dropShell, beforeFail)
@ -105,7 +105,7 @@ func withCacheDir(
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},
}...)
}

98
container/absolute.go Normal file
View File

@ -0,0 +1,98 @@
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
}
// 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() })
}

325
container/absolute_test.go Normal file
View File

@ -0,0 +1,325 @@
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("MustAbsolute: 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())
})
}
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)
}
})
}

73
container/autoetc.go Normal file
View File

@ -0,0 +1,73 @@
package container
import (
"encoding/gob"
"fmt"
"os"
"syscall"
)
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) early(*setupState) error { return nil }
func (e *AutoEtcOp) apply(state *setupState) error {
if state.nonrepeatable&nrAutoEtc != 0 {
return msg.WrapErr(syscall.EINVAL, "autoetc is not repeatable")
}
state.nonrepeatable |= nrAutoEtc
const target = sysrootPath + FHSEtc
rel := e.hostRel() + "/"
if err := os.MkdirAll(target, 0755); err != nil {
return wrapErrSelf(err)
}
if d, err := os.ReadDir(toSysroot(e.hostPath().String())); 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(FHSProc+"mounts", target+n); err != nil {
return wrapErrSelf(err)
}
default:
if err = os.Symlink(rel+n, target+n); err != nil {
return wrapErrSelf(err)
}
}
}
}
return nil
}
// bypasses abs check, use with caution!
func (e *AutoEtcOp) hostPath() *Absolute { return &Absolute{FHSEtc + 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) }

96
container/autoroot.go Normal file
View File

@ -0,0 +1,96 @@
package container
import (
"encoding/gob"
"fmt"
"os"
"syscall"
)
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, prefix string, flags int) *Ops {
*f = append(*f, &AutoRootOp{host, prefix, flags, nil})
return f
}
type AutoRootOp struct {
Host *Absolute
Prefix string
// 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) early(state *setupState) error {
if r.Host == nil {
return syscall.EBADE
}
if d, err := os.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); err != nil {
return err
}
r.resolved = append(r.resolved, op)
}
}
return nil
}
}
func (r *AutoRootOp) apply(state *setupState) error {
if state.nonrepeatable&nrAutoRoot != 0 {
return msg.WrapErr(syscall.EINVAL, "autoroot is not repeatable")
}
state.nonrepeatable |= nrAutoRoot
for _, op := range r.resolved {
msg.Verbosef("%s %s", op.prefix(), op)
if err := op.apply(state); err != nil {
return err
}
}
return nil
}
func (r *AutoRootOp) Is(op Op) bool {
vr, ok := op.(*AutoRootOp)
return ok && ((r == nil && vr == nil) || (r != nil && vr != nil &&
r.Host == vr.Host && r.Prefix == vr.Prefix && r.Flags == vr.Flags))
}
func (*AutoRootOp) prefix() string { return "setting up" }
func (r *AutoRootOp) String() string {
return fmt.Sprintf("auto root %q prefix %s flags %#x", r.Host, r.Prefix, r.Flags)
}
// IsAutoRootBindable returns whether a dir entry name is selected for AutoRoot.
func IsAutoRootBindable(name string) bool {
switch name {
case "proc":
case "dev":
case "tmp":
case "mnt":
case "etc":
default:
return true
}
return false
}

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 (

View File

@ -9,7 +9,6 @@ import (
"io" "io"
"os" "os"
"os/exec" "os/exec"
"path"
"strconv" "strconv"
. "syscall" . "syscall"
"time" "time"
@ -17,21 +16,22 @@ import (
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
) )
const (
// CancelSignal is the signal expected by container init on context cancel.
// A custom [Container.Cancel] function must eventually deliver this signal.
CancelSignal = SIGTERM
)
type ( 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,
// with behaviour identical to its [exec.Cmd] counterpart. // with behaviour identical to its [exec.Cmd] counterpart.
ExtraFiles []*os.File ExtraFiles []*os.File
// Custom [exec.Cmd] initialisation function.
CommandContext func(ctx context.Context) (cmd *exec.Cmd)
// param encoder for shim and init // param encoder for shim and init
setup *gob.Encoder setup *gob.Encoder
// cancels cmd // cancels cmd
@ -52,13 +52,17 @@ 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.
ForwardCancel bool
// time to wait for linger processes after death of initial process
AdoptWaitDelay time.Duration
// Mapped Uid in user namespace. // Mapped Uid in user namespace.
Uid int Uid int
@ -68,6 +72,7 @@ type (
Hostname string Hostname string
// Sequential container setup ops. // Sequential container setup ops.
*Ops *Ops
// Seccomp system call filter rules. // Seccomp system call filter rules.
SeccompRules []seccomp.NativeRule SeccompRules []seccomp.NativeRule
// Extra seccomp flags. // Extra seccomp flags.
@ -76,6 +81,7 @@ type (
SeccompPresets seccomp.FilterPreset SeccompPresets seccomp.FilterPreset
// Do not load seccomp program. // Do not load seccomp program.
SeccompDisable bool SeccompDisable bool
// Permission bits of newly created parent directories. // Permission bits of newly created parent directories.
// The zero value is interpreted as 0755. // The zero value is interpreted as 0755.
ParentPerm os.FileMode ParentPerm os.FileMode
@ -90,22 +96,18 @@ type (
} }
) )
// Start starts the container init. The init process blocks until Serve is called.
func (p *Container) Start() error { func (p *Container) Start() error {
if p.cmd != nil { if p.cmd != nil {
return errors.New("sandbox: already started") return errors.New("container: already started")
} }
if p.Ops == nil || len(*p.Ops) == 0 { if p.Ops == nil || len(*p.Ops) == 0 {
return errors.New("sandbox: starting an empty container") return errors.New("container: starting an empty container")
} }
ctx, cancel := context.WithCancel(p.ctx) 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()
@ -118,34 +120,47 @@ func (p *Container) Start() error {
p.SeccompPresets |= seccomp.PresetDenyTTY p.SeccompPresets |= seccomp.PresetDenyTTY
} }
if p.CommandContext != nil { if p.AdoptWaitDelay == 0 {
p.cmd = p.CommandContext(ctx) p.AdoptWaitDelay = 5 * time.Second
} else { }
p.cmd = exec.CommandContext(ctx, MustExecutable()) // to allow disabling this behaviour
p.cmd.Args = []string{"init"} if p.AdoptWaitDelay < 0 {
p.AdoptWaitDelay = 0
} }
p.cmd = exec.CommandContext(ctx, MustExecutable())
p.cmd.Args = []string{initName}
p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
p.cmd.WaitDelay = p.WaitDelay p.cmd.WaitDelay = p.WaitDelay
if p.Cancel != nil { if p.Cancel != nil {
p.cmd.Cancel = func() error { return p.Cancel(p.cmd) } p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
} else { } else {
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(SIGTERM) } p.cmd.Cancel = func() error { return p.cmd.Process.Signal(CancelSignal) }
} }
p.cmd.Dir = "/" p.cmd.Dir = FHSRoot
p.cmd.SysProcAttr = &SysProcAttr{ 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 {
@ -164,6 +179,8 @@ func (p *Container) Start() error {
return nil return nil
} }
// Serve serves [Container.Params] to the container init.
// Serve must only be called once.
func (p *Container) Serve() error { func (p *Container) Serve() error {
if p.setup == nil { if p.setup == nil {
panic("invalid serve") panic("invalid serve")
@ -172,33 +189,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)
} }
@ -217,6 +217,7 @@ func (p *Container) Serve() error {
return err return err
} }
// Wait waits for the container init process to exit.
func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() } func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() }
func (p *Container) String() string { func (p *Container) String() string {
@ -224,8 +225,23 @@ func (p *Container) String() string {
p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets)) p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets))
} }
func New(ctx context.Context, name string, args ...string) *Container { // ProcessState returns the address to os.ProcessState held by the underlying [exec.Cmd].
return &Container{name: name, ctx: ctx, func (p *Container) ProcessState() *os.ProcessState {
Params: Params{Args: append([]string{name}, args...), Dir: "/", Ops: new(Ops)}, if p.cmd == nil {
return nil
} }
return p.cmd.ProcessState
}
// New returns the address to a new instance of [Container] that requires further initialisation before use.
func New(ctx context.Context) *Container {
return &Container{ctx: ctx, Params: Params{Ops: new(Ops)}}
}
// NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields.
func NewCommand(ctx context.Context, pathname *Absolute, name string, args ...string) *Container {
z := New(ctx)
z.Path = pathname
z.Args = append([]string{name}, args...)
return z
} }

View File

@ -4,172 +4,326 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/gob" "encoding/gob"
"errors"
"fmt"
"log" "log"
"os" "os"
"os/exec" "os/exec"
"os/signal"
"strconv"
"strings" "strings"
"syscall" "syscall"
"testing" "testing"
"time"
"hakurei.app/command"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/container/vfs" "hakurei.app/container/vfs"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/ldd"
) )
const ( const (
ignore = "\x00" ignore = "\x00"
ignoreV = -1 ignoreV = -1
pathPrefix = "/etc/hakurei/"
pathWantMnt = pathPrefix + "want-mnt"
pathReadonly = pathPrefix + "readonly"
) )
func TestContainer(t *testing.T) { type testVal any
{
oldVerbose := hlog.Load()
oldOutput := container.GetOutput()
internal.InstallOutput(true)
t.Cleanup(func() { hlog.Store(oldVerbose) })
t.Cleanup(func() { container.SetOutput(oldOutput) })
}
testCases := []struct { func emptyOps(t *testing.T) (*container.Ops, context.Context) { return new(container.Ops), t.Context() }
func earlyOps(ops *container.Ops) func(t *testing.T) (*container.Ops, context.Context) {
return func(t *testing.T) (*container.Ops, context.Context) { return ops, t.Context() }
}
func emptyMnt(*testing.T, context.Context) []*vfs.MountInfoEntry { return nil }
func earlyMnt(mnt ...*vfs.MountInfoEntry) func(*testing.T, context.Context) []*vfs.MountInfoEntry {
return func(*testing.T, context.Context) []*vfs.MountInfoEntry { return mnt }
}
var containerTestCases = []struct {
name string name string
filter bool filter bool
session bool session bool
net bool net bool
ops *container.Ops ro bool
mnt []*vfs.MountInfoEntry
host string ops func(t *testing.T) (*container.Ops, context.Context)
mnt func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry
uid int
gid int
rules []seccomp.NativeRule rules []seccomp.NativeRule
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, "test-minimal", emptyOps, emptyMnt,
nil, 0, seccomp.PresetStrict}, 1000, 100, nil, 0, seccomp.PresetStrict},
{"allow", true, true, true, {"allow", true, true, true, false,
new(container.Ops), nil, "test-minimal", emptyOps, emptyMnt,
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, "test-no-filter", emptyOps, emptyMnt,
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, "test-no-filter", emptyOps, emptyMnt,
[]seccomp.NativeRule{ 1, 31, []seccomp.NativeRule{{seccomp.ScmpSyscall(syscall.SYS_SETUID), seccomp.ScmpErrno(syscall.EPERM), nil}}, 0, seccomp.PresetExt},
{seccomp.ScmpSyscall(syscall.SYS_SETUID), seccomp.ScmpErrno(syscall.EPERM), nil},
}, 0, seccomp.PresetExt}, {"tmpfs", true, false, false, true,
{"tmpfs", true, false, false, earlyOps(new(container.Ops).
new(container.Ops). Tmpfs(hst.AbsTmp, 0, 0755),
Tmpfs(hst.Tmp, 0, 0755), ),
[]*vfs.MountInfoEntry{ earlyMnt(
e("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore), ent("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", ignore),
}, "test-tmpfs", ),
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{ ),
e("/", "/dev", "rw,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore), earlyMnt(
e("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
e("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), ent("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), ent("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), ent("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), ent("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore), ent("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"), ent("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"), 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"),
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"),
),
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)
}
} }
for _, tc := range testCases { return new(container.Ops).
Overlay(hst.AbsTmp, upper, work, lower0, lower1),
context.WithValue(context.WithValue(context.WithValue(context.WithValue(t.Context(),
testVal("lower1"), lower1),
testVal("lower0"), lower0),
testVal("work"), work),
testVal("upper"), upper)
},
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{
ent("/", hst.Tmp, "rw", "overlay", "overlay",
"rw,lowerdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+
",upperdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(*container.Absolute).String())+
",workdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("work")).(*container.Absolute).String())+
",redirect_dir=nofollow,uuid=on,userxattr"),
}
},
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
{"overlay ephemeral", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) {
tempDir := container.MustAbs(t.TempDir())
lower0, lower1 :=
tempDir.Append("lower0"),
tempDir.Append("lower1")
for _, a := range []*container.Absolute{lower0, lower1} {
if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err)
}
}
return new(container.Ops).
OverlayEphemeral(hst.AbsTmp, lower0, lower1),
t.Context()
},
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{
// contains random suffix
ent("/", hst.Tmp, "rw", "overlay", "overlay", ignore),
}
},
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
{"overlay readonly", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) {
tempDir := container.MustAbs(t.TempDir())
lower0, lower1 :=
tempDir.Append("lower0"),
tempDir.Append("lower1")
for _, a := range []*container.Absolute{lower0, lower1} {
if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err)
}
}
return new(container.Ops).
OverlayReadonly(hst.AbsTmp, lower0, lower1),
context.WithValue(context.WithValue(t.Context(),
testVal("lower1"), lower1),
testVal("lower0"), lower0)
},
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{
ent("/", hst.Tmp, "rw", "overlay", "overlay",
"ro,lowerdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+
",redirect_dir=nofollow,userxattr"),
}
},
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
}
func TestContainer(t *testing.T) {
replaceOutput(t)
t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) {
wantErr := context.Canceled
wantExitCode := 0
if err := c.Wait(); !errors.Is(err, wantErr) {
container.GetOutput().PrintBaseErr(err, "wait:")
t.Errorf("Wait: error = %v, want %v", err, wantErr)
}
if ps := c.ProcessState(); ps == nil {
t.Errorf("ProcessState unexpectedly returned nil")
} else if code := ps.ExitCode(); code != wantExitCode {
t.Errorf("ExitCode: %d, want %d", code, wantExitCode)
}
}))
t.Run("forward", testContainerCancel(func(c *container.Container) {
c.ForwardCancel = true
}, func(t *testing.T, c *container.Container) {
var exitError *exec.ExitError
if err := c.Wait(); !errors.As(err, &exitError) {
container.GetOutput().PrintBaseErr(err, "wait:")
t.Errorf("Wait: error = %v", err)
}
if code := exitError.ExitCode(); code != blockExitCodeInterrupt {
t.Errorf("ExitCode: %d, want %d", code, blockExitCodeInterrupt)
}
}))
for i, tc := range containerTestCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) wantOps, wantOpsCtx := tc.ops(t)
wantMnt := tc.mnt(t, wantOpsCtx)
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
defer cancel() defer cancel()
c := container.New(ctx, "/usr/bin/sandbox.test", "-test.v", var libPaths []*container.Absolute
"-test.run=TestHelperCheckContainer", "--", "check", tc.host) c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i))
c.Uid = 1000 c.Uid = tc.uid
c.Gid = 100 c.Gid = tc.gid
c.Hostname = tc.host c.Hostname = hostnameFromTestCase(tc.name)
c.CommandContext = commandContext 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.Ops = tc.ops }
c.WaitDelay = helperDefaultTimeout
*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
c.SeccompDisable = !tc.filter c.SeccompDisable = !tc.filter
c.RetainSession = tc.session c.RetainSession = tc.session
c.HostNet = tc.net c.HostNet = tc.net
if c.Args[5] == "" {
if name, err := os.Hostname(); err != nil {
t.Fatalf("cannot get hostname: %v", err)
} else {
c.Args[5] = name
}
}
c. c.
Tmpfs("/tmp", 0, 0755). Readonly(container.MustAbs(pathReadonly), 0755).
Bind(os.Args[0], os.Args[0], 0). Tmpfs(container.MustAbs("/tmp"), 0, 0755).
Mkdir("/usr/bin", 0755). Place(container.MustAbs("/etc/hostname"), []byte(c.Hostname))
Link(os.Args[0], "/usr/bin/sandbox.test").
Place("/etc/hostname", []byte(c.Args[5]))
// in case test has cgo enabled
var libPaths []string
if entries, err := ldd.ExecFilter(ctx,
commandContext,
func(v []byte) []byte {
return bytes.SplitN(v, []byte("TestHelperInit\n"), 2)[1]
}, os.Args[0]); err != nil {
log.Fatalf("ldd: %v", err)
} else {
libPaths = ldd.Path(entries)
}
for _, name := range libPaths {
c.Bind(name, name, 0)
}
// 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
mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths)) mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
mnt = append(mnt, e("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore))
mnt = append(mnt, tc.mnt...)
mnt = append(mnt, mnt = append(mnt,
e("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore), ent("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
e(ignore, os.Args[0], "ro,nosuid,nodev,relatime", ignore, ignore, ignore), // Bind(os.Args[0], helperInnerPath, 0)
e(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore), ent(ignore, helperInnerPath, "ro,nosuid,nodev,relatime", ignore, ignore, ignore),
) )
for _, name := range libPaths { for _, a := range libPaths {
mnt = append(mnt, e(ignore, name, "ro,nosuid,nodev,relatime", ignore, ignore, ignore)) // Bind(name, name, 0)
mnt = append(mnt, ent(ignore, a.String(), "ro,nosuid,nodev,relatime", ignore, ignore, ignore))
} }
mnt = append(mnt, e("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw")) mnt = append(mnt, wantMnt...)
mnt = append(mnt,
// Readonly(pathReadonly, 0755)
ent("/", pathReadonly, "ro,nosuid,nodev", "tmpfs", "readonly", ignore),
// Tmpfs("/tmp", 0, 0755)
ent("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", ignore),
// Place("/etc/hostname", []byte(hostname))
ent(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
// Proc("/proc")
ent("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"),
// Place(pathWantMnt, want.Bytes())
ent(ignore, pathWantMnt, "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
)
want := new(bytes.Buffer) 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.Stdin = want c.Place(container.MustAbs(pathWantMnt), want.Bytes())
if tc.ro {
c.Remount(container.MustAbs("/"), syscall.MS_RDONLY)
}
if err := c.Start(); err != nil { 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)
} }
}) })
} }
} }
func e(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry { func ent(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry {
return &vfs.MountInfoEntry{ return &vfs.MountInfoEntry{
ID: ignoreV, ID: ignoreV,
Parent: ignoreV, Parent: ignoreV,
@ -184,8 +338,52 @@ func e(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoE
} }
} }
func hostnameFromTestCase(name string) string {
return "test-" + strings.Join(strings.Fields(name), "-")
}
func testContainerCancel(
containerExtra func(c *container.Container),
waitCheck func(t *testing.T, c *container.Container),
) func(t *testing.T) {
return func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
c := helperNewContainer(ctx, "block")
c.Stdout, c.Stderr = os.Stdout, os.Stderr
c.WaitDelay = helperDefaultTimeout
if containerExtra != nil {
containerExtra(c)
}
ready := make(chan struct{})
if r, w, err := os.Pipe(); err != nil {
t.Fatalf("cannot pipe: %v", err)
} else {
c.ExtraFiles = append(c.ExtraFiles, w)
go func() {
defer close(ready)
if _, err = r.Read(make([]byte, 1)); err != nil {
panic(err.Error())
}
}()
}
if err := c.Start(); err != nil {
container.GetOutput().PrintBaseErr(err, "start:")
t.Fatalf("cannot start container: %v", err)
} else if err = c.Serve(); err != nil {
container.GetOutput().PrintBaseErr(err, "serve:")
t.Errorf("cannot serve setup params: %v", err)
}
<-ready
cancel()
waitCheck(t, c)
}
}
func TestContainerString(t *testing.T) { 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,
@ -197,49 +395,79 @@ func TestContainerString(t *testing.T) {
} }
} }
func TestHelperInit(t *testing.T) { const (
if len(os.Args) != 5 || os.Args[4] != "init" { blockExitCodeInterrupt = 2
return )
}
container.SetOutput(hlog.Output{})
container.Init(hlog.Prepare, internal.InstallOutput)
}
func TestHelperCheckContainer(t *testing.T) { func init() {
if len(os.Args) != 6 || os.Args[4] != "check" { helperCommands = append(helperCommands, func(c command.Command) {
return c.Command("block", command.UsageInternal, func(args []string) error {
if _, err := os.NewFile(3, "sync").Write([]byte{0}); err != nil {
return fmt.Errorf("write to sync pipe: %v", err)
} }
{
t.Run("user", func(t *testing.T) { sig := make(chan os.Signal, 1)
if uid := syscall.Getuid(); uid != 1000 { signal.Notify(sig, os.Interrupt)
t.Errorf("Getuid: %d, want 1000", uid) go func() { <-sig; os.Exit(blockExitCodeInterrupt) }()
}
if gid := syscall.Getgid(); gid != 100 {
t.Errorf("Getgid: %d, want 100", gid)
} }
select {}
}) })
t.Run("hostname", func(t *testing.T) {
if name, err := os.Hostname(); err != nil { c.Command("container", command.UsageInternal, func(args []string) error {
t.Fatalf("cannot get hostname: %v", err) if len(args) != 1 {
} else if name != os.Args[5] { return syscall.EINVAL
t.Errorf("Hostname: %q, want %q", name, os.Args[5]) }
tc := containerTestCases[0]
if i, err := strconv.Atoi(args[0]); err != nil {
return fmt.Errorf("cannot parse test case index: %v", err)
} else {
tc = containerTestCases[i]
}
if uid := syscall.Getuid(); uid != tc.uid {
return fmt.Errorf("uid: %d, want %d", uid, tc.uid)
}
if gid := syscall.Getgid(); gid != tc.gid {
return fmt.Errorf("gid: %d, want %d", gid, tc.gid)
}
wantHost := hostnameFromTestCase(tc.name)
if host, err := os.Hostname(); err != nil {
return fmt.Errorf("cannot get hostname: %v", err)
} else if host != wantHost {
return fmt.Errorf("hostname: %q, want %q", host, wantHost)
} }
if p, err := os.ReadFile("/etc/hostname"); err != nil { if p, err := os.ReadFile("/etc/hostname"); err != nil {
t.Fatalf("%v", err) return fmt.Errorf("cannot read /etc/hostname: %v", err)
} else if string(p) != os.Args[5] { } else if string(p) != wantHost {
t.Errorf("/etc/hostname: %q, want %q", string(p), os.Args[5]) return fmt.Errorf("/etc/hostname: %q, want %q", string(p), wantHost)
} }
})
t.Run("mount", func(t *testing.T) { if _, err := os.Create(pathReadonly + "/nonexistent"); !errors.Is(err, syscall.EROFS) {
return err
}
{
var fail bool
var mnt []*vfs.MountInfoEntry var mnt []*vfs.MountInfoEntry
if err := gob.NewDecoder(os.Stdin).Decode(&mnt); err != nil { if f, err := os.Open(pathWantMnt); err != nil {
t.Fatalf("cannot receive expected mount points: %v", err) return fmt.Errorf("cannot open expected mount points: %v", err)
} else if err = gob.NewDecoder(f).Decode(&mnt); err != nil {
return fmt.Errorf("cannot parse expected mount points: %v", err)
} else if err = f.Close(); err != nil {
return fmt.Errorf("cannot close expected mount points: %v", err)
}
if tc.ro && len(mnt) > 0 {
// Remount("/", syscall.MS_RDONLY)
mnt[0].VfsOptstr = "ro,nosuid,nodev"
} }
var d *vfs.MountInfoDecoder var d *vfs.MountInfoDecoder
if f, err := os.Open("/proc/self/mountinfo"); err != nil { if f, err := os.Open("/proc/self/mountinfo"); err != nil {
t.Fatalf("cannot open mountinfo: %v", err) return fmt.Errorf("cannot open mountinfo: %v", err)
} else { } else {
d = vfs.NewMountInfoDecoder(f) d = vfs.NewMountInfoDecoder(f)
} }
@ -247,8 +475,7 @@ func TestHelperCheckContainer(t *testing.T) {
i := 0 i := 0
for cur := range d.Entries() { for cur := range d.Entries() {
if i == len(mnt) { if i == len(mnt) {
t.Errorf("got more than %d entries", len(mnt)) return fmt.Errorf("got more than %d entries", len(mnt))
break
} }
// ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags // ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags
@ -258,24 +485,28 @@ func TestHelperCheckContainer(t *testing.T) {
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime") mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime")
if !cur.EqualWithIgnore(mnt[i], "\x00") { if !cur.EqualWithIgnore(mnt[i], "\x00") {
t.Errorf("[FAIL] %s", cur) fail = true
log.Printf("[FAIL] %s", cur)
} else { } else {
t.Logf("[ OK ] %s", cur) log.Printf("[ OK ] %s", cur)
} }
i++ i++
} }
if err := d.Err(); err != nil { if err := d.Err(); err != nil {
t.Errorf("cannot parse mountinfo: %v", err) return fmt.Errorf("cannot parse mountinfo: %v", err)
} }
if i != len(mnt) { if i != len(mnt) {
t.Errorf("got %d entries, want %d", i, len(mnt)) return fmt.Errorf("got %d entries, want %d", i, len(mnt))
} }
if fail {
return errors.New("one or more mountinfo entries do not match")
}
}
return nil
})
}) })
} }
func commandContext(ctx context.Context) *exec.Cmd {
return exec.CommandContext(ctx, os.Args[0], "-test.v",
"-test.run=TestHelperInit", "--", "init")
}

View File

@ -18,9 +18,6 @@ import (
) )
const ( const (
// time to wait for linger processes after death of initial process
residualProcessTimeout = 5 * time.Second
/* intermediate tmpfs mount point /* intermediate tmpfs mount point
this path might seem like a weird choice, however there are many good reasons to use it: this path might seem like a weird choice, however there are many good reasons to use it:
@ -35,7 +32,7 @@ const (
it should be noted that none of this should become relevant at any point since the resulting it should be noted that none of this should become relevant at any point since the resulting
intermediate root tmpfs should be effectively anonymous */ intermediate root tmpfs should be effectively anonymous */
intermediateHostPath = "/proc/self/fd" intermediateHostPath = FHSProc + "self/fd"
// setup params file descriptor // setup params file descriptor
setupEnv = "HAKUREI_SETUP" setupEnv = "HAKUREI_SETUP"
@ -66,7 +63,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
offsetSetup int offsetSetup int
) )
if f, err := Receive(setupEnv, &params, &setupFile); err != nil { if f, err := Receive(setupEnv, &params, &setupFile); err != nil {
if errors.Is(err, ErrInvalid) { if errors.Is(err, EBADF) {
log.Fatal("invalid setup descriptor") log.Fatal("invalid setup descriptor")
} }
if errors.Is(err, ErrNotSet) { if errors.Is(err, ErrNotSet) {
@ -92,17 +89,17 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
if err := SetDumpable(SUID_DUMP_USER); err != nil { if err := SetDumpable(SUID_DUMP_USER); err != nil {
log.Fatalf("cannot set SUID_DUMP_USER: %s", err) log.Fatalf("cannot set SUID_DUMP_USER: %s", err)
} }
if err := os.WriteFile("/proc/self/uid_map", if err := os.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) log.Fatalf("%v", err)
} }
if err := os.WriteFile("/proc/self/setgroups", if err := os.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) log.Fatalf("%v", err)
} }
if err := os.WriteFile("/proc/self/gid_map", if err := os.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) log.Fatalf("%v", err)
@ -121,16 +118,22 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
// cache sysctl before pivot_root // cache sysctl before pivot_root
LastCap() LastCap()
if err := Mount("", "/", "", MS_SILENT|MS_SLAVE|MS_REC, ""); err != nil { if err := Mount(zeroString, FHSRoot, zeroString, MS_SILENT|MS_SLAVE|MS_REC, zeroString); err != nil {
log.Fatalf("cannot make / rslave: %v", err) log.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 {
log.Fatalf("invalid op %d", i) log.Fatalf("invalid op %d", i)
} }
if err := op.early(&params.Params); err != nil { if err := op.early(state); err != nil {
msg.PrintBaseErr(err, msg.PrintBaseErr(err,
fmt.Sprintf("cannot prepare op %d:", i)) fmt.Sprintf("cannot prepare op %d:", i))
msg.BeforeExit() msg.BeforeExit()
@ -138,7 +141,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
} }
} }
if err := Mount("rootfs", intermediateHostPath, "tmpfs", MS_NODEV|MS_NOSUID, ""); err != nil { if err := Mount(SourceTmpfsRootfs, intermediateHostPath, FstypeTmpfs, MS_NODEV|MS_NOSUID, zeroString); err != nil {
log.Fatalf("cannot mount intermediate root: %v", err) log.Fatalf("cannot mount intermediate root: %v", err)
} }
if err := os.Chdir(intermediateHostPath); err != nil { if err := os.Chdir(intermediateHostPath); err != nil {
@ -148,7 +151,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
if err := os.Mkdir(sysrootDir, 0755); err != nil { if err := os.Mkdir(sysrootDir, 0755); err != nil {
log.Fatalf("%v", err) log.Fatalf("%v", err)
} }
if err := Mount(sysrootDir, sysrootDir, "", MS_SILENT|MS_MGC_VAL|MS_BIND|MS_REC, ""); err != nil { if err := Mount(sysrootDir, sysrootDir, zeroString, MS_SILENT|MS_BIND|MS_REC, zeroString); err != nil {
log.Fatalf("cannot bind sysroot: %v", err) log.Fatalf("cannot bind sysroot: %v", err)
} }
@ -159,14 +162,18 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
if err := PivotRoot(intermediateHostPath, hostDir); err != nil { if err := PivotRoot(intermediateHostPath, hostDir); err != nil {
log.Fatalf("cannot pivot into intermediate root: %v", err) log.Fatalf("cannot pivot into intermediate root: %v", err)
} }
if err := os.Chdir("/"); err != nil { if err := os.Chdir(FHSRoot); err != nil {
log.Fatalf("%v", err) log.Fatalf("%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) msg.Verbosef("%s %s", op.prefix(), op)
if err := op.apply(&params.Params); err != nil { if err := op.apply(state); err != nil {
msg.PrintBaseErr(err, msg.PrintBaseErr(err,
fmt.Sprintf("cannot apply op %d:", i)) fmt.Sprintf("cannot apply op %d:", i))
msg.BeforeExit() msg.BeforeExit()
@ -175,7 +182,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
} }
// 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 := Mount(hostDir, hostDir, zeroString, MS_SILENT|MS_REC|MS_PRIVATE, zeroString); err != nil {
log.Fatalf("cannot make host root rprivate: %v", err) log.Fatalf("cannot make host root rprivate: %v", err)
} }
if err := Unmount(hostDir, MNT_DETACH); err != nil { if err := Unmount(hostDir, MNT_DETACH); err != nil {
@ -185,7 +192,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
{ {
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 = Open(FHSRoot, O_DIRECTORY|O_RDONLY, 0)
return return
}); err != nil { }); err != nil {
log.Fatalf("cannot open intermediate root: %v", err) log.Fatalf("cannot open intermediate root: %v", err)
@ -203,7 +210,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
if err := Unmount(".", MNT_DETACH); err != nil { if err := Unmount(".", MNT_DETACH); err != nil {
log.Fatalf("cannot unmount intemediate root: %v", err) log.Fatalf("cannot unmount intemediate root: %v", err)
} }
if err := os.Chdir("/"); err != nil { if err := os.Chdir(FHSRoot); err != nil {
log.Fatalf("%v", err) log.Fatalf("%v", err)
} }
@ -270,20 +277,21 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
} }
Umask(oldmask) 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)
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
log.Fatalf("%v", err) log.Fatalf("%v", err)
} }
msg.Suspend() msg.Suspend()
if err := closeSetup(); err != nil { if err := closeSetup(); err != nil {
log.Println("cannot close setup pipe:", err) log.Printf("cannot close setup pipe: %v", err)
// not fatal // not fatal
} }
@ -317,7 +325,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
} }
} }
if !errors.Is(err, ECHILD) { if !errors.Is(err, ECHILD) {
log.Println("unexpected wait4 response:", err) log.Printf("unexpected wait4 response: %v", err)
} }
close(done) close(done)
@ -325,7 +333,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
// 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, SIGINT, SIGTERM) signal.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{})
@ -335,9 +343,16 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
select { select {
case s := <-sig: case s := <-sig:
if msg.Resume() { if msg.Resume() {
msg.Verbosef("terminating on %s after process start", s.String()) msg.Verbosef("%s after process start", s.String())
} else { } else {
msg.Verbosef("terminating on %s", s.String()) msg.Verbosef("got %s", s.String())
}
if s == CancelSignal && params.ForwardCancel && cmd.Process != nil {
msg.Verbose("forwarding context cancellation")
if err := cmd.Process.Signal(os.Interrupt); err != nil {
log.Printf("cannot forward cancellation: %v", err)
}
continue
} }
os.Exit(0) os.Exit(0)
case w := <-info: case w := <-info:
@ -357,10 +372,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
msg.Verbosef("initial process exited with status %#x", w.wstatus) msg.Verbosef("initial process exited with status %#x", w.wstatus)
} }
go func() { go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
time.Sleep(residualProcessTimeout)
close(timeout)
}()
} }
case <-done: case <-done:
msg.BeforeExit() msg.BeforeExit()
@ -373,9 +385,11 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
} }
} }
const initName = "init"
// TryArgv0 calls [Init] if the last element of argv0 is "init". // TryArgv0 calls [Init] if the last element of argv0 is "init".
func TryArgv0(v Msg, prepare func(prefix string), setVerbose func(verbose bool)) { func TryArgv0(v Msg, prepare func(prefix string), setVerbose func(verbose bool)) {
if len(os.Args) > 0 && path.Base(os.Args[0]) == "init" { if len(os.Args) > 0 && path.Base(os.Args[0]) == initName {
msg = v msg = v
Init(prepare, setVerbose) Init(prepare, setVerbose)
msg.BeforeExit() msg.BeforeExit()

73
container/init_test.go Normal file
View File

@ -0,0 +1,73 @@
package container_test
import (
"context"
"log"
"os"
"testing"
"time"
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/ldd"
)
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...)
}

View File

@ -5,11 +5,96 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "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"
// 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 {
if eq { if eq {
msg.Verbosef("resolved %q flags %#x", target, flags) msg.Verbosef("resolved %q flags %#x", target, flags)
@ -17,11 +102,16 @@ func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) err
msg.Verbosef("resolved %q on %q flags %#x", source, target, flags) msg.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 := 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.remount(target, flags)
}
// remount applies flags on target, recursively if MS_REC is set.
func (p *procPaths) remount(target string, flags uintptr) error {
var targetFinal string var targetFinal string
if v, err := filepath.EvalSymlinks(target); err != nil { if v, err := filepath.EvalSymlinks(target); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
@ -83,6 +173,7 @@ func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) err
}) })
} }
// remountWithFlags remounts mount point described by [vfs.MountInfoNode].
func remountWithFlags(n *vfs.MountInfoNode, mf uintptr) error { func remountWithFlags(n *vfs.MountInfoNode, mf uintptr) error {
kf, unmatched := n.Flags() kf, unmatched := n.Flags()
if len(unmatched) != 0 { if len(unmatched) != 0 {
@ -91,14 +182,15 @@ func remountWithFlags(n *vfs.MountInfoNode, mf uintptr) error {
if kf&mf != mf { if kf&mf != mf {
return wrapErrSuffix( return wrapErrSuffix(
Mount("none", n.Clean, "", MS_SILENT|MS_BIND|MS_REMOUNT|kf|mf, ""), Mount(SourceNone, n.Clean, FstypeNULL, MS_SILENT|MS_BIND|MS_REMOUNT|kf|mf, zeroString),
fmt.Sprintf("cannot remount %q:", n.Clean)) 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.
func mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
if err := os.MkdirAll(target, parentPerm(perm)); err != nil { if err := os.MkdirAll(target, parentPerm(perm)); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
} }
@ -107,8 +199,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), 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 +213,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)
}

49
container/mount_test.go Normal file
View File

@ -0,0 +1,49 @@
package container
import (
"os"
"testing"
)
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)
}
})
}
}

139
container/msg_test.go Normal file
View File

@ -0,0 +1,139 @@
package container_test
import (
"log"
"strings"
"sync/atomic"
"syscall"
"testing"
"hakurei.app/container"
"hakurei.app/internal/hlog"
)
func TestDefaultMsg(t *testing.T) {
{
w := log.Writer()
f := log.Flags()
t.Cleanup(func() { log.SetOutput(w); log.SetFlags(f) })
}
msg := new(container.DefaultMsg)
t.Run("is verbose", func(t *testing.T) {
if !msg.IsVerbose() {
t.Error("IsVerbose unexpected outcome")
}
})
t.Run("verbose", func(t *testing.T) {
log.SetOutput(panicWriter{})
msg.Suspend()
msg.Verbose()
msg.Verbosef("\x00")
msg.Resume()
buf := new(strings.Builder)
log.SetOutput(buf)
log.SetFlags(0)
msg.Verbose()
msg.Verbosef("\x00")
want := "\n\x00\n"
if buf.String() != want {
t.Errorf("Verbose: %q, want %q", buf.String(), want)
}
})
t.Run("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() })
}
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

@ -13,56 +13,115 @@ import (
"unsafe" "unsafe"
) )
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.*"
// intermediate root file name pattern for [TmpfileOp]
intermediatePatternTmpfile = "tmp.*"
)
const (
nrAutoEtc = 1 << iota
nrAutoRoot
)
type ( type (
Ops []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 { Op interface {
// early is called in host root. // early is called in host root.
early(params *Params) error early(state *setupState) error
// apply is called in intermediate root. // apply is called in intermediate root.
apply(params *Params) error apply(state *setupState) error
prefix() string prefix() string
Is(op Op) bool Is(op Op) bool
fmt.Stringer fmt.Stringer
} }
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) } func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) }
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
}
type RemountOp struct {
Target *Absolute
Flags uintptr
}
func (*RemountOp) early(*setupState) error { return nil }
func (r *RemountOp) apply(*setupState) error {
if r.Target == nil {
return EBADE
}
return wrapErrSuffix(hostProc.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 == *vr }
func (*RemountOp) prefix() string { return "remounting" }
func (r *RemountOp) String() string { return fmt.Sprintf("%q flags %#x", r.Target, r.Flags) }
func init() { gob.Register(new(BindMountOp)) } func init() { gob.Register(new(BindMountOp)) }
// BindMountOp bind mounts host path Source on container path Target. // Bind appends an [Op] that bind mounts host path [BindMountOp.Source] on container path [BindMountOp.Target].
func (f *Ops) Bind(source, target *Absolute, flags int) *Ops {
*f = append(*f, &BindMountOp{nil, source, target, flags})
return f
}
type BindMountOp struct { type BindMountOp struct {
Source, SourceFinal, Target string sourceFinal, Source, Target *Absolute
Flags int Flags int
} }
const ( const (
// BindOptional skips nonexistent host paths.
BindOptional = 1 << iota BindOptional = 1 << iota
// BindWritable mounts filesystem read-write.
BindWritable BindWritable
// BindDevice allows access to devices (special files) on this filesystem.
BindDevice BindDevice
) )
func (b *BindMountOp) early(*Params) error { func (b *BindMountOp) early(*setupState) error {
if !path.IsAbs(b.Source) { if b.Source == nil || b.Target == nil {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", b.Source)) return EBADE
} }
if v, err := filepath.EvalSymlinks(b.Source); err != nil { if pathname, err := filepath.EvalSymlinks(b.Source.String()); err != nil {
if os.IsNotExist(err) && b.Flags&BindOptional != 0 { if os.IsNotExist(err) && b.Flags&BindOptional != 0 {
b.SourceFinal = "\x00" // leave sourceFinal as nil
return nil return nil
} }
return wrapErrSelf(err) return wrapErrSelf(err)
} else { } else {
b.SourceFinal = v b.sourceFinal, err = NewAbs(pathname)
return nil return err
} }
} }
func (b *BindMountOp) apply(*Params) error { func (b *BindMountOp) apply(*setupState) error {
if b.SourceFinal == "\x00" { if b.sourceFinal == nil {
if b.Flags&BindOptional == 0 { if b.Flags&BindOptional == 0 {
// unreachable // unreachable
return EBADE return EBADE
@ -70,12 +129,8 @@ func (b *BindMountOp) apply(*Params) error {
return nil return nil
} }
if !path.IsAbs(b.SourceFinal) || !path.IsAbs(b.Target) { source := toHost(b.sourceFinal.String())
return msg.WrapErr(EBADE, "path is not absolute") target := toSysroot(b.Target.String())
}
source := toHost(b.SourceFinal)
target := toSysroot(b.Target)
// this perm value emulates bwrap behaviour as it clears bits from 0755 based on // 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 // op->perms which is never set for any bind setup op so always results in 0700
@ -97,7 +152,7 @@ func (b *BindMountOp) apply(*Params) error {
flags |= MS_NODEV flags |= MS_NODEV
} }
return hostProc.bindMount(source, target, flags, b.SourceFinal == b.Target) 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 (b *BindMountOp) Is(op Op) bool { vb, ok := op.(*BindMountOp); return ok && *b == *vb }
@ -106,67 +161,80 @@ func (b *BindMountOp) String() string {
if b.Source == b.Target { if b.Source == b.Target {
return fmt.Sprintf("%q flags %#x", b.Source, b.Flags) 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) return fmt.Sprintf("%q on %q flags %#x", b.Source, b.Target, b.Flags)
}
func (f *Ops) Bind(source, target string, flags int) *Ops {
*f = append(*f, &BindMountOp{source, "", target, flags})
return f
} }
func init() { gob.Register(new(MountProcOp)) } func init() { gob.Register(new(MountProcOp)) }
// MountProcOp mounts a private instance of proc. // Proc appends an [Op] that mounts a private instance of proc.
type MountProcOp string func (f *Ops) Proc(target *Absolute) *Ops {
*f = append(*f, &MountProcOp{target})
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 (f *Ops) Proc(dest string) *Ops {
*f = append(*f, MountProcOp(dest))
return f return f
} }
type MountProcOp struct {
Target *Absolute
}
func (p *MountProcOp) early(*setupState) error { return nil }
func (p *MountProcOp) apply(state *setupState) error {
if p.Target == nil {
return EBADE
}
target := toSysroot(p.Target.String())
if err := os.MkdirAll(target, state.ParentPerm); err != nil {
return wrapErrSelf(err)
}
return wrapErrSuffix(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 == nil && vp == nil) || p == vp)
}
func (*MountProcOp) prefix() string { return "mounting" }
func (p *MountProcOp) String() string { return fmt.Sprintf("proc on %q", p.Target) }
func init() { gob.Register(new(MountDevOp)) } func init() { gob.Register(new(MountDevOp)) }
// MountDevOp mounts part of host dev. // Dev appends an [Op] that mounts a subset of host /dev.
type MountDevOp string func (f *Ops) Dev(target *Absolute, mqueue bool) *Ops {
*f = append(*f, &MountDevOp{target, mqueue, false})
return f
}
func (d MountDevOp) early(*Params) error { return nil } // DevWritable appends an [Op] that mounts a writable subset of host /dev.
func (d MountDevOp) apply(params *Params) error { // There is usually no good reason to write to /dev, so this should always be followed by a [RemountOp].
v := string(d) func (f *Ops) DevWritable(target *Absolute, mqueue bool) *Ops {
*f = append(*f, &MountDevOp{target, mqueue, true})
return f
}
if !path.IsAbs(v) { type MountDevOp struct {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", v)) Target *Absolute
Mqueue bool
Write bool
}
func (d *MountDevOp) early(*setupState) error { return nil }
func (d *MountDevOp) apply(state *setupState) error {
if d.Target == nil {
return EBADE
} }
target := toSysroot(v) target := toSysroot(d.Target.String())
if err := mountTmpfs("devtmpfs", v, 0, params.ParentPerm); err != nil { if err := mountTmpfs(SourceTmpfsDevtmpfs, target, MS_NOSUID|MS_NODEV, 0, state.ParentPerm); err != nil {
return err return err
} }
for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} { for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} {
targetPath := toSysroot(path.Join(v, name)) targetPath := path.Join(target, name)
if err := ensureFile(targetPath, 0444, params.ParentPerm); err != nil { if err := ensureFile(targetPath, 0444, state.ParentPerm); err != nil {
return err return err
} }
if err := hostProc.bindMount( if err := hostProc.bindMount(
toHost("/dev/"+name), toHost(FHSDev+name),
targetPath, targetPath,
0, 0,
true, true,
@ -176,15 +244,15 @@ func (d MountDevOp) apply(params *Params) error {
} }
for i, name := range []string{"stdin", "stdout", "stderr"} { for i, name := range []string{"stdin", "stdout", "stderr"} {
if err := os.Symlink( if err := os.Symlink(
"/proc/self/fd/"+string(rune(i+'0')), FHSProc+"self/fd/"+string(rune(i+'0')),
path.Join(target, name), path.Join(target, name),
); err != nil { ); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
} }
} }
for _, pair := range [][2]string{ for _, pair := range [][2]string{
{"/proc/self/fd", "fd"}, {FHSProc + "self/fd", "fd"},
{"/proc/kcore", "core"}, {FHSProc + "kcore", "core"},
{"pts/ptmx", "ptmx"}, {"pts/ptmx", "ptmx"},
} { } {
if err := os.Symlink(pair[0], path.Join(target, pair[1])); err != nil { if err := os.Symlink(pair[0], path.Join(target, pair[1])); err != nil {
@ -194,22 +262,22 @@ func (d MountDevOp) apply(params *Params) error {
devPtsPath := path.Join(target, "pts") devPtsPath := path.Join(target, "pts")
for _, name := range []string{path.Join(target, "shm"), devPtsPath} { for _, name := range []string{path.Join(target, "shm"), devPtsPath} {
if err := os.Mkdir(name, params.ParentPerm); err != nil { if err := os.Mkdir(name, state.ParentPerm); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
} }
} }
if err := Mount("devpts", devPtsPath, "devpts", MS_NOSUID|MS_NOEXEC, if err := Mount(SourceDevpts, devPtsPath, FstypeDevpts, MS_NOSUID|MS_NOEXEC,
"newinstance,ptmxmode=0666,mode=620"); err != nil { "newinstance,ptmxmode=0666,mode=620"); err != nil {
return wrapErrSuffix(err, return wrapErrSuffix(err,
fmt.Sprintf("cannot mount devpts on %q:", devPtsPath)) fmt.Sprintf("cannot mount devpts on %q:", devPtsPath))
} }
if params.RetainSession { if state.RetainSession {
var buf [8]byte var buf [8]byte
if _, _, errno := Syscall(SYS_IOCTL, 1, TIOCGWINSZ, uintptr(unsafe.Pointer(&buf[0]))); errno == 0 { if _, _, errno := Syscall(SYS_IOCTL, 1, TIOCGWINSZ, uintptr(unsafe.Pointer(&buf[0]))); errno == 0 {
consolePath := toSysroot(path.Join(v, "console")) consolePath := path.Join(target, "console")
if err := ensureFile(consolePath, 0444, params.ParentPerm); err != nil { if err := ensureFile(consolePath, 0444, state.ParentPerm); err != nil {
return err return err
} }
if name, err := os.Readlink(hostProc.stdout()); err != nil { if name, err := os.Readlink(hostProc.stdout()); err != nil {
@ -225,104 +293,263 @@ func (d MountDevOp) apply(params *Params) error {
} }
} }
return nil if d.Mqueue {
} mqueueTarget := path.Join(target, "mqueue")
if err := os.Mkdir(mqueueTarget, state.ParentPerm); err != 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 (f *Ops) Dev(dest string) *Ops {
*f = append(*f, MountDevOp(dest))
return f
}
func init() { gob.Register(new(MountMqueueOp)) }
// MountMqueueOp mounts a private mqueue instance on container Path.
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 wrapErrSelf(err)
} }
return wrapErrSuffix(Mount("mqueue", target, "mqueue", MS_NOSUID|MS_NOEXEC|MS_NODEV, ""), if err := Mount(SourceMqueue, mqueueTarget, FstypeMqueue, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString); err != nil {
fmt.Sprintf("cannot mount mqueue on %q:", v)) return wrapErrSuffix(err, "cannot mount mqueue:")
}
}
if d.Write {
return nil
}
return wrapErrSuffix(hostProc.remount(target, MS_RDONLY),
fmt.Sprintf("cannot remount %q:", target))
} }
func (m MountMqueueOp) Is(op Op) bool { vm, ok := op.(MountMqueueOp); return ok && m == vm } func (d *MountDevOp) Is(op Op) bool { vd, ok := op.(*MountDevOp); return ok && *d == *vd }
func (MountMqueueOp) prefix() string { return "mounting" } func (*MountDevOp) prefix() string { return "mounting" }
func (m MountMqueueOp) String() string { return fmt.Sprintf("mqueue on %q", string(m)) } func (d *MountDevOp) String() string {
func (f *Ops) Mqueue(dest string) *Ops { if d.Mqueue {
*f = append(*f, MountMqueueOp(dest)) return fmt.Sprintf("dev on %q with mqueue", d.Target)
return f }
return fmt.Sprintf("dev on %q", d.Target)
} }
func init() { gob.Register(new(MountTmpfsOp)) } func init() { gob.Register(new(MountTmpfsOp)) }
// MountTmpfsOp mounts tmpfs on container Path. // Tmpfs appends an [Op] that mounts tmpfs on container path [MountTmpfsOp.Path].
func (f *Ops) Tmpfs(target *Absolute, size int, perm os.FileMode) *Ops {
*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
}
type MountTmpfsOp struct { type MountTmpfsOp struct {
Path string FSName string
Path *Absolute
Flags uintptr
Size int Size int
Perm os.FileMode Perm os.FileMode
} }
func (t *MountTmpfsOp) early(*Params) error { return nil } func (t *MountTmpfsOp) early(*setupState) error { return nil }
func (t *MountTmpfsOp) apply(*Params) error { func (t *MountTmpfsOp) apply(*setupState) error {
if !path.IsAbs(t.Path) { if t.Path == nil {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", t.Path)) return EBADE
} }
if t.Size < 0 || t.Size > math.MaxUint>>1 { if t.Size < 0 || t.Size > math.MaxUint>>1 {
return msg.WrapErr(EBADE, fmt.Sprintf("size %d out of bounds", t.Size)) return msg.WrapErr(EBADE, fmt.Sprintf("size %d out of bounds", t.Size))
} }
return mountTmpfs("tmpfs", t.Path, t.Size, t.Perm) return 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 == *vt } func (t *MountTmpfsOp) Is(op Op) bool { vt, ok := op.(*MountTmpfsOp); return ok && *t == *vt }
func (*MountTmpfsOp) prefix() string { return "mounting" } func (*MountTmpfsOp) prefix() string { return "mounting" }
func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) } func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }
func (f *Ops) Tmpfs(dest string, size int, perm os.FileMode) *Ops {
*f = append(*f, &MountTmpfsOp{dest, size, perm}) 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 return f
} }
func init() { gob.Register(new(SymlinkOp)) } // 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...)
}
// SymlinkOp creates a symlink in the container filesystem. // OverlayReadonly appends an [Op] that mounts the overlay pseudo filesystem readonly on [MountOverlayOp.Target]
type SymlinkOp [2]string func (f *Ops) OverlayReadonly(target *Absolute, layers ...*Absolute) *Ops {
return f.Overlay(target, nil, nil, layers...)
}
func (l *SymlinkOp) early(*Params) error { type MountOverlayOp struct {
if strings.HasPrefix(l[0], "*") { Target *Absolute
l[0] = l[0][1:]
if !path.IsAbs(l[0]) { // Any filesystem, does not need to be on a writable filesystem.
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", l[0])) 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 [FHSRoot],
// an ephemeral upperdir and workdir will be set up.
//
// If both Work and Upper are empty strings, 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
}
func (o *MountOverlayOp) early(*setupState) 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(EINVAL, fmt.Sprintf("upperdir has unexpected value %q", o.Upper))
} }
if name, err := os.Readlink(l[0]); err != nil { }
// readonly handled in apply
if !o.ephemeral {
if o.Upper != o.Work && (o.Upper == nil || o.Work == nil) {
// unreachable
return msg.WrapErr(ENOTRECOVERABLE, "impossible overlay state reached")
}
if o.Upper != nil {
if v, err := filepath.EvalSymlinks(o.Upper.String()); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
} else { } else {
l[0] = name o.upper = EscapeOverlayDataSegment(toHost(v))
}
}
if o.Work != nil {
if v, err := filepath.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 {
if a == nil {
return EBADE
}
if v, err := filepath.EvalSymlinks(a.String()); err != nil {
return wrapErrSelf(err)
} else {
o.lower[i] = EscapeOverlayDataSegment(toHost(v))
} }
} }
return nil 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]) func (o *MountOverlayOp) apply(state *setupState) error {
if err := os.MkdirAll(path.Dir(target), params.ParentPerm); err != nil { if o.Target == nil {
return EBADE
}
target := toSysroot(o.Target.String())
if err := os.MkdirAll(target, state.ParentPerm); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
} }
if err := os.Symlink(l[0], target); err != nil {
if o.ephemeral {
var err error
// these directories are created internally, therefore early (absolute, symlink, prefix, escape) is bypassed
if o.upper, err = os.MkdirTemp(FHSRoot, intermediatePatternOverlayUpper); err != nil {
return wrapErrSelf(err)
}
if o.work, err = os.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(EINVAL, "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(EINVAL, "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(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.Target == vo.Target &&
slices.Equal(o.Lower, vo.Lower) &&
o.Upper == vo.Upper &&
o.Work == 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))
}
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
}
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) early(*setupState) error {
if l.Dereference {
if !isAbs(l.LinkName) {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", l.LinkName))
}
if name, err := os.Readlink(l.LinkName); err != nil {
return wrapErrSelf(err)
} else {
l.LinkName = name
}
}
return nil
}
func (l *SymlinkOp) apply(state *setupState) error {
if l.Target == nil {
return EBADE
}
target := toSysroot(l.Target.String())
if err := os.MkdirAll(path.Dir(target), state.ParentPerm); err != nil {
return wrapErrSelf(err)
}
if err := os.Symlink(l.LinkName, target); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
} }
return nil return nil
@ -330,56 +557,65 @@ func (l *SymlinkOp) apply(params *Params) error {
func (l *SymlinkOp) Is(op Op) bool { vl, ok := op.(*SymlinkOp); return ok && *l == *vl } func (l *SymlinkOp) Is(op Op) bool { vl, ok := op.(*SymlinkOp); return ok && *l == *vl }
func (*SymlinkOp) prefix() string { return "creating" } func (*SymlinkOp) prefix() string { return "creating" }
func (l *SymlinkOp) String() string { return fmt.Sprintf("symlink on %q target %q", l[1], l[0]) } func (l *SymlinkOp) String() string {
func (f *Ops) Link(target, linkName string) *Ops { return fmt.Sprintf("symlink on %q linkname %q", l.Target, l.LinkName)
*f = append(*f, &SymlinkOp{target, linkName})
return f
} }
func init() { gob.Register(new(MkdirOp)) } func init() { gob.Register(new(MkdirOp)) }
// MkdirOp creates a directory in the container filesystem. // Mkdir appends an [Op] that creates a directory in the container filesystem.
func (f *Ops) Mkdir(name *Absolute, perm os.FileMode) *Ops {
*f = append(*f, &MkdirOp{name, perm})
return f
}
type MkdirOp struct { type MkdirOp struct {
Path string Path *Absolute
Perm os.FileMode Perm os.FileMode
} }
func (m *MkdirOp) early(*Params) error { return nil } func (m *MkdirOp) early(*setupState) error { return nil }
func (m *MkdirOp) apply(*Params) error { func (m *MkdirOp) apply(*setupState) error {
if !path.IsAbs(m.Path) { if m.Path == nil {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", m.Path)) return EBADE
} }
return wrapErrSelf(os.MkdirAll(toSysroot(m.Path.String()), m.Perm))
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 (m *MkdirOp) Is(op Op) bool { vm, ok := op.(*MkdirOp); return ok && m == vm }
func (*MkdirOp) prefix() string { return "creating" } func (*MkdirOp) prefix() string { return "creating" }
func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) } func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) }
func (f *Ops) Mkdir(dest string, perm os.FileMode) *Ops {
*f = append(*f, &MkdirOp{dest, perm})
return f
}
func init() { gob.Register(new(TmpfileOp)) } func init() { gob.Register(new(TmpfileOp)) }
// TmpfileOp places a file in container Path containing Data. // Place appends an [Op] that places a file in container path [TmpfileOp.Path] containing [TmpfileOp.Data].
func (f *Ops) Place(name *Absolute, data []byte) *Ops {
*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
}
type TmpfileOp struct { type TmpfileOp struct {
Path string Path *Absolute
Data []byte Data []byte
} }
func (t *TmpfileOp) early(*Params) error { return nil } func (t *TmpfileOp) early(*setupState) error { return nil }
func (t *TmpfileOp) apply(params *Params) error { func (t *TmpfileOp) apply(state *setupState) error {
if !path.IsAbs(t.Path) { if t.Path == nil {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", t.Path)) return EBADE
} }
var tmpPath string var tmpPath string
if f, err := os.CreateTemp("/", "tmp.*"); err != nil { if f, err := os.CreateTemp(FHSRoot, intermediatePatternTmpfile); err != nil {
return wrapErrSelf(err) return wrapErrSelf(err)
} else if _, err = f.Write(t.Data); err != nil { } else if _, err = f.Write(t.Data); err != nil {
return wrapErrSuffix(err, return wrapErrSuffix(err,
@ -391,8 +627,8 @@ func (t *TmpfileOp) apply(params *Params) error {
tmpPath = f.Name() tmpPath = f.Name()
} }
target := toSysroot(t.Path) target := toSysroot(t.Path.String())
if err := ensureFile(target, 0444, params.ParentPerm); err != nil { if err := ensureFile(target, 0444, state.ParentPerm); err != nil {
return err return err
} else if err = hostProc.bindMount( } else if err = hostProc.bindMount(
tmpPath, tmpPath,
@ -415,68 +651,3 @@ func (*TmpfileOp) prefix() string { return "placing" }
func (t *TmpfileOp) String() string { func (t *TmpfileOp) String() string {
return fmt.Sprintf("tmpfile %q (%d bytes)", t.Path, len(t.Data)) return fmt.Sprintf("tmpfile %q (%d bytes)", t.Path, len(t.Data))
} }
func (f *Ops) Place(name string, data []byte) *Ops { *f = append(*f, &TmpfileOp{name, data}); return f }
func (f *Ops) PlaceP(name string, dataP **[]byte) *Ops {
t := &TmpfileOp{Path: name}
*dataP = &t.Data
*f = append(*f, t)
return f
}
func init() { gob.Register(new(AutoEtcOp)) }
// AutoEtcOp 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.
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) }
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
}

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,11 @@ 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")
) )
// 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.
@ -35,7 +35,7 @@ func Receive(key string, e any, v **os.File) (func() error, error) {
} 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 v != nil {
*v = setup *v = setup

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"
) )
@ -63,9 +134,9 @@ func ensureFile(name string, perm, pperm os.FileMode) error {
return err return err
} }
var hostProc = newProcPats(hostPath) var hostProc = newProcPaths(hostPath)
func newProcPats(prefix string) *procPaths { func newProcPaths(prefix string) *procPaths {
return &procPaths{prefix + "/proc", prefix + "/proc/self"} return &procPaths{prefix + "/proc", prefix + "/proc/self"}
} }

42
container/path_test.go Normal file
View File

@ -0,0 +1,42 @@
package container
import "testing"
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)) }

View File

@ -79,7 +79,7 @@ func TestExport(t *testing.T) {
func BenchmarkExport(b *testing.B) { func BenchmarkExport(b *testing.B) {
buf := make([]byte, 8) buf := make([]byte, 8)
for i := 0; i < b.N; i++ { for b.Loop() {
e := New( e := New(
Preset(PresetExt|PresetDenyNS|PresetDenyTTY|PresetDenyDevel|PresetLinux32, Preset(PresetExt|PresetDenyNS|PresetDenyTTY|PresetDenyDevel|PresetLinux32,
AllowMultiarch|AllowCAN|AllowBluetooth), AllowMultiarch|AllowCAN|AllowBluetooth),

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

12
flake.lock generated
View File

@ -7,11 +7,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1748665073, "lastModified": 1753479839,
"narHash": "sha256-RMhjnPKWtCoIIHiuR9QKD7xfsKb3agxzMfJY8V9MOew=", "narHash": "sha256-E/rPVh7vyPMJUFl2NAew+zibNGfVbANr8BP8nLRbLkQ=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "282e1e029cb6ab4811114fc85110613d72771dea", "rev": "0b9bf983db4d064764084cd6748efb1ab8297d1e",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -23,11 +23,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1749024892, "lastModified": 1753345091,
"narHash": "sha256-OGcDEz60TXQC+gVz5sdtgGJdKVYr6rwdzQKuZAJQpCA=", "narHash": "sha256-CdX2Rtvp5I8HGu9swBmYuq+ILwRxpXdJwlpg8jvN4tU=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "8f1b52b04f2cb6e5ead50bd28d76528a2f0380ef", "rev": "3ff0e34b1383648053bba8ed03f201d3466f90c9",
"type": "github" "type": "github"
}, },
"original": { "original": {

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

@ -8,12 +8,13 @@ import (
"os/exec" "os/exec"
"testing" "testing"
"hakurei.app/container"
"hakurei.app/helper" "hakurei.app/helper"
) )
func TestCmd(t *testing.T) { func TestCmd(t *testing.T) {
t.Run("start non-existent helper path", func(t *testing.T) { t.Run("start non-existent helper path", func(t *testing.T) {
h := helper.NewDirect(t.Context(), "/proc/nonexistent", argsWt, false, argF, nil, nil) h := helper.NewDirect(t.Context(), container.Nonexistent, argsWt, false, argF, nil, nil)
if err := h.Start(); !errors.Is(err, os.ErrNotExist) { if err := h.Start(); !errors.Is(err, os.ErrNotExist) {
t.Errorf("Start: error = %v, wantErr %v", t.Errorf("Start: error = %v, wantErr %v",

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

@ -4,20 +4,17 @@ import (
"context" "context"
"io" "io"
"os" "os"
"os/exec"
"testing" "testing"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/helper" "hakurei.app/helper"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
) )
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(), "/nonexistent", argsWt, false, argF, nil, nil) h := helper.New(t.Context(), container.MustAbs(container.Nonexistent), "hakurei", argsWt, false, argF, nil, nil)
wantErr := "sandbox: 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 {
t.Errorf("Start: error = %v, wantErr %q", t.Errorf("Start: error = %v, wantErr %q",
err, wantErr) err, wantErr)
@ -25,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
@ -34,22 +31,13 @@ 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.CommandContext = func(ctx context.Context) (cmd *exec.Cmd) { z.
return exec.CommandContext(ctx, os.Args[0], "-test.v", Bind(container.AbsFHSRoot, container.AbsFHSRoot, 0).
"-test.run=TestHelperInit", "--", "init") Proc(container.AbsFHSProc).
} Dev(container.AbsFHSDev, true)
z.Bind("/", "/", 0).Proc("/proc").Dev("/dev")
}, nil) }, nil)
}) })
}) })
} }
func TestHelperInit(t *testing.T) {
if len(os.Args) != 5 || os.Args[4] != "init" {
return
}
container.SetOutput(hlog.Output{})
container.Init(hlog.Prepare, func(bool) { internal.InstallOutput(false) })
}

View File

@ -38,7 +38,6 @@ func argF(argsFd, statFd int) []string {
func argFChecked(argsFd, statFd int) (args []string) { func argFChecked(argsFd, statFd int) (args []string) {
args = make([]string, 0, 6) args = make([]string, 0, 6)
args = append(args, "-test.run=TestHelperStub", "--")
if argsFd > -1 { if argsFd > -1 {
args = append(args, "--args", strconv.Itoa(argsFd)) args = append(args, "--args", strconv.Itoa(argsFd))
} }

View File

@ -25,7 +25,7 @@ func InternalHelperStub() {
sp = v sp = v
} }
genericStub(flagRestoreFiles(3, ap, sp)) genericStub(flagRestoreFiles(1, ap, sp))
os.Exit(0) os.Exit(0)
} }

View File

@ -1,9 +1,17 @@
package helper_test package helper_test
import ( import (
"os"
"testing" "testing"
"hakurei.app/container"
"hakurei.app/helper" "hakurei.app/helper"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
) )
func TestHelperStub(t *testing.T) { helper.InternalHelperStub() } func TestMain(m *testing.M) {
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
helper.InternalHelperStub()
os.Exit(m.Run())
}

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,12 +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 // absolute path to home directory in the init mount namespace
Data string `json:"data"` Data *container.Absolute `json:"data"`
// directory to enter and use as home in the container mount namespace, empty for Data // directory to enter and use as home in the container mount namespace, nil for Data
Dir string `json:"dir"` Dir *container.Absolute `json:"dir,omitempty"`
// extra acl ops, dispatches before container init // extra acl ops, dispatches before container init
ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"` ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
@ -51,25 +56,89 @@ type Config struct {
// abstract container configuration baseline // abstract container configuration baseline
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
Net bool `json:"net,omitempty"`
// disallow accessing abstract UNIX domain sockets created outside the container
ScopeAbstract bool `json:"scope_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
Filesystem []FilesystemConfigJSON `json:"filesystem"`
// create symlinks inside container filesystem
Link []LinkConfig `json:"symlink"`
// automatically bind mount top-level directories to container root;
// the zero value disables this behaviour
AutoRoot *container.Absolute `json:"auto_root,omitempty"`
// extra flags for AutoRoot
RootFlags int `json:"root_flags,omitempty"`
// read-only /etc directory
Etc *container.Absolute `json:"etc,omitempty"`
// automatically set up /etc symlinks
AutoEtc bool `json:"auto_etc"`
}
LinkConfig struct {
// symlink target in container
Target *container.Absolute `json:"target"`
// linkname the symlink points to;
// prepend '*' to dereference an absolute pathname on host
Linkname string `json:"linkname"`
}
)
// 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,65 +0,0 @@
package hst
import (
"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"`
// 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"`
// disallow accessing abstract UNIX domain sockets created outside the container
ScopeAbstract bool `json:"scope_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 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)
}
})
}

120
hst/fs.go Normal file
View File

@ -0,0 +1,120 @@
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(ops *container.Ops)
fmt.Stringer
}
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}
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)}
default:
return FSTypeError(t.Type)
}
return json.Unmarshal(data, f.FilesystemConfig)
}

287
hst/fs_test.go Normal file
View File

@ -0,0 +1,287 @@
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}`},
}
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(*container.Ops) { 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(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
}

102
hst/fsbind.go Normal file
View File

@ -0,0 +1,102 @@
package hst
import (
"encoding/gob"
"strings"
"hakurei.app/container"
)
func init() { gob.Register(new(FSBind)) }
// FilesystemBind is the [FilesystemConfig.Type] name 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 src 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 filesystem read-only
Write bool `json:"write,omitempty"`
// do not disable device files, implies Write
Device bool `json:"dev,omitempty"`
// skip this mount point if the host path does not exist
Optional bool `json:"optional,omitempty"`
}
func (b *FSBind) Valid() bool { return b != nil && b.Source != nil }
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(ops *container.Ops) {
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.Optional {
flags |= container.BindOptional
}
ops.Bind(b.Source, target, flags)
}
func (b *FSBind) String() string {
g := 4
if !b.Valid() {
return "<invalid>"
}
g += len(b.Source.String())
if b.Target != nil {
g += len(b.Target.String())
}
expr := new(strings.Builder)
expr.Grow(g)
if b.Device {
expr.WriteString("d")
} else if b.Write {
expr.WriteString("w")
}
if !b.Optional {
expr.WriteString("*")
} else {
expr.WriteString("+")
}
expr.WriteString(b.Source.String())
if b.Target != nil {
expr.WriteString(":" + b.Target.String())
}
return expr.String()
}

66
hst/fsbind_test.go Normal file
View File

@ -0,0 +1,66 @@
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>"},
{"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 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("/"),
"*/"},
})
}

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 [FilesystemConfig.Type] name 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(ops *container.Ops) {
if !e.Valid() {
return
}
size := e.Size
if size < 0 {
size = 0
}
perm := e.Perm
if perm == 0 {
perm = fsEphemeralDefaultPerm
}
if e.Write {
ops.Tmpfs(e.Target, size, perm)
} else {
ops.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"},
})
}

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 [FilesystemConfig.Type] name 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(op *container.Ops) {
if !o.Valid() {
return
}
if o.Upper != nil && o.Work != nil { // rw
op.Overlay(o.Target, o.Upper, o.Work, o.Lower...)
} else { // ro
op.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"},
})
}

120
hst/hst.go Normal file
View File

@ -0,0 +1,120 @@
// 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`)
SharePath *container.Absolute `json:"share_path"`
// XDG_RUNTIME_DIR value (usually `/run/user/%d`)
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 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"),
Data: container.AbsFHSVarLib.Append("hakurei/u0/org.chromium.Chromium"),
Dir: 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,
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: []FilesystemConfigJSON{
{&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")}},
{&FSBind{Source: container.AbsFHSRun.Append("current-system")}},
{&FSBind{Source: container.AbsFHSRun.Append("opengl-driver")}},
{&FSBind{Source: container.AbsFHSVarLib.Append("hakurei/u0/org.chromium.Chromium"),
Target: container.MustAbs("/data/data/org.chromium.Chromium"), Write: true}},
{&FSBind{Source: container.AbsFHSDev.Append("dri"), Device: true, Optional: true}},
},
Link: []LinkConfig{{container.AbsFHSRunUser.Append("65534"), container.FHSRunUser + "150"}},
AutoRoot: container.AbsFHSVarLib.Append("hakurei/base/org.debian"),
RootFlags: container.BindWritable,
Etc: container.AbsFHSEtc,
AutoEtc: 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": [
@ -80,8 +84,10 @@ func TestTemplate(t *testing.T) {
], ],
"container": { "container": {
"hostname": "localhost", "hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1, "seccomp_flags": 1,
"seccomp_presets": 1, "seccomp_presets": 1,
"seccomp_compat": true,
"devel": true, "devel": true,
"userns": true, "userns": true,
"net": true, "net": true,
@ -96,39 +102,55 @@ func TestTemplate(t *testing.T) {
"device": true, "device": true,
"filesystem": [ "filesystem": [
{ {
"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"
}, },
{ {
"type": "bind",
"src": "/run/current-system" "src": "/run/current-system"
}, },
{ {
"type": "bind",
"src": "/run/opengl-driver" "src": "/run/opengl-driver"
}, },
{ {
"src": "/var/db/nix-channels" "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
}, },
{ {
"type": "bind",
"src": "/dev/dri", "src": "/dev/dri",
"dev": true "dev": true,
"optional": true
} }
], ],
"symlink": [ "symlink": [
[ {
"/run/user/65534", "target": "/run/user/65534",
"/run/user/150" "linkname": "/run/user/150"
] }
], ],
"etc": "/etc", "auto_root": "/var/lib/hakurei/base/org.debian",
"auto_etc": true, "root_flags": 2,
"cover": [ "etc": "/etc/",
"/var/run/nscd" "auto_etc": true
]
} }
}` }`

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,92 +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,
SeccompFlags: seccomp.AllowMultiarch,
SeccompPresets: seccomp.PresetExt,
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,35 @@ 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, Net: true, MapRealUID: true, Env: nil, AutoEtc: true,
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}),
}, },
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 +63,7 @@ var testCasesNixos = []sealTestCase{
DirectWayland: true, DirectWayland: true,
Username: "u0_a1", Username: "u0_a1",
Data: "/var/lib/persist/module/hakurei/0/1", Data: m("/var/lib/persist/module/hakurei/0/1"),
Identity: 1, Groups: []string{}, Identity: 1, Groups: []string{},
}, },
state.ID{ state.ID{
@ -97,8 +111,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,35 +129,37 @@ 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). Remount(m("/dev/"), syscall.MS_RDONLY).
Bind("/tmp/hakurei.1971/runtime/1", "/run/user/1971", container.BindWritable). Tmpfs(m("/run/user/"), 4096, 0755).
Bind("/tmp/hakurei.1971/tmpdir/1", "/tmp", container.BindWritable). Bind(m("/tmp/hakurei.1971/runtime/1"), m("/run/user/1971"), container.BindWritable).
Bind("/var/lib/persist/module/hakurei/0/1", "/var/lib/persist/module/hakurei/0/1", container.BindWritable). Bind(m("/tmp/hakurei.1971/tmpdir/1"), m("/tmp/"), container.BindWritable).
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("/var/lib/persist/module/hakurei/0/1"), m("/var/lib/persist/module/hakurei/0/1"), container.BindWritable).
Place("/etc/group", []byte("hakurei:x:100:\n")). 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/wayland-0", "/run/user/1971/wayland-0", 0). Place(m("/etc/group"), []byte("hakurei:x:100:\n")).
Bind("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native", 0). Bind(m("/run/user/1971/wayland-0"), m("/run/user/1971/wayland-0"), 0).
Place(hst.Tmp+"/pulse-cookie", nil). Bind(m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse"), m("/run/user/1971/pulse/native"), 0).
Bind("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus", 0). Place(m(hst.Tmp+"/pulse-cookie"), nil).
Bind("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket", 0). Bind(m("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus"), m("/run/user/1971/bus"), 0).
Tmpfs("/var/run/nscd", 8192, 0755), 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,
}, },
}, },
} }

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", Data: 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,35 +43,27 @@ var testCasesPd = []sealTestCase{
"XDG_SESSION_TYPE=tty", "XDG_SESSION_TYPE=tty",
}, },
Ops: new(container.Ops). Ops: new(container.Ops).
Proc("/proc"). Root(m("/"), "4a450b6596d7bc15bd01780eb9a607ac", 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). Tmpfs(m("/run/user/1971"), 8192, 0755).
Bind("/lib64", "/lib64", container.BindWritable). Tmpfs(m("/run/dbus"), 8192, 0755).
Bind("/nix", "/nix", container.BindWritable). Etc(m("/etc/"), "4a450b6596d7bc15bd01780eb9a607ac").
Bind("/root", "/root", container.BindWritable). Remount(m("/dev/"), syscall.MS_RDONLY).
Bind("/run", "/run", container.BindWritable). Tmpfs(m("/run/user/"), 4096, 0755).
Bind("/srv", "/srv", container.BindWritable). Bind(m("/tmp/hakurei.1971/runtime/0"), m("/run/user/65534"), container.BindWritable).
Bind("/sys", "/sys", container.BindWritable). Bind(m("/tmp/hakurei.1971/tmpdir/0"), m("/tmp/"), container.BindWritable).
Bind("/usr", "/usr", container.BindWritable). Bind(m("/home/chronos"), m("/home/chronos"), 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,
RetainSession: true, RetainSession: true,
ForwardCancel: true,
}, },
}, },
{ {
@ -81,7 +74,7 @@ var testCasesPd = []sealTestCase{
Identity: 9, Identity: 9,
Groups: []string{"video"}, Groups: []string{"video"},
Username: "chronos", Username: "chronos",
Data: "/home/chronos", Data: m("/home/chronos"),
SessionBus: &dbus.Config{ SessionBus: &dbus.Config{
Talk: []string{ Talk: []string{
"org.freedesktop.Notifications", "org.freedesktop.Notifications",
@ -113,7 +106,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,
@ -167,8 +160,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",
@ -185,41 +178,33 @@ var testCasesPd = []sealTestCase{
"XDG_SESSION_TYPE=tty", "XDG_SESSION_TYPE=tty",
}, },
Ops: new(container.Ops). Ops: new(container.Ops).
Proc("/proc"). Root(m("/"), "ebf083d1b175911782d413369b64ce7c", 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). Tmpfs(m("/run/user/1971"), 8192, 0755).
Bind("/nix", "/nix", container.BindWritable). Tmpfs(m("/run/dbus"), 8192, 0755).
Bind("/root", "/root", container.BindWritable). Etc(m("/etc/"), "ebf083d1b175911782d413369b64ce7c").
Bind("/run", "/run", container.BindWritable). Remount(m("/dev/"), syscall.MS_RDONLY).
Bind("/srv", "/srv", container.BindWritable). Tmpfs(m("/run/user/"), 4096, 0755).
Bind("/sys", "/sys", container.BindWritable). Bind(m("/tmp/hakurei.1971/runtime/9"), m("/run/user/65534"), container.BindWritable).
Bind("/usr", "/usr", container.BindWritable). Bind(m("/tmp/hakurei.1971/tmpdir/9"), m("/tmp/"), container.BindWritable).
Bind("/var", "/var", container.BindWritable). Bind(m("/home/chronos"), m("/home/chronos"), 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,
RetainSession: true, RetainSession: 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{
@ -33,10 +34,14 @@ func newContainer(s *hst.ContainerConfig, os sys.State, uid, gid *int) (*contain
RetainSession: s.Tty, RetainSession: s.Tty,
HostNet: s.Net, HostNet: s.Net,
ScopeAbstract: s.ScopeAbstract, ScopeAbstract: s.ScopeAbstract,
// the container is canceled when shim is requested to exit or receives an interrupt or termination signal;
// this behaviour is implemented in the shim
ForwardCancel: s.WaitDelay >= 0,
} }
{ {
ops := make(container.Ops, 0, preallocateOpsCount+len(s.Filesystem)+len(s.Link)+len(s.Cover)) ops := make(container.Ops, 0, preallocateOpsCount+len(s.Filesystem)+len(s.Link))
params.Ops = &ops params.Ops = &ops
} }
@ -69,14 +74,18 @@ func newContainer(s *hst.ContainerConfig, os sys.State, uid, gid *int) (*contain
*gid = container.OverflowGid() *gid = container.OverflowGid()
} }
if s.AutoRoot != nil {
params.Root(s.AutoRoot, prefix, s.RootFlags)
}
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;
@ -85,7 +94,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
@ -100,7 +109,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)
@ -118,63 +127,123 @@ func newContainer(s *hst.ContainerConfig, os sys.State, uid, gid *int) (*contain
} }
} }
var hidePathSourceCount int
for i, c := range s.Filesystem {
if !c.Valid() {
return nil, nil, fmt.Errorf("invalid filesystem at index %d", i)
}
c.Apply(params.Ops)
// fs counter
hidePathSourceCount += len(c.Host())
}
// AutoRoot is a collection of many BindMountOp internally
var autoRootEntries []fs.DirEntry
if s.AutoRoot != nil {
if d, err := os.ReadDir(s.AutoRoot.String()); err != nil {
return nil, nil, err
} else {
// autoroot counter
hidePathSourceCount += len(d)
autoRootEntries = d
}
}
hidePathSource := make([]*container.Absolute, 0, hidePathSourceCount)
// fs append
for _, c := range s.Filesystem { for _, c := range s.Filesystem {
if c == nil { // all entries already checked above
continue hidePathSource = append(hidePathSource, c.Host()...)
} }
if !path.IsAbs(c.Src) { // autoroot append
return nil, nil, fmt.Errorf("src path %q is not absolute", c.Src) if s.AutoRoot != nil {
for _, ent := range autoRootEntries {
name := ent.Name()
if container.IsAutoRootBindable(name) {
hidePathSource = append(hidePathSource, s.AutoRoot.Append(name))
}
}
} }
dest := c.Dst // evaluated path, input path
if c.Dst == "" { hidePathSourceEval := make([][2]string, len(hidePathSource))
dest = c.Src for i, a := range hidePathSource {
} else if !path.IsAbs(dest) { if a == nil {
return nil, nil, fmt.Errorf("dst path %q is not absolute", dest) // unreachable
return nil, nil, syscall.ENOTRECOVERABLE
} }
srcH := c.Src hidePathSourceEval[i] = [2]string{a.String(), a.String()}
if err := evalSymlinks(os, &srcH); err != nil { 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 { for i, l := range s.Link {
params.Link(l[0], l[1]) if l.Target == nil || l.Linkname == "" {
return nil, nil, fmt.Errorf("invalid link at index %d", i)
}
linkname := l.Linkname
var dereference bool
if linkname[0] == '*' && path.IsAbs(linkname[1:]) {
linkname = linkname[1:]
dereference = true
}
params.Link(l.Target, linkname, dereference)
}
if !s.AutoEtc {
if s.Etc != nil {
params.Bind(s.Etc, container.AbsFHSEtc, 0)
}
} else {
if s.Etc == nil {
params.Etc(container.AbsFHSEtc, prefix)
} else {
params.Etc(s.Etc, prefix)
}
}
// no more ContainerConfig paths beyond this point
if !s.Device {
params.Remount(container.AbsFHSDev, syscall.MS_RDONLY)
} }
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) }
@ -123,7 +123,15 @@ func (seal *outcome) Run(rs *RunState) error {
// this prevents blocking forever on an early failure // this prevents blocking forever on an early failure
waitErr, setupErr := make(chan error, 1), make(chan error, 1) waitErr, setupErr := make(chan error, 1), make(chan error, 1)
go func() { waitErr <- cmd.Wait(); cancel() }() go func() { waitErr <- cmd.Wait(); cancel() }()
go func() { setupErr <- e.Encode(&shimParams{os.Getpid(), seal.container, seal.user.data, hlog.Load()}) }() go func() {
setupErr <- e.Encode(&shimParams{
os.Getpid(),
seal.waitDelay,
seal.container,
seal.user.data.String(),
hlog.Load(),
})
}()
select { select {
case err := <-setupErr: case err := <-setupErr:

View File

@ -15,6 +15,7 @@ import (
"strings" "strings"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
"time"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/hst" "hakurei.app/hst"
@ -48,9 +49,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")
@ -66,8 +65,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
@ -79,6 +78,7 @@ type outcome struct {
sys *system.I sys *system.I
ctx context.Context ctx context.Context
waitDelay time.Duration
container *container.Params container *container.Params
env map[string]string env map[string]string
sync *os.File sync *os.File
@ -91,9 +91,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
@ -105,48 +105,48 @@ 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 // home directory host path
data string data *container.Absolute
// app user home directory // app user home directory
home string home *container.Absolute
// passwd database username // passwd database username
username string username string
} }
@ -157,6 +157,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.Data == nil {
return hlog.WrapErr(os.ErrInvalid, "invalid data directory")
}
{ {
// encode initial configuration for state tracking // encode initial configuration for state tracking
ct := new(bytes.Buffer) ct := new(bytes.Buffer)
@ -169,7 +176,7 @@ 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))
} }
@ -186,11 +193,7 @@ 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) { if seal.user.home == nil {
return hlog.WrapErr(ErrHome,
fmt.Sprintf("invalid home directory %q", seal.user.data))
}
if seal.user.home == "" {
seal.user.home = seal.user.data 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 {
@ -208,26 +211,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
@ -239,58 +241,47 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
Net: true, Net: true,
Tty: true, Tty: true,
AutoEtc: 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: AutoRoot: container.AbsFHSRoot,
b = append(b, &hst.FilesystemConfig{Src: p, Write: true, Must: true}) RootFlags: container.BindWritable,
}
}
conf.Filesystem = append(conf.Filesystem, b...)
} }
// 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}})
}
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
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
@ -302,69 +293,53 @@ 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.Bind(seal.user.data, seal.user.home, container.BindWritable)
seal.container.Dir = homeDir seal.container.Dir = seal.user.home
seal.env["HOME"] = homeDir seal.env["HOME"] = seal.user.home.String()
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"))
} }
@ -373,19 +348,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
@ -395,35 +370,36 @@ 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 {
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) socketDir := container.AbsFHSTmp.Append(".X11-unix")
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))
@ -432,7 +408,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))
@ -447,39 +423,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 {
@ -487,30 +462,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)
@ -523,7 +497,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

@ -0,0 +1,65 @@
#include "shim-signal.h"
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static pid_t hakurei_shim_param_ppid = -1;
static int hakurei_shim_fd = -1;
static ssize_t hakurei_shim_write(const void *buf, size_t count) {
int savedErrno = errno;
ssize_t ret = write(hakurei_shim_fd, buf, count);
if (ret == -1 && errno != EAGAIN)
exit(EXIT_FAILURE);
errno = savedErrno;
return ret;
}
/* see shim_linux.go for handling of the value */
static void hakurei_shim_sigaction(int sig, siginfo_t *si, void *ucontext) {
if (sig != SIGCONT || si == NULL) {
/* unreachable */
hakurei_shim_write("\2", 1);
return;
}
if (si->si_pid == hakurei_shim_param_ppid) {
/* monitor requests shim exit */
hakurei_shim_write("\0", 1);
return;
}
/* unexpected si_pid */
hakurei_shim_write("\3", 1);
if (getppid() != hakurei_shim_param_ppid)
/* shim orphaned before monitor delivers a signal */
hakurei_shim_write("\1", 1);
}
void hakurei_shim_setup_cont_signal(pid_t ppid, int fd) {
if (hakurei_shim_param_ppid != -1 || hakurei_shim_fd != -1)
*(int *)NULL = 0; /* unreachable */
struct sigaction new_action = {0}, old_action = {0};
if (sigaction(SIGCONT, NULL, &old_action) != 0)
return;
if (old_action.sa_handler != SIG_DFL) {
errno = ENOTRECOVERABLE;
return;
}
new_action.sa_sigaction = hakurei_shim_sigaction;
if (sigemptyset(&new_action.sa_mask) != 0)
return;
new_action.sa_flags = SA_ONSTACK | SA_SIGINFO;
if (sigaction(SIGCONT, &new_action, NULL) != 0)
return;
errno = 0;
hakurei_shim_param_ppid = ppid;
hakurei_shim_fd = fd;
}

View File

@ -0,0 +1,3 @@
#include <signal.h>
void hakurei_shim_setup_cont_signal(pid_t ppid, int fd);

View File

@ -3,10 +3,13 @@ package app
import ( import (
"context" "context"
"errors" "errors"
"io"
"log" "log"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"runtime"
"sync/atomic"
"syscall" "syscall"
"time" "time"
@ -16,55 +19,7 @@ import (
"hakurei.app/internal/hlog" "hakurei.app/internal/hlog"
) )
/* //#include "shim-signal.h"
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <signal.h>
static pid_t hakurei_shim_param_ppid = -1;
// this cannot unblock hlog since Go code is not async-signal-safe
static void hakurei_shim_sigaction(int sig, siginfo_t *si, void *ucontext) {
if (sig != SIGCONT || si == NULL) {
// unreachable
fprintf(stderr, "sigaction: sa_sigaction got invalid siginfo\n");
return;
}
// monitor requests shim exit
if (si->si_pid == hakurei_shim_param_ppid)
exit(254);
fprintf(stderr, "sigaction: got SIGCONT from process %d\n", si->si_pid);
// shim orphaned before monitor delivers a signal
if (getppid() != hakurei_shim_param_ppid)
exit(3);
}
void hakurei_shim_setup_cont_signal(pid_t ppid) {
struct sigaction new_action = {0}, old_action = {0};
if (sigaction(SIGCONT, NULL, &old_action) != 0)
return;
if (old_action.sa_handler != SIG_DFL) {
errno = ENOTRECOVERABLE;
return;
}
new_action.sa_sigaction = hakurei_shim_sigaction;
if (sigemptyset(&new_action.sa_mask) != 0)
return;
new_action.sa_flags = SA_ONSTACK | SA_SIGINFO;
if (sigaction(SIGCONT, &new_action, NULL) != 0)
return;
errno = 0;
hakurei_shim_param_ppid = ppid;
}
*/
import "C" import "C"
const shimEnv = "HAKUREI_SHIM" const shimEnv = "HAKUREI_SHIM"
@ -73,6 +28,10 @@ type shimParams struct {
// monitor pid, checked against ppid in signal handler // monitor pid, checked against ppid in signal handler
Monitor int Monitor int
// duration to wait for after interrupting a container's initial process before the container is killed;
// zero value defaults to [DefaultShimWaitDelay], values exceeding [MaxShimWaitDelay] becomes [MaxShimWaitDelay]
WaitDelay time.Duration
// finalised container params // finalised container params
Container *container.Params Container *container.Params
// path to outer home directory // path to outer home directory
@ -82,6 +41,16 @@ type shimParams struct {
Verbose bool Verbose bool
} }
const (
// ShimExitRequest is returned when the monitor process requests shim exit.
ShimExitRequest = 254
// ShimExitOrphan is returned when the shim is orphaned before monitor delivers a signal.
ShimExitOrphan = 3
DefaultShimWaitDelay = 5 * time.Second
MaxShimWaitDelay = 30 * time.Second
)
// ShimMain is the main function of the shim process and runs as the unconstrained target user. // ShimMain is the main function of the shim process and runs as the unconstrained target user.
func ShimMain() { func ShimMain() {
hlog.Prepare("shim") hlog.Prepare("shim")
@ -95,7 +64,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) {
@ -106,18 +75,63 @@ func ShimMain() {
} else { } else {
internal.InstallOutput(params.Verbose) internal.InstallOutput(params.Verbose)
closeSetup = f closeSetup = f
}
var signalPipe io.ReadCloser
// the Go runtime does not expose siginfo_t so SIGCONT is handled in C to check si_pid // the Go runtime does not expose siginfo_t so SIGCONT is handled in C to check si_pid
if _, err = C.hakurei_shim_setup_cont_signal(C.pid_t(params.Monitor)); err != nil { if r, w, err := os.Pipe(); err != nil {
log.Fatalf("cannot pipe: %v", err)
} else if _, err = C.hakurei_shim_setup_cont_signal(C.pid_t(params.Monitor), C.int(w.Fd())); err != nil {
log.Fatalf("cannot install SIGCONT handler: %v", err) log.Fatalf("cannot install SIGCONT handler: %v", err)
} else {
defer runtime.KeepAlive(w)
signalPipe = r
} }
// pdeath_signal delivery is checked as if the dying process called kill(2), see kernel/exit.c // pdeath_signal delivery is checked as if the dying process called kill(2), see kernel/exit.c
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGCONT), 0); errno != 0 { if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGCONT), 0); errno != 0 {
log.Fatalf("cannot set parent-death signal: %v", errno) log.Fatalf("cannot set parent-death signal: %v", errno)
} }
// signal handler outcome
var cancelContainer atomic.Pointer[context.CancelFunc]
go func() {
buf := make([]byte, 1)
for {
if _, err := signalPipe.Read(buf); err != nil {
log.Fatalf("cannot read from signal pipe: %v", err)
} }
switch buf[0] {
case 0: // got SIGCONT from monitor: shim exit requested
if fp := cancelContainer.Load(); params.Container.ForwardCancel && fp != nil && *fp != nil {
(*fp)()
// shim now bound by ShimWaitDelay, implemented below
continue
}
// setup has not completed, terminate immediately
hlog.Resume()
os.Exit(ShimExitRequest)
return
case 1: // got SIGCONT after adoption: monitor died before delivering signal
hlog.BeforeExit()
os.Exit(ShimExitOrphan)
return
case 2: // unreachable
log.Println("sa_sigaction got invalid siginfo")
case 3: // got SIGCONT from unexpected process: hopefully the terminal driver
log.Println("got SIGCONT from unexpected process")
default: // unreachable
log.Fatalf("got invalid message %d from signal handler", buf[0])
}
}
}()
if params.Container == nil || params.Container.Ops == nil { if params.Container == nil || params.Container.Ops == nil {
log.Fatal("invalid container params") log.Fatal("invalid container params")
} }
@ -143,17 +157,19 @@ func ShimMain() {
log.Fatalf("path %q is not a directory", params.Home) 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)
defer stop() // unreachable 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
z.Cancel = func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) }
z.WaitDelay = 2 * time.Second z.WaitDelay = params.WaitDelay
if z.WaitDelay == 0 {
z.WaitDelay = DefaultShimWaitDelay
}
if z.WaitDelay > MaxShimWaitDelay {
z.WaitDelay = MaxShimWaitDelay
}
if err := z.Start(); err != nil { if err := z.Start(); err != nil {
hlog.PrintBaseError(err, "cannot start container:") hlog.PrintBaseError(err, "cannot start container:")

View File

@ -3,10 +3,11 @@ package sys
import ( import (
"io/fs" "io/fs"
"log"
"os/user" "os/user"
"path"
"strconv" "strconv"
"hakurei.app/container"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/hlog" "hakurei.app/internal/hlog"
) )
@ -50,18 +51,23 @@ type State interface {
// CopyPaths is a generic implementation of [hst.Paths]. // CopyPaths is a generic implementation of [hst.Paths].
func CopyPaths(os State, v *hst.Paths) { func CopyPaths(os State, v *hst.Paths) {
v.SharePath = path.Join(os.TempDir(), "hakurei."+strconv.Itoa(os.Getuid())) if tempDir, err := container.NewAbs(os.TempDir()); err != nil {
log.Fatalf("invalid TMPDIR: %v", err)
hlog.Verbosef("process share directory at %q", v.SharePath)
if r, ok := os.LookupEnv(xdgRuntimeDir); !ok || r == "" || !path.IsAbs(r) {
// fall back to path in share since hakurei has no hard XDG dependency
v.RunDirPath = path.Join(v.SharePath, "run")
v.RuntimePath = path.Join(v.RunDirPath, "compat")
} else { } else {
v.RuntimePath = r v.TempDir = tempDir
v.RunDirPath = path.Join(v.RuntimePath, "hakurei")
} }
v.SharePath = v.TempDir.Append("hakurei." + strconv.Itoa(os.Getuid()))
hlog.Verbosef("process share directory at %q", v.SharePath)
r, _ := os.LookupEnv(xdgRuntimeDir)
if a, err := container.NewAbs(r); err != nil {
// fall back to path in share since hakurei has no hard XDG dependency
v.RunDirPath = v.SharePath.Append("run")
v.RuntimePath = v.RunDirPath.Append("compat")
} else {
v.RuntimePath = a
v.RunDirPath = v.RuntimePath.Append("hakurei")
}
hlog.Verbosef("runtime directory at %q", v.RunDirPath) hlog.Verbosef("runtime directory at %q", v.RunDirPath)
} }

View File

@ -86,7 +86,7 @@ func (s *Std) Uid(aid int) (int, error) {
cmd.Path = hsuPath cmd.Path = hsuPath
cmd.Stderr = os.Stderr // pass through fatal messages cmd.Stderr = os.Stderr // pass through fatal messages
cmd.Env = []string{"HAKUREI_APP_ID=" + strconv.Itoa(aid)} cmd.Env = []string{"HAKUREI_APP_ID=" + strconv.Itoa(aid)}
cmd.Dir = "/" cmd.Dir = container.FHSRoot
var ( var (
p []byte p []byte
exitError *exec.ExitError exitError *exec.ExitError

View File

@ -12,30 +12,38 @@ import (
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
) )
const lddTimeout = 2 * time.Second const (
lddName = "ldd"
lddTimeout = 2 * time.Second
)
var ( var (
msgStatic = []byte("Not a valid dynamic program") msgStatic = []byte("Not a valid dynamic program")
msgStaticGlibc = []byte("not a dynamic executable") msgStaticGlibc = []byte("not a dynamic executable")
) )
func Exec(ctx context.Context, p string) ([]*Entry, error) { return ExecFilter(ctx, nil, nil, p) } func Exec(ctx context.Context, p string) ([]*Entry, error) {
func ExecFilter(ctx context.Context,
commandContext func(context.Context) *exec.Cmd,
f func([]byte) []byte,
p string) ([]*Entry, error) {
c, cancel := context.WithTimeout(ctx, lddTimeout) c, cancel := context.WithTimeout(ctx, lddTimeout)
defer cancel() defer cancel()
z := container.New(c, "ldd", p)
z.CommandContext = commandContext var toolPath *container.Absolute
z.Hostname = "hakurei-ldd" if s, err := exec.LookPath(lddName); err != nil {
return nil, err
} else if toolPath, err = container.NewAbs(s); err != nil {
return nil, err
}
z := container.NewCommand(c, toolPath, lddName, p)
z.Hostname = "hakurei-" + lddName
z.SeccompFlags |= seccomp.AllowMultiarch z.SeccompFlags |= seccomp.AllowMultiarch
z.SeccompPresets |= seccomp.PresetStrict z.SeccompPresets |= seccomp.PresetStrict
stdout, stderr := new(bytes.Buffer), new(bytes.Buffer) stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
z.Stdout = stdout z.Stdout = stdout
z.Stderr = stderr z.Stderr = stderr
z.Bind("/", "/", 0).Proc("/proc").Dev("/dev") z.
Bind(container.AbsFHSRoot, container.AbsFHSRoot, 0).
Proc(container.AbsFHSProc).
Dev(container.AbsFHSDev, false)
if err := z.Start(); err != nil { if err := z.Start(); err != nil {
return nil, err return nil, err
@ -54,8 +62,5 @@ func ExecFilter(ctx context.Context,
} }
v := stdout.Bytes() v := stdout.Bytes()
if f != nil {
v = f(v)
}
return Parse(v) return Parse(v)
} }

View File

@ -1,21 +1,20 @@
package ldd package ldd
import ( import (
"path" "hakurei.app/container"
"slices"
) )
// Path returns a deterministic, deduplicated slice of absolute directory paths in entries. // Path returns a deterministic, deduplicated slice of absolute directory paths in entries.
func Path(entries []*Entry) []string { func Path(entries []*Entry) []*container.Absolute {
p := make([]string, 0, len(entries)*2) p := make([]*container.Absolute, 0, len(entries)*2)
for _, entry := range entries { for _, entry := range entries {
if path.IsAbs(entry.Path) { if a, err := container.NewAbs(entry.Path); err == nil {
p = append(p, path.Dir(entry.Path)) p = append(p, a.Dir())
} }
if path.IsAbs(entry.Name) { if a, err := container.NewAbs(entry.Name); err == nil {
p = append(p, path.Dir(entry.Name)) p = append(p, a.Dir())
} }
} }
slices.Sort(p) container.SortAbs(p)
return slices.Compact(p) return container.CompactAbs(p)
} }

View File

@ -82,7 +82,8 @@ in
own = [ own = [
"${id}.*" "${id}.*"
"org.mpris.MediaPlayer2.${id}.*" "org.mpris.MediaPlayer2.${id}.*"
] ++ ext.own; ]
++ ext.own;
inherit (ext) call broadcast; inherit (ext) call broadcast;
}; };
@ -101,8 +102,7 @@ in
}; };
command = if app.command == null then app.name else app.command; command = if app.command == null then app.name else app.command;
script = if app.script == null then ("exec " + command + " $@") else app.script; script = if app.script == null then ("exec " + command + " $@") else app.script;
enablements = with app.capability; (if wayland then 1 else 0) + (if x11 then 2 else 0) + (if dbus then 4 else 0) + (if pulse then 8 else 0); isGraphical = if app.gpu != null then app.gpu else app.enablements.wayland || app.enablements.x11;
isGraphical = if app.gpu != null then app.gpu else app.capability.wayland || app.capability.x11;
conf = { conf = {
path = path =
@ -115,7 +115,7 @@ in
app.path; app.path;
args = if app.args == null then [ "${app.name}-start" ] else app.args; args = if app.args == null then [ "${app.name}-start" ] else app.args;
inherit id enablements; inherit id;
inherit (dbusConfig) session_bus system_bus; inherit (dbusConfig) session_bus system_bus;
direct_wayland = app.insecureWayland; direct_wayland = app.insecureWayland;
@ -123,10 +123,12 @@ in
username = getsubname fid app.identity; username = getsubname fid app.identity;
data = getsubhome fid app.identity; data = getsubhome fid app.identity;
inherit (app) identity groups; inherit (cfg) shell;
inherit (app) identity groups enablements;
container = { container = {
inherit (app) inherit (app)
wait_delay
devel devel
userns userns
net net
@ -140,69 +142,82 @@ in
filesystem = filesystem =
let let
bind = src: { inherit src; }; bind = src: {
mustBind = src: { type = "bind";
inherit src; inherit src;
require = true;
}; };
devBind = src: { optBind = src: {
type = "bind";
inherit src;
optional = true;
};
optDevBind = src: {
type = "bind";
inherit src; inherit src;
dev = true; dev = true;
optional = true;
}; };
in in
[ [
(mustBind "/bin") (bind "/bin")
(mustBind "/usr/bin") (bind "/usr/bin")
(mustBind "/nix/store") (bind "/nix/store")
(bind "/sys/block") (optBind "/sys/block")
(bind "/sys/bus") (optBind "/sys/bus")
(bind "/sys/class") (optBind "/sys/class")
(bind "/sys/dev") (optBind "/sys/dev")
(bind "/sys/devices") (optBind "/sys/devices")
] ]
++ optionals app.nix [ ++ optionals app.nix [
(mustBind "/nix/var") (bind "/nix/var")
] ]
++ optionals isGraphical [ ++ optionals isGraphical [
(devBind "/dev/dri") (optDevBind "/dev/dri")
(devBind "/dev/nvidiactl") (optDevBind "/dev/nvidiactl")
(devBind "/dev/nvidia-modeset") (optDevBind "/dev/nvidia-modeset")
(devBind "/dev/nvidia-uvm") (optDevBind "/dev/nvidia-uvm")
(devBind "/dev/nvidia-uvm-tools") (optDevBind "/dev/nvidia-uvm-tools")
(devBind "/dev/nvidia0") (optDevBind "/dev/nvidia0")
] ]
++ optionals app.useCommonPaths cfg.commonPaths ++ optionals app.useCommonPaths cfg.commonPaths
++ app.extraPaths; ++ app.extraPaths;
auto_etc = true; auto_etc = true;
cover = [ "/var/run/nscd" ];
symlink = symlink = [
[ {
[ target = "/run/current-system";
"*/run/current-system" linkname = "*/run/current-system";
"/run/current-system" }
]
] ]
++ optionals (isGraphical && config.hardware.graphics.enable) ( ++ optionals (isGraphical && config.hardware.graphics.enable) (
[ [
[ {
config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver"."L+".argument target = "/run/opengl-driver";
"/run/opengl-driver" linkname = config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver"."L+".argument;
] }
] ]
++ optionals (app.multiarch && config.hardware.graphics.enable32Bit) [ ++ optionals (app.multiarch && config.hardware.graphics.enable32Bit) [
[ {
config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver-32"."L+".argument target = "/run/opengl-driver-32";
/run/opengl-driver-32 linkname = config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver-32"."L+".argument;
] }
] ]
); );
}; };
}; };
checkedConfig =
name: value:
let
file = pkgs.writeText name (builtins.toJSON value);
in
pkgs.runCommand "checked-${name}" { nativeBuildInputs = [ cfg.package ]; } ''
ln -vs ${file} "$out"
hakurei show ${file}
'';
in in
pkgs.writeShellScriptBin app.name '' pkgs.writeShellScriptBin app.name ''
exec hakurei${if app.verbose then " -v" else ""} app ${pkgs.writeText "hakurei-app-${app.name}.json" (builtins.toJSON conf)} $@ exec hakurei${if app.verbose then " -v" else ""} app ${checkedConfig "hakurei-app-${app.name}.json" conf} $@
'' ''
) )
] ]
@ -211,7 +226,7 @@ in
pkg = if app.share != null then app.share else pkgs.${app.name}; pkg = if app.share != null then app.share else pkgs.${app.name};
copy = source: "[ -d '${source}' ] && cp -Lrv '${source}' $out/share || true"; copy = source: "[ -d '${source}' ] && cp -Lrv '${source}' $out/share || true";
in in
optional (app.capability.wayland || app.capability.x11) ( optional (app.enablements.wayland || app.enablements.x11) (
pkgs.runCommand "${app.name}-share" { } '' pkgs.runCommand "${app.name}-share" { } ''
mkdir -p $out/share mkdir -p $out/share
${copy "${pkg}/share/applications"} ${copy "${pkg}/share/applications"}

View File

@ -3,38 +3,6 @@ packages:
let let
inherit (lib) types mkOption mkEnableOption; inherit (lib) types mkOption mkEnableOption;
mountPoint =
let
inherit (types)
str
submodule
nullOr
listOf
;
in
listOf (submodule {
options = {
dst = mkOption {
type = nullOr str;
default = null;
description = ''
Mount point in container, same as src if null.
'';
};
src = mkOption {
type = str;
description = ''
Host filesystem path to make available to the container.
'';
};
write = mkEnableOption "mounting path as writable";
dev = mkEnableOption "use of device files";
require = mkEnableOption "start failure if the bind mount cannot be established for any reason";
};
});
in in
{ {
@ -76,6 +44,7 @@ in
type = type =
let let
inherit (types) inherit (types)
int
ints ints
str str
bool bool
@ -195,6 +164,16 @@ in
''; '';
}; };
wait_delay = mkOption {
type = nullOr int;
default = null;
description = ''
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.
Setting this to null defaults to five seconds.
'';
};
devel = mkEnableOption "debugging-related kernel interfaces"; devel = mkEnableOption "debugging-related kernel interfaces";
userns = mkEnableOption "user namespace creation"; userns = mkEnableOption "user namespace creation";
tty = mkEnableOption "access to the controlling terminal"; tty = mkEnableOption "access to the controlling terminal";
@ -226,16 +205,16 @@ in
}; };
extraPaths = mkOption { extraPaths = mkOption {
type = mountPoint; type = anything;
default = [ ]; default = [ ];
description = '' description = ''
Extra paths to make available to the container. Extra paths to make available to the container.
''; '';
}; };
capability = { enablements = {
wayland = mkOption { wayland = mkOption {
type = bool; type = nullOr bool;
default = true; default = true;
description = '' description = ''
Whether to share the Wayland socket. Whether to share the Wayland socket.
@ -243,7 +222,7 @@ in
}; };
x11 = mkOption { x11 = mkOption {
type = bool; type = nullOr bool;
default = false; default = false;
description = '' description = ''
Whether to share the X11 socket and allow connection. Whether to share the X11 socket and allow connection.
@ -251,7 +230,7 @@ in
}; };
dbus = mkOption { dbus = mkOption {
type = bool; type = nullOr bool;
default = true; default = true;
description = '' description = ''
Whether to proxy D-Bus. Whether to proxy D-Bus.
@ -259,7 +238,7 @@ in
}; };
pulse = mkOption { pulse = mkOption {
type = bool; type = nullOr bool;
default = true; default = true;
description = '' description = ''
Whether to share the PulseAudio socket and cookie. Whether to share the PulseAudio socket and cookie.
@ -284,13 +263,21 @@ in
}; };
commonPaths = mkOption { commonPaths = mkOption {
type = mountPoint; type = types.anything;
default = [ ]; default = [ ];
description = '' description = ''
Common extra paths to make available to the container. Common extra paths to make available to the container.
''; '';
}; };
shell = mkOption {
type = types.str;
default = "/run/current-system/sw/bin/bash";
description = ''
Absolute path to preferred shell.
'';
};
stateDir = mkOption { stateDir = mkOption {
type = types.str; type = types.str;
description = '' description = ''

View File

@ -14,7 +14,7 @@
wayland-scanner, wayland-scanner,
xorg, xorg,
# for planterette # for hpkg
zstd, zstd,
gnutar, gnutar,
coreutils, coreutils,
@ -32,7 +32,7 @@
buildGoModule rec { buildGoModule rec {
pname = "hakurei"; pname = "hakurei";
version = "0.1.1"; version = "0.1.3";
srcFiltered = builtins.path { srcFiltered = builtins.path {
name = "${pname}-src"; name = "${pname}-src";
@ -124,7 +124,7 @@ buildGoModule rec {
makeBinaryWrapper "$out/libexec/hakurei" "$out/bin/hakurei" \ makeBinaryWrapper "$out/libexec/hakurei" "$out/bin/hakurei" \
--inherit-argv0 --prefix PATH : ${lib.makeBinPath appPackages} --inherit-argv0 --prefix PATH : ${lib.makeBinPath appPackages}
makeBinaryWrapper "$out/libexec/planterette" "$out/bin/planterette" \ makeBinaryWrapper "$out/libexec/hpkg" "$out/bin/hpkg" \
--inherit-argv0 --prefix PATH : ${ --inherit-argv0 --prefix PATH : ${
lib.makeBinPath ( lib.makeBinPath (
appPackages appPackages

View File

@ -3,6 +3,7 @@ package system
import ( import (
"testing" "testing"
"hakurei.app/container"
"hakurei.app/system/acl" "hakurei.app/system/acl"
) )
@ -52,19 +53,19 @@ func TestACLString(t *testing.T) {
et Enablement et Enablement
perms []acl.Perm perms []acl.Perm
}{ }{
{`--- type: process path: "/nonexistent"`, Process, []acl.Perm{}}, {`--- type: process path: "/proc/nonexistent"`, Process, []acl.Perm{}},
{`r-- type: user path: "/nonexistent"`, User, []acl.Perm{acl.Read}}, {`r-- type: user path: "/proc/nonexistent"`, User, []acl.Perm{acl.Read}},
{`-w- type: wayland path: "/nonexistent"`, EWayland, []acl.Perm{acl.Write}}, {`-w- type: wayland path: "/proc/nonexistent"`, EWayland, []acl.Perm{acl.Write}},
{`--x type: x11 path: "/nonexistent"`, EX11, []acl.Perm{acl.Execute}}, {`--x type: x11 path: "/proc/nonexistent"`, EX11, []acl.Perm{acl.Execute}},
{`rw- type: dbus path: "/nonexistent"`, EDBus, []acl.Perm{acl.Read, acl.Write}}, {`rw- type: dbus path: "/proc/nonexistent"`, EDBus, []acl.Perm{acl.Read, acl.Write}},
{`r-x type: pulseaudio path: "/nonexistent"`, EPulse, []acl.Perm{acl.Read, acl.Execute}}, {`r-x type: pulseaudio path: "/proc/nonexistent"`, EPulse, []acl.Perm{acl.Read, acl.Execute}},
{`rwx type: user path: "/nonexistent"`, User, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, {`rwx type: user path: "/proc/nonexistent"`, User, []acl.Perm{acl.Read, acl.Write, acl.Execute}},
{`rwx type: process path: "/nonexistent"`, Process, []acl.Perm{acl.Read, acl.Write, acl.Write, acl.Execute}}, {`rwx type: process path: "/proc/nonexistent"`, Process, []acl.Perm{acl.Read, acl.Write, acl.Write, acl.Execute}},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.want, func(t *testing.T) { t.Run(tc.want, func(t *testing.T) {
a := &ACL{et: tc.et, perms: tc.perms, path: "/nonexistent"} a := &ACL{et: tc.et, perms: tc.perms, path: container.Nonexistent}
if got := a.String(); got != tc.want { if got := a.String(); got != tc.want {
t.Errorf("String() = %v, want %v", t.Errorf("String() = %v, want %v",
got, tc.want) got, tc.want)

View File

@ -1,22 +1,17 @@
package dbus_test package dbus_test
import ( import (
"bytes"
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"os" "os"
"os/exec"
"strings" "strings"
"syscall" "syscall"
"testing" "testing"
"time" "time"
"hakurei.app/container"
"hakurei.app/helper" "hakurei.app/helper"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
) )
@ -64,20 +59,23 @@ func TestFinalise(t *testing.T) {
} }
func TestProxyStartWaitCloseString(t *testing.T) { func TestProxyStartWaitCloseString(t *testing.T) {
oldWaitDelay := helper.WaitDelay t.Run("sandbox", func(t *testing.T) { testProxyFinaliseStartWaitCloseString(t, true) })
helper.WaitDelay = 16 * time.Second
t.Cleanup(func() { helper.WaitDelay = oldWaitDelay })
t.Run("sandbox", func(t *testing.T) {
proxyName := dbus.ProxyName
dbus.ProxyName = os.Args[0]
t.Cleanup(func() { dbus.ProxyName = proxyName })
testProxyFinaliseStartWaitCloseString(t, true)
})
t.Run("direct", func(t *testing.T) { testProxyFinaliseStartWaitCloseString(t, false) }) t.Run("direct", func(t *testing.T) { testProxyFinaliseStartWaitCloseString(t, false) })
} }
func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) { func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) {
{
oldWaitDelay := helper.WaitDelay
helper.WaitDelay = 16 * time.Second
t.Cleanup(func() { helper.WaitDelay = oldWaitDelay })
}
{
proxyName := dbus.ProxyName
dbus.ProxyName = os.Args[0]
t.Cleanup(func() { dbus.ProxyName = proxyName })
}
var p *dbus.Proxy var p *dbus.Proxy
t.Run("string for nil proxy", func(t *testing.T) { t.Run("string for nil proxy", func(t *testing.T) {
@ -122,35 +120,12 @@ func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) {
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel() defer cancel()
if !useSandbox {
p = dbus.NewDirect(ctx, final, nil)
} else {
p = dbus.New(ctx, final, nil)
}
p.CommandContext = func(ctx context.Context) (cmd *exec.Cmd) {
return exec.CommandContext(ctx, os.Args[0], "-test.v",
"-test.run=TestHelperInit", "--", "init")
}
p.CmdF = func(v any) {
if useSandbox {
z := v.(*container.Container)
if z.Args[0] != dbus.ProxyName {
panic(fmt.Sprintf("unexpected argv0 %q", os.Args[0]))
}
z.Args = append([]string{os.Args[0], "-test.run=TestHelperStub", "--"}, z.Args[1:]...)
} else {
cmd := v.(*exec.Cmd)
if cmd.Args[0] != dbus.ProxyName {
panic(fmt.Sprintf("unexpected argv0 %q", os.Args[0]))
}
cmd.Err = nil
cmd.Path = os.Args[0]
cmd.Args = append([]string{os.Args[0], "-test.run=TestHelperStub", "--"}, cmd.Args[1:]...)
}
}
p.FilterF = func(v []byte) []byte { return bytes.SplitN(v, []byte("TestHelperInit\n"), 2)[1] }
output := new(strings.Builder) output := new(strings.Builder)
if !useSandbox {
p = dbus.NewDirect(ctx, final, output)
} else {
p = dbus.New(ctx, final, output)
}
t.Run("invalid wait", func(t *testing.T) { t.Run("invalid wait", func(t *testing.T) {
wantErr := "dbus: not started" wantErr := "dbus: not started"
@ -176,9 +151,9 @@ func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) {
} }
t.Run("string", func(t *testing.T) { t.Run("string", func(t *testing.T) {
wantSubstr := fmt.Sprintf("%s -test.run=TestHelperStub -- --args=3 --fd=4", os.Args[0]) wantSubstr := fmt.Sprintf("%s --args=3 --fd=4", os.Args[0])
if useSandbox { if useSandbox {
wantSubstr = fmt.Sprintf(`argv: ["%s" "-test.run=TestHelperStub" "--" "--args=3" "--fd=4"], filter: true, rules: 0, flags: 0x1, presets: 0xf`, os.Args[0]) wantSubstr = `argv: ["xdg-dbus-proxy" "--args=3" "--fd=4"], filter: true, rules: 0, flags: 0x1, presets: 0xf`
} }
if got := p.String(); !strings.Contains(got, wantSubstr) { if got := p.String(); !strings.Contains(got, wantSubstr) {
t.Errorf("String: %q, want %q", t.Errorf("String: %q, want %q",
@ -203,11 +178,3 @@ func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) {
}) })
} }
} }
func TestHelperInit(t *testing.T) {
if len(os.Args) != 5 || os.Args[4] != "init" {
return
}
container.SetOutput(hlog.Output{})
container.Init(hlog.Prepare, internal.InstallOutput)
}

View File

@ -5,9 +5,6 @@ import (
"errors" "errors"
"os" "os"
"os/exec" "os/exec"
"path"
"path/filepath"
"slices"
"strconv" "strconv"
"syscall" "syscall"
@ -36,9 +33,6 @@ func (p *Proxy) Start() error {
if !p.useSandbox { if !p.useSandbox {
p.helper = helper.NewDirect(ctx, p.name, p.final, true, argF, func(cmd *exec.Cmd) { p.helper = helper.NewDirect(ctx, p.name, p.final, true, argF, func(cmd *exec.Cmd) {
if p.CmdF != nil {
p.CmdF(cmd)
}
if p.output != nil { if p.output != nil {
cmd.Stdout, cmd.Stderr = p.output, p.output cmd.Stdout, cmd.Stderr = p.output, p.output
} }
@ -46,24 +40,26 @@ func (p *Proxy) Start() error {
cmd.Env = make([]string, 0) cmd.Env = make([]string, 0)
}, nil) }, nil)
} else { } else {
toolPath := p.name var toolPath *container.Absolute
if filepath.Base(p.name) == p.name { if a, err := container.NewAbs(p.name); err != nil {
if s, err := exec.LookPath(p.name); err != nil { if p.name, err = exec.LookPath(p.name); err != nil {
return err
} else if toolPath, err = container.NewAbs(p.name); err != nil {
return err return err
} else {
toolPath = s
} }
} else {
toolPath = a
} }
var libPaths []string var libPaths []*container.Absolute
if entries, err := ldd.ExecFilter(ctx, p.CommandContext, p.FilterF, toolPath); err != nil { if entries, err := ldd.Exec(ctx, toolPath.String()); err != nil {
return err return err
} else { } else {
libPaths = ldd.Path(entries) libPaths = ldd.Path(entries)
} }
p.helper = helper.New( p.helper = helper.New(
ctx, toolPath, ctx, toolPath, "xdg-dbus-proxy",
p.final, true, p.final, true,
argF, func(z *container.Container) { argF, func(z *container.Container) {
z.SeccompFlags |= seccomp.AllowMultiarch z.SeccompFlags |= seccomp.AllowMultiarch
@ -73,57 +69,56 @@ func (p *Proxy) Start() error {
z.ScopeAbstract = false z.ScopeAbstract = false
z.Hostname = "hakurei-dbus" z.Hostname = "hakurei-dbus"
z.CommandContext = p.CommandContext
if p.output != nil { if p.output != nil {
z.Stdout, z.Stderr = p.output, p.output z.Stdout, z.Stderr = p.output, p.output
} }
if p.CmdF != nil {
p.CmdF(z)
}
// these lib paths are unpredictable, so mount them first so they cannot cover anything // these lib paths are unpredictable, so mount them first so they cannot cover anything
for _, name := range libPaths { for _, name := range libPaths {
z.Bind(name, name, 0) z.Bind(name, name, 0)
} }
// upstream bus directories // upstream bus directories
upstreamPaths := make([]string, 0, 2) upstreamPaths := make([]*container.Absolute, 0, 2)
for _, addr := range [][]AddrEntry{p.final.SessionUpstream, p.final.SystemUpstream} { for _, addr := range [][]AddrEntry{p.final.SessionUpstream, p.final.SystemUpstream} {
for _, ent := range addr { for _, ent := range addr {
if ent.Method != "unix" { if ent.Method != "unix" {
continue continue
} }
for _, pair := range ent.Values { for _, pair := range ent.Values {
if pair[0] != "path" || !path.IsAbs(pair[1]) { if pair[0] != "path" {
continue continue
} }
upstreamPaths = append(upstreamPaths, path.Dir(pair[1])) if a, err := container.NewAbs(pair[1]); err != nil {
continue
} else {
upstreamPaths = append(upstreamPaths, a.Dir())
} }
} }
} }
slices.Sort(upstreamPaths) }
upstreamPaths = slices.Compact(upstreamPaths) container.SortAbs(upstreamPaths)
upstreamPaths = container.CompactAbs(upstreamPaths)
for _, name := range upstreamPaths { for _, name := range upstreamPaths {
z.Bind(name, name, 0) z.Bind(name, name, 0)
} }
// parent directories of bind paths // parent directories of bind paths
sockDirPaths := make([]string, 0, 2) sockDirPaths := make([]*container.Absolute, 0, 2)
if d := path.Dir(p.final.Session[1]); path.IsAbs(d) { if a, err := container.NewAbs(p.final.Session[1]); err == nil {
sockDirPaths = append(sockDirPaths, d) sockDirPaths = append(sockDirPaths, a.Dir())
} }
if d := path.Dir(p.final.System[1]); path.IsAbs(d) { if a, err := container.NewAbs(p.final.System[1]); err == nil {
sockDirPaths = append(sockDirPaths, d) sockDirPaths = append(sockDirPaths, a.Dir())
} }
slices.Sort(sockDirPaths) container.SortAbs(sockDirPaths)
sockDirPaths = slices.Compact(sockDirPaths) sockDirPaths = container.CompactAbs(sockDirPaths)
for _, name := range sockDirPaths { for _, name := range sockDirPaths {
z.Bind(name, name, container.BindWritable) z.Bind(name, name, container.BindWritable)
} }
// xdg-dbus-proxy bin path // xdg-dbus-proxy bin path
binPath := path.Dir(toolPath) binPath := toolPath.Dir()
z.Bind(binPath, binPath, 0) z.Bind(binPath, binPath, 0)
}, nil) }, nil)
} }

17
system/dbus/proc_test.go Normal file
View File

@ -0,0 +1,17 @@
package dbus_test
import (
"os"
"testing"
"hakurei.app/container"
"hakurei.app/helper"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
)
func TestMain(m *testing.M) {
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
helper.InternalHelperStub()
os.Exit(m.Run())
}

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"os/exec"
"sync" "sync"
"syscall" "syscall"
@ -37,10 +36,6 @@ type Proxy struct {
useSandbox bool useSandbox bool
name string name string
CmdF func(any)
CommandContext func(ctx context.Context) (cmd *exec.Cmd)
FilterF func([]byte) []byte
mu, pmu sync.RWMutex mu, pmu sync.RWMutex
} }

View File

@ -1,9 +0,0 @@
package dbus_test
import (
"testing"
"hakurei.app/helper"
)
func TestHelperStub(t *testing.T) { helper.InternalHelperStub() }

View File

@ -3,6 +3,8 @@ package system
import ( import (
"os" "os"
"testing" "testing"
"hakurei.app/container"
) )
func TestEnsure(t *testing.T) { func TestEnsure(t *testing.T) {
@ -60,11 +62,11 @@ func TestMkdirString(t *testing.T) {
t.Run(tc.want, func(t *testing.T) { t.Run(tc.want, func(t *testing.T) {
m := &Mkdir{ m := &Mkdir{
et: tc.et, et: tc.et,
path: "/nonexistent", path: container.Nonexistent,
perm: 0701, perm: 0701,
ephemeral: tc.ephemeral, ephemeral: tc.ephemeral,
} }
want := "mode: " + os.FileMode(0701).String() + " type: " + tc.want + " path: \"/nonexistent\"" want := "mode: " + os.FileMode(0701).String() + " type: " + tc.want + ` path: "/proc/nonexistent"`
if got := m.String(); got != want { if got := m.String(); got != want {
t.Errorf("String() = %v, want %v", got, want) t.Errorf("String() = %v, want %v", got, want)
} }

View File

@ -82,13 +82,18 @@
jack.enable = true; jack.enable = true;
}; };
virtualisation.qemu.options = [ virtualisation = {
# Hopefully reduces spurious test failures:
memorySize = 4096;
qemu.options = [
# Need to switch to a different GPU driver than the default one (-vga std) so that Sway can launch: # Need to switch to a different GPU driver than the default one (-vga std) so that Sway can launch:
"-vga none -device virtio-gpu-pci" "-vga none -device virtio-gpu-pci"
# Increase Go test compiler performance: # Increase Go test compiler performance:
"-smp 8" "-smp 8"
]; ];
};
environment.hakurei = { environment.hakurei = {
enable = true; enable = true;
@ -121,7 +126,22 @@
wayland-utils wayland-utils
]; ];
command = "foot"; command = "foot";
capability = { enablements = {
dbus = false;
pulse = false;
};
};
"cat.gensokyo.extern.foot.noEnablements.immediate" = {
name = "ne-foot-immediate";
identity = 1;
shareUid = true;
verbose = true;
wait_delay = -1;
share = pkgs.foot;
packages = [ ];
command = "foot";
enablements = {
dbus = false; dbus = false;
pulse = false; pulse = false;
}; };
@ -134,7 +154,7 @@
share = pkgs.foot; share = pkgs.foot;
packages = [ pkgs.foot ]; packages = [ pkgs.foot ];
command = "foot"; command = "foot";
capability.dbus = false; enablements.dbus = false;
}; };
"cat.gensokyo.extern.Alacritty.x11" = { "cat.gensokyo.extern.Alacritty.x11" = {
@ -151,7 +171,7 @@
mesa-demos mesa-demos
]; ];
command = "alacritty"; command = "alacritty";
capability = { enablements = {
wayland = false; wayland = false;
x11 = true; x11 = true;
dbus = false; dbus = false;
@ -172,7 +192,7 @@
wayland-utils wayland-utils
]; ];
command = "foot"; command = "foot";
capability = { enablements = {
dbus = false; dbus = false;
pulse = false; pulse = false;
}; };
@ -184,7 +204,7 @@
verbose = true; verbose = true;
share = pkgs.strace; share = pkgs.strace;
command = "strace true"; command = "strace true";
capability = { enablements = {
wayland = false; wayland = false;
x11 = false; x11 = false;
dbus = false; dbus = false;

View File

@ -0,0 +1,60 @@
{ pkgs, ... }:
{
system.stateVersion = "23.05";
users.users = {
alice = {
isNormalUser = true;
description = "Alice Foobar";
password = "foobar";
uid = 1000;
extraGroups = [ "wheel" ];
};
untrusted = {
isNormalUser = true;
description = "Untrusted user";
password = "foobar";
uid = 1001;
};
};
home-manager.users.alice.home.stateVersion = "24.11";
security = {
sudo.wheelNeedsPassword = false;
rtkit.enable = true;
};
services = {
getty.autologinUser = "alice";
pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
jack.enable = true;
};
};
environment.variables = {
SWAYSOCK = "/tmp/sway-ipc.sock";
WLR_RENDERER = "pixman";
};
programs = {
sway.enable = true;
bash.loginShellInit = ''
if [ "$(tty)" = "/dev/tty1" ]; then
set -e
mkdir -p ~/.config/sway
(sed s/Mod4/Mod1/ /etc/sway/config &&
echo 'output * bg ${pkgs.nixos-artwork.wallpapers.simple-light-gray.gnomeFilePath} fill') > ~/.config/sway/config
sway --validate
systemd-cat --identifier=session sway && touch /tmp/sway-exit-ok
fi
'';
};
}

View File

@ -0,0 +1,25 @@
{ pkgs, ... }:
{
environment.hakurei = {
enable = true;
stateDir = "/var/lib/hakurei";
users.alice = 0;
apps = {
"cat.gensokyo.extern.foot.noEnablements" = {
name = "ne-foot";
identity = 1;
shareUid = true;
verbose = true;
share = pkgs.foot;
packages = [ pkgs.foot ];
command = "foot";
enablements = {
dbus = false;
pulse = false;
};
};
};
extraHomeConfig.home.stateVersion = "23.05";
};
}

View File

@ -0,0 +1,28 @@
{ lib, pkgs, ... }:
let
tracing = name: "\"/sys/kernel/debug/tracing/${name}\"";
in
{
environment.systemPackages = [
(pkgs.writeShellScriptBin "hakurei-set-up-tracing" ''
set -e
echo "$1" > ${tracing "set_graph_function"}
echo function_graph > ${tracing "current_tracer"}
echo funcgraph-tail > ${tracing "trace_options"}
echo funcgraph-retval > ${tracing "trace_options"}
echo nofuncgraph-cpu > ${tracing "trace_options"}
echo nofuncgraph-overhead > ${tracing "trace_options"}
echo nofuncgraph-duration > ${tracing "trace_options"}
'')
(pkgs.writeShellScriptBin "hakurei-print-trace" "exec cat ${tracing "trace"}")
(pkgs.writeShellScriptBin "hakurei-consume-trace" "exec cat ${tracing "trace_pipe"}")
];
boot.kernelPatches = [
{
name = "funcgraph-retval";
patch = null;
extraStructuredConfig.FUNCTION_GRAPH_RETVAL = lib.kernel.yes;
}
];
}

56
test/interactive/vm.nix Normal file
View File

@ -0,0 +1,56 @@
{
virtualisation.vmVariant.virtualisation = {
memorySize = 4096;
qemu.options = [
"-vga none -device virtio-gpu-pci"
"-smp 8"
];
mountHostNixStore = true;
writableStore = true;
writableStoreUseTmpfs = false;
sharedDirectories = {
cwd = {
target = "/mnt/.ro-cwd";
source = ''"$OLDPWD"'';
securityModel = "none";
};
};
fileSystems = {
"/mnt/.ro-cwd".options = [
"ro"
"noatime"
];
"/mnt/cwd".overlay = {
lowerdir = [ "/mnt/.ro-cwd" ];
upperdir = "/tmp/.cwd/upper";
workdir = "/tmp/.cwd/work";
};
"/mnt/src".overlay = {
lowerdir = [ ../.. ];
upperdir = "/tmp/.src/upper";
workdir = "/tmp/.src/work";
};
};
};
systemd.services = {
logrotate-checkconf.enable = false;
hakurei-src-fix-ownership = {
wantedBy = [ "multi-user.target" ];
wants = [ "mnt-src.mount" ];
after = [ "mnt-src.mount" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
chown -R alice:users /mnt/src/
chmod -R +w /mnt/src/
'';
};
};
}

View File

@ -23,10 +23,9 @@ let
; ;
}; };
callTestCase = importTestCase =
path: identity: path:
let import path {
tc = import path {
inherit inherit
fs fs
ent ent
@ -34,6 +33,11 @@ let
system system
; ;
}; };
callTestCase =
path: identity:
let
tc = importTestCase path;
in in
{ {
name = "check-sandbox-${tc.name}"; name = "check-sandbox-${tc.name}";
@ -61,9 +65,13 @@ let
testCaseName = name: "cat.gensokyo.hakurei.test." + name; testCaseName = name: "cat.gensokyo.hakurei.test." + name;
in in
{ {
apps = {
${testCaseName "preset"} = callTestCase ./preset.nix 1; ${testCaseName "preset"} = callTestCase ./preset.nix 1;
${testCaseName "tty"} = callTestCase ./tty.nix 2; ${testCaseName "tty"} = callTestCase ./tty.nix 2;
${testCaseName "mapuid"} = callTestCase ./mapuid.nix 3; ${testCaseName "mapuid"} = callTestCase ./mapuid.nix 3;
${testCaseName "device"} = callTestCase ./device.nix 4; ${testCaseName "device"} = callTestCase ./device.nix 4;
${testCaseName "pdlike"} = callTestCase ./pdlike.nix 5; ${testCaseName "pdlike"} = callTestCase ./pdlike.nix 5;
};
pd = importTestCase ./pd.nix;
} }

View File

@ -47,7 +47,10 @@ in
]; ];
fs = fs "dead" { fs = fs "dead" {
".hakurei" = fs "800001ed" { } null; ".hakurei" = fs "800001ed" {
".ro-store" = fs "801001fd" null null;
store = fs "800001ff" null null;
} null;
bin = fs "800001ed" { sh = fs "80001ff" null null; } null; bin = fs "800001ed" { sh = fs "80001ff" null null; } null;
dev = fs "800001ed" null null; dev = fs "800001ed" null null;
etc = fs "800001ed" { etc = fs "800001ed" {
@ -173,13 +176,6 @@ in
} null; } null;
} null; } null;
".local" = fs "800001ed" { ".local" = fs "800001ed" {
share = fs "800001ed" {
dbus-1 = fs "800001ed" {
services = fs "800001ed" {
"ca.desrt.dconf.service" = fs "80001ff" null null;
} null;
} null;
} null;
state = fs "800001ed" { state = fs "800001ed" {
".keep" = fs "80001ff" null ""; ".keep" = fs "80001ff" null "";
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null; home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;
@ -202,15 +198,14 @@ in
} null; } null;
} null; } null;
} null; } null;
run = fs "800001ed" { nscd = fs "800001ed" { } null; } null;
cache = fs "800001ed" { private = fs "800001c0" null null; } null; cache = fs "800001ed" { private = fs "800001c0" null null; } null;
} null; } null;
} null; } null;
mount = [ mount = [
(ent "/sysroot" "/" "rw,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000004,gid=1000004") (ent "/sysroot" "/" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000004,gid=1000004")
(ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw") (ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw")
(ent "/" "/.hakurei" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000004,gid=1000004") (ent "/" "/.hakurei" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000004,gid=1000004")
(ent "/" "/dev" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) (ent "/" "/dev" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,gid=3,mode=620,ptmxmode=666") (ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,gid=3,mode=620,ptmxmode=666")
(ent "/" "/dev/shm" "rw,nosuid,nodev" "tmpfs" "tmpfs" ignore) (ent "/" "/dev/shm" "rw,nosuid,nodev" "tmpfs" "tmpfs" ignore)
@ -226,8 +221,10 @@ in
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") (ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) (ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/.hakurei/.ro-store" "rw,relatime" "overlay" "overlay" "ro,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,redirect_dir=nofollow,userxattr")
(ent "/" "/.hakurei/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,upperdir=/host/tmp/.hakurei-store-rw/upper,workdir=/host/tmp/.hakurei-store-rw/work,redirect_dir=nofollow,userxattr")
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000004,gid=1000004") (ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000004,gid=1000004")
(ent "/tmp/hakurei.1000/runtime/4" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/tmp/hakurei.1000/runtime/4" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/hakurei.1000/tmpdir/4" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/tmp/hakurei.1000/tmpdir/4" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/lib/hakurei/u0/a4" "/var/lib/hakurei/u0/a4" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/var/lib/hakurei/u0/a4" "/var/lib/hakurei/u0/a4" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
@ -236,7 +233,6 @@ in
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore) (ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/var/run/nscd" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8k,mode=755,uid=1000004,gid=1000004")
]; ];
seccomp = true; seccomp = true;

View File

@ -56,7 +56,10 @@ in
]; ];
fs = fs "dead" { fs = fs "dead" {
".hakurei" = fs "800001ed" { } null; ".hakurei" = fs "800001ed" {
".ro-store" = fs "801001fd" null null;
store = fs "800001ff" null null;
} null;
bin = fs "800001ed" { sh = fs "80001ff" null null; } null; bin = fs "800001ed" { sh = fs "80001ff" null null; } null;
dev = fs "800001ed" { dev = fs "800001ed" {
core = fs "80001ff" null null; core = fs "80001ff" null null;
@ -199,13 +202,6 @@ in
} null; } null;
} null; } null;
".local" = fs "800001ed" { ".local" = fs "800001ed" {
share = fs "800001ed" {
dbus-1 = fs "800001ed" {
services = fs "800001ed" {
"ca.desrt.dconf.service" = fs "80001ff" null null;
} null;
} null;
} null;
state = fs "800001ed" { state = fs "800001ed" {
".keep" = fs "80001ff" null ""; ".keep" = fs "80001ff" null "";
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null; home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;
@ -228,16 +224,15 @@ in
} null; } null;
} null; } null;
} null; } null;
run = fs "800001ed" { nscd = fs "800001ed" { } null; } null;
cache = fs "800001ed" { private = fs "800001c0" null null; } null; cache = fs "800001ed" { private = fs "800001c0" null null; } null;
} null; } null;
} null; } null;
mount = [ mount = [
(ent "/sysroot" "/" "rw,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000003,gid=1000003") (ent "/sysroot" "/" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000003,gid=1000003")
(ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw") (ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw")
(ent "/" "/.hakurei" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000003,gid=1000003") (ent "/" "/.hakurei" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000003,gid=1000003")
(ent "/" "/dev" "rw,nosuid,nodev,relatime" "tmpfs" "devtmpfs" "rw,mode=755,uid=1000003,gid=1000003") (ent "/" "/dev" "ro,nosuid,nodev,relatime" "tmpfs" "devtmpfs" "rw,mode=755,uid=1000003,gid=1000003")
(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)
@ -256,8 +251,10 @@ in
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") (ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) (ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/.hakurei/.ro-store" "rw,relatime" "overlay" "overlay" "ro,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,redirect_dir=nofollow,userxattr")
(ent "/" "/.hakurei/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,upperdir=/host/tmp/.hakurei-store-rw/upper,workdir=/host/tmp/.hakurei-store-rw/work,redirect_dir=nofollow,userxattr")
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000003,gid=1000003") (ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000003,gid=1000003")
(ent "/tmp/hakurei.1000/runtime/3" "/run/user/1000" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/tmp/hakurei.1000/runtime/3" "/run/user/1000" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/hakurei.1000/tmpdir/3" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/tmp/hakurei.1000/tmpdir/3" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/lib/hakurei/u0/a3" "/var/lib/hakurei/u0/a3" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/var/lib/hakurei/u0/a3" "/var/lib/hakurei/u0/a3" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
@ -266,7 +263,6 @@ in
(ent ignore "/run/user/1000/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent ignore "/run/user/1000/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/1000/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore) (ent ignore "/run/user/1000/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent ignore "/run/user/1000/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent ignore "/run/user/1000/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/var/run/nscd" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8k,mode=755,uid=1000003,gid=1000003")
]; ];
seccomp = true; seccomp = true;

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