Compare commits

...

172 Commits

Author SHA1 Message Date
34ccda84b2
release: 0.3.0
All checks were successful
Release / Create release (push) Successful in 39s
Test / Sandbox (push) Successful in 39s
Test / Hakurei (push) Successful in 3m20s
Test / Create distribution (push) Successful in 24s
Test / Sandbox (race detector) (push) Successful in 4m0s
Test / Hpkg (push) Successful in 3m37s
Test / Hakurei (race detector) (push) Successful in 4m53s
Test / Flake checks (push) Successful in 1m37s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-06 01:37:15 +09:00
042013bb04
container/std: syscall JSON adapter
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 39s
Test / Hakurei (push) Successful in 43s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Hpkg (push) Successful in 40s
Test / Flake checks (push) Successful in 1m36s
This provides cross-platform JSON adapter for syscall number.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-06 00:57:53 +09:00
5c2b63a7f1
container: add 386 constants
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m17s
Test / Hakurei (push) Successful in 3m11s
Test / Hpkg (push) Successful in 4m0s
Test / Sandbox (race detector) (push) Successful in 4m16s
Test / Hakurei (race detector) (push) Successful in 5m2s
Test / Flake checks (push) Successful in 1m24s
While it is unlikely a use case for hakurei on i686 exists, it does not hurt to have this support.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-05 20:21:14 +09:00
9fd97e71d0
treewide: fit test untyped int literals in 32-bit
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m17s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 3m56s
Test / Sandbox (race detector) (push) Successful in 4m6s
Test / Hakurei (race detector) (push) Successful in 5m2s
Test / Flake checks (push) Successful in 1m24s
This enables hakurei test suite to run on 32-bit targets.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-05 20:13:19 +09:00
fba201c995
container/std: relocate rule types
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m13s
Test / Hpkg (push) Successful in 3m56s
Test / Sandbox (race detector) (push) Successful in 4m14s
Test / Hakurei (race detector) (push) Successful in 5m3s
Test / Flake checks (push) Successful in 1m28s
This enables its use in hst for #15.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-05 06:00:39 +09:00
7f27a6dc51
container/seccomp: use native types
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m16s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m12s
Test / Hakurei (race detector) (push) Successful in 5m1s
Test / Flake checks (push) Successful in 1m30s
This prepares NativeRule for relocation to std for #15.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-05 05:48:59 +09:00
b65aba9446
container/seccomp: alias libseccomp types
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m18s
Test / Hpkg (push) Successful in 4m6s
Test / Sandbox (race detector) (push) Successful in 4m20s
Test / Hakurei (race detector) (push) Successful in 5m2s
Test / Flake checks (push) Successful in 1m29s
This enables tests to refer to these types and check its size.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-05 05:21:43 +09:00
becaf8b6d7
std: relocate seccomp lookup tables
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m18s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m5s
Test / Sandbox (race detector) (push) Successful in 4m9s
Test / Hakurei (race detector) (push) Successful in 5m0s
Test / Flake checks (push) Successful in 1m28s
This should enable resolving NativeRule in hst.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-05 04:48:05 +09:00
54c0d6bf48
container/seccomp/pnr: define pseudo syscalls
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m21s
Test / Hakurei (push) Successful in 3m12s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m5s
Test / Hakurei (race detector) (push) Successful in 4m58s
Test / Flake checks (push) Successful in 1m27s
This eliminates the cgo dependency from syscall lookup.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-05 04:32:41 +09:00
c1399f5030
std: rename from comp
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 3m59s
Test / Sandbox (race detector) (push) Successful in 4m10s
Test / Hakurei (race detector) (push) Successful in 5m4s
Test / Flake checks (push) Successful in 1m28s
Seccomp lookup tables are going to be relocated here, and PNR constants.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-05 02:47:43 +09:00
9ac63aac0c
hst/grp_pwd: add extra test cases
All checks were successful
Test / Create distribution (push) Successful in 45s
Test / Sandbox (push) Successful in 2m31s
Test / Hakurei (push) Successful in 3m37s
Test / Hpkg (push) Successful in 4m15s
Test / Sandbox (race detector) (push) Successful in 4m21s
Test / Hakurei (race detector) (push) Successful in 5m16s
Test / Flake checks (push) Successful in 1m26s
Does not change coverage but this helps me crosscheck with my phone.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-05 01:42:42 +09:00
cb9ebf0e15
hst/grp_pwd: specify new uid format
All checks were successful
Test / Create distribution (push) Successful in 27s
Test / Sandbox (push) Successful in 41s
Test / Sandbox (race detector) (push) Successful in 41s
Test / Hpkg (push) Successful in 42s
Test / Hakurei (push) Successful in 47s
Test / Hakurei (race detector) (push) Successful in 46s
Test / Flake checks (push) Successful in 1m31s
This leaves slots available for additional uid ranges in Rosa OS.

This breaks all existing installations! Users are required to fix ownership manually.

Closes #18.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-04 08:24:41 +09:00
9a2a7b749f
cmd/hakurei/print: handle nil config
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Sandbox (race detector) (push) Successful in 40s
Test / Sandbox (push) Successful in 41s
Test / Hakurei (push) Successful in 44s
Test / Hpkg (push) Successful in 42s
Test / Hakurei (race detector) (push) Successful in 45s
Test / Flake checks (push) Successful in 1m37s
There is nothing to print in this case, and such a nil check is missing.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-03 02:20:18 +09:00
ec5cb9400c
cmd/hpkg/test: print share directory
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 40s
Test / Hakurei (push) Successful in 43s
Test / Hakurei (race detector) (push) Successful in 44s
Test / Hpkg (push) Successful in 40s
Test / Flake checks (push) Successful in 1m30s
This is more useful now that state is tracked here.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-03 01:51:57 +09:00
ae66b3d2fb
message: rename NewMsg to New
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m15s
Test / Hpkg (push) Successful in 4m7s
Test / Sandbox (race detector) (push) Successful in 4m14s
Test / Hakurei (race detector) (push) Successful in 5m7s
Test / Flake checks (push) Successful in 1m37s
Should have done this when relocating this from container. Now is a good time to rename it before v0.3.x.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-03 01:49:27 +09:00
149bc3671a
internal/store: remove compat adapter
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m16s
Test / Hakurei (push) Successful in 3m17s
Test / Sandbox (race detector) (push) Successful in 4m12s
Test / Hpkg (push) Successful in 4m18s
Test / Hakurei (race detector) (push) Successful in 5m3s
Test / Flake checks (push) Successful in 1m30s
This is no longer used as everything has been migrated.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-03 01:26:01 +09:00
24435694a5
hst/config: make identifier omitempty
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m11s
Test / Hakurei (push) Successful in 3m17s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m12s
Test / Hakurei (race detector) (push) Successful in 5m7s
Test / Flake checks (push) Successful in 1m33s
This is an optional field. Serialise it as such.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-03 01:23:15 +09:00
1c168babf2
cmd/hakurei/print: use new store interface
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m11s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m11s
Test / Hakurei (race detector) (push) Successful in 5m3s
Test / Flake checks (push) Successful in 1m40s
This removes the final uses of the compat interfaces.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-03 01:19:16 +09:00
0edcb7c1d3
test: print share directory
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (race detector) (push) Successful in 41s
Test / Sandbox (push) Successful in 41s
Test / Hpkg (push) Successful in 41s
Test / Hakurei (push) Successful in 2m24s
Test / Hakurei (race detector) (push) Successful in 3m3s
Test / Flake checks (push) Successful in 1m29s
This is more useful now that state is tracked here.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 17:00:59 +09:00
0e5ca74b98
cmd/hakurei/print: serialise array for ps
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 40s
Test / Sandbox (race detector) (push) Successful in 42s
Test / Hakurei (push) Successful in 2m25s
Test / Hakurei (race detector) (push) Successful in 3m7s
Test / Hpkg (push) Successful in 3m13s
Test / Flake checks (push) Successful in 1m27s
Wanted to do this for a long time, since the key is redundant. This also makes it easier to migrate to the new store interface.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 16:37:08 +09:00
23ae7822bf
cmd/hakurei/parse: use new store interface
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m21s
Test / Sandbox (race detector) (push) Successful in 4m16s
Test / Hpkg (push) Successful in 4m15s
Test / Hakurei (race detector) (push) Successful in 4m58s
Test / Hakurei (push) Successful in 2m16s
Test / Flake checks (push) Successful in 1m28s
This greatly reduces overhead. The iterator also significantly cleans up the usage code.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 16:00:41 +09:00
898b5aed3d
internal/store: iterator over all entries
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m27s
Test / Hakurei (push) Successful in 3m13s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m10s
Test / Hakurei (race detector) (push) Successful in 4m59s
Test / Flake checks (push) Successful in 1m31s
This is quite convenient for searching the store or printing active instance information.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 15:54:00 +09:00
7c3c3135d8
internal/outcome: track state in TMPDIR
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m7s
Test / Hpkg (push) Successful in 4m3s
Test / Sandbox (race detector) (push) Successful in 4m10s
Test / Hakurei (race detector) (push) Successful in 4m56s
Test / Flake checks (push) Successful in 1m30s
The SharePath is a more stable path than RunDirPath, since it is available all the time and should remain consistent. This also fits better into the intended use case of XDG_RUNTIME_DIR.

Closes #17.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 12:40:58 +09:00
f33aea9ff9
internal/env: cleaner runtime dir fallback
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m16s
Test / Hakurei (push) Successful in 3m10s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m14s
Test / Hakurei (race detector) (push) Successful in 4m57s
Test / Flake checks (push) Successful in 1m28s
This now places rundir inside the fallback runtime dir, so special case in internal/outcome is avoided.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 12:22:32 +09:00
e7fc311d0b
internal/outcome/shim: cover reparent and exit request paths
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Hakurei (push) Successful in 42s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Hpkg (push) Successful in 41s
Test / Flake checks (push) Successful in 1m31s
These test cases were missed when making the changes.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 11:58:09 +09:00
f5274067f6
internal/outcome/process: nil-safe unlock when failing to lock
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m9s
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hpkg (push) Successful in 4m13s
Test / Hakurei (race detector) (push) Successful in 4m57s
Test / Flake checks (push) Successful in 1m26s
This also prints a debug message which might be useful.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 11:47:51 +09:00
e7161f8e61
internal/outcome: measure finalise time
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m16s
Test / Hakurei (push) Successful in 3m11s
Test / Sandbox (race detector) (push) Successful in 4m4s
Test / Hpkg (push) Successful in 4m8s
Test / Hakurei (race detector) (push) Successful in 4m56s
Test / Flake checks (push) Successful in 1m19s
This also increases precision of state time output.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 05:17:33 +09:00
6931ad95c3
internal/outcome/shim: EOF as exit request fallback
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 55s
Test / Sandbox (race detector) (push) Successful in 53s
Test / Hpkg (push) Successful in 53s
Test / Hakurei (race detector) (push) Successful in 1m1s
Test / Hakurei (push) Successful in 1m3s
Test / Flake checks (push) Successful in 1m34s
In some cases the signal might be delivered before the signal handler is installed, and synchronising against such a case is too expensive. Instead, use the pipe being closed as a fallback to the regular exit request. This change also moves installation of the signal handler early.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 04:41:26 +09:00
2ba599b399
internal/outcome/process: use new store interface
All checks were successful
Test / Create distribution (push) Successful in 42s
Test / Sandbox (push) Successful in 2m26s
Test / Hakurei (push) Successful in 3m20s
Test / Hpkg (push) Successful in 4m7s
Test / Sandbox (race detector) (push) Successful in 4m15s
Test / Hakurei (race detector) (push) Successful in 5m5s
Test / Flake checks (push) Successful in 1m32s
This change also spawns shim before committing system state, leaving it blocking on the setup pipe. The internal/outcome/process structure is also entirely reworked to be much more readable and less error-prone, while enabling basic performance measurements. A long-standing bug where segment lock is not held during Commit is also resolved.

Closes #19.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-02 04:25:45 +09:00
d3d3417125
internal/outcome/process: relocate start and serve
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m11s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m5s
Test / Hakurei (race detector) (push) Successful in 4m57s
Test / Flake checks (push) Successful in 1m30s
This is useful for reordering these operations for further cleanup.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-11-01 19:14:59 +09:00
651cdf9ccb
internal/outcome: remove guard on main
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 3m7s
Test / Sandbox (race detector) (push) Successful in 4m8s
Test / Hpkg (push) Successful in 4m9s
Test / Hakurei (race detector) (push) Successful in 4m54s
Test / Flake checks (push) Successful in 1m29s
This is no longer exported. Such a check is pointless.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-31 22:58:26 +09:00
68ff0a2ba6
container/params: expose pipe
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m16s
Test / Hakurei (push) Successful in 3m16s
Test / Hpkg (push) Successful in 4m11s
Test / Sandbox (race detector) (push) Successful in 4m13s
Test / Hakurei (race detector) (push) Successful in 5m3s
Test / Flake checks (push) Successful in 1m30s
This increases flexibility of how caller wants to handle the I/O. Also makes it no longer rely on finalizer.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-31 22:39:02 +09:00
6a0ecced90
internal/store: expose save via handle
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Sandbox (push) Successful in 42s
Test / Sandbox (race detector) (push) Successful in 42s
Test / Hakurei (push) Successful in 46s
Test / Hakurei (race detector) (push) Successful in 46s
Test / Hpkg (push) Successful in 42s
Test / Flake checks (push) Successful in 1m30s
The handle is otherwise inaccessible without the compat interface. This change also moves compatibility methods to separate adapter structs to avoid inadvertently using them.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-31 04:20:22 +09:00
b667fea1cb
internal/store: export new interface
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m13s
Test / Hpkg (push) Successful in 4m4s
Test / Sandbox (race detector) (push) Successful in 4m16s
Test / Hakurei (race detector) (push) Successful in 4m58s
Test / Flake checks (push) Successful in 1m30s
This exposes store operations safe for direct access, and enables #19 to be implemented in internal/outcome.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-31 03:41:26 +09:00
b25ade5f3d
internal/store: rename compat interface
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m17s
Test / Hakurei (push) Successful in 3m9s
Test / Sandbox (race detector) (push) Successful in 4m3s
Test / Hpkg (push) Successful in 4m4s
Test / Hakurei (race detector) (push) Successful in 4m54s
Test / Flake checks (push) Successful in 1m25s
The new store implementation will be exported as Store.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-30 18:53:59 +09:00
ebdcff1049
internal/store: rename from state
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m55s
Test / Flake checks (push) Successful in 1m25s
This reduces collision with local variable names, and generally makes sense for the new store package, since it no longer specifies the state struct.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-30 18:43:55 +09:00
46c5ce4936
internal/outcome/shim: check full behaviour
All checks were successful
Test / Create distribution (push) Successful in 24s
Test / Hakurei (push) Successful in 42s
Test / Sandbox (push) Successful in 38s
Test / Hakurei (race detector) (push) Successful in 42s
Test / Sandbox (race detector) (push) Successful in 38s
Test / Hpkg (push) Successful in 39s
Test / Flake checks (push) Successful in 1m21s
This took significant effort to stub out, and achieves full coverage after c5aefe5e9d.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-30 05:20:49 +09:00
36f8064905
internal/outcome/process: output via msg
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 3m57s
Test / Sandbox (race detector) (push) Successful in 4m8s
Test / Hakurei (race detector) (push) Successful in 4m54s
Test / Flake checks (push) Successful in 1m27s
This makes it possible to instrument output behaviour through stub.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-30 03:41:38 +09:00
eeb9f98e5b
internal/outcome/shim: move signal constants
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m17s
Test / Hpkg (push) Successful in 4m11s
Test / Sandbox (race detector) (push) Successful in 4m16s
Test / Hakurei (race detector) (push) Successful in 5m1s
Test / Flake checks (push) Successful in 1m30s
The magic numbers hurt readability.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-30 01:20:51 +09:00
3f9f331501
internal/outcome/shim: remove noop resume
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m11s
Test / Hpkg (push) Successful in 3m59s
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m54s
Test / Flake checks (push) Successful in 1m27s
The shim does not suspend output to begin with. These are leftovers from when container startup code suspends output.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-29 23:31:39 +09:00
2563391086
internal/outcome/shim: params check early
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m16s
Test / Hakurei (push) Successful in 3m18s
Test / Hpkg (push) Successful in 4m0s
Test / Sandbox (race detector) (push) Successful in 4m11s
Test / Hakurei (race detector) (push) Successful in 4m56s
Test / Flake checks (push) Successful in 1m29s
This is unreachable, but keeping it here as a failsafe until more test cases are added.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-29 23:10:12 +09:00
a0b4e47acc
internal/outcome: rename from app
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m11s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m55s
Test / Flake checks (push) Successful in 1m27s
This is less ambiguous, and more accurately describes the purpose of the package.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-29 04:33:13 +09:00
a52f7038e5
internal/env: relocate from app
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m8s
Test / Hakurei (push) Successful in 3m10s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m53s
Test / Flake checks (push) Successful in 1m27s
This package is much cleaner to stub independently, and makes no sense to lump into app.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-29 04:11:49 +09:00
274686d10d
internal/validate: relocate from app
All checks were successful
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m23s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 4m7s
Test / Sandbox (race detector) (push) Successful in 4m11s
Test / Hakurei (race detector) (push) Successful in 5m1s
Test / Flake checks (push) Successful in 1m30s
These are free of the dispatcher from internal/app. This change relocates them into their own package.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-29 03:40:09 +09:00
65342d588f
internal/app/state: improve store internals
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m6s
Test / Hakurei (race detector) (push) Successful in 4m50s
Test / Flake checks (push) Successful in 1m27s
This fully exposes the store internals for #19 and are final preparations for removing the legacy store interface.

This change also fixes a potential deadlock in the handle initialisation mkdir failure path. This however is never reachable in hakurei as the store is never accessed concurrently.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-29 03:21:00 +09:00
5e5826459e
internal/app/state: improve handles internals
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m8s
Test / Hakurei (push) Successful in 3m10s
Test / Hpkg (push) Successful in 3m56s
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m53s
Test / Flake checks (push) Successful in 1m31s
This replaces the Store interface with something better reflecting the underlying data format for #19. An implementation of Store is provided on top of the new code to ease transition.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-28 22:00:54 +09:00
4a463b7f03
internal/app/state: use absolute pathnames
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 3m56s
Test / Sandbox (race detector) (push) Successful in 4m6s
Test / Hakurei (race detector) (push) Successful in 4m48s
Test / Flake checks (push) Successful in 1m26s
This is less error-prone and fits better into internal/app which already uses check.Absolute for all pathnames.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-26 03:41:19 +09:00
dacd9550e0
internal/app/state: acquire big lock for toplevel operations
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 3m5s
Test / Hpkg (push) Successful in 3m54s
Test / Sandbox (race detector) (push) Successful in 4m3s
Test / Hakurei (race detector) (push) Successful in 4m50s
Test / Flake checks (push) Successful in 1m24s
This avoids getting into an inconsistent state for simultaneous calls to List and Do on a previously unknown identity.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-26 03:27:56 +09:00
546b00429f
treewide: update doc comments
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m22s
Test / Hakurei (push) Successful in 3m10s
Test / Hpkg (push) Successful in 3m58s
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m57s
Test / Flake checks (push) Successful in 1m29s
Some internal/app/state types were relocated to hst as part of the API. This change updates doc comments referring to them.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-26 03:00:04 +09:00
86f4219062
internal/app/state/data: check full entry behaviour
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m5s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m13s
Test / Hakurei (race detector) (push) Successful in 4m55s
Test / Flake checks (push) Successful in 1m29s
This eventually gets relocated to internal/app.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-26 01:49:14 +09:00
fe2929d5f7
internal/app/state: include et header
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m17s
Test / Hakurei (push) Successful in 3m5s
Test / Hpkg (push) Successful in 3m55s
Test / Sandbox (race detector) (push) Successful in 4m2s
Test / Hakurei (race detector) (push) Successful in 4m49s
Test / Flake checks (push) Successful in 1m22s
This is the initial step of implementing #19.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-25 22:01:26 +09:00
470e545d27
internal/app/state: use internal/lockedfile
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m11s
Test / Hpkg (push) Successful in 4m0s
Test / Sandbox (race detector) (push) Successful in 4m4s
Test / Hakurei (race detector) (push) Successful in 4m52s
Test / Flake checks (push) Successful in 1m30s
This is a pretty solid implementation backed by robust tests, with a much cleaner interface.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-25 21:29:24 +09:00
8d3381821f
internal/app/state: export correct backend value
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m17s
Test / Hakurei (push) Successful in 3m7s
Test / Sandbox (race detector) (push) Successful in 3m52s
Test / Hpkg (push) Successful in 3m59s
Test / Hakurei (race detector) (push) Successful in 4m46s
Test / Flake checks (push) Successful in 1m25s
This references the underlying multiBackend due to a typo, making the whole dance with c a noop.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-25 21:11:05 +09:00
e9d00b9071
container/executable: handle nil msg
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m4s
Test / Sandbox (race detector) (push) Successful in 3m59s
Test / Hpkg (push) Successful in 4m2s
Test / Hakurei (race detector) (push) Successful in 4m45s
Test / Flake checks (push) Successful in 1m37s
This is useful in some tests.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-25 21:08:54 +09:00
4f41afee0f
internal/app/state: fixed size et-only header
All checks were successful
Test / Create distribution (push) Successful in 46s
Test / Sandbox (push) Successful in 2m29s
Test / Hakurei (push) Successful in 3m26s
Test / Sandbox (race detector) (push) Successful in 4m15s
Test / Hpkg (push) Successful in 4m14s
Test / Hakurei (race detector) (push) Successful in 5m3s
Test / Flake checks (push) Successful in 1m21s
This header improves the robustness of the format and significantly reduces cleanup overhead.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-25 19:15:06 +09:00
7de593e816
cmd/hakurei: short identifier from lower half
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 40s
Test / Hakurei (push) Successful in 2m14s
Test / Hakurei (race detector) (push) Successful in 2m57s
Test / Hpkg (push) Successful in 3m12s
Test / Flake checks (push) Successful in 1m25s
The upper half is now a nanosecond timestamp. Lower half is still random bytes, so use lower half for short identifier.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-24 00:47:39 +09:00
2442eda8d9
hst/instance: embed config struct
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 41s
Test / Sandbox (race detector) (push) Successful in 40s
Test / Hakurei (push) Successful in 2m20s
Test / Hakurei (race detector) (push) Successful in 2m59s
Test / Hpkg (push) Successful in 3m20s
Test / Flake checks (push) Successful in 1m28s
This makes the resulting json easier to parse since it can now be deserialised into the config struct.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-24 00:42:16 +09:00
05488bfb8f
hst/instance: store priv side pid
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m8s
Test / Sandbox (race detector) (push) Successful in 3m58s
Test / Hpkg (push) Successful in 4m1s
Test / Hakurei (race detector) (push) Successful in 4m44s
Test / Flake checks (push) Successful in 1m29s
This can receive signals, so is more useful to the caller.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-23 23:19:55 +09:00
dd94818f20
hst/instance: define instance state
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m6s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m5s
Test / Hakurei (race detector) (push) Successful in 4m51s
Test / Flake checks (push) Successful in 1m30s
This is now part of the hst API. This change also improves identifier generation and serialisation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-23 22:59:02 +09:00
0fd357e7f6
container/init: do not suspend output
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Hakurei (push) Successful in 42s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Hpkg (push) Successful in 41s
Test / Flake checks (push) Successful in 1m20s
Init is not very talkative after process start even when verbose. Suspending output here is pointless and does more harm than good.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-23 08:11:00 +09:00
57231d4acf
container/init: improve signal handling
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m9s
Test / Sandbox (race detector) (push) Successful in 3m57s
Test / Hpkg (push) Successful in 3m58s
Test / Hakurei (race detector) (push) Successful in 4m43s
Test / Flake checks (push) Successful in 1m30s
The SIGTERM signal is delivered in many other cases and can lead to strange behaviour. The unconditional resume of the logger also causes strange behaviour in the cancellation forwarding path. This change also passes through additional signals.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-23 08:02:03 +09:00
c5aefe5e9d
internal/app/shim: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Hakurei (push) Successful in 44s
Test / Hpkg (push) Successful in 41s
Test / Flake checks (push) Successful in 1m20s
This does not yet have full coverage. Test cases covering failsafe paths and error injection will be added eventually.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-23 06:07:41 +09:00
0f8ffee44d
internal/app: test case for hst template
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m8s
Test / Hakurei (push) Successful in 3m5s
Test / Hpkg (push) Successful in 3m58s
Test / Sandbox (race detector) (push) Successful in 4m2s
Test / Hakurei (race detector) (push) Successful in 4m42s
Test / Flake checks (push) Successful in 1m21s
This helps with other areas of the test suite as they're all based on hst.Template. This also helps contributors understand the behaviour of internal/app as hst.Template covers almost every aspect of it.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-23 04:46:58 +09:00
1685a4d000
cmd/hsu: reduce excessive test range
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m40s
Test / Sandbox (race detector) (push) Successful in 2m25s
Test / Hakurei (push) Successful in 2m36s
Test / Hakurei (race detector) (push) Successful in 3m13s
Test / Hpkg (push) Successful in 3m33s
Test / Flake checks (push) Successful in 1m24s
This is quite a simple piece of code, this many test cases is excessive and wastes time in the integration vm.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-23 04:32:32 +09:00
6c338b433a
internal/app: reduce test case indentation
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m9s
Test / Sandbox (race detector) (push) Successful in 4m3s
Test / Hpkg (push) Successful in 4m4s
Test / Hakurei (race detector) (push) Successful in 4m44s
Test / Flake checks (push) Successful in 1m28s
This improves readability on narrower displays.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-22 07:40:32 +09:00
8accd3b219
internal/app/shim: use syscall dispatcher
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m9s
Test / Sandbox (race detector) (push) Successful in 3m58s
Test / Hpkg (push) Successful in 4m5s
Test / Hakurei (race detector) (push) Successful in 4m46s
Test / Flake checks (push) Successful in 1m28s
This enables instrumented testing of the shim.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-22 06:58:45 +09:00
c5f59c5488
container/syscall: export prctl wrapper
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m3s
Test / Sandbox (race detector) (push) Successful in 3m58s
Test / Hpkg (push) Successful in 4m4s
Test / Hakurei (race detector) (push) Successful in 4m46s
Test / Flake checks (push) Successful in 1m27s
This is useful as package "syscall" does not provide such a wrapper. This change also improves error handling to fully conform to the manpage.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-22 05:26:54 +09:00
fcd9becf9a
cmd/hsu: run in locked thread
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m41s
Test / Sandbox (race detector) (push) Successful in 2m23s
Test / Hakurei (push) Successful in 2m35s
Test / Hakurei (race detector) (push) Successful in 3m14s
Test / Hpkg (push) Successful in 3m39s
Test / Flake checks (push) Successful in 1m27s
Goroutine scheduling is not helpful in the setuid wrapper, it is not particularly harmful but lock here anyway.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-22 05:09:08 +09:00
622f945c22
container/init: check msg in entrypoint
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m10s
Test / Sandbox (race detector) (push) Successful in 3m59s
Test / Hpkg (push) Successful in 4m8s
Test / Hakurei (race detector) (push) Successful in 4m46s
Test / Flake checks (push) Successful in 1m27s
This covers invalid call to Init.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-22 04:20:08 +09:00
e94acc424c
container/comp: rename from bits
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 3m53s
Test / Sandbox (race detector) (push) Successful in 4m2s
Test / Hakurei (race detector) (push) Successful in 4m43s
Test / Flake checks (push) Successful in 1m23s
This package will also hold syscall lookup tables for seccomp.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-21 20:54:03 +09:00
b1a4d801be
hst/container: flags string representation
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m9s
Test / Sandbox (race detector) (push) Successful in 3m56s
Test / Hpkg (push) Successful in 4m5s
Test / Hakurei (race detector) (push) Successful in 4m42s
Test / Hakurei (push) Successful in 2m9s
Test / Flake checks (push) Successful in 1m28s
This is useful for a user-facing representation other than JSON. This also gets rid of the ugly, outdated flags string builder in cmd/hakurei.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-21 20:29:52 +09:00
56beae17fe
test: assert hst CGO_ENABLED=0
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 40s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Hpkg (push) Successful in 41s
Test / Hakurei (push) Successful in 2m29s
Test / Hakurei (race detector) (push) Successful in 3m7s
Test / Flake checks (push) Successful in 1m24s
The hst package only deals with data serialisation, however since many parts of hakurei make use of C libraries in some way it can be easy to inadvertently depend on cgo.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-21 19:49:04 +09:00
ea978101b1
cmd/hakurei/parse: close config fd
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m5s
Test / Sandbox (race detector) (push) Successful in 3m54s
Test / Hpkg (push) Successful in 3m57s
Test / Hakurei (race detector) (push) Successful in 4m43s
Test / Flake checks (push) Successful in 1m20s
This is cleaner than relying on the finalizer.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-21 06:05:36 +09:00
fbd1638e7f
test/interactive/trace: update nix attribute
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 40s
Test / Sandbox (race detector) (push) Successful in 40s
Test / Hakurei (race detector) (push) Successful in 44s
Test / Hakurei (push) Successful in 45s
Test / Hpkg (push) Successful in 42s
Test / Flake checks (push) Successful in 1m28s
Updated according to evaluation warning.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-21 06:03:09 +09:00
d42067df7c
cmd/hakurei/json: friendly error messages
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Hakurei (push) Successful in 44s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Hpkg (push) Successful in 41s
Test / Flake checks (push) Successful in 1m23s
This change handles errors returned by encoding/json and prints significantly cleaner messages.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-21 05:17:25 +09:00
b9459a80c7
container/init: check use constants for open flags
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m11s
Test / Hakurei (push) Successful in 3m8s
Test / Sandbox (race detector) (push) Successful in 3m58s
Test / Hpkg (push) Successful in 4m6s
Test / Hakurei (race detector) (push) Successful in 4m45s
Test / Flake checks (push) Successful in 1m28s
These bits are arch-specific.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-21 03:13:58 +09:00
f8189d1488
container/syscall: dot-import syscall
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m7s
Test / Hpkg (push) Successful in 3m57s
Test / Sandbox (race detector) (push) Successful in 4m2s
Test / Hakurei (race detector) (push) Successful in 4m44s
Test / Flake checks (push) Successful in 1m38s
This avoids having arch-specific constants for arm64.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-21 03:09:14 +09:00
5063b774c1
hst: expose version string
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m6s
Test / Hakurei (push) Successful in 3m0s
Test / Hpkg (push) Successful in 3m56s
Test / Sandbox (race detector) (push) Successful in 4m0s
Test / Hakurei (race detector) (push) Successful in 4m44s
Test / Flake checks (push) Successful in 1m20s
The hst API is tied to this version string.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-21 01:56:44 +09:00
766dd89ffa
internal: clean up build strings
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m5s
Test / Hpkg (push) Successful in 4m4s
Test / Sandbox (race detector) (push) Successful in 4m9s
Test / Hakurei (race detector) (push) Successful in 4m46s
Test / Flake checks (push) Successful in 1m30s
These names are less ambiguous and should be understandable without reading the source code.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-21 01:49:36 +09:00
699c19e972
hst/container: optional runtime and tmpdir sharing
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Hakurei (push) Successful in 42s
Test / Hpkg (push) Successful in 40s
Test / Hakurei (race detector) (push) Successful in 44s
Test / Flake checks (push) Successful in 1m23s
Sharing and persisting these directories do not always make sense. Make it optional here.

Closes #16.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-19 04:11:38 +09:00
b5b30aea2e
test: place marker in common path
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Sandbox (push) Successful in 41s
Test / Hakurei (race detector) (push) Successful in 45s
Test / Hpkg (push) Successful in 42s
Test / Hakurei (push) Successful in 46s
Test / Flake checks (push) Successful in 1m33s
This discontinues the dependency on shared tmpdir and xdg_runtime_dir implementation detail, for #16.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-19 03:50:48 +09:00
c0e860000a
internal/app: remove spfinal
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 1m39s
Test / Sandbox (race detector) (push) Successful in 4m3s
Test / Hpkg (push) Successful in 4m12s
Test / Hakurei (race detector) (push) Successful in 4m10s
Test / Hakurei (push) Successful in 4m9s
Test / Flake checks (push) Successful in 1m36s
This no longer needs to be an independent outcomeOp since spFilesystemOp is moved late.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-19 02:58:46 +09:00
d87020f0ca
hst/config: validate env early
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m8s
Test / Sandbox (race detector) (push) Successful in 3m58s
Test / Hpkg (push) Successful in 4m3s
Test / Hakurei (race detector) (push) Successful in 4m44s
Test / Flake checks (push) Successful in 1m26s
This should happen in hst since it requires no system state.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-19 02:39:49 +09:00
e47aebb7a0
internal/app/outcome: apply configured filesystems late
All checks were successful
Test / Create distribution (push) Successful in 27s
Test / Sandbox (push) Successful in 1m42s
Test / Hakurei (push) Successful in 2m37s
Test / Hpkg (push) Successful in 3m33s
Test / Sandbox (race detector) (push) Successful in 4m10s
Test / Hakurei (race detector) (push) Successful in 4m49s
Test / Flake checks (push) Successful in 1m29s
This enables configured filesystems to cover system mount points.

Closes #8.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-19 01:41:52 +09:00
543bf69102
internal/app/spx11: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m16s
Test / Hakurei (push) Successful in 3m7s
Test / Sandbox (race detector) (push) Successful in 4m0s
Test / Hpkg (push) Successful in 3m59s
Test / Hakurei (race detector) (push) Successful in 4m47s
Test / Flake checks (push) Successful in 1m29s
This outcomeOp will likely never change.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-19 01:00:12 +09:00
4cfb1fda8f
internal/app/spwayland: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m5s
Test / Hpkg (push) Successful in 3m57s
Test / Sandbox (race detector) (push) Successful in 4m3s
Test / Hakurei (race detector) (push) Successful in 4m45s
Test / Flake checks (push) Successful in 1m28s
This op is quite clean. Might get slightly more complex at some point passing socket fd.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-19 00:30:56 +09:00
c12183959a
internal/app/dispatcher: report correct field
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m5s
Test / Sandbox (race detector) (push) Successful in 3m59s
Test / Hpkg (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m51s
Test / Flake checks (push) Successful in 1m30s
This was mistakenly reporting sharePath on inequivalence causing very confusing output.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 23:59:10 +09:00
f5845e312e
internal/app/sptmpdir: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m6s
Test / Sandbox (race detector) (push) Successful in 3m58s
Test / Hpkg (push) Successful in 3m59s
Test / Hakurei (race detector) (push) Successful in 4m43s
Test / Flake checks (push) Successful in 1m27s
Another simple one. This will change when shared tmpdir and xdg runtime dir becomes optional.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 23:46:10 +09:00
a103c4a7c7
internal/app/hsu: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m4s
Test / Hpkg (push) Successful in 4m0s
Test / Sandbox (race detector) (push) Successful in 4m3s
Test / Hakurei (race detector) (push) Successful in 4m44s
Test / Flake checks (push) Successful in 1m22s
The stub exec.ExitError is hairy as usual, but internal/app is not cross-platform, so this is okay.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 20:45:42 +09:00
67ec82ae1b
ldd/exec: raise timeout
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m4s
Test / Sandbox (race detector) (push) Successful in 3m58s
Test / Hpkg (push) Successful in 3m58s
Test / Hakurei (race detector) (push) Successful in 6m9s
Test / Flake checks (push) Successful in 1m28s
This mostly helps with tests.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 18:03:09 +09:00
f6f0cb56ae
internal/app/hsu: remove wrapper method
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m11s
Test / Sandbox (race detector) (push) Successful in 3m53s
Test / Hpkg (push) Successful in 3m54s
Test / Hakurei (race detector) (push) Successful in 4m43s
Test / Hakurei (push) Successful in 2m13s
Test / Flake checks (push) Successful in 1m27s
This was added to reduce the size of diffs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 17:35:20 +09:00
d4284c109d
internal/app/spruntime: emulate pam_systemd type
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Hakurei (push) Successful in 44s
Test / Hakurei (race detector) (push) Successful in 44s
Test / Hpkg (push) Successful in 42s
Test / Sandbox (push) Successful in 1m42s
Test / Sandbox (race detector) (push) Successful in 2m29s
Test / Flake checks (push) Successful in 1m22s
This sets XDG_SESSION_TYPE to the corresponding values specified in pam_systemd(8) according to enablements.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 04:33:04 +09:00
030ad2a73b
internal/app/spruntime: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 38s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 3m9s
Test / Sandbox (race detector) (push) Successful in 4m2s
Test / Hpkg (push) Successful in 4m11s
Test / Hakurei (race detector) (push) Successful in 4m48s
Test / Flake checks (push) Successful in 1m25s
This one is quite simple and has no state. Needs to emulate pam_systemd behaviour so that will change.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 03:41:49 +09:00
78d7955abd
internal/app/sppulse: check cookie discovery
All checks were successful
Test / Create distribution (push) Successful in 48s
Test / Sandbox (push) Successful in 2m22s
Test / Hakurei (push) Successful in 3m17s
Test / Sandbox (race detector) (push) Successful in 4m13s
Test / Hpkg (push) Successful in 4m18s
Test / Hakurei (race detector) (push) Successful in 5m0s
Test / Flake checks (push) Successful in 1m37s
There's quite a bit of code duplication here, but since this is already quite simple it is best to leave it as is for now.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 01:30:33 +09:00
b066495a7d
internal/app/sppulse: check buf error injection
All checks were successful
Test / Create distribution (push) Successful in 54s
Test / Hpkg (push) Successful in 4m16s
Test / Sandbox (push) Successful in 1m45s
Test / Hakurei (push) Successful in 2m27s
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m53s
Test / Flake checks (push) Successful in 1m38s
The loadFile behaviour does not guarantee the buffer to be zeroed or not clobbered if an error is returned, but for the current implementation it is good to check.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 01:01:52 +09:00
82299d34c6
internal/app/sppulse: correctly handle small cookie
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m6s
Test / Sandbox (race detector) (push) Successful in 3m55s
Test / Hpkg (push) Successful in 4m8s
Test / Hakurei (race detector) (push) Successful in 4m46s
Test / Flake checks (push) Successful in 1m19s
The trailing zero bytes need to be sliced off, so send cookie size alongside buffer content.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-17 08:03:03 +09:00
792013cefb
internal/app/sppulse: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m5s
Test / Sandbox (race detector) (push) Successful in 3m58s
Test / Hpkg (push) Successful in 4m9s
Test / Hakurei (race detector) (push) Successful in 4m42s
Test / Flake checks (push) Successful in 1m27s
Still needs to check the relocated functions separately.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-17 06:32:21 +09:00
3f39132935
internal/app/dispatcher: reduce check code duplication
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m6s
Test / Hakurei (push) Successful in 3m3s
Test / Sandbox (race detector) (push) Successful in 3m56s
Test / Hpkg (push) Successful in 3m58s
Test / Hakurei (race detector) (push) Successful in 4m42s
Test / Flake checks (push) Successful in 1m28s
This also improves readability of test cases.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-17 05:47:12 +09:00
c922c3f80e
internal/app/sppulse: relocate hard to test code
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m1s
Test / Sandbox (race detector) (push) Successful in 3m59s
Test / Hpkg (push) Successful in 4m8s
Test / Hakurei (race detector) (push) Successful in 4m48s
Test / Flake checks (push) Successful in 1m19s
These are better tested separately instead of creating many op test cases.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-16 05:47:49 +09:00
6cf58ca1b3
internal/app/spfinal: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m2s
Test / Hpkg (push) Successful in 3m56s
Test / Sandbox (race detector) (push) Successful in 4m1s
Test / Hakurei (race detector) (push) Successful in 4m45s
Test / Flake checks (push) Successful in 1m25s
This will be merged with spFilesystemOp eventually.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-16 02:08:31 +09:00
425421d9b1
hst/container: rename constants
All checks were successful
Test / Create distribution (push) Successful in 1m16s
Test / Sandbox (push) Successful in 3m4s
Test / Hakurei (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m50s
Test / Hpkg (push) Successful in 5m4s
Test / Hakurei (race detector) (push) Successful in 5m38s
Test / Flake checks (push) Successful in 1m30s
The shim is an implementation detail and should not be mentioned in the API.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-16 00:27:00 +09:00
5e0f15d76b
hst/container: additional shim exit codes
All checks were successful
Test / Create distribution (push) Successful in 57s
Test / Sandbox (push) Successful in 4m26s
Test / Sandbox (race detector) (push) Successful in 6m36s
Test / Hakurei (push) Successful in 6m58s
Test / Hakurei (race detector) (push) Successful in 8m54s
Test / Hpkg (push) Successful in 9m13s
Test / Flake checks (push) Successful in 3m13s
These are now considered stable, defined behaviour and can be used by external programs to determine shim outcome.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-15 22:09:33 +09:00
ae65491223
container/init: use one channel for wait4
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 3m12s
Test / Hpkg (push) Successful in 4m3s
Test / Sandbox (race detector) (push) Successful in 4m6s
Test / Hakurei (race detector) (push) Successful in 4m51s
Test / Flake checks (push) Successful in 1m31s
When using two channels it is possible for the other case to be reached before all pending winfo are consumed, causing incorrect reporting.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-15 21:35:19 +09:00
52e3324ef4
test/sandbox: ignore nondeterministic mount point
All checks were successful
Test / Create distribution (push) Successful in 27s
Test / Sandbox (race detector) (push) Successful in 42s
Test / Sandbox (push) Successful in 43s
Test / Hakurei (race detector) (push) Successful in 46s
Test / Hpkg (push) Successful in 43s
Test / Hakurei (push) Successful in 47s
Test / Flake checks (push) Successful in 1m30s
No idea what systemd is doing with this to cause its options to change.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-14 07:08:39 +09:00
f95e0a7568
hst/config: hold acl struct by value
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (race detector) (push) Successful in 4m6s
Test / Hpkg (push) Successful in 4m12s
Test / Hakurei (race detector) (push) Successful in 4m46s
Test / Sandbox (push) Successful in 1m22s
Test / Hakurei (push) Successful in 2m18s
Test / Flake checks (push) Successful in 1m37s
Doc comments are also reworded for clarity.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-14 07:02:14 +09:00
4c647add0d
hst/container: pack boolean options
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 4m2s
Test / Hakurei (race detector) (push) Successful in 4m46s
Test / Sandbox (race detector) (push) Successful in 2m11s
Test / Flake checks (push) Successful in 1m37s
The memory saving is relatively insignificant, however this increases serialisation efficiency.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-14 06:39:00 +09:00
a341466942
hst: separate container config
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m11s
Test / Hakurei (push) Successful in 3m7s
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hpkg (push) Successful in 4m9s
Test / Hakurei (race detector) (push) Successful in 4m47s
Test / Flake checks (push) Successful in 1m31s
The booleans are getting packed into a single field. This requires non-insignificant amount of code for JSON serialisation to stay compatible.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-14 04:23:05 +09:00
e4ee8df83c
internal/app/spdbus: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 37s
Test / Sandbox (push) Successful in 2m16s
Test / Sandbox (race detector) (push) Successful in 4m2s
Test / Hpkg (push) Successful in 4m13s
Test / Hakurei (race detector) (push) Successful in 4m47s
Test / Hakurei (push) Successful in 2m11s
Test / Flake checks (push) Successful in 1m30s
This is not done very cleanly, however this op is pending removal for the in-process dbus proxy so not worth spending too much effort here. As long as it checks all paths it is good enough.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-14 01:51:01 +09:00
048c1957f1
helper/args: variadic check function
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 1m30s
Test / Hakurei (push) Successful in 2m21s
Test / Hpkg (push) Successful in 3m23s
Test / Sandbox (race detector) (push) Successful in 4m1s
Test / Hakurei (race detector) (push) Successful in 4m46s
Test / Flake checks (push) Successful in 1m27s
This package turns out to be much less widely used than anticipated, and might be facing removal. This change makes test cases cleaner.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-14 01:48:56 +09:00
790d77075e
system/dbus: remove builder state leak
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (race detector) (push) Successful in 3m56s
Test / Hpkg (push) Successful in 4m2s
Test / Hakurei (race detector) (push) Successful in 4m44s
Test / Sandbox (push) Successful in 1m23s
Test / Hakurei (push) Successful in 2m14s
Test / Flake checks (push) Successful in 1m26s
This enables external testing of system.I state.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-14 01:33:44 +09:00
e5ff40e7d3
container: synchronise after notify
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m18s
Test / Hakurei (push) Successful in 3m7s
Test / Sandbox (race detector) (push) Successful in 3m59s
Test / Hpkg (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m45s
Test / Flake checks (push) Successful in 1m23s
This should eliminate intermittent failures in the forward test.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-13 19:17:19 +09:00
123d7fbfd5
container/seccomp: remove export pipe
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m11s
Test / Sandbox (race detector) (push) Successful in 4m2s
Test / Hpkg (push) Successful in 4m19s
Test / Hakurei (race detector) (push) Successful in 4m47s
Test / Hakurei (push) Successful in 2m13s
Test / Flake checks (push) Successful in 1m32s
This was only useful when wrapping bwrap.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-13 18:51:35 +09:00
7638a44fa6
treewide: parallel tests
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Hakurei (push) Successful in 44s
Test / Sandbox (push) Successful in 41s
Test / Hakurei (race detector) (push) Successful in 44s
Test / Sandbox (race detector) (push) Successful in 41s
Test / Hpkg (push) Successful in 41s
Test / Flake checks (push) Successful in 1m24s
Most tests already had no global state, however parallel was never enabled. This change enables it for all applicable tests.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-13 04:38:48 +09:00
a14b6535a6
helper/stub: write ready byte late
All checks were successful
Test / Create distribution (push) Successful in 27s
Test / Sandbox (race detector) (push) Successful in 41s
Test / Sandbox (push) Successful in 41s
Test / Hakurei (push) Successful in 44s
Test / Hakurei (race detector) (push) Successful in 44s
Test / Hpkg (push) Successful in 42s
Test / Flake checks (push) Successful in 1m30s
Hopefully eliminates spurious failures.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-13 01:55:44 +09:00
763ab27e09
system: remove tmpfiles
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m33s
Test / Hakurei (race detector) (push) Successful in 5m21s
Test / Flake checks (push) Successful in 1m32s
This is no longer used.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-13 01:12:44 +09:00
bff2a1e748
container/initplace: remove indirect method
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m30s
Test / Sandbox (push) Successful in 1m24s
Test / Hakurei (race detector) (push) Successful in 5m20s
Test / Hakurei (push) Successful in 2m13s
Test / Flake checks (push) Successful in 1m29s
This is no longer useful and is highly error-prone.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-13 01:06:45 +09:00
8a91234cb4
hst: reword and improve doc comments
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m9s
Test / Hpkg (push) Successful in 3m58s
Test / Sandbox (race detector) (push) Successful in 4m31s
Test / Hakurei (race detector) (push) Successful in 5m19s
Test / Hakurei (push) Successful in 2m12s
Test / Flake checks (push) Successful in 1m31s
This corrects minor mistakes in doc comments and adds them for undocumented constants.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-12 05:03:14 +09:00
db7051a368
internal/app/spcontainer: check fs init behaviour
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 3m53s
Test / Sandbox (race detector) (push) Successful in 4m34s
Test / Sandbox (push) Successful in 1m21s
Test / Hakurei (race detector) (push) Successful in 5m22s
Test / Flake checks (push) Successful in 1m34s
This covers every statement. Some of them are unreachable unless the kernel returns garbage.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-12 03:58:53 +09:00
36f312b3ba
internal/app/spcontainer: resolve path through dispatcher
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m13s
Test / Hpkg (push) Successful in 4m2s
Test / Hakurei (race detector) (push) Successful in 5m23s
Test / Hakurei (push) Successful in 2m14s
Test / Sandbox (race detector) (push) Successful in 2m7s
Test / Flake checks (push) Successful in 1m32s
This prevents state from os tainting the test data.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-11 20:20:41 +09:00
037144b06e
system/dbus: use well-known address in spec
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m13s
Test / Hpkg (push) Successful in 4m8s
Test / Hakurei (race detector) (push) Successful in 5m26s
Test / Hakurei (push) Successful in 2m14s
Test / Sandbox (race detector) (push) Successful in 2m4s
Test / Flake checks (push) Successful in 1m32s
The session bus still performs non-standard formatting since it makes no sense for hakurei to start the session bus.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-11 18:52:06 +09:00
f5a597c406
hst: rename /.hakurei constant
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m3s
Test / Hpkg (push) Successful in 3m57s
Test / Sandbox (race detector) (push) Successful in 4m30s
Test / Hakurei (race detector) (push) Successful in 5m16s
Test / Flake checks (push) Successful in 1m20s
This provides disambiguation from fhs.AbsTmp.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-11 14:32:35 +09:00
8874aaf81b
hst: remove template bind nix store
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 3m5s
Test / Hpkg (push) Successful in 3m59s
Test / Sandbox (race detector) (push) Successful in 4m35s
Test / Hakurei (race detector) (push) Successful in 5m25s
Test / Flake checks (push) Successful in 1m28s
This does not add anything meaningful to the template, since there are already prior examples showing src-only bind ops. Remove this since it causes confusion by covering the previous mount point targeting /nix/store.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-11 13:59:10 +09:00
04a27c8e47
hst: use plausible overlay template
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m11s
Test / Hakurei (push) Successful in 3m6s
Test / Hpkg (push) Successful in 3m57s
Test / Hakurei (race detector) (push) Successful in 5m19s
Test / Sandbox (race detector) (push) Successful in 2m7s
Test / Flake checks (push) Successful in 1m39s
The current value is copied from a test case, and does not resemble its intended use case.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-11 13:51:08 +09:00
9e3df0905b
internal/app/spcontainer: check params init behaviour
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Hakurei (push) Successful in 3m6s
Test / Hpkg (push) Successful in 4m4s
Test / Sandbox (push) Successful in 1m21s
Test / Hakurei (race detector) (push) Successful in 5m23s
Test / Sandbox (race detector) (push) Successful in 2m8s
Test / Flake checks (push) Successful in 1m31s
This change also significantly reduces duplicate information in test case.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-11 02:44:02 +09:00
9290748761
internal/app/spaccount: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Hakurei (push) Successful in 3m7s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m27s
Test / Hakurei (race detector) (push) Successful in 5m19s
Test / Sandbox (push) Successful in 1m18s
Test / Flake checks (push) Successful in 1m30s
This begins the effort of fully covering internal/app.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-11 00:54:04 +09:00
23084888a0
internal/app/spaccount: apply default in shim
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m19s
Test / Hpkg (push) Successful in 4m6s
Test / Hakurei (race detector) (push) Successful in 5m20s
Test / Sandbox (race detector) (push) Successful in 2m10s
Test / Hakurei (push) Successful in 2m13s
Test / Flake checks (push) Successful in 1m37s
The original code clobbers hst.Config, and was not changed when being ported over.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-11 00:38:06 +09:00
50f6fcb326
container/stub: mark test overrides as helper
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m35s
Test / Sandbox (push) Successful in 1m24s
Test / Hakurei (race detector) (push) Successful in 5m23s
Test / Hakurei (push) Successful in 2m16s
Test / Flake checks (push) Successful in 1m21s
This fixes line information in test reporting messages.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-10 22:15:20 +09:00
070e346587
internal/app: relocate params state initialisation
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m5s
Test / Hpkg (push) Successful in 4m9s
Test / Hakurei (race detector) (push) Successful in 5m18s
Test / Sandbox (race detector) (push) Successful in 2m9s
Test / Flake checks (push) Successful in 1m40s
This is useful for testing.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-10 22:00:49 +09:00
24de7c50a0
internal/app: relocate state initialisation
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m4s
Test / Hpkg (push) Successful in 4m4s
Test / Sandbox (race detector) (push) Successful in 4m37s
Test / Hakurei (race detector) (push) Successful in 5m18s
Test / Flake checks (push) Successful in 1m28s
This is useful for testing.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-10 20:15:58 +09:00
f6dd9dab6a
internal/app: hold path hiding in op
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 4m12s
Test / Sandbox (race detector) (push) Successful in 4m37s
Test / Hakurei (race detector) (push) Successful in 5m21s
Test / Flake checks (push) Successful in 1m34s
This makes no sense to be part of the global state.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-10 19:56:30 +09:00
776650af01
hst/config: negative WaitDelay bypasses default
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m19s
Test / Hpkg (push) Successful in 4m4s
Test / Sandbox (race detector) (push) Successful in 4m44s
Test / Hakurei (race detector) (push) Successful in 5m25s
Test / Hakurei (push) Successful in 2m16s
Test / Flake checks (push) Successful in 1m30s
This behaviour might be useful, so do not lock it out. This change also fixes an oversight where the unchecked value is used to determine ForwardCancel.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-10 05:11:32 +09:00
109aaee659
internal/app: copy parts of config to state
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m7s
Test / Hpkg (push) Successful in 4m4s
Test / Sandbox (race detector) (push) Successful in 4m30s
Test / Hakurei (race detector) (push) Successful in 5m20s
Test / Flake checks (push) Successful in 1m34s
This is less error-prone than passing the address to the entire hst.Config struct, and reduces the likelihood of accidentally clobbering hst.Config. This also improves ease of testing.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-10 03:19:09 +09:00
22ee5ae151
internal/app: filter ops in implementation
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m18s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m28s
Test / Hakurei (race detector) (push) Successful in 5m19s
Test / Hakurei (push) Successful in 2m14s
Test / Flake checks (push) Successful in 1m33s
This is cleaner and less error-prone, and should also result in negligibly less memory allocation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-10 02:23:34 +09:00
4246256d78
internal/app: hold config address in state
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m6s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m32s
Test / Hakurei (race detector) (push) Successful in 5m22s
Test / Flake checks (push) Successful in 1m34s
This can be removed eventually as it is barely used.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-10 01:21:01 +09:00
a941ac025f
container/init: unwrap descriptive fatal error
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m6s
Test / Hpkg (push) Successful in 4m0s
Test / Hakurei (race detector) (push) Successful in 5m20s
Test / Sandbox (race detector) (push) Successful in 2m3s
Test / Flake checks (push) Successful in 1m27s
These errors are printed with a descriptive message prefixed to them, so it is more readable to expose the underlying errno.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-09 22:04:35 +09:00
87b5c30ef6
message: relocate from container
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m22s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m28s
Test / Hakurei (race detector) (push) Successful in 5m21s
Test / Hakurei (push) Successful in 2m9s
Test / Flake checks (push) Successful in 1m29s
This package is quite useful. This change allows it to be imported without importing container.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-09 05:18:19 +09:00
df9b77b077
internal/app: do not encode config early
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m11s
Test / Hpkg (push) Successful in 4m10s
Test / Sandbox (race detector) (push) Successful in 4m40s
Test / Hakurei (race detector) (push) Successful in 5m21s
Test / Hakurei (push) Successful in 2m18s
Test / Flake checks (push) Successful in 1m32s
Finalise no longer clobbers hst.Config.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-09 04:38:54 +09:00
a40d182706
internal/app: build container state in shim
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 40s
Test / Hakurei (race detector) (push) Successful in 44s
Test / Hakurei (push) Successful in 44s
Test / Hpkg (push) Successful in 41s
Test / Flake checks (push) Successful in 1m21s
This significantly decreases ipc overhead.

Closes #3.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-08 22:30:40 +09:00
e5baaf416f
internal/app: check transmitted ops
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Hakurei (push) Successful in 3m13s
Test / Hpkg (push) Successful in 4m5s
Test / Sandbox (race detector) (push) Successful in 4m28s
Test / Hakurei (race detector) (push) Successful in 5m23s
Test / Sandbox (push) Successful in 1m25s
Test / Flake checks (push) Successful in 1m33s
This simulates params to shim and this is the last step before params to shim is merged.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-08 20:02:09 +09:00
ee6c471fe6
internal/app: relocate ops condition
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m14s
Test / Hpkg (push) Successful in 4m4s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hakurei (race detector) (push) Successful in 5m24s
Test / Hakurei (push) Successful in 2m18s
Test / Flake checks (push) Successful in 1m32s
This allows reuse and finer grained testing of fromConfig.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-08 19:39:00 +09:00
16bf3178d3
internal/app: relocate dynamic exported state
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 3m55s
Test / Sandbox (race detector) (push) Successful in 4m30s
Test / Hakurei (race detector) (push) Successful in 5m18s
Test / Flake checks (push) Successful in 1m30s
This allows reuse of the populateEarly method in test instrumentation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-08 18:34:17 +09:00
034c59a26a
internal/app: relocate late sys/params outcome
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m11s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m30s
Test / Hakurei (race detector) (push) Successful in 5m22s
Test / Flake checks (push) Successful in 1m29s
This will end up merged with another op after reordering. For now relocate it into its dedicated op for test instrumentation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-08 18:26:50 +09:00
5bf28901a4
cmd/hsu: check against setgid bit
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m10s
Test / Hpkg (push) Successful in 4m5s
Test / Sandbox (race detector) (push) Successful in 4m33s
Test / Hakurei (race detector) (push) Successful in 5m20s
Test / Hakurei (push) Successful in 2m18s
Test / Flake checks (push) Successful in 1m31s
The getgroups behaviour is already checked for, but it never hurts to be more careful in a setuid program.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-08 18:22:24 +09:00
9b507715d4
hst/dbus: validate interface strings
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m3s
Test / Hpkg (push) Successful in 3m58s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 5m11s
Test / Flake checks (push) Successful in 1m22s
This is relocated to hst to validate early.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-08 04:57:22 +09:00
12ab7ea3b4
hst/fs: access ops through interface
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Hakurei (push) Successful in 3m14s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m28s
Test / Hakurei (race detector) (push) Successful in 5m22s
Test / Sandbox (push) Successful in 1m28s
Test / Flake checks (push) Successful in 1m29s
This removes the final hakurei.app/container import from hst.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-07 23:59:48 +09:00
1f0226f7e0
container/check: relocate overlay escape
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m31s
Test / Hakurei (race detector) (push) Successful in 5m25s
Test / Flake checks (push) Successful in 1m40s
This is used in hst to format strings.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-07 23:56:19 +09:00
584ce3da68
container/bits: move bind bits
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 4m14s
Test / Sandbox (race detector) (push) Successful in 4m29s
Test / Hakurei (race detector) (push) Successful in 5m21s
Test / Flake checks (push) Successful in 1m31s
This allows referring to the bits without importing container.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-07 21:38:31 +09:00
5d18af0007
container/fhs: move pathname constants
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m6s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m29s
Test / Hakurei (race detector) (push) Successful in 3m5s
Test / Hakurei (push) Successful in 2m10s
Test / Flake checks (push) Successful in 1m21s
This allows referencing FHS pathnames without importing container.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-07 21:29:16 +09:00
0e6c1a5026
container/check: move absolute pathname
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Hpkg (push) Successful in 4m3s
Test / Sandbox (race detector) (push) Successful in 4m26s
Test / Hakurei (race detector) (push) Successful in 5m19s
Test / Sandbox (push) Successful in 1m28s
Test / Hakurei (push) Successful in 2m16s
Test / Flake checks (push) Successful in 1m37s
This allows use of absolute pathname values without importing container.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-07 20:57:58 +09:00
d23b4dc9e6
hst/dbus: move dbus config struct
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m11s
Test / Hakurei (push) Successful in 3m12s
Test / Hpkg (push) Successful in 4m0s
Test / Hakurei (race detector) (push) Successful in 5m20s
Test / Sandbox (race detector) (push) Successful in 2m11s
Test / Flake checks (push) Successful in 1m31s
This allows holding a xdg-dbus-proxy configuration without importing system/dbus.

It also makes more sense in the project structure since the config struct is part of the hst API however the rest of the implementation is not.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-07 19:03:51 +09:00
3ce63e95d7
container: move seccomp preset bits
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m13s
Test / Hpkg (push) Successful in 4m2s
Test / Hakurei (race detector) (push) Successful in 5m16s
Test / Sandbox (race detector) (push) Successful in 2m5s
Test / Hakurei (push) Successful in 2m16s
Test / Flake checks (push) Successful in 1m33s
This allows holding the bits without cgo.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-07 18:28:20 +09:00
2489766efe
hst/config: identity bounds check early
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m4s
Test / Hpkg (push) Successful in 3m53s
Test / Sandbox (race detector) (push) Successful in 4m28s
Test / Hakurei (race detector) (push) Successful in 5m16s
Test / Flake checks (push) Successful in 1m30s
This makes sense to do here instead of in internal/app.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-07 17:58:28 +09:00
9e48d7f562
hst/config: move container fields from toplevel
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m7s
Test / Hpkg (push) Successful in 3m54s
Test / Hakurei (race detector) (push) Successful in 5m18s
Test / Sandbox (race detector) (push) Successful in 2m10s
Test / Hakurei (push) Successful in 2m13s
Test / Flake checks (push) Successful in 1m33s
This change also moves pd behaviour to cmd/hakurei, as this does not belong in the hst API.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-07 04:24:45 +09:00
f280994957
internal/app: check nscd socket for path hiding
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Hakurei (push) Successful in 45s
Test / Hakurei (race detector) (push) Successful in 45s
Test / Hpkg (push) Successful in 42s
Test / Sandbox (push) Successful in 1m32s
Test / Sandbox (race detector) (push) Successful in 2m19s
Test / Flake checks (push) Successful in 1m26s
This can seriously break things, and exposes extra host attack surface, so include it here.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-05 20:47:30 +09:00
ae7b343cde
hst: reword and move constants
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 4m0s
Test / Sandbox (race detector) (push) Successful in 4m25s
Test / Hakurei (race detector) (push) Successful in 5m14s
Test / Sandbox (push) Successful in 1m26s
Test / Flake checks (push) Successful in 1m32s
These values are considered part of the stable, exported API, so move them to hst.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-05 17:40:32 +09:00
a63a372fe0
internal/app: merge static stub
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Hakurei (push) Successful in 3m4s
Test / Hpkg (push) Successful in 3m58s
Test / Hakurei (race detector) (push) Successful in 5m16s
Test / Sandbox (push) Successful in 1m20s
Test / Sandbox (race detector) (push) Successful in 2m9s
Test / Flake checks (push) Successful in 1m32s
These tests now serve as integration tests, and finer grained tests for each op will be added slowly.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-05 17:15:14 +09:00
16f9001f5f
hst/config: update doc comments
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m11s
Test / Hpkg (push) Successful in 4m0s
Test / Sandbox (race detector) (push) Successful in 4m28s
Test / Hakurei (race detector) (push) Successful in 5m15s
Test / Hakurei (push) Successful in 2m15s
Test / Flake checks (push) Successful in 1m21s
Some information here are horribly out of date. This change updates and improves all doc comments.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-05 04:12:53 +09:00
80ad2e4e23
internal/app: do not offset base value
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m12s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m23s
Test / Hakurei (race detector) (push) Successful in 5m16s
Test / Hakurei (push) Successful in 2m9s
Test / Flake checks (push) Successful in 1m25s
This value is applied to the shim, it is incorrect to offset the base value as well.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-05 03:59:52 +09:00
92b83bd599
internal/app: apply pd behaviour to outcomeState
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m6s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m29s
Test / Hakurei (race detector) (push) Successful in 2m56s
Test / Flake checks (push) Successful in 1m34s
This avoids needlessly clobbering hst.Config.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-05 03:53:23 +09:00
8ace214832
system/wayland: hang up security-context-v1 internally
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 40s
Test / Hakurei (push) Successful in 43s
Test / Hakurei (race detector) (push) Successful in 44s
Test / Hpkg (push) Successful in 41s
Test / Flake checks (push) Successful in 1m26s
This should have been an implementation detail and should not be up to the caller to close.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-05 03:25:13 +09:00
eb5ee4fece
internal/app: modularise outcome finalise
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m10s
Test / Hpkg (push) Successful in 4m8s
Test / Sandbox (race detector) (push) Successful in 4m35s
Test / Hakurei (race detector) (push) Successful in 5m16s
Test / Flake checks (push) Successful in 1m30s
This is the initial effort of splitting up host and container side of finalisation for params to shim. The new layout also enables much finer grained unit testing of each step, as well as partition access to per-app state for each step.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-05 02:52:50 +09:00
9462af08f3
system/dbus: dump buffer internally
All checks were successful
Test / Create distribution (push) Successful in 44s
Test / Sandbox (push) Successful in 2m32s
Test / Hpkg (push) Successful in 4m13s
Test / Sandbox (race detector) (push) Successful in 4m49s
Test / Hakurei (race detector) (push) Successful in 5m31s
Test / Hakurei (push) Successful in 2m11s
Test / Flake checks (push) Successful in 1m28s
This should have been an implementation detail and should not be up to the caller to call it.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-04 20:31:14 +09:00
a5f0aa3f30
internal/app: declutter and merge small files
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m4s
Test / Hakurei (push) Successful in 3m2s
Test / Hpkg (push) Successful in 4m3s
Test / Hakurei (race detector) (push) Successful in 5m8s
Test / Sandbox (race detector) (push) Successful in 2m4s
Test / Flake checks (push) Successful in 1m26s
This should make internal/app easier to work with for the upcoming params to shim.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-03 16:59:29 +09:00
dd0bb0a391
internal/app: check username validation
All checks were successful
Test / Create distribution (push) Successful in 38s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m6s
Test / Hpkg (push) Successful in 3m59s
Test / Sandbox (race detector) (push) Successful in 4m36s
Test / Hakurei (race detector) (push) Successful in 5m18s
Test / Flake checks (push) Successful in 1m25s
This stuff should be hardcoded in libc, but check it anyway.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-03 16:42:42 +09:00
d16da6da8c
system: enforce absolute paths
All checks were successful
Test / Create distribution (push) Successful in 1m17s
Test / Sandbox (push) Successful in 2m56s
Test / Hakurei (push) Successful in 3m54s
Test / Hpkg (push) Successful in 4m51s
Test / Sandbox (race detector) (push) Successful in 5m3s
Test / Hakurei (race detector) (push) Successful in 6m0s
Test / Flake checks (push) Successful in 1m38s
This is less error-prone, and is quite easy to integrate considering internal/app has already migrated to container.Absolute.

Closes #11.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-03 02:26:14 +09:00
e58181a930
internal/app/paths: defer extra formatting
All checks were successful
Test / Create distribution (push) Successful in 1m14s
Test / Hakurei (push) Successful in 3m50s
Test / Hpkg (push) Successful in 4m44s
Test / Sandbox (race detector) (push) Successful in 4m51s
Test / Sandbox (push) Successful in 1m37s
Test / Hakurei (race detector) (push) Successful in 3m12s
Test / Flake checks (push) Successful in 1m41s
This reduces payload size for params to shim.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-30 00:21:26 +09:00
71e70b7b5f
internal/app/paths: do not print messages
All checks were successful
Test / Create distribution (push) Successful in 56s
Test / Sandbox (push) Successful in 2m32s
Test / Hakurei (push) Successful in 3m36s
Test / Hpkg (push) Successful in 4m30s
Test / Hakurei (race detector) (push) Successful in 5m40s
Test / Sandbox (race detector) (push) Successful in 2m12s
Test / Flake checks (push) Successful in 1m32s
This change was missed while merging the rest of the logging changes.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-29 09:30:57 +09:00
afa1a8043e
helper/proc: raise FulfillmentTimeout in tests
All checks were successful
Test / Create distribution (push) Successful in 1m1s
Test / Sandbox (push) Successful in 2m30s
Test / Hakurei (push) Successful in 3m36s
Test / Hpkg (push) Successful in 4m22s
Test / Sandbox (race detector) (push) Successful in 4m41s
Test / Hakurei (race detector) (push) Successful in 5m41s
Test / Flake checks (push) Successful in 1m32s
This appears to be yet another source of spurious test failures.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-29 07:44:33 +09:00
1ba1cb8865
hst/config: remove seccomp bit fields
All checks were successful
Test / Create distribution (push) Successful in 1m12s
Test / Sandbox (push) Successful in 2m46s
Test / Hpkg (push) Successful in 4m40s
Test / Sandbox (race detector) (push) Successful in 4m50s
Test / Hakurei (race detector) (push) Successful in 5m51s
Test / Hakurei (push) Successful in 2m36s
Test / Flake checks (push) Successful in 1m41s
These serve little purpose and are not friendly for use from other languages.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-29 07:07:16 +09:00
44ba7a5f02
hst/enablement: move bits from system
All checks were successful
Test / Create distribution (push) Successful in 54s
Test / Sandbox (push) Successful in 2m33s
Test / Hakurei (push) Successful in 3m36s
Test / Hpkg (push) Successful in 4m30s
Test / Sandbox (race detector) (push) Successful in 4m48s
Test / Hakurei (race detector) (push) Successful in 5m47s
Test / Flake checks (push) Successful in 1m40s
This is part of the hst API, should not be in the implementation package.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-29 06:34:29 +09:00
dc467493d8
internal: remove hlog
All checks were successful
Test / Create distribution (push) Successful in 1m11s
Test / Sandbox (push) Successful in 2m37s
Test / Hpkg (push) Successful in 4m41s
Test / Sandbox (race detector) (push) Successful in 4m53s
Test / Hakurei (race detector) (push) Successful in 5m53s
Test / Hakurei (push) Successful in 2m44s
Test / Flake checks (push) Successful in 1m48s
This package has been fully replaced by container.Msg.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-29 06:21:04 +09:00
46cd3a28c8
container: remove global msg
All checks were successful
Test / Create distribution (push) Successful in 1m10s
Test / Sandbox (push) Successful in 2m40s
Test / Hakurei (push) Successful in 3m58s
Test / Hpkg (push) Successful in 4m44s
Test / Sandbox (race detector) (push) Successful in 5m1s
Test / Hakurei (race detector) (push) Successful in 6m2s
Test / Flake checks (push) Successful in 1m47s
This frees all container instances of side effects.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-09-29 06:11:47 +09:00
296 changed files with 17414 additions and 8115 deletions

View File

@ -6,43 +6,64 @@ import (
"io" "io"
"log" "log"
"os" "os"
"os/exec"
"os/user" "os/user"
"strconv" "strconv"
"sync" "sync"
"time" "time"
_ "unsafe"
"hakurei.app/command" "hakurei.app/command"
"hakurei.app/container" "hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal" "hakurei.app/internal"
"hakurei.app/internal/app" "hakurei.app/internal/env"
"hakurei.app/internal/app/state" "hakurei.app/internal/outcome"
"hakurei.app/internal/hlog" "hakurei.app/message"
"hakurei.app/system"
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
) )
func buildCommand(ctx context.Context, out io.Writer) command.Command { //go:linkname optionalErrorUnwrap hakurei.app/container.optionalErrorUnwrap
func optionalErrorUnwrap(_ error) error
func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
var ( var (
flagVerbose bool flagVerbose bool
flagJSON bool flagJSON bool
) )
c := command.New(out, log.Printf, "hakurei", func([]string) error { internal.InstallOutput(flagVerbose); return nil }). c := command.New(out, log.Printf, "hakurei", func([]string) error {
msg.SwapVerbose(flagVerbose)
if early.yamaLSM != nil {
msg.Verbosef("cannot enable ptrace protection via Yama LSM: %v", early.yamaLSM)
// not fatal
}
if early.dumpable != nil {
log.Printf("cannot set SUID_DUMP_DISABLE: %s", early.dumpable)
// not fatal
}
return nil
}).
Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity"). Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable") Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")
c.Command("shim", command.UsageInternal, func([]string) error { app.ShimMain(); return errSuccess }) c.Command("shim", command.UsageInternal, func([]string) error { outcome.Shim(msg); return errSuccess })
c.Command("app", "Load app from configuration file", func(args []string) error { c.Command("app", "Load and start container from configuration file", func(args []string) error {
if len(args) < 1 { if len(args) < 1 {
log.Fatal("app requires at least 1 argument") log.Fatal("app requires at least 1 argument")
} }
// config extraArgs... // config extraArgs...
config := tryPath(args[0]) config := tryPath(msg, args[0])
config.Args = append(config.Args, args[1:]...) if config != nil && config.Container != nil {
config.Container.Args = append(config.Container.Args, args[1:]...)
}
app.Main(ctx, config) outcome.Main(ctx, msg, config)
panic("unreachable") panic("unreachable")
}) })
@ -59,17 +80,13 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
flagHomeDir string flagHomeDir string
flagUserName string flagUserName string
flagPrivateRuntime, flagPrivateTmpdir bool
flagWayland, flagX11, flagDBus, flagPulse bool flagWayland, flagX11, flagDBus, flagPulse bool
) )
c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error { c.NewCommand("run", "Configure and start a permissive container", func(args []string) error {
// initialise config from flags if flagIdentity < hst.IdentityStart || flagIdentity > hst.IdentityEnd {
config := &hst.Config{
ID: flagID,
Args: args,
}
if flagIdentity < 0 || flagIdentity > 9999 {
log.Fatalf("identity %d out of range", flagIdentity) log.Fatalf("identity %d out of range", flagIdentity)
} }
@ -78,15 +95,15 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
passwd *user.User passwd *user.User
passwdOnce sync.Once passwdOnce sync.Once
passwdFunc = func() { passwdFunc = func() {
us := strconv.Itoa(app.HsuUid(new(app.Hsu).MustID(), flagIdentity)) us := strconv.Itoa(hst.ToUser(new(outcome.Hsu).MustID(msg), flagIdentity))
if u, err := user.LookupId(us); err != nil { if u, err := user.LookupId(us); err != nil {
hlog.Verbosef("cannot look up uid %s", us) msg.Verbosef("cannot look up uid %s", us)
passwd = &user.User{ passwd = &user.User{
Uid: us, Uid: us,
Gid: us, Gid: us,
Username: "chronos", Username: "chronos",
Name: "Hakurei Permissive Default", Name: "Hakurei Permissive Default",
HomeDir: container.FHSVarEmpty, HomeDir: fhs.VarEmpty,
} }
} else { } else {
passwd = u passwd = u
@ -94,60 +111,138 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
} }
) )
if flagHomeDir == "os" { // paths are identical, resolve inner shell and program path
passwdOnce.Do(passwdFunc) shell := fhs.AbsRoot.Append("bin", "sh")
flagHomeDir = passwd.HomeDir if a, err := check.NewAbs(os.Getenv("SHELL")); err == nil {
shell = a
}
progPath := shell
if len(args) > 0 {
if p, err := exec.LookPath(args[0]); err != nil {
log.Fatal(optionalErrorUnwrap(err))
return err
} else if progPath, err = check.NewAbs(p); err != nil {
log.Fatal(err.Error())
return err
}
} }
if flagUserName == "chronos" { var et hst.Enablement
passwdOnce.Do(passwdFunc) if flagWayland {
flagUserName = passwd.Username et |= hst.EWayland
}
if flagX11 {
et |= hst.EX11
}
if flagDBus {
et |= hst.EDBus
}
if flagPulse {
et |= hst.EPulse
} }
config.Identity = flagIdentity config := &hst.Config{
config.Groups = flagGroups ID: flagID,
config.Username = flagUserName Identity: flagIdentity,
Groups: flagGroups,
Enablements: hst.NewEnablements(et),
if a, err := container.NewAbs(flagHomeDir); err != nil { Container: &hst.ContainerConfig{
Filesystem: []hst.FilesystemConfigJSON{
// autoroot, includes the home directory
{FilesystemConfig: &hst.FSBind{
Target: fhs.AbsRoot,
Source: fhs.AbsRoot,
Write: true,
Special: true,
}},
},
Username: flagUserName,
Shell: shell,
Path: progPath,
Args: args,
Flags: hst.FUserns | hst.FHostNet | hst.FHostAbstract | hst.FTty,
},
}
// bind GPU stuff
if et&(hst.EX11|hst.EWayland) != 0 {
config.Container.Filesystem = append(config.Container.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
Source: fhs.AbsDev.Append("dri"),
Device: true,
Optional: true,
}})
}
config.Container.Filesystem = append(config.Container.Filesystem,
// opportunistically bind kvm
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
Source: fhs.AbsDev.Append("kvm"),
Device: true,
Optional: true,
}},
// do autoetc last
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
Target: fhs.AbsEtc,
Source: fhs.AbsEtc,
Special: true,
}},
)
if config.Container.Username == "chronos" {
passwdOnce.Do(passwdFunc)
config.Container.Username = passwd.Username
}
{
homeDir := flagHomeDir
if homeDir == "os" {
passwdOnce.Do(passwdFunc)
homeDir = passwd.HomeDir
}
if a, err := check.NewAbs(homeDir); err != nil {
log.Fatal(err.Error()) log.Fatal(err.Error())
return err return err
} else { } else {
config.Home = a config.Container.Home = a
}
} }
var e system.Enablement if !flagPrivateRuntime {
if flagWayland { config.Container.Flags |= hst.FShareRuntime
e |= system.EWayland
} }
if flagX11 { if !flagPrivateTmpdir {
e |= system.EX11 config.Container.Flags |= hst.FShareTmpdir
} }
if flagDBus {
e |= system.EDBus
}
if flagPulse {
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 flagDBus { if flagDBus {
if flagDBusConfigSession == "builtin" { if flagDBusConfigSession == "builtin" {
config.SessionBus = dbus.NewConfig(flagID, true, flagDBusMpris) config.SessionBus = dbus.NewConfig(flagID, true, flagDBusMpris)
} else { } else {
if conf, err := dbus.NewConfigFromFile(flagDBusConfigSession); err != nil { if f, err := os.Open(flagDBusConfigSession); err != nil {
log.Fatalf("cannot load session bus proxy config from %q: %s", flagDBusConfigSession, err) log.Fatal(err.Error())
} else { } else {
config.SessionBus = conf decodeJSON(log.Fatal, "load session bus proxy config", f, &config.SessionBus)
if err = f.Close(); err != nil {
log.Fatal(err.Error())
}
} }
} }
// system bus proxy is optional // system bus proxy is optional
if flagDBusConfigSystem != "nil" { if flagDBusConfigSystem != "nil" {
if conf, err := dbus.NewConfigFromFile(flagDBusConfigSystem); err != nil { if f, err := os.Open(flagDBusConfigSystem); err != nil {
log.Fatalf("cannot load system bus proxy config from %q: %s", flagDBusConfigSystem, err) log.Fatal(err.Error())
} else { } else {
config.SystemBus = conf decodeJSON(log.Fatal, "load system bus proxy config", f, &config.SystemBus)
if err = f.Close(); err != nil {
log.Fatal(err.Error())
}
} }
} }
@ -162,7 +257,7 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
} }
} }
app.Main(ctx, config) outcome.Main(ctx, msg, config)
panic("unreachable") panic("unreachable")
}). }).
Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"), Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),
@ -183,6 +278,10 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
"Container home directory"). "Container home directory").
Flag(&flagUserName, "u", command.StringFlag("chronos"), Flag(&flagUserName, "u", command.StringFlag("chronos"),
"Passwd user name within sandbox"). "Passwd user name within sandbox").
Flag(&flagPrivateRuntime, "private-runtime", command.BoolFlag(false),
"Do not share XDG_RUNTIME_DIR between containers under the same identity").
Flag(&flagPrivateTmpdir, "private-tmpdir", command.BoolFlag(false),
"Do not share TMPDIR between containers under the same identity").
Flag(&flagWayland, "wayland", command.BoolFlag(false), Flag(&flagWayland, "wayland", command.BoolFlag(false),
"Enable connection to Wayland via security-context-v1"). "Enable connection to Wayland via security-context-v1").
Flag(&flagX11, "X", command.BoolFlag(false), Flag(&flagX11, "X", command.BoolFlag(false),
@ -194,7 +293,10 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
} }
{ {
var flagShort bool var (
flagShort bool
flagNoStore bool
)
c.NewCommand("show", "Show live or local app configuration", func(args []string) error { c.NewCommand("show", "Show live or local app configuration", func(args []string) error {
switch len(args) { switch len(args) {
case 0: // system case 0: // system
@ -202,48 +304,50 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
case 1: // instance case 1: // instance
name := args[0] name := args[0]
config, entry := tryShort(name)
if config == nil { var (
config = tryPath(name) config *hst.Config
entry *hst.State
)
if !flagNoStore {
var sc hst.Paths
env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil))
entry = tryIdentifier(msg, name, outcome.NewStore(&sc))
}
if entry == nil {
config = tryPath(msg, name)
} else {
config = entry.Config
}
if !printShowInstance(os.Stdout, time.Now().UTC(), entry, config, flagShort, flagJSON) {
os.Exit(1)
} }
printShowInstance(os.Stdout, time.Now().UTC(), entry, config, flagShort, flagJSON)
default: default:
log.Fatal("show requires 1 argument") log.Fatal("show requires 1 argument")
} }
return errSuccess return errSuccess
}).Flag(&flagShort, "short", command.BoolFlag(false), "Omit filesystem information") }).
Flag(&flagShort, "short", command.BoolFlag(false), "Omit filesystem information").
Flag(&flagNoStore, "no-store", command.BoolFlag(false), "Do not attempt to match from active instances")
} }
{ {
var flagShort bool var flagShort bool
c.NewCommand("ps", "List active instances", func(args []string) error { c.NewCommand("ps", "List active instances", func(args []string) error {
var sc hst.Paths var sc hst.Paths
app.CopyPaths(&sc, new(app.Hsu).MustID()) env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil))
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(sc.RunDirPath.String()), flagShort, flagJSON) printPs(msg, os.Stdout, time.Now().UTC(), outcome.NewStore(&sc), flagShort, flagJSON)
return errSuccess return errSuccess
}).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id") }).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id")
} }
c.Command("version", "Display version information", func(args []string) error { c.Command("version", "Display version information", func(args []string) error { fmt.Println(internal.Version()); return errSuccess })
fmt.Println(internal.Version()) c.Command("license", "Show full license text", func(args []string) error { fmt.Println(license); return errSuccess })
return errSuccess c.Command("template", "Produce a config template", func(args []string) error { encodeJSON(log.Fatal, os.Stdout, false, hst.Template()); return errSuccess })
}) c.Command("help", "Show this help message", func([]string) error { c.PrintHelp(); return errSuccess })
c.Command("license", "Show full license text", func(args []string) error {
fmt.Println(license)
return errSuccess
})
c.Command("template", "Produce a config template", func(args []string) error {
printJSON(os.Stdout, false, hst.Template())
return errSuccess
})
c.Command("help", "Show this help message", func([]string) error {
c.PrintHelp()
return errSuccess
})
return c return c
} }

View File

@ -7,9 +7,12 @@ import (
"testing" "testing"
"hakurei.app/command" "hakurei.app/command"
"hakurei.app/message"
) )
func TestHelp(t *testing.T) { func TestHelp(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
args []string args []string
@ -20,8 +23,8 @@ func TestHelp(t *testing.T) {
Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS] Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS]
Commands: Commands:
app Load app from configuration file app Load and start container from configuration file
run Configure and start a permissive default sandbox run Configure and start a permissive container
show Show live or local app configuration show Show live or local app configuration
ps List active instances ps List active instances
version Display version information version Display version information
@ -33,7 +36,7 @@ Commands:
}, },
{ {
"run", []string{"run", "-h"}, ` "run", []string{"run", "-h"}, `
Usage: hakurei run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--wayland] [-X] [--dbus] [--pulse] COMMAND [OPTIONS] Usage: hakurei run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--private-runtime] [--private-tmpdir] [--wayland] [-X] [--dbus] [--pulse] COMMAND [OPTIONS]
Flags: Flags:
-X Enable direct connection to X11 -X Enable direct connection to X11
@ -55,6 +58,10 @@ Flags:
Reverse-DNS style Application identifier, leave empty to inherit instance identifier Reverse-DNS style Application identifier, leave empty to inherit instance identifier
-mpris -mpris
Allow owning MPRIS D-Bus path, has no effect if custom config is available Allow owning MPRIS D-Bus path, has no effect if custom config is available
-private-runtime
Do not share XDG_RUNTIME_DIR between containers under the same identity
-private-tmpdir
Do not share TMPDIR between containers under the same identity
-pulse -pulse
Enable direct connection to PulseAudio Enable direct connection to PulseAudio
-u string -u string
@ -67,8 +74,10 @@ Flags:
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
out := new(bytes.Buffer) out := new(bytes.Buffer)
c := buildCommand(t.Context(), out) c := buildCommand(t.Context(), message.New(nil), new(earlyHardeningErrs), out)
if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) { if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) {
t.Errorf("Parse: error = %v; want %v", t.Errorf("Parse: error = %v; want %v",
err, command.ErrHelp) err, command.ErrHelp)

60
cmd/hakurei/json.go Normal file
View File

@ -0,0 +1,60 @@
package main
import (
"encoding/json"
"errors"
"io"
"strconv"
)
// decodeJSON decodes json from r and stores it in v. A non-nil error results in a call to fatal.
func decodeJSON(fatal func(v ...any), op string, r io.Reader, v any) {
err := json.NewDecoder(r).Decode(v)
if err == nil {
return
}
var (
syntaxError *json.SyntaxError
unmarshalTypeError *json.UnmarshalTypeError
msg string
)
switch {
case errors.As(err, &syntaxError) && syntaxError != nil:
msg = syntaxError.Error() +
" at byte " + strconv.FormatInt(syntaxError.Offset, 10)
case errors.As(err, &unmarshalTypeError) && unmarshalTypeError != nil:
msg = "inappropriate " + unmarshalTypeError.Value +
" at byte " + strconv.FormatInt(unmarshalTypeError.Offset, 10)
default:
// InvalidUnmarshalError: incorrect usage, does not need to be handled
// io.ErrUnexpectedEOF: no additional error information available
msg = err.Error()
}
fatal("cannot " + op + ": " + msg)
}
// encodeJSON encodes v to output. A non-nil error results in a call to fatal.
func encodeJSON(fatal func(v ...any), output io.Writer, short bool, v any) {
encoder := json.NewEncoder(output)
if !short {
encoder.SetIndent("", " ")
}
if err := encoder.Encode(v); err != nil {
var marshalerError *json.MarshalerError
if errors.As(err, &marshalerError) && marshalerError != nil {
// this likely indicates an implementation error in hst
fatal("cannot encode json for " + marshalerError.Type.String() + ": " + marshalerError.Err.Error())
return
}
// UnsupportedTypeError, UnsupportedValueError: incorrect usage, does not need to be handled
fatal("cannot write json: " + err.Error())
}
}

107
cmd/hakurei/json_test.go Normal file
View File

@ -0,0 +1,107 @@
package main_test
import (
"io"
"reflect"
"strings"
"testing"
_ "unsafe"
"hakurei.app/container/stub"
)
//go:linkname decodeJSON hakurei.app/cmd/hakurei.decodeJSON
func decodeJSON(fatal func(v ...any), op string, r io.Reader, v any)
func TestDecodeJSON(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
t reflect.Type
data string
want any
msg string
}{
{"success", reflect.TypeFor[uintptr](), "3735928559\n", uintptr(0xdeadbeef), ""},
{"syntax", reflect.TypeFor[*int](), "\x00", nil,
`cannot load sample: invalid character '\x00' looking for beginning of value at byte 1`},
{"type", reflect.TypeFor[uintptr](), "-1", nil,
`cannot load sample: inappropriate number -1 at byte 2`},
{"default", reflect.TypeFor[*int](), "{", nil,
"cannot load sample: unexpected EOF"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var (
gotP = reflect.New(tc.t)
gotMsg *string
)
decodeJSON(func(v ...any) {
if gotMsg != nil {
t.Fatal("fatal called twice")
}
msg := v[0].(string)
gotMsg = &msg
}, "load sample", strings.NewReader(tc.data), gotP.Interface())
if tc.msg != "" {
if gotMsg == nil {
t.Errorf("decodeJSON: success, want fatal %q", tc.msg)
} else if *gotMsg != tc.msg {
t.Errorf("decodeJSON: fatal = %q, want %q", *gotMsg, tc.msg)
}
} else if gotMsg != nil {
t.Errorf("decodeJSON: fatal = %q", *gotMsg)
} else if !reflect.DeepEqual(gotP.Elem().Interface(), tc.want) {
t.Errorf("decodeJSON: %#v, want %#v", gotP.Elem().Interface(), tc.want)
}
})
}
}
//go:linkname encodeJSON hakurei.app/cmd/hakurei.encodeJSON
func encodeJSON(fatal func(v ...any), output io.Writer, short bool, v any)
func TestEncodeJSON(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
v any
want string
}{
{"marshaler", errorJSONMarshaler{},
`cannot encode json for main_test.errorJSONMarshaler: unique error 3735928559 injected by the test suite`},
{"default", func() {},
`cannot write json: json: unsupported type: func()`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var called bool
encodeJSON(func(v ...any) {
if called {
t.Fatal("fatal called twice")
}
called = true
if v[0].(string) != tc.want {
t.Errorf("encodeJSON: fatal = %q, want %q", v[0].(string), tc.want)
}
}, nil, false, tc.v)
if !called {
t.Errorf("encodeJSON: success, want fatal %q", tc.want)
}
})
}
}
// errorJSONMarshaler implements json.Marshaler.
type errorJSONMarshaler struct{}
func (errorJSONMarshaler) MarshalJSON() ([]byte, error) { return nil, stub.UniqueError(0xdeadbeef) }

View File

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

View File

@ -1,7 +1,7 @@
package main package main
import ( import (
"encoding/json" "encoding/hex"
"errors" "errors"
"io" "io"
"log" "log"
@ -11,52 +11,49 @@ import (
"syscall" "syscall"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/app" "hakurei.app/internal/store"
"hakurei.app/internal/app/state" "hakurei.app/message"
"hakurei.app/internal/hlog"
) )
func tryPath(name string) (config *hst.Config) { // tryPath attempts to read [hst.Config] from multiple sources.
var r io.Reader // tryPath reads from [os.Stdin] if name has value "-".
// Otherwise, name is passed to tryFd, and if that returns nil, name is passed to [os.Open].
func tryPath(msg message.Msg, name string) (config *hst.Config) {
var r io.ReadCloser
config = new(hst.Config) config = new(hst.Config)
if name != "-" { if name != "-" {
r = tryFd(name) r = tryFd(msg, name)
if r == nil { if r == nil {
hlog.Verbose("load configuration from file") msg.Verbose("load configuration from file")
if f, err := os.Open(name); err != nil { if f, err := os.Open(name); err != nil {
log.Fatalf("cannot access configuration file %q: %s", name, err) log.Fatal(err.Error())
return
} else { } else {
// finalizer closes f
r = f r = f
} }
} else {
defer func() {
if err := r.(io.ReadCloser).Close(); err != nil {
log.Printf("cannot close config fd: %v", err)
}
}()
} }
} else { } else {
r = os.Stdin r = os.Stdin
} }
if err := json.NewDecoder(r).Decode(&config); err != nil { decodeJSON(log.Fatal, "load configuration", r, &config)
log.Fatalf("cannot load configuration: %v", err) if err := r.Close(); err != nil {
log.Fatal(err.Error())
} }
return return
} }
func tryFd(name string) io.ReadCloser { // tryFd returns a [io.ReadCloser] if name represents an integer corresponding to a valid file descriptor.
func tryFd(msg message.Msg, name string) io.ReadCloser {
if v, err := strconv.Atoi(name); err != nil { if v, err := strconv.Atoi(name); err != nil {
if !errors.Is(err, strconv.ErrSyntax) { if !errors.Is(err, strconv.ErrSyntax) {
hlog.Verbosef("name cannot be interpreted as int64: %v", err) msg.Verbosef("name cannot be interpreted as int64: %v", err)
} }
return nil return nil
} else { } else {
hlog.Verbosef("trying config stream from %d", v) msg.Verbosef("trying config stream from %d", v)
fd := uintptr(v) fd := uintptr(v)
if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 { if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 {
if errors.Is(errno, syscall.EBADF) { if errors.Is(errno, syscall.EBADF) {
@ -68,10 +65,29 @@ func tryFd(name string) io.ReadCloser {
} }
} }
func tryShort(name string) (config *hst.Config, entry *state.State) { // shortLengthMin is the minimum length a short form identifier can have and still be interpreted as an identifier.
likePrefix := false const shortLengthMin = 1 << 3
if len(name) <= 32 {
likePrefix = true // shortIdentifier returns an eight character short representation of [hst.ID] from its random bytes.
func shortIdentifier(id *hst.ID) string {
return shortIdentifierString(id.String())
}
// shortIdentifierString implements shortIdentifier on an arbitrary string.
func shortIdentifierString(s string) string {
return s[len(hst.ID{}) : len(hst.ID{})+shortLengthMin]
}
// tryIdentifier attempts to match [hst.State] from a [hex] representation of [hst.ID] or a prefix of its lower half.
func tryIdentifier(msg message.Msg, name string, s *store.Store) *hst.State {
const (
likeShort = 1 << iota
likeFull
)
var likely uintptr
if len(name) >= shortLengthMin && len(name) <= len(hst.ID{}) { // half the hex representation
// cannot safely decode here due to unknown alignment
for _, c := range name { for _, c := range name {
if c >= '0' && c <= '9' { if c >= '0' && c <= '9' {
continue continue
@ -79,35 +95,68 @@ func tryShort(name string) (config *hst.Config, entry *state.State) {
if c >= 'a' && c <= 'f' { if c >= 'a' && c <= 'f' {
continue continue
} }
likePrefix = false return nil
break
} }
likely |= likeShort
} else if len(name) == hex.EncodedLen(len(hst.ID{})) {
likely |= likeFull
} }
// try to match from state store if likely == 0 {
if likePrefix && len(name) >= 8 { return nil
hlog.Verbose("argument looks like prefix")
var sc hst.Paths
app.CopyPaths(&sc, new(app.Hsu).MustID())
s := state.NewMulti(sc.RunDirPath.String())
if entries, err := state.Join(s); err != nil {
log.Printf("cannot join store: %v", err)
// drop to fetch from file
} else {
for id := range entries {
v := id.String()
if strings.HasPrefix(v, name) {
// match, use config from this state entry
entry = entries[id]
config = entry.Config
break
} }
hlog.Verbosef("instance %s skipped", v) entries, copyError := s.All()
} defer func() {
if err := copyError(); err != nil {
msg.GetLogger().Println(getMessage("cannot iterate over store:", err))
} }
}()
switch {
case likely&likeShort != 0:
msg.Verbose("argument looks like short identifier")
for eh := range entries {
if eh.DecodeErr != nil {
msg.Verbose(getMessage("skipping instance:", eh.DecodeErr))
continue
} }
return if strings.HasPrefix(eh.ID.String()[len(hst.ID{}):], name) {
var entry hst.State
if _, err := eh.Load(&entry); err != nil {
msg.GetLogger().Println(getMessage("cannot load state entry:", err))
continue
}
return &entry
}
}
return nil
case likely&likeFull != 0:
var likelyID hst.ID
if likelyID.UnmarshalText([]byte(name)) != nil {
return nil
}
msg.Verbose("argument looks like identifier")
for eh := range entries {
if eh.DecodeErr != nil {
msg.Verbose(getMessage("skipping instance:", eh.DecodeErr))
continue
}
if eh.ID == likelyID {
var entry hst.State
if _, err := eh.Load(&entry); err != nil {
msg.GetLogger().Println(getMessage("cannot load state entry:", err))
continue
}
return &entry
}
}
return nil
default:
panic("unreachable")
}
} }

117
cmd/hakurei/parse_test.go Normal file
View File

@ -0,0 +1,117 @@
package main
import (
"bytes"
"reflect"
"testing"
"time"
"hakurei.app/container/check"
"hakurei.app/hst"
"hakurei.app/internal/store"
"hakurei.app/message"
)
func TestShortIdentifier(t *testing.T) {
t.Parallel()
id := hst.ID{
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10,
}
const want = "fedcba98"
if got := shortIdentifier(&id); got != want {
t.Errorf("shortIdentifier: %q, want %q", got, want)
}
}
func TestTryIdentifier(t *testing.T) {
t.Parallel()
msg := message.New(nil)
id := hst.ID{
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10,
}
withBase := func(extra ...hst.State) []hst.State {
return append([]hst.State{
{ID: (hst.ID)(bytes.Repeat([]byte{0xaa}, len(hst.ID{}))), PID: 0xbeef, ShimPID: 0xcafe, Config: hst.Template(), Time: time.Unix(0, 0xdeadbeef0)},
{ID: (hst.ID)(bytes.Repeat([]byte{0xab}, len(hst.ID{}))), PID: 0x1beef, ShimPID: 0x1cafe, Config: hst.Template(), Time: time.Unix(0, 0xdeadbeef1)},
{ID: (hst.ID)(bytes.Repeat([]byte{0xf0}, len(hst.ID{}))), PID: 0x2beef, ShimPID: 0x2cafe, Config: hst.Template(), Time: time.Unix(0, 0xdeadbeef2)},
{ID: (hst.ID)(bytes.Repeat([]byte{0xfe}, len(hst.ID{}))), PID: 0xbed, ShimPID: 0xfff, Config: func() *hst.Config {
template := hst.Template()
template.Identity = hst.IdentityEnd
return template
}(), Time: time.Unix(0, 0xcafebabe0)},
{ID: (hst.ID)(bytes.Repeat([]byte{0xfc}, len(hst.ID{}))), PID: 0x1bed, ShimPID: 0x1fff, Config: func() *hst.Config {
template := hst.Template()
template.Identity = 0xfc
return template
}(), Time: time.Unix(0, 0xcafebabe1)},
{ID: (hst.ID)(bytes.Repeat([]byte{0xce}, len(hst.ID{}))), PID: 0x2bed, ShimPID: 0x2fff, Config: func() *hst.Config {
template := hst.Template()
template.Identity = 0xce
return template
}(), Time: time.Unix(0, 0xcafebabe2)},
}, extra...)
}
sampleEntry := hst.State{
ID: id,
PID: 0xcafe,
ShimPID: 0xdead,
Config: hst.Template(),
}
testCases := []struct {
name string
s string
data []hst.State
want *hst.State
}{
{"likely entries fault", "ffffffff", nil, nil},
{"likely short too short", "ff", nil, nil},
{"likely short too long", "fffffffffffffffff", nil, nil},
{"likely short invalid lower", "fffffff\x00", nil, nil},
{"likely short invalid higher", "0000000\xff", nil, nil},
{"short no match", "fedcba98", withBase(), nil},
{"short match", "fedcba98", withBase(sampleEntry), &sampleEntry},
{"short match single", "fedcba98", []hst.State{sampleEntry}, &sampleEntry},
{"short match longer", "fedcba98765", withBase(sampleEntry), &sampleEntry},
{"likely long invalid", "0123456789abcdeffedcba987654321\x00", nil, nil},
{"long no match", "0123456789abcdeffedcba9876543210", withBase(), nil},
{"long match", "0123456789abcdeffedcba9876543210", withBase(sampleEntry), &sampleEntry},
{"long match single", "0123456789abcdeffedcba9876543210", []hst.State{sampleEntry}, &sampleEntry},
}
for _, tc := range testCases {
base := check.MustAbs(t.TempDir()).Append("store")
s := store.New(base)
for i := range tc.data {
if h, err := s.Handle(tc.data[i].Identity); err != nil {
t.Fatalf("Handle: error = %v", err)
} else {
var unlock func()
if unlock, err = h.Lock(); err != nil {
t.Fatalf("Lock: error = %v", err)
}
_, err = h.Save(&tc.data[i])
unlock()
if err != nil {
t.Fatalf("Save: error = %v", err)
}
}
}
// store must not be written to beyond this point
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := tryIdentifier(msg, tc.s, store.New(base))
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("tryIdentifier: %#v, want %#v", got, tc.want)
}
})
}
}

View File

@ -1,7 +1,7 @@
package main package main
import ( import (
"encoding/json" "bytes"
"fmt" "fmt"
"io" "io"
"log" "log"
@ -12,23 +12,27 @@ import (
"time" "time"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/app" "hakurei.app/internal"
"hakurei.app/internal/app/state" "hakurei.app/internal/env"
"hakurei.app/system/dbus" "hakurei.app/internal/outcome"
"hakurei.app/internal/store"
"hakurei.app/message"
) )
// printShowSystem populates and writes a representation of [hst.Info] to output.
func printShowSystem(output io.Writer, short, flagJSON bool) { func printShowSystem(output io.Writer, short, flagJSON bool) {
t := newPrinter(output) t := newPrinter(output)
defer t.MustFlush() defer t.MustFlush()
info := &hst.Info{User: new(app.Hsu).MustID()} info := &hst.Info{Version: internal.Version(), User: new(outcome.Hsu).MustID(nil)}
app.CopyPaths(&info.Paths, info.User) env.CopyPaths().Copy(&info.Paths, info.User)
if flagJSON { if flagJSON {
printJSON(output, short, info) encodeJSON(log.Fatal, output, short, info)
return return
} }
t.Printf("Version:\t%s\n", info.Version)
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("TempDir:\t%s\n", info.TempDir)
t.Printf("SharePath:\t%s\n", info.SharePath) t.Printf("SharePath:\t%s\n", info.SharePath)
@ -36,15 +40,19 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
t.Printf("RunDirPath:\t%s\n", info.RunDirPath) t.Printf("RunDirPath:\t%s\n", info.RunDirPath)
} }
// printShowInstance writes a representation of [hst.State] or [hst.Config] to output.
func printShowInstance( func printShowInstance(
output io.Writer, now time.Time, output io.Writer, now time.Time,
instance *state.State, config *hst.Config, instance *hst.State, config *hst.Config,
short, flagJSON bool) { short, flagJSON bool,
) (valid bool) {
valid = true
if flagJSON { if flagJSON {
if instance != nil { if instance != nil {
printJSON(output, short, instance) encodeJSON(log.Fatal, output, short, instance)
} else { } else {
printJSON(output, short, config) encodeJSON(log.Fatal, output, short, config)
} }
return return
} }
@ -52,13 +60,21 @@ func printShowInstance(
t := newPrinter(output) t := newPrinter(output)
defer t.MustFlush() defer t.MustFlush()
if config.Container == nil { if err := config.Validate(); err != nil {
mustPrint(output, "Warning: this configuration uses permissive defaults!\n\n") valid = false
if m, ok := message.GetMessage(err); ok {
mustPrint(output, "Error: "+m+"!\n\n")
}
}
if config == nil {
// nothing to print
return
} }
if instance != nil { if instance != nil {
t.Printf("State\n") t.Printf("State\n")
t.Printf(" Instance:\t%s (%d)\n", instance.ID.String(), instance.PID) t.Printf(" Instance:\t%s (%d -> %d)\n", instance.ID.String(), instance.PID, instance.ShimPID)
t.Printf(" Uptime:\t%s\n", now.Sub(instance.Time).Round(time.Second).String()) t.Printf(" Uptime:\t%s\n", now.Sub(instance.Time).Round(time.Second).String())
t.Printf("\n") t.Printf("\n")
} }
@ -73,39 +89,33 @@ func printShowInstance(
if len(config.Groups) > 0 { if len(config.Groups) > 0 {
t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", ")) t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", "))
} }
if config.Home != nil {
t.Printf(" Home:\t%s\n", config.Home)
}
if config.Container != nil { if config.Container != nil {
params := config.Container if config.Container.Home != nil {
if params.Hostname != "" { t.Printf(" Home:\t%s\n", config.Container.Home)
t.Printf(" Hostname:\t%s\n", params.Hostname)
} }
flags := make([]string, 0, 7) if config.Container.Hostname != "" {
writeFlag := func(name string, value bool) { t.Printf(" Hostname:\t%s\n", config.Container.Hostname)
if value {
flags = append(flags, name)
} }
} flags := config.Container.Flags.String()
writeFlag("userns", params.Userns)
writeFlag("devel", params.Devel)
writeFlag("net", params.HostNet)
writeFlag("abstract", params.HostAbstract)
writeFlag("device", params.Device)
writeFlag("tty", params.Tty)
writeFlag("mapuid", params.MapRealUID)
writeFlag("directwl", config.DirectWayland)
if len(flags) == 0 {
flags = append(flags, "none")
}
t.Printf(" Flags:\t%s\n", strings.Join(flags, " "))
if config.Path != nil { // this is included in the upper hst.Config struct but is relevant here
t.Printf(" Path:\t%s\n", config.Path) const flagDirectWayland = "directwl"
if config.DirectWayland {
// hardcoded value when every flag is unset
if flags == "none" {
flags = flagDirectWayland
} else {
flags += ", " + flagDirectWayland
} }
} }
if len(config.Args) > 0 { t.Printf(" Flags:\t%s\n", flags)
t.Printf(" Arguments:\t%s\n", strings.Join(config.Args, " "))
if config.Container.Path != nil {
t.Printf(" Path:\t%s\n", config.Container.Path)
}
if len(config.Container.Args) > 0 {
t.Printf(" Arguments:\t%s\n", strings.Join(config.Container.Args, " "))
}
} }
t.Printf("\n") t.Printf("\n")
@ -114,6 +124,7 @@ func printShowInstance(
t.Printf("Filesystem\n") t.Printf("Filesystem\n")
for _, f := range config.Container.Filesystem { for _, f := range config.Container.Filesystem {
if !f.Valid() { if !f.Valid() {
valid = false
t.Println(" <invalid>") t.Println(" <invalid>")
continue continue
} }
@ -123,17 +134,14 @@ func printShowInstance(
} }
if len(config.ExtraPerms) > 0 { if len(config.ExtraPerms) > 0 {
t.Printf("Extra ACL\n") t.Printf("Extra ACL\n")
for _, p := range config.ExtraPerms { for i := range config.ExtraPerms {
if p == nil { t.Printf(" %s\n", config.ExtraPerms[i].String())
continue
}
t.Printf(" %s\n", p.String())
} }
t.Printf("\n") t.Printf("\n")
} }
} }
printDBus := func(c *dbus.Config) { printDBus := func(c *hst.BusConfig) {
t.Printf(" Filter:\t%v\n", c.Filter) t.Printf(" Filter:\t%v\n", c.Filter)
if len(c.See) > 0 { if len(c.See) > 0 {
t.Printf(" See:\t%q\n", c.See) t.Printf(" See:\t%q\n", c.See)
@ -161,121 +169,106 @@ func printShowInstance(
printDBus(config.SystemBus) printDBus(config.SystemBus)
t.Printf("\n") t.Printf("\n")
} }
}
func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON bool) {
var entries state.Entries
if e, err := state.Join(s); err != nil {
log.Fatalf("cannot join store: %v", err)
} else {
entries = e
}
if err := s.Close(); err != nil {
log.Printf("cannot close store: %v", err)
}
if !short && flagJSON {
es := make(map[string]*state.State, len(entries))
for id, instance := range entries {
es[id.String()] = instance
}
printJSON(output, short, es)
return return
} }
// sort state entries by id string to ensure consistency between runs // printPs writes a representation of active instances to output.
exp := make([]*expandedStateEntry, 0, len(entries)) func printPs(msg message.Msg, output io.Writer, now time.Time, s *store.Store, short, flagJSON bool) {
for id, instance := range entries { f := func(a func(eh *store.EntryHandle)) {
// gracefully skip nil states entries, copyError := s.All()
if instance == nil { for eh := range entries {
log.Printf("got invalid state entry %s", id.String()) a(eh)
continue }
if err := copyError(); err != nil {
msg.GetLogger().Println(getMessage("cannot iterate over store:", err))
}
} }
// gracefully skip inconsistent states if short { // short output requires identifier only
if id != instance.ID { var identifiers []*hst.ID
log.Printf("possible store corruption: entry %s has id %s", f(func(eh *store.EntryHandle) {
id.String(), instance.ID.String()) if _, err := eh.Load(nil); err != nil { // passes through decode error
continue msg.GetLogger().Println(getMessage("cannot validate state entry header:", err))
return
} }
exp = append(exp, &expandedStateEntry{s: id.String(), State: instance}) identifiers = append(identifiers, &eh.ID)
} })
slices.SortFunc(exp, func(a, b *expandedStateEntry) int { return a.Time.Compare(b.Time) }) slices.SortFunc(identifiers, func(a, b *hst.ID) int { return bytes.Compare(a[:], b[:]) })
if short {
if flagJSON { if flagJSON {
v := make([]string, len(exp)) encodeJSON(log.Fatal, output, short, identifiers)
for i, e := range exp {
v[i] = e.s
}
printJSON(output, short, v)
} else { } else {
for _, e := range exp { for _, id := range identifiers {
mustPrintln(output, e.s[:8]) mustPrintln(output, shortIdentifier(id))
} }
} }
return return
} }
// long output requires full instance state
var instances []*hst.State
f(func(eh *store.EntryHandle) {
var state hst.State
if _, err := eh.Load(&state); err != nil { // passes through decode error
msg.GetLogger().Println(getMessage("cannot load state entry:", err))
return
}
instances = append(instances, &state)
})
slices.SortFunc(instances, func(a, b *hst.State) int { return bytes.Compare(a.ID[:], b.ID[:]) })
if flagJSON {
encodeJSON(log.Fatal, output, short, instances)
return
}
t := newPrinter(output) t := newPrinter(output)
defer t.MustFlush() defer t.MustFlush()
t.Println("\tInstance\tPID\tApplication\tUptime") t.Println("\tInstance\tPID\tApplication\tUptime")
for _, e := range exp { for _, instance := range instances {
if len(e.s) != 1<<5 {
// unreachable
log.Printf("possible store corruption: invalid instance string %s", e.s)
continue
}
as := "(No configuration information)" as := "(No configuration information)"
if e.Config != nil { if instance.Config != nil {
as = strconv.Itoa(e.Config.Identity) as = strconv.Itoa(instance.Config.Identity)
id := e.Config.ID id := instance.Config.ID
if id == "" { if id == "" {
id = "app.hakurei." + e.s[:8] id = "app.hakurei." + shortIdentifier(&instance.ID)
} }
as += " (" + id + ")" as += " (" + id + ")"
} }
t.Printf("\t%s\t%d\t%s\t%s\n", t.Printf("\t%s\t%d\t%s\t%s\n",
e.s[:8], e.PID, as, now.Sub(e.Time).Round(time.Second).String()) shortIdentifier(&instance.ID), instance.PID, as, now.Sub(instance.Time).Round(time.Second).String())
}
}
type expandedStateEntry struct {
s string
*state.State
}
func printJSON(output io.Writer, short bool, v any) {
encoder := json.NewEncoder(output)
if !short {
encoder.SetIndent("", " ")
}
if err := encoder.Encode(v); err != nil {
log.Fatalf("cannot serialise: %v", err)
} }
} }
// newPrinter returns a configured, wrapped [tabwriter.Writer].
func newPrinter(output io.Writer) *tp { return &tp{tabwriter.NewWriter(output, 0, 1, 4, ' ', 0)} } func newPrinter(output io.Writer) *tp { return &tp{tabwriter.NewWriter(output, 0, 1, 4, ' ', 0)} }
// tp wraps [tabwriter.Writer] to provide additional formatting methods.
type tp struct{ *tabwriter.Writer } type tp struct{ *tabwriter.Writer }
// Printf calls [fmt.Fprintf] on the underlying [tabwriter.Writer].
func (p *tp) Printf(format string, a ...any) { func (p *tp) Printf(format string, a ...any) {
if _, err := fmt.Fprintf(p, format, a...); err != nil { if _, err := fmt.Fprintf(p, format, a...); err != nil {
log.Fatalf("cannot write to tabwriter: %v", err) log.Fatalf("cannot write to tabwriter: %v", err)
} }
} }
// Println calls [fmt.Fprintln] on the underlying [tabwriter.Writer].
func (p *tp) Println(a ...any) { func (p *tp) Println(a ...any) {
if _, err := fmt.Fprintln(p, a...); err != nil { if _, err := fmt.Fprintln(p, a...); err != nil {
log.Fatalf("cannot write to tabwriter: %v", err) log.Fatalf("cannot write to tabwriter: %v", err)
} }
} }
// MustFlush calls the Flush method of [tabwriter.Writer] and calls [log.Fatalf] on a non-nil error.
func (p *tp) MustFlush() { func (p *tp) MustFlush() {
if err := p.Writer.Flush(); err != nil { if err := p.Writer.Flush(); err != nil {
log.Fatalf("cannot flush tabwriter: %v", err) log.Fatalf("cannot flush tabwriter: %v", err)
} }
} }
func mustPrint(output io.Writer, a ...any) { func mustPrint(output io.Writer, a ...any) {
if _, err := fmt.Fprint(output, a...); err != nil { if _, err := fmt.Fprint(output, a...); err != nil {
log.Fatalf("cannot print: %v", err) log.Fatalf("cannot print: %v", err)
@ -286,3 +279,11 @@ func mustPrintln(output io.Writer, a ...any) {
log.Fatalf("cannot print: %v", err) log.Fatalf("cannot print: %v", err)
} }
} }
// getMessage returns a [message.Error] message if available, or err prefixed with fallback otherwise.
func getMessage(fallback string, err error) string {
if m, ok := message.GetMessage(err); ok {
return m
}
return fmt.Sprintln(fallback, err)
}

View File

@ -1,47 +1,72 @@
package main package main
import ( import (
"bytes"
"log"
"strings" "strings"
"testing" "testing"
"time" "time"
"hakurei.app/container/check"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/app/state" "hakurei.app/internal/store"
"hakurei.app/system/dbus" "hakurei.app/message"
) )
var ( var (
testID = state.ID{ testID = hst.ID{
0x8e, 0x2c, 0x76, 0xb0, 0x8e, 0x2c, 0x76, 0xb0,
0x66, 0xda, 0xbe, 0x57, 0x66, 0xda, 0xbe, 0x57,
0x4c, 0xf0, 0x73, 0xbd, 0x4c, 0xf0, 0x73, 0xbd,
0xb4, 0x6e, 0xb5, 0xc1, 0xb4, 0x6e, 0xb5, 0xc1,
} }
testState = &state.State{ testState = hst.State{
ID: testID, ID: testID,
PID: 0xDEADBEEF, PID: 0xcafe,
ShimPID: 0xdead,
Config: hst.Template(), Config: hst.Template(),
Time: testAppTime, Time: testAppTime,
} }
testStateSmall = hst.State{
ID: (hst.ID)(bytes.Repeat([]byte{0xaa}, len(hst.ID{}))),
PID: 0xbeef,
ShimPID: 0xcafe,
Config: &hst.Config{
Enablements: hst.NewEnablements(hst.EWayland | hst.EPulse),
Identity: 1,
Container: &hst.ContainerConfig{
Shell: check.MustAbs("/bin/sh"),
Home: check.MustAbs("/data/data/uk.gensokyo.cat"),
Path: check.MustAbs("/usr/bin/cat"),
Args: []string{"cat"},
Flags: hst.FUserns,
},
},
Time: time.Unix(0, 0xdeadbeef).UTC(),
}
testTime = time.Unix(3752, 1).UTC() testTime = time.Unix(3752, 1).UTC()
testAppTime = time.Unix(0, 9).UTC() testAppTime = time.Unix(0, 9).UTC()
) )
func Test_printShowInstance(t *testing.T) { func TestPrintShowInstance(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
instance *state.State instance *hst.State
config *hst.Config config *hst.Config
short, json bool short, json bool
want string want string
valid bool
}{ }{
{"nil", nil, nil, false, false, "Error: invalid configuration!\n\n", false},
{"config", nil, hst.Template(), false, false, `App {"config", nil, hst.Template(), false, false, `App
Identity: 9 (org.chromium.Chromium) Identity: 9 (org.chromium.Chromium)
Enablements: wayland, dbus, pulseaudio Enablements: wayland, dbus, pulseaudio
Groups: video, dialout, plugdev Groups: video, dialout, plugdev
Home: /data/data/org.chromium.Chromium Home: /data/data/org.chromium.Chromium
Hostname: localhost Hostname: localhost
Flags: userns devel net abstract device tty mapuid Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir
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
@ -49,8 +74,7 @@ Filesystem
autoroot:w:/var/lib/hakurei/base/org.debian autoroot:w:/var/lib/hakurei/base/org.debian
autoetc:/etc/ autoetc:/etc/
w+ephemeral(-rwxr-xr-x):/tmp/ w+ephemeral(-rwxr-xr-x):/tmp/
w*/nix/store:/mnt-root/nix/.rw-store/upper:/mnt-root/nix/.rw-store/work:/mnt-root/nix/.ro-store w*/nix/store:/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper:/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work:/var/lib/hakurei/base/org.nixos/ro-store
*/nix/store
/run/current-system@ /run/current-system@
/run/opengl-driver@ /run/opengl-driver@
w-/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium w-/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
@ -71,21 +95,41 @@ System bus
Filter: true Filter: true
Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"] Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"]
`}, `, true},
{"config pd", nil, new(hst.Config), false, false, `Warning: this configuration uses permissive defaults! {"config pd", nil, new(hst.Config), false, false, `Error: configuration missing container state!
App App
Identity: 0 Identity: 0
Enablements: (no enablements) Enablements: (no enablements)
`}, `, false},
{"config flag none", nil, &hst.Config{Container: new(hst.ContainerConfig)}, false, false, `App {"config flag none", nil, &hst.Config{Container: new(hst.ContainerConfig)}, false, false, `Error: container configuration missing path to home directory!
App
Identity: 0 Identity: 0
Enablements: (no enablements) Enablements: (no enablements)
Flags: none Flags: none
`}, `, false},
{"config nil entries", nil, &hst.Config{Container: &hst.ContainerConfig{Filesystem: make([]hst.FilesystemConfigJSON, 1)}, ExtraPerms: make([]*hst.ExtraPermConfig, 1)}, false, false, `App {"config flag none directwl", nil, &hst.Config{DirectWayland: true, Container: new(hst.ContainerConfig)}, false, false, `Error: container configuration missing path to home directory!
App
Identity: 0
Enablements: (no enablements)
Flags: directwl
`, false},
{"config flag directwl", nil, &hst.Config{DirectWayland: true, Container: &hst.ContainerConfig{Flags: hst.FMultiarch}}, false, false, `Error: container configuration missing path to home directory!
App
Identity: 0
Enablements: (no enablements)
Flags: multiarch, directwl
`, false},
{"config nil entries", nil, &hst.Config{Container: &hst.ContainerConfig{Filesystem: make([]hst.FilesystemConfigJSON, 1)}, ExtraPerms: make([]hst.ExtraPermConfig, 1)}, false, false, `Error: container configuration missing path to home directory!
App
Identity: 0 Identity: 0
Enablements: (no enablements) Enablements: (no enablements)
Flags: none Flags: none
@ -94,9 +138,10 @@ Filesystem
<invalid> <invalid>
Extra ACL Extra ACL
<invalid>
`}, `, false},
{"config pd dbus see", nil, &hst.Config{SessionBus: &dbus.Config{See: []string{"org.example.test"}}}, false, false, `Warning: this configuration uses permissive defaults! {"config pd dbus see", nil, &hst.Config{SessionBus: &hst.BusConfig{See: []string{"org.example.test"}}}, false, false, `Error: configuration missing container state!
App App
Identity: 0 Identity: 0
@ -106,10 +151,10 @@ Session bus
Filter: false Filter: false
See: ["org.example.test"] See: ["org.example.test"]
`}, `, false},
{"instance", testState, hst.Template(), false, false, `State {"instance", &testState, hst.Template(), false, false, `State
Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (3735928559) Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (51966 -> 57005)
Uptime: 1h2m32s Uptime: 1h2m32s
App App
@ -118,7 +163,7 @@ App
Groups: video, dialout, plugdev Groups: video, dialout, plugdev
Home: /data/data/org.chromium.Chromium Home: /data/data/org.chromium.Chromium
Hostname: localhost Hostname: localhost
Flags: userns devel net abstract device tty mapuid Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir
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
@ -126,8 +171,7 @@ Filesystem
autoroot:w:/var/lib/hakurei/base/org.debian autoroot:w:/var/lib/hakurei/base/org.debian
autoetc:/etc/ autoetc:/etc/
w+ephemeral(-rwxr-xr-x):/tmp/ w+ephemeral(-rwxr-xr-x):/tmp/
w*/nix/store:/mnt-root/nix/.rw-store/upper:/mnt-root/nix/.rw-store/work:/mnt-root/nix/.ro-store w*/nix/store:/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper:/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work:/var/lib/hakurei/base/org.nixos/ro-store
*/nix/store
/run/current-system@ /run/current-system@
/run/opengl-driver@ /run/opengl-driver@
w-/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium w-/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
@ -148,51 +192,26 @@ System bus
Filter: true Filter: true
Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"] Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"]
`}, `, true},
{"instance pd", testState, new(hst.Config), false, false, `Warning: this configuration uses permissive defaults! {"instance pd", &testState, new(hst.Config), false, false, `Error: configuration missing container state!
State State
Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (3735928559) Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (51966 -> 57005)
Uptime: 1h2m32s Uptime: 1h2m32s
App App
Identity: 0 Identity: 0
Enablements: (no enablements) Enablements: (no enablements)
`}, `, false},
{"json nil", nil, nil, false, true, `null {"json nil", nil, nil, false, true, `null
`}, `, true},
{"json instance", testState, nil, false, true, `{ {"json instance", &testState, nil, false, true, `{
"instance": [ "instance": "8e2c76b066dabe574cf073bdb46eb5c1",
142, "pid": 51966,
44, "shim_pid": 57005,
118,
176,
102,
218,
190,
87,
76,
240,
115,
189,
180,
110,
181,
193
],
"pid": 3735928559,
"config": {
"id": "org.chromium.Chromium", "id": "org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"enablements": { "enablements": {
"wayland": true, "wayland": true,
"dbus": true, "dbus": true,
@ -234,9 +253,6 @@ App
"broadcast": null, "broadcast": null,
"filter": true "filter": true
}, },
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"extra_perms": [ "extra_perms": [
{ {
"ensure": true, "ensure": true,
@ -259,22 +275,11 @@ App
"container": { "container": {
"hostname": "localhost", "hostname": "localhost",
"wait_delay": -1, "wait_delay": -1,
"seccomp_flags": 1,
"seccomp_presets": 1,
"seccomp_compat": true,
"devel": true,
"userns": true,
"host_net": true,
"host_abstract": true,
"tty": true,
"multiarch": true,
"env": { "env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY", "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com", "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT" "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
}, },
"map_real_uid": true,
"device": true,
"filesystem": [ "filesystem": [
{ {
"type": "bind", "type": "bind",
@ -299,14 +304,10 @@ App
"type": "overlay", "type": "overlay",
"dst": "/nix/store", "dst": "/nix/store",
"lower": [ "lower": [
"/mnt-root/nix/.ro-store" "/var/lib/hakurei/base/org.nixos/ro-store"
], ],
"upper": "/mnt-root/nix/.rw-store/upper", "upper": "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper",
"work": "/mnt-root/nix/.rw-store/work" "work": "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work"
},
{
"type": "bind",
"src": "/nix/store"
}, },
{ {
"type": "link", "type": "link",
@ -333,22 +334,35 @@ App
"dev": true, "dev": true,
"optional": true "optional": true
} }
] ],
} "username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"seccomp_compat": true,
"devel": true,
"userns": true,
"host_net": true,
"host_abstract": true,
"tty": true,
"multiarch": true,
"map_real_uid": true,
"device": true,
"share_runtime": true,
"share_tmpdir": true
}, },
"time": "1970-01-01T00:00:00.000000009Z" "time": "1970-01-01T00:00:00.000000009Z"
} }
`}, `, true},
{"json config", nil, hst.Template(), false, true, `{ {"json config", nil, hst.Template(), false, true, `{
"id": "org.chromium.Chromium", "id": "org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"enablements": { "enablements": {
"wayland": true, "wayland": true,
"dbus": true, "dbus": true,
@ -390,9 +404,6 @@ App
"broadcast": null, "broadcast": null,
"filter": true "filter": true
}, },
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"extra_perms": [ "extra_perms": [
{ {
"ensure": true, "ensure": true,
@ -415,22 +426,11 @@ App
"container": { "container": {
"hostname": "localhost", "hostname": "localhost",
"wait_delay": -1, "wait_delay": -1,
"seccomp_flags": 1,
"seccomp_presets": 1,
"seccomp_compat": true,
"devel": true,
"userns": true,
"host_net": true,
"host_abstract": true,
"tty": true,
"multiarch": true,
"env": { "env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY", "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com", "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT" "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
}, },
"map_real_uid": true,
"device": true,
"filesystem": [ "filesystem": [
{ {
"type": "bind", "type": "bind",
@ -455,14 +455,10 @@ App
"type": "overlay", "type": "overlay",
"dst": "/nix/store", "dst": "/nix/store",
"lower": [ "lower": [
"/mnt-root/nix/.ro-store" "/var/lib/hakurei/base/org.nixos/ro-store"
], ],
"upper": "/mnt-root/nix/.rw-store/upper", "upper": "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper",
"work": "/mnt-root/nix/.rw-store/work" "work": "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work"
},
{
"type": "bind",
"src": "/nix/store"
}, },
{ {
"type": "link", "type": "link",
@ -489,76 +485,82 @@ App
"dev": true, "dev": true,
"optional": true "optional": true
} }
] ],
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"seccomp_compat": true,
"devel": true,
"userns": true,
"host_net": true,
"host_abstract": true,
"tty": true,
"multiarch": true,
"map_real_uid": true,
"device": true,
"share_runtime": true,
"share_tmpdir": true
} }
} }
`}, `, true},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
output := new(strings.Builder) output := new(strings.Builder)
printShowInstance(output, testTime, tc.instance, tc.config, tc.short, tc.json) gotValid := printShowInstance(output, testTime, tc.instance, tc.config, tc.short, tc.json)
if got := output.String(); got != tc.want { if got := output.String(); got != tc.want {
t.Errorf("printShowInstance: got\n%s\nwant\n%s", t.Errorf("printShowInstance: \n%s\nwant\n%s", got, tc.want)
got, tc.want)
return return
} }
if gotValid != tc.valid {
t.Errorf("printShowInstance: valid = %v, want %v", gotValid, tc.valid)
}
}) })
} }
} }
func Test_printPs(t *testing.T) { func TestPrintPs(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
entries state.Entries data []hst.State
short, json bool short, json bool
want string want, log string
}{ }{
{"no entries", make(state.Entries), false, false, " Instance PID Application Uptime\n"}, {"no entries", []hst.State{}, false, false, " Instance PID Application Uptime\n", ""},
{"no entries short", make(state.Entries), true, false, ""}, {"no entries short", []hst.State{}, true, false, "", ""},
{"nil instance", state.Entries{testID: nil}, false, false, " Instance PID Application Uptime\n"},
{"state corruption", state.Entries{state.ID{}: testState}, false, false, " Instance PID Application Uptime\n"},
{"valid pd", state.Entries{testID: &state.State{ID: testID, PID: 1 << 8, Config: new(hst.Config), Time: testAppTime}}, false, false, ` Instance PID Application Uptime {"invalid config", []hst.State{{ID: testID, PID: 1 << 8, Config: new(hst.Config), Time: testAppTime}}, false, false, " Instance PID Application Uptime\n", "check: configuration missing container state\n"},
8e2c76b0 256 0 (app.hakurei.8e2c76b0) 1h2m32s
`},
{"valid", state.Entries{testID: testState}, false, false, ` Instance PID Application Uptime {"valid", []hst.State{testStateSmall, testState}, false, false, ` Instance PID Application Uptime
8e2c76b0 3735928559 9 (org.chromium.Chromium) 1h2m32s 4cf073bd 51966 9 (org.chromium.Chromium) 1h2m32s
`}, aaaaaaaa 48879 1 (app.hakurei.aaaaaaaa) 1h2m28s
{"valid short", state.Entries{testID: testState}, true, false, "8e2c76b0\n"}, `, ""},
{"valid json", state.Entries{testID: testState}, false, true, `{ {"valid single", []hst.State{testState}, false, false, ` Instance PID Application Uptime
"8e2c76b066dabe574cf073bdb46eb5c1": { 4cf073bd 51966 9 (org.chromium.Chromium) 1h2m32s
"instance": [ `, ""},
142,
44, {"valid short", []hst.State{testStateSmall, testState}, true, false, "4cf073bd\naaaaaaaa\n", ""},
118, {"valid short single", []hst.State{testState}, true, false, "4cf073bd\n", ""},
176,
102, {"valid json", []hst.State{testState, testStateSmall}, false, true, `[
218, {
190, "instance": "8e2c76b066dabe574cf073bdb46eb5c1",
87, "pid": 51966,
76, "shim_pid": 57005,
240,
115,
189,
180,
110,
181,
193
],
"pid": 3735928559,
"config": {
"id": "org.chromium.Chromium", "id": "org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"enablements": { "enablements": {
"wayland": true, "wayland": true,
"dbus": true, "dbus": true,
@ -600,9 +602,6 @@ func Test_printPs(t *testing.T) {
"broadcast": null, "broadcast": null,
"filter": true "filter": true
}, },
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"extra_perms": [ "extra_perms": [
{ {
"ensure": true, "ensure": true,
@ -625,22 +624,11 @@ func Test_printPs(t *testing.T) {
"container": { "container": {
"hostname": "localhost", "hostname": "localhost",
"wait_delay": -1, "wait_delay": -1,
"seccomp_flags": 1,
"seccomp_presets": 1,
"seccomp_compat": true,
"devel": true,
"userns": true,
"host_net": true,
"host_abstract": true,
"tty": true,
"multiarch": true,
"env": { "env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY", "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com", "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT" "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
}, },
"map_real_uid": true,
"device": true,
"filesystem": [ "filesystem": [
{ {
"type": "bind", "type": "bind",
@ -665,14 +653,10 @@ func Test_printPs(t *testing.T) {
"type": "overlay", "type": "overlay",
"dst": "/nix/store", "dst": "/nix/store",
"lower": [ "lower": [
"/mnt-root/nix/.ro-store" "/var/lib/hakurei/base/org.nixos/ro-store"
], ],
"upper": "/mnt-root/nix/.rw-store/upper", "upper": "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper",
"work": "/mnt-root/nix/.rw-store/work" "work": "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work"
},
{
"type": "bind",
"src": "/nix/store"
}, },
{ {
"type": "link", "type": "link",
@ -699,34 +683,95 @@ func Test_printPs(t *testing.T) {
"dev": true, "dev": true,
"optional": true "optional": true
} }
] ],
} "username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"home": "/data/data/org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"seccomp_compat": true,
"devel": true,
"userns": true,
"host_net": true,
"host_abstract": true,
"tty": true,
"multiarch": true,
"map_real_uid": true,
"device": true,
"share_runtime": true,
"share_tmpdir": true
}, },
"time": "1970-01-01T00:00:00.000000009Z" "time": "1970-01-01T00:00:00.000000009Z"
},
{
"instance": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"pid": 48879,
"shim_pid": 51966,
"enablements": {
"wayland": true,
"pulse": true
},
"identity": 1,
"groups": null,
"container": {
"env": null,
"filesystem": null,
"shell": "/bin/sh",
"home": "/data/data/uk.gensokyo.cat",
"path": "/usr/bin/cat",
"args": [
"cat"
],
"userns": true,
"map_real_uid": false
},
"time": "1970-01-01T00:00:03.735928559Z"
} }
} ]
`}, `, ""},
{"valid short json", state.Entries{testID: testState}, true, true, `["8e2c76b066dabe574cf073bdb46eb5c1"] {"valid short json", []hst.State{testStateSmall, testState}, true, true, `["8e2c76b066dabe574cf073bdb46eb5c1","aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]
`}, `, ""},
} }
for _, tc := range testCases { for _, tc := range testCases {
s := store.New(check.MustAbs(t.TempDir()).Append("store"))
for i := range tc.data {
if h, err := s.Handle(tc.data[i].Identity); err != nil {
t.Fatalf("Handle: error = %v", err)
} else {
var unlock func()
if unlock, err = h.Lock(); err != nil {
t.Fatalf("Lock: error = %v", err)
}
_, err = h.Save(&tc.data[i])
unlock()
if err != nil {
t.Fatalf("Save: error = %v", err)
}
}
}
// store must not be written to beyond this point
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
output := new(strings.Builder) t.Parallel()
printPs(output, testTime, stubStore(tc.entries), tc.short, tc.json)
if got := output.String(); got != tc.want { var printBuf, logBuf bytes.Buffer
t.Errorf("printPs: got\n%s\nwant\n%s", msg := message.New(log.New(&logBuf, "check: ", 0))
got, tc.want) msg.SwapVerbose(true)
printPs(msg, &printBuf, testTime, s, tc.short, tc.json)
if got := printBuf.String(); got != tc.want {
t.Errorf("printPs:\n%s\nwant\n%s", got, tc.want)
return return
} }
if got := logBuf.String(); got != tc.log {
t.Errorf("msg:\n%s\nwant\n%s", got, tc.log)
}
}) })
} }
} }
// stubStore implements [state.Store] and returns test samples via [state.Joiner].
type stubStore state.Entries
func (s stubStore) Join() (state.Entries, error) { return state.Entries(s), nil }
func (s stubStore) Do(int, func(c state.Cursor)) (bool, error) { panic("unreachable") }
func (s stubStore) List() ([]int, error) { panic("unreachable") }
func (s stubStore) Close() error { return nil }

View File

@ -5,10 +5,9 @@ import (
"log" "log"
"os" "os"
"hakurei.app/container" "hakurei.app/container/check"
"hakurei.app/container/seccomp" "hakurei.app/container/fhs"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/system/dbus"
) )
type appInfo struct { type appInfo struct {
@ -38,9 +37,9 @@ type appInfo struct {
// passed through to [hst.Config] // passed through to [hst.Config]
DirectWayland bool `json:"direct_wayland,omitempty"` DirectWayland bool `json:"direct_wayland,omitempty"`
// passed through to [hst.Config] // passed through to [hst.Config]
SystemBus *dbus.Config `json:"system_bus,omitempty"` SystemBus *hst.BusConfig `json:"system_bus,omitempty"`
// passed through to [hst.Config] // passed through to [hst.Config]
SessionBus *dbus.Config `json:"session_bus,omitempty"` SessionBus *hst.BusConfig `json:"session_bus,omitempty"`
// passed through to [hst.Config] // passed through to [hst.Config]
Enablements *hst.Enablements `json:"enablements,omitempty"` Enablements *hst.Enablements `json:"enablements,omitempty"`
@ -56,69 +55,82 @@ type appInfo struct {
// store path to nixGL source // store path to nixGL source
NixGL string `json:"nix_gl,omitempty"` NixGL string `json:"nix_gl,omitempty"`
// store path to activate-and-exec script // store path to activate-and-exec script
Launcher *container.Absolute `json:"launcher"` Launcher *check.Absolute `json:"launcher"`
// store path to /run/current-system // store path to /run/current-system
CurrentSystem *container.Absolute `json:"current_system"` CurrentSystem *check.Absolute `json:"current_system"`
// store path to home-manager activation package // store path to home-manager activation package
ActivationPackage string `json:"activation_package"` ActivationPackage string `json:"activation_package"`
} }
func (app *appInfo) toHst(pathSet *appPathSet, pathname *container.Absolute, argv []string, flagDropShell bool) *hst.Config { func (app *appInfo) toHst(pathSet *appPathSet, pathname *check.Absolute, argv []string, flagDropShell bool) *hst.Config {
config := &hst.Config{ config := &hst.Config{
ID: app.ID, ID: app.ID,
Path: pathname,
Args: argv,
Enablements: app.Enablements, Enablements: app.Enablements,
SystemBus: app.SystemBus, SystemBus: app.SystemBus,
SessionBus: app.SessionBus, SessionBus: app.SessionBus,
DirectWayland: app.DirectWayland, DirectWayland: app.DirectWayland,
Username: "hakurei",
Shell: pathShell,
Home: pathDataData.Append(app.ID),
Identity: app.Identity, Identity: app.Identity,
Groups: app.Groups, Groups: app.Groups,
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name), Hostname: formatHostname(app.Name),
Devel: app.Devel,
Userns: app.Userns,
HostNet: app.HostNet,
HostAbstract: app.HostAbstract,
Device: app.Device,
Tty: app.Tty || flagDropShell,
MapRealUID: app.MapRealUID,
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}}, {FilesystemConfig: &hst.FSBind{Target: fhs.AbsEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append("store"), Target: pathNixStore}}, {FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append("store"), Target: pathNixStore}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}}, {FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}}, {FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}}, {FilesystemConfig: &hst.FSLink{Target: fhs.AbsUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.metaPath, Target: hst.AbsTmp.Append("app")}}, {FilesystemConfig: &hst.FSBind{Source: pathSet.metaPath, Target: hst.AbsPrivateTmp.Append("app")}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSEtc.Append("resolv.conf"), Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsEtc.Append("resolv.conf"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("block"), Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("block"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("bus"), Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("bus"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("class"), Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("class"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("dev"), Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("dev"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("devices"), Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("devices"), Optional: true}},
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID), Source: pathSet.homeDir, Write: true, Ensure: true}}, {FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID), Source: pathSet.homeDir, Write: true, Ensure: true}},
}, },
Username: "hakurei",
Shell: pathShell,
Home: pathDataData.Append(app.ID),
Path: pathname,
Args: argv,
}, },
ExtraPerms: []*hst.ExtraPermConfig{ ExtraPerms: []hst.ExtraPermConfig{
{Path: dataHome, Execute: true}, {Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true}, {Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
}, },
} }
if app.Devel {
config.Container.Flags |= hst.FDevel
}
if app.Userns {
config.Container.Flags |= hst.FUserns
}
if app.HostNet {
config.Container.Flags |= hst.FHostNet
}
if app.HostAbstract {
config.Container.Flags |= hst.FHostAbstract
}
if app.Device {
config.Container.Flags |= hst.FDevice
}
if app.Tty || flagDropShell {
config.Container.Flags |= hst.FTty
}
if app.MapRealUID {
config.Container.Flags |= hst.FMapRealUID
}
if app.Multiarch { if app.Multiarch {
config.Container.SeccompFlags |= seccomp.AllowMultiarch config.Container.Flags |= hst.FMultiarch
}
if app.Bluetooth {
config.Container.SeccompFlags |= seccomp.AllowBluetooth
} }
config.Container.Flags |= hst.FShareRuntime | hst.FShareTmpdir
return config return config
} }

View File

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

View File

@ -7,36 +7,37 @@ import (
"strconv" "strconv"
"sync/atomic" "sync/atomic"
"hakurei.app/container" "hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/hlog" "hakurei.app/message"
) )
const bash = "bash" const bash = "bash"
var ( var (
dataHome *container.Absolute dataHome *check.Absolute
) )
func init() { func init() {
// dataHome // dataHome
if a, err := container.NewAbs(os.Getenv("HAKUREI_DATA_HOME")); err == nil { if a, err := check.NewAbs(os.Getenv("HAKUREI_DATA_HOME")); err == nil {
dataHome = a dataHome = a
} else { } else {
dataHome = container.AbsFHSVarLib.Append("hakurei/" + strconv.Itoa(os.Getuid())) dataHome = fhs.AbsVarLib.Append("hakurei/" + strconv.Itoa(os.Getuid()))
} }
} }
var ( var (
pathBin = container.AbsFHSRoot.Append("bin") pathBin = fhs.AbsRoot.Append("bin")
pathNix = container.MustAbs("/nix/") pathNix = check.MustAbs("/nix/")
pathNixStore = pathNix.Append("store/") pathNixStore = pathNix.Append("store/")
pathCurrentSystem = container.AbsFHSRun.Append("current-system") pathCurrentSystem = fhs.AbsRun.Append("current-system")
pathSwBin = pathCurrentSystem.Append("sw/bin/") pathSwBin = pathCurrentSystem.Append("sw/bin/")
pathShell = pathSwBin.Append(bash) pathShell = pathSwBin.Append(bash)
pathData = container.MustAbs("/data") pathData = check.MustAbs("/data")
pathDataData = pathData.Append("data") pathDataData = pathData.Append("data")
) )
@ -51,8 +52,8 @@ func lookPath(file string) string {
var beforeRunFail = new(atomic.Pointer[func()]) var beforeRunFail = new(atomic.Pointer[func()])
func mustRun(name string, arg ...string) { func mustRun(msg message.Msg, name string, arg ...string) {
hlog.Verbosef("spawning process: %q %q", name, arg) msg.Verbosef("spawning process: %q %q", name, arg)
cmd := exec.Command(name, arg...) cmd := exec.Command(name, arg...)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
@ -65,15 +66,15 @@ func mustRun(name string, arg ...string) {
type appPathSet struct { type appPathSet struct {
// ${dataHome}/${id} // ${dataHome}/${id}
baseDir *container.Absolute baseDir *check.Absolute
// ${baseDir}/app // ${baseDir}/app
metaPath *container.Absolute metaPath *check.Absolute
// ${baseDir}/files // ${baseDir}/files
homeDir *container.Absolute homeDir *check.Absolute
// ${baseDir}/cache // ${baseDir}/cache
cacheDir *container.Absolute cacheDir *check.Absolute
// ${baseDir}/cache/nix // ${baseDir}/cache/nix
nixPath *container.Absolute nixPath *check.Absolute
} }
func pathSetByApp(id string) *appPathSet { func pathSetByApp(id string) *appPathSet {
@ -89,28 +90,28 @@ func pathSetByApp(id string) *appPathSet {
func appendGPUFilesystem(config *hst.Config) { func appendGPUFilesystem(config *hst.Config) {
config.Container.Filesystem = append(config.Container.Filesystem, []hst.FilesystemConfigJSON{ config.Container.Filesystem = append(config.Container.Filesystem, []hst.FilesystemConfigJSON{
// flatpak commit 763a686d874dd668f0236f911de00b80766ffe79 // flatpak commit 763a686d874dd668f0236f911de00b80766ffe79
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("dri"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("dri"), Device: true, Optional: true}},
// mali // mali
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("mali"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("mali"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("mali0"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("mali0"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("umplock"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("umplock"), Device: true, Optional: true}},
// nvidia // nvidia
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidiactl"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidiactl"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia-modeset"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia-modeset"), Device: true, Optional: true}},
// nvidia OpenCL/CUDA // nvidia OpenCL/CUDA
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia-uvm"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia-uvm"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia-uvm-tools"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia-uvm-tools"), Device: true, Optional: true}},
// flatpak commit d2dff2875bb3b7e2cd92d8204088d743fd07f3ff // flatpak commit d2dff2875bb3b7e2cd92d8204088d743fd07f3ff
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia0"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia1"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia0"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia1"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia2"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia3"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia2"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia3"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia4"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia5"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia4"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia5"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia6"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia7"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia6"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia7"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia8"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia9"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia8"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia9"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia10"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia11"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia10"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia11"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia12"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia13"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia12"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia13"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia14"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia15"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia14"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia15"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia16"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia17"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia16"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia17"), Device: true, Optional: true}},
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia18"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia19"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia18"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: fhs.AbsDev.Append("nvidia19"), Device: true, Optional: true}},
}...) }...)
} }

View File

@ -11,12 +11,12 @@ import (
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal" "hakurei.app/internal"
"hakurei.app/internal/hlog" "hakurei.app/message"
) )
var hakureiPath = internal.MustHakureiPath() var hakureiPathVal = internal.MustHakureiPath().String()
func mustRunApp(ctx context.Context, config *hst.Config, beforeFail func()) { func mustRunApp(ctx context.Context, msg message.Msg, config *hst.Config, beforeFail func()) {
var ( var (
cmd *exec.Cmd cmd *exec.Cmd
st io.WriteCloser st io.WriteCloser
@ -26,10 +26,10 @@ func mustRunApp(ctx context.Context, config *hst.Config, beforeFail func()) {
beforeFail() beforeFail()
log.Fatalf("cannot pipe: %v", err) log.Fatalf("cannot pipe: %v", err)
} else { } else {
if hlog.Load() { if msg.IsVerbose() {
cmd = exec.CommandContext(ctx, hakureiPath, "-v", "app", "3") cmd = exec.CommandContext(ctx, hakureiPathVal, "-v", "app", "3")
} else { } else {
cmd = exec.CommandContext(ctx, hakureiPath, "app", "3") cmd = exec.CommandContext(ctx, hakureiPathVal, "app", "3")
} }
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.ExtraFiles = []*os.File{r} cmd.ExtraFiles = []*os.File{r}
@ -51,7 +51,8 @@ func mustRunApp(ctx context.Context, config *hst.Config, beforeFail func()) {
var exitError *exec.ExitError var exitError *exec.ExitError
if errors.As(err, &exitError) { if errors.As(err, &exitError) {
beforeFail() beforeFail()
internal.Exit(exitError.ExitCode()) msg.BeforeExit()
os.Exit(exitError.ExitCode())
} else { } else {
beforeFail() beforeFail()
log.Fatalf("cannot wait: %v", err) log.Fatalf("cannot wait: %v", err)

View File

@ -58,15 +58,13 @@ def check_state(name, enablements):
instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei --json ps")) instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei --json ps"))
if len(instances) != 1: if len(instances) != 1:
raise Exception(f"unexpected state length {len(instances)}") raise Exception(f"unexpected state length {len(instances)}")
instance = next(iter(instances.values())) instance = instances[0]
config = instance['config'] if len(instance['container']['args']) != 1 or not (instance['container']['args'][0].startswith("/nix/store/")) or f"hakurei-{name}-" not in (instance['container']['args'][0]):
raise Exception(f"unexpected args {instance['container']['args']}")
if len(config['args']) != 1 or not (config['args'][0].startswith("/nix/store/")) or f"hakurei-{name}-" not in (config['args'][0]): if instance['enablements'] != enablements:
raise Exception(f"unexpected args {instance['config']['args']}") raise Exception(f"unexpected enablements {instance['enablements']}")
if config['enablements'] != enablements:
raise Exception(f"unexpected enablements {instance['config']['enablements']}")
start_all() start_all()
@ -94,15 +92,19 @@ machine.wait_for_file("/tmp/hakurei.0/tmpdir/2/success-client")
collect_state_ui("app_wayland") collect_state_ui("app_wayland")
check_state("foot", {"wayland": True, "dbus": True, "pulse": True}) 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 10002"))
machine.send_chars("exit\n") machine.send_chars("exit\n")
machine.wait_until_fails("pgrep foot") machine.wait_until_fails("pgrep foot")
# Verify acl cleanup on XDG_RUNTIME_DIR: # Verify acl cleanup on XDG_RUNTIME_DIR:
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002") machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 10002")
# Exit Sway and verify process exit status 0: # Exit Sway and verify process exit status 0:
swaymsg("exit", succeed=False) swaymsg("exit", succeed=False)
machine.wait_for_file("/tmp/sway-exit-ok") machine.wait_for_file("/tmp/sway-exit-ok")
# Print hakurei runDir contents: # Print hakurei share and rundir contents:
print(machine.succeed("find /tmp/hakurei.0 "
+ "-path '/tmp/hakurei.0/runtime/*/*' -prune -o "
+ "-path '/tmp/hakurei.0/tmpdir/*/*' -prune -o "
+ "-print"))
print(machine.succeed("find /run/user/1000/hakurei")) print(machine.succeed("find /run/user/1000/hakurei"))

View File

@ -2,22 +2,55 @@ package main
import ( import (
"context" "context"
"os"
"strings" "strings"
"hakurei.app/container" "hakurei.app/container/check"
"hakurei.app/container/seccomp" "hakurei.app/container/fhs"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal" "hakurei.app/message"
) )
func withNixDaemon( func withNixDaemon(
ctx context.Context, ctx context.Context,
msg message.Msg,
action string, command []string, net bool, updateConfig func(config *hst.Config) *hst.Config, action string, command []string, net bool, updateConfig func(config *hst.Config) *hst.Config,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(), app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
) { ) {
mustRunAppDropShell(ctx, updateConfig(&hst.Config{ flags := hst.FMultiarch | hst.FUserns // nix sandbox requires userns
if net {
flags |= hst.FHostNet
}
if dropShell {
flags |= hst.FTty
}
mustRunAppDropShell(ctx, msg, updateConfig(&hst.Config{
ID: app.ID, ID: app.ID,
ExtraPerms: []hst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
},
Identity: app.Identity,
Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action,
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: fhs.AbsEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath, Target: pathNix, Write: true}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: fhs.AbsUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID), Source: pathSet.homeDir, Write: true, Ensure: true}},
},
Username: "hakurei",
Shell: pathShell,
Home: pathDataData.Append(app.ID),
Path: pathShell, Path: pathShell,
Args: []string{bash, "-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
@ -31,48 +64,26 @@ func withNixDaemon(
" && pkill nix-daemon", " && pkill nix-daemon",
}, },
Username: "hakurei", Flags: flags,
Shell: pathShell,
Home: pathDataData.Append(app.ID),
ExtraPerms: []*hst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
},
Identity: app.Identity,
Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action,
Userns: true, // nix sandbox requires userns
HostNet: net,
SeccompFlags: seccomp.AllowMultiarch,
Tty: dropShell,
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath, Target: pathNix, Write: true}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID), Source: pathSet.homeDir, Write: true, Ensure: true}},
},
}, },
}), dropShell, beforeFail) }), dropShell, beforeFail)
} }
func withCacheDir( func withCacheDir(
ctx context.Context, ctx context.Context,
action string, command []string, workDir *container.Absolute, msg message.Msg,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) { action string, command []string, workDir *check.Absolute,
mustRunAppDropShell(ctx, &hst.Config{ app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
) {
flags := hst.FMultiarch
if dropShell {
flags |= hst.FTty
}
mustRunAppDropShell(ctx, msg, &hst.Config{
ID: app.ID, ID: app.ID,
Path: pathShell, ExtraPerms: []hst.ExtraPermConfig{
Args: []string{bash, "-lc", strings.Join(command, " && ")},
Username: "nixos",
Shell: pathShell,
Home: pathDataData.Append(app.ID, "cache"),
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},
{Path: workDir, Execute: true}, {Path: workDir, Execute: true},
@ -82,27 +93,38 @@ func withCacheDir(
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action, Hostname: formatHostname(app.Name) + "-" + action,
SeccompFlags: seccomp.AllowMultiarch,
Tty: dropShell,
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: workDir.Append(container.FHSEtc), Special: true}}, {FilesystemConfig: &hst.FSBind{Target: fhs.AbsEtc, Source: workDir.Append(fhs.Etc), Special: true}},
{FilesystemConfig: &hst.FSBind{Source: workDir.Append("nix"), Target: pathNix}}, {FilesystemConfig: &hst.FSBind{Source: workDir.Append("nix"), Target: pathNix}},
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}}, {FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}}, {FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}}, {FilesystemConfig: &hst.FSLink{Target: fhs.AbsUsrBin, Linkname: pathSwBin.String()}},
{FilesystemConfig: &hst.FSBind{Source: workDir, Target: hst.AbsTmp.Append("bundle")}}, {FilesystemConfig: &hst.FSBind{Source: workDir, Target: hst.AbsPrivateTmp.Append("bundle")}},
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID, "cache"), Source: pathSet.cacheDir, Write: true, Ensure: true}}, {FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID, "cache"), Source: pathSet.cacheDir, Write: true, Ensure: true}},
}, },
Username: "nixos",
Shell: pathShell,
Home: pathDataData.Append(app.ID, "cache"),
Path: pathShell,
Args: []string{bash, "-lc", strings.Join(command, " && ")},
Flags: flags,
}, },
}, dropShell, beforeFail) }, dropShell, beforeFail)
} }
func mustRunAppDropShell(ctx context.Context, config *hst.Config, dropShell bool, beforeFail func()) { func mustRunAppDropShell(ctx context.Context, msg message.Msg, config *hst.Config, dropShell bool, beforeFail func()) {
if dropShell { if dropShell {
config.Args = []string{bash, "-l"} if config.Container != nil {
mustRunApp(ctx, config, beforeFail) config.Container.Args = []string{bash, "-l"}
}
mustRunApp(ctx, msg, config, beforeFail)
beforeFail() beforeFail()
internal.Exit(0) msg.BeforeExit()
os.Exit(0)
} }
mustRunApp(ctx, config, beforeFail) mustRunApp(ctx, msg, config, beforeFail)
} }

16
cmd/hsu/hst.go Normal file
View File

@ -0,0 +1,16 @@
package main
/* copied from hst and must never be changed */
const (
userOffset = 100000
rangeSize = userOffset / 10
identityStart = 0
identityEnd = appEnd - appStart
appStart = rangeSize * 1
appEnd = appStart + rangeSize - 1
)
func toUser(userid, appid uint32) uint32 { return userid*userOffset + appStart + appid }

View File

@ -1,11 +1,14 @@
package main package main
// minimise imports to avoid inadvertently calling init or global variable functions
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"log" "log"
"os" "os"
"path" "path"
"runtime"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
@ -13,15 +16,23 @@ import (
) )
const ( const (
hsuConfFile = "/etc/hsurc" // envIdentity is the name of the environment variable holding a
// single byte representing the shim setup pipe file descriptor.
envShim = "HAKUREI_SHIM" envShim = "HAKUREI_SHIM"
envIdentity = "HAKUREI_IDENTITY" // envGroups holds a ' ' separated list of string representations of
// supplementary group gid. Membership requirements are enforced.
envGroups = "HAKUREI_GROUPS" envGroups = "HAKUREI_GROUPS"
PR_SET_NO_NEW_PRIVS = 0x26
) )
// hakureiPath is the absolute path to Hakurei.
//
// This is set by the linker.
var hakureiPath string
func main() { func main() {
const PR_SET_NO_NEW_PRIVS = 0x26
runtime.LockOSThread()
log.SetFlags(0) log.SetFlags(0)
log.SetPrefix("hsu: ") log.SetPrefix("hsu: ")
log.SetOutput(os.Stderr) log.SetOutput(os.Stderr)
@ -29,31 +40,34 @@ func main() {
if os.Geteuid() != 0 { if os.Geteuid() != 0 {
log.Fatal("this program must be owned by uid 0 and have the setuid bit set") log.Fatal("this program must be owned by uid 0 and have the setuid bit set")
} }
if os.Getegid() != os.Getgid() {
log.Fatal("this program must not have the setgid bit set")
}
puid := os.Getuid() puid := os.Getuid()
if puid == 0 { if puid == 0 {
log.Fatal("this program must not be started by root") log.Fatal("this program must not be started by root")
} }
if !path.IsAbs(hakureiPath) {
log.Fatal("this program is compiled incorrectly")
return
}
var toolPath string var toolPath string
pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe") pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
if p, err := os.Readlink(pexe); err != nil { if p, err := os.Readlink(pexe); err != nil {
log.Fatalf("cannot read parent executable path: %v", err) log.Fatalf("cannot read parent executable path: %v", err)
} else if strings.HasSuffix(p, " (deleted)") { } else if strings.HasSuffix(p, " (deleted)") {
log.Fatal("hakurei executable has been deleted") log.Fatal("hakurei executable has been deleted")
} else if p != mustCheckPath(hmain) { } else if p != hakureiPath {
log.Fatal("this program must be started by hakurei") log.Fatal("this program must be started by hakurei")
} else { } else {
toolPath = p toolPath = p
} }
// uid = 1000000 +
// id * 10000 +
// identity
uid := 1000000
// refuse to run if hsurc is not protected correctly // refuse to run if hsurc is not protected correctly
if s, err := os.Stat(hsuConfFile); err != nil { if s, err := os.Stat(hsuConfPath); err != nil {
log.Fatal(err) log.Fatal(err)
} else if s.Mode().Perm() != 0400 { } else if s.Mode().Perm() != 0400 {
log.Fatal("bad hsurc perm") log.Fatal("bad hsurc perm")
@ -62,25 +76,13 @@ func main() {
} }
// authenticate before accepting user input // authenticate before accepting user input
var id int userid := mustParseConfig(puid)
if f, err := os.Open(hsuConfFile); err != nil {
log.Fatal(err)
} else if v, ok := mustParseConfig(f, puid); !ok {
log.Fatalf("uid %d is not in the hsurc file", puid)
} else {
id = v
if err = f.Close(); err != nil {
log.Fatal(err)
}
uid += id * 10000
}
// pass through setup fd to shim // pass through setup fd to shim
var shimSetupFd string var shimSetupFd string
if s, ok := os.LookupEnv(envShim); !ok { if s, ok := os.LookupEnv(envShim); !ok {
// hakurei requests hsurc user id // hakurei requests hsurc user id
fmt.Print(id) fmt.Print(userid)
os.Exit(0) os.Exit(0)
} else if len(s) != 1 || s[0] > '9' || s[0] < '3' { } else if len(s) != 1 || s[0] > '9' || s[0] < '3' {
log.Fatal("HAKUREI_SHIM holds an invalid value") log.Fatal("HAKUREI_SHIM holds an invalid value")
@ -88,13 +90,22 @@ func main() {
shimSetupFd = s shimSetupFd = s
} }
// allowed identity range 0 to 9999 // start is going ahead at this point
if as, ok := os.LookupEnv(envIdentity); !ok { identity := mustReadIdentity()
log.Fatal("HAKUREI_IDENTITY not set")
} else if identity, err := parseUint32Fast(as); err != nil || identity < 0 || identity > 9999 { const (
log.Fatal("invalid identity") // first possible uid outcome
} else { uidStart = 10000
uid += identity // last possible uid outcome
uidEnd = 999919999
)
// cast to int for use with library functions
uid := int(toUser(userid, identity))
// final bounds check to catch any bugs
if uid < uidStart || uid >= uidEnd {
panic("uid out of bounds")
} }
// supplementary groups // supplementary groups
@ -124,11 +135,6 @@ func main() {
suppGroups = []int{uid} suppGroups = []int{uid}
} }
// final bounds check to catch any bugs
if uid < 1000000 || uid >= 2000000 {
panic("uid out of bounds")
}
// careful! users in the allowlist is effectively allowed to drop groups via hsu // careful! users in the allowlist is effectively allowed to drop groups via hsu
if err := syscall.Setresgid(uid, uid, uid); err != nil { if err := syscall.Setresgid(uid, uid, uid); err != nil {

View File

@ -19,5 +19,5 @@ buildGoModule {
ldflags = lib.attrsets.foldlAttrs ( ldflags = lib.attrsets.foldlAttrs (
ldflags: name: value: ldflags: name: value:
ldflags ++ [ "-X main.${name}=${value}" ] ldflags ++ [ "-X main.${name}=${value}" ]
) [ "-s -w" ] { hmain = "${hakurei}/libexec/hakurei"; }; ) [ "-s -w" ] { hakureiPath = "${hakurei}/libexec/hakurei"; };
} }

View File

@ -6,62 +6,128 @@ import (
"fmt" "fmt"
"io" "io"
"log" "log"
"math"
"os"
"strings" "strings"
) )
func parseUint32Fast(s string) (int, error) { const (
// useridStart is the first userid.
useridStart = 0
// useridEnd is the last userid.
useridEnd = useridStart + rangeSize - 1
)
// parseUint32Fast parses a string representation of an unsigned 32-bit integer value
// using the fast path only. This limits the range of values it is defined in.
func parseUint32Fast(s string) (uint32, error) {
sLen := len(s) sLen := len(s)
if sLen < 1 { if sLen < 1 {
return -1, errors.New("zero length string") return 0, errors.New("zero length string")
} }
if sLen > 10 { if sLen > 10 {
return -1, errors.New("string too long") return 0, errors.New("string too long")
} }
n := 0 var n uint32
for i, ch := range []byte(s) { for i, ch := range []byte(s) {
ch -= '0' ch -= '0'
if ch > 9 { if ch > 9 {
return -1, fmt.Errorf("invalid character '%s' at index %d", string(ch+'0'), i) return 0, fmt.Errorf("invalid character '%s' at index %d", string(ch+'0'), i)
} }
n = n*10 + int(ch) n = n*10 + uint32(ch)
} }
return n, nil return n, nil
} }
func parseConfig(r io.Reader, puid int) (fid int, ok bool, err error) { // parseConfig reads a list of allowed users from r until it encounters puid or [io.EOF].
//
// Each line of the file specifies a hakurei userid to kernel uid mapping. A line consists
// of the string representation of the uid of the user wishing to start hakurei containers,
// followed by a space, followed by the string representation of its userid. Duplicate uid
// entries are ignored, with the first occurrence taking effect.
//
// All string representations are parsed by calling parseUint32Fast.
func parseConfig(r io.Reader, puid uint32) (userid uint32, ok bool, err error) {
s := bufio.NewScanner(r) s := bufio.NewScanner(r)
var line, puid0 int var (
line uintptr
puid0 uint32
)
for s.Scan() { for s.Scan() {
line++ line++
// <puid> <fid> // <puid> <userid>
lf := strings.SplitN(s.Text(), " ", 2) lf := strings.SplitN(s.Text(), " ", 2)
if len(lf) != 2 { if len(lf) != 2 {
return -1, false, fmt.Errorf("invalid entry on line %d", line) return useridEnd + 1, false, fmt.Errorf("invalid entry on line %d", line)
} }
puid0, err = parseUint32Fast(lf[0]) puid0, err = parseUint32Fast(lf[0])
if err != nil || puid0 < 1 { if err != nil || puid0 < 1 {
return -1, false, fmt.Errorf("invalid parent uid on line %d", line) return useridEnd + 1, false, fmt.Errorf("invalid parent uid on line %d", line)
} }
ok = puid0 == puid ok = puid0 == puid
if ok { if ok {
// allowed fid range 0 to 99 // userid bound to a range, uint32 size allows this to be increased if needed
if fid, err = parseUint32Fast(lf[1]); err != nil || fid < 0 || fid > 99 { if userid, err = parseUint32Fast(lf[1]); err != nil ||
return -1, false, fmt.Errorf("invalid identity on line %d", line) userid < useridStart || userid > useridEnd {
return useridEnd + 1, false, fmt.Errorf("invalid userid on line %d", line)
} }
return return
} }
} }
return -1, false, s.Err() return useridEnd + 1, false, s.Err()
} }
func mustParseConfig(r io.Reader, puid int) (int, bool) { // hsuConfPath is an absolute pathname to the hsu configuration file.
fid, ok, err := parseConfig(r, puid) // Its contents are interpreted by parseConfig.
if err != nil { const hsuConfPath = "/etc/hsurc"
// mustParseConfig calls parseConfig to interpret the contents of hsuConfPath,
// terminating the program if an error is encountered, the syntax is incorrect,
// or the current user is not authorised to use hsu because its uid is missing.
//
// Therefore, code after this function call can assume an authenticated state.
//
// mustParseConfig returns the userid value of the current user.
func mustParseConfig(puid int) (userid uint32) {
if puid > math.MaxUint32 {
log.Fatalf("got impossible uid %d", puid)
}
var ok bool
if f, err := os.Open(hsuConfPath); err != nil {
log.Fatal(err)
} else if userid, ok, err = parseConfig(f, uint32(puid)); err != nil {
log.Fatal(err)
} else if err = f.Close(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
return fid, ok if !ok {
log.Fatalf("uid %d is not in the hsurc file", puid)
}
return
}
// envIdentity is the name of the environment variable holding a
// string representation of the current application identity.
var envIdentity = "HAKUREI_IDENTITY"
// mustReadIdentity calls parseUint32Fast to interpret the value stored in envIdentity,
// terminating the program if the value is not set, malformed, or out of bounds.
func mustReadIdentity() uint32 {
// ranges defined in hst and copied to this package to avoid importing hst
if as, ok := os.LookupEnv(envIdentity); !ok {
log.Fatal("HAKUREI_IDENTITY not set")
panic("unreachable")
} else if identity, err := parseUint32Fast(as); err != nil ||
identity < identityStart || identity > identityEnd {
log.Fatal("invalid identity")
panic("unreachable")
} else {
return identity
}
} }

View File

@ -2,94 +2,105 @@ package main
import ( import (
"bytes" "bytes"
"math"
"strconv" "strconv"
"testing" "testing"
) )
func Test_parseUint32Fast(t *testing.T) { func TestParseUint32Fast(t *testing.T) {
t.Parallel()
t.Run("zero-length", func(t *testing.T) { t.Run("zero-length", func(t *testing.T) {
t.Parallel()
if _, err := parseUint32Fast(""); err == nil || err.Error() != "zero length string" { if _, err := parseUint32Fast(""); err == nil || err.Error() != "zero length string" {
t.Errorf(`parseUint32Fast(""): error = %v`, err) t.Errorf(`parseUint32Fast(""): error = %v`, err)
return return
} }
}) })
t.Run("overflow", func(t *testing.T) { t.Run("overflow", func(t *testing.T) {
t.Parallel()
if _, err := parseUint32Fast("10000000000"); err == nil || err.Error() != "string too long" { if _, err := parseUint32Fast("10000000000"); err == nil || err.Error() != "string too long" {
t.Errorf("parseUint32Fast: error = %v", err) t.Errorf("parseUint32Fast: error = %v", err)
return return
} }
}) })
t.Run("invalid byte", func(t *testing.T) { t.Run("invalid byte", func(t *testing.T) {
t.Parallel()
if _, err := parseUint32Fast("meow"); err == nil || err.Error() != "invalid character 'm' at index 0" { if _, err := parseUint32Fast("meow"); err == nil || err.Error() != "invalid character 'm' at index 0" {
t.Errorf(`parseUint32Fast("meow"): error = %v`, err) t.Errorf(`parseUint32Fast("meow"): error = %v`, err)
return return
} }
}) })
t.Run("full range", func(t *testing.T) {
testRange := func(i, end int) { t.Run("range", func(t *testing.T) {
t.Parallel()
testRange := func(i, end uint32) {
for ; i < end; i++ { for ; i < end; i++ {
s := strconv.Itoa(i) s := strconv.Itoa(int(i))
w := i w := i
t.Run("parse "+s, func(t *testing.T) { t.Run("parse "+s, func(t *testing.T) {
t.Parallel() t.Parallel()
v, err := parseUint32Fast(s) v, err := parseUint32Fast(s)
if err != nil { if err != nil {
t.Errorf("parseUint32Fast(%q): error = %v", t.Errorf("parseUint32Fast(%q): error = %v", s, err)
s, err)
return return
} }
if v != w { if v != w {
t.Errorf("parseUint32Fast(%q): got %v", t.Errorf("parseUint32Fast(%q): got %v", s, v)
s, v)
return return
} }
}) })
} }
} }
testRange(0, 5000) testRange(0, 2500)
testRange(105000, 110000) testRange(23002500, 23005000)
testRange(23005000, 23010000) testRange(math.MaxUint32-2500, math.MaxUint32)
testRange(456005000, 456010000)
testRange(7890005000, 7890010000)
}) })
} }
func Test_parseConfig(t *testing.T) { func TestParseConfig(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
puid, want int puid, want uint32
wantErr string wantErr string
rc string rc string
}{ }{
{"empty", 0, -1, "", ``}, {"empty", 0, useridEnd + 1, "", ``},
{"invalid field", 0, -1, "invalid entry on line 1", `9`}, {"invalid field", 0, useridEnd + 1, "invalid entry on line 1", `9`},
{"invalid puid", 0, -1, "invalid parent uid on line 1", `f 9`}, {"invalid puid", 0, useridEnd + 1, "invalid parent uid on line 1", `f 9`},
{"invalid fid", 1000, -1, "invalid identity on line 1", `1000 f`}, {"invalid userid", 1000, useridEnd + 1, "invalid userid on line 1", `1000 f`},
{"match", 1000, 0, "", `1000 0`}, {"match", 1000, 0, "", `1000 0`},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
fid, ok, err := parseConfig(bytes.NewBufferString(tc.rc), tc.puid) t.Parallel()
userid, ok, err := parseConfig(bytes.NewBufferString(tc.rc), tc.puid)
if err == nil && tc.wantErr != "" { if err == nil && tc.wantErr != "" {
t.Errorf("parseConfig: error = %v; wantErr %q", t.Errorf("parseConfig: error = %v; want %q", err, tc.wantErr)
err, tc.wantErr)
return return
} }
if err != nil && err.Error() != tc.wantErr { if err != nil && err.Error() != tc.wantErr {
t.Errorf("parseConfig: error = %q; wantErr %q", t.Errorf("parseConfig: error = %q; want %q", err, tc.wantErr)
err, tc.wantErr)
return return
} }
if ok == (tc.want == -1) { if ok == (tc.want == useridEnd+1) {
t.Errorf("parseConfig: ok = %v; want %v", t.Errorf("parseConfig: ok = %v; want %v", ok, tc.want)
ok, tc.want)
return return
} }
if fid != tc.want { if userid != tc.want {
t.Errorf("parseConfig: fid = %v; want %v", t.Errorf("parseConfig: %v; want %v", userid, tc.want)
fid, tc.want)
} }
}) })
} }

View File

@ -1,20 +0,0 @@
package main
import (
"log"
"path"
)
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
var (
hmain = compPoison
)
func mustCheckPath(p string) string {
if p != compPoison && p != "" && path.IsAbs(p) {
return p
}
log.Fatal("this program is compiled incorrectly")
return compPoison
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -49,41 +49,10 @@ func capset(hdrp *capHeader, datap *[2]capData) error {
} }
// capBoundingSetDrop drops a capability from the calling thread's capability bounding set. // capBoundingSetDrop drops a capability from the calling thread's capability bounding set.
func capBoundingSetDrop(cap uintptr) error { func capBoundingSetDrop(cap uintptr) error { return Prctl(syscall.PR_CAPBSET_DROP, cap, 0) }
r, _, errno := syscall.Syscall(
syscall.SYS_PRCTL,
syscall.PR_CAPBSET_DROP,
cap, 0,
)
if r != 0 {
return errno
}
return nil
}
// capAmbientClearAll clears the ambient capability set of the calling thread. // capAmbientClearAll clears the ambient capability set of the calling thread.
func capAmbientClearAll() error { func capAmbientClearAll() error { return Prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0) }
r, _, errno := syscall.Syscall(
syscall.SYS_PRCTL,
PR_CAP_AMBIENT,
PR_CAP_AMBIENT_CLEAR_ALL, 0,
)
if r != 0 {
return errno
}
return nil
}
// capAmbientRaise adds to the ambient capability set of the calling thread. // capAmbientRaise adds to the ambient capability set of the calling thread.
func capAmbientRaise(cap uintptr) error { func capAmbientRaise(cap uintptr) error { return Prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, cap) }
r, _, errno := syscall.Syscall(
syscall.SYS_PRCTL,
PR_CAP_AMBIENT,
PR_CAP_AMBIENT_RAISE,
cap,
)
if r != 0 {
return errno
}
return nil
}

View File

@ -3,6 +3,8 @@ package container
import "testing" import "testing"
func TestCapToIndex(t *testing.T) { func TestCapToIndex(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
cap uintptr cap uintptr
@ -14,6 +16,7 @@ func TestCapToIndex(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := capToIndex(tc.cap); got != tc.want { if got := capToIndex(tc.cap); got != tc.want {
t.Errorf("capToIndex: %#x, want %#x", got, tc.want) t.Errorf("capToIndex: %#x, want %#x", got, tc.want)
} }
@ -22,6 +25,8 @@ func TestCapToIndex(t *testing.T) {
} }
func TestCapToMask(t *testing.T) { func TestCapToMask(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
cap uintptr cap uintptr
@ -33,6 +38,7 @@ func TestCapToMask(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := capToMask(tc.cap); got != tc.want { if got := capToMask(tc.cap); got != tc.want {
t.Errorf("capToMask: %#x, want %#x", got, tc.want) t.Errorf("capToMask: %#x, want %#x", got, tc.want)
} }

View File

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

View File

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

View File

@ -0,0 +1,29 @@
package check
import "strings"
const (
// SpecialOverlayEscape is the escape string for overlay mount options.
SpecialOverlayEscape = `\`
// SpecialOverlayOption is the separator string between overlay mount options.
SpecialOverlayOption = ","
// SpecialOverlayPath is the separator string between overlay paths.
SpecialOverlayPath = ":"
)
// EscapeOverlayDataSegment escapes a string for formatting into the data argument of an overlay mount call.
func EscapeOverlayDataSegment(s string) string {
if s == "" {
return ""
}
if f := strings.SplitN(s, "\x00", 2); len(f) > 0 {
s = f[0]
}
return strings.NewReplacer(
SpecialOverlayEscape, SpecialOverlayEscape+SpecialOverlayEscape,
SpecialOverlayOption, SpecialOverlayEscape+SpecialOverlayOption,
SpecialOverlayPath, SpecialOverlayEscape+SpecialOverlayPath,
).Replace(s)
}

View File

@ -0,0 +1,31 @@
package check_test
import (
"testing"
"hakurei.app/container/check"
)
func TestEscapeOverlayDataSegment(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
s string
want string
}{
{"zero", "", ""},
{"multi", `\\\:,:,\\\`, `\\\\\\\:\,\:\,\\\\\\`},
{"bwrap", `/path :,\`, `/path \:\,\\`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := check.EscapeOverlayDataSegment(tc.s); got != tc.want {
t.Errorf("escapeOverlayDataSegment: %s, want %s", got, tc.want)
}
})
}
}

View File

@ -14,13 +14,20 @@ import (
. "syscall" . "syscall"
"time" "time"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/message"
) )
const ( const (
// CancelSignal is the signal expected by container init on context cancel. // CancelSignal is the signal expected by container init on context cancel.
// A custom [Container.Cancel] function must eventually deliver this signal. // A custom [Container.Cancel] function must eventually deliver this signal.
CancelSignal = SIGTERM CancelSignal = SIGUSR2
// Timeout for writing initParams to Container.setup.
initSetupTimeout = 5 * time.Second
) )
type ( type (
@ -33,8 +40,8 @@ type (
// with behaviour identical to its [exec.Cmd] counterpart. // with behaviour identical to its [exec.Cmd] counterpart.
ExtraFiles []*os.File ExtraFiles []*os.File
// param encoder for shim and init // param pipe for shim and init
setup *gob.Encoder setup *os.File
// cancels cmd // cancels cmd
cancel context.CancelFunc cancel context.CancelFunc
// closed after Wait returns // closed after Wait returns
@ -49,17 +56,18 @@ type (
cmd *exec.Cmd cmd *exec.Cmd
ctx context.Context ctx context.Context
msg message.Msg
Params Params
} }
// Params holds container configuration and is safe to serialise. // Params holds container configuration and is safe to serialise.
Params struct { Params struct {
// Working directory in the container. // Working directory in the container.
Dir *Absolute Dir *check.Absolute
// Initial process environment. // Initial process environment.
Env []string Env []string
// Pathname of initial process in the container. // Pathname of initial process in the container.
Path *Absolute Path *check.Absolute
// Initial process argv. // Initial process argv.
Args []string Args []string
// Deliver SIGINT to the initial process on context cancellation. // Deliver SIGINT to the initial process on context cancellation.
@ -77,11 +85,11 @@ type (
*Ops *Ops
// Seccomp system call filter rules. // Seccomp system call filter rules.
SeccompRules []seccomp.NativeRule SeccompRules []std.NativeRule
// Extra seccomp flags. // Extra seccomp flags.
SeccompFlags seccomp.ExportFlag SeccompFlags seccomp.ExportFlag
// Seccomp presets. Has no effect unless SeccompRules is zero-length. // Seccomp presets. Has no effect unless SeccompRules is zero-length.
SeccompPresets seccomp.FilterPreset SeccompPresets std.FilterPreset
// Do not load seccomp program. // Do not load seccomp program.
SeccompDisable bool SeccompDisable bool
@ -162,14 +170,14 @@ func (p *Container) Start() error {
// map to overflow id to work around ownership checks // map to overflow id to work around ownership checks
if p.Uid < 1 { if p.Uid < 1 {
p.Uid = OverflowUid() p.Uid = OverflowUid(p.msg)
} }
if p.Gid < 1 { if p.Gid < 1 {
p.Gid = OverflowGid() p.Gid = OverflowGid(p.msg)
} }
if !p.RetainSession { if !p.RetainSession {
p.SeccompPresets |= seccomp.PresetDenyTTY p.SeccompPresets |= std.PresetDenyTTY
} }
if p.AdoptWaitDelay == 0 { if p.AdoptWaitDelay == 0 {
@ -197,7 +205,7 @@ func (p *Container) Start() error {
} else { } else {
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(CancelSignal) } p.cmd.Cancel = func() error { return p.cmd.Process.Signal(CancelSignal) }
} }
p.cmd.Dir = FHSRoot p.cmd.Dir = fhs.Root
p.cmd.SysProcAttr = &SysProcAttr{ p.cmd.SysProcAttr = &SysProcAttr{
Setsid: !p.RetainSession, Setsid: !p.RetainSession,
Pdeathsig: SIGKILL, Pdeathsig: SIGKILL,
@ -223,10 +231,10 @@ func (p *Container) Start() error {
} }
// 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, f, err := Setup(&p.cmd.ExtraFiles); err != nil {
return &StartError{true, "set up params stream", err, false, false} return &StartError{true, "set up params stream", err, false, false}
} else { } else {
p.setup = e p.setup = f
p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)} p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
} }
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...) p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...)
@ -263,19 +271,19 @@ func (p *Container) Start() error {
} }
return &StartError{false, "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", ENOSYS, true, false} return &StartError{false, "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", ENOSYS, true, false}
} else { } else {
msg.Verbosef("landlock abi version %d", abi) p.msg.Verbosef("landlock abi version %d", abi)
} }
if rulesetFd, err := rulesetAttr.Create(0); err != nil { if rulesetFd, err := rulesetAttr.Create(0); err != nil {
return &StartError{true, "create landlock ruleset", err, false, false} return &StartError{true, "create landlock ruleset", err, false, false}
} else { } else {
msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr) p.msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
if err = LandlockRestrictSelf(rulesetFd, 0); err != nil { if err = LandlockRestrictSelf(rulesetFd, 0); err != nil {
_ = Close(rulesetFd) _ = Close(rulesetFd)
return &StartError{true, "enforce landlock ruleset", err, false, false} return &StartError{true, "enforce landlock ruleset", err, false, false}
} }
if err = Close(rulesetFd); err != nil { if err = Close(rulesetFd); err != nil {
msg.Verbosef("cannot close landlock ruleset: %v", err) p.msg.Verbosef("cannot close landlock ruleset: %v", err)
// not fatal // not fatal
} }
} }
@ -283,7 +291,7 @@ func (p *Container) Start() error {
landlockOut: landlockOut:
} }
msg.Verbose("starting container init") p.msg.Verbose("starting container init")
if err := p.cmd.Start(); err != nil { if err := p.cmd.Start(); err != nil {
return &StartError{false, "start container init", err, false, true} return &StartError{false, "start container init", err, false, true}
} }
@ -305,6 +313,9 @@ func (p *Container) Serve() error {
setup := p.setup setup := p.setup
p.setup = nil p.setup = nil
if err := setup.SetDeadline(time.Now().Add(initSetupTimeout)); err != nil {
return &StartError{true, "set init pipe deadline", err, false, true}
}
if p.Path == nil { if p.Path == nil {
p.cancel() p.cancel()
@ -313,21 +324,20 @@ func (p *Container) Serve() error {
// do not transmit nil // do not transmit nil
if p.Dir == nil { if p.Dir == nil {
p.Dir = AbsFHSRoot p.Dir = fhs.AbsRoot
} }
if p.SeccompRules == nil { if p.SeccompRules == nil {
p.SeccompRules = make([]seccomp.NativeRule, 0) p.SeccompRules = make([]std.NativeRule, 0)
} }
err := setup.Encode( err := gob.NewEncoder(setup).Encode(&initParams{
&initParams{
p.Params, p.Params,
Getuid(), Getuid(),
Getgid(), Getgid(),
len(p.ExtraFiles), len(p.ExtraFiles),
msg.IsVerbose(), p.msg.IsVerbose(),
}, })
) _ = setup.Close()
if err != nil { if err != nil {
p.cancel() p.cancel()
} }
@ -392,17 +402,21 @@ func (p *Container) ProcessState() *os.ProcessState {
} }
// New returns the address to a new instance of [Container] that requires further initialisation before use. // New returns the address to a new instance of [Container] that requires further initialisation before use.
func New(ctx context.Context) *Container { func New(ctx context.Context, msg message.Msg) *Container {
p := &Container{ctx: ctx, Params: Params{Ops: new(Ops)}} if msg == nil {
msg = message.New(nil)
}
p := &Container{ctx: ctx, msg: msg, Params: Params{Ops: new(Ops)}}
c, cancel := context.WithCancel(ctx) c, cancel := context.WithCancel(ctx)
p.cancel = cancel p.cancel = cancel
p.cmd = exec.CommandContext(c, MustExecutable()) p.cmd = exec.CommandContext(c, MustExecutable(msg))
return p return p
} }
// NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields. // NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields.
func NewCommand(ctx context.Context, pathname *Absolute, name string, args ...string) *Container { func NewCommand(ctx context.Context, msg message.Msg, pathname *check.Absolute, name string, args ...string) *Container {
z := New(ctx) z := New(ctx, msg)
z.Path = pathname z.Path = pathname
z.Args = append([]string{name}, args...) z.Args = append([]string{name}, args...)
return z return z

View File

@ -20,15 +20,18 @@ import (
"hakurei.app/command" "hakurei.app/command"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/check"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/container/vfs" "hakurei.app/container/vfs"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/ldd" "hakurei.app/ldd"
"hakurei.app/message"
) )
func TestStartError(t *testing.T) { func TestStartError(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
err error err error
@ -136,6 +139,8 @@ func TestStartError(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("error", func(t *testing.T) { t.Run("error", func(t *testing.T) {
if got := tc.err.Error(); got != tc.s { if got := tc.err.Error(); got != tc.s {
t.Errorf("Error: %q, want %q", got, tc.s) t.Errorf("Error: %q, want %q", got, tc.s)
@ -152,13 +157,13 @@ func TestStartError(t *testing.T) {
}) })
t.Run("msg", func(t *testing.T) { t.Run("msg", func(t *testing.T) {
if got, ok := container.GetErrorMessage(tc.err); !ok { if got, ok := message.GetMessage(tc.err); !ok {
if tc.msg != "" { if tc.msg != "" {
t.Errorf("GetErrorMessage: err does not implement MessageError") t.Errorf("GetMessage: err does not implement MessageError")
} }
return return
} else if got != tc.msg { } else if got != tc.msg {
t.Errorf("GetErrorMessage: %q, want %q", got, tc.msg) t.Errorf("GetMessage: %q, want %q", got, tc.msg)
} }
}) })
}) })
@ -199,35 +204,35 @@ var containerTestCases = []struct {
uid int uid int
gid int gid int
rules []seccomp.NativeRule rules []std.NativeRule
flags seccomp.ExportFlag flags seccomp.ExportFlag
presets seccomp.FilterPreset presets std.FilterPreset
}{ }{
{"minimal", true, false, false, true, {"minimal", true, false, false, true,
emptyOps, emptyMnt, emptyOps, emptyMnt,
1000, 100, nil, 0, seccomp.PresetStrict}, 1000, 100, nil, 0, std.PresetStrict},
{"allow", true, true, true, false, {"allow", true, true, true, false,
emptyOps, emptyMnt, emptyOps, emptyMnt,
1000, 100, nil, 0, seccomp.PresetExt | seccomp.PresetDenyDevel}, 1000, 100, nil, 0, std.PresetExt | std.PresetDenyDevel},
{"no filter", false, true, true, true, {"no filter", false, true, true, true,
emptyOps, emptyMnt, emptyOps, emptyMnt,
1000, 100, nil, 0, seccomp.PresetExt}, 1000, 100, nil, 0, std.PresetExt},
{"custom rules", true, true, true, false, {"custom rules", true, true, true, false,
emptyOps, emptyMnt, emptyOps, emptyMnt,
1, 31, []seccomp.NativeRule{{Syscall: seccomp.ScmpSyscall(syscall.SYS_SETUID), Errno: seccomp.ScmpErrno(syscall.EPERM)}}, 0, seccomp.PresetExt}, 1, 31, []std.NativeRule{{Syscall: std.ScmpSyscall(syscall.SYS_SETUID), Errno: std.ScmpErrno(syscall.EPERM)}}, 0, std.PresetExt},
{"tmpfs", true, false, false, true, {"tmpfs", true, false, false, true,
earlyOps(new(container.Ops). earlyOps(new(container.Ops).
Tmpfs(hst.AbsTmp, 0, 0755), Tmpfs(hst.AbsPrivateTmp, 0, 0755),
), ),
earlyMnt( earlyMnt(
ent("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", ignore), ent("/", hst.PrivateTmp, "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", ignore),
), ),
9, 9, nil, 0, seccomp.PresetStrict}, 9, 9, nil, 0, std.PresetStrict},
{"dev", true, true /* go test output is not a tty */, false, false, {"dev", true, true /* go test output is not a tty */, false, false,
earlyOps(new(container.Ops). earlyOps(new(container.Ops).
Dev(container.MustAbs("/dev"), true), Dev(check.MustAbs("/dev"), true),
), ),
earlyMnt( earlyMnt(
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore), ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
@ -241,11 +246,11 @@ var containerTestCases = []struct {
ent("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"), ent("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore), ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
), ),
1971, 100, nil, 0, seccomp.PresetStrict}, 1971, 100, nil, 0, std.PresetStrict},
{"dev no mqueue", true, true /* go test output is not a tty */, false, false, {"dev no mqueue", true, true /* go test output is not a tty */, false, false,
earlyOps(new(container.Ops). earlyOps(new(container.Ops).
Dev(container.MustAbs("/dev"), false), Dev(check.MustAbs("/dev"), false),
), ),
earlyMnt( earlyMnt(
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore), ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
@ -258,24 +263,24 @@ var containerTestCases = []struct {
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"), ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore), ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
), ),
1971, 100, nil, 0, seccomp.PresetStrict}, 1971, 100, nil, 0, std.PresetStrict},
{"overlay", true, false, false, true, {"overlay", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) { func(t *testing.T) (*container.Ops, context.Context) {
tempDir := container.MustAbs(t.TempDir()) tempDir := check.MustAbs(t.TempDir())
lower0, lower1, upper, work := lower0, lower1, upper, work :=
tempDir.Append("lower0"), tempDir.Append("lower0"),
tempDir.Append("lower1"), tempDir.Append("lower1"),
tempDir.Append("upper"), tempDir.Append("upper"),
tempDir.Append("work") tempDir.Append("work")
for _, a := range []*container.Absolute{lower0, lower1, upper, work} { for _, a := range []*check.Absolute{lower0, lower1, upper, work} {
if err := os.Mkdir(a.String(), 0755); err != nil { if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err) t.Fatalf("Mkdir: error = %v", err)
} }
} }
return new(container.Ops). return new(container.Ops).
Overlay(hst.AbsTmp, upper, work, lower0, lower1), Overlay(hst.AbsPrivateTmp, upper, work, lower0, lower1),
context.WithValue(context.WithValue(context.WithValue(context.WithValue(t.Context(), context.WithValue(context.WithValue(context.WithValue(context.WithValue(t.Context(),
testVal("lower1"), lower1), testVal("lower1"), lower1),
testVal("lower0"), lower0), testVal("lower0"), lower0),
@ -284,74 +289,74 @@ var containerTestCases = []struct {
}, },
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry { func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{ return []*vfs.MountInfoEntry{
ent("/", hst.Tmp, "rw", "overlay", "overlay", ent("/", hst.PrivateTmp, "rw", "overlay", "overlay",
"rw,lowerdir="+ "rw,lowerdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+ container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*check.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+ container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
",upperdir="+ ",upperdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(*container.Absolute).String())+ container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(*check.Absolute).String())+
",workdir="+ ",workdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("work")).(*container.Absolute).String())+ container.InternalToHostOvlEscape(ctx.Value(testVal("work")).(*check.Absolute).String())+
",redirect_dir=nofollow,uuid=on,userxattr"), ",redirect_dir=nofollow,uuid=on,userxattr"),
} }
}, },
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict}, 1 << 3, 1 << 14, nil, 0, std.PresetStrict},
{"overlay ephemeral", true, false, false, true, {"overlay ephemeral", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) { func(t *testing.T) (*container.Ops, context.Context) {
tempDir := container.MustAbs(t.TempDir()) tempDir := check.MustAbs(t.TempDir())
lower0, lower1 := lower0, lower1 :=
tempDir.Append("lower0"), tempDir.Append("lower0"),
tempDir.Append("lower1") tempDir.Append("lower1")
for _, a := range []*container.Absolute{lower0, lower1} { for _, a := range []*check.Absolute{lower0, lower1} {
if err := os.Mkdir(a.String(), 0755); err != nil { if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err) t.Fatalf("Mkdir: error = %v", err)
} }
} }
return new(container.Ops). return new(container.Ops).
OverlayEphemeral(hst.AbsTmp, lower0, lower1), OverlayEphemeral(hst.AbsPrivateTmp, lower0, lower1),
t.Context() t.Context()
}, },
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry { func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{ return []*vfs.MountInfoEntry{
// contains random suffix // contains random suffix
ent("/", hst.Tmp, "rw", "overlay", "overlay", ignore), ent("/", hst.PrivateTmp, "rw", "overlay", "overlay", ignore),
} }
}, },
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict}, 1 << 3, 1 << 14, nil, 0, std.PresetStrict},
{"overlay readonly", true, false, false, true, {"overlay readonly", true, false, false, true,
func(t *testing.T) (*container.Ops, context.Context) { func(t *testing.T) (*container.Ops, context.Context) {
tempDir := container.MustAbs(t.TempDir()) tempDir := check.MustAbs(t.TempDir())
lower0, lower1 := lower0, lower1 :=
tempDir.Append("lower0"), tempDir.Append("lower0"),
tempDir.Append("lower1") tempDir.Append("lower1")
for _, a := range []*container.Absolute{lower0, lower1} { for _, a := range []*check.Absolute{lower0, lower1} {
if err := os.Mkdir(a.String(), 0755); err != nil { if err := os.Mkdir(a.String(), 0755); err != nil {
t.Fatalf("Mkdir: error = %v", err) t.Fatalf("Mkdir: error = %v", err)
} }
} }
return new(container.Ops). return new(container.Ops).
OverlayReadonly(hst.AbsTmp, lower0, lower1), OverlayReadonly(hst.AbsPrivateTmp, lower0, lower1),
context.WithValue(context.WithValue(t.Context(), context.WithValue(context.WithValue(t.Context(),
testVal("lower1"), lower1), testVal("lower1"), lower1),
testVal("lower0"), lower0) testVal("lower0"), lower0)
}, },
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry { func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
return []*vfs.MountInfoEntry{ return []*vfs.MountInfoEntry{
ent("/", hst.Tmp, "rw", "overlay", "overlay", ent("/", hst.PrivateTmp, "rw", "overlay", "overlay",
"ro,lowerdir="+ "ro,lowerdir="+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+ container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*check.Absolute).String())+":"+
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+ container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
",redirect_dir=nofollow,userxattr"), ",redirect_dir=nofollow,userxattr"),
} }
}, },
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict}, 1 << 3, 1 << 14, nil, 0, std.PresetStrict},
} }
func TestContainer(t *testing.T) { func TestContainer(t *testing.T) {
replaceOutput(t) t.Parallel()
t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) { t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) {
wantErr := context.Canceled wantErr := context.Canceled
@ -386,13 +391,15 @@ func TestContainer(t *testing.T) {
for i, tc := range containerTestCases { for i, tc := range containerTestCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
wantOps, wantOpsCtx := tc.ops(t) wantOps, wantOpsCtx := tc.ops(t)
wantMnt := tc.mnt(t, wantOpsCtx) wantMnt := tc.mnt(t, wantOpsCtx)
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout) ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
defer cancel() defer cancel()
var libPaths []*container.Absolute var libPaths []*check.Absolute
c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i)) c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i))
c.Uid = tc.uid c.Uid = tc.uid
c.Gid = tc.gid c.Gid = tc.gid
@ -413,11 +420,11 @@ func TestContainer(t *testing.T) {
c.HostNet = tc.net c.HostNet = tc.net
c. c.
Readonly(container.MustAbs(pathReadonly), 0755). Readonly(check.MustAbs(pathReadonly), 0755).
Tmpfs(container.MustAbs("/tmp"), 0, 0755). Tmpfs(check.MustAbs("/tmp"), 0, 0755).
Place(container.MustAbs("/etc/hostname"), []byte(c.Hostname)) Place(check.MustAbs("/etc/hostname"), []byte(c.Hostname))
// needs /proc to check mountinfo // needs /proc to check mountinfo
c.Proc(container.MustAbs("/proc")) c.Proc(check.MustAbs("/proc"))
// mountinfo cannot be resolved directly by helper due to libPaths nondeterminism // mountinfo cannot be resolved directly by helper due to libPaths nondeterminism
mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths)) mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
@ -448,10 +455,10 @@ func TestContainer(t *testing.T) {
_, _ = output.WriteTo(os.Stdout) _, _ = output.WriteTo(os.Stdout)
t.Fatalf("cannot serialise expected mount points: %v", err) t.Fatalf("cannot serialise expected mount points: %v", err)
} }
c.Place(container.MustAbs(pathWantMnt), want.Bytes()) c.Place(check.MustAbs(pathWantMnt), want.Bytes())
if tc.ro { if tc.ro {
c.Remount(container.MustAbs("/"), syscall.MS_RDONLY) c.Remount(check.MustAbs("/"), syscall.MS_RDONLY)
} }
if err := c.Start(); err != nil { if err := c.Start(); err != nil {
@ -505,6 +512,7 @@ func testContainerCancel(
waitCheck func(t *testing.T, c *container.Container), waitCheck func(t *testing.T, c *container.Container),
) func(t *testing.T) { ) func(t *testing.T) {
return func(t *testing.T) { return func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout) ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
c := helperNewContainer(ctx, "block") c := helperNewContainer(ctx, "block")
@ -547,12 +555,14 @@ func testContainerCancel(
} }
func TestContainerString(t *testing.T) { func TestContainerString(t *testing.T) {
c := container.NewCommand(t.Context(), container.MustAbs("/run/current-system/sw/bin/ldd"), "ldd", "/usr/bin/env") t.Parallel()
msg := message.New(nil)
c := container.NewCommand(t.Context(), msg, check.MustAbs("/run/current-system/sw/bin/ldd"), "ldd", "/usr/bin/env")
c.SeccompFlags |= seccomp.AllowMultiarch c.SeccompFlags |= seccomp.AllowMultiarch
c.SeccompRules = seccomp.Preset( c.SeccompRules = seccomp.Preset(
seccomp.PresetExt|seccomp.PresetDenyNS|seccomp.PresetDenyTTY, std.PresetExt|std.PresetDenyNS|std.PresetDenyTTY,
c.SeccompFlags) c.SeccompFlags)
c.SeccompPresets = seccomp.PresetStrict c.SeccompPresets = std.PresetStrict
want := `argv: ["ldd" "/usr/bin/env"], filter: true, rules: 65, flags: 0x1, presets: 0xf` want := `argv: ["ldd" "/usr/bin/env"], filter: true, rules: 65, flags: 0x1, presets: 0xf`
if got := c.String(); got != want { if got := c.String(); got != want {
t.Errorf("String: %s, want %s", got, want) t.Errorf("String: %s, want %s", got, want)
@ -566,13 +576,12 @@ const (
func init() { func init() {
helperCommands = append(helperCommands, func(c command.Command) { helperCommands = append(helperCommands, func(c command.Command) {
c.Command("block", command.UsageInternal, func(args []string) error { c.Command("block", command.UsageInternal, func(args []string) error {
if _, err := os.NewFile(3, "sync").Write([]byte{0}); err != nil {
return fmt.Errorf("write to sync pipe: %v", err)
}
{
sig := make(chan os.Signal, 1) sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt) signal.Notify(sig, os.Interrupt)
go func() { <-sig; os.Exit(blockExitCodeInterrupt) }() go func() { <-sig; os.Exit(blockExitCodeInterrupt) }()
if _, err := os.NewFile(3, "sync").Write([]byte{0}); err != nil {
return fmt.Errorf("write to sync pipe: %v", err)
} }
select {} select {}
}) })
@ -683,13 +692,13 @@ const (
) )
var ( var (
absHelperInnerPath = container.MustAbs(helperInnerPath) absHelperInnerPath = check.MustAbs(helperInnerPath)
) )
var helperCommands []func(c command.Command) var helperCommands []func(c command.Command)
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput) container.TryArgv0(nil)
if os.Getenv(envDoCheck) == "1" { if os.Getenv(envDoCheck) == "1" {
c := command.New(os.Stderr, log.Printf, "helper", func(args []string) error { c := command.New(os.Stderr, log.Printf, "helper", func(args []string) error {
@ -711,13 +720,14 @@ func TestMain(m *testing.M) {
os.Exit(m.Run()) os.Exit(m.Run())
} }
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*container.Absolute, args ...string) (c *container.Container) { func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*check.Absolute, args ...string) (c *container.Container) {
c = container.NewCommand(ctx, absHelperInnerPath, "helper", args...) msg := message.New(nil)
c = container.NewCommand(ctx, msg, absHelperInnerPath, "helper", args...)
c.Env = append(c.Env, envDoCheck+"=1") c.Env = append(c.Env, envDoCheck+"=1")
c.Bind(container.MustAbs(os.Args[0]), absHelperInnerPath, 0) c.Bind(check.MustAbs(os.Args[0]), absHelperInnerPath, 0)
// in case test has cgo enabled // in case test has cgo enabled
if entries, err := ldd.Exec(ctx, os.Args[0]); err != nil { if entries, err := ldd.Exec(ctx, msg, os.Args[0]); err != nil {
log.Fatalf("ldd: %v", err) log.Fatalf("ldd: %v", err)
} else { } else {
*libPaths = ldd.Path(entries) *libPaths = ldd.Path(entries)
@ -730,5 +740,5 @@ func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*container.Abso
} }
func helperNewContainer(ctx context.Context, args ...string) (c *container.Container) { func helperNewContainer(ctx context.Context, args ...string) (c *container.Container) {
return helperNewContainerLibPaths(ctx, new([]*container.Absolute), args...) return helperNewContainerLibPaths(ctx, new([]*check.Absolute), args...)
} }

View File

@ -3,7 +3,6 @@ package container
import ( import (
"io" "io"
"io/fs" "io/fs"
"log"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
@ -12,6 +11,8 @@ import (
"syscall" "syscall"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/message"
) )
type osFile interface { type osFile interface {
@ -38,7 +39,7 @@ type syscallDispatcher interface {
setNoNewPrivs() error setNoNewPrivs() error
// lastcap provides [LastCap]. // lastcap provides [LastCap].
lastcap() uintptr lastcap(msg message.Msg) uintptr
// capset provides capset. // capset provides capset.
capset(hdrp *capHeader, datap *[2]capData) error capset(hdrp *capHeader, datap *[2]capData) error
// capBoundingSetDrop provides capBoundingSetDrop. // capBoundingSetDrop provides capBoundingSetDrop.
@ -53,16 +54,16 @@ type syscallDispatcher interface {
receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
// bindMount provides procPaths.bindMount. // bindMount provides procPaths.bindMount.
bindMount(source, target string, flags uintptr) error bindMount(msg message.Msg, source, target string, flags uintptr) error
// remount provides procPaths.remount. // remount provides procPaths.remount.
remount(target string, flags uintptr) error remount(msg message.Msg, target string, flags uintptr) error
// mountTmpfs provides mountTmpfs. // mountTmpfs provides mountTmpfs.
mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error
// ensureFile provides ensureFile. // ensureFile provides ensureFile.
ensureFile(name string, perm, pperm os.FileMode) error ensureFile(name string, perm, pperm os.FileMode) error
// seccompLoad provides [seccomp.Load]. // seccompLoad provides [seccomp.Load].
seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error
// notify provides [signal.Notify]. // notify provides [signal.Notify].
notify(c chan<- os.Signal, sig ...os.Signal) notify(c chan<- os.Signal, sig ...os.Signal)
// start starts [os/exec.Cmd]. // start starts [os/exec.Cmd].
@ -122,22 +123,12 @@ type syscallDispatcher interface {
// wait4 provides syscall.Wait4 // wait4 provides syscall.Wait4
wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error)
// printf provides [log.Printf]. // printf provides the Printf method of [log.Logger].
printf(format string, v ...any) printf(msg message.Msg, format string, v ...any)
// fatal provides [log.Fatal] // fatal provides the Fatal method of [log.Logger]
fatal(v ...any) fatal(msg message.Msg, v ...any)
// fatalf provides [log.Fatalf] // fatalf provides the Fatalf method of [log.Logger]
fatalf(format string, v ...any) fatalf(msg message.Msg, format string, v ...any)
// verbose provides [Msg.Verbose].
verbose(v ...any)
// verbosef provides [Msg.Verbosef].
verbosef(format string, v ...any)
// suspend provides [Msg.Suspend].
suspend()
// resume provides [Msg.Resume].
resume() bool
// beforeExit provides [Msg.BeforeExit].
beforeExit()
} }
// direct implements syscallDispatcher on the current kernel. // direct implements syscallDispatcher on the current kernel.
@ -151,7 +142,7 @@ func (direct) setPtracer(pid uintptr) error { return SetPtracer(pid) }
func (direct) setDumpable(dumpable uintptr) error { return SetDumpable(dumpable) } func (direct) setDumpable(dumpable uintptr) error { return SetDumpable(dumpable) }
func (direct) setNoNewPrivs() error { return SetNoNewPrivs() } func (direct) setNoNewPrivs() error { return SetNoNewPrivs() }
func (direct) lastcap() uintptr { return LastCap() } func (direct) lastcap(msg message.Msg) uintptr { return LastCap(msg) }
func (direct) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) } func (direct) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) }
func (direct) capBoundingSetDrop(cap uintptr) error { return capBoundingSetDrop(cap) } func (direct) capBoundingSetDrop(cap uintptr) error { return capBoundingSetDrop(cap) }
func (direct) capAmbientClearAll() error { return capAmbientClearAll() } func (direct) capAmbientClearAll() error { return capAmbientClearAll() }
@ -161,11 +152,11 @@ func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
return Receive(key, e, fdp) return Receive(key, e, fdp)
} }
func (direct) bindMount(source, target string, flags uintptr) error { func (direct) bindMount(msg message.Msg, source, target string, flags uintptr) error {
return hostProc.bindMount(source, target, flags) return hostProc.bindMount(msg, source, target, flags)
} }
func (direct) remount(target string, flags uintptr) error { func (direct) remount(msg message.Msg, target string, flags uintptr) error {
return hostProc.remount(target, flags) return hostProc.remount(msg, target, flags)
} }
func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error { func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
return mountTmpfs(k, fsname, target, flags, size, perm) return mountTmpfs(k, fsname, target, flags, size, perm)
@ -174,7 +165,7 @@ func (direct) ensureFile(name string, perm, pperm os.FileMode) error {
return ensureFile(name, perm, pperm) return ensureFile(name, perm, pperm)
} }
func (direct) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error { func (direct) seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error {
return seccomp.Load(rules, flags) return seccomp.Load(rules, flags)
} }
func (direct) notify(c chan<- os.Signal, sig ...os.Signal) { signal.Notify(c, sig...) } func (direct) notify(c chan<- os.Signal, sig ...os.Signal) { signal.Notify(c, sig...) }
@ -232,11 +223,6 @@ func (direct) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *s
return syscall.Wait4(pid, wstatus, options, rusage) return syscall.Wait4(pid, wstatus, options, rusage)
} }
func (direct) printf(format string, v ...any) { log.Printf(format, v...) } func (direct) printf(msg message.Msg, format string, v ...any) { msg.GetLogger().Printf(format, v...) }
func (direct) fatal(v ...any) { log.Fatal(v...) } func (direct) fatal(msg message.Msg, v ...any) { msg.GetLogger().Fatal(v...) }
func (direct) fatalf(format string, v ...any) { log.Fatalf(format, v...) } func (direct) fatalf(msg message.Msg, format string, v ...any) { msg.GetLogger().Fatalf(format, v...) }
func (direct) verbose(v ...any) { msg.Verbose(v...) }
func (direct) verbosef(format string, v ...any) { msg.Verbosef(format, v...) }
func (direct) suspend() { msg.Suspend() }
func (direct) resume() bool { return msg.Resume() }
func (direct) beforeExit() { msg.BeforeExit() }

View File

@ -2,8 +2,10 @@ package container
import ( import (
"bytes" "bytes"
"fmt"
"io" "io"
"io/fs" "io/fs"
"log"
"os" "os"
"os/exec" "os/exec"
"reflect" "reflect"
@ -15,7 +17,9 @@ import (
"time" "time"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/container/stub" "hakurei.app/container/stub"
"hakurei.app/message"
) )
type opValidTestCase struct { type opValidTestCase struct {
@ -29,10 +33,12 @@ func checkOpsValid(t *testing.T, testCases []opValidTestCase) {
t.Run("valid", func(t *testing.T) { t.Run("valid", func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
if got := tc.op.Valid(); got != tc.want { if got := tc.op.Valid(); got != tc.want {
t.Errorf("Valid: %v, want %v", got, tc.want) t.Errorf("Valid: %v, want %v", got, tc.want)
@ -53,10 +59,12 @@ func checkOpsBuilder(t *testing.T, testCases []opsBuilderTestCase) {
t.Run("build", func(t *testing.T) { t.Run("build", func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
if !slices.EqualFunc(*tc.ops, tc.want, func(op Op, v Op) bool { return op.Is(v) }) { if !slices.EqualFunc(*tc.ops, tc.want, func(op Op, v Op) bool { return op.Is(v) }) {
t.Errorf("Ops: %#v, want %#v", tc.ops, tc.want) t.Errorf("Ops: %#v, want %#v", tc.ops, tc.want)
@ -77,10 +85,12 @@ func checkOpIs(t *testing.T, testCases []opIsTestCase) {
t.Run("is", func(t *testing.T) { t.Run("is", func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
if got := tc.op.Is(tc.v); got != tc.want { if got := tc.op.Is(tc.v); got != tc.want {
t.Errorf("Is: %v, want %v", got, tc.want) t.Errorf("Is: %v, want %v", got, tc.want)
@ -103,10 +113,12 @@ func checkOpMeta(t *testing.T, testCases []opMetaTestCase) {
t.Run("meta", func(t *testing.T) { t.Run("meta", func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
t.Run("prefix", func(t *testing.T) { t.Run("prefix", func(t *testing.T) {
t.Helper() t.Helper()
@ -136,7 +148,7 @@ func call(name string, args stub.ExpectArgs, ret any, err error) stub.Call {
type simpleTestCase struct { type simpleTestCase struct {
name string name string
f func(k syscallDispatcher) error f func(k *kstub) error
want stub.Expect want stub.Expect
wantErr error wantErr error
} }
@ -147,6 +159,7 @@ func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
wait4signal := make(chan struct{}) wait4signal := make(chan struct{})
k := &kstub{wait4signal, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, s} }, tc.want)} k := &kstub{wait4signal, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, s} }, tc.want)}
@ -180,16 +193,18 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
t.Run("behaviour", func(t *testing.T) { t.Run("behaviour", func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Helper() t.Helper()
t.Parallel()
state := &setupState{Params: tc.params}
k := &kstub{nil, stub.New(t, k := &kstub{nil, stub.New(t,
func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, s} }, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, s} },
stub.Expect{Calls: slices.Concat(tc.early, []stub.Call{{Name: stub.CallSeparator}}, tc.apply)}, stub.Expect{Calls: slices.Concat(tc.early, []stub.Call{{Name: stub.CallSeparator}}, tc.apply)},
)} )}
state := &setupState{Params: tc.params, Msg: k}
defer stub.HandleExit(t) defer stub.HandleExit(t)
errEarly := tc.op.early(state, k) errEarly := tc.op.early(state, k)
k.Expects(stub.CallSeparator) k.Expects(stub.CallSeparator)
@ -327,7 +342,11 @@ func (k *kstub) setDumpable(dumpable uintptr) error {
} }
func (k *kstub) setNoNewPrivs() error { k.Helper(); return k.Expects("setNoNewPrivs").Err } func (k *kstub) setNoNewPrivs() error { k.Helper(); return k.Expects("setNoNewPrivs").Err }
func (k *kstub) lastcap() uintptr { k.Helper(); return k.Expects("lastcap").Ret.(uintptr) } func (k *kstub) lastcap(msg message.Msg) uintptr {
k.Helper()
k.checkMsg(msg)
return k.Expects("lastcap").Ret.(uintptr)
}
func (k *kstub) capset(hdrp *capHeader, datap *[2]capData) error { func (k *kstub) capset(hdrp *capHeader, datap *[2]capData) error {
k.Helper() k.Helper()
@ -403,16 +422,18 @@ func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error
return return
} }
func (k *kstub) bindMount(source, target string, flags uintptr) error { func (k *kstub) bindMount(msg message.Msg, source, target string, flags uintptr) error {
k.Helper() k.Helper()
k.checkMsg(msg)
return k.Expects("bindMount").Error( return k.Expects("bindMount").Error(
stub.CheckArg(k.Stub, "source", source, 0), stub.CheckArg(k.Stub, "source", source, 0),
stub.CheckArg(k.Stub, "target", target, 1), stub.CheckArg(k.Stub, "target", target, 1),
stub.CheckArg(k.Stub, "flags", flags, 2)) stub.CheckArg(k.Stub, "flags", flags, 2))
} }
func (k *kstub) remount(target string, flags uintptr) error { func (k *kstub) remount(msg message.Msg, target string, flags uintptr) error {
k.Helper() k.Helper()
k.checkMsg(msg)
return k.Expects("remount").Error( return k.Expects("remount").Error(
stub.CheckArg(k.Stub, "target", target, 0), stub.CheckArg(k.Stub, "target", target, 0),
stub.CheckArg(k.Stub, "flags", flags, 1)) stub.CheckArg(k.Stub, "flags", flags, 1))
@ -436,7 +457,7 @@ func (k *kstub) ensureFile(name string, perm, pperm os.FileMode) error {
stub.CheckArg(k.Stub, "pperm", pperm, 2)) stub.CheckArg(k.Stub, "pperm", pperm, 2))
} }
func (k *kstub) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error { func (k *kstub) seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error {
k.Helper() k.Helper()
return k.Expects("seccompLoad").Error( return k.Expects("seccompLoad").Error(
stub.CheckArgReflect(k.Stub, "rules", rules, 0), stub.CheckArgReflect(k.Stub, "rules", rules, 0),
@ -694,7 +715,7 @@ func (k *kstub) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage
return return
} }
func (k *kstub) printf(format string, v ...any) { func (k *kstub) printf(_ message.Msg, format string, v ...any) {
k.Helper() k.Helper()
if k.Expects("printf").Error( if k.Expects("printf").Error(
stub.CheckArg(k.Stub, "format", format, 0), stub.CheckArg(k.Stub, "format", format, 0),
@ -703,7 +724,7 @@ func (k *kstub) printf(format string, v ...any) {
} }
} }
func (k *kstub) fatal(v ...any) { func (k *kstub) fatal(_ message.Msg, v ...any) {
k.Helper() k.Helper()
if k.Expects("fatal").Error( if k.Expects("fatal").Error(
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil { stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil {
@ -712,7 +733,7 @@ func (k *kstub) fatal(v ...any) {
panic(stub.PanicExit) panic(stub.PanicExit)
} }
func (k *kstub) fatalf(format string, v ...any) { func (k *kstub) fatalf(_ message.Msg, format string, v ...any) {
k.Helper() k.Helper()
if k.Expects("fatalf").Error( if k.Expects("fatalf").Error(
stub.CheckArg(k.Stub, "format", format, 0), stub.CheckArg(k.Stub, "format", format, 0),
@ -722,7 +743,35 @@ func (k *kstub) fatalf(format string, v ...any) {
panic(stub.PanicExit) panic(stub.PanicExit)
} }
func (k *kstub) verbose(v ...any) { func (k *kstub) checkMsg(msg message.Msg) {
k.Helper()
var target *kstub
if state, ok := msg.(*setupState); ok {
target = state.Msg.(*kstub)
} else {
target = msg.(*kstub)
}
if k != target {
panic(fmt.Sprintf("unexpected Msg: %#v", msg))
}
}
func (k *kstub) GetLogger() *log.Logger { panic("unreachable") }
func (k *kstub) IsVerbose() bool { panic("unreachable") }
func (k *kstub) SwapVerbose(verbose bool) bool {
k.Helper()
expect := k.Expects("swapVerbose")
if expect.Error(
stub.CheckArg(k.Stub, "verbose", verbose, 0)) != nil {
k.FailNow()
}
return expect.Ret.(bool)
}
func (k *kstub) Verbose(v ...any) {
k.Helper() k.Helper()
if k.Expects("verbose").Error( if k.Expects("verbose").Error(
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil { stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil {
@ -730,7 +779,7 @@ func (k *kstub) verbose(v ...any) {
} }
} }
func (k *kstub) verbosef(format string, v ...any) { func (k *kstub) Verbosef(format string, v ...any) {
k.Helper() k.Helper()
if k.Expects("verbosef").Error( if k.Expects("verbosef").Error(
stub.CheckArg(k.Stub, "format", format, 0), stub.CheckArg(k.Stub, "format", format, 0),
@ -739,6 +788,6 @@ func (k *kstub) verbosef(format string, v ...any) {
} }
} }
func (k *kstub) suspend() { k.Helper(); k.Expects("suspend") } func (k *kstub) Suspend() bool { k.Helper(); return k.Expects("suspend").Ret.(bool) }
func (k *kstub) resume() bool { k.Helper(); return k.Expects("resume").Ret.(bool) } func (k *kstub) Resume() bool { k.Helper(); return k.Expects("resume").Ret.(bool) }
func (k *kstub) beforeExit() { k.Helper(); k.Expects("beforeExit") } func (k *kstub) BeforeExit() { k.Helper(); k.Expects("beforeExit") }

View File

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

View File

@ -8,11 +8,14 @@ import (
"syscall" "syscall"
"testing" "testing"
"hakurei.app/container/check"
"hakurei.app/container/stub" "hakurei.app/container/stub"
"hakurei.app/container/vfs" "hakurei.app/container/vfs"
) )
func TestMessageFromError(t *testing.T) { func TestMessageFromError(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
err error err error
@ -34,7 +37,7 @@ func TestMessageFromError(t *testing.T) {
Err: stub.UniqueError(0xdeadbeef), Err: stub.UniqueError(0xdeadbeef),
}, "cannot mount /sysroot: unique error 3735928559 injected by the test suite", true}, }, "cannot mount /sysroot: unique error 3735928559 injected by the test suite", true},
{"absolute", &AbsoluteError{"etc/mtab"}, {"absolute", &check.AbsoluteError{Pathname: "etc/mtab"},
`path "etc/mtab" is not absolute`, true}, `path "etc/mtab" is not absolute`, true},
{"repeat", OpRepeatError("autoetc"), {"repeat", OpRepeatError("autoetc"),
@ -43,8 +46,8 @@ func TestMessageFromError(t *testing.T) {
{"state", OpStateError("overlay"), {"state", OpStateError("overlay"),
"impossible overlay state reached", true}, "impossible overlay state reached", true},
{"vfs parse", &vfs.DecoderError{Op: "parse", Line: 0xdeadbeef, Err: &strconv.NumError{Func: "Atoi", Num: "meow", Err: strconv.ErrSyntax}}, {"vfs parse", &vfs.DecoderError{Op: "parse", Line: 0xdead, Err: &strconv.NumError{Func: "Atoi", Num: "meow", Err: strconv.ErrSyntax}},
`cannot parse mountinfo at line 3735928559: numeric field "meow" invalid syntax`, true}, `cannot parse mountinfo at line 57005: numeric field "meow" invalid syntax`, true},
{"tmpfs", TmpfsSizeError(-1), {"tmpfs", TmpfsSizeError(-1),
"tmpfs size -1 out of bounds", true}, "tmpfs size -1 out of bounds", true},
@ -53,6 +56,7 @@ func TestMessageFromError(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, ok := messageFromError(tc.err) got, ok := messageFromError(tc.err)
if got != tc.want { if got != tc.want {
t.Errorf("messageFromError: %q, want %q", got, tc.want) t.Errorf("messageFromError: %q, want %q", got, tc.want)
@ -65,6 +69,8 @@ func TestMessageFromError(t *testing.T) {
} }
func TestMountError(t *testing.T) { func TestMountError(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
err error err error
@ -110,6 +116,7 @@ func TestMountError(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("is", func(t *testing.T) { t.Run("is", func(t *testing.T) {
if !errors.Is(tc.err, tc.errno) { if !errors.Is(tc.err, tc.errno) {
t.Errorf("Is: %#v is not %v", tc.err, tc.errno) t.Errorf("Is: %#v is not %v", tc.err, tc.errno)
@ -124,6 +131,7 @@ func TestMountError(t *testing.T) {
} }
t.Run("zero", func(t *testing.T) { t.Run("zero", func(t *testing.T) {
t.Parallel()
if errors.Is(new(MountError), syscall.Errno(0)) { if errors.Is(new(MountError), syscall.Errno(0)) {
t.Errorf("Is: zero MountError unexpected true") t.Errorf("Is: zero MountError unexpected true")
} }
@ -131,6 +139,8 @@ func TestMountError(t *testing.T) {
} }
func TestErrnoFallback(t *testing.T) { func TestErrnoFallback(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
err error err error
@ -153,6 +163,7 @@ func TestErrnoFallback(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
errno, err := errnoFallback(tc.name, Nonexistent, tc.err) errno, err := errnoFallback(tc.name, Nonexistent, tc.err)
if errno != tc.wantErrno { if errno != tc.wantErrno {
t.Errorf("errnoFallback: errno = %v, want %v", errno, tc.wantErrno) t.Errorf("errnoFallback: errno = %v, want %v", errno, tc.wantErrno)

View File

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

View File

@ -5,13 +5,14 @@ import (
"testing" "testing"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/message"
) )
func TestExecutable(t *testing.T) { func TestExecutable(t *testing.T) {
t.Parallel()
for i := 0; i < 16; i++ { for i := 0; i < 16; i++ {
if got := container.MustExecutable(); got != os.Args[0] { if got := container.MustExecutable(message.New(nil)); got != os.Args[0] {
t.Errorf("MustExecutable: %q, want %q", t.Errorf("MustExecutable: %q, want %q", got, os.Args[0])
got, os.Args[0])
} }
} }
} }

41
container/fhs/abs.go Normal file
View File

@ -0,0 +1,41 @@
package fhs
import (
_ "unsafe"
"hakurei.app/container/check"
)
/* constants in this file bypass abs check, be extremely careful when changing them! */
//go:linkname unsafeAbs hakurei.app/container/check.unsafeAbs
func unsafeAbs(_ string) *check.Absolute
var (
// AbsRoot is [Root] as [check.Absolute].
AbsRoot = unsafeAbs(Root)
// AbsEtc is [Etc] as [check.Absolute].
AbsEtc = unsafeAbs(Etc)
// AbsTmp is [Tmp] as [check.Absolute].
AbsTmp = unsafeAbs(Tmp)
// AbsRun is [Run] as [check.Absolute].
AbsRun = unsafeAbs(Run)
// AbsRunUser is [RunUser] as [check.Absolute].
AbsRunUser = unsafeAbs(RunUser)
// AbsUsrBin is [UsrBin] as [check.Absolute].
AbsUsrBin = unsafeAbs(UsrBin)
// AbsVar is [Var] as [check.Absolute].
AbsVar = unsafeAbs(Var)
// AbsVarLib is [VarLib] as [check.Absolute].
AbsVarLib = unsafeAbs(VarLib)
// AbsDev is [Dev] as [check.Absolute].
AbsDev = unsafeAbs(Dev)
// AbsProc is [Proc] as [check.Absolute].
AbsProc = unsafeAbs(Proc)
// AbsSys is [Sys] as [check.Absolute].
AbsSys = unsafeAbs(Sys)
)

38
container/fhs/fhs.go Normal file
View File

@ -0,0 +1,38 @@
// Package fhs provides constant and checked pathname values for common FHS paths.
package fhs
const (
// Root points to the file system root.
Root = "/"
// Etc points to the directory for system-specific configuration.
Etc = "/etc/"
// Tmp points to the place for small temporary files.
Tmp = "/tmp/"
// Run points to a "tmpfs" file system for system packages to place runtime data, socket files, and similar.
Run = "/run/"
// RunUser points to a directory containing per-user runtime directories,
// each usually individually mounted "tmpfs" instances.
RunUser = Run + "user/"
// Usr points to vendor-supplied operating system resources.
Usr = "/usr/"
// UsrBin points to binaries and executables for user commands that shall appear in the $PATH search path.
UsrBin = Usr + "bin/"
// Var points to persistent, variable system data. Writable during normal system operation.
Var = "/var/"
// VarLib points to persistent system data.
VarLib = Var + "lib/"
// VarEmpty points to a nonstandard directory that is usually empty.
VarEmpty = Var + "empty/"
// Dev points to the root directory for device nodes.
Dev = "/dev/"
// Proc points to a virtual kernel file system exposing the process list and other functionality.
Proc = "/proc/"
// ProcSys points to a hierarchy below /proc/ that exposes a number of kernel tunables.
ProcSys = Proc + "sys/"
// Sys points to a virtual kernel file system exposing discovered devices and other functionality.
Sys = "/sys/"
)

View File

@ -3,6 +3,7 @@ package container
import ( import (
"errors" "errors"
"fmt" "fmt"
"log"
"os" "os"
"os/exec" "os/exec"
"path" "path"
@ -11,7 +12,9 @@ import (
. "syscall" . "syscall"
"time" "time"
"hakurei.app/container/fhs"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/message"
) )
const ( const (
@ -29,7 +32,7 @@ const (
it should be noted that none of this should become relevant at any point since the resulting it should be noted that none of this should become relevant at any point since the resulting
intermediate root tmpfs should be effectively anonymous */ intermediate root tmpfs should be effectively anonymous */
intermediateHostPath = FHSProc + "self/fd" intermediateHostPath = fhs.Proc + "self/fd"
// setup params file descriptor // setup params file descriptor
setupEnv = "HAKUREI_SETUP" setupEnv = "HAKUREI_SETUP"
@ -59,6 +62,7 @@ type (
setupState struct { setupState struct {
nonrepeatable uintptr nonrepeatable uintptr
*Params *Params
message.Msg
} }
) )
@ -91,20 +95,22 @@ type initParams struct {
Verbose bool Verbose bool
} }
func Init(prepareLogger func(prefix string), setVerbose func(verbose bool)) { // Init is called by [TryArgv0] if the current process is the container init.
initEntrypoint(direct{}, prepareLogger, setVerbose) func Init(msg message.Msg) { initEntrypoint(direct{}, msg) }
func initEntrypoint(k syscallDispatcher, msg message.Msg) {
k.lockOSThread()
if msg == nil {
panic("attempting to call initEntrypoint with nil msg")
} }
func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setVerbose func(verbose bool)) {
k.lockOSThread()
prepareLogger("init")
if k.getpid() != 1 { if k.getpid() != 1 {
k.fatal("this process must run as pid 1") k.fatal(msg, "this process must run as pid 1")
} }
if err := k.setPtracer(0); err != nil { if err := k.setPtracer(0); err != nil {
k.verbosef("cannot enable ptrace protection via Yama LSM: %v", err) msg.Verbosef("cannot enable ptrace protection via Yama LSM: %v", err)
// not fatal: this program has no additional privileges at initial program start // not fatal: this program has no additional privileges at initial program start
} }
@ -116,65 +122,65 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
) )
if f, err := k.receive(setupEnv, &params, &setupFd); err != nil { if f, err := k.receive(setupEnv, &params, &setupFd); err != nil {
if errors.Is(err, EBADF) { if errors.Is(err, EBADF) {
k.fatal("invalid setup descriptor") k.fatal(msg, "invalid setup descriptor")
} }
if errors.Is(err, ErrReceiveEnv) { if errors.Is(err, ErrReceiveEnv) {
k.fatal("HAKUREI_SETUP not set") k.fatal(msg, setupEnv+" not set")
} }
k.fatalf("cannot decode init setup payload: %v", err) k.fatalf(msg, "cannot decode init setup payload: %v", err)
} else { } else {
if params.Ops == nil { if params.Ops == nil {
k.fatal("invalid setup parameters") k.fatal(msg, "invalid setup parameters")
} }
if params.ParentPerm == 0 { if params.ParentPerm == 0 {
params.ParentPerm = 0755 params.ParentPerm = 0755
} }
setVerbose(params.Verbose) msg.SwapVerbose(params.Verbose)
k.verbose("received setup parameters") msg.Verbose("received setup parameters")
closeSetup = f closeSetup = f
offsetSetup = int(setupFd + 1) offsetSetup = int(setupFd + 1)
} }
// write uid/gid map here so parent does not need to set dumpable // write uid/gid map here so parent does not need to set dumpable
if err := k.setDumpable(SUID_DUMP_USER); err != nil { if err := k.setDumpable(SUID_DUMP_USER); err != nil {
k.fatalf("cannot set SUID_DUMP_USER: %v", err) k.fatalf(msg, "cannot set SUID_DUMP_USER: %v", err)
} }
if err := k.writeFile(FHSProc+"self/uid_map", if err := k.writeFile(fhs.Proc+"self/uid_map",
append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...), append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...),
0); err != nil { 0); err != nil {
k.fatalf("%v", err) k.fatalf(msg, "%v", err)
} }
if err := k.writeFile(FHSProc+"self/setgroups", if err := k.writeFile(fhs.Proc+"self/setgroups",
[]byte("deny\n"), []byte("deny\n"),
0); err != nil && !os.IsNotExist(err) { 0); err != nil && !os.IsNotExist(err) {
k.fatalf("%v", err) k.fatalf(msg, "%v", err)
} }
if err := k.writeFile(FHSProc+"self/gid_map", if err := k.writeFile(fhs.Proc+"self/gid_map",
append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...), append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...),
0); err != nil { 0); err != nil {
k.fatalf("%v", err) k.fatalf(msg, "%v", err)
} }
if err := k.setDumpable(SUID_DUMP_DISABLE); err != nil { if err := k.setDumpable(SUID_DUMP_DISABLE); err != nil {
k.fatalf("cannot set SUID_DUMP_DISABLE: %v", err) k.fatalf(msg, "cannot set SUID_DUMP_DISABLE: %v", err)
} }
oldmask := k.umask(0) oldmask := k.umask(0)
if params.Hostname != "" { if params.Hostname != "" {
if err := k.sethostname([]byte(params.Hostname)); err != nil { if err := k.sethostname([]byte(params.Hostname)); err != nil {
k.fatalf("cannot set hostname: %v", err) k.fatalf(msg, "cannot set hostname: %v", err)
} }
} }
// cache sysctl before pivot_root // cache sysctl before pivot_root
lastcap := k.lastcap() lastcap := k.lastcap(msg)
if err := k.mount(zeroString, FHSRoot, zeroString, MS_SILENT|MS_SLAVE|MS_REC, zeroString); err != nil { if err := k.mount(zeroString, fhs.Root, zeroString, MS_SILENT|MS_SLAVE|MS_REC, zeroString); err != nil {
k.fatalf("cannot make / rslave: %v", err) k.fatalf(msg, "cannot make / rslave: %v", optionalErrorUnwrap(err))
} }
state := &setupState{Params: &params.Params} state := &setupState{Params: &params.Params, Msg: msg}
/* early is called right before pivot_root into intermediate root; /* early is called right before pivot_root into intermediate root;
this step is mostly for gathering information that would otherwise be difficult to obtain this step is mostly for gathering information that would otherwise be difficult to obtain
@ -182,41 +188,41 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
the state of the mount namespace */ the state of the mount namespace */
for i, op := range *params.Ops { for i, op := range *params.Ops {
if op == nil || !op.Valid() { if op == nil || !op.Valid() {
k.fatalf("invalid op at index %d", i) k.fatalf(msg, "invalid op at index %d", i)
} }
if err := op.early(state, k); err != nil { if err := op.early(state, k); err != nil {
if m, ok := messageFromError(err); ok { if m, ok := messageFromError(err); ok {
k.fatal(m) k.fatal(msg, m)
} else { } else {
k.fatalf("cannot prepare op at index %d: %v", i, err) k.fatalf(msg, "cannot prepare op at index %d: %v", i, err)
} }
} }
} }
if err := k.mount(SourceTmpfsRootfs, intermediateHostPath, FstypeTmpfs, MS_NODEV|MS_NOSUID, zeroString); err != nil { if err := k.mount(SourceTmpfsRootfs, intermediateHostPath, FstypeTmpfs, MS_NODEV|MS_NOSUID, zeroString); err != nil {
k.fatalf("cannot mount intermediate root: %v", err) k.fatalf(msg, "cannot mount intermediate root: %v", optionalErrorUnwrap(err))
} }
if err := k.chdir(intermediateHostPath); err != nil { if err := k.chdir(intermediateHostPath); err != nil {
k.fatalf("cannot enter intermediate host path: %v", err) k.fatalf(msg, "cannot enter intermediate host path: %v", err)
} }
if err := k.mkdir(sysrootDir, 0755); err != nil { if err := k.mkdir(sysrootDir, 0755); err != nil {
k.fatalf("%v", err) k.fatalf(msg, "%v", err)
} }
if err := k.mount(sysrootDir, sysrootDir, zeroString, MS_SILENT|MS_BIND|MS_REC, zeroString); err != nil { if err := k.mount(sysrootDir, sysrootDir, zeroString, MS_SILENT|MS_BIND|MS_REC, zeroString); err != nil {
k.fatalf("cannot bind sysroot: %v", err) k.fatalf(msg, "cannot bind sysroot: %v", optionalErrorUnwrap(err))
} }
if err := k.mkdir(hostDir, 0755); err != nil { if err := k.mkdir(hostDir, 0755); err != nil {
k.fatalf("%v", err) k.fatalf(msg, "%v", err)
} }
// pivot_root uncovers intermediateHostPath in hostDir // pivot_root uncovers intermediateHostPath in hostDir
if err := k.pivotRoot(intermediateHostPath, hostDir); err != nil { if err := k.pivotRoot(intermediateHostPath, hostDir); err != nil {
k.fatalf("cannot pivot into intermediate root: %v", err) k.fatalf(msg, "cannot pivot into intermediate root: %v", err)
} }
if err := k.chdir(FHSRoot); err != nil { if err := k.chdir(fhs.Root); err != nil {
k.fatalf("cannot enter intermediate root: %v", err) k.fatalf(msg, "cannot enter intermediate root: %v", err)
} }
/* apply is called right after pivot_root and entering the new root; /* apply is called right after pivot_root and entering the new root;
@ -226,64 +232,64 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
for i, op := range *params.Ops { for i, op := range *params.Ops {
// ops already checked during early setup // ops already checked during early setup
if prefix, ok := op.prefix(); ok { if prefix, ok := op.prefix(); ok {
k.verbosef("%s %s", prefix, op) msg.Verbosef("%s %s", prefix, op)
} }
if err := op.apply(state, k); err != nil { if err := op.apply(state, k); err != nil {
if m, ok := messageFromError(err); ok { if m, ok := messageFromError(err); ok {
k.fatal(m) k.fatal(msg, m)
} else { } else {
k.fatalf("cannot apply op at index %d: %v", i, err) k.fatalf(msg, "cannot apply op at index %d: %v", i, err)
} }
} }
} }
// setup requiring host root complete at this point // setup requiring host root complete at this point
if err := k.mount(hostDir, hostDir, zeroString, MS_SILENT|MS_REC|MS_PRIVATE, zeroString); err != nil { if err := k.mount(hostDir, hostDir, zeroString, MS_SILENT|MS_REC|MS_PRIVATE, zeroString); err != nil {
k.fatalf("cannot make host root rprivate: %v", err) k.fatalf(msg, "cannot make host root rprivate: %v", optionalErrorUnwrap(err))
} }
if err := k.unmount(hostDir, MNT_DETACH); err != nil { if err := k.unmount(hostDir, MNT_DETACH); err != nil {
k.fatalf("cannot unmount host root: %v", err) k.fatalf(msg, "cannot unmount host root: %v", err)
} }
{ {
var fd int var fd int
if err := IgnoringEINTR(func() (err error) { if err := IgnoringEINTR(func() (err error) {
fd, err = k.open(FHSRoot, O_DIRECTORY|O_RDONLY, 0) fd, err = k.open(fhs.Root, O_DIRECTORY|O_RDONLY, 0)
return return
}); err != nil { }); err != nil {
k.fatalf("cannot open intermediate root: %v", err) k.fatalf(msg, "cannot open intermediate root: %v", err)
} }
if err := k.chdir(sysrootPath); err != nil { if err := k.chdir(sysrootPath); err != nil {
k.fatalf("cannot enter sysroot: %v", err) k.fatalf(msg, "cannot enter sysroot: %v", err)
} }
if err := k.pivotRoot(".", "."); err != nil { if err := k.pivotRoot(".", "."); err != nil {
k.fatalf("cannot pivot into sysroot: %v", err) k.fatalf(msg, "cannot pivot into sysroot: %v", err)
} }
if err := k.fchdir(fd); err != nil { if err := k.fchdir(fd); err != nil {
k.fatalf("cannot re-enter intermediate root: %v", err) k.fatalf(msg, "cannot re-enter intermediate root: %v", err)
} }
if err := k.unmount(".", MNT_DETACH); err != nil { if err := k.unmount(".", MNT_DETACH); err != nil {
k.fatalf("cannot unmount intermediate root: %v", err) k.fatalf(msg, "cannot unmount intermediate root: %v", err)
} }
if err := k.chdir(FHSRoot); err != nil { if err := k.chdir(fhs.Root); err != nil {
k.fatalf("cannot enter root: %v", err) k.fatalf(msg, "cannot enter root: %v", err)
} }
if err := k.close(fd); err != nil { if err := k.close(fd); err != nil {
k.fatalf("cannot close intermediate root: %v", err) k.fatalf(msg, "cannot close intermediate root: %v", err)
} }
} }
if err := k.capAmbientClearAll(); err != nil { if err := k.capAmbientClearAll(); err != nil {
k.fatalf("cannot clear the ambient capability set: %v", err) k.fatalf(msg, "cannot clear the ambient capability set: %v", err)
} }
for i := uintptr(0); i <= lastcap; i++ { for i := uintptr(0); i <= lastcap; i++ {
if params.Privileged && i == CAP_SYS_ADMIN { if params.Privileged && i == CAP_SYS_ADMIN {
continue continue
} }
if err := k.capBoundingSetDrop(i); err != nil { if err := k.capBoundingSetDrop(i); err != nil {
k.fatalf("cannot drop capability from bounding set: %v", err) k.fatalf(msg, "cannot drop capability from bounding set: %v", err)
} }
} }
@ -292,29 +298,29 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN) keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN)
if err := k.capAmbientRaise(CAP_SYS_ADMIN); err != nil { if err := k.capAmbientRaise(CAP_SYS_ADMIN); err != nil {
k.fatalf("cannot raise CAP_SYS_ADMIN: %v", err) k.fatalf(msg, "cannot raise CAP_SYS_ADMIN: %v", err)
} }
} }
if err := k.capset( if err := k.capset(
&capHeader{_LINUX_CAPABILITY_VERSION_3, 0}, &capHeader{_LINUX_CAPABILITY_VERSION_3, 0},
&[2]capData{{0, keep[0], keep[0]}, {0, keep[1], keep[1]}}, &[2]capData{{0, keep[0], keep[0]}, {0, keep[1], keep[1]}},
); err != nil { ); err != nil {
k.fatalf("cannot capset: %v", err) k.fatalf(msg, "cannot capset: %v", err)
} }
if !params.SeccompDisable { if !params.SeccompDisable {
rules := params.SeccompRules rules := params.SeccompRules
if len(rules) == 0 { // non-empty rules slice always overrides presets if len(rules) == 0 { // non-empty rules slice always overrides presets
k.verbosef("resolving presets %#x", params.SeccompPresets) msg.Verbosef("resolving presets %#x", params.SeccompPresets)
rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags) rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags)
} }
if err := k.seccompLoad(rules, params.SeccompFlags); err != nil { if err := k.seccompLoad(rules, params.SeccompFlags); err != nil {
// this also indirectly asserts PR_SET_NO_NEW_PRIVS // this also indirectly asserts PR_SET_NO_NEW_PRIVS
k.fatalf("cannot load syscall filter: %v", err) k.fatalf(msg, "cannot load syscall filter: %v", err)
} }
k.verbosef("%d filter rules loaded", len(rules)) msg.Verbosef("%d filter rules loaded", len(rules))
} else { } else {
k.verbose("syscall filter not configured") msg.Verbose("syscall filter not configured")
} }
extraFiles := make([]*os.File, params.Count) extraFiles := make([]*os.File, params.Count)
@ -331,14 +337,13 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
cmd.ExtraFiles = extraFiles cmd.ExtraFiles = extraFiles
cmd.Dir = params.Dir.String() cmd.Dir = params.Dir.String()
k.verbosef("starting initial program %s", params.Path) msg.Verbosef("starting initial program %s", params.Path)
if err := k.start(cmd); err != nil { if err := k.start(cmd); err != nil {
k.fatalf("%v", err) k.fatalf(msg, "%v", err)
} }
k.suspend()
if err := closeSetup(); err != nil { if err := closeSetup(); err != nil {
k.printf("cannot close setup pipe: %v", err) k.printf(msg, "cannot close setup pipe: %v", err)
// not fatal // not fatal
} }
@ -346,10 +351,14 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
wpid int wpid int
wstatus WaitStatus wstatus WaitStatus
} }
// info is closed as the wait4 thread terminates
// when there are no longer any processes left to reap
info := make(chan winfo, 1) info := make(chan winfo, 1)
done := make(chan struct{})
k.new(func(k syscallDispatcher) { k.new(func(k syscallDispatcher) {
k.lockOSThread()
var ( var (
err error err error
wpid = -2 wpid = -2
@ -372,15 +381,16 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
} }
} }
if !errors.Is(err, ECHILD) { if !errors.Is(err, ECHILD) {
k.printf("unexpected wait4 response: %v", err) k.printf(msg, "unexpected wait4 response: %v", err)
} }
close(done) close(info)
}) })
// handle signals to dump withheld messages // handle signals to dump withheld messages
sig := make(chan os.Signal, 2) sig := make(chan os.Signal, 2)
k.notify(sig, os.Interrupt, CancelSignal) k.notify(sig, CancelSignal,
os.Interrupt, SIGTERM, SIGQUIT)
// 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{})
@ -389,62 +399,73 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV
for { for {
select { select {
case s := <-sig: case s := <-sig:
if k.resume() {
k.verbosef("%s after process start", s.String())
} else {
k.verbosef("got %s", s.String())
}
if s == CancelSignal && params.ForwardCancel && cmd.Process != nil { if s == CancelSignal && params.ForwardCancel && cmd.Process != nil {
k.verbose("forwarding context cancellation") msg.Verbose("forwarding context cancellation")
if err := k.signal(cmd, os.Interrupt); err != nil { if err := k.signal(cmd, os.Interrupt); err != nil {
k.printf("cannot forward cancellation: %v", err) k.printf(msg, "cannot forward cancellation: %v", err)
} }
continue continue
} }
k.beforeExit()
if s == SIGTERM || s == SIGQUIT {
msg.Verbosef("got %s, forwarding to initial process", s.String())
if err := k.signal(cmd, s); err != nil {
k.printf(msg, "cannot forward signal: %v", err)
}
continue
}
msg.Verbosef("got %s", s.String())
msg.BeforeExit()
k.exit(0) k.exit(0)
case w := <-info: case w, ok := <-info:
if w.wpid == cmd.Process.Pid { if !ok {
// initial process exited, output is most likely available again msg.BeforeExit()
k.resume() k.exit(r)
continue // unreachable
}
if w.wpid == cmd.Process.Pid {
switch { switch {
case w.wstatus.Exited(): case w.wstatus.Exited():
r = w.wstatus.ExitStatus() r = w.wstatus.ExitStatus()
k.verbosef("initial process exited with code %d", w.wstatus.ExitStatus()) msg.Verbosef("initial process exited with code %d", w.wstatus.ExitStatus())
case w.wstatus.Signaled(): case w.wstatus.Signaled():
r = 128 + int(w.wstatus.Signal()) r = 128 + int(w.wstatus.Signal())
k.verbosef("initial process exited with signal %s", w.wstatus.Signal()) msg.Verbosef("initial process exited with signal %s", w.wstatus.Signal())
default: default:
r = 255 r = 255
k.verbosef("initial process exited with status %#x", w.wstatus) msg.Verbosef("initial process exited with status %#x", w.wstatus)
} }
go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }() go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
} }
case <-done:
k.beforeExit()
k.exit(r)
case <-timeout: case <-timeout:
k.printf("timeout exceeded waiting for lingering processes") k.printf(msg, "timeout exceeded waiting for lingering processes")
k.beforeExit() msg.BeforeExit()
k.exit(r) k.exit(r)
} }
} }
} }
// initName is the prefix used by log.std in the init process.
const initName = "init" const initName = "init"
// TryArgv0 calls [Init] if the last element of argv0 is "init". // TryArgv0 calls [Init] if the last element of argv0 is "init".
func TryArgv0(v Msg, prepare func(prefix string), setVerbose func(verbose bool)) { // If a nil msg is passed, the system logger is used instead.
func TryArgv0(msg message.Msg) {
if msg == nil {
log.SetPrefix(initName + ": ")
log.SetFlags(0)
msg = message.New(log.Default())
}
if len(os.Args) > 0 && path.Base(os.Args[0]) == initName { if len(os.Args) > 0 && path.Base(os.Args[0]) == initName {
msg = v Init(msg)
Init(prepare, setVerbose)
msg.BeforeExit() msg.BeforeExit()
os.Exit(0) os.Exit(0)
} }

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import (
"syscall" "syscall"
"unsafe" "unsafe"
"hakurei.app/container/seccomp" "hakurei.app/container/std"
) )
// include/uapi/linux/landlock.h // include/uapi/linux/landlock.h
@ -14,7 +14,8 @@ const (
LANDLOCK_CREATE_RULESET_VERSION = 1 << iota LANDLOCK_CREATE_RULESET_VERSION = 1 << iota
) )
type LandlockAccessFS uintptr // LandlockAccessFS is bitmask of handled filesystem actions.
type LandlockAccessFS uint64
const ( const (
LANDLOCK_ACCESS_FS_EXECUTE LandlockAccessFS = 1 << iota LANDLOCK_ACCESS_FS_EXECUTE LandlockAccessFS = 1 << iota
@ -105,7 +106,8 @@ func (f LandlockAccessFS) String() string {
} }
} }
type LandlockAccessNet uintptr // LandlockAccessNet is bitmask of handled network actions.
type LandlockAccessNet uint64
const ( const (
LANDLOCK_ACCESS_NET_BIND_TCP LandlockAccessNet = 1 << iota LANDLOCK_ACCESS_NET_BIND_TCP LandlockAccessNet = 1 << iota
@ -140,7 +142,8 @@ func (f LandlockAccessNet) String() string {
} }
} }
type LandlockScope uintptr // LandlockScope is bitmask of scopes restricting a Landlock domain from accessing outside resources.
type LandlockScope uint64
const ( const (
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET LandlockScope = 1 << iota LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET LandlockScope = 1 << iota
@ -175,6 +178,7 @@ func (f LandlockScope) String() string {
} }
} }
// RulesetAttr is equivalent to struct landlock_ruleset_attr.
type RulesetAttr struct { type RulesetAttr struct {
// Bitmask of handled filesystem actions. // Bitmask of handled filesystem actions.
HandledAccessFS LandlockAccessFS HandledAccessFS LandlockAccessFS
@ -212,7 +216,7 @@ func (rulesetAttr *RulesetAttr) Create(flags uintptr) (fd int, err error) {
size = unsafe.Sizeof(*rulesetAttr) size = unsafe.Sizeof(*rulesetAttr)
} }
rulesetFd, _, errno := syscall.Syscall(seccomp.SYS_LANDLOCK_CREATE_RULESET, pointer, size, flags) rulesetFd, _, errno := syscall.Syscall(std.SYS_LANDLOCK_CREATE_RULESET, pointer, size, flags)
fd = int(rulesetFd) fd = int(rulesetFd)
err = errno err = errno
@ -231,7 +235,7 @@ func LandlockGetABI() (int, error) {
} }
func LandlockRestrictSelf(rulesetFd int, flags uintptr) error { func LandlockRestrictSelf(rulesetFd int, flags uintptr) error {
r, _, errno := syscall.Syscall(seccomp.SYS_LANDLOCK_RESTRICT_SELF, uintptr(rulesetFd), flags, 0) r, _, errno := syscall.Syscall(std.SYS_LANDLOCK_RESTRICT_SELF, uintptr(rulesetFd), flags, 0)
if r != 0 { if r != 0 {
return errno return errno
} }

View File

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

View File

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

View File

@ -10,22 +10,24 @@ import (
) )
func TestBindMount(t *testing.T) { func TestBindMount(t *testing.T) {
t.Parallel()
checkSimple(t, "bindMount", []simpleTestCase{ checkSimple(t, "bindMount", []simpleTestCase{
{"mount", func(k syscallDispatcher) error { {"mount", func(k *kstub) error {
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY) return newProcPaths(k, hostPath).bindMount(nil, "/host/nix", "/sysroot/nix", syscall.MS_RDONLY)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, stub.UniqueError(0xbad)), call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, stub.UniqueError(0xbad)),
}}, stub.UniqueError(0xbad)}, }}, stub.UniqueError(0xbad)},
{"success ne", func(k syscallDispatcher) error { {"success ne", func(k *kstub) error {
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/.host-nix", syscall.MS_RDONLY) return newProcPaths(k, hostPath).bindMount(k, "/host/nix", "/sysroot/.host-nix", syscall.MS_RDONLY)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/.host-nix", "", uintptr(0x9000), ""}, nil, nil), call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/.host-nix", "", uintptr(0x9000), ""}, nil, nil),
call("remount", stub.ExpectArgs{"/sysroot/.host-nix", uintptr(1)}, nil, nil), call("remount", stub.ExpectArgs{"/sysroot/.host-nix", uintptr(1)}, nil, nil),
}}, nil}, }}, nil},
{"success", func(k syscallDispatcher) error { {"success", func(k *kstub) error {
return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY) return newProcPaths(k, hostPath).bindMount(k, "/host/nix", "/sysroot/nix", syscall.MS_RDONLY)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, nil), call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, nil),
call("remount", stub.ExpectArgs{"/sysroot/nix", uintptr(1)}, nil, nil), call("remount", stub.ExpectArgs{"/sysroot/nix", uintptr(1)}, nil, nil),
@ -34,6 +36,8 @@ func TestBindMount(t *testing.T) {
} }
func TestRemount(t *testing.T) { func TestRemount(t *testing.T) {
t.Parallel()
const sampleMountinfoNix = `254 407 253:0 / /host rw,relatime master:1 - ext4 /dev/disk/by-label/nixos rw const sampleMountinfoNix = `254 407 253:0 / /host rw,relatime master:1 - ext4 /dev/disk/by-label/nixos rw
255 254 0:28 / /host/mnt/.ro-cwd ro,noatime master:2 - 9p cwd ro,access=client,msize=16384,trans=virtio 255 254 0:28 / /host/mnt/.ro-cwd ro,noatime master:2 - 9p cwd ro,access=client,msize=16384,trans=virtio
256 254 0:29 / /host/nix/.ro-store rw,relatime master:3 - 9p nix-store rw,cache=f,access=client,msize=16384,trans=virtio 256 254 0:29 / /host/nix/.ro-store rw,relatime master:3 - 9p nix-store rw,cache=f,access=client,msize=16384,trans=virtio
@ -65,8 +69,8 @@ func TestRemount(t *testing.T) {
403 397 0:63 / /host/run/user/1000 rw,nosuid,nodev,relatime master:295 - tmpfs tmpfs rw,size=401060k,nr_inodes=100265,mode=700,uid=1000,gid=100 403 397 0:63 / /host/run/user/1000 rw,nosuid,nodev,relatime master:295 - tmpfs tmpfs rw,size=401060k,nr_inodes=100265,mode=700,uid=1000,gid=100
404 254 0:46 / /host/mnt/cwd rw,relatime master:96 - overlay overlay rw,lowerdir=/mnt/.ro-cwd,upperdir=/tmp/.cwd/upper,workdir=/tmp/.cwd/work 404 254 0:46 / /host/mnt/cwd rw,relatime master:96 - overlay overlay rw,lowerdir=/mnt/.ro-cwd,upperdir=/tmp/.cwd/upper,workdir=/tmp/.cwd/work
405 254 0:47 / /host/mnt/src rw,relatime master:99 - overlay overlay rw,lowerdir=/nix/store/ihcrl3zwvp2002xyylri2wz0drwajx4z-ns0pa7q2b1jpx9pbf1l9352x6rniwxjn-source,upperdir=/tmp/.src/upper,workdir=/tmp/.src/work 405 254 0:47 / /host/mnt/src rw,relatime master:99 - overlay overlay rw,lowerdir=/nix/store/ihcrl3zwvp2002xyylri2wz0drwajx4z-ns0pa7q2b1jpx9pbf1l9352x6rniwxjn-source,upperdir=/tmp/.src/upper,workdir=/tmp/.src/work
407 253 0:65 / / rw,nosuid,nodev,relatime - tmpfs rootfs rw,uid=1000000,gid=1000000 407 253 0:65 / / rw,nosuid,nodev,relatime - tmpfs rootfs rw,uid=10000,gid=10000
408 407 0:65 /sysroot /sysroot rw,nosuid,nodev,relatime - tmpfs rootfs rw,uid=1000000,gid=1000000 408 407 0:65 /sysroot /sysroot rw,nosuid,nodev,relatime - tmpfs rootfs rw,uid=10000,gid=10000
409 408 253:0 /bin /sysroot/bin rw,nosuid,nodev,relatime master:1 - ext4 /dev/disk/by-label/nixos rw 409 408 253:0 /bin /sysroot/bin rw,nosuid,nodev,relatime master:1 - ext4 /dev/disk/by-label/nixos rw
410 408 253:0 /home /sysroot/home rw,nosuid,nodev,relatime master:1 - ext4 /dev/disk/by-label/nixos rw 410 408 253:0 /home /sysroot/home rw,nosuid,nodev,relatime master:1 - ext4 /dev/disk/by-label/nixos rw
411 408 253:0 /lib64 /sysroot/lib64 rw,nosuid,nodev,relatime master:1 - ext4 /dev/disk/by-label/nixos rw 411 408 253:0 /lib64 /sysroot/lib64 rw,nosuid,nodev,relatime master:1 - ext4 /dev/disk/by-label/nixos rw
@ -77,82 +81,82 @@ func TestRemount(t *testing.T) {
416 415 0:30 / /sysroot/nix/store ro,relatime master:5 - overlay overlay rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work` 416 415 0:30 / /sysroot/nix/store ro,relatime master:5 - overlay overlay rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work`
checkSimple(t, "remount", []simpleTestCase{ checkSimple(t, "remount", []simpleTestCase{
{"evalSymlinks", func(k syscallDispatcher) error { {"evalSymlinks", func(k *kstub) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", stub.UniqueError(6)), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", stub.UniqueError(6)),
}}, stub.UniqueError(6)}, }}, stub.UniqueError(6)},
{"open", func(k syscallDispatcher) error { {"open", func(k *kstub) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, stub.UniqueError(5)), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdead, stub.UniqueError(5)),
}}, &os.PathError{Op: "open", Path: "/sysroot/nix", Err: stub.UniqueError(5)}}, }}, &os.PathError{Op: "open", Path: "/sysroot/nix", Err: stub.UniqueError(5)}},
{"readlink", func(k syscallDispatcher) error { {"readlink", func(k *kstub) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdead, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", stub.UniqueError(4)), call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", stub.UniqueError(4)),
}}, stub.UniqueError(4)}, }}, stub.UniqueError(4)},
{"close", func(k syscallDispatcher) error { {"close", func(k *kstub) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdead, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil), call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdeadbeef}, nil, stub.UniqueError(3)), call("close", stub.ExpectArgs{0xdead}, nil, stub.UniqueError(3)),
}}, &os.PathError{Op: "close", Path: "/sysroot/nix", Err: stub.UniqueError(3)}}, }}, &os.PathError{Op: "close", Path: "/sysroot/nix", Err: stub.UniqueError(3)}},
{"mountinfo no match", func(k syscallDispatcher) error { {"mountinfo no match", func(k *kstub) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount(k, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/.hakurei", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/.hakurei", nil),
call("verbosef", stub.ExpectArgs{"target resolves to %q", []any{"/sysroot/.hakurei"}}, nil, nil), call("verbosef", stub.ExpectArgs{"target resolves to %q", []any{"/sysroot/.hakurei"}}, nil, nil),
call("open", stub.ExpectArgs{"/sysroot/.hakurei", 0x280000, uint32(0)}, 0xdeadbeef, nil), call("open", stub.ExpectArgs{"/sysroot/.hakurei", 0x280000, uint32(0)}, 0xdead, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/.hakurei", nil), call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/.hakurei", nil),
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil), call("close", stub.ExpectArgs{0xdead}, nil, nil),
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil), call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
}}, &vfs.DecoderError{Op: "unfold", Line: -1, Err: vfs.UnfoldTargetError("/sysroot/.hakurei")}}, }}, &vfs.DecoderError{Op: "unfold", Line: -1, Err: vfs.UnfoldTargetError("/sysroot/.hakurei")}},
{"mountinfo", func(k syscallDispatcher) error { {"mountinfo", func(k *kstub) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdead, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil), call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil), call("close", stub.ExpectArgs{0xdead}, nil, nil),
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile("\x00"), nil), call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile("\x00"), nil),
}}, &vfs.DecoderError{Op: "parse", Line: 0, Err: vfs.ErrMountInfoFields}}, }}, &vfs.DecoderError{Op: "parse", Line: 0, Err: vfs.ErrMountInfoFields}},
{"mount", func(k syscallDispatcher) error { {"mount", func(k *kstub) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdead, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil), call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil), call("close", stub.ExpectArgs{0xdead}, nil, nil),
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil), call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, stub.UniqueError(2)), call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, stub.UniqueError(2)),
}}, stub.UniqueError(2)}, }}, stub.UniqueError(2)},
{"mount propagate", func(k syscallDispatcher) error { {"mount propagate", func(k *kstub) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdead, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil), call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil), call("close", stub.ExpectArgs{0xdead}, nil, nil),
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil), call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil), call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, stub.UniqueError(1)), call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, stub.UniqueError(1)),
}}, stub.UniqueError(1)}, }}, stub.UniqueError(1)},
{"success toplevel", func(k syscallDispatcher) error { {"success toplevel", func(k *kstub) error {
return newProcPaths(k, hostPath).remount("/sysroot/bin", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount(nil, "/sysroot/bin", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/bin"}, "/sysroot/bin", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/bin"}, "/sysroot/bin", nil),
call("open", stub.ExpectArgs{"/sysroot/bin", 0x280000, uint32(0)}, 0xbabe, nil), call("open", stub.ExpectArgs{"/sysroot/bin", 0x280000, uint32(0)}, 0xbabe, nil),
@ -162,51 +166,51 @@ func TestRemount(t *testing.T) {
call("mount", stub.ExpectArgs{"none", "/sysroot/bin", "", uintptr(0x209027), ""}, nil, nil), call("mount", stub.ExpectArgs{"none", "/sysroot/bin", "", uintptr(0x209027), ""}, nil, nil),
}}, nil}, }}, nil},
{"success EACCES", func(k syscallDispatcher) error { {"success EACCES", func(k *kstub) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdead, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil), call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil), call("close", stub.ExpectArgs{0xdead}, nil, nil),
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil), call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil), call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, syscall.EACCES), call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, syscall.EACCES),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil), call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil),
}}, nil}, }}, nil},
{"success no propagate", func(k syscallDispatcher) error { {"success no propagate", func(k *kstub) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdead, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil), call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil), call("close", stub.ExpectArgs{0xdead}, nil, nil),
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil), call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil), call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil),
}}, nil}, }}, nil},
{"success case sensitive", func(k syscallDispatcher) error { {"success case sensitive", func(k *kstub) error {
return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil),
call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdead, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil), call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil), call("close", stub.ExpectArgs{0xdead}, nil, nil),
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil), call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil), call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, nil), call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil), call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil),
}}, nil}, }}, nil},
{"success", func(k syscallDispatcher) error { {"success", func(k *kstub) error {
return newProcPaths(k, hostPath).remount("/sysroot/.nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) return newProcPaths(k, hostPath).remount(k, "/sysroot/.nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("evalSymlinks", stub.ExpectArgs{"/sysroot/.nix"}, "/sysroot/NIX", nil), call("evalSymlinks", stub.ExpectArgs{"/sysroot/.nix"}, "/sysroot/NIX", nil),
call("verbosef", stub.ExpectArgs{"target resolves to %q", []any{"/sysroot/NIX"}}, nil, nil), call("verbosef", stub.ExpectArgs{"target resolves to %q", []any{"/sysroot/NIX"}}, nil, nil),
call("open", stub.ExpectArgs{"/sysroot/NIX", 0x280000, uint32(0)}, 0xdeadbeef, nil), call("open", stub.ExpectArgs{"/sysroot/NIX", 0x280000, uint32(0)}, 0xdead, nil),
call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", nil), call("readlink", stub.ExpectArgs{"/host/proc/self/fd/57005"}, "/sysroot/nix", nil),
call("close", stub.ExpectArgs{0xdeadbeef}, nil, nil), call("close", stub.ExpectArgs{0xdead}, nil, nil),
call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil), call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil), call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil),
call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, nil), call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, nil),
@ -216,19 +220,21 @@ func TestRemount(t *testing.T) {
} }
func TestRemountWithFlags(t *testing.T) { func TestRemountWithFlags(t *testing.T) {
t.Parallel()
checkSimple(t, "remountWithFlags", []simpleTestCase{ checkSimple(t, "remountWithFlags", []simpleTestCase{
{"noop unmatched", func(k syscallDispatcher) error { {"noop unmatched", func(k *kstub) error {
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime,cat"}}, 0) return remountWithFlags(k, k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime,cat"}}, 0)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("verbosef", stub.ExpectArgs{"unmatched vfs options: %q", []any{[]string{"cat"}}}, nil, nil), call("verbosef", stub.ExpectArgs{"unmatched vfs options: %q", []any{[]string{"cat"}}}, nil, nil),
}}, nil}, }}, nil},
{"noop", func(k syscallDispatcher) error { {"noop", func(k *kstub) error {
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, 0) return remountWithFlags(k, nil, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, 0)
}, stub.Expect{}, nil}, }, stub.Expect{}, nil},
{"success", func(k syscallDispatcher) error { {"success", func(k *kstub) error {
return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, syscall.MS_RDONLY) return remountWithFlags(k, nil, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, syscall.MS_RDONLY)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("mount", stub.ExpectArgs{"none", "", "", uintptr(0x209021), ""}, nil, nil), call("mount", stub.ExpectArgs{"none", "", "", uintptr(0x209021), ""}, nil, nil),
}}, nil}, }}, nil},
@ -236,21 +242,23 @@ func TestRemountWithFlags(t *testing.T) {
} }
func TestMountTmpfs(t *testing.T) { func TestMountTmpfs(t *testing.T) {
t.Parallel()
checkSimple(t, "mountTmpfs", []simpleTestCase{ checkSimple(t, "mountTmpfs", []simpleTestCase{
{"mkdirAll", func(k syscallDispatcher) error { {"mkdirAll", func(k *kstub) error {
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700) return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, stub.UniqueError(0)), call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, stub.UniqueError(0)),
}}, stub.UniqueError(0)}, }}, stub.UniqueError(0)},
{"success no size", func(k syscallDispatcher) error { {"success no size", func(k *kstub) error {
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 0, 0710) return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 0, 0710)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0750)}, nil, nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0750)}, nil, nil),
call("mount", stub.ExpectArgs{"ephemeral", "/sysroot/run/user/1000", "tmpfs", uintptr(0), "mode=0710"}, nil, nil), call("mount", stub.ExpectArgs{"ephemeral", "/sysroot/run/user/1000", "tmpfs", uintptr(0), "mode=0710"}, nil, nil),
}}, nil}, }}, nil},
{"success", func(k syscallDispatcher) error { {"success", func(k *kstub) error {
return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700) return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700)
}, stub.Expect{Calls: []stub.Call{ }, stub.Expect{Calls: []stub.Call{
call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, nil),
@ -260,6 +268,8 @@ func TestMountTmpfs(t *testing.T) {
} }
func TestParentPerm(t *testing.T) { func TestParentPerm(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
perm os.FileMode perm os.FileMode
want os.FileMode want os.FileMode
@ -275,29 +285,10 @@ func TestParentPerm(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.perm.String(), func(t *testing.T) { t.Run(tc.perm.String(), func(t *testing.T) {
t.Parallel()
if got := parentPerm(tc.perm); got != tc.want { if got := parentPerm(tc.perm); got != tc.want {
t.Errorf("parentPerm: %#o, want %#o", got, tc.want) t.Errorf("parentPerm: %#o, want %#o", got, tc.want)
} }
}) })
} }
} }
func TestEscapeOverlayDataSegment(t *testing.T) {
testCases := []struct {
name string
s string
want string
}{
{"zero", zeroString, zeroString},
{"multi", `\\\:,:,\\\`, `\\\\\\\:\,\:\,\\\\\\`},
{"bwrap", `/path :,\`, `/path \:\,\\`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := EscapeOverlayDataSegment(tc.s); got != tc.want {
t.Errorf("escapeOverlayDataSegment: %s, want %s", got, tc.want)
}
})
}
}

View File

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

View File

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

View File

@ -9,13 +9,13 @@ import (
) )
// 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.
func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) { func Setup(extraFiles *[]*os.File) (int, *os.File, error) {
if r, w, err := os.Pipe(); err != nil { if r, w, err := os.Pipe(); err != nil {
return -1, nil, err return -1, nil, err
} else { } else {
fd := 3 + len(*extraFiles) fd := 3 + len(*extraFiles)
*extraFiles = append(*extraFiles, r) *extraFiles = append(*extraFiles, r)
return fd, gob.NewEncoder(w), nil return fd, w, nil
} }
} }
@ -31,7 +31,7 @@ func Receive(key string, e any, fdp *uintptr) (func() error, error) {
return nil, ErrReceiveEnv return nil, ErrReceiveEnv
} else { } else {
if fd, err := strconv.Atoi(s); err != nil { if fd, err := strconv.Atoi(s); err != nil {
return nil, errors.Unwrap(err) return nil, optionalErrorUnwrap(err)
} else { } else {
setup = os.NewFile(uintptr(fd), "setup") setup = os.NewFile(uintptr(fd), "setup")
if setup == nil { if setup == nil {

View File

@ -1,6 +1,7 @@
package container_test package container_test
import ( import (
"encoding/gob"
"errors" "errors"
"os" "os"
"slices" "slices"
@ -55,16 +56,20 @@ func TestSetupReceive(t *testing.T) {
t.Run("setup receive", func(t *testing.T) { t.Run("setup receive", func(t *testing.T) {
check := func(t *testing.T, useNilFdp bool) { check := func(t *testing.T, useNilFdp bool) {
const key = "TEST_SETUP_RECEIVE" const key = "TEST_SETUP_RECEIVE"
payload := []int{syscall.MS_MGC_VAL, syscall.MS_MGC_MSK, syscall.MS_ASYNC, syscall.MS_ACTIVE} payload := []uint64{syscall.MS_MGC_VAL, syscall.MS_MGC_MSK, syscall.MS_ASYNC, syscall.MS_ACTIVE}
encoderDone := make(chan error, 1) encoderDone := make(chan error, 1)
extraFiles := make([]*os.File, 0, 1) extraFiles := make([]*os.File, 0, 1)
if fd, encoder, err := container.Setup(&extraFiles); err != nil { deadline, _ := t.Deadline()
if fd, f, err := container.Setup(&extraFiles); err != nil {
t.Fatalf("Setup: error = %v", err) t.Fatalf("Setup: error = %v", err)
} else if fd != 3 { } else if fd != 3 {
t.Fatalf("Setup: fd = %d, want 3", fd) t.Fatalf("Setup: fd = %d, want 3", fd)
} else { } else {
go func() { encoderDone <- encoder.Encode(payload) }() if err = f.SetDeadline(deadline); err != nil {
t.Fatal(err.Error())
}
go func() { encoderDone <- gob.NewEncoder(f).Encode(payload) }()
} }
if len(extraFiles) != 1 { if len(extraFiles) != 1 {
@ -81,7 +86,7 @@ func TestSetupReceive(t *testing.T) {
} }
var ( var (
gotPayload []int gotPayload []uint64
fdp *uintptr fdp *uintptr
) )
if !useNilFdp { if !useNilFdp {

View File

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

View File

@ -10,6 +10,7 @@ import (
"testing" "testing"
"unsafe" "unsafe"
"hakurei.app/container/check"
"hakurei.app/container/vfs" "hakurei.app/container/vfs"
) )
@ -49,8 +50,8 @@ func TestToHost(t *testing.T) {
} }
} }
// InternalToHostOvlEscape exports toHost passed to EscapeOverlayDataSegment. // InternalToHostOvlEscape exports toHost passed to [check.EscapeOverlayDataSegment].
func InternalToHostOvlEscape(s string) string { return EscapeOverlayDataSegment(toHost(s)) } func InternalToHostOvlEscape(s string) string { return check.EscapeOverlayDataSegment(toHost(s)) }
func TestCreateFile(t *testing.T) { func TestCreateFile(t *testing.T) {
t.Run("nonexistent", func(t *testing.T) { t.Run("nonexistent", func(t *testing.T) {
@ -172,8 +173,8 @@ func TestProcPaths(t *testing.T) {
} }
}) })
t.Run("fd", func(t *testing.T) { t.Run("fd", func(t *testing.T) {
want := "/host/proc/self/fd/9223372036854775807" want := "/host/proc/self/fd/2147483647"
if got := hostProc.fd(math.MaxInt64); got != want { if got := hostProc.fd(math.MaxInt32); got != want {
t.Errorf("stdout: %q, want %q", got, want) t.Errorf("stdout: %q, want %q", got, want)
} }
}) })

View File

@ -9,14 +9,16 @@
#define LEN(arr) (sizeof(arr) / sizeof((arr)[0])) #define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch, int32_t hakurei_scmp_make_filter(int *ret_p, uintptr_t allocate_p,
uint32_t multiarch, uint32_t arch, uint32_t multiarch,
struct hakurei_syscall_rule *rules, struct hakurei_syscall_rule *rules,
size_t rules_sz, hakurei_export_flag flags) { size_t rules_sz, hakurei_export_flag flags) {
int i; int i;
int last_allowed_family; int last_allowed_family;
int disallowed; int disallowed;
struct hakurei_syscall_rule *rule; struct hakurei_syscall_rule *rule;
void *buf;
size_t len = 0;
int32_t res = 0; /* refer to resPrefix for message */ int32_t res = 0; /* refer to resPrefix for message */
@ -108,14 +110,26 @@ int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch,
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1,
SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1)); SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1));
if (fd < 0) { if (allocate_p == 0) {
*ret_p = seccomp_load(ctx); *ret_p = seccomp_load(ctx);
if (*ret_p != 0) { if (*ret_p != 0) {
res = 7; res = 7;
goto out; goto out;
} }
} else { } else {
*ret_p = seccomp_export_bpf(ctx, fd); *ret_p = seccomp_export_bpf_mem(ctx, NULL, &len);
if (*ret_p != 0) {
res = 6;
goto out;
}
buf = hakurei_scmp_allocate(allocate_p, len);
if (buf == NULL) {
res = 4;
goto out;
}
*ret_p = seccomp_export_bpf_mem(ctx, buf, &len);
if (*ret_p != 0) { if (*ret_p != 0) {
res = 6; res = 6;
goto out; goto out;

View File

@ -18,7 +18,8 @@ struct hakurei_syscall_rule {
struct scmp_arg_cmp *arg; struct scmp_arg_cmp *arg;
}; };
int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch, extern void *hakurei_scmp_allocate(uintptr_t f, size_t len);
uint32_t multiarch, int32_t hakurei_scmp_make_filter(int *ret_p, uintptr_t allocate_p,
uint32_t arch, uint32_t multiarch,
struct hakurei_syscall_rule *rules, struct hakurei_syscall_rule *rules,
size_t rules_sz, hakurei_export_flag flags); size_t rules_sz, hakurei_export_flag flags);

View File

@ -3,7 +3,7 @@ package seccomp
/* /*
#cgo linux pkg-config: --static libseccomp #cgo linux pkg-config: --static libseccomp
#include <libseccomp-helper.h> #include "libseccomp-helper.h"
#include <sys/personality.h> #include <sys/personality.h>
*/ */
import "C" import "C"
@ -11,23 +11,23 @@ import (
"errors" "errors"
"fmt" "fmt"
"runtime" "runtime"
"runtime/cgo"
"syscall" "syscall"
"unsafe" "unsafe"
"hakurei.app/container/std"
) )
const ( // ErrInvalidRules is returned for a zero-length rules slice.
PER_LINUX = C.PER_LINUX var ErrInvalidRules = errors.New("invalid native rules slice")
PER_LINUX32 = C.PER_LINUX32
)
var (
ErrInvalidRules = errors.New("invalid native rules slice")
)
// LibraryError represents a libseccomp error. // LibraryError represents a libseccomp error.
type LibraryError struct { type LibraryError struct {
// User facing description of the libseccomp function returning the error.
Prefix string Prefix string
// Negated errno value returned by libseccomp.
Seccomp syscall.Errno Seccomp syscall.Errno
// Global errno value on return.
Errno error Errno error
} }
@ -56,20 +56,16 @@ func (e *LibraryError) Is(err error) bool {
} }
type ( type (
ScmpSyscall = C.int // scmpUint is equivalent to [std.ScmpUint].
ScmpErrno = C.int scmpUint = C.uint
// scmpInt is equivalent to [std.ScmpInt].
scmpInt = C.int
// syscallRule is equivalent to [std.NativeRule].
syscallRule = C.struct_hakurei_syscall_rule
) )
// A NativeRule specifies an arch-specific action taken by seccomp under certain conditions. // ExportFlag configures filter behaviour that are not implemented as rules.
type NativeRule struct {
// Syscall is the arch-dependent syscall number to act against.
Syscall ScmpSyscall
// Errno is the errno value to return when the condition is satisfied.
Errno ScmpErrno
// Arg is the optional struct scmp_arg_cmp passed to libseccomp.
Arg *ScmpArgCmp
}
type ExportFlag = C.hakurei_export_flag type ExportFlag = C.hakurei_export_flag
const ( const (
@ -88,12 +84,23 @@ var resPrefix = [...]string{
3: "seccomp_arch_add failed (multiarch)", 3: "seccomp_arch_add failed (multiarch)",
4: "internal libseccomp failure", 4: "internal libseccomp failure",
5: "seccomp_rule_add failed", 5: "seccomp_rule_add failed",
6: "seccomp_export_bpf failed", 6: "seccomp_export_bpf_mem failed",
7: "seccomp_load failed", 7: "seccomp_load failed",
} }
// Export streams filter contents to fd, or installs it to the current process if fd < 0. // cbAllocateBuffer is the function signature for the function handle passed to hakurei_export_filter
func Export(fd int, rules []NativeRule, flags ExportFlag) error { // which allocates the buffer that the resulting bpf program is copied into, and writes its slice header
// to a value held by the caller.
type cbAllocateBuffer = func(len C.size_t) (buf unsafe.Pointer)
//export hakurei_scmp_allocate
func hakurei_scmp_allocate(f C.uintptr_t, len C.size_t) (buf unsafe.Pointer) {
return cgo.Handle(f).Value().(cbAllocateBuffer)(len)
}
// makeFilter generates a bpf program from a slice of [std.NativeRule] and writes the resulting byte slice to p.
// The filter is installed to the current process if p is nil.
func makeFilter(rules []std.NativeRule, flags ExportFlag, p *[]byte) error {
if len(rules) == 0 { if len(rules) == 0 {
return ErrInvalidRules return ErrInvalidRules
} }
@ -117,36 +124,66 @@ func Export(fd int, rules []NativeRule, flags ExportFlag) error {
var ret C.int var ret C.int
rulesPinner := new(runtime.Pinner) var scmpPinner runtime.Pinner
for i := range rules { for i := range rules {
rule := &rules[i] rule := &rules[i]
rulesPinner.Pin(rule) scmpPinner.Pin(rule)
if rule.Arg != nil { if rule.Arg != nil {
rulesPinner.Pin(rule.Arg) scmpPinner.Pin(rule.Arg)
} }
} }
res, err := C.hakurei_export_filter(
&ret, C.int(fd), var allocateP cgo.Handle
if p != nil {
allocateP = cgo.NewHandle(func(len C.size_t) (buf unsafe.Pointer) {
// this is so the slice header gets a Go pointer
*p = make([]byte, len)
buf = unsafe.Pointer(unsafe.SliceData(*p))
scmpPinner.Pin(buf)
return
})
}
res, err := C.hakurei_scmp_make_filter(
&ret, C.uintptr_t(allocateP),
arch, multiarch, arch, multiarch,
(*C.struct_hakurei_syscall_rule)(unsafe.Pointer(&rules[0])), (*syscallRule)(unsafe.Pointer(&rules[0])),
C.size_t(len(rules)), C.size_t(len(rules)),
flags, flags,
) )
rulesPinner.Unpin() scmpPinner.Unpin()
if p != nil {
allocateP.Delete()
}
if prefix := resPrefix[res]; prefix != "" { if prefix := resPrefix[res]; prefix != "" {
return &LibraryError{ return &LibraryError{prefix, syscall.Errno(-ret), err}
prefix,
-syscall.Errno(ret),
err,
}
} }
return err return err
} }
// ScmpCompare is the equivalent of scmp_compare; // Export generates a bpf program from a slice of [std.NativeRule].
// Comparison operators // Errors returned by libseccomp is wrapped in [LibraryError].
type ScmpCompare = C.enum_scmp_compare func Export(rules []std.NativeRule, flags ExportFlag) (data []byte, err error) {
err = makeFilter(rules, flags, &data)
return
}
// Load generates a bpf program from a slice of [std.NativeRule] and enforces it on the current process.
// Errors returned by libseccomp is wrapped in [LibraryError].
func Load(rules []std.NativeRule, flags ExportFlag) error { return makeFilter(rules, flags, nil) }
type (
// Comparison operators.
scmpCompare = C.enum_scmp_compare
// Argument datum.
scmpDatum = C.scmp_datum_t
// Argument / Value comparison definition.
scmpArgCmp = C.struct_scmp_arg_cmp
)
const ( const (
_SCMP_CMP_MIN = C._SCMP_CMP_MIN _SCMP_CMP_MIN = C._SCMP_CMP_MIN
@ -169,26 +206,19 @@ const (
_SCMP_CMP_MAX = C._SCMP_CMP_MAX _SCMP_CMP_MAX = C._SCMP_CMP_MAX
) )
// ScmpDatum is the equivalent of scmp_datum_t; const (
// Argument datum // PersonaLinux is passed in a [std.ScmpDatum] for filtering calls to syscall.SYS_PERSONALITY.
type ScmpDatum uint64 PersonaLinux = C.PER_LINUX
// PersonaLinux32 is passed in a [std.ScmpDatum] for filtering calls to syscall.SYS_PERSONALITY.
PersonaLinux32 = C.PER_LINUX32
)
// ScmpArgCmp is the equivalent of struct scmp_arg_cmp; // syscallResolveName resolves a syscall number by name via seccomp_syscall_resolve_name.
// Argument / Value comparison definition // This function is only for testing the lookup tables and included here for convenience.
type ScmpArgCmp struct { func syscallResolveName(s string) (trap int, ok bool) {
// argument number, starting at 0
Arg C.uint
// the comparison op, e.g. SCMP_CMP_*
Op ScmpCompare
DatumA, DatumB ScmpDatum
}
// only used for testing
func syscallResolveName(s string) (trap int) {
v := C.CString(s) v := C.CString(s)
trap = int(C.seccomp_syscall_resolve_name(v)) trap = int(C.seccomp_syscall_resolve_name(v))
C.free(unsafe.Pointer(v)) C.free(unsafe.Pointer(v))
ok = trap != C.__NR_SCMP_ERROR
return return
} }

View File

@ -3,15 +3,77 @@ package seccomp_test
import ( import (
"crypto/sha512" "crypto/sha512"
"errors" "errors"
"io"
"slices"
"syscall" "syscall"
"testing" "testing"
. "hakurei.app/container/seccomp" . "hakurei.app/container/seccomp"
. "hakurei.app/container/std"
) )
func TestLibraryError(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
sample *LibraryError
want string
wantIs bool
compare error
}{
{
"full",
&LibraryError{Prefix: "seccomp_export_bpf failed", Seccomp: syscall.ECANCELED, Errno: syscall.EBADF},
"seccomp_export_bpf failed: operation canceled (bad file descriptor)",
true,
&LibraryError{Prefix: "seccomp_export_bpf failed", Seccomp: syscall.ECANCELED, Errno: syscall.EBADF},
},
{
"errno only",
&LibraryError{Prefix: "seccomp_init failed", Errno: syscall.ENOMEM},
"seccomp_init failed: cannot allocate memory",
false,
nil,
},
{
"seccomp only",
&LibraryError{Prefix: "internal libseccomp failure", Seccomp: syscall.EFAULT},
"internal libseccomp failure: bad address",
true,
syscall.EFAULT,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if errors.Is(tc.sample, tc.compare) != tc.wantIs {
t.Errorf("errors.Is(%#v, %#v) did not return %v",
tc.sample, tc.compare, tc.wantIs)
}
if got := tc.sample.Error(); got != tc.want {
t.Errorf("Error: %q, want %q",
got, tc.want)
}
})
}
t.Run("invalid", func(t *testing.T) {
t.Parallel()
wantPanic := "invalid libseccomp error"
defer func() {
if r := recover(); r != wantPanic {
t.Errorf("panic: %q, want %q", r, wantPanic)
}
}()
_ = new(LibraryError).Error()
})
}
func TestExport(t *testing.T) { func TestExport(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
flags ExportFlag flags ExportFlag
@ -31,64 +93,38 @@ func TestExport(t *testing.T) {
{"hakurei tty", 0, PresetExt | PresetDenyNS | PresetDenyDevel, false}, {"hakurei tty", 0, PresetExt | PresetDenyNS | PresetDenyDevel, false},
} }
buf := make([]byte, 8)
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
e := New(Preset(tc.presets, tc.flags), tc.flags) t.Parallel()
want := bpfExpected[bpfPreset{tc.flags, tc.presets}] want := bpfExpected[bpfPreset{tc.flags, tc.presets}]
digest := sha512.New() if data, err := Export(Preset(tc.presets, tc.flags), tc.flags); (err != nil) != tc.wantErr {
t.Errorf("Export: error = %v, wantErr %v", err, tc.wantErr)
if _, err := io.CopyBuffer(digest, e, buf); (err != nil) != tc.wantErr {
t.Errorf("Exporter: error = %v, wantErr %v", err, tc.wantErr)
return return
} } else if got := sha512.Sum512(data); got != want {
if err := e.Close(); err != nil { t.Fatalf("Export: hash = %x, want %x", got, want)
t.Errorf("Close: error = %v", err)
}
if got := digest.Sum(nil); !slices.Equal(got, want) {
t.Fatalf("Export() hash = %x, want %x",
got, want)
return return
} }
}) })
} }
t.Run("close without use", func(t *testing.T) {
e := New(Preset(0, 0), 0)
if err := e.Close(); !errors.Is(err, syscall.EINVAL) {
t.Errorf("Close: error = %v", err)
return
}
})
t.Run("close partial read", func(t *testing.T) {
e := New(Preset(0, 0), 0)
if _, err := e.Read(nil); err != nil {
t.Errorf("Read: error = %v", err)
return
}
// the underlying implementation uses buffered io, so the outcome of this is nondeterministic;
// that is not harmful however, so both outcomes are checked for here
if err := e.Close(); err != nil &&
(!errors.Is(err, syscall.ECANCELED) || !errors.Is(err, syscall.EBADF)) {
t.Errorf("Close: error = %v", err)
return
}
})
} }
func BenchmarkExport(b *testing.B) { func BenchmarkExport(b *testing.B) {
buf := make([]byte, 8) const exportFlags = AllowMultiarch | AllowCAN | AllowBluetooth
const presetFlags = PresetExt | PresetDenyNS | PresetDenyTTY | PresetDenyDevel | PresetLinux32
var want = bpfExpected[bpfPreset{exportFlags, presetFlags}]
for b.Loop() { for b.Loop() {
e := New( data, err := Export(Preset(presetFlags, exportFlags), exportFlags)
Preset(PresetExt|PresetDenyNS|PresetDenyTTY|PresetDenyDevel|PresetLinux32,
AllowMultiarch|AllowCAN|AllowBluetooth), b.StopTimer()
AllowMultiarch|AllowCAN|AllowBluetooth) if err != nil {
if _, err := io.CopyBuffer(io.Discard, e, buf); err != nil { b.Fatalf("Export: error = %v", err)
b.Fatalf("cannot export: %v", err)
}
if err := e.Close(); err != nil {
b.Fatalf("cannot close exporter: %v", err)
} }
if got := sha512.Sum512(data); got != want {
b.Fatalf("Export: hash = %x, want %x", got, want)
return
}
b.StartTimer()
} }
} }

View File

@ -4,27 +4,14 @@ package seccomp
import ( import (
. "syscall" . "syscall"
)
type FilterPreset int . "hakurei.app/container/std"
const (
// PresetExt are project-specific extensions.
PresetExt FilterPreset = 1 << iota
// PresetDenyNS denies namespace setup syscalls.
PresetDenyNS
// PresetDenyTTY denies faking input.
PresetDenyTTY
// PresetDenyDevel denies development-related syscalls.
PresetDenyDevel
// PresetLinux32 sets PER_LINUX32.
PresetLinux32
) )
func Preset(presets FilterPreset, flags ExportFlag) (rules []NativeRule) { func Preset(presets FilterPreset, flags ExportFlag) (rules []NativeRule) {
allowedPersonality := PER_LINUX allowedPersonality := PersonaLinux
if presets&PresetLinux32 != 0 { if presets&PresetLinux32 != 0 {
allowedPersonality = PER_LINUX32 allowedPersonality = PersonaLinux32
} }
presetDevelFinal := presetDevel(ScmpDatum(allowedPersonality)) presetDevelFinal := presetDevel(ScmpDatum(allowedPersonality))

View File

@ -0,0 +1,27 @@
package seccomp_test
import (
. "hakurei.app/container/seccomp"
. "hakurei.app/container/std"
)
var bpfExpected = bpfLookup{
{AllowMultiarch | AllowCAN |
AllowBluetooth, PresetExt |
PresetDenyNS | PresetDenyTTY | PresetDenyDevel |
PresetLinux32}: toHash(
"e67735d24caba42b6801e829ea4393727a36c5e37b8a51e5648e7886047e8454484ff06872aaef810799c29cbd0c1b361f423ad0ef518e33f68436372cc90eb1"),
{0, 0}: toHash(
"5dbcc08a4a1ccd8c12dd0cf6d9817ea6d4f40246e1db7a60e71a50111c4897d69f6fb6d710382d70c18910c2e4fa2d2aeb2daed835dd2fabe3f71def628ade59"),
{0, PresetExt}: toHash(
"d6c0f130dbb5c793d1c10f730455701875778138bd2d03ca009d674842fd97a10815a8c539b76b7801a73de19463938701216b756c053ec91cfe304cba04a0ed"),
{0, PresetStrict}: toHash(
"af7d7b66f2e83f9a850472170c1b83d1371426faa9d0dee4e85b179d3ec75ca92828cb8529eb3012b559497494b2eab4d4b140605e3a26c70dfdbe5efe33c105"),
{0, PresetDenyNS | PresetDenyTTY | PresetDenyDevel}: toHash(
"adfb4397e6eeae8c477d315d58204aae854d60071687b8df4c758e297780e02deee1af48328cef80e16e4d6ab1a66ef13e42247c3475cf447923f15cbc17a6a6"),
{0, PresetExt | PresetDenyDevel}: toHash(
"5d641321460cf54a7036a40a08e845082e1f6d65b9dee75db85ef179f2732f321b16aee2258b74273b04e0d24562e8b1e727930a7e787f41eb5c8aaa0bc22793"),
{0, PresetExt | PresetDenyNS | PresetDenyDevel}: toHash(
"b1f802d39de5897b1e4cb0e82a199f53df0a803ea88e2fd19491fb8c90387c9e2eaa7e323f565fecaa0202a579eb050531f22e6748e04cfd935b8faac35983ec"),
}

View File

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

View File

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

View File

@ -1,28 +1,30 @@
package seccomp_test package seccomp_test
import ( import (
"crypto/sha512"
"encoding/hex" "encoding/hex"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/container/std"
) )
type ( type (
bpfPreset = struct { bpfPreset = struct {
seccomp.ExportFlag seccomp.ExportFlag
seccomp.FilterPreset std.FilterPreset
} }
bpfLookup map[bpfPreset][]byte bpfLookup map[bpfPreset][sha512.Size]byte
) )
func toHash(s string) []byte { func toHash(s string) [sha512.Size]byte {
if len(s) != 128 { if len(s) != sha512.Size*2 {
panic("bad sha512 string length") panic("bad sha512 string length")
} }
if v, err := hex.DecodeString(s); err != nil { if v, err := hex.DecodeString(s); err != nil {
panic(err.Error()) panic(err.Error())
} else if len(v) != 64 { } else if len(v) != sha512.Size {
panic("unreachable") panic("unreachable")
} else { } else {
return v return ([sha512.Size]byte)(v)
} }
} }

View File

@ -1,78 +0,0 @@
package seccomp
import (
"context"
"errors"
"syscall"
"hakurei.app/helper/proc"
)
const (
PresetStrict = PresetExt | PresetDenyNS | PresetDenyTTY | PresetDenyDevel
)
// New returns an inactive Encoder instance.
func New(rules []NativeRule, flags ExportFlag) *Encoder { return &Encoder{newExporter(rules, flags)} }
// Load loads a filter into the kernel.
func Load(rules []NativeRule, flags ExportFlag) error { return Export(-1, rules, flags) }
/*
An Encoder writes a BPF program to an output stream.
Methods of Encoder are not safe for concurrent use.
An Encoder must not be copied after first use.
*/
type Encoder struct {
*exporter
}
func (e *Encoder) Read(p []byte) (n int, err error) {
if err = e.prepare(); err != nil {
return
}
return e.r.Read(p)
}
func (e *Encoder) Close() error {
if e.r == nil {
return syscall.EINVAL
}
// this hangs if the cgo thread fails to exit
return errors.Join(e.closeWrite(), <-e.exportErr)
}
// NewFile returns an instance of exporter implementing [proc.File].
func NewFile(rules []NativeRule, flags ExportFlag) proc.File {
return &File{rules: rules, flags: flags}
}
// File implements [proc.File] and provides access to the read end of exporter pipe.
type File struct {
rules []NativeRule
flags ExportFlag
proc.BaseFile
}
func (f *File) ErrCount() int { return 2 }
func (f *File) Fulfill(ctx context.Context, dispatchErr func(error)) error {
e := newExporter(f.rules, f.flags)
if err := e.prepare(); err != nil {
return err
}
f.Set(e.r)
go func() {
select {
case err := <-e.exportErr:
dispatchErr(nil)
dispatchErr(err)
case <-ctx.Done():
dispatchErr(e.closeWrite())
dispatchErr(<-e.exportErr)
}
}()
return nil
}

View File

@ -1,60 +0,0 @@
// Package seccomp provides high level wrappers around libseccomp.
package seccomp
import (
"os"
"runtime"
"sync"
)
type exporter struct {
rules []NativeRule
flags ExportFlag
r, w *os.File
prepareOnce sync.Once
prepareErr error
closeOnce sync.Once
closeErr error
exportErr <-chan error
}
func (e *exporter) prepare() error {
e.prepareOnce.Do(func() {
if r, w, err := os.Pipe(); err != nil {
e.prepareErr = err
return
} else {
e.r, e.w = r, w
}
ec := make(chan error, 1)
go func(fd uintptr) {
ec <- Export(int(fd), e.rules, e.flags)
close(ec)
_ = e.closeWrite()
runtime.KeepAlive(e.w)
}(e.w.Fd())
e.exportErr = ec
runtime.SetFinalizer(e, (*exporter).closeWrite)
})
return e.prepareErr
}
func (e *exporter) closeWrite() error {
e.closeOnce.Do(func() {
if e.w == nil {
panic("closeWrite called on invalid exporter")
}
e.closeErr = e.w.Close()
// no need for a finalizer anymore
runtime.SetFinalizer(e, nil)
})
return e.closeErr
}
func newExporter(rules []NativeRule, flags ExportFlag) *exporter {
return &exporter{rules: rules, flags: flags}
}

View File

@ -1,65 +0,0 @@
package seccomp_test
import (
"errors"
"runtime"
"syscall"
"testing"
"hakurei.app/container/seccomp"
)
func TestLibraryError(t *testing.T) {
testCases := []struct {
name string
sample *seccomp.LibraryError
want string
wantIs bool
compare error
}{
{
"full",
&seccomp.LibraryError{Prefix: "seccomp_export_bpf failed", Seccomp: syscall.ECANCELED, Errno: syscall.EBADF},
"seccomp_export_bpf failed: operation canceled (bad file descriptor)",
true,
&seccomp.LibraryError{Prefix: "seccomp_export_bpf failed", Seccomp: syscall.ECANCELED, Errno: syscall.EBADF},
},
{
"errno only",
&seccomp.LibraryError{Prefix: "seccomp_init failed", Errno: syscall.ENOMEM},
"seccomp_init failed: cannot allocate memory",
false,
nil,
},
{
"seccomp only",
&seccomp.LibraryError{Prefix: "internal libseccomp failure", Seccomp: syscall.EFAULT},
"internal libseccomp failure: bad address",
true,
syscall.EFAULT,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if errors.Is(tc.sample, tc.compare) != tc.wantIs {
t.Errorf("errors.Is(%#v, %#v) did not return %v",
tc.sample, tc.compare, tc.wantIs)
}
if got := tc.sample.Error(); got != tc.want {
t.Errorf("Error: %q, want %q",
got, tc.want)
}
})
}
t.Run("invalid", func(t *testing.T) {
wantPanic := "invalid libseccomp error"
defer func() {
if r := recover(); r != wantPanic {
t.Errorf("panic: %q, want %q", r, wantPanic)
}
}()
runtime.KeepAlive(new(seccomp.LibraryError).Error())
})
}

View File

@ -0,0 +1,63 @@
package seccomp
import (
"reflect"
"testing"
"unsafe"
"hakurei.app/container/std"
)
func TestSyscallResolveName(t *testing.T) {
t.Parallel()
for name, want := range std.Syscalls() {
t.Run(name, func(t *testing.T) {
t.Parallel()
// this checks the std implementation against libseccomp.
if got, ok := syscallResolveName(name); !ok || got != want {
t.Errorf("syscallResolveName(%q) = %d, want %d", name, got, want)
}
})
}
}
func TestRuleType(t *testing.T) {
assertKind[std.ScmpUint, scmpUint](t)
assertKind[std.ScmpInt, scmpInt](t)
assertSize[std.NativeRule, syscallRule](t)
assertKind[std.ScmpDatum, scmpDatum](t)
assertKind[std.ScmpCompare, scmpCompare](t)
assertSize[std.ScmpArgCmp, scmpArgCmp](t)
}
// assertSize asserts that native and equivalent are of the same size.
func assertSize[native, equivalent any](t *testing.T) {
t.Helper()
got, want := unsafe.Sizeof(*new(native)), unsafe.Sizeof(*new(equivalent))
if got != want {
t.Fatalf("%s: %d, want %d", reflect.TypeFor[native]().Name(), got, want)
}
}
// assertKind asserts that native and equivalent are of the same kind.
func assertKind[native, equivalent any](t *testing.T) {
t.Helper()
assertSize[native, equivalent](t)
nativeType, equivalentType := reflect.TypeFor[native](), reflect.TypeFor[equivalent]()
got, want := nativeType.Kind(), equivalentType.Kind()
if got == reflect.Invalid || want == reflect.Invalid {
t.Fatalf("%s: invalid call to assertKind", nativeType.Name())
}
if got == reflect.Struct {
t.Fatalf("%s: struct is unsupported by assertKind", nativeType.Name())
}
if got != want {
t.Fatalf("%s: %s, want %s", nativeType.Name(), nativeType.Kind(), equivalentType.Kind())
}
}

View File

@ -1,48 +0,0 @@
package seccomp
/*
#cgo linux pkg-config: --static libseccomp
#include <seccomp.h>
*/
import "C"
var syscallNumExtra = map[string]int{
"umount": SYS_UMOUNT,
"subpage_prot": SYS_SUBPAGE_PROT,
"switch_endian": SYS_SWITCH_ENDIAN,
"vm86": SYS_VM86,
"vm86old": SYS_VM86OLD,
"clock_adjtime64": SYS_CLOCK_ADJTIME64,
"clock_settime64": SYS_CLOCK_SETTIME64,
"chown32": SYS_CHOWN32,
"fchown32": SYS_FCHOWN32,
"lchown32": SYS_LCHOWN32,
"setgid32": SYS_SETGID32,
"setgroups32": SYS_SETGROUPS32,
"setregid32": SYS_SETREGID32,
"setresgid32": SYS_SETRESGID32,
"setresuid32": SYS_SETRESUID32,
"setreuid32": SYS_SETREUID32,
"setuid32": SYS_SETUID32,
}
const (
SYS_UMOUNT = C.__SNR_umount
SYS_SUBPAGE_PROT = C.__SNR_subpage_prot
SYS_SWITCH_ENDIAN = C.__SNR_switch_endian
SYS_VM86 = C.__SNR_vm86
SYS_VM86OLD = C.__SNR_vm86old
SYS_CLOCK_ADJTIME64 = C.__SNR_clock_adjtime64
SYS_CLOCK_SETTIME64 = C.__SNR_clock_settime64
SYS_CHOWN32 = C.__SNR_chown32
SYS_FCHOWN32 = C.__SNR_fchown32
SYS_LCHOWN32 = C.__SNR_lchown32
SYS_SETGID32 = C.__SNR_setgid32
SYS_SETGROUPS32 = C.__SNR_setgroups32
SYS_SETREGID32 = C.__SNR_setregid32
SYS_SETRESGID32 = C.__SNR_setresgid32
SYS_SETRESUID32 = C.__SNR_setresuid32
SYS_SETREUID32 = C.__SNR_setreuid32
SYS_SETUID32 = C.__SNR_setuid32
)

View File

@ -1,61 +0,0 @@
package seccomp
/*
#cgo linux pkg-config: --static libseccomp
#include <seccomp.h>
*/
import "C"
import "syscall"
const (
SYS_NEWFSTATAT = syscall.SYS_FSTATAT
)
var syscallNumExtra = map[string]int{
"uselib": SYS_USELIB,
"clock_adjtime64": SYS_CLOCK_ADJTIME64,
"clock_settime64": SYS_CLOCK_SETTIME64,
"umount": SYS_UMOUNT,
"chown": SYS_CHOWN,
"chown32": SYS_CHOWN32,
"fchown32": SYS_FCHOWN32,
"lchown": SYS_LCHOWN,
"lchown32": SYS_LCHOWN32,
"setgid32": SYS_SETGID32,
"setgroups32": SYS_SETGROUPS32,
"setregid32": SYS_SETREGID32,
"setresgid32": SYS_SETRESGID32,
"setresuid32": SYS_SETRESUID32,
"setreuid32": SYS_SETREUID32,
"setuid32": SYS_SETUID32,
"modify_ldt": SYS_MODIFY_LDT,
"subpage_prot": SYS_SUBPAGE_PROT,
"switch_endian": SYS_SWITCH_ENDIAN,
"vm86": SYS_VM86,
"vm86old": SYS_VM86OLD,
}
const (
SYS_USELIB = C.__SNR_uselib
SYS_CLOCK_ADJTIME64 = C.__SNR_clock_adjtime64
SYS_CLOCK_SETTIME64 = C.__SNR_clock_settime64
SYS_UMOUNT = C.__SNR_umount
SYS_CHOWN = C.__SNR_chown
SYS_CHOWN32 = C.__SNR_chown32
SYS_FCHOWN32 = C.__SNR_fchown32
SYS_LCHOWN = C.__SNR_lchown
SYS_LCHOWN32 = C.__SNR_lchown32
SYS_SETGID32 = C.__SNR_setgid32
SYS_SETGROUPS32 = C.__SNR_setgroups32
SYS_SETREGID32 = C.__SNR_setregid32
SYS_SETRESGID32 = C.__SNR_setresgid32
SYS_SETRESUID32 = C.__SNR_setresuid32
SYS_SETREUID32 = C.__SNR_setreuid32
SYS_SETUID32 = C.__SNR_setuid32
SYS_MODIFY_LDT = C.__SNR_modify_ldt
SYS_SUBPAGE_PROT = C.__SNR_subpage_prot
SYS_SWITCH_ENDIAN = C.__SNR_switch_endian
SYS_VM86 = C.__SNR_vm86
SYS_VM86OLD = C.__SNR_vm86old
)

View File

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

32
container/std/bits.go Normal file
View File

@ -0,0 +1,32 @@
// Package std contains constants from container packages without depending on cgo.
package std
const (
// BindOptional skips nonexistent host paths.
BindOptional = 1 << iota
// BindWritable mounts filesystem read-write.
BindWritable
// BindDevice allows access to devices (special files) on this filesystem.
BindDevice
// BindEnsure attempts to create the host path if it does not exist.
BindEnsure
)
// FilterPreset specifies parts of the syscall filter preset to enable.
type FilterPreset int
const (
// PresetExt are project-specific extensions.
PresetExt FilterPreset = 1 << iota
// PresetDenyNS denies namespace setup syscalls.
PresetDenyNS
// PresetDenyTTY denies faking input.
PresetDenyTTY
// PresetDenyDevel denies development-related syscalls.
PresetDenyDevel
// PresetLinux32 sets PER_LINUX32.
PresetLinux32
// PresetStrict is a strict preset useful as a default value.
PresetStrict = PresetExt | PresetDenyNS | PresetDenyTTY | PresetDenyDevel
)

View File

@ -9,6 +9,7 @@ use POSIX ();
my $command = "mksysnum_linux.pl ". join(' ', @ARGV); my $command = "mksysnum_linux.pl ". join(' ', @ARGV);
my $uname_arch = (POSIX::uname)[4]; my $uname_arch = (POSIX::uname)[4];
my %syscall_cutoff_arch = ( my %syscall_cutoff_arch = (
"x86" => 340,
"x86_64" => 302, "x86_64" => 302,
"aarch64" => 281, "aarch64" => 281,
); );
@ -17,7 +18,7 @@ print <<EOF;
// $command // $command
// Code generated by the command above; DO NOT EDIT. // Code generated by the command above; DO NOT EDIT.
package seccomp package std
import . "syscall" import . "syscall"

267
container/std/pnr.go Normal file
View File

@ -0,0 +1,267 @@
// Code generated from include/seccomp-syscalls.h; DO NOT EDIT.
package std
/*
* pseudo syscall definitions
*/
const (
/* socket syscalls */
__PNR_socket = -101
__PNR_bind = -102
__PNR_connect = -103
__PNR_listen = -104
__PNR_accept = -105
__PNR_getsockname = -106
__PNR_getpeername = -107
__PNR_socketpair = -108
__PNR_send = -109
__PNR_recv = -110
__PNR_sendto = -111
__PNR_recvfrom = -112
__PNR_shutdown = -113
__PNR_setsockopt = -114
__PNR_getsockopt = -115
__PNR_sendmsg = -116
__PNR_recvmsg = -117
__PNR_accept4 = -118
__PNR_recvmmsg = -119
__PNR_sendmmsg = -120
/* ipc syscalls */
__PNR_semop = -201
__PNR_semget = -202
__PNR_semctl = -203
__PNR_semtimedop = -204
__PNR_msgsnd = -211
__PNR_msgrcv = -212
__PNR_msgget = -213
__PNR_msgctl = -214
__PNR_shmat = -221
__PNR_shmdt = -222
__PNR_shmget = -223
__PNR_shmctl = -224
/* single syscalls */
__PNR_arch_prctl = -10001
__PNR_bdflush = -10002
__PNR_break = -10003
__PNR_chown32 = -10004
__PNR_epoll_ctl_old = -10005
__PNR_epoll_wait_old = -10006
__PNR_fadvise64_64 = -10007
__PNR_fchown32 = -10008
__PNR_fcntl64 = -10009
__PNR_fstat64 = -10010
__PNR_fstatat64 = -10011
__PNR_fstatfs64 = -10012
__PNR_ftime = -10013
__PNR_ftruncate64 = -10014
__PNR_getegid32 = -10015
__PNR_geteuid32 = -10016
__PNR_getgid32 = -10017
__PNR_getgroups32 = -10018
__PNR_getresgid32 = -10019
__PNR_getresuid32 = -10020
__PNR_getuid32 = -10021
__PNR_gtty = -10022
__PNR_idle = -10023
__PNR_ipc = -10024
__PNR_lchown32 = -10025
__PNR__llseek = -10026
__PNR_lock = -10027
__PNR_lstat64 = -10028
__PNR_mmap2 = -10029
__PNR_mpx = -10030
__PNR_newfstatat = -10031
__PNR__newselect = -10032
__PNR_nice = -10033
__PNR_oldfstat = -10034
__PNR_oldlstat = -10035
__PNR_oldolduname = -10036
__PNR_oldstat = -10037
__PNR_olduname = -10038
__PNR_prof = -10039
__PNR_profil = -10040
__PNR_readdir = -10041
__PNR_security = -10042
__PNR_sendfile64 = -10043
__PNR_setfsgid32 = -10044
__PNR_setfsuid32 = -10045
__PNR_setgid32 = -10046
__PNR_setgroups32 = -10047
__PNR_setregid32 = -10048
__PNR_setresgid32 = -10049
__PNR_setresuid32 = -10050
__PNR_setreuid32 = -10051
__PNR_setuid32 = -10052
__PNR_sgetmask = -10053
__PNR_sigaction = -10054
__PNR_signal = -10055
__PNR_sigpending = -10056
__PNR_sigprocmask = -10057
__PNR_sigreturn = -10058
__PNR_sigsuspend = -10059
__PNR_socketcall = -10060
__PNR_ssetmask = -10061
__PNR_stat64 = -10062
__PNR_statfs64 = -10063
__PNR_stime = -10064
__PNR_stty = -10065
__PNR_truncate64 = -10066
__PNR_tuxcall = -10067
__PNR_ugetrlimit = -10068
__PNR_ulimit = -10069
__PNR_umount = -10070
__PNR_vm86 = -10071
__PNR_vm86old = -10072
__PNR_waitpid = -10073
__PNR_create_module = -10074
__PNR_get_kernel_syms = -10075
__PNR_get_thread_area = -10076
__PNR_nfsservctl = -10077
__PNR_query_module = -10078
__PNR_set_thread_area = -10079
__PNR__sysctl = -10080
__PNR_uselib = -10081
__PNR_vserver = -10082
__PNR_arm_fadvise64_64 = -10083
__PNR_arm_sync_file_range = -10084
__PNR_pciconfig_iobase = -10086
__PNR_pciconfig_read = -10087
__PNR_pciconfig_write = -10088
__PNR_sync_file_range2 = -10089
__PNR_syscall = -10090
__PNR_afs_syscall = -10091
__PNR_fadvise64 = -10092
__PNR_getpmsg = -10093
__PNR_ioperm = -10094
__PNR_iopl = -10095
__PNR_migrate_pages = -10097
__PNR_modify_ldt = -10098
__PNR_putpmsg = -10099
__PNR_sync_file_range = -10100
__PNR_select = -10101
__PNR_vfork = -10102
__PNR_cachectl = -10103
__PNR_cacheflush = -10104
__PNR_sysmips = -10106
__PNR_timerfd = -10107
__PNR_time = -10108
__PNR_getrandom = -10109
__PNR_memfd_create = -10110
__PNR_kexec_file_load = -10111
__PNR_sysfs = -10145
__PNR_oldwait4 = -10146
__PNR_access = -10147
__PNR_alarm = -10148
__PNR_chmod = -10149
__PNR_chown = -10150
__PNR_creat = -10151
__PNR_dup2 = -10152
__PNR_epoll_create = -10153
__PNR_epoll_wait = -10154
__PNR_eventfd = -10155
__PNR_fork = -10156
__PNR_futimesat = -10157
__PNR_getdents = -10158
__PNR_getpgrp = -10159
__PNR_inotify_init = -10160
__PNR_lchown = -10161
__PNR_link = -10162
__PNR_lstat = -10163
__PNR_mkdir = -10164
__PNR_mknod = -10165
__PNR_open = -10166
__PNR_pause = -10167
__PNR_pipe = -10168
__PNR_poll = -10169
__PNR_readlink = -10170
__PNR_rename = -10171
__PNR_rmdir = -10172
__PNR_signalfd = -10173
__PNR_stat = -10174
__PNR_symlink = -10175
__PNR_unlink = -10176
__PNR_ustat = -10177
__PNR_utime = -10178
__PNR_utimes = -10179
__PNR_getrlimit = -10180
__PNR_mmap = -10181
__PNR_breakpoint = -10182
__PNR_set_tls = -10183
__PNR_usr26 = -10184
__PNR_usr32 = -10185
__PNR_multiplexer = -10186
__PNR_rtas = -10187
__PNR_spu_create = -10188
__PNR_spu_run = -10189
__PNR_swapcontext = -10190
__PNR_sys_debug_setcontext = -10191
__PNR_switch_endian = -10191
__PNR_get_mempolicy = -10192
__PNR_move_pages = -10193
__PNR_mbind = -10194
__PNR_set_mempolicy = -10195
__PNR_s390_runtime_instr = -10196
__PNR_s390_pci_mmio_read = -10197
__PNR_s390_pci_mmio_write = -10198
__PNR_membarrier = -10199
__PNR_userfaultfd = -10200
__PNR_pkey_mprotect = -10201
__PNR_pkey_alloc = -10202
__PNR_pkey_free = -10203
__PNR_get_tls = -10204
__PNR_s390_guarded_storage = -10205
__PNR_s390_sthyi = -10206
__PNR_subpage_prot = -10207
__PNR_statx = -10208
__PNR_io_pgetevents = -10209
__PNR_rseq = -10210
__PNR_setrlimit = -10211
__PNR_clock_adjtime64 = -10212
__PNR_clock_getres_time64 = -10213
__PNR_clock_gettime64 = -10214
__PNR_clock_nanosleep_time64 = -10215
__PNR_clock_settime64 = -10216
__PNR_clone3 = -10217
__PNR_fsconfig = -10218
__PNR_fsmount = -10219
__PNR_fsopen = -10220
__PNR_fspick = -10221
__PNR_futex_time64 = -10222
__PNR_io_pgetevents_time64 = -10223
__PNR_move_mount = -10224
__PNR_mq_timedreceive_time64 = -10225
__PNR_mq_timedsend_time64 = -10226
__PNR_open_tree = -10227
__PNR_pidfd_open = -10228
__PNR_pidfd_send_signal = -10229
__PNR_ppoll_time64 = -10230
__PNR_pselect6_time64 = -10231
__PNR_recvmmsg_time64 = -10232
__PNR_rt_sigtimedwait_time64 = -10233
__PNR_sched_rr_get_interval_time64 = -10234
__PNR_semtimedop_time64 = -10235
__PNR_timer_gettime64 = -10236
__PNR_timer_settime64 = -10237
__PNR_timerfd_gettime64 = -10238
__PNR_timerfd_settime64 = -10239
__PNR_utimensat_time64 = -10240
__PNR_ppoll = -10241
__PNR_renameat = -10242
__PNR_riscv_flush_icache = -10243
__PNR_memfd_secret = -10244
__PNR_map_shadow_stack = -10245
__PNR_fstat = -10246
__PNR_atomic_barrier = -10247
__PNR_atomic_cmpxchg_32 = -10248
__PNR_getpagesize = -10249
__PNR_riscv_hwprobe = -10250
__PNR_uretprobe = -10251
)

76
container/std/seccomp.go Normal file
View File

@ -0,0 +1,76 @@
package std
import (
"encoding/json"
"strconv"
)
type (
// ScmpUint is equivalent to C.uint.
ScmpUint uint32
// ScmpInt is equivalent to C.int.
ScmpInt int32
// ScmpSyscall represents a syscall number passed to libseccomp via [NativeRule.Syscall].
ScmpSyscall ScmpInt
// ScmpErrno represents an errno value passed to libseccomp via [NativeRule.Errno].
ScmpErrno ScmpInt
// ScmpCompare is equivalent to enum scmp_compare;
ScmpCompare ScmpUint
// ScmpDatum is equivalent to scmp_datum_t.
ScmpDatum uint64
// ScmpArgCmp is equivalent to struct scmp_arg_cmp.
ScmpArgCmp struct {
// argument number, starting at 0
Arg ScmpUint `json:"arg"`
// the comparison op, e.g. SCMP_CMP_*
Op ScmpCompare `json:"op"`
DatumA ScmpDatum `json:"a,omitempty"`
DatumB ScmpDatum `json:"b,omitempty"`
}
// A NativeRule specifies an arch-specific action taken by seccomp under certain conditions.
NativeRule struct {
// Syscall is the arch-dependent syscall number to act against.
Syscall ScmpSyscall `json:"syscall"`
// Errno is the errno value to return when the condition is satisfied.
Errno ScmpErrno `json:"errno"`
// Arg is the optional struct scmp_arg_cmp passed to libseccomp.
Arg *ScmpArgCmp `json:"arg,omitempty"`
}
)
// MarshalJSON resolves the name of [ScmpSyscall] and encodes it as a [json] string.
// If such a name does not exist, the syscall number is encoded instead.
func (num *ScmpSyscall) MarshalJSON() ([]byte, error) {
n := int(*num)
for name, cur := range Syscalls() {
if cur == n {
return json.Marshal(name)
}
}
return json.Marshal(n)
}
// SyscallNameError is returned when trying to unmarshal an invalid syscall name into [ScmpSyscall].
type SyscallNameError string
func (e SyscallNameError) Error() string { return "invalid syscall name " + strconv.Quote(string(e)) }
// UnmarshalJSON looks up the syscall number corresponding to name encoded in data
// by calling [SyscallResolveName].
func (num *ScmpSyscall) UnmarshalJSON(data []byte) error {
var name string
if err := json.Unmarshal(data, &name); err != nil {
return err
}
if n, ok := SyscallResolveName(name); !ok {
return SyscallNameError(name)
} else {
*num = ScmpSyscall(n)
return nil
}
}

View File

@ -0,0 +1,63 @@
package std_test
import (
"encoding/json"
"errors"
"math"
"reflect"
"syscall"
"testing"
"hakurei.app/container/std"
)
func TestScmpSyscall(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
data string
want std.ScmpSyscall
err error
}{
{"select", `"select"`, syscall.SYS_SELECT, nil},
{"clone3", `"clone3"`, std.SYS_CLONE3, nil},
{"oob", `-2147483647`, -math.MaxInt32,
&json.UnmarshalTypeError{Value: "number", Type: reflect.TypeFor[string](), Offset: 11}},
{"name", `"nonexistent_syscall"`, -math.MaxInt32,
std.SyscallNameError("nonexistent_syscall")},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("decode", func(t *testing.T) {
var got std.ScmpSyscall
if err := json.Unmarshal([]byte(tc.data), &got); !reflect.DeepEqual(err, tc.err) {
t.Fatalf("Unmarshal: error = %#v, want %#v", err, tc.err)
} else if err == nil && got != tc.want {
t.Errorf("Unmarshal: %v, want %v", got, tc.want)
}
})
if errors.As(tc.err, new(std.SyscallNameError)) {
return
}
t.Run("encode", func(t *testing.T) {
if got, err := json.Marshal(&tc.want); err != nil {
t.Fatalf("Marshal: error = %v", err)
} else if string(got) != tc.data {
t.Errorf("Marshal: %s, want %s", string(got), tc.data)
}
})
})
}
t.Run("error", func(t *testing.T) {
const want = `invalid syscall name "\x00"`
if got := std.SyscallNameError("\x00").Error(); got != want {
t.Fatalf("Error: %q, want %q", got, want)
}
})
}

View File

@ -1,4 +1,4 @@
package seccomp package std
import "iter" import "iter"

View File

@ -0,0 +1,13 @@
package std
var syscallNumExtra = map[string]int{
"kexec_file_load": SYS_KEXEC_FILE_LOAD,
"subpage_prot": SYS_SUBPAGE_PROT,
"switch_endian": SYS_SWITCH_ENDIAN,
}
const (
SYS_KEXEC_FILE_LOAD = __PNR_kexec_file_load
SYS_SUBPAGE_PROT = __PNR_subpage_prot
SYS_SWITCH_ENDIAN = __PNR_switch_endian
)

View File

@ -0,0 +1,41 @@
package std
var syscallNumExtra = map[string]int{
"umount": SYS_UMOUNT,
"subpage_prot": SYS_SUBPAGE_PROT,
"switch_endian": SYS_SWITCH_ENDIAN,
"vm86": SYS_VM86,
"vm86old": SYS_VM86OLD,
"clock_adjtime64": SYS_CLOCK_ADJTIME64,
"clock_settime64": SYS_CLOCK_SETTIME64,
"chown32": SYS_CHOWN32,
"fchown32": SYS_FCHOWN32,
"lchown32": SYS_LCHOWN32,
"setgid32": SYS_SETGID32,
"setgroups32": SYS_SETGROUPS32,
"setregid32": SYS_SETREGID32,
"setresgid32": SYS_SETRESGID32,
"setresuid32": SYS_SETRESUID32,
"setreuid32": SYS_SETREUID32,
"setuid32": SYS_SETUID32,
}
const (
SYS_UMOUNT = __PNR_umount
SYS_SUBPAGE_PROT = __PNR_subpage_prot
SYS_SWITCH_ENDIAN = __PNR_switch_endian
SYS_VM86 = __PNR_vm86
SYS_VM86OLD = __PNR_vm86old
SYS_CLOCK_ADJTIME64 = __PNR_clock_adjtime64
SYS_CLOCK_SETTIME64 = __PNR_clock_settime64
SYS_CHOWN32 = __PNR_chown32
SYS_FCHOWN32 = __PNR_fchown32
SYS_LCHOWN32 = __PNR_lchown32
SYS_SETGID32 = __PNR_setgid32
SYS_SETGROUPS32 = __PNR_setgroups32
SYS_SETREGID32 = __PNR_setregid32
SYS_SETRESGID32 = __PNR_setresgid32
SYS_SETRESUID32 = __PNR_setresuid32
SYS_SETREUID32 = __PNR_setreuid32
SYS_SETUID32 = __PNR_setuid32
)

View File

@ -0,0 +1,55 @@
package std
import "syscall"
const (
SYS_NEWFSTATAT = syscall.SYS_FSTATAT
)
var syscallNumExtra = map[string]int{
"uselib": SYS_USELIB,
"clock_adjtime64": SYS_CLOCK_ADJTIME64,
"clock_settime64": SYS_CLOCK_SETTIME64,
"umount": SYS_UMOUNT,
"chown": SYS_CHOWN,
"chown32": SYS_CHOWN32,
"fchown32": SYS_FCHOWN32,
"lchown": SYS_LCHOWN,
"lchown32": SYS_LCHOWN32,
"setgid32": SYS_SETGID32,
"setgroups32": SYS_SETGROUPS32,
"setregid32": SYS_SETREGID32,
"setresgid32": SYS_SETRESGID32,
"setresuid32": SYS_SETRESUID32,
"setreuid32": SYS_SETREUID32,
"setuid32": SYS_SETUID32,
"modify_ldt": SYS_MODIFY_LDT,
"subpage_prot": SYS_SUBPAGE_PROT,
"switch_endian": SYS_SWITCH_ENDIAN,
"vm86": SYS_VM86,
"vm86old": SYS_VM86OLD,
}
const (
SYS_USELIB = __PNR_uselib
SYS_CLOCK_ADJTIME64 = __PNR_clock_adjtime64
SYS_CLOCK_SETTIME64 = __PNR_clock_settime64
SYS_UMOUNT = __PNR_umount
SYS_CHOWN = __PNR_chown
SYS_CHOWN32 = __PNR_chown32
SYS_FCHOWN32 = __PNR_fchown32
SYS_LCHOWN = __PNR_lchown
SYS_LCHOWN32 = __PNR_lchown32
SYS_SETGID32 = __PNR_setgid32
SYS_SETGROUPS32 = __PNR_setgroups32
SYS_SETREGID32 = __PNR_setregid32
SYS_SETRESGID32 = __PNR_setresgid32
SYS_SETRESUID32 = __PNR_setresuid32
SYS_SETREUID32 = __PNR_setreuid32
SYS_SETUID32 = __PNR_setuid32
SYS_MODIFY_LDT = __PNR_modify_ldt
SYS_SUBPAGE_PROT = __PNR_subpage_prot
SYS_SWITCH_ENDIAN = __PNR_switch_endian
SYS_VM86 = __PNR_vm86
SYS_VM86OLD = __PNR_vm86old
)

View File

@ -0,0 +1,579 @@
// mksysnum_linux.pl /usr/include/asm/unistd_32.h
// Code generated by the command above; DO NOT EDIT.
package std
import . "syscall"
var syscallNum = map[string]int{
"restart_syscall": SYS_RESTART_SYSCALL,
"exit": SYS_EXIT,
"fork": SYS_FORK,
"read": SYS_READ,
"write": SYS_WRITE,
"open": SYS_OPEN,
"close": SYS_CLOSE,
"waitpid": SYS_WAITPID,
"creat": SYS_CREAT,
"link": SYS_LINK,
"unlink": SYS_UNLINK,
"execve": SYS_EXECVE,
"chdir": SYS_CHDIR,
"time": SYS_TIME,
"mknod": SYS_MKNOD,
"chmod": SYS_CHMOD,
"lchown": SYS_LCHOWN,
"break": SYS_BREAK,
"oldstat": SYS_OLDSTAT,
"lseek": SYS_LSEEK,
"getpid": SYS_GETPID,
"mount": SYS_MOUNT,
"umount": SYS_UMOUNT,
"setuid": SYS_SETUID,
"getuid": SYS_GETUID,
"stime": SYS_STIME,
"ptrace": SYS_PTRACE,
"alarm": SYS_ALARM,
"oldfstat": SYS_OLDFSTAT,
"pause": SYS_PAUSE,
"utime": SYS_UTIME,
"stty": SYS_STTY,
"gtty": SYS_GTTY,
"access": SYS_ACCESS,
"nice": SYS_NICE,
"ftime": SYS_FTIME,
"sync": SYS_SYNC,
"kill": SYS_KILL,
"rename": SYS_RENAME,
"mkdir": SYS_MKDIR,
"rmdir": SYS_RMDIR,
"dup": SYS_DUP,
"pipe": SYS_PIPE,
"times": SYS_TIMES,
"prof": SYS_PROF,
"brk": SYS_BRK,
"setgid": SYS_SETGID,
"getgid": SYS_GETGID,
"signal": SYS_SIGNAL,
"geteuid": SYS_GETEUID,
"getegid": SYS_GETEGID,
"acct": SYS_ACCT,
"umount2": SYS_UMOUNT2,
"lock": SYS_LOCK,
"ioctl": SYS_IOCTL,
"fcntl": SYS_FCNTL,
"mpx": SYS_MPX,
"setpgid": SYS_SETPGID,
"ulimit": SYS_ULIMIT,
"oldolduname": SYS_OLDOLDUNAME,
"umask": SYS_UMASK,
"chroot": SYS_CHROOT,
"ustat": SYS_USTAT,
"dup2": SYS_DUP2,
"getppid": SYS_GETPPID,
"getpgrp": SYS_GETPGRP,
"setsid": SYS_SETSID,
"sigaction": SYS_SIGACTION,
"sgetmask": SYS_SGETMASK,
"ssetmask": SYS_SSETMASK,
"setreuid": SYS_SETREUID,
"setregid": SYS_SETREGID,
"sigsuspend": SYS_SIGSUSPEND,
"sigpending": SYS_SIGPENDING,
"sethostname": SYS_SETHOSTNAME,
"setrlimit": SYS_SETRLIMIT,
"getrlimit": SYS_GETRLIMIT,
"getrusage": SYS_GETRUSAGE,
"gettimeofday": SYS_GETTIMEOFDAY,
"settimeofday": SYS_SETTIMEOFDAY,
"getgroups": SYS_GETGROUPS,
"setgroups": SYS_SETGROUPS,
"select": SYS_SELECT,
"symlink": SYS_SYMLINK,
"oldlstat": SYS_OLDLSTAT,
"readlink": SYS_READLINK,
"uselib": SYS_USELIB,
"swapon": SYS_SWAPON,
"reboot": SYS_REBOOT,
"readdir": SYS_READDIR,
"mmap": SYS_MMAP,
"munmap": SYS_MUNMAP,
"truncate": SYS_TRUNCATE,
"ftruncate": SYS_FTRUNCATE,
"fchmod": SYS_FCHMOD,
"fchown": SYS_FCHOWN,
"getpriority": SYS_GETPRIORITY,
"setpriority": SYS_SETPRIORITY,
"profil": SYS_PROFIL,
"statfs": SYS_STATFS,
"fstatfs": SYS_FSTATFS,
"ioperm": SYS_IOPERM,
"socketcall": SYS_SOCKETCALL,
"syslog": SYS_SYSLOG,
"setitimer": SYS_SETITIMER,
"getitimer": SYS_GETITIMER,
"stat": SYS_STAT,
"lstat": SYS_LSTAT,
"fstat": SYS_FSTAT,
"olduname": SYS_OLDUNAME,
"iopl": SYS_IOPL,
"vhangup": SYS_VHANGUP,
"idle": SYS_IDLE,
"vm86old": SYS_VM86OLD,
"wait4": SYS_WAIT4,
"swapoff": SYS_SWAPOFF,
"sysinfo": SYS_SYSINFO,
"ipc": SYS_IPC,
"fsync": SYS_FSYNC,
"sigreturn": SYS_SIGRETURN,
"clone": SYS_CLONE,
"setdomainname": SYS_SETDOMAINNAME,
"uname": SYS_UNAME,
"modify_ldt": SYS_MODIFY_LDT,
"adjtimex": SYS_ADJTIMEX,
"mprotect": SYS_MPROTECT,
"sigprocmask": SYS_SIGPROCMASK,
"create_module": SYS_CREATE_MODULE,
"init_module": SYS_INIT_MODULE,
"delete_module": SYS_DELETE_MODULE,
"get_kernel_syms": SYS_GET_KERNEL_SYMS,
"quotactl": SYS_QUOTACTL,
"getpgid": SYS_GETPGID,
"fchdir": SYS_FCHDIR,
"bdflush": SYS_BDFLUSH,
"sysfs": SYS_SYSFS,
"personality": SYS_PERSONALITY,
"afs_syscall": SYS_AFS_SYSCALL,
"setfsuid": SYS_SETFSUID,
"setfsgid": SYS_SETFSGID,
"_llseek": SYS__LLSEEK,
"getdents": SYS_GETDENTS,
"_newselect": SYS__NEWSELECT,
"flock": SYS_FLOCK,
"msync": SYS_MSYNC,
"readv": SYS_READV,
"writev": SYS_WRITEV,
"getsid": SYS_GETSID,
"fdatasync": SYS_FDATASYNC,
"_sysctl": SYS__SYSCTL,
"mlock": SYS_MLOCK,
"munlock": SYS_MUNLOCK,
"mlockall": SYS_MLOCKALL,
"munlockall": SYS_MUNLOCKALL,
"sched_setparam": SYS_SCHED_SETPARAM,
"sched_getparam": SYS_SCHED_GETPARAM,
"sched_setscheduler": SYS_SCHED_SETSCHEDULER,
"sched_getscheduler": SYS_SCHED_GETSCHEDULER,
"sched_yield": SYS_SCHED_YIELD,
"sched_get_priority_max": SYS_SCHED_GET_PRIORITY_MAX,
"sched_get_priority_min": SYS_SCHED_GET_PRIORITY_MIN,
"sched_rr_get_interval": SYS_SCHED_RR_GET_INTERVAL,
"nanosleep": SYS_NANOSLEEP,
"mremap": SYS_MREMAP,
"setresuid": SYS_SETRESUID,
"getresuid": SYS_GETRESUID,
"vm86": SYS_VM86,
"query_module": SYS_QUERY_MODULE,
"poll": SYS_POLL,
"nfsservctl": SYS_NFSSERVCTL,
"setresgid": SYS_SETRESGID,
"getresgid": SYS_GETRESGID,
"prctl": SYS_PRCTL,
"rt_sigreturn": SYS_RT_SIGRETURN,
"rt_sigaction": SYS_RT_SIGACTION,
"rt_sigprocmask": SYS_RT_SIGPROCMASK,
"rt_sigpending": SYS_RT_SIGPENDING,
"rt_sigtimedwait": SYS_RT_SIGTIMEDWAIT,
"rt_sigqueueinfo": SYS_RT_SIGQUEUEINFO,
"rt_sigsuspend": SYS_RT_SIGSUSPEND,
"pread64": SYS_PREAD64,
"pwrite64": SYS_PWRITE64,
"chown": SYS_CHOWN,
"getcwd": SYS_GETCWD,
"capget": SYS_CAPGET,
"capset": SYS_CAPSET,
"sigaltstack": SYS_SIGALTSTACK,
"sendfile": SYS_SENDFILE,
"getpmsg": SYS_GETPMSG,
"putpmsg": SYS_PUTPMSG,
"vfork": SYS_VFORK,
"ugetrlimit": SYS_UGETRLIMIT,
"mmap2": SYS_MMAP2,
"truncate64": SYS_TRUNCATE64,
"ftruncate64": SYS_FTRUNCATE64,
"stat64": SYS_STAT64,
"lstat64": SYS_LSTAT64,
"fstat64": SYS_FSTAT64,
"lchown32": SYS_LCHOWN32,
"getuid32": SYS_GETUID32,
"getgid32": SYS_GETGID32,
"geteuid32": SYS_GETEUID32,
"getegid32": SYS_GETEGID32,
"setreuid32": SYS_SETREUID32,
"setregid32": SYS_SETREGID32,
"getgroups32": SYS_GETGROUPS32,
"setgroups32": SYS_SETGROUPS32,
"fchown32": SYS_FCHOWN32,
"setresuid32": SYS_SETRESUID32,
"getresuid32": SYS_GETRESUID32,
"setresgid32": SYS_SETRESGID32,
"getresgid32": SYS_GETRESGID32,
"chown32": SYS_CHOWN32,
"setuid32": SYS_SETUID32,
"setgid32": SYS_SETGID32,
"setfsuid32": SYS_SETFSUID32,
"setfsgid32": SYS_SETFSGID32,
"pivot_root": SYS_PIVOT_ROOT,
"mincore": SYS_MINCORE,
"madvise": SYS_MADVISE,
"getdents64": SYS_GETDENTS64,
"fcntl64": SYS_FCNTL64,
"gettid": SYS_GETTID,
"readahead": SYS_READAHEAD,
"setxattr": SYS_SETXATTR,
"lsetxattr": SYS_LSETXATTR,
"fsetxattr": SYS_FSETXATTR,
"getxattr": SYS_GETXATTR,
"lgetxattr": SYS_LGETXATTR,
"fgetxattr": SYS_FGETXATTR,
"listxattr": SYS_LISTXATTR,
"llistxattr": SYS_LLISTXATTR,
"flistxattr": SYS_FLISTXATTR,
"removexattr": SYS_REMOVEXATTR,
"lremovexattr": SYS_LREMOVEXATTR,
"fremovexattr": SYS_FREMOVEXATTR,
"tkill": SYS_TKILL,
"sendfile64": SYS_SENDFILE64,
"futex": SYS_FUTEX,
"sched_setaffinity": SYS_SCHED_SETAFFINITY,
"sched_getaffinity": SYS_SCHED_GETAFFINITY,
"set_thread_area": SYS_SET_THREAD_AREA,
"get_thread_area": SYS_GET_THREAD_AREA,
"io_setup": SYS_IO_SETUP,
"io_destroy": SYS_IO_DESTROY,
"io_getevents": SYS_IO_GETEVENTS,
"io_submit": SYS_IO_SUBMIT,
"io_cancel": SYS_IO_CANCEL,
"fadvise64": SYS_FADVISE64,
"exit_group": SYS_EXIT_GROUP,
"lookup_dcookie": SYS_LOOKUP_DCOOKIE,
"epoll_create": SYS_EPOLL_CREATE,
"epoll_ctl": SYS_EPOLL_CTL,
"epoll_wait": SYS_EPOLL_WAIT,
"remap_file_pages": SYS_REMAP_FILE_PAGES,
"set_tid_address": SYS_SET_TID_ADDRESS,
"timer_create": SYS_TIMER_CREATE,
"timer_settime": SYS_TIMER_SETTIME,
"timer_gettime": SYS_TIMER_GETTIME,
"timer_getoverrun": SYS_TIMER_GETOVERRUN,
"timer_delete": SYS_TIMER_DELETE,
"clock_settime": SYS_CLOCK_SETTIME,
"clock_gettime": SYS_CLOCK_GETTIME,
"clock_getres": SYS_CLOCK_GETRES,
"clock_nanosleep": SYS_CLOCK_NANOSLEEP,
"statfs64": SYS_STATFS64,
"fstatfs64": SYS_FSTATFS64,
"tgkill": SYS_TGKILL,
"utimes": SYS_UTIMES,
"fadvise64_64": SYS_FADVISE64_64,
"vserver": SYS_VSERVER,
"mbind": SYS_MBIND,
"get_mempolicy": SYS_GET_MEMPOLICY,
"set_mempolicy": SYS_SET_MEMPOLICY,
"mq_open": SYS_MQ_OPEN,
"mq_unlink": SYS_MQ_UNLINK,
"mq_timedsend": SYS_MQ_TIMEDSEND,
"mq_timedreceive": SYS_MQ_TIMEDRECEIVE,
"mq_notify": SYS_MQ_NOTIFY,
"mq_getsetattr": SYS_MQ_GETSETATTR,
"kexec_load": SYS_KEXEC_LOAD,
"waitid": SYS_WAITID,
"add_key": SYS_ADD_KEY,
"request_key": SYS_REQUEST_KEY,
"keyctl": SYS_KEYCTL,
"ioprio_set": SYS_IOPRIO_SET,
"ioprio_get": SYS_IOPRIO_GET,
"inotify_init": SYS_INOTIFY_INIT,
"inotify_add_watch": SYS_INOTIFY_ADD_WATCH,
"inotify_rm_watch": SYS_INOTIFY_RM_WATCH,
"migrate_pages": SYS_MIGRATE_PAGES,
"openat": SYS_OPENAT,
"mkdirat": SYS_MKDIRAT,
"mknodat": SYS_MKNODAT,
"fchownat": SYS_FCHOWNAT,
"futimesat": SYS_FUTIMESAT,
"fstatat64": SYS_FSTATAT64,
"unlinkat": SYS_UNLINKAT,
"renameat": SYS_RENAMEAT,
"linkat": SYS_LINKAT,
"symlinkat": SYS_SYMLINKAT,
"readlinkat": SYS_READLINKAT,
"fchmodat": SYS_FCHMODAT,
"faccessat": SYS_FACCESSAT,
"pselect6": SYS_PSELECT6,
"ppoll": SYS_PPOLL,
"unshare": SYS_UNSHARE,
"set_robust_list": SYS_SET_ROBUST_LIST,
"get_robust_list": SYS_GET_ROBUST_LIST,
"splice": SYS_SPLICE,
"sync_file_range": SYS_SYNC_FILE_RANGE,
"tee": SYS_TEE,
"vmsplice": SYS_VMSPLICE,
"move_pages": SYS_MOVE_PAGES,
"getcpu": SYS_GETCPU,
"epoll_pwait": SYS_EPOLL_PWAIT,
"utimensat": SYS_UTIMENSAT,
"signalfd": SYS_SIGNALFD,
"timerfd_create": SYS_TIMERFD_CREATE,
"eventfd": SYS_EVENTFD,
"fallocate": SYS_FALLOCATE,
"timerfd_settime": SYS_TIMERFD_SETTIME,
"timerfd_gettime": SYS_TIMERFD_GETTIME,
"signalfd4": SYS_SIGNALFD4,
"eventfd2": SYS_EVENTFD2,
"epoll_create1": SYS_EPOLL_CREATE1,
"dup3": SYS_DUP3,
"pipe2": SYS_PIPE2,
"inotify_init1": SYS_INOTIFY_INIT1,
"preadv": SYS_PREADV,
"pwritev": SYS_PWRITEV,
"rt_tgsigqueueinfo": SYS_RT_TGSIGQUEUEINFO,
"perf_event_open": SYS_PERF_EVENT_OPEN,
"recvmmsg": __PNR_recvmmsg,
"fanotify_init": SYS_FANOTIFY_INIT,
"fanotify_mark": SYS_FANOTIFY_MARK,
"prlimit64": SYS_PRLIMIT64,
"name_to_handle_at": SYS_NAME_TO_HANDLE_AT,
"open_by_handle_at": SYS_OPEN_BY_HANDLE_AT,
"clock_adjtime": SYS_CLOCK_ADJTIME,
"syncfs": SYS_SYNCFS,
"sendmmsg": __PNR_sendmmsg,
"setns": SYS_SETNS,
"process_vm_readv": SYS_PROCESS_VM_READV,
"process_vm_writev": SYS_PROCESS_VM_WRITEV,
"kcmp": SYS_KCMP,
"finit_module": SYS_FINIT_MODULE,
"sched_setattr": SYS_SCHED_SETATTR,
"sched_getattr": SYS_SCHED_GETATTR,
"renameat2": SYS_RENAMEAT2,
"seccomp": SYS_SECCOMP,
"getrandom": SYS_GETRANDOM,
"memfd_create": SYS_MEMFD_CREATE,
"bpf": SYS_BPF,
"execveat": SYS_EXECVEAT,
"socket": __PNR_socket,
"socketpair": __PNR_socketpair,
"bind": __PNR_bind,
"connect": __PNR_connect,
"listen": __PNR_listen,
"accept4": __PNR_accept4,
"getsockopt": __PNR_getsockopt,
"setsockopt": __PNR_setsockopt,
"getsockname": __PNR_getsockname,
"getpeername": __PNR_getpeername,
"sendto": __PNR_sendto,
"sendmsg": __PNR_sendmsg,
"recvfrom": __PNR_recvfrom,
"recvmsg": __PNR_recvmsg,
"shutdown": __PNR_shutdown,
"userfaultfd": SYS_USERFAULTFD,
"membarrier": SYS_MEMBARRIER,
"mlock2": SYS_MLOCK2,
"copy_file_range": SYS_COPY_FILE_RANGE,
"preadv2": SYS_PREADV2,
"pwritev2": SYS_PWRITEV2,
"pkey_mprotect": SYS_PKEY_MPROTECT,
"pkey_alloc": SYS_PKEY_ALLOC,
"pkey_free": SYS_PKEY_FREE,
"statx": SYS_STATX,
"arch_prctl": SYS_ARCH_PRCTL,
"io_pgetevents": SYS_IO_PGETEVENTS,
"rseq": SYS_RSEQ,
"semget": __PNR_semget,
"semctl": __PNR_semctl,
"shmget": __PNR_shmget,
"shmctl": __PNR_shmctl,
"shmat": __PNR_shmat,
"shmdt": __PNR_shmdt,
"msgget": __PNR_msgget,
"msgsnd": __PNR_msgsnd,
"msgrcv": __PNR_msgrcv,
"msgctl": __PNR_msgctl,
"clock_gettime64": SYS_CLOCK_GETTIME64,
"clock_settime64": SYS_CLOCK_SETTIME64,
"clock_adjtime64": SYS_CLOCK_ADJTIME64,
"clock_getres_time64": SYS_CLOCK_GETRES_TIME64,
"clock_nanosleep_time64": SYS_CLOCK_NANOSLEEP_TIME64,
"timer_gettime64": SYS_TIMER_GETTIME64,
"timer_settime64": SYS_TIMER_SETTIME64,
"timerfd_gettime64": SYS_TIMERFD_GETTIME64,
"timerfd_settime64": SYS_TIMERFD_SETTIME64,
"utimensat_time64": SYS_UTIMENSAT_TIME64,
"pselect6_time64": SYS_PSELECT6_TIME64,
"ppoll_time64": SYS_PPOLL_TIME64,
"io_pgetevents_time64": SYS_IO_PGETEVENTS_TIME64,
"recvmmsg_time64": SYS_RECVMMSG_TIME64,
"mq_timedsend_time64": SYS_MQ_TIMEDSEND_TIME64,
"mq_timedreceive_time64": SYS_MQ_TIMEDRECEIVE_TIME64,
"semtimedop_time64": SYS_SEMTIMEDOP_TIME64,
"rt_sigtimedwait_time64": SYS_RT_SIGTIMEDWAIT_TIME64,
"futex_time64": SYS_FUTEX_TIME64,
"sched_rr_get_interval_time64": SYS_SCHED_RR_GET_INTERVAL_TIME64,
"pidfd_send_signal": SYS_PIDFD_SEND_SIGNAL,
"io_uring_setup": SYS_IO_URING_SETUP,
"io_uring_enter": SYS_IO_URING_ENTER,
"io_uring_register": SYS_IO_URING_REGISTER,
"open_tree": SYS_OPEN_TREE,
"move_mount": SYS_MOVE_MOUNT,
"fsopen": SYS_FSOPEN,
"fsconfig": SYS_FSCONFIG,
"fsmount": SYS_FSMOUNT,
"fspick": SYS_FSPICK,
"pidfd_open": SYS_PIDFD_OPEN,
"clone3": SYS_CLONE3,
"close_range": SYS_CLOSE_RANGE,
"openat2": SYS_OPENAT2,
"pidfd_getfd": SYS_PIDFD_GETFD,
"faccessat2": SYS_FACCESSAT2,
"process_madvise": SYS_PROCESS_MADVISE,
"epoll_pwait2": SYS_EPOLL_PWAIT2,
"mount_setattr": SYS_MOUNT_SETATTR,
"quotactl_fd": SYS_QUOTACTL_FD,
"landlock_create_ruleset": SYS_LANDLOCK_CREATE_RULESET,
"landlock_add_rule": SYS_LANDLOCK_ADD_RULE,
"landlock_restrict_self": SYS_LANDLOCK_RESTRICT_SELF,
"memfd_secret": SYS_MEMFD_SECRET,
"process_mrelease": SYS_PROCESS_MRELEASE,
"futex_waitv": SYS_FUTEX_WAITV,
"set_mempolicy_home_node": SYS_SET_MEMPOLICY_HOME_NODE,
"cachestat": SYS_CACHESTAT,
"fchmodat2": SYS_FCHMODAT2,
"map_shadow_stack": SYS_MAP_SHADOW_STACK,
"futex_wake": SYS_FUTEX_WAKE,
"futex_wait": SYS_FUTEX_WAIT,
"futex_requeue": SYS_FUTEX_REQUEUE,
"statmount": SYS_STATMOUNT,
"listmount": SYS_LISTMOUNT,
"lsm_get_self_attr": SYS_LSM_GET_SELF_ATTR,
"lsm_set_self_attr": SYS_LSM_SET_SELF_ATTR,
"lsm_list_modules": SYS_LSM_LIST_MODULES,
"mseal": SYS_MSEAL,
}
const (
SYS_NAME_TO_HANDLE_AT = 341
SYS_OPEN_BY_HANDLE_AT = 342
SYS_CLOCK_ADJTIME = 343
SYS_SYNCFS = 344
SYS_SENDMMSG = 345
SYS_SETNS = 346
SYS_PROCESS_VM_READV = 347
SYS_PROCESS_VM_WRITEV = 348
SYS_KCMP = 349
SYS_FINIT_MODULE = 350
SYS_SCHED_SETATTR = 351
SYS_SCHED_GETATTR = 352
SYS_RENAMEAT2 = 353
SYS_SECCOMP = 354
SYS_GETRANDOM = 355
SYS_MEMFD_CREATE = 356
SYS_BPF = 357
SYS_EXECVEAT = 358
SYS_SOCKET = 359
SYS_SOCKETPAIR = 360
SYS_BIND = 361
SYS_CONNECT = 362
SYS_LISTEN = 363
SYS_ACCEPT4 = 364
SYS_GETSOCKOPT = 365
SYS_SETSOCKOPT = 366
SYS_GETSOCKNAME = 367
SYS_GETPEERNAME = 368
SYS_SENDTO = 369
SYS_SENDMSG = 370
SYS_RECVFROM = 371
SYS_RECVMSG = 372
SYS_SHUTDOWN = 373
SYS_USERFAULTFD = 374
SYS_MEMBARRIER = 375
SYS_MLOCK2 = 376
SYS_COPY_FILE_RANGE = 377
SYS_PREADV2 = 378
SYS_PWRITEV2 = 379
SYS_PKEY_MPROTECT = 380
SYS_PKEY_ALLOC = 381
SYS_PKEY_FREE = 382
SYS_STATX = 383
SYS_ARCH_PRCTL = 384
SYS_IO_PGETEVENTS = 385
SYS_RSEQ = 386
SYS_SEMGET = 393
SYS_SEMCTL = 394
SYS_SHMGET = 395
SYS_SHMCTL = 396
SYS_SHMAT = 397
SYS_SHMDT = 398
SYS_MSGGET = 399
SYS_MSGSND = 400
SYS_MSGRCV = 401
SYS_MSGCTL = 402
SYS_CLOCK_GETTIME64 = 403
SYS_CLOCK_SETTIME64 = 404
SYS_CLOCK_ADJTIME64 = 405
SYS_CLOCK_GETRES_TIME64 = 406
SYS_CLOCK_NANOSLEEP_TIME64 = 407
SYS_TIMER_GETTIME64 = 408
SYS_TIMER_SETTIME64 = 409
SYS_TIMERFD_GETTIME64 = 410
SYS_TIMERFD_SETTIME64 = 411
SYS_UTIMENSAT_TIME64 = 412
SYS_PSELECT6_TIME64 = 413
SYS_PPOLL_TIME64 = 414
SYS_IO_PGETEVENTS_TIME64 = 416
SYS_RECVMMSG_TIME64 = 417
SYS_MQ_TIMEDSEND_TIME64 = 418
SYS_MQ_TIMEDRECEIVE_TIME64 = 419
SYS_SEMTIMEDOP_TIME64 = 420
SYS_RT_SIGTIMEDWAIT_TIME64 = 421
SYS_FUTEX_TIME64 = 422
SYS_SCHED_RR_GET_INTERVAL_TIME64 = 423
SYS_PIDFD_SEND_SIGNAL = 424
SYS_IO_URING_SETUP = 425
SYS_IO_URING_ENTER = 426
SYS_IO_URING_REGISTER = 427
SYS_OPEN_TREE = 428
SYS_MOVE_MOUNT = 429
SYS_FSOPEN = 430
SYS_FSCONFIG = 431
SYS_FSMOUNT = 432
SYS_FSPICK = 433
SYS_PIDFD_OPEN = 434
SYS_CLONE3 = 435
SYS_CLOSE_RANGE = 436
SYS_OPENAT2 = 437
SYS_PIDFD_GETFD = 438
SYS_FACCESSAT2 = 439
SYS_PROCESS_MADVISE = 440
SYS_EPOLL_PWAIT2 = 441
SYS_MOUNT_SETATTR = 442
SYS_QUOTACTL_FD = 443
SYS_LANDLOCK_CREATE_RULESET = 444
SYS_LANDLOCK_ADD_RULE = 445
SYS_LANDLOCK_RESTRICT_SELF = 446
SYS_MEMFD_SECRET = 447
SYS_PROCESS_MRELEASE = 448
SYS_FUTEX_WAITV = 449
SYS_SET_MEMPOLICY_HOME_NODE = 450
SYS_CACHESTAT = 451
SYS_FCHMODAT2 = 452
SYS_MAP_SHADOW_STACK = 453
SYS_FUTEX_WAKE = 454
SYS_FUTEX_WAIT = 455
SYS_FUTEX_REQUEUE = 456
SYS_STATMOUNT = 457
SYS_LISTMOUNT = 458
SYS_LSM_GET_SELF_ATTR = 459
SYS_LSM_SET_SELF_ATTR = 460
SYS_LSM_LIST_MODULES = 461
SYS_MSEAL = 462
)

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