Compare commits

...

249 Commits

Author SHA1 Message Date
0e957cc9c1
release: 0.0.2
All checks were successful
Release / Create release (push) Successful in 43s
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 40s
Test / Hakurei (push) Successful in 45s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Planterette (push) Successful in 1m41s
Test / Hakurei (race detector) (push) Successful in 1m44s
Test / Flake checks (push) Successful in 1m14s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-06-25 21:11:11 +09:00
aa454b158f
cmd/planterette: remove hsu special case
All checks were successful
Test / Hakurei (push) Successful in 42s
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 40s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Sandbox (race detector) (push) Successful in 38s
Test / Planterette (push) Successful in 40s
Test / Flake checks (push) Successful in 1m15s
Remove special case and invoke hakurei out of process.

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

Still better than nothing though.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-21 17:05:17 +09:00
228 changed files with 13826 additions and 8359 deletions

View File

@ -20,5 +20,5 @@ jobs:
uses: https://gitea.com/actions/release-action@main
with:
files: |-
result/fortify-**
result/hakurei-**
api_key: '${{secrets.RELEASE_TOKEN}}'

View File

@ -5,25 +5,107 @@ on:
- pull_request
jobs:
test:
name: Run NixOS test
hakurei:
name: Hakurei
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run tests
run: |
nix --print-build-logs --experimental-features 'nix-command flakes' flake check
nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.nixos-tests
- name: Run NixOS test
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.hakurei
- name: Upload test output
uses: actions/upload-artifact@v3
with:
name: "nixos-vm-output"
name: "hakurei-vm-output"
path: result/*
retention-days: 1
race:
name: Hakurei (race detector)
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run NixOS test
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.race
- name: Upload test output
uses: actions/upload-artifact@v3
with:
name: "hakurei-race-vm-output"
path: result/*
retention-days: 1
sandbox:
name: Sandbox
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run NixOS test
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.sandbox
- name: Upload test output
uses: actions/upload-artifact@v3
with:
name: "sandbox-vm-output"
path: result/*
retention-days: 1
sandbox-race:
name: Sandbox (race detector)
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run NixOS test
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.sandbox-race
- name: Upload test output
uses: actions/upload-artifact@v3
with:
name: "sandbox-race-vm-output"
path: result/*
retention-days: 1
planterette:
name: Planterette
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run NixOS test
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.planterette
- name: Upload test output
uses: actions/upload-artifact@v3
with:
name: "planterette-vm-output"
path: result/*
retention-days: 1
check:
name: Flake checks
needs:
- hakurei
- race
- sandbox
- sandbox-race
- planterette
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run checks
run: nix --print-build-logs --experimental-features 'nix-command flakes' flake check
dist:
name: Create distribution
runs-on: nix
@ -34,15 +116,15 @@ jobs:
- name: Build for test
id: build-test
run: >-
export FORTIFY_REV="$(git rev-parse --short HEAD)" &&
sed -i.old 's/version = /version = "0.0.0-'$FORTIFY_REV'"; # version = /' package.nix &&
export HAKUREI_REV="$(git rev-parse --short HEAD)" &&
sed -i.old 's/version = /version = "0.0.0-'$HAKUREI_REV'"; # version = /' package.nix &&
nix build --print-out-paths --print-build-logs .#dist &&
mv package.nix.old package.nix &&
echo "rev=$FORTIFY_REV" >> $GITHUB_OUTPUT
echo "rev=$HAKUREI_REV" >> $GITHUB_OUTPUT
- name: Upload test build
uses: actions/upload-artifact@v3
with:
name: "fortify-${{ steps.build-test.outputs.rev }}"
name: "hakurei-${{ steps.build-test.outputs.rev }}"
path: result/*
retention-days: 1

1
.github/workflows/README vendored Normal file
View File

@ -0,0 +1 @@
This port is solely for releasing to the github mirror and serves no purpose during development.

46
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,46 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
name: Create release
runs-on: ubuntu-latest
permissions:
packages: write
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Nix
uses: nixbuild/nix-quick-install-action@v32
with:
nix_conf: |
keep-env-derivations = true
keep-outputs = true
- name: Restore and cache Nix store
uses: nix-community/cache-nix-action@v6
with:
primary-key: build-${{ runner.os }}-${{ hashFiles('**/*.nix') }}
restore-prefixes-first-match: build-${{ runner.os }}-
gc-max-store-size-linux: 1G
purge: true
purge-prefixes: build-${{ runner.os }}-
purge-created: 60
purge-primary-key: never
- name: Build for release
run: nix build --print-out-paths --print-build-logs .#dist
- name: Release
uses: softprops/action-gh-release@v2
with:
files: |-
result/hakurei-**

48
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,48 @@
name: Test
on:
- push
jobs:
dist:
name: Create distribution
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Nix
uses: nixbuild/nix-quick-install-action@v32
with:
nix_conf: |
keep-env-derivations = true
keep-outputs = true
- name: Restore and cache Nix store
uses: nix-community/cache-nix-action@v6
with:
primary-key: build-${{ runner.os }}-${{ hashFiles('**/*.nix') }}
restore-prefixes-first-match: build-${{ runner.os }}-
gc-max-store-size-linux: 1G
purge: true
purge-prefixes: build-${{ runner.os }}-
purge-created: 60
purge-primary-key: never
- name: Build for test
id: build-test
run: >-
export HAKUREI_REV="$(git rev-parse --short HEAD)" &&
sed -i.old 's/version = /version = "0.0.0-'$HAKUREI_REV'"; # version = /' package.nix &&
nix build --print-out-paths --print-build-logs .#dist &&
mv package.nix.old package.nix &&
echo "rev=$HAKUREI_REV" >> $GITHUB_OUTPUT
- name: Upload test build
uses: actions/upload-artifact@v4
with:
name: "hakurei-${{ steps.build-test.outputs.rev }}"
path: result/*
retention-days: 1

4
.gitignore vendored
View File

@ -5,7 +5,7 @@
*.so
*.dylib
*.pkg
/fortify
/hakurei
# Test binary, built with `go test -c`
*.test
@ -29,4 +29,4 @@ go.work.sum
security-context-v1-protocol.*
# release
/dist/fortify-*
/dist/hakurei-*

105
README.md
View File

@ -1,77 +1,79 @@
Fortify
=======
<p align="center">
<a href="https://git.gensokyo.uk/security/hakurei">
<picture>
<img src="https://basement.gensokyo.uk/images/yukari1.png" width="200px" alt="Yukari">
</picture>
</a>
</p>
[![Go Reference](https://pkg.go.dev/badge/git.gensokyo.uk/security/fortify.svg)](https://pkg.go.dev/git.gensokyo.uk/security/fortify)
[![Go Report Card](https://goreportcard.com/badge/git.gensokyo.uk/security/fortify)](https://goreportcard.com/report/git.gensokyo.uk/security/fortify)
<p align="center">
<a href="https://pkg.go.dev/git.gensokyo.uk/security/hakurei"><img src="https://pkg.go.dev/badge/git.gensokyo.uk/security/hakurei.svg" alt="Go Reference" /></a>
<a href="https://goreportcard.com/report/git.gensokyo.uk/security/hakurei"><img src="https://goreportcard.com/badge/git.gensokyo.uk/security/hakurei" alt="Go Report Card" /></a>
</p>
Lets you run graphical applications as another user in a confined environment with a nice NixOS
module to configure target users and provide launchers and desktop files for your privileged user.
Hakurei is a tool for running sandboxed graphical applications as dedicated subordinate users on the Linux kernel.
It also implements [planterette (WIP)](cmd/planterette), a self-contained Android-like package manager with modern security features.
Why would you want this?
## NixOS Module usage
- It protects the desktop environment from applications.
- It protects applications from each other.
- It provides UID isolation on top of the standard application sandbox.
If you have a flakes-enabled nix environment, you can try out the tool by running:
```shell
nix run git+https://git.gensokyo.uk/security/fortify -- help
```
## Module usage
The NixOS module currently requires home-manager to function correctly.
Full module documentation can be found [here](options.md).
The NixOS module currently requires home-manager to configure subordinate users. Full module documentation can be found [here](options.md).
To use the module, import it into your configuration with
```nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
fortify = {
url = "git+https://git.gensokyo.uk/security/fortify";
hakurei = {
url = "git+https://git.gensokyo.uk/security/hakurei";
# Optional but recommended to limit the size of your system closure.
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, fortify, ... }:
outputs = { self, nixpkgs, hakurei, ... }:
{
nixosConfigurations.fortify = nixpkgs.lib.nixosSystem {
nixosConfigurations.hakurei = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
fortify.nixosModules.fortify
hakurei.nixosModules.hakurei
];
};
};
}
```
This adds the `environment.fortify` option:
This adds the `environment.hakurei` option:
```nix
{ pkgs, ... }:
{
environment.fortify = {
environment.hakurei = {
enable = true;
stateDir = "/var/lib/persist/module/fortify";
stateDir = "/var/lib/hakurei";
users = {
alice = 0;
nixos = 10;
};
apps = [
commonPaths = [
{
src = "/sdcard";
write = true;
}
];
extraHomeConfig = {
home.stateVersion = "23.05";
};
apps = {
"org.chromium.Chromium" = {
name = "chromium";
id = "org.chromium.Chromium";
identity = 1;
packages = [ pkgs.chromium ];
userns = true;
mapRealUid = true;
@ -104,16 +106,20 @@ This adds the `environment.fortify` option:
broadcast = { };
};
};
}
{
};
"org.claws_mail.Claws-Mail" = {
name = "claws-mail";
id = "org.claws_mail.Claws-Mail";
identity = 2;
packages = [ pkgs.claws-mail ];
gpu = false;
capability.pulse = false;
}
{
};
"org.weechat" = {
name = "weechat";
identity = 3;
shareUid = true;
packages = [ pkgs.weechat ];
capability = {
wayland = false;
@ -121,10 +127,12 @@ This adds the `environment.fortify` option:
dbus = true;
pulse = false;
};
}
{
};
"dev.vencord.Vesktop" = {
name = "discord";
id = "dev.vencord.Vesktop";
identity = 3;
shareUid = true;
packages = [ pkgs.vesktop ];
share = pkgs.vesktop;
command = "vesktop --ozone-platform-hint=wayland";
@ -142,9 +150,12 @@ This adds the `environment.fortify` option:
};
system.filter = true;
};
}
{
};
"io.looking-glass" = {
name = "looking-glass-client";
identity = 4;
useCommonPaths = false;
groups = [ "plugdev" ];
extraPaths = [
{
@ -155,8 +166,8 @@ This adds the `environment.fortify` option:
extraConfig = {
programs.looking-glass-client.enable = true;
};
}
];
};
};
};
}
```

View File

@ -1,10 +1,11 @@
#include "acl-update.h"
#include <stdlib.h>
#include <stdbool.h>
#include <sys/acl.h>
#include <acl/libacl.h>
#include <stdbool.h>
#include <stdlib.h>
#include <sys/acl.h>
int f_acl_update_file_by_uid(const char *path_p, uid_t uid, acl_perm_t *perms, size_t plen) {
int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid, acl_perm_t *perms,
size_t plen) {
int ret = -1;
bool v;
int i;
@ -19,7 +20,8 @@ int f_acl_update_file_by_uid(const char *path_p, uid_t uid, acl_perm_t *perms, s
goto out;
// prune entries by uid
for (i = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); i == 1; i = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) {
for (i = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); i == 1;
i = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) {
if (acl_get_tag_type(entry, &tag_type) != 0)
return -1;
if (tag_type != ACL_USER)

View File

@ -1,3 +1,4 @@
#include <sys/acl.h>
int f_acl_update_file_by_uid(const char *path_p, uid_t uid, acl_perm_t *perms, size_t plen);
int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid, acl_perm_t *perms,
size_t plen);

View File

@ -23,7 +23,7 @@ func Update(name string, uid int, perms ...Perm) error {
p = &perms[0]
}
r, err := C.f_acl_update_file_by_uid(
r, err := C.hakurei_acl_update_file_by_uid(
C.CString(name),
C.uid_t(uid),
(*C.acl_perm_t)(p),

View File

@ -7,7 +7,7 @@ import (
"reflect"
"testing"
"git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/hakurei/acl"
)
const testFileName = "acl.test"

View File

@ -1,90 +0,0 @@
package main
import (
"encoding/json"
"log"
"os"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/system"
)
type bundleInfo struct {
Name string `json:"name"`
Version string `json:"version"`
// passed through to [fst.Config]
ID string `json:"id"`
// passed through to [fst.Config]
AppID int `json:"app_id"`
// passed through to [fst.Config]
Groups []string `json:"groups,omitempty"`
// passed through to [fst.Config]
UserNS bool `json:"userns,omitempty"`
// passed through to [fst.Config]
Net bool `json:"net,omitempty"`
// passed through to [fst.Config]
Dev bool `json:"dev,omitempty"`
// passed through to [fst.Config]
NoNewSession bool `json:"no_new_session,omitempty"`
// passed through to [fst.Config]
MapRealUID bool `json:"map_real_uid,omitempty"`
// passed through to [fst.Config]
DirectWayland bool `json:"direct_wayland,omitempty"`
// passed through to [fst.Config]
SystemBus *dbus.Config `json:"system_bus,omitempty"`
// passed through to [fst.Config]
SessionBus *dbus.Config `json:"session_bus,omitempty"`
// passed through to [fst.Config]
Enablements system.Enablements `json:"enablements"`
// passed through inverted to [bwrap.SyscallPolicy]
Devel bool `json:"devel,omitempty"`
// passed through to [bwrap.SyscallPolicy]
Multiarch bool `json:"multiarch,omitempty"`
// passed through to [bwrap.SyscallPolicy]
Bluetooth bool `json:"bluetooth,omitempty"`
// allow gpu access within sandbox
GPU bool `json:"gpu"`
// store path to nixGL mesa wrappers
Mesa string `json:"mesa,omitempty"`
// store path to nixGL source
NixGL string `json:"nix_gl,omitempty"`
// store path to activate-and-exec script
Launcher string `json:"launcher"`
// store path to /run/current-system
CurrentSystem string `json:"current_system"`
// store path to home-manager activation package
ActivationPackage string `json:"activation_package"`
}
func loadBundleInfo(name string, beforeFail func()) *bundleInfo {
bundle := new(bundleInfo)
if f, err := os.Open(name); err != nil {
beforeFail()
log.Fatalf("cannot open bundle: %v", err)
} else if err = json.NewDecoder(f).Decode(&bundle); err != nil {
beforeFail()
log.Fatalf("cannot parse bundle metadata: %v", err)
} else if err = f.Close(); err != nil {
log.Printf("cannot close bundle metadata: %v", err)
// not fatal
}
if bundle.ID == "" {
beforeFail()
log.Fatal("application identifier must not be empty")
}
return bundle
}
func formatHostname(name string) string {
if h, err := os.Hostname(); err != nil {
log.Printf("cannot get hostname: %v", err)
return "fortify-" + name
} else {
return h + "-" + name
}
}

View File

@ -1,191 +0,0 @@
package main
import (
"encoding/json"
"flag"
"log"
"os"
"path"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
func actionInstall(args []string) {
set := flag.NewFlagSet("install", flag.ExitOnError)
var (
dropShellInstall bool
dropShellActivate bool
)
set.BoolVar(&dropShellInstall, "si", false, "Drop to a shell on installation")
set.BoolVar(&dropShellActivate, "sa", false, "Drop to a shell on activation")
// Ignore errors; set is set for ExitOnError.
_ = set.Parse(args)
args = set.Args()
if len(args) != 1 {
log.Fatal("invalid argument")
}
pkgPath := args[0]
if !path.IsAbs(pkgPath) {
if dir, err := os.Getwd(); err != nil {
log.Fatalf("cannot get current directory: %v", err)
} else {
pkgPath = path.Join(dir, pkgPath)
}
}
/*
Look up paths to programs started by fpkg.
This is done here to ease error handling as cleanup is not yet required.
*/
var (
_ = lookPath("zstd")
tar = lookPath("tar")
chmod = lookPath("chmod")
rm = lookPath("rm")
)
/*
Extract package and set up for cleanup.
*/
var workDir string
if p, err := os.MkdirTemp("", "fpkg.*"); err != nil {
log.Fatalf("cannot create temporary directory: %v", err)
} else {
workDir = p
}
cleanup := func() {
// should be faster than a native implementation
mustRun(chmod, "-R", "+w", workDir)
mustRun(rm, "-rf", workDir)
}
beforeRunFail.Store(&cleanup)
mustRun(tar, "-C", workDir, "-xf", pkgPath)
/*
Parse bundle and app metadata, do pre-install checks.
*/
bundle := loadBundleInfo(path.Join(workDir, "bundle.json"), cleanup)
pathSet := pathSetByApp(bundle.ID)
app := bundle
if s, err := os.Stat(pathSet.metaPath); err != nil {
if !os.IsNotExist(err) {
cleanup()
log.Fatalf("cannot access %q: %v", pathSet.metaPath, err)
}
// did not modify app, clean installation condition met later
} else if s.IsDir() {
cleanup()
log.Fatalf("metadata path %q is not a file", pathSet.metaPath)
} else {
app = loadBundleInfo(pathSet.metaPath, cleanup)
if app.ID != bundle.ID {
cleanup()
log.Fatalf("app %q claims to have identifier %q", bundle.ID, app.ID)
}
// sec: should verify credentials
}
if app != bundle {
// do not try to re-install
if app.NixGL == bundle.NixGL &&
app.CurrentSystem == bundle.CurrentSystem &&
app.Launcher == bundle.Launcher &&
app.ActivationPackage == bundle.ActivationPackage {
cleanup()
log.Printf("package %q is identical to local application %q", pkgPath, app.ID)
internal.Exit(0)
}
// AppID determines uid
if app.AppID != bundle.AppID {
cleanup()
log.Fatalf("package %q app id %d differs from installed %d", pkgPath, bundle.AppID, app.AppID)
}
// sec: should compare version string
fmsg.Verbosef("installing application %q version %q over local %q", bundle.ID, bundle.Version, app.Version)
} else {
fmsg.Verbosef("application %q clean installation", bundle.ID)
// sec: should install credentials
}
/*
Setup steps for files owned by the target user.
*/
withCacheDir("install", []string{
// export inner bundle path in the environment
"export BUNDLE=" + fst.Tmp + "/bundle",
// replace inner /etc
"mkdir -p etc",
"chmod -R +w etc",
"rm -rf etc",
"cp -dRf $BUNDLE/etc etc",
// replace inner /nix
"mkdir -p nix",
"chmod -R +w nix",
"rm -rf nix",
"cp -dRf /nix nix",
// copy from binary cache
"nix copy --offline --no-check-sigs --all --from file://$BUNDLE/res --to $PWD",
// deduplicate nix store
"nix store --offline --store $PWD optimise",
// make cache directory world-readable for autoetc
"chmod 0755 .",
}, workDir, bundle, pathSet, dropShellInstall, cleanup)
if bundle.GPU {
withCacheDir("mesa-wrappers", []string{
// link nixGL mesa wrappers
"mkdir -p nix/.nixGL",
"ln -s " + bundle.Mesa + "/bin/nixGLIntel nix/.nixGL/nixGL",
"ln -s " + bundle.Mesa + "/bin/nixVulkanIntel nix/.nixGL/nixVulkan",
}, workDir, bundle, pathSet, false, cleanup)
}
/*
Activate home-manager generation.
*/
withNixDaemon("activate", []string{
// clean up broken links
"mkdir -p .local/state/{nix,home-manager}",
"chmod -R +w .local/state/{nix,home-manager}",
"rm -rf .local/state/{nix,home-manager}",
// run activation script
bundle.ActivationPackage + "/activate",
}, false, func(config *fst.Config) *fst.Config { return config }, bundle, pathSet, dropShellActivate, cleanup)
/*
Installation complete. Write metadata to block re-installs or downgrades.
*/
// serialise metadata to ensure consistency
if f, err := os.OpenFile(pathSet.metaPath+"~", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil {
cleanup()
log.Fatalf("cannot create metadata file: %v", err)
} else if err = json.NewEncoder(f).Encode(bundle); err != nil {
cleanup()
log.Fatalf("cannot write metadata: %v", err)
} else if err = f.Close(); err != nil {
log.Printf("cannot close metadata file: %v", err)
// not fatal
}
if err := os.Rename(pathSet.metaPath+"~", pathSet.metaPath); err != nil {
cleanup()
log.Fatalf("cannot rename metadata file: %v", err)
}
cleanup()
}

View File

@ -1,50 +0,0 @@
package main
import (
"flag"
"log"
"os"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
const shell = "/run/current-system/sw/bin/bash"
func init() {
if err := os.Setenv("SHELL", shell); err != nil {
log.Fatalf("cannot set $SHELL: %v", err)
}
}
var (
flagVerbose bool
)
func init() {
flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
}
func main() {
fmsg.Prepare("fpkg")
flag.Parse()
fmsg.Store(flagVerbose)
args := flag.Args()
if len(args) < 1 {
log.Fatal("invalid argument")
}
switch args[0] {
case "install":
actionInstall(args[1:])
case "start":
actionStart(args[1:])
default:
log.Fatal("invalid argument")
}
internal.Exit(0)
}

View File

@ -1,71 +0,0 @@
package main
import (
"log"
"os"
"os/exec"
"path"
"strconv"
"sync/atomic"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
var (
dataHome string
)
func init() {
// dataHome
if p, ok := os.LookupEnv("FORTIFY_DATA_HOME"); ok {
dataHome = p
} else {
dataHome = "/var/lib/fortify/" + strconv.Itoa(os.Getuid())
}
}
func lookPath(file string) string {
if p, err := exec.LookPath(file); err != nil {
log.Fatalf("%s: command not found", file)
return ""
} else {
return p
}
}
var beforeRunFail = new(atomic.Pointer[func()])
func mustRun(name string, arg ...string) {
fmsg.Verbosef("spawning process: %q %q", name, arg)
cmd := exec.Command(name, arg...)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
if f := beforeRunFail.Swap(nil); f != nil {
(*f)()
}
log.Fatalf("%s: %v", name, err)
}
}
type appPathSet struct {
// ${dataHome}/${id}
baseDir string
// ${baseDir}/app
metaPath string
// ${baseDir}/files
homeDir string
// ${baseDir}/cache
cacheDir string
// ${baseDir}/cache/nix
nixPath string
}
func pathSetByApp(id string) *appPathSet {
pathSet := new(appPathSet)
pathSet.baseDir = path.Join(dataHome, id)
pathSet.metaPath = path.Join(pathSet.baseDir, "app")
pathSet.homeDir = path.Join(pathSet.baseDir, "files")
pathSet.cacheDir = path.Join(pathSet.baseDir, "cache")
pathSet.nixPath = path.Join(pathSet.cacheDir, "nix")
return pathSet
}

View File

@ -1,178 +0,0 @@
package main
import (
"flag"
"log"
"path"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal"
)
func actionStart(args []string) {
set := flag.NewFlagSet("start", flag.ExitOnError)
var (
dropShell bool
dropShellNixGL bool
autoDrivers bool
)
set.BoolVar(&dropShell, "s", false, "Drop to a shell")
set.BoolVar(&dropShellNixGL, "sg", false, "Drop to a shell on nixGL build")
set.BoolVar(&autoDrivers, "autodrivers", false, "Attempt automatic opengl driver detection")
// Ignore errors; set is set for ExitOnError.
_ = set.Parse(args)
args = set.Args()
if len(args) < 1 {
log.Fatal("invalid argument")
}
/*
Parse app metadata.
*/
id := args[0]
pathSet := pathSetByApp(id)
app := loadBundleInfo(pathSet.metaPath, func() {})
if app.ID != id {
log.Fatalf("app %q claims to have identifier %q", id, app.ID)
}
/*
Prepare nixGL.
*/
if app.GPU && autoDrivers {
withNixDaemon("nix-gl", []string{
"mkdir -p /nix/.nixGL/auto",
"rm -rf /nix/.nixGL/auto",
"export NIXPKGS_ALLOW_UNFREE=1",
"nix build --impure " +
"--out-link /nix/.nixGL/auto/opengl " +
"--override-input nixpkgs path:/etc/nixpkgs " +
"path:" + app.NixGL,
"nix build --impure " +
"--out-link /nix/.nixGL/auto/vulkan " +
"--override-input nixpkgs path:/etc/nixpkgs " +
"path:" + app.NixGL + "#nixVulkanNvidia",
}, true, func(config *fst.Config) *fst.Config {
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, []*fst.FilesystemConfig{
{Src: "/etc/resolv.conf"},
{Src: "/sys/block"},
{Src: "/sys/bus"},
{Src: "/sys/class"},
{Src: "/sys/dev"},
{Src: "/sys/devices"},
}...)
appendGPUFilesystem(config)
return config
}, app, pathSet, dropShellNixGL, func() {})
}
/*
Create app configuration.
*/
command := make([]string, 1, len(args))
if !dropShell {
command[0] = app.Launcher
} else {
command[0] = shell
}
command = append(command, args[1:]...)
config := &fst.Config{
ID: app.ID,
Command: command,
Confinement: fst.ConfinementConfig{
AppID: app.AppID,
Groups: app.Groups,
Username: "fortify",
Inner: path.Join("/data/data", app.ID),
Outer: pathSet.homeDir,
Sandbox: &fst.SandboxConfig{
Hostname: formatHostname(app.Name),
UserNS: app.UserNS,
Net: app.Net,
Dev: app.Dev,
Syscall: &bwrap.SyscallPolicy{DenyDevel: !app.Devel, Multiarch: app.Multiarch, Bluetooth: app.Bluetooth},
NoNewSession: app.NoNewSession || dropShell,
MapRealUID: app.MapRealUID,
DirectWayland: app.DirectWayland,
Filesystem: []*fst.FilesystemConfig{
{Src: path.Join(pathSet.nixPath, "store"), Dst: "/nix/store", Must: true},
{Src: pathSet.metaPath, Dst: path.Join(fst.Tmp, "app"), Must: true},
{Src: "/etc/resolv.conf"},
{Src: "/sys/block"},
{Src: "/sys/bus"},
{Src: "/sys/class"},
{Src: "/sys/dev"},
{Src: "/sys/devices"},
},
Link: [][2]string{
{app.CurrentSystem, "/run/current-system"},
{"/run/current-system/sw/bin", "/bin"},
{"/run/current-system/sw/bin", "/usr/bin"},
},
Etc: path.Join(pathSet.cacheDir, "etc"),
AutoEtc: true,
},
ExtraPerms: []*fst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
},
SystemBus: app.SystemBus,
SessionBus: app.SessionBus,
Enablements: app.Enablements,
},
}
/*
Expose GPU devices.
*/
if app.GPU {
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem,
&fst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(fst.Tmp, "nixGL")})
appendGPUFilesystem(config)
}
/*
Spawn app.
*/
fortifyApp(config, func() {})
internal.Exit(0)
}
func appendGPUFilesystem(config *fst.Config) {
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, []*fst.FilesystemConfig{
// flatpak commit 763a686d874dd668f0236f911de00b80766ffe79
{Src: "/dev/dri", Device: true},
// mali
{Src: "/dev/mali", Device: true},
{Src: "/dev/mali0", Device: true},
{Src: "/dev/umplock", Device: true},
// nvidia
{Src: "/dev/nvidiactl", Device: true},
{Src: "/dev/nvidia-modeset", Device: true},
// nvidia OpenCL/CUDA
{Src: "/dev/nvidia-uvm", Device: true},
{Src: "/dev/nvidia-uvm-tools", Device: true},
// flatpak commit d2dff2875bb3b7e2cd92d8204088d743fd07f3ff
{Src: "/dev/nvidia0", Device: true}, {Src: "/dev/nvidia1", Device: true},
{Src: "/dev/nvidia2", Device: true}, {Src: "/dev/nvidia3", Device: true},
{Src: "/dev/nvidia4", Device: true}, {Src: "/dev/nvidia5", Device: true},
{Src: "/dev/nvidia6", Device: true}, {Src: "/dev/nvidia7", Device: true},
{Src: "/dev/nvidia8", Device: true}, {Src: "/dev/nvidia9", Device: true},
{Src: "/dev/nvidia10", Device: true}, {Src: "/dev/nvidia11", Device: true},
{Src: "/dev/nvidia12", Device: true}, {Src: "/dev/nvidia13", Device: true},
{Src: "/dev/nvidia14", Device: true}, {Src: "/dev/nvidia15", Device: true},
{Src: "/dev/nvidia16", Device: true}, {Src: "/dev/nvidia17", Device: true},
{Src: "/dev/nvidia18", Device: true}, {Src: "/dev/nvidia19", Device: true},
}...)
}

View File

@ -1,101 +0,0 @@
package main
import (
"path"
"strings"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal"
)
func withNixDaemon(
action string, command []string, net bool, updateConfig func(config *fst.Config) *fst.Config,
app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
) {
fortifyAppDropShell(updateConfig(&fst.Config{
ID: app.ID,
Command: []string{shell, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
// start nix-daemon
"nix-daemon --store / & " +
// wait for socket to appear
"(while [ ! -S /nix/var/nix/daemon-socket/socket ]; do sleep 0.01; done) && " +
// create directory so nix stops complaining
"mkdir -p /nix/var/nix/profiles/per-user/root/channels && " +
strings.Join(command, " && ") +
// terminate nix-daemon
" && pkill nix-daemon",
},
Confinement: fst.ConfinementConfig{
AppID: app.AppID,
Username: "fortify",
Inner: path.Join("/data/data", app.ID),
Outer: pathSet.homeDir,
Sandbox: &fst.SandboxConfig{
Hostname: formatHostname(app.Name) + "-" + action,
UserNS: true, // nix sandbox requires userns
Net: net,
Syscall: &bwrap.SyscallPolicy{Multiarch: true},
NoNewSession: dropShell,
Filesystem: []*fst.FilesystemConfig{
{Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true},
},
Link: [][2]string{
{app.CurrentSystem, "/run/current-system"},
{"/run/current-system/sw/bin", "/bin"},
{"/run/current-system/sw/bin", "/usr/bin"},
},
Etc: path.Join(pathSet.cacheDir, "etc"),
AutoEtc: true,
},
ExtraPerms: []*fst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
},
},
}), dropShell, beforeFail)
}
func withCacheDir(action string, command []string, workDir string, app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
fortifyAppDropShell(&fst.Config{
ID: app.ID,
Command: []string{shell, "-lc", strings.Join(command, " && ")},
Confinement: fst.ConfinementConfig{
AppID: app.AppID,
Username: "nixos",
Inner: path.Join("/data/data", app.ID, "cache"),
Outer: pathSet.cacheDir, // this also ensures cacheDir via shim
Sandbox: &fst.SandboxConfig{
Hostname: formatHostname(app.Name) + "-" + action,
Syscall: &bwrap.SyscallPolicy{Multiarch: true},
NoNewSession: dropShell,
Filesystem: []*fst.FilesystemConfig{
{Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true},
{Src: workDir, Dst: path.Join(fst.Tmp, "bundle"), Must: true},
},
Link: [][2]string{
{app.CurrentSystem, "/run/current-system"},
{"/run/current-system/sw/bin", "/bin"},
{"/run/current-system/sw/bin", "/usr/bin"},
},
Etc: path.Join(workDir, "etc"),
AutoEtc: true,
},
ExtraPerms: []*fst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
{Path: workDir, Execute: true},
},
},
}, dropShell, beforeFail)
}
func fortifyAppDropShell(config *fst.Config, dropShell bool, beforeFail func()) {
if dropShell {
config.Command = []string{shell, "-l"}
fortifyApp(config, beforeFail)
beforeFail()
internal.Exit(0)
}
fortifyApp(config, beforeFail)
}

View File

@ -13,22 +13,17 @@ import (
)
const (
compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
fsuConfFile = "/etc/fsurc"
envShim = "FORTIFY_SHIM"
envAID = "FORTIFY_APP_ID"
envGroups = "FORTIFY_GROUPS"
hsuConfFile = "/etc/hsurc"
envShim = "HAKUREI_SHIM"
envAID = "HAKUREI_APP_ID"
envGroups = "HAKUREI_GROUPS"
PR_SET_NO_NEW_PRIVS = 0x26
)
var (
Fmain = compPoison
)
func main() {
log.SetFlags(0)
log.SetPrefix("fsu: ")
log.SetPrefix("hsu: ")
log.SetOutput(os.Stderr)
if os.Geteuid() != 0 {
@ -40,20 +35,16 @@ func main() {
log.Fatal("this program must not be started by root")
}
var fmain string
if p, ok := checkPath(Fmain); !ok {
log.Fatal("invalid fortify path, this copy of fsu is not compiled correctly")
} else {
fmain = p
}
var toolPath string
pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
if p, err := os.Readlink(pexe); err != nil {
log.Fatalf("cannot read parent executable path: %v", err)
} else if strings.HasSuffix(p, " (deleted)") {
log.Fatal("fortify executable has been deleted")
} else if p != fmain {
log.Fatal("this program must be started by fortify")
log.Fatal("hakurei executable has been deleted")
} else if p != mustCheckPath(hmain) {
log.Fatal("this program must be started by hakurei")
} else {
toolPath = p
}
// uid = 1000000 +
@ -61,27 +52,27 @@ func main() {
// aid
uid := 1000000
// refuse to run if fsurc is not protected correctly
if s, err := os.Stat(fsuConfFile); err != nil {
// refuse to run if hsurc is not protected correctly
if s, err := os.Stat(hsuConfFile); err != nil {
log.Fatal(err)
} else if s.Mode().Perm() != 0400 {
log.Fatal("bad fsurc perm")
log.Fatal("bad hsurc perm")
} else if st := s.Sys().(*syscall.Stat_t); st.Uid != 0 || st.Gid != 0 {
log.Fatal("fsurc must be owned by uid 0")
log.Fatal("hsurc must be owned by uid 0")
}
// authenticate before accepting user input
if f, err := os.Open(fsuConfFile); err != nil {
if f, err := os.Open(hsuConfFile); err != nil {
log.Fatal(err)
} else if fid, ok := mustParseConfig(f, puid); !ok {
log.Fatalf("uid %d is not in the fsurc file", puid)
log.Fatalf("uid %d is not in the hsurc file", puid)
} else {
uid += fid * 10000
}
// allowed aid range 0 to 9999
if as, ok := os.LookupEnv(envAID); !ok {
log.Fatal("FORTIFY_APP_ID not set")
log.Fatal("HAKUREI_APP_ID not set")
} else if aid, err := parseUint32Fast(as); err != nil || aid < 0 || aid > 9999 {
log.Fatal("invalid aid")
} else {
@ -91,12 +82,12 @@ func main() {
// pass through setup fd to shim
var shimSetupFd string
if s, ok := os.LookupEnv(envShim); !ok {
// fortify requests target uid
// hakurei requests target uid
// print resolved uid and exit
fmt.Print(uid)
os.Exit(0)
} else if len(s) != 1 || s[0] > '9' || s[0] < '3' {
log.Fatal("FORTIFY_SHIM holds an invalid value")
log.Fatal("HAKUREI_SHIM holds an invalid value")
} else {
shimSetupFd = s
}
@ -133,7 +124,7 @@ func main() {
panic("uid out of bounds")
}
// careful! users in the allowlist is effectively allowed to drop groups via fsu
// careful! users in the allowlist is effectively allowed to drop groups via hsu
if err := syscall.Setresgid(uid, uid, uid); err != nil {
log.Fatalf("cannot set gid: %v", err)
@ -147,13 +138,9 @@ func main() {
if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 {
log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
}
if err := syscall.Exec(fmain, []string{"fortify", "shim"}, []string{envShim + "=" + shimSetupFd}); err != nil {
if err := syscall.Exec(toolPath, []string{"hakurei", "shim"}, []string{envShim + "=" + shimSetupFd}); err != nil {
log.Fatalf("cannot start shim: %v", err)
}
panic("unreachable")
}
func checkPath(p string) (string, bool) {
return p, p != compPoison && p != "" && path.IsAbs(p)
}

23
cmd/hsu/package.nix Normal file
View File

@ -0,0 +1,23 @@
{
lib,
buildGoModule,
hakurei ? abort "hakurei package required",
}:
buildGoModule {
pname = "${hakurei.pname}-hsu";
inherit (hakurei) version;
src = ./.;
inherit (hakurei) vendorHash;
env.CGO_ENABLED = 0;
preBuild = ''
go mod init hsu >& /dev/null
'';
ldflags = lib.attrsets.foldlAttrs (
ldflags: name: value:
ldflags ++ [ "-X main.${name}=${value}" ]
) [ "-s -w" ] { hmain = "${hakurei}/libexec/hakurei"; };
}

View File

@ -50,7 +50,7 @@ func parseConfig(r io.Reader, puid int) (fid int, ok bool, err error) {
if ok {
// allowed fid range 0 to 99
if fid, err = parseUint32Fast(lf[1]); err != nil || fid < 0 || fid > 99 {
return -1, false, fmt.Errorf("invalid fortify uid on line %d", line)
return -1, false, fmt.Errorf("invalid identity on line %d", line)
}
return
}

View File

@ -65,7 +65,7 @@ func Test_parseConfig(t *testing.T) {
{"empty", 0, -1, "", ``},
{"invalid field", 0, -1, "invalid entry on line 1", `9`},
{"invalid puid", 0, -1, "invalid parent uid on line 1", `f 9`},
{"invalid fid", 1000, -1, "invalid fortify uid on line 1", `1000 f`},
{"invalid fid", 1000, -1, "invalid identity on line 1", `1000 f`},
{"match", 1000, 0, "", `1000 0`},
}

20
cmd/hsu/path.go Normal file
View File

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

154
cmd/planterette/app.go Normal file
View File

@ -0,0 +1,154 @@
package main
import (
"encoding/json"
"log"
"os"
"path"
"git.gensokyo.uk/security/hakurei/dbus"
"git.gensokyo.uk/security/hakurei/hst"
"git.gensokyo.uk/security/hakurei/sandbox/seccomp"
"git.gensokyo.uk/security/hakurei/system"
)
type appInfo struct {
Name string `json:"name"`
Version string `json:"version"`
// passed through to [hst.Config]
ID string `json:"id"`
// passed through to [hst.Config]
Identity int `json:"identity"`
// passed through to [hst.Config]
Groups []string `json:"groups,omitempty"`
// passed through to [hst.Config]
Devel bool `json:"devel,omitempty"`
// passed through to [hst.Config]
Userns bool `json:"userns,omitempty"`
// passed through to [hst.Config]
Net bool `json:"net,omitempty"`
// passed through to [hst.Config]
Device bool `json:"dev,omitempty"`
// passed through to [hst.Config]
Tty bool `json:"tty,omitempty"`
// passed through to [hst.Config]
MapRealUID bool `json:"map_real_uid,omitempty"`
// passed through to [hst.Config]
DirectWayland bool `json:"direct_wayland,omitempty"`
// passed through to [hst.Config]
SystemBus *dbus.Config `json:"system_bus,omitempty"`
// passed through to [hst.Config]
SessionBus *dbus.Config `json:"session_bus,omitempty"`
// passed through to [hst.Config]
Enablements system.Enablement `json:"enablements"`
// passed through to [hst.Config]
Multiarch bool `json:"multiarch,omitempty"`
// passed through to [hst.Config]
Bluetooth bool `json:"bluetooth,omitempty"`
// allow gpu access within sandbox
GPU bool `json:"gpu"`
// store path to nixGL mesa wrappers
Mesa string `json:"mesa,omitempty"`
// store path to nixGL source
NixGL string `json:"nix_gl,omitempty"`
// store path to activate-and-exec script
Launcher string `json:"launcher"`
// store path to /run/current-system
CurrentSystem string `json:"current_system"`
// store path to home-manager activation package
ActivationPackage string `json:"activation_package"`
}
func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool) *hst.Config {
config := &hst.Config{
ID: app.ID,
Path: argv[0],
Args: argv,
Enablements: app.Enablements,
SystemBus: app.SystemBus,
SessionBus: app.SessionBus,
DirectWayland: app.DirectWayland,
Username: "hakurei",
Shell: shellPath,
Data: pathSet.homeDir,
Dir: path.Join("/data/data", app.ID),
Identity: app.Identity,
Groups: app.Groups,
Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name),
Devel: app.Devel,
Userns: app.Userns,
Net: app.Net,
Device: app.Device,
Tty: app.Tty || flagDropShell,
MapRealUID: app.MapRealUID,
Filesystem: []*hst.FilesystemConfig{
{Src: path.Join(pathSet.nixPath, "store"), Dst: "/nix/store", Must: true},
{Src: pathSet.metaPath, Dst: path.Join(hst.Tmp, "app"), Must: true},
{Src: "/etc/resolv.conf"},
{Src: "/sys/block"},
{Src: "/sys/bus"},
{Src: "/sys/class"},
{Src: "/sys/dev"},
{Src: "/sys/devices"},
},
Link: [][2]string{
{app.CurrentSystem, "/run/current-system"},
{"/run/current-system/sw/bin", "/bin"},
{"/run/current-system/sw/bin", "/usr/bin"},
},
Etc: path.Join(pathSet.cacheDir, "etc"),
AutoEtc: true,
},
ExtraPerms: []*hst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
},
}
if app.Multiarch {
config.Container.Seccomp |= seccomp.FilterMultiarch
}
if app.Bluetooth {
config.Container.Seccomp |= seccomp.FilterBluetooth
}
return config
}
func loadAppInfo(name string, beforeFail func()) *appInfo {
bundle := new(appInfo)
if f, err := os.Open(name); err != nil {
beforeFail()
log.Fatalf("cannot open bundle: %v", err)
} else if err = json.NewDecoder(f).Decode(&bundle); err != nil {
beforeFail()
log.Fatalf("cannot parse bundle metadata: %v", err)
} else if err = f.Close(); err != nil {
log.Printf("cannot close bundle metadata: %v", err)
// not fatal
}
if bundle.ID == "" {
beforeFail()
log.Fatal("application identifier must not be empty")
}
return bundle
}
func formatHostname(name string) string {
if h, err := os.Hostname(); err != nil {
log.Printf("cannot get hostname: %v", err)
return "hakurei-" + name
} else {
return h + "-" + name
}
}

View File

@ -7,6 +7,8 @@
{
lib,
stdenv,
closureInfo,
writeScript,
runtimeShell,
writeText,
@ -15,18 +17,21 @@
runCommand,
fetchFromGitHub,
zstd,
nix,
sqlite,
name ? throw "name is required",
version ? throw "version is required",
pname ? "${name}-${version}",
modules ? [ ],
nixosModules ? [ ],
script ? ''
exec "$SHELL" "$@"
'',
id ? name,
app_id ? throw "app_id is required",
identity ? throw "identity is required",
groups ? [ ],
userns ? false,
net ? true,
@ -52,7 +57,7 @@ let
modules = modules ++ [
{
home = {
username = "fortify";
username = "hakurei";
homeDirectory = "/data/data/${id}";
stateVersion = "22.11";
};
@ -60,7 +65,7 @@ let
];
};
launcher = writeScript "fortify-${pname}" ''
launcher = writeScript "hakurei-${pname}" ''
#!${runtimeShell} -el
${script}
'';
@ -72,6 +77,8 @@ let
etc.nixpkgs.source = nixpkgs.outPath;
systemPackages = [ pkgs.nix ];
};
imports = nixosModules;
};
nixos = nixpkgs.lib.nixosSystem {
inherit system;
@ -140,7 +147,7 @@ let
name
version
id
app_id
identity
launcher
groups
userns
@ -164,11 +171,7 @@ let
broadcast = { };
});
enablements =
(if allow_wayland then 1 else 0)
+ (if allow_x11 then 2 else 0)
+ (if allow_dbus then 4 else 0)
+ (if allow_pulse then 8 else 0);
enablements = (if allow_wayland then 1 else 0) + (if allow_x11 then 2 else 0) + (if allow_dbus then 4 else 0) + (if allow_pulse then 8 else 0);
mesa = if gpu then mesaWrappers else null;
nix_gl = if gpu then nixGL else null;
@ -177,26 +180,73 @@ let
};
in
writeScript "fortify-${pname}-bundle-prelude" ''
#!${runtimeShell} -el
OUT="$(mktemp -d)"
TAR="$(mktemp -u)"
set -x
stdenv.mkDerivation {
name = "${pname}.pkg";
inherit version;
__structuredAttrs = true;
nix copy --no-check-sigs --to "$OUT" "${nix}" "${nixos.config.system.build.toplevel}"
nix store --store "$OUT" optimise
chmod -R +r "$OUT/nix/var"
nix copy --no-check-sigs --to "file://$OUT/res?compression=zstd&compression-level=19&parallel-compression=true" \
"${homeManagerConfiguration.activationPackage}" \
"${launcher}" ${if gpu then "${mesaWrappers} ${nixGL}" else ""}
mkdir -p "$OUT/etc"
tar -C "$OUT/etc" -xf "${etc}/etc.tar"
cp "${writeText "bundle.json" info}" "$OUT/bundle.json"
nativeBuildInputs = [
zstd
nix
sqlite
];
# creating an intermediate file improves zstd performance
tar -C "$OUT" -cf "$TAR" .
chmod +w -R "$OUT" && rm -rf "$OUT"
buildCommand = ''
NIX_ROOT="$(mktemp -d)"
export USER="nobody"
zstd -T0 -19 -fo "${pname}.pkg" "$TAR"
rm "$TAR"
''
# create bootstrap store
bootstrapClosureInfo="${
closureInfo {
rootPaths = [
nix
nixos.config.system.build.toplevel
];
}
}"
echo "copying bootstrap store paths..."
mkdir -p "$NIX_ROOT/nix/store"
xargs -n 1 -a "$bootstrapClosureInfo/store-paths" cp -at "$NIX_ROOT/nix/store/"
NIX_REMOTE="local?root=$NIX_ROOT" nix-store --load-db < "$bootstrapClosureInfo/registration"
NIX_REMOTE="local?root=$NIX_ROOT" nix-store --optimise
sqlite3 "$NIX_ROOT/nix/var/nix/db/db.sqlite" "UPDATE ValidPaths SET registrationTime = ''${SOURCE_DATE_EPOCH}"
chmod -R +r "$NIX_ROOT/nix/var"
# create binary cache
closureInfo="${
closureInfo {
rootPaths =
[
homeManagerConfiguration.activationPackage
launcher
]
++ optionals gpu [
mesaWrappers
nixGL
];
}
}"
echo "copying application paths..."
TMP_STORE="$(mktemp -d)"
mkdir -p "$TMP_STORE/nix/store"
xargs -n 1 -a "$closureInfo/store-paths" cp -at "$TMP_STORE/nix/store/"
NIX_REMOTE="local?root=$TMP_STORE" nix-store --load-db < "$closureInfo/registration"
sqlite3 "$TMP_STORE/nix/var/nix/db/db.sqlite" "UPDATE ValidPaths SET registrationTime = ''${SOURCE_DATE_EPOCH}"
NIX_REMOTE="local?root=$TMP_STORE" nix --offline --extra-experimental-features nix-command \
--verbose --log-format raw-with-logs \
copy --all --no-check-sigs --to \
"file://$NIX_ROOT/res?compression=zstd&compression-level=19&parallel-compression=true"
# package /etc
mkdir -p "$NIX_ROOT/etc"
tar -C "$NIX_ROOT/etc" -xf "${etc}/etc.tar"
# write metadata
cp "${writeText "bundle.json" info}" "$NIX_ROOT/bundle.json"
# create an intermediate file to improve zstd performance
INTER="$(mktemp)"
tar -C "$NIX_ROOT" -cf "$INTER" .
zstd -T0 -19 -fo "$out" "$INTER"
'';
}

333
cmd/planterette/main.go Normal file
View File

@ -0,0 +1,333 @@
package main
import (
"context"
"encoding/json"
"errors"
"log"
"os"
"os/signal"
"path"
"syscall"
"git.gensokyo.uk/security/hakurei/command"
"git.gensokyo.uk/security/hakurei/hst"
"git.gensokyo.uk/security/hakurei/internal"
"git.gensokyo.uk/security/hakurei/internal/hlog"
)
const shellPath = "/run/current-system/sw/bin/bash"
var (
errSuccess = errors.New("success")
)
func init() {
hlog.Prepare("planterette")
if err := os.Setenv("SHELL", shellPath); err != nil {
log.Fatalf("cannot set $SHELL: %v", err)
}
}
func main() {
if os.Geteuid() == 0 {
log.Fatal("this program must not run as root")
}
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer stop() // unreachable
var (
flagVerbose bool
flagDropShell bool
)
c := command.New(os.Stderr, log.Printf, "planterette", func([]string) error { internal.InstallFmsg(flagVerbose); return nil }).
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next hakurei action")
{
var (
flagDropShellActivate bool
)
c.NewCommand("install", "Install an application from its package", func(args []string) error {
if len(args) != 1 {
log.Println("invalid argument")
return syscall.EINVAL
}
pkgPath := args[0]
if !path.IsAbs(pkgPath) {
if dir, err := os.Getwd(); err != nil {
log.Printf("cannot get current directory: %v", err)
return err
} else {
pkgPath = path.Join(dir, pkgPath)
}
}
/*
Look up paths to programs started by planterette.
This is done here to ease error handling as cleanup is not yet required.
*/
var (
_ = lookPath("zstd")
tar = lookPath("tar")
chmod = lookPath("chmod")
rm = lookPath("rm")
)
/*
Extract package and set up for cleanup.
*/
var workDir string
if p, err := os.MkdirTemp("", "planterette.*"); err != nil {
log.Printf("cannot create temporary directory: %v", err)
return err
} else {
workDir = p
}
cleanup := func() {
// should be faster than a native implementation
mustRun(chmod, "-R", "+w", workDir)
mustRun(rm, "-rf", workDir)
}
beforeRunFail.Store(&cleanup)
mustRun(tar, "-C", workDir, "-xf", pkgPath)
/*
Parse bundle and app metadata, do pre-install checks.
*/
bundle := loadAppInfo(path.Join(workDir, "bundle.json"), cleanup)
pathSet := pathSetByApp(bundle.ID)
a := bundle
if s, err := os.Stat(pathSet.metaPath); err != nil {
if !os.IsNotExist(err) {
cleanup()
log.Printf("cannot access %q: %v", pathSet.metaPath, err)
return err
}
// did not modify app, clean installation condition met later
} else if s.IsDir() {
cleanup()
log.Printf("metadata path %q is not a file", pathSet.metaPath)
return syscall.EBADMSG
} else {
a = loadAppInfo(pathSet.metaPath, cleanup)
if a.ID != bundle.ID {
cleanup()
log.Printf("app %q claims to have identifier %q",
bundle.ID, a.ID)
return syscall.EBADE
}
// sec: should verify credentials
}
if a != bundle {
// do not try to re-install
if a.NixGL == bundle.NixGL &&
a.CurrentSystem == bundle.CurrentSystem &&
a.Launcher == bundle.Launcher &&
a.ActivationPackage == bundle.ActivationPackage {
cleanup()
log.Printf("package %q is identical to local application %q",
pkgPath, a.ID)
return errSuccess
}
// identity determines uid
if a.Identity != bundle.Identity {
cleanup()
log.Printf("package %q identity %d differs from installed %d",
pkgPath, bundle.Identity, a.Identity)
return syscall.EBADE
}
// sec: should compare version string
hlog.Verbosef("installing application %q version %q over local %q",
bundle.ID, bundle.Version, a.Version)
} else {
hlog.Verbosef("application %q clean installation", bundle.ID)
// sec: should install credentials
}
/*
Setup steps for files owned by the target user.
*/
withCacheDir(ctx, "install", []string{
// export inner bundle path in the environment
"export BUNDLE=" + hst.Tmp + "/bundle",
// replace inner /etc
"mkdir -p etc",
"chmod -R +w etc",
"rm -rf etc",
"cp -dRf $BUNDLE/etc etc",
// replace inner /nix
"mkdir -p nix",
"chmod -R +w nix",
"rm -rf nix",
"cp -dRf /nix nix",
// copy from binary cache
"nix copy --offline --no-check-sigs --all --from file://$BUNDLE/res --to $PWD",
// deduplicate nix store
"nix store --offline --store $PWD optimise",
// make cache directory world-readable for autoetc
"chmod 0755 .",
}, workDir, bundle, pathSet, flagDropShell, cleanup)
if bundle.GPU {
withCacheDir(ctx, "mesa-wrappers", []string{
// link nixGL mesa wrappers
"mkdir -p nix/.nixGL",
"ln -s " + bundle.Mesa + "/bin/nixGLIntel nix/.nixGL/nixGL",
"ln -s " + bundle.Mesa + "/bin/nixVulkanIntel nix/.nixGL/nixVulkan",
}, workDir, bundle, pathSet, false, cleanup)
}
/*
Activate home-manager generation.
*/
withNixDaemon(ctx, "activate", []string{
// clean up broken links
"mkdir -p .local/state/{nix,home-manager}",
"chmod -R +w .local/state/{nix,home-manager}",
"rm -rf .local/state/{nix,home-manager}",
// run activation script
bundle.ActivationPackage + "/activate",
}, false, func(config *hst.Config) *hst.Config { return config },
bundle, pathSet, flagDropShellActivate, cleanup)
/*
Installation complete. Write metadata to block re-installs or downgrades.
*/
// serialise metadata to ensure consistency
if f, err := os.OpenFile(pathSet.metaPath+"~", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil {
cleanup()
log.Printf("cannot create metadata file: %v", err)
return err
} else if err = json.NewEncoder(f).Encode(bundle); err != nil {
cleanup()
log.Printf("cannot write metadata: %v", err)
return err
} else if err = f.Close(); err != nil {
log.Printf("cannot close metadata file: %v", err)
// not fatal
}
if err := os.Rename(pathSet.metaPath+"~", pathSet.metaPath); err != nil {
cleanup()
log.Printf("cannot rename metadata file: %v", err)
return err
}
cleanup()
return errSuccess
}).
Flag(&flagDropShellActivate, "s", command.BoolFlag(false), "Drop to a shell on activation")
}
{
var (
flagDropShellNixGL bool
flagAutoDrivers bool
)
c.NewCommand("start", "Start an application", func(args []string) error {
if len(args) < 1 {
log.Println("invalid argument")
return syscall.EINVAL
}
/*
Parse app metadata.
*/
id := args[0]
pathSet := pathSetByApp(id)
a := loadAppInfo(pathSet.metaPath, func() {})
if a.ID != id {
log.Printf("app %q claims to have identifier %q", id, a.ID)
return syscall.EBADE
}
/*
Prepare nixGL.
*/
if a.GPU && flagAutoDrivers {
withNixDaemon(ctx, "nix-gl", []string{
"mkdir -p /nix/.nixGL/auto",
"rm -rf /nix/.nixGL/auto",
"export NIXPKGS_ALLOW_UNFREE=1",
"nix build --impure " +
"--out-link /nix/.nixGL/auto/opengl " +
"--override-input nixpkgs path:/etc/nixpkgs " +
"path:" + a.NixGL,
"nix build --impure " +
"--out-link /nix/.nixGL/auto/vulkan " +
"--override-input nixpkgs path:/etc/nixpkgs " +
"path:" + a.NixGL + "#nixVulkanNvidia",
}, true, func(config *hst.Config) *hst.Config {
config.Container.Filesystem = append(config.Container.Filesystem, []*hst.FilesystemConfig{
{Src: "/etc/resolv.conf"},
{Src: "/sys/block"},
{Src: "/sys/bus"},
{Src: "/sys/class"},
{Src: "/sys/dev"},
{Src: "/sys/devices"},
}...)
appendGPUFilesystem(config)
return config
}, a, pathSet, flagDropShellNixGL, func() {})
}
/*
Create app configuration.
*/
argv := make([]string, 1, len(args))
if !flagDropShell {
argv[0] = a.Launcher
} else {
argv[0] = shellPath
}
argv = append(argv, args[1:]...)
config := a.toFst(pathSet, argv, flagDropShell)
/*
Expose GPU devices.
*/
if a.GPU {
config.Container.Filesystem = append(config.Container.Filesystem,
&hst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(hst.Tmp, "nixGL")})
appendGPUFilesystem(config)
}
/*
Spawn app.
*/
mustRunApp(ctx, config, func() {})
return errSuccess
}).
Flag(&flagDropShellNixGL, "s", command.BoolFlag(false), "Drop to a shell on nixGL build").
Flag(&flagAutoDrivers, "auto-drivers", command.BoolFlag(false), "Attempt automatic opengl driver detection")
}
c.MustParse(os.Args[1:], func(err error) {
hlog.Verbosef("command returned %v", err)
if errors.Is(err, errSuccess) {
hlog.BeforeExit()
os.Exit(0)
}
})
log.Fatal("unreachable")
}

101
cmd/planterette/paths.go Normal file
View File

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

View File

@ -1,6 +1,7 @@
package main
import (
"context"
"encoding/json"
"errors"
"io"
@ -8,33 +9,27 @@ import (
"os"
"os/exec"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/hakurei/hst"
"git.gensokyo.uk/security/hakurei/internal"
"git.gensokyo.uk/security/hakurei/internal/hlog"
)
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
var hakureiPath = internal.MustHakureiPath()
var (
Fmain = compPoison
)
func fortifyApp(config *fst.Config, beforeFail func()) {
func mustRunApp(ctx context.Context, config *hst.Config, beforeFail func()) {
var (
cmd *exec.Cmd
st io.WriteCloser
)
if p, ok := internal.Path(Fmain); !ok {
beforeFail()
log.Fatal("invalid fortify path, this copy of fpkg is not compiled correctly")
} else if r, w, err := os.Pipe(); err != nil {
if r, w, err := os.Pipe(); err != nil {
beforeFail()
log.Fatalf("cannot pipe: %v", err)
} else {
if fmsg.Load() {
cmd = exec.Command(p, "-v", "app", "3")
if hlog.Load() {
cmd = exec.CommandContext(ctx, hakureiPath, "-v", "app", "3")
} else {
cmd = exec.Command(p, "app", "3")
cmd = exec.CommandContext(ctx, hakureiPath, "app", "3")
}
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.ExtraFiles = []*os.File{r}
@ -50,7 +45,7 @@ func fortifyApp(config *fst.Config, beforeFail func()) {
if err := cmd.Start(); err != nil {
beforeFail()
log.Fatalf("cannot start fortify: %v", err)
log.Fatalf("cannot start hakurei: %v", err)
}
if err := cmd.Wait(); err != nil {
var exitError *exec.ExitError

View File

@ -0,0 +1,62 @@
{ pkgs, ... }:
{
users.users = {
alice = {
isNormalUser = true;
description = "Alice Foobar";
password = "foobar";
uid = 1000;
};
};
home-manager.users.alice.home.stateVersion = "24.11";
# Automatically login on tty1 as a normal user:
services.getty.autologinUser = "alice";
environment = {
variables = {
SWAYSOCK = "/tmp/sway-ipc.sock";
WLR_RENDERER = "pixman";
};
};
# Automatically configure and start Sway when logging in on tty1:
programs.bash.loginShellInit = ''
if [ "$(tty)" = "/dev/tty1" ]; then
set -e
mkdir -p ~/.config/sway
(sed s/Mod4/Mod1/ /etc/sway/config &&
echo 'output * bg ${pkgs.nixos-artwork.wallpapers.simple-light-gray.gnomeFilePath} fill' &&
echo 'output Virtual-1 res 1680x1050') > ~/.config/sway/config
sway --validate
systemd-cat --identifier=session sway && touch /tmp/sway-exit-ok
fi
'';
programs.sway.enable = true;
virtualisation = {
diskSize = 6 * 1024;
qemu.options = [
# Need to switch to a different GPU driver than the default one (-vga std) so that Sway can launch:
"-vga none -device virtio-gpu-pci"
# Increase zstd performance:
"-smp 8"
];
};
environment.hakurei = {
enable = true;
stateDir = "/var/lib/hakurei";
users.alice = 0;
extraHomeConfig = {
home.stateVersion = "23.05";
};
};
}

View File

@ -0,0 +1,34 @@
{
nixosTest,
callPackage,
system,
self,
}:
let
buildPackage = self.buildPackage.${system};
in
nixosTest {
name = "planterette";
nodes.machine = {
environment.etc = {
"foot.pkg".source = callPackage ./foot.nix { inherit buildPackage; };
};
imports = [
./configuration.nix
self.nixosModules.hakurei
self.inputs.home-manager.nixosModules.home-manager
];
};
# adapted from nixos sway integration tests
# testScriptWithTypes:49: error: Cannot call function of unknown type
# (machine.succeed if succeed else machine.execute)(
# ^
# Found 1 error in 1 file (checked 1 source file)
skipTypeCheck = true;
testScript = builtins.readFile ./test.py;
}

View File

@ -0,0 +1,48 @@
{
lib,
buildPackage,
foot,
wayland-utils,
inconsolata,
}:
buildPackage {
name = "foot";
inherit (foot) version;
identity = 2;
id = "org.codeberg.dnkl.foot";
modules = [
{
home.packages = [
foot
# For wayland-info:
wayland-utils
];
}
];
nixosModules = [
{
# To help with OCR:
environment.etc."xdg/foot/foot.ini".text = lib.generators.toINI { } {
main = {
font = "inconsolata:size=14";
};
colors = rec {
foreground = "000000";
background = "ffffff";
regular2 = foreground;
};
};
fonts.packages = [ inconsolata ];
}
];
script = ''
exec foot "$@"
'';
}

View File

@ -0,0 +1,108 @@
import json
import shlex
q = shlex.quote
NODE_GROUPS = ["nodes", "floating_nodes"]
def swaymsg(command: str = "", succeed=True, type="command"):
assert command != "" or type != "command", "Must specify command or type"
shell = q(f"swaymsg -t {q(type)} -- {q(command)}")
with machine.nested(
f"sending swaymsg {shell!r}" + " (allowed to fail)" * (not succeed)
):
ret = (machine.succeed if succeed else machine.execute)(
f"su - alice -c {shell}"
)
# execute also returns a status code, but disregard.
if not succeed:
_, ret = ret
if not succeed and not ret:
return None
parsed = json.loads(ret)
return parsed
def walk(tree):
yield tree
for group in NODE_GROUPS:
for node in tree.get(group, []):
yield from walk(node)
def wait_for_window(pattern):
def func(last_chance):
nodes = (node["name"] for node in walk(swaymsg(type="get_tree")))
if last_chance:
nodes = list(nodes)
machine.log(f"Last call! Current list of windows: {nodes}")
return any(pattern in name for name in nodes)
retry(func)
def collect_state_ui(name):
swaymsg(f"exec hakurei ps > '/tmp/{name}.ps'")
machine.copy_from_vm(f"/tmp/{name}.ps", "")
swaymsg(f"exec hakurei --json ps > '/tmp/{name}.json'")
machine.copy_from_vm(f"/tmp/{name}.json", "")
machine.screenshot(name)
def check_state(name, enablements):
instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei --json ps"))
if len(instances) != 1:
raise Exception(f"unexpected state length {len(instances)}")
instance = next(iter(instances.values()))
config = instance['config']
if len(config['args']) != 1 or not (config['args'][0].startswith("/nix/store/")) or f"hakurei-{name}-" not in (config['args'][0]):
raise Exception(f"unexpected args {instance['config']['args']}")
if config['enablements'] != enablements:
raise Exception(f"unexpected enablements {instance['config']['enablements']}")
start_all()
machine.wait_for_unit("multi-user.target")
# To check hakurei's version:
print(machine.succeed("sudo -u alice -i hakurei version"))
# Wait for Sway to complete startup:
machine.wait_for_file("/run/user/1000/wayland-1")
machine.wait_for_file("/tmp/sway-ipc.sock")
# Prepare planterette directory:
machine.succeed("install -dm 0700 -o alice -g users /var/lib/hakurei/1000")
# Install planterette app:
swaymsg("exec planterette -v install /etc/foot.pkg && touch /tmp/planterette-install-ok")
machine.wait_for_file("/tmp/planterette-install-ok")
# Start app (foot) with Wayland enablement:
swaymsg("exec planterette -v start org.codeberg.dnkl.foot")
wait_for_window("hakurei@machine-foot")
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
machine.wait_for_file("/tmp/hakurei.1000/tmpdir/2/success-client")
collect_state_ui("app_wayland")
check_state("foot", 13)
# Verify acl on XDG_RUNTIME_DIR:
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002"))
machine.send_chars("exit\n")
machine.wait_until_fails("pgrep foot")
# Verify acl cleanup on XDG_RUNTIME_DIR:
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002")
# Exit Sway and verify process exit status 0:
swaymsg("exit", succeed=False)
machine.wait_for_file("/tmp/sway-exit-ok")
# Print hakurei runDir contents:
print(machine.succeed("find /run/user/1000/hakurei"))

114
cmd/planterette/with.go Normal file
View File

@ -0,0 +1,114 @@
package main
import (
"context"
"path"
"strings"
"git.gensokyo.uk/security/hakurei/hst"
"git.gensokyo.uk/security/hakurei/internal"
"git.gensokyo.uk/security/hakurei/sandbox/seccomp"
)
func withNixDaemon(
ctx context.Context,
action string, command []string, net bool, updateConfig func(config *hst.Config) *hst.Config,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
) {
mustRunAppDropShell(ctx, updateConfig(&hst.Config{
ID: app.ID,
Path: shellPath,
Args: []string{shellPath, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
// start nix-daemon
"nix-daemon --store / & " +
// wait for socket to appear
"(while [ ! -S /nix/var/nix/daemon-socket/socket ]; do sleep 0.01; done) && " +
// create directory so nix stops complaining
"mkdir -p /nix/var/nix/profiles/per-user/root/channels && " +
strings.Join(command, " && ") +
// terminate nix-daemon
" && pkill nix-daemon",
},
Username: "hakurei",
Shell: shellPath,
Data: pathSet.homeDir,
Dir: path.Join("/data/data", 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
Net: net,
Seccomp: seccomp.FilterMultiarch,
Tty: dropShell,
Filesystem: []*hst.FilesystemConfig{
{Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true},
},
Link: [][2]string{
{app.CurrentSystem, "/run/current-system"},
{"/run/current-system/sw/bin", "/bin"},
{"/run/current-system/sw/bin", "/usr/bin"},
},
Etc: path.Join(pathSet.cacheDir, "etc"),
AutoEtc: true,
},
}), dropShell, beforeFail)
}
func withCacheDir(
ctx context.Context,
action string, command []string, workDir string,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
mustRunAppDropShell(ctx, &hst.Config{
ID: app.ID,
Path: shellPath,
Args: []string{shellPath, "-lc", strings.Join(command, " && ")},
Username: "nixos",
Shell: shellPath,
Data: pathSet.cacheDir, // this also ensures cacheDir via shim
Dir: path.Join("/data/data", app.ID, "cache"),
ExtraPerms: []*hst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
{Path: workDir, Execute: true},
},
Identity: app.Identity,
Container: &hst.ContainerConfig{
Hostname: formatHostname(app.Name) + "-" + action,
Seccomp: seccomp.FilterMultiarch,
Tty: dropShell,
Filesystem: []*hst.FilesystemConfig{
{Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true},
{Src: workDir, Dst: path.Join(hst.Tmp, "bundle"), Must: true},
},
Link: [][2]string{
{app.CurrentSystem, "/run/current-system"},
{"/run/current-system/sw/bin", "/bin"},
{"/run/current-system/sw/bin", "/usr/bin"},
},
Etc: path.Join(workDir, "etc"),
AutoEtc: true,
},
}, dropShell, beforeFail)
}
func mustRunAppDropShell(ctx context.Context, config *hst.Config, dropShell bool, beforeFail func()) {
if dropShell {
config.Args = []string{shellPath, "-l"}
mustRunApp(ctx, config, beforeFail)
beforeFail()
internal.Exit(0)
}
mustRunApp(ctx, config, beforeFail)
}

65
command/builder.go Normal file
View File

@ -0,0 +1,65 @@
package command
import (
"flag"
"fmt"
"io"
)
// New initialises a root Node.
func New(output io.Writer, logf LogFunc, name string, early HandlerFunc) Command {
c := rootNode{newNode(output, logf, name, "")}
c.f = early
return c
}
func newNode(output io.Writer, logf LogFunc, name, usage string) *node {
n := &node{
name: name, usage: usage,
out: output, logf: logf,
set: flag.NewFlagSet(name, flag.ContinueOnError),
}
n.set.SetOutput(output)
n.set.Usage = func() {
_ = n.writeHelp()
if n.suffix.Len() > 0 {
_, _ = fmt.Fprintln(output, "Flags:")
n.set.PrintDefaults()
_, _ = fmt.Fprintln(output)
}
}
return n
}
func (n *node) Command(name, usage string, f HandlerFunc) Node {
n.NewCommand(name, usage, f)
return n
}
func (n *node) NewCommand(name, usage string, f HandlerFunc) Flag[Node] {
if f == nil {
panic("invalid handler")
}
if name == "" || usage == "" {
panic("invalid subcommand")
}
s := newNode(n.out, n.logf, name, usage)
s.f = f
if !n.adopt(s) {
panic("attempted to initialise subcommand with non-unique name")
}
return s
}
func (n *node) New(name, usage string) Node {
if name == "" || usage == "" {
panic("invalid subcommand tree")
}
s := newNode(n.out, n.logf, name, usage)
if !n.adopt(s) {
panic("attempted to initialise subcommand tree with non-unique name")
}
return s
}

56
command/builder_test.go Normal file
View File

@ -0,0 +1,56 @@
package command_test
import (
"testing"
"git.gensokyo.uk/security/hakurei/command"
)
func TestBuild(t *testing.T) {
c := command.New(nil, nil, "test", nil)
stubHandler := func([]string) error { panic("unreachable") }
t.Run("nil direct handler", func(t *testing.T) {
defer checkRecover(t, "Command", "invalid handler")
c.Command("name", "usage", nil)
})
t.Run("direct zero length", func(t *testing.T) {
wantPanic := "invalid subcommand"
t.Run("zero length name", func(t *testing.T) { defer checkRecover(t, "Command", wantPanic); c.Command("", "usage", stubHandler) })
t.Run("zero length usage", func(t *testing.T) { defer checkRecover(t, "Command", wantPanic); c.Command("name", "", stubHandler) })
})
t.Run("direct adopt unique names", func(t *testing.T) {
c.Command("d0", "usage", stubHandler)
c.Command("d1", "usage", stubHandler)
})
t.Run("direct adopt non-unique name", func(t *testing.T) {
defer checkRecover(t, "Command", "attempted to initialise subcommand with non-unique name")
c.Command("d0", "usage", stubHandler)
})
t.Run("zero length", func(t *testing.T) {
wantPanic := "invalid subcommand tree"
t.Run("zero length name", func(t *testing.T) { defer checkRecover(t, "New", wantPanic); c.New("", "usage") })
t.Run("zero length usage", func(t *testing.T) { defer checkRecover(t, "New", wantPanic); c.New("name", "") })
})
t.Run("direct adopt unique names", func(t *testing.T) {
c.New("t0", "usage")
c.New("t1", "usage")
})
t.Run("direct adopt non-unique name", func(t *testing.T) {
defer checkRecover(t, "Command", "attempted to initialise subcommand tree with non-unique name")
c.New("t0", "usage")
})
}
func checkRecover(t *testing.T, name, wantPanic string) {
if r := recover(); r != wantPanic {
t.Errorf("%s: panic = %v; wantPanic %v",
name, r, wantPanic)
}
}

55
command/command.go Normal file
View File

@ -0,0 +1,55 @@
// Package command implements generic nested command parsing.
package command
import (
"flag"
"strings"
)
// UsageInternal causes the command to be hidden from help text when set as the usage string.
const UsageInternal = "internal"
type (
// HandlerFunc is called when matching a directly handled subcommand tree.
HandlerFunc = func(args []string) error
// LogFunc is the function signature of a printf function.
LogFunc = func(format string, a ...any)
// FlagDefiner is a deferred flag definer value, usually encapsulating the default value.
FlagDefiner interface {
// Define defines the flag in set.
Define(b *strings.Builder, set *flag.FlagSet, p any, name, usage string)
}
Flag[T any] interface {
// Flag defines a generic flag type in Node's flag set.
Flag(p any, name string, value FlagDefiner, usage string) T
}
Command interface {
Parse(arguments []string) error
// MustParse determines exit outcomes for Parse errors
// and calls handleError if [HandlerFunc] returns a non-nil error.
MustParse(arguments []string, handleError func(error))
baseNode[Command]
}
Node baseNode[Node]
baseNode[T any] interface {
// Command appends a subcommand with direct command handling.
Command(name, usage string, f HandlerFunc) T
// New returns a new subcommand tree.
New(name, usage string) (sub Node)
// NewCommand returns a new subcommand with direct command handling.
NewCommand(name, usage string, f HandlerFunc) (sub Flag[Node])
// PrintHelp prints a help message to the configured writer.
PrintHelp()
Flag[T]
}
)

77
command/flag.go Normal file
View File

@ -0,0 +1,77 @@
package command
import (
"errors"
"flag"
"strings"
)
// FlagError wraps errors returned by [flag].
type FlagError struct{ error }
func (e FlagError) Success() bool { return errors.Is(e.error, flag.ErrHelp) }
func (e FlagError) Is(target error) bool {
return (e.error == nil && target == nil) ||
((e.error != nil && target != nil) && e.error.Error() == target.Error())
}
func (n *node) Flag(p any, name string, value FlagDefiner, usage string) Node {
value.Define(&n.suffix, n.set, p, name, usage)
return n
}
// StringFlag is the default value of a string flag.
type StringFlag string
func (v StringFlag) Define(b *strings.Builder, set *flag.FlagSet, p any, name, usage string) {
set.StringVar(p.(*string), name, string(v), usage)
b.WriteString(" [" + prettyFlag(name) + " <value>]")
}
// IntFlag is the default value of an int flag.
type IntFlag int
func (v IntFlag) Define(b *strings.Builder, set *flag.FlagSet, p any, name, usage string) {
set.IntVar(p.(*int), name, int(v), usage)
b.WriteString(" [" + prettyFlag(name) + " <int>]")
}
// BoolFlag is the default value of a bool flag.
type BoolFlag bool
func (v BoolFlag) Define(b *strings.Builder, set *flag.FlagSet, p any, name, usage string) {
set.BoolVar(p.(*bool), name, bool(v), usage)
b.WriteString(" [" + prettyFlag(name) + "]")
}
// RepeatableFlag implements an ordered, repeatable string flag.
type RepeatableFlag []string
func (r *RepeatableFlag) String() string {
if r == nil {
return "<nil>"
}
return strings.Join(*r, " ")
}
func (r *RepeatableFlag) Set(v string) error {
*r = append(*r, v)
return nil
}
func (r *RepeatableFlag) Define(b *strings.Builder, set *flag.FlagSet, _ any, name, usage string) {
set.Var(r, name, usage)
b.WriteString(" [" + prettyFlag(name) + " <value>]")
}
// this has no effect on parse outcome
func prettyFlag(name string) string {
switch len(name) {
case 0:
panic("zero length flag name")
case 1:
return "-" + name
default:
return "--" + name
}
}

53
command/help.go Normal file
View File

@ -0,0 +1,53 @@
package command
import (
"errors"
"fmt"
"io"
"strings"
"text/tabwriter"
)
var ErrHelp = errors.New("help requested")
func (n *node) PrintHelp() { _ = n.writeHelp() }
func (n *node) writeHelp() error {
if _, err := fmt.Fprintf(n.out,
"\nUsage:\t%s [-h | --help]%s COMMAND [OPTIONS]\n",
strings.Join(append(n.prefix, n.name), " "), &n.suffix,
); err != nil {
return err
}
if n.child != nil {
if _, err := fmt.Fprint(n.out, "\nCommands:\n"); err != nil {
return err
}
}
tw := tabwriter.NewWriter(n.out, 0, 1, 4, ' ', 0)
if err := n.child.writeCommands(tw); err != nil {
return err
}
if err := tw.Flush(); err != nil {
return err
}
_, err := n.out.Write([]byte{'\n'})
if err == nil {
err = ErrHelp
}
return err
}
func (n *node) writeCommands(w io.Writer) error {
if n == nil {
return nil
}
if n.usage != UsageInternal {
if _, err := fmt.Fprintf(w, "\t%s\t%s\n", n.name, n.usage); err != nil {
return err
}
}
return n.next.writeCommands(w)
}

40
command/node.go Normal file
View File

@ -0,0 +1,40 @@
package command
import (
"flag"
"io"
"strings"
)
type node struct {
child, next *node
name, usage string
out io.Writer
logf LogFunc
prefix []string
suffix strings.Builder
f HandlerFunc
set *flag.FlagSet
}
func (n *node) adopt(v *node) bool {
if n.child != nil {
return n.child.append(v)
}
n.child = v
return true
}
func (n *node) append(v *node) bool {
if n.name == v.name {
return false
}
if n.next != nil {
return n.next.append(v)
}
n.next = v
return true
}

105
command/parse.go Normal file
View File

@ -0,0 +1,105 @@
package command
import (
"errors"
"log"
"os"
)
var (
ErrEmptyTree = errors.New("subcommand tree has no nodes")
ErrNoMatch = errors.New("did not match any subcommand")
)
func (n *node) Parse(arguments []string) error {
if n.usage == "" { // root node has zero length usage
if n.next != nil {
panic("invalid toplevel state")
}
goto match
}
if len(arguments) == 0 {
// unreachable: zero length args cause upper level to return with a help message
panic("attempted to parse with zero length args")
}
if arguments[0] != n.name {
if n.next == nil {
n.printf("%q is not a valid command", arguments[0])
return ErrNoMatch
}
n.next.prefix = n.prefix
return n.next.Parse(arguments)
}
arguments = arguments[1:]
match:
if n.child != nil {
// propagate help prefix early: flag set usage dereferences help
n.child.prefix = append(n.prefix, n.name)
}
if n.set.Parsed() {
panic("invalid set state")
}
if err := n.set.Parse(arguments); err != nil {
return FlagError{err}
}
args := n.set.Args()
if n.child != nil {
if n.f != nil {
if n.usage != "" { // root node early special case
panic("invalid subcommand tree state")
}
// special case: root node calls HandlerFunc for initialisation
if err := n.f(nil); err != nil {
return err
}
}
if len(args) == 0 {
return n.writeHelp()
}
return n.child.Parse(args)
}
if n.f == nil {
n.printf("%q has no subcommands", n.name)
return ErrEmptyTree
}
return n.f(args)
}
func (n *node) printf(format string, a ...any) {
if n.logf == nil {
log.Printf(format, a...)
} else {
n.logf(format, a...)
}
}
func (n *node) MustParse(arguments []string, handleError func(error)) {
switch err := n.Parse(arguments); err {
case nil:
return
case ErrHelp:
os.Exit(0)
case ErrNoMatch:
os.Exit(1)
case ErrEmptyTree:
os.Exit(1)
default:
var flagError FlagError
if !errors.As(err, &flagError) { // returned by HandlerFunc
handleError(err)
os.Exit(1)
}
if flagError.Success() {
os.Exit(0)
}
os.Exit(1)
}
}

344
command/parse_test.go Normal file
View File

@ -0,0 +1,344 @@
package command_test
import (
"bytes"
"errors"
"flag"
"fmt"
"io"
"log"
"strings"
"testing"
"git.gensokyo.uk/security/hakurei/command"
)
func TestParse(t *testing.T) {
testCases := []struct {
name string
buildTree func(wout, wlog io.Writer) command.Command
args []string
want string
wantLog string
wantErr error
}{
{
"d=0 empty sub",
func(wout, wlog io.Writer) command.Command { return command.New(wout, newLogFunc(wlog), "root", nil) },
[]string{""},
"", "test: \"root\" has no subcommands\n", command.ErrEmptyTree,
},
{
"d=0 empty sub garbage",
func(wout, wlog io.Writer) command.Command { return command.New(wout, newLogFunc(wlog), "root", nil) },
[]string{"a", "b", "c", "d"},
"", "test: \"root\" has no subcommands\n", command.ErrEmptyTree,
},
{
"d=0 no match",
buildTestCommand,
[]string{"nonexistent"},
"", "test: \"nonexistent\" is not a valid command\n", command.ErrNoMatch,
},
{
"d=0 direct error",
buildTestCommand,
[]string{"error"},
"", "", errSuccess,
},
{
"d=0 direct error garbage",
buildTestCommand,
[]string{"error", "0", "1", "2"},
"", "", errSuccess,
},
{
"d=0 direct success out of order",
buildTestCommand,
[]string{"succeed"},
"", "", nil,
},
{
"d=0 direct success output",
buildTestCommand,
[]string{"print", "0", "1", "2"},
"012", "", nil,
},
{
"d=0 out of order string flag",
buildTestCommand,
[]string{"string", "--string", "64d3b4b7b21788585845060e2199a78f"},
"flag provided but not defined: -string\n\nUsage:\ttest string [-h | --help] COMMAND [OPTIONS]\n\n", "",
errors.New("flag provided but not defined: -string"),
},
{
"d=0 string flag",
buildTestCommand,
[]string{"--string", "64d3b4b7b21788585845060e2199a78f", "string"},
"64d3b4b7b21788585845060e2199a78f", "", nil,
},
{
"d=0 int flag",
buildTestCommand,
[]string{"--int", "2147483647", "int"},
"2147483647", "", nil,
},
{
"d=0 repeat flag",
buildTestCommand,
[]string{"--repeat", "0", "--repeat", "1", "--repeat", "2", "--repeat", "3", "--repeat", "4", "repeat"},
"[0 1 2 3 4]", "", nil,
},
{
"d=0 bool flag",
buildTestCommand,
[]string{"-v", "succeed"},
"", "test: verbose\n", nil,
},
{
"d=0 bool flag early error",
buildTestCommand,
[]string{"--fail", "succeed"},
"", "", errSuccess,
},
{
"d=1 empty sub",
buildTestCommand,
[]string{"empty"},
"", "test: \"empty\" has no subcommands\n", command.ErrEmptyTree,
},
{
"d=1 empty sub garbage",
buildTestCommand,
[]string{"empty", "a", "b", "c", "d"},
"", "test: \"empty\" has no subcommands\n", command.ErrEmptyTree,
},
{
"d=1 empty sub help",
buildTestCommand,
[]string{"empty", "-h"},
"\nUsage:\ttest empty [-h | --help] COMMAND [OPTIONS]\n\n", "", flag.ErrHelp,
},
{
"d=1 no match",
buildTestCommand,
[]string{"join", "23aa3bb0", "34986782", "d8859355", "cd9ac317", ", "},
"", "test: \"23aa3bb0\" is not a valid command\n", command.ErrNoMatch,
},
{
"d=1 direct success out",
buildTestCommand,
[]string{"join", "out", "23aa3bb0", "34986782", "d8859355", "cd9ac317", ", "},
"23aa3bb0, 34986782, d8859355, cd9ac317", "", nil,
},
{
"d=1 direct success log",
buildTestCommand,
[]string{"join", "log", "23aa3bb0", "34986782", "d8859355", "cd9ac317", ", "},
"", "test: 23aa3bb0, 34986782, d8859355, cd9ac317\n", nil,
},
{
"d=4 empty sub",
buildTestCommand,
[]string{"deep", "d=2", "d=3", "d=4"},
"", "test: \"d=4\" has no subcommands\n", command.ErrEmptyTree},
{
"d=0 help",
buildTestCommand,
[]string{},
`
Usage: test [-h | --help] [-v] [--fail] [--string <value>] [--int <int>] [--repeat <value>] COMMAND [OPTIONS]
Commands:
error return an error
print wraps Fprint
string print string passed by flag
int print int passed by flag
repeat print repeated values passed by flag
empty empty subcommand
join wraps strings.Join
succeed this command succeeds
deep top level of command tree with various levels
`, "", command.ErrHelp,
},
{
"d=0 help flag",
buildTestCommand,
[]string{"-h"},
`
Usage: test [-h | --help] [-v] [--fail] [--string <value>] [--int <int>] [--repeat <value>] COMMAND [OPTIONS]
Commands:
error return an error
print wraps Fprint
string print string passed by flag
int print int passed by flag
repeat print repeated values passed by flag
empty empty subcommand
join wraps strings.Join
succeed this command succeeds
deep top level of command tree with various levels
Flags:
-fail
fail early
-int int
store value for the "int" command (default -1)
-repeat value
store value for the "repeat" command
-string string
store value for the "string" command (default "default")
-v verbose output
`, "", flag.ErrHelp,
},
{
"d=1 help",
buildTestCommand,
[]string{"join"},
`
Usage: test join [-h | --help] COMMAND [OPTIONS]
Commands:
out write result to wout
log log result to wlog
`, "", command.ErrHelp,
},
{
"d=1 help flag",
buildTestCommand,
[]string{"join", "-h"},
`
Usage: test join [-h | --help] COMMAND [OPTIONS]
Commands:
out write result to wout
log log result to wlog
`, "", flag.ErrHelp,
},
{
"d=2 help",
buildTestCommand,
[]string{"deep", "d=2"},
`
Usage: test deep d=2 [-h | --help] COMMAND [OPTIONS]
Commands:
d=3 relative third level
`, "", command.ErrHelp,
},
{
"d=2 help flag",
buildTestCommand,
[]string{"deep", "d=2", "-h"},
`
Usage: test deep d=2 [-h | --help] COMMAND [OPTIONS]
Commands:
d=3 relative third level
`, "", flag.ErrHelp,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
wout, wlog := new(bytes.Buffer), new(bytes.Buffer)
c := tc.buildTree(wout, wlog)
if err := c.Parse(tc.args); !errors.Is(err, tc.wantErr) {
t.Errorf("Parse: error = %v; wantErr %v", err, tc.wantErr)
}
if got := wout.String(); got != tc.want {
t.Errorf("Parse: %s want %s", got, tc.want)
}
if gotLog := wlog.String(); gotLog != tc.wantLog {
t.Errorf("Parse: log = %s wantLog %s", gotLog, tc.wantLog)
}
})
}
}
var (
errJoinLen = errors.New("not enough arguments to join")
errSuccess = errors.New("success")
)
func buildTestCommand(wout, wlog io.Writer) (c command.Command) {
var (
flagVerbose bool
flagFail bool
flagString string
flagInt int
flagRepeat command.RepeatableFlag
)
logf := newLogFunc(wlog)
c = command.New(wout, logf, "test", func([]string) error {
if flagVerbose {
logf("verbose")
}
if flagFail {
return errSuccess
}
return nil
}).
Flag(&flagVerbose, "v", command.BoolFlag(false), "verbose output").
Flag(&flagFail, "fail", command.BoolFlag(false), "fail early").
Command("error", "return an error", func([]string) error {
return errSuccess
}).
Command("print", "wraps Fprint", func(args []string) error {
a := make([]any, len(args))
for i, v := range args {
a[i] = v
}
_, err := fmt.Fprint(wout, a...)
return err
}).
Flag(&flagString, "string", command.StringFlag("default"), "store value for the \"string\" command").
Command("string", "print string passed by flag", func(args []string) error { _, err := fmt.Fprint(wout, flagString); return err }).
Flag(&flagInt, "int", command.IntFlag(-1), "store value for the \"int\" command").
Command("int", "print int passed by flag", func(args []string) error { _, err := fmt.Fprint(wout, flagInt); return err }).
Flag(nil, "repeat", &flagRepeat, "store value for the \"repeat\" command").
Command("repeat", "print repeated values passed by flag", func(args []string) error { _, err := fmt.Fprint(wout, flagRepeat); return err })
c.New("empty", "empty subcommand")
c.New("hidden", command.UsageInternal)
c.New("join", "wraps strings.Join").
Command("out", "write result to wout", func(args []string) error {
if len(args) == 0 {
return errJoinLen
}
_, err := fmt.Fprint(wout, strings.Join(args[:len(args)-1], args[len(args)-1]))
return err
}).
Command("log", "log result to wlog", func(args []string) error {
if len(args) == 0 {
return errJoinLen
}
logf("%s", strings.Join(args[:len(args)-1], args[len(args)-1]))
return nil
})
c.Command("succeed", "this command succeeds", func([]string) error { return nil })
c.New("deep", "top level of command tree with various levels").
New("d=2", "relative second level").
New("d=3", "relative third level").
New("d=4", "relative fourth level")
return
}
func newLogFunc(w io.Writer) command.LogFunc { return log.New(w, "test: ", 0).Printf }

View File

@ -0,0 +1,54 @@
package command
import (
"flag"
"testing"
)
func TestParseUnreachable(t *testing.T) {
// top level bypasses name matching and recursive calls to Parse
// returns when encountering zero-length args
t.Run("zero-length args", func(t *testing.T) {
defer checkRecover(t, "Parse", "attempted to parse with zero length args")
_ = newNode(panicWriter{}, nil, " ", " ").Parse(nil)
})
// top level must not have siblings
t.Run("toplevel siblings", func(t *testing.T) {
defer checkRecover(t, "Parse", "invalid toplevel state")
n := newNode(panicWriter{}, nil, " ", "")
n.append(newNode(panicWriter{}, nil, " ", " "))
_ = n.Parse(nil)
})
// a node with descendents must not have a direct handler
t.Run("sub handle conflict", func(t *testing.T) {
defer checkRecover(t, "Parse", "invalid subcommand tree state")
n := newNode(panicWriter{}, nil, " ", " ")
n.adopt(newNode(panicWriter{}, nil, " ", " "))
n.f = func([]string) error { panic("unreachable") }
_ = n.Parse([]string{" "})
})
// this would only happen if a node was matched twice
t.Run("parsed flag set", func(t *testing.T) {
defer checkRecover(t, "Parse", "invalid set state")
n := newNode(panicWriter{}, nil, " ", "")
set := flag.NewFlagSet("parsed", flag.ContinueOnError)
set.SetOutput(panicWriter{})
_ = set.Parse(nil)
n.set = set
_ = n.Parse(nil)
})
}
type panicWriter struct{}
func (p panicWriter) Write([]byte) (int, error) { panic("unreachable") }
func checkRecover(t *testing.T, name, wantPanic string) {
if r := recover(); r != wantPanic {
t.Errorf("%s: panic = %v; wantPanic %v",
name, r, wantPanic)
}
}

14
command/wrap.go Normal file
View File

@ -0,0 +1,14 @@
package command
// the top level node wants [Command] returned for its builder methods
type rootNode struct{ *node }
func (r rootNode) Command(name, usage string, f HandlerFunc) Command {
r.node.Command(name, usage, f)
return r
}
func (r rootNode) Flag(p any, name string, value FlagDefiner, usage string) Command {
r.node.Flag(p, name, value, usage)
return r
}

View File

@ -1,82 +0,0 @@
#compdef fortify
_fortify_app() {
__fortify_files
return $?
}
_fortify_run() {
_arguments \
'--id[App ID, leave empty to disable security context app_id]:id' \
'-a[Fortify application ID]: :_numbers' \
'-g[Groups inherited by the app process]: :_groups' \
'-d[Application home directory]: :_files -/' \
'-u[Passwd name within sandbox]: :_users' \
'--wayland[Share Wayland socket]' \
'-X[Share X11 socket and allow connection]' \
'--dbus[Proxy D-Bus connection]' \
'--pulse[Share PulseAudio socket and cookie]' \
'--dbus-config[Path to D-Bus proxy config file]: :_files -g "*.json"' \
'--dbus-system[Path to system D-Bus proxy config file]: :_files -g "*.json"' \
'--mpris[Allow owning MPRIS D-Bus path]' \
'--dbus-log[Force logging in the D-Bus proxy]'
}
_fortify_ps() {
_arguments \
'--short[Print instance id]'
}
_fortify_show() {
_alternative \
'instances:domains:__fortify_instances' \
'files:files:__fortify_files'
}
__fortify_files() {
_files -g "*.(json|ftfy)"
return $?
}
__fortify_instances() {
local -a out
shift -p
out=( ${(f)"$(_call_program commands fortify ps --short 2>&1)"} )
if (( $#out == 0 )); then
_message "No active instances"
else
_describe "active instances" out
fi
return $?
}
(( $+functions[_fortify_commands] )) || _fortify_commands()
{
local -a _fortify_cmds
_fortify_cmds=(
"app:Launch app defined by the specified config file"
"run:Configure and start a permissive default sandbox"
"show:Show the contents of an app configuration"
"ps:List active apps and their state"
"version:Show fortify version"
"license:Show full license text"
"template:Produce a config template"
"help:Show help message"
)
if (( CURRENT == 1 )); then
_describe -t commands 'action' _fortify_cmds || compadd "$@"
else
local curcontext="$curcontext"
cmd="${${_fortify_cmds[(r)$words[1]:*]%%:*}}"
if (( $+functions[_fortify_$cmd] )); then
_fortify_$cmd
else
_message "no more options"
fi
fi
}
_arguments -C \
'-v[Verbose output]' \
'--json[Format output in JSON when applicable]' \
'*::fortify command:_fortify_commands'

View File

@ -5,7 +5,7 @@ import (
"reflect"
"testing"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/hakurei/dbus"
)
func TestParse(t *testing.T) {

View File

@ -5,8 +5,12 @@ import (
"errors"
"io"
"os"
"strings"
)
// ProxyPair is an upstream dbus address and a downstream socket path.
type ProxyPair [2]string
type Config struct {
// See set 'see' policy for NAME (--see=NAME)
See []string `json:"see"`
@ -24,7 +28,62 @@ type Config struct {
Filter bool `json:"filter"`
}
func (c *Config) Args(bus [2]string) (args []string) {
func (c *Config) interfaces(yield func(string) bool) {
for _, iface := range c.See {
if !yield(iface) {
return
}
}
for _, iface := range c.Talk {
if !yield(iface) {
return
}
}
for _, iface := range c.Own {
if !yield(iface) {
return
}
}
for iface := range c.Call {
if !yield(iface) {
return
}
}
for iface := range c.Broadcast {
if !yield(iface) {
return
}
}
}
func (c *Config) checkInterfaces(segment string) error {
for iface := range c.interfaces {
/*
xdg-dbus-proxy fails without output when this condition is not met:
char *dot = strrchr (filter->interface, '.');
if (dot != NULL)
{
*dot = 0;
if (strcmp (dot + 1, "*") != 0)
filter->member = g_strdup (dot + 1);
}
trim ".*" since they are removed before searching for '.':
if (g_str_has_suffix (name, ".*"))
{
name[strlen (name) - 2] = 0;
wildcard = TRUE;
}
*/
if strings.IndexByte(strings.TrimSuffix(iface, ".*"), '.') == -1 {
return &BadInterfaceError{iface, segment}
}
}
return nil
}
func (c *Config) Args(bus ProxyPair) (args []string) {
argc := 2 + len(c.See) + len(c.Talk) + len(c.Own) + len(c.Call) + len(c.Broadcast)
if c.Log {
argc++
@ -60,9 +119,7 @@ func (c *Config) Args(bus [2]string) (args []string) {
return
}
func (c *Config) Load(r io.Reader) error {
return json.NewDecoder(r).Decode(&c)
}
func (c *Config) Load(r io.Reader) error { return json.NewDecoder(r).Decode(&c) }
// NewConfigFromFile opens the target config file at path and parses its contents into *Config.
func NewConfigFromFile(path string) (*Config, error) {

View File

@ -9,7 +9,7 @@ import (
"strings"
"testing"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/hakurei/dbus"
)
func TestConfig_Args(t *testing.T) {

View File

@ -1,74 +1,40 @@
package dbus_test
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"strings"
"syscall"
"testing"
"time"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/helper"
"git.gensokyo.uk/security/hakurei/dbus"
"git.gensokyo.uk/security/hakurei/helper"
"git.gensokyo.uk/security/hakurei/internal"
"git.gensokyo.uk/security/hakurei/internal/hlog"
"git.gensokyo.uk/security/hakurei/sandbox"
)
func TestNew(t *testing.T) {
for _, tc := range [][2][2]string{
{
{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/1ca5d183ef4c99e74c3e544715f32702/bus"},
{"unix:path=/run/dbus/system_bus_socket", "/tmp/fortify.1971/1ca5d183ef4c99e74c3e544715f32702/system_bus_socket"},
},
{
{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/881ac3796ff3f3bf0a773824383187a0/bus"},
{"unix:path=/run/dbus/system_bus_socket", "/tmp/fortify.1971/881ac3796ff3f3bf0a773824383187a0/system_bus_socket"},
},
{
{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/3d1a5084520ef79c0c6a49a675bac701/bus"},
{"unix:path=/run/dbus/system_bus_socket", "/tmp/fortify.1971/3d1a5084520ef79c0c6a49a675bac701/system_bus_socket"},
},
{
{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/2a1639bab712799788ea0ff7aa280c35/bus"},
{"unix:path=/run/dbus/system_bus_socket", "/tmp/fortify.1971/2a1639bab712799788ea0ff7aa280c35/system_bus_socket"},
},
} {
t.Run("create instance for "+tc[0][0]+" and "+tc[1][0], func(t *testing.T) {
if got := dbus.New(tc[0], tc[1]); !got.CompareTestNew(tc[0], tc[1]) {
t.Errorf("New(%q, %q) = %v",
tc[0], tc[1],
got)
}
})
}
}
func TestProxy_Seal(t *testing.T) {
t.Run("double seal panic", func(t *testing.T) {
defer func() {
want := "dbus proxy sealed twice"
if r := recover(); r != want {
t.Errorf("Seal: panic = %q, want %q",
r, want)
}
}()
p := dbus.New([2]string{}, [2]string{})
_ = p.Seal(dbus.NewConfig("", true, false), nil)
_ = p.Seal(dbus.NewConfig("", true, false), nil)
})
ep := dbus.New([2]string{}, [2]string{})
if err := ep.Seal(nil, nil); !errors.Is(err, dbus.ErrConfig) {
t.Errorf("Seal(nil, nil) error = %v, want %v",
err, dbus.ErrConfig)
func TestFinalise(t *testing.T) {
if _, err := dbus.Finalise(dbus.ProxyPair{}, dbus.ProxyPair{}, nil, nil); !errors.Is(err, syscall.EBADE) {
t.Errorf("Finalise: error = %v, want %v",
err, syscall.EBADE)
}
for id, tc := range testCasePairs() {
t.Run("create seal for "+id, func(t *testing.T) {
p := dbus.New(tc[0].bus, tc[1].bus)
if err := p.Seal(tc[0].c, tc[1].c); (errors.Is(err, helper.ErrContainsNull)) != tc[0].wantErr {
t.Errorf("Seal(%p, %p) error = %v, wantErr %v",
tc[0].c, tc[1].c,
t.Run("create final for "+id, func(t *testing.T) {
var wt io.WriterTo
if v, err := dbus.Finalise(tc[0].bus, tc[1].bus, tc[0].c, tc[1].c); (errors.Is(err, syscall.EINVAL)) != tc[0].wantErr {
t.Errorf("Finalise: error = %v, wantErr %v",
err, tc[0].wantErr)
return
} else {
wt = v
}
// rest of the tests happen for sealed instances
@ -81,136 +47,167 @@ func TestProxy_Seal(t *testing.T) {
args := append(tc[0].want, tc[1].want...)
for _, arg := range args {
want.WriteString(arg)
want.WriteByte('\x00')
want.WriteByte(0)
}
wt := p.AccessTestProxySeal()
got := new(strings.Builder)
if _, err := wt.WriteTo(got); err != nil {
t.Errorf("p.seal.WriteTo(): %v", err)
t.Errorf("WriteTo: error = %v", err)
}
if want.String() != got.String() {
t.Errorf("Seal(%p, %p) seal = %v, want %v",
tc[0].c, tc[1].c,
t.Errorf("Seal: %q, want %q",
got.String(), want.String())
}
})
}
}
func TestProxy_Start_Wait_Close_String(t *testing.T) {
t.Run("sandboxed", func(t *testing.T) {
testProxyStartWaitCloseString(t, true)
})
t.Run("direct", func(t *testing.T) {
testProxyStartWaitCloseString(t, false)
func TestProxyStartWaitCloseString(t *testing.T) {
oldWaitDelay := helper.WaitDelay
helper.WaitDelay = 16 * time.Second
t.Cleanup(func() { helper.WaitDelay = oldWaitDelay })
t.Run("sandbox", func(t *testing.T) {
proxyName := dbus.ProxyName
dbus.ProxyName = os.Args[0]
t.Cleanup(func() { dbus.ProxyName = proxyName })
testProxyFinaliseStartWaitCloseString(t, true)
})
t.Run("direct", func(t *testing.T) { testProxyFinaliseStartWaitCloseString(t, false) })
}
func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) {
var p *dbus.Proxy
t.Run("string for nil proxy", func(t *testing.T) {
want := "(invalid dbus proxy)"
if got := p.String(); got != want {
t.Errorf("String: %q, want %q",
got, want)
}
})
t.Run("invalid start", func(t *testing.T) {
if !useSandbox {
p = dbus.NewDirect(t.Context(), nil, nil)
} else {
p = dbus.New(t.Context(), nil, nil)
}
if err := p.Start(); !errors.Is(err, syscall.ENOTRECOVERABLE) {
t.Errorf("Start: error = %q, wantErr %q",
err, syscall.ENOTRECOVERABLE)
return
}
})
for id, tc := range testCasePairs() {
// this test does not test errors
if tc[0].wantErr {
continue
}
t.Run("string for nil proxy", func(t *testing.T) {
var p *dbus.Proxy
want := "(invalid dbus proxy)"
if got := p.String(); got != want {
t.Errorf("String() = %v, want %v",
got, want)
t.Run("proxy for "+id, func(t *testing.T) {
var final *dbus.Final
t.Run("finalise", func(t *testing.T) {
if v, err := dbus.Finalise(tc[0].bus, tc[1].bus, tc[0].c, tc[1].c); err != nil {
t.Errorf("Finalise: error = %v, wantErr %v",
err, tc[0].wantErr)
return
} else {
final = v
}
})
t.Run("proxy for "+id, func(t *testing.T) {
helper.InternalReplaceExecCommand(t)
overridePath(t)
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
if !useSandbox {
p = dbus.NewDirect(ctx, final, nil)
} else {
p = dbus.New(ctx, final, nil)
}
p := dbus.New(tc[0].bus, tc[1].bus)
p.CommandContext = func(ctx context.Context) (cmd *exec.Cmd) {
return exec.CommandContext(ctx, os.Args[0], "-test.v",
"-test.run=TestHelperInit", "--", "init")
}
p.CmdF = func(v any) {
if useSandbox {
container := v.(*sandbox.Container)
if container.Args[0] != dbus.ProxyName {
panic(fmt.Sprintf("unexpected argv0 %q", os.Args[0]))
}
container.Args = append([]string{os.Args[0], "-test.run=TestHelperStub", "--"}, container.Args[1:]...)
} else {
cmd := v.(*exec.Cmd)
if cmd.Args[0] != dbus.ProxyName {
panic(fmt.Sprintf("unexpected argv0 %q", os.Args[0]))
}
cmd.Err = nil
cmd.Path = os.Args[0]
cmd.Args = append([]string{os.Args[0], "-test.run=TestHelperStub", "--"}, cmd.Args[1:]...)
}
}
p.FilterF = func(v []byte) []byte { return bytes.SplitN(v, []byte("TestHelperInit\n"), 2)[1] }
output := new(strings.Builder)
t.Run("unsealed behaviour of "+id, func(t *testing.T) {
t.Run("unsealed string of "+id, func(t *testing.T) {
want := "(unsealed dbus proxy)"
if got := p.String(); got != want {
t.Errorf("String() = %v, want %v",
got, want)
return
}
})
t.Run("unsealed start of "+id, func(t *testing.T) {
want := "proxy not sealed"
if err := p.Start(context.Background(), nil, sandbox); err == nil || err.Error() != want {
t.Errorf("Start() error = %v, wantErr %q",
err, errors.New(want))
return
}
})
t.Run("unsealed wait of "+id, func(t *testing.T) {
t.Run("invalid wait", func(t *testing.T) {
wantErr := "dbus: not started"
if err := p.Wait(); err == nil || err.Error() != wantErr {
t.Errorf("Wait() error = %v, wantErr %v",
t.Errorf("Wait: error = %v, wantErr %v",
err, wantErr)
}
})
})
t.Run("seal with "+id, func(t *testing.T) {
if err := p.Seal(tc[0].c, tc[1].c); err != nil {
t.Errorf("Seal(%p, %p) error = %v, wantErr %v",
tc[0].c, tc[1].c,
err, tc[0].wantErr)
return
}
})
t.Run("sealed behaviour of "+id, func(t *testing.T) {
want := strings.Join(append(tc[0].want, tc[1].want...), " ")
t.Run("string", func(t *testing.T) {
want := "(unused dbus proxy)"
if got := p.String(); got != want {
t.Errorf("String() = %v, want %v",
t.Errorf("String: %q, want %q",
got, want)
return
}
})
t.Run("sealed start of "+id, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := p.Start(ctx, output, sandbox); err != nil {
t.Fatalf("Start(nil, nil) error = %v",
t.Run("start", func(t *testing.T) {
if err := p.Start(); err != nil {
t.Fatalf("Start: error = %v",
err)
}
t.Run("started string of "+id, func(t *testing.T) {
wantSubstr := dbus.ProxyName + " --args="
t.Run("string", func(t *testing.T) {
wantSubstr := fmt.Sprintf("%s -test.run=TestHelperStub -- --args=3 --fd=4", os.Args[0])
if useSandbox {
wantSubstr = fmt.Sprintf(`argv: ["%s" "-test.run=TestHelperStub" "--" "--args=3" "--fd=4"], flags: 0x0, seccomp: 0x3e`, os.Args[0])
}
if got := p.String(); !strings.Contains(got, wantSubstr) {
t.Errorf("String() = %v, want %v",
p.String(), wantSubstr)
t.Errorf("String: %q, want %q",
got, wantSubstr)
return
}
})
t.Run("started wait of "+id, func(t *testing.T) {
p.Close()
t.Run("wait", func(t *testing.T) {
done := make(chan struct{})
go func() {
if err := p.Wait(); err != nil {
t.Errorf("Wait() error = %v\noutput: %s",
t.Errorf("Wait: error = %v\noutput: %s",
err, output.String())
}
})
close(done)
}()
p.Close()
<-done
})
})
})
}
}
func overridePath(t *testing.T) {
proxyName := dbus.ProxyName
dbus.ProxyName = "/nonexistent-xdg-dbus-proxy"
t.Cleanup(func() {
dbus.ProxyName = proxyName
})
func TestHelperInit(t *testing.T) {
if len(os.Args) != 5 || os.Args[4] != "init" {
return
}
sandbox.SetOutput(hlog.Output{})
sandbox.Init(hlog.Prepare, internal.InstallFmsg)
}

View File

@ -1,13 +1,13 @@
package dbus
import "io"
import (
"context"
"io"
)
// CompareTestNew provides TestNew with comparison access to unexported Proxy fields.
func (p *Proxy) CompareTestNew(session, system [2]string) bool {
return session == p.session && system == p.system
}
// AccessTestProxySeal provides TestProxy_Seal with access to unexported Proxy seal field.
func (p *Proxy) AccessTestProxySeal() io.WriterTo {
return p.seal
// NewDirect returns a new instance of [Proxy] with its sandbox disabled.
func NewDirect(ctx context.Context, final *Final, output io.Writer) *Proxy {
p := New(ctx, final, output)
p.useSandbox = false
return p
}

188
dbus/proc.go Normal file
View File

@ -0,0 +1,188 @@
package dbus
import (
"context"
"errors"
"os"
"os/exec"
"path"
"path/filepath"
"slices"
"strconv"
"syscall"
"git.gensokyo.uk/security/hakurei/helper"
"git.gensokyo.uk/security/hakurei/ldd"
"git.gensokyo.uk/security/hakurei/sandbox"
"git.gensokyo.uk/security/hakurei/sandbox/seccomp"
)
// Start starts and configures a D-Bus proxy process.
func (p *Proxy) Start() error {
if p.final == nil || p.final.WriterTo == nil {
return syscall.ENOTRECOVERABLE
}
p.mu.Lock()
defer p.mu.Unlock()
p.pmu.Lock()
defer p.pmu.Unlock()
if p.cancel != nil || p.cause != nil {
return errors.New("dbus: already started")
}
ctx, cancel := context.WithCancelCause(p.ctx)
if !p.useSandbox {
p.helper = helper.NewDirect(ctx, p.name, p.final, true, argF, func(cmd *exec.Cmd) {
if p.CmdF != nil {
p.CmdF(cmd)
}
if p.output != nil {
cmd.Stdout, cmd.Stderr = p.output, p.output
}
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
cmd.Env = make([]string, 0)
}, nil)
} else {
toolPath := p.name
if filepath.Base(p.name) == p.name {
if s, err := exec.LookPath(p.name); err != nil {
return err
} else {
toolPath = s
}
}
var libPaths []string
if entries, err := ldd.ExecFilter(ctx, p.CommandContext, p.FilterF, toolPath); err != nil {
return err
} else {
libPaths = ldd.Path(entries)
}
p.helper = helper.New(
ctx, toolPath,
p.final, true,
argF, func(container *sandbox.Container) {
container.Seccomp |= seccomp.FilterMultiarch
container.Hostname = "hakurei-dbus"
container.CommandContext = p.CommandContext
if p.output != nil {
container.Stdout, container.Stderr = p.output, p.output
}
if p.CmdF != nil {
p.CmdF(container)
}
// these lib paths are unpredictable, so mount them first so they cannot cover anything
for _, name := range libPaths {
container.Bind(name, name, 0)
}
// upstream bus directories
upstreamPaths := make([]string, 0, 2)
for _, addr := range [][]AddrEntry{p.final.SessionUpstream, p.final.SystemUpstream} {
for _, ent := range addr {
if ent.Method != "unix" {
continue
}
for _, pair := range ent.Values {
if pair[0] != "path" || !path.IsAbs(pair[1]) {
continue
}
upstreamPaths = append(upstreamPaths, path.Dir(pair[1]))
}
}
}
slices.Sort(upstreamPaths)
upstreamPaths = slices.Compact(upstreamPaths)
for _, name := range upstreamPaths {
container.Bind(name, name, 0)
}
// parent directories of bind paths
sockDirPaths := make([]string, 0, 2)
if d := path.Dir(p.final.Session[1]); path.IsAbs(d) {
sockDirPaths = append(sockDirPaths, d)
}
if d := path.Dir(p.final.System[1]); path.IsAbs(d) {
sockDirPaths = append(sockDirPaths, d)
}
slices.Sort(sockDirPaths)
sockDirPaths = slices.Compact(sockDirPaths)
for _, name := range sockDirPaths {
container.Bind(name, name, sandbox.BindWritable)
}
// xdg-dbus-proxy bin path
binPath := path.Dir(toolPath)
container.Bind(binPath, binPath, 0)
}, nil)
}
if err := p.helper.Start(); err != nil {
cancel(err)
p.helper = nil
return err
}
p.cancel, p.cause = cancel, func() error { return context.Cause(ctx) }
return nil
}
var proxyClosed = errors.New("proxy closed")
// Wait blocks until xdg-dbus-proxy exits and releases resources.
func (p *Proxy) Wait() error {
p.mu.RLock()
defer p.mu.RUnlock()
p.pmu.RLock()
if p.helper == nil || p.cancel == nil || p.cause == nil {
p.pmu.RUnlock()
return errors.New("dbus: not started")
}
errs := make([]error, 3)
errs[0] = p.helper.Wait()
if errors.Is(errs[0], context.Canceled) &&
errors.Is(p.cause(), proxyClosed) {
errs[0] = nil
}
p.pmu.RUnlock()
// ensure socket removal so ephemeral directory is empty at revert
if err := os.Remove(p.final.Session[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
errs[1] = err
}
if p.final.System[1] != "" {
if err := os.Remove(p.final.System[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
errs[2] = err
}
}
return errors.Join(errs...)
}
// Close cancels the context passed to the helper instance attached to xdg-dbus-proxy.
func (p *Proxy) Close() {
p.pmu.Lock()
defer p.pmu.Unlock()
if p.cancel == nil {
panic("dbus: not started")
}
p.cancel(proxyClosed)
}
func argF(argsFd, statFd int) []string {
if statFd == -1 {
return []string{"--args=" + strconv.Itoa(argsFd)}
} else {
return []string{"--args=" + strconv.Itoa(argsFd), "--fd=" + strconv.Itoa(statFd)}
}
}

View File

@ -2,94 +2,116 @@ package dbus
import (
"context"
"errors"
"fmt"
"io"
"os/exec"
"sync"
"syscall"
"git.gensokyo.uk/security/fortify/helper"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/hakurei/helper"
)
// ProxyName is the file name or path to the proxy program.
// Overriding ProxyName will only affect Proxy instance created after the change.
var ProxyName = "xdg-dbus-proxy"
// Proxy holds references to a xdg-dbus-proxy process, and should never be copied.
// Once sealed, configuration changes will no longer be possible and attempting to do so will result in a panic.
type Proxy struct {
helper helper.Helper
bwrap *bwrap.Config
ctx context.Context
cancel context.CancelCauseFunc
name string
session [2]string
system [2]string
sysP bool
seal io.WriterTo
lock sync.RWMutex
type BadInterfaceError struct {
Interface string
Segment string
}
func (p *Proxy) Session() [2]string { return p.session }
func (p *Proxy) System() [2]string { return p.system }
func (p *Proxy) Sealed() bool { p.lock.RLock(); defer p.lock.RUnlock(); return p.seal != nil }
func (e *BadInterfaceError) Error() string {
return fmt.Sprintf("bad interface string %q in %s bus configuration", e.Interface, e.Segment)
}
var (
ErrConfig = errors.New("no configuration to seal")
)
// Proxy holds the state of a xdg-dbus-proxy process, and should never be copied.
type Proxy struct {
helper helper.Helper
ctx context.Context
cancel context.CancelCauseFunc
cause func() error
final *Final
output io.Writer
useSandbox bool
name string
CmdF func(any)
CommandContext func(ctx context.Context) (cmd *exec.Cmd)
FilterF func([]byte) []byte
mu, pmu sync.RWMutex
}
func (p *Proxy) String() string {
if p == nil {
return "(invalid dbus proxy)"
}
p.lock.RLock()
defer p.lock.RUnlock()
p.mu.RLock()
defer p.mu.RUnlock()
if p.helper != nil {
return p.helper.String()
}
if p.seal != nil {
return p.seal.(fmt.Stringer).String()
}
return "(unsealed dbus proxy)"
return "(unused dbus proxy)"
}
// Seal seals the Proxy instance.
func (p *Proxy) Seal(session, system *Config) error {
p.lock.Lock()
defer p.lock.Unlock()
if p.seal != nil {
panic("dbus proxy sealed twice")
}
// Final describes the outcome of a proxy configuration.
type Final struct {
Session, System ProxyPair
// parsed upstream address
SessionUpstream, SystemUpstream []AddrEntry
io.WriterTo
}
// Finalise creates a checked argument writer for [Proxy].
func Finalise(sessionBus, systemBus ProxyPair, session, system *Config) (final *Final, err error) {
if session == nil && system == nil {
return ErrConfig
return nil, syscall.EBADE
}
var args []string
if session != nil {
args = append(args, session.Args(p.session)...)
if err = session.checkInterfaces("session"); err != nil {
return
}
args = append(args, session.Args(sessionBus)...)
}
if system != nil {
args = append(args, system.Args(p.system)...)
p.sysP = true
if err = system.checkInterfaces("system"); err != nil {
return
}
if seal, err := helper.NewCheckedArgs(args); err != nil {
return err
} else {
p.seal = seal
args = append(args, system.Args(systemBus)...)
}
return nil
final = &Final{Session: sessionBus, System: systemBus}
final.WriterTo, err = helper.NewCheckedArgs(args)
if err != nil {
return
}
if session != nil {
final.SessionUpstream, err = Parse([]byte(final.Session[0]))
if err != nil {
return
}
}
if system != nil {
final.SystemUpstream, err = Parse([]byte(final.System[0]))
if err != nil {
return
}
}
return
}
// New returns a reference to a new unsealed Proxy.
func New(session, system [2]string) *Proxy {
return &Proxy{name: ProxyName, session: session, system: system}
// New returns a new instance of [Proxy].
func New(ctx context.Context, final *Final, output io.Writer) *Proxy {
return &Proxy{name: ProxyName, ctx: ctx, final: final, output: output, useSandbox: true}
}

View File

@ -1,175 +0,0 @@
package dbus
import (
"context"
"errors"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"strconv"
"strings"
"git.gensokyo.uk/security/fortify/helper"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/ldd"
)
// Start launches the D-Bus proxy.
func (p *Proxy) Start(ctx context.Context, output io.Writer, sandbox bool) error {
p.lock.Lock()
defer p.lock.Unlock()
if p.seal == nil {
return errors.New("proxy not sealed")
}
var (
h helper.Helper
argF = func(argsFD, statFD int) []string {
if statFD == -1 {
return []string{"--args=" + strconv.Itoa(argsFD)}
} else {
return []string{"--args=" + strconv.Itoa(argsFD), "--fd=" + strconv.Itoa(statFD)}
}
}
)
if !sandbox {
h = helper.New(p.seal, p.name, argF)
// xdg-dbus-proxy does not need to inherit the environment
h.SetEnv(make([]string, 0))
} else {
// look up absolute path if name is just a file name
toolPath := p.name
if filepath.Base(p.name) == p.name {
if s, err := exec.LookPath(p.name); err != nil {
return err
} else {
toolPath = s
}
}
// resolve libraries by parsing ldd output
var proxyDeps []*ldd.Entry
if toolPath != "/nonexistent-xdg-dbus-proxy" {
if l, err := ldd.Exec(ctx, toolPath); err != nil {
return err
} else {
proxyDeps = l
}
}
bc := &bwrap.Config{
Unshare: nil,
Hostname: "fortify-dbus",
Chdir: "/",
Syscall: &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
Clearenv: true,
NewSession: true,
DieWithParent: true,
}
// resolve proxy socket directories
bindTarget := make(map[string]struct{}, 2)
for _, ps := range []string{p.session[1], p.system[1]} {
if pd := path.Dir(ps); len(pd) > 0 {
if pd[0] == '/' {
bindTarget[pd] = struct{}{}
}
}
}
for k := range bindTarget {
bc.Bind(k, k, false, true)
}
roBindTarget := make(map[string]struct{}, 2+1+len(proxyDeps))
// xdb-dbus-proxy bin and dependencies
roBindTarget[path.Dir(toolPath)] = struct{}{}
for _, ent := range proxyDeps {
if path.IsAbs(ent.Path) {
roBindTarget[path.Dir(ent.Path)] = struct{}{}
}
if path.IsAbs(ent.Name) {
roBindTarget[path.Dir(ent.Name)] = struct{}{}
}
}
// resolve upstream bus directories
for _, as := range []string{p.session[0], p.system[0]} {
if len(as) > 0 && strings.HasPrefix(as, "unix:path=/") {
// leave / intact
roBindTarget[path.Dir(as[10:])] = struct{}{}
}
}
for k := range roBindTarget {
bc.Bind(k, k)
}
h = helper.MustNewBwrap(bc, toolPath, p.seal, argF, nil, nil)
p.bwrap = bc
}
if output != nil {
h.Stdout(output).Stderr(output)
}
c, cancel := context.WithCancelCause(ctx)
if err := h.Start(c, true); err != nil {
cancel(err)
return err
}
p.helper = h
p.ctx = c
p.cancel = cancel
return nil
}
var proxyClosed = errors.New("proxy closed")
// Wait blocks until xdg-dbus-proxy exits and releases resources.
func (p *Proxy) Wait() error {
p.lock.RLock()
defer p.lock.RUnlock()
if p.helper == nil {
return errors.New("dbus: not started")
}
errs := make([]error, 3)
errs[0] = p.helper.Wait()
if p.cancel == nil &&
errors.Is(errs[0], context.Canceled) &&
errors.Is(context.Cause(p.ctx), proxyClosed) {
errs[0] = nil
}
// ensure socket removal so ephemeral directory is empty at revert
if err := os.Remove(p.session[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
errs[1] = err
}
if p.sysP {
if err := os.Remove(p.system[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
errs[2] = err
}
}
return errors.Join(errs...)
}
// Close cancels the context passed to the helper instance attached to xdg-dbus-proxy.
func (p *Proxy) Close() {
p.lock.Lock()
defer p.lock.Unlock()
if p.cancel == nil {
panic("dbus: not started")
}
p.cancel(proxyClosed)
p.cancel = nil
}

View File

@ -3,7 +3,13 @@ package dbus_test
import (
"sync"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/hakurei/dbus"
)
const (
sampleHostPath = "/tmp/bus"
sampleHostAddr = "unix:path=" + sampleHostPath
sampleBindPath = "/tmp/proxied_bus"
)
var samples = []dbusTestCase{
@ -19,10 +25,10 @@ var samples = []dbusTestCase{
Log: false,
Filter: true,
}, false, false,
[2]string{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/bus"},
[2]string{sampleHostAddr, sampleBindPath},
[]string{
"unix:path=/run/user/1971/bus",
"/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/bus",
sampleHostAddr,
sampleBindPath,
"--filter",
"--talk=org.freedesktop.Notifications",
"--talk=org.freedesktop.FileManager1",
@ -48,9 +54,10 @@ var samples = []dbusTestCase{
Log: false,
Filter: true,
}, false, false,
[2]string{"unix:path=/run/dbus/system_bus_socket", "/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/system_bus_socket"},
[]string{"unix:path=/run/dbus/system_bus_socket",
"/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/system_bus_socket",
[2]string{sampleHostAddr, sampleBindPath},
[]string{
sampleHostAddr,
sampleBindPath,
"--filter",
"--talk=org.bluez",
"--talk=org.freedesktop.Avahi",
@ -68,10 +75,10 @@ var samples = []dbusTestCase{
Log: false,
Filter: true,
}, false, false,
[2]string{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/34c24f16a0d791d28835ededaf446033/bus"},
[2]string{sampleHostAddr, sampleBindPath},
[]string{
"unix:path=/run/user/1971/bus",
"/tmp/fortify.1971/34c24f16a0d791d28835ededaf446033/bus",
sampleHostAddr,
sampleBindPath,
"--filter",
"--talk=org.freedesktop.Notifications",
"--talk=org.kde.StatusNotifierWatcher",
@ -91,10 +98,10 @@ var samples = []dbusTestCase{
Log: true,
Filter: true,
}, false, false,
[2]string{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/5da7845287a936efbc2fa75d7d81e501/bus"},
[2]string{sampleHostAddr, sampleBindPath},
[]string{
"unix:path=/run/user/1971/bus",
"/tmp/fortify.1971/5da7845287a936efbc2fa75d7d81e501/bus",
sampleHostAddr,
sampleBindPath,
"--filter",
"--see=uk.gensokyo.CrashTestDummy1",
"--talk=org.freedesktop.Notifications",
@ -114,10 +121,10 @@ var samples = []dbusTestCase{
Log: true,
Filter: true,
}, false, true,
[2]string{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/5da7845287a936efbc2fa75d7d81e501/bus"},
[2]string{sampleHostAddr, sampleBindPath},
[]string{
"unix:path=/run/user/1971/bus",
"/tmp/fortify.1971/5da7845287a936efbc2fa75d7d81e501/bus",
sampleHostAddr,
sampleBindPath,
"--filter",
"--see=uk.gensokyo.CrashTestDummy",
"--talk=org.freedesktop.Notifications",

View File

@ -3,9 +3,7 @@ package dbus_test
import (
"testing"
"git.gensokyo.uk/security/fortify/helper"
"git.gensokyo.uk/security/hakurei/helper"
)
func TestHelperChildStub(t *testing.T) {
helper.InternalChildStub()
}
func TestHelperStub(t *testing.T) { helper.InternalHelperStub() }

82
dist/comp/_hakurei vendored Normal file
View File

@ -0,0 +1,82 @@
#compdef hakurei
_hakurei_app() {
__hakurei_files
return $?
}
_hakurei_run() {
_arguments \
'--id[Reverse-DNS style Application identifier, leave empty to inherit instance identifier]:id' \
'-a[Application identity]: :_numbers' \
'-g[Groups inherited by all container processes]: :_groups' \
'-d[Container home directory]: :_files -/' \
'-u[Passwd user name within sandbox]: :_users' \
'--wayland[Enable connection to Wayland via security-context-v1]' \
'-X[Enable direct connection to X11]' \
'--dbus[Enable proxied connection to D-Bus]' \
'--pulse[Enable direct connection to PulseAudio]' \
'--dbus-config[Path to session bus proxy config file]: :_files -g "*.json"' \
'--dbus-system[Path to system bus proxy config file]: :_files -g "*.json"' \
'--mpris[Allow owning MPRIS D-Bus path]' \
'--dbus-log[Force buffered logging in the D-Bus proxy]'
}
_hakurei_ps() {
_arguments \
'--short[List instances only]'
}
_hakurei_show() {
_alternative \
'instances:domains:__hakurei_instances' \
'files:files:__hakurei_files'
}
__hakurei_files() {
_files -g "*.(json|hakurei)"
return $?
}
__hakurei_instances() {
local -a out
shift -p
out=( ${(f)"$(_call_program commands hakurei ps --short 2>&1)"} )
if (( $#out == 0 )); then
_message "No active instances"
else
_describe "active instances" out
fi
return $?
}
(( $+functions[_hakurei_commands] )) || _hakurei_commands()
{
local -a _hakurei_cmds
_hakurei_cmds=(
"app:Load app from configuration file"
"run:Configure and start a permissive default sandbox"
"show:Show live or local app configuration"
"ps:List active instances"
"version:Display version information"
"license:Show full license text"
"template:Produce a config template"
"help:Show help message"
)
if (( CURRENT == 1 )); then
_describe -t commands 'action' _hakurei_cmds || compadd "$@"
else
local curcontext="$curcontext"
cmd="${${_hakurei_cmds[(r)$words[1]:*]%%:*}}"
if (( $+functions[_hakurei_$cmd] )); then
_hakurei_$cmd
else
_message "no more options"
fi
fi
}
_arguments -C \
'-v[Increase log verbosity]' \
'--json[Serialise output in JSON when applicable]' \
'*::hakurei command:_hakurei_commands'

12
dist/install.sh vendored
View File

@ -1,12 +1,12 @@
#!/bin/sh
cd "$(dirname -- "$0")" || exit 1
install -vDm0755 "bin/fortify" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fortify"
install -vDm0755 "bin/fpkg" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fpkg"
install -vDm0755 "bin/hakurei" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hakurei"
install -vDm0755 "bin/planterette" "${HAKUREI_INSTALL_PREFIX}/usr/bin/planterette"
install -vDm6511 "bin/fsu" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fsu"
if [ ! -f "${FORTIFY_INSTALL_PREFIX}/etc/fsurc" ]; then
install -vDm0400 "fsurc.default" "${FORTIFY_INSTALL_PREFIX}/etc/fsurc"
install -vDm6511 "bin/hsu" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hsu"
if [ ! -f "${HAKUREI_INSTALL_PREFIX}/etc/hsurc" ]; then
install -vDm0400 "hsurc.default" "${HAKUREI_INSTALL_PREFIX}/etc/hsurc"
fi
install -vDm0644 "comp/_fortify" "${FORTIFY_INSTALL_PREFIX}/usr/share/zsh/site-functions/_fortify"
install -vDm0644 "comp/_hakurei" "${HAKUREI_INSTALL_PREFIX}/usr/share/zsh/site-functions/_hakurei"

15
dist/release.sh vendored
View File

@ -1,18 +1,19 @@
#!/bin/sh -e
cd "$(dirname -- "$0")/.."
VERSION="${FORTIFY_VERSION:-untagged}"
pname="fortify-${VERSION}"
VERSION="${HAKUREI_VERSION:-untagged}"
pname="hakurei-${VERSION}"
out="dist/${pname}"
mkdir -p "${out}"
cp -v "README.md" "dist/fsurc.default" "dist/install.sh" "${out}"
cp -rv "comp" "${out}"
cp -v "README.md" "dist/hsurc.default" "dist/install.sh" "${out}"
cp -rv "dist/comp" "${out}"
go generate ./...
go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w -buildid= -extldflags '-static'
-X git.gensokyo.uk/security/fortify/internal.Version=${VERSION}
-X git.gensokyo.uk/security/fortify/internal.Fsu=/usr/bin/fsu
-X main.Fmain=/usr/bin/fortify" ./...
-X git.gensokyo.uk/security/hakurei/internal.version=${VERSION}
-X git.gensokyo.uk/security/hakurei/internal.hakurei=/usr/bin/hakurei
-X git.gensokyo.uk/security/hakurei/internal.hsu=/usr/bin/hsu
-X main.hmain=/usr/bin/hakurei" ./...
rm -f "./${out}.tar.gz" && tar -C dist -czf "${out}.tar.gz" "${pname}"
rm -rf "./${out}"

View File

@ -1,46 +0,0 @@
package main
import (
"errors"
"log"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
func logWaitError(err error) {
var e *fmsg.BaseError
if !fmsg.AsBaseError(err, &e) {
log.Println("wait failed:", err)
} else {
// Wait only returns either *app.ProcessError or *app.StateStoreError wrapped in a *app.BaseError
var se *app.StateStoreError
if !errors.As(err, &se) {
// does not need special handling
log.Print(e.Message())
} else {
// inner error are either unwrapped store errors
// or joined errors returned by *appSealTx revert
// wrapped in *app.BaseError
var ej app.RevertCompoundError
if !errors.As(se.InnerErr, &ej) {
// does not require special handling
log.Print(e.Message())
} else {
errs := ej.Unwrap()
// every error here is wrapped in *app.BaseError
for _, ei := range errs {
var eb *fmsg.BaseError
if !errors.As(ei, &eb) {
// unreachable
log.Println("invalid error type returned by revert:", ei)
} else {
// print inner *app.BaseError message
log.Print(eb.Message())
}
}
}
}
}
}

16
flake.lock generated
View File

@ -7,32 +7,32 @@
]
},
"locked": {
"lastModified": 1736373539,
"narHash": "sha256-dinzAqCjenWDxuy+MqUQq0I4zUSfaCvN9rzuCmgMZJY=",
"lastModified": 1748665073,
"narHash": "sha256-RMhjnPKWtCoIIHiuR9QKD7xfsKb3agxzMfJY8V9MOew=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "bd65bc3cde04c16755955630b344bc9e35272c56",
"rev": "282e1e029cb6ab4811114fc85110613d72771dea",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "release-24.11",
"ref": "release-25.05",
"repo": "home-manager",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1739333913,
"narHash": "sha256-JXt5FtySR+yBm5ny8zG/hX1IybF/7R66jZfXxXSb6wY=",
"lastModified": 1749024892,
"narHash": "sha256-OGcDEz60TXQC+gVz5sdtgGJdKVYr6rwdzQKuZAJQpCA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "7d83f668aee9e41d574c398a9bb569047e8a3f5d",
"rev": "8f1b52b04f2cb6e5ead50bd28d76528a2f0380ef",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.11-small",
"ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}

161
flake.nix
View File

@ -1,11 +1,11 @@
{
description = "fortify sandbox tool and nixos module";
description = "hakurei container tool and nixos module";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11-small";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
home-manager = {
url = "github:nix-community/home-manager/release-24.11";
url = "github:nix-community/home-manager/release-25.05";
inputs.nixpkgs.follows = "nixpkgs";
};
};
@ -27,12 +27,12 @@
nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
in
{
nixosModules.fortify = import ./nixos.nix;
nixosModules.hakurei = import ./nixos.nix self.packages;
buildPackage = forAllSystems (
system:
nixpkgsFor.${system}.callPackage (
import ./bundle.nix {
import ./cmd/planterette/build.nix {
inherit
nixpkgsFor
system
@ -57,18 +57,30 @@
;
in
{
check-formatting =
runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; }
''
hakurei = callPackage ./test { inherit system self; };
race = callPackage ./test {
inherit system self;
withRace = true;
};
sandbox = callPackage ./test/sandbox { inherit self; };
sandbox-race = callPackage ./test/sandbox {
inherit self;
withRace = true;
};
planterette = callPackage ./cmd/planterette/test { inherit system self; };
formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } ''
cd ${./.}
echo "running nixfmt..."
nixfmt --check .
nixfmt --width=256 --check .
touch $out
'';
check-lint =
lint =
runCommandLocal "check-lint"
{
nativeBuildInputs = [
@ -87,123 +99,68 @@
touch $out
'';
nixos-tests = callPackage ./test.nix {
inherit system self home-manager;
inherit (self.packages.${system}) fortify;
};
}
);
packages = forAllSystems (
system:
let
inherit (self.packages.${system}) fortify;
inherit (self.packages.${system}) hakurei hsu;
pkgs = nixpkgsFor.${system};
in
{
default = self.packages.${system}.fortify;
fortify = pkgs.callPackage ./package.nix { };
default = hakurei;
hakurei = pkgs.pkgsStatic.callPackage ./package.nix {
inherit (pkgs)
# passthru.buildInputs
go
gcc
dist =
pkgs.runCommand "${fortify.name}-dist" { inherit (self.devShells.${system}.default) buildInputs; }
''
# nativeBuildInputs
pkg-config
wayland-scanner
makeBinaryWrapper
# appPackages
glibc
xdg-dbus-proxy
# planterette
zstd
gnutar
coreutils
;
};
hsu = pkgs.callPackage ./cmd/hsu/package.nix { inherit (self.packages.${system}) hakurei; };
dist = pkgs.runCommand "${hakurei.name}-dist" { buildInputs = hakurei.targetPkgs ++ [ pkgs.pkgsStatic.musl ]; } ''
# go requires XDG_CACHE_HOME for the build cache
export XDG_CACHE_HOME="$(mktemp -d)"
# get a different workdir as go does not like /build
cd $(mktemp -d) && cp -r ${fortify.src}/. . && chmod -R +w .
cd $(mktemp -d) \
&& cp -r ${hakurei.src}/. . \
&& chmod +w cmd && cp -r ${hsu.src}/. cmd/hsu/ \
&& chmod -R +w .
export FORTIFY_VERSION="v${fortify.version}"
./dist/release.sh && mkdir $out && cp -v "dist/fortify-$FORTIFY_VERSION.tar.gz"* $out
export HAKUREI_VERSION="v${hakurei.version}"
./dist/release.sh && mkdir $out && cp -v "dist/hakurei-$HAKUREI_VERSION.tar.gz"* $out
'';
fhs = pkgs.buildFHSEnv {
pname = "fortify-fhs";
inherit (fortify) version;
targetPkgs =
pkgs:
with pkgs;
[
go
gcc
pkg-config
wayland-scanner
]
++ (
with pkgs.pkgsStatic;
[
musl
libffi
libseccomp
acl
wayland
wayland-protocols
]
++ (with xorg; [
libxcb
libXau
libXdmcp
xorgproto
])
);
extraOutputsToInstall = [ "dev" ];
profile = ''
export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH"
'';
};
}
);
devShells = forAllSystems (
system:
let
inherit (self.packages.${system}) fortify fhs;
inherit (self.packages.${system}) hakurei;
pkgs = nixpkgsFor.${system};
in
{
default = pkgs.mkShell {
buildInputs =
with pkgs;
[
go
gcc
]
# buildInputs
++ (
with pkgsStatic;
[
musl
libffi
libseccomp
acl
wayland
wayland-protocols
]
++ (with xorg; [
libxcb
libXau
libXdmcp
])
)
# nativeBuildInputs
++ [
pkg-config
wayland-scanner
makeBinaryWrapper
];
};
fhs = fhs.env;
withPackage = nixpkgsFor.${system}.mkShell {
buildInputs = [ self.packages.${system}.fortify ] ++ self.devShells.${system}.default.buildInputs;
};
default = pkgs.mkShell { buildInputs = hakurei.targetPkgs; };
withPackage = pkgs.mkShell { buildInputs = [ hakurei ] ++ hakurei.targetPkgs; };
generateDoc =
let
pkgs = nixpkgsFor.${system};
inherit (pkgs) lib;
doc =
@ -212,17 +169,17 @@
specialArgs = {
inherit pkgs;
};
modules = [ ./options.nix ];
modules = [ (import ./options.nix self.packages) ];
};
cleanEval = lib.filterAttrsRecursive (n: _: n != "_module") eval;
in
pkgs.nixosOptionsDoc { inherit (cleanEval) options; };
docText = pkgs.runCommand "fortify-module-docs.md" { } ''
docText = pkgs.runCommand "hakurei-module-docs.md" { } ''
cat ${doc.optionsCommonMark} > $out
sed -i '/*Declared by:*/,+1 d' $out
'';
in
nixpkgsFor.${system}.mkShell {
pkgs.mkShell {
shellHook = ''
exec cat ${docText} > options.md
'';

View File

@ -1,47 +0,0 @@
package fst
import (
"context"
"time"
)
type App interface {
// ID returns a copy of [fst.ID] held by App.
ID() ID
// Seal determines the outcome of config as a [SealedApp].
// The value of config might be overwritten and must not be used again.
Seal(config *Config) (SealedApp, error)
String() string
}
type SealedApp interface {
// Run commits sealed system setup and starts the app process.
Run(ctx context.Context, rs *RunState) error
}
// RunState stores the outcome of a call to [SealedApp.Run].
type RunState struct {
// Time is the exact point in time where the process was created.
// Location must be set to UTC.
//
// Time is nil if no process was ever created.
Time *time.Time
// ExitCode is the value returned by shim.
ExitCode int
// RevertErr is stored by the deferred revert call.
RevertErr error
// WaitErr is error returned by the underlying wait syscall.
WaitErr error
}
// Paths contains environment-dependent paths used by fortify.
type Paths struct {
// path to shared directory (usually `/tmp/fortify.%d`)
SharePath string `json:"share_path"`
// XDG_RUNTIME_DIR value (usually `/run/user/%d`)
RuntimePath string `json:"runtime_path"`
// application runtime directory (usually `/run/user/%d/fortify`)
RunDirPath string `json:"run_dir_path"`
}

View File

@ -1,166 +0,0 @@
package fst
import (
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/system"
)
const Tmp = "/.fortify"
// Config is used to seal an app
type Config struct {
// reverse-DNS style arbitrary identifier string from config;
// passed to wayland security-context-v1 as application ID
// and used as part of defaults in dbus session proxy
ID string `json:"id"`
// final argv, passed to init
Command []string `json:"command"`
Confinement ConfinementConfig `json:"confinement"`
}
// ConfinementConfig defines fortified child's confinement
type ConfinementConfig struct {
// numerical application id, determines uid in the init namespace
AppID int `json:"app_id"`
// list of supplementary groups to inherit
Groups []string `json:"groups"`
// passwd username in the sandbox, defaults to passwd name of target uid or chronos
Username string `json:"username,omitempty"`
// home directory in sandbox, empty for outer
Inner string `json:"home_inner"`
// home directory in init namespace
Outer string `json:"home"`
// bwrap sandbox confinement configuration
Sandbox *SandboxConfig `json:"sandbox"`
// extra acl ops, runs after everything else
ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
// reference to a system D-Bus proxy configuration,
// nil value disables system bus proxy
SystemBus *dbus.Config `json:"system_bus,omitempty"`
// reference to a session D-Bus proxy configuration,
// nil value makes session bus proxy assume built-in defaults
SessionBus *dbus.Config `json:"session_bus,omitempty"`
// system resources to expose to the sandbox
Enablements system.Enablements `json:"enablements"`
}
type ExtraPermConfig struct {
Ensure bool `json:"ensure,omitempty"`
Path string `json:"path"`
Read bool `json:"r,omitempty"`
Write bool `json:"w,omitempty"`
Execute bool `json:"x,omitempty"`
}
func (e *ExtraPermConfig) String() string {
buf := make([]byte, 0, 5+len(e.Path))
buf = append(buf, '-', '-', '-')
if e.Ensure {
buf = append(buf, '+')
}
buf = append(buf, ':')
buf = append(buf, []byte(e.Path)...)
if e.Read {
buf[0] = 'r'
}
if e.Write {
buf[1] = 'w'
}
if e.Execute {
buf[2] = 'x'
}
return string(buf)
}
type FilesystemConfig struct {
// mount point in sandbox, same as src if empty
Dst string `json:"dst,omitempty"`
// host filesystem path to make available to sandbox
Src string `json:"src"`
// write access
Write bool `json:"write,omitempty"`
// device access
Device bool `json:"dev,omitempty"`
// fail if mount fails
Must bool `json:"require,omitempty"`
}
// Template returns a fully populated instance of Config.
func Template() *Config {
return &Config{
ID: "org.chromium.Chromium",
Command: []string{
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland",
},
Confinement: ConfinementConfig{
AppID: 9,
Groups: []string{"video"},
Username: "chronos",
Outer: "/var/lib/persist/home/org.chromium.Chromium",
Inner: "/var/lib/fortify",
Sandbox: &SandboxConfig{
Hostname: "localhost",
UserNS: true,
Net: true,
Dev: true,
Syscall: &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
NoNewSession: true,
MapRealUID: true,
DirectWayland: false,
// example API credentials pulled from Google Chrome
// DO NOT USE THESE IN A REAL BROWSER
Env: map[string]string{
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT",
},
Filesystem: []*FilesystemConfig{
{Src: "/nix/store"},
{Src: "/run/current-system"},
{Src: "/run/opengl-driver"},
{Src: "/var/db/nix-channels"},
{Src: "/var/lib/fortify/u0/org.chromium.Chromium",
Dst: "/data/data/org.chromium.Chromium", Write: true, Must: true},
{Src: "/dev/dri", Device: true},
},
Link: [][2]string{{"/run/user/65534", "/run/user/150"}},
Etc: "/etc",
AutoEtc: true,
Override: []string{"/var/run/nscd"},
},
ExtraPerms: []*ExtraPermConfig{
{Path: "/var/lib/fortify/u0", Ensure: true, Execute: true},
{Path: "/var/lib/fortify/u0/org.chromium.Chromium", Read: true, Write: true, Execute: true},
},
SystemBus: &dbus.Config{
See: nil,
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
Own: nil,
Call: nil,
Broadcast: nil,
Log: false,
Filter: true,
},
SessionBus: &dbus.Config{
See: nil,
Talk: []string{"org.freedesktop.Notifications", "org.freedesktop.FileManager1", "org.freedesktop.ScreenSaver",
"org.freedesktop.secrets", "org.kde.kwalletd5", "org.kde.kwalletd6", "org.gnome.SessionManager"},
Own: []string{"org.chromium.Chromium.*", "org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"},
Call: map[string]string{"org.freedesktop.portal.*": "*"},
Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"},
Log: false,
Filter: true,
},
Enablements: system.EWayland.Mask() | system.EDBus.Mask() | system.EPulse.Mask(),
},
}
}

View File

@ -1,243 +0,0 @@
package fst
import (
"errors"
"fmt"
"io/fs"
"path"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/helper/bwrap"
)
// SandboxConfig describes resources made available to the sandbox.
type SandboxConfig struct {
// unix hostname within sandbox
Hostname string `json:"hostname,omitempty"`
// allow userns within sandbox
UserNS bool `json:"userns,omitempty"`
// share net namespace
Net bool `json:"net,omitempty"`
// share all devices
Dev bool `json:"dev,omitempty"`
// seccomp syscall filter policy
Syscall *bwrap.SyscallPolicy `json:"syscall"`
// do not run in new session
NoNewSession bool `json:"no_new_session,omitempty"`
// map target user uid to privileged user uid in the user namespace
MapRealUID bool `json:"map_real_uid"`
// direct access to wayland socket; when this gets set no attempt is made to attach security-context-v1
// and the bare socket is mounted to the sandbox
DirectWayland bool `json:"direct_wayland,omitempty"`
// final environment variables
Env map[string]string `json:"env"`
// sandbox host filesystem access
Filesystem []*FilesystemConfig `json:"filesystem"`
// symlinks created inside the sandbox
Link [][2]string `json:"symlink"`
// read-only /etc directory
Etc string `json:"etc,omitempty"`
// automatically set up /etc symlinks
AutoEtc bool `json:"auto_etc"`
// mount tmpfs over these paths,
// runs right before [ConfinementConfig.ExtraPerms]
Override []string `json:"override"`
}
// SandboxSys encapsulates system functions used during the creation of [bwrap.Config].
type SandboxSys interface {
Geteuid() int
Paths() Paths
ReadDir(name string) ([]fs.DirEntry, error)
EvalSymlinks(path string) (string, error)
Println(v ...any)
Printf(format string, v ...any)
}
// Bwrap returns the address of the corresponding bwrap.Config to s.
// Note that remaining tmpfs entries must be queued by the caller prior to launch.
func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) {
if s == nil {
return nil, errors.New("nil sandbox config")
}
if s.Syscall == nil {
sys.Println("syscall filter not configured, PROCEED WITH CAUTION")
}
if !s.MapRealUID {
// mapped uid defaults to 65534 to work around file ownership checks due to a bwrap limitation
*uid = 65534
} else {
// some programs fail to connect to dbus session running as a different uid, so a separate workaround
// is introduced to map priv-side caller uid in namespace
*uid = sys.Geteuid()
}
conf := (&bwrap.Config{
Net: s.Net,
UserNS: s.UserNS,
UID: uid,
GID: uid,
Hostname: s.Hostname,
Clearenv: true,
SetEnv: s.Env,
/* this is only 4 KiB of memory on a 64-bit system,
permissive defaults on NixOS results in around 100 entries
so this capacity should eliminate copies for most setups */
Filesystem: make([]bwrap.FSBuilder, 0, 256),
Syscall: s.Syscall,
NewSession: !s.NoNewSession,
DieWithParent: true,
AsInit: true,
// initialise unconditionally as Once cannot be justified
// for saving such a miniscule amount of memory
Chmod: make(bwrap.ChmodConfig),
}).
Procfs("/proc").
Tmpfs(Tmp, 4*1024)
if !s.Dev {
conf.DevTmpfs("/dev").Mqueue("/dev/mqueue")
} else {
conf.Bind("/dev", "/dev", false, true, true)
}
if !s.AutoEtc {
if s.Etc == "" {
conf.Dir("/etc")
} else {
conf.Bind(s.Etc, "/etc")
}
}
// retrieve paths and hide them if they're made available in the sandbox
var hidePaths []string
sc := sys.Paths()
hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath)
_, systemBusAddr := dbus.Address()
if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
return nil, err
} else {
// there is usually only one, do not preallocate
for _, entry := range entries {
if entry.Method != "unix" {
continue
}
for _, pair := range entry.Values {
if pair[0] == "path" {
if path.IsAbs(pair[1]) {
// get parent dir of socket
dir := path.Dir(pair[1])
if dir == "." || dir == "/" {
sys.Printf("dbus socket %q is in an unusual location", pair[1])
}
hidePaths = append(hidePaths, dir)
} else {
sys.Printf("dbus socket %q is not absolute", pair[1])
}
}
}
}
}
hidePathMatch := make([]bool, len(hidePaths))
for i := range hidePaths {
if err := evalSymlinks(sys, &hidePaths[i]); err != nil {
return nil, err
}
}
for _, c := range s.Filesystem {
if c == nil {
continue
}
if !path.IsAbs(c.Src) {
return nil, fmt.Errorf("src path %q is not absolute", c.Src)
}
dest := c.Dst
if c.Dst == "" {
dest = c.Src
} else if !path.IsAbs(dest) {
return nil, fmt.Errorf("dst path %q is not absolute", dest)
}
srcH := c.Src
if err := evalSymlinks(sys, &srcH); err != nil {
return nil, err
}
for i := range hidePaths {
// skip matched entries
if hidePathMatch[i] {
continue
}
if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil {
return nil, err
} else if ok {
hidePathMatch[i] = true
sys.Printf("hiding paths from %q", c.Src)
}
}
conf.Bind(c.Src, dest, !c.Must, c.Write, c.Device)
}
// hide marked paths before setting up shares
for i, ok := range hidePathMatch {
if ok {
conf.Tmpfs(hidePaths[i], 8192)
}
}
for _, l := range s.Link {
conf.Symlink(l[0], l[1])
}
if s.AutoEtc {
etc := s.Etc
if etc == "" {
etc = "/etc"
}
conf.Bind(etc, Tmp+"/etc")
// link host /etc contents to prevent passwd/group from being overwritten
if d, err := sys.ReadDir(etc); err != nil {
return nil, err
} else {
for _, ent := range d {
name := ent.Name()
switch name {
case "passwd":
case "group":
case "mtab":
conf.Symlink("/proc/mounts", "/etc/"+name)
default:
conf.Symlink(Tmp+"/etc/"+name, "/etc/"+name)
}
}
}
}
return conf, nil
}
func evalSymlinks(sys SandboxSys, v *string) error {
if p, err := sys.EvalSymlinks(*v); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return err
}
sys.Printf("path %q does not yet exist", *v)
} else {
*v = p
}
return nil
}

View File

@ -1,2 +0,0 @@
// Package fst exports shared fortify types.
package fst

4
go.mod
View File

@ -1,3 +1,3 @@
module git.gensokyo.uk/security/fortify
module git.gensokyo.uk/security/hakurei
go 1.22
go 1.24

View File

@ -1,38 +1,17 @@
package helper
import (
"errors"
"bytes"
"io"
"strings"
"syscall"
)
var (
ErrContainsNull = errors.New("argument contains null character")
)
type argsWt []string
// checks whether any element contains the null character
// must be called before args use and args must not be modified after call
func (a argsWt) check() error {
for _, arg := range a {
for _, b := range arg {
if b == '\x00' {
return ErrContainsNull
}
}
}
return nil
}
type argsWt [][]byte
func (a argsWt) WriteTo(w io.Writer) (int64, error) {
// assuming already checked
nt := 0
// write null terminated arguments
for _, arg := range a {
n, err := w.Write([]byte(arg + "\x00"))
n, err := w.Write(arg)
nt += n
if err != nil {
@ -44,18 +23,32 @@ func (a argsWt) WriteTo(w io.Writer) (int64, error) {
}
func (a argsWt) String() string {
return strings.Join(a, " ")
return string(
bytes.TrimSuffix(
bytes.ReplaceAll(
bytes.Join(a, nil),
[]byte{0}, []byte{' '},
),
[]byte{' '},
),
)
}
// NewCheckedArgs returns a checked argument writer for args.
// Callers must not retain any references to args.
func NewCheckedArgs(args []string) (io.WriterTo, error) {
a := argsWt(args)
return a, a.check()
// NewCheckedArgs returns a checked null-terminated argument writer for a copy of args.
func NewCheckedArgs(args []string) (wt io.WriterTo, err error) {
a := make(argsWt, len(args))
for i, arg := range args {
a[i], err = syscall.ByteSliceFromString(arg)
if err != nil {
return
}
}
wt = a
return
}
// MustNewCheckedArgs returns a checked argument writer for args and panics if check fails.
// Callers must not retain any references to args.
// MustNewCheckedArgs returns a checked null-terminated argument writer for a copy of args.
// If s contains a NUL byte this function panics instead of returning an error.
func MustNewCheckedArgs(args []string) io.WriterTo {
a, err := NewCheckedArgs(args)
if err != nil {

View File

@ -4,34 +4,33 @@ import (
"errors"
"fmt"
"strings"
"syscall"
"testing"
"git.gensokyo.uk/security/fortify/helper"
"git.gensokyo.uk/security/hakurei/helper"
)
func Test_argsFD_String(t *testing.T) {
func TestArgsString(t *testing.T) {
wantString := strings.Join(wantArgs, " ")
if got := argsWt.(fmt.Stringer).String(); got != wantString {
t.Errorf("String(): got %v; want %v",
t.Errorf("String: %q, want %q",
got, wantString)
}
}
func TestNewCheckedArgs(t *testing.T) {
args := []string{"\x00"}
if _, err := helper.NewCheckedArgs(args); !errors.Is(err, helper.ErrContainsNull) {
t.Errorf("NewCheckedArgs(%q) error = %v, wantErr %v",
args,
err, helper.ErrContainsNull)
if _, err := helper.NewCheckedArgs(args); !errors.Is(err, syscall.EINVAL) {
t.Errorf("NewCheckedArgs: error = %v, wantErr %v",
err, syscall.EINVAL)
}
t.Run("must panic", func(t *testing.T) {
badPayload := []string{"\x00"}
defer func() {
wantPanic := "argument contains null character"
wantPanic := "invalid argument"
if r := recover(); r != wantPanic {
t.Errorf("MustNewCheckedArgs(%q) panic = %v, wantPanic %v",
badPayload,
t.Errorf("MustNewCheckedArgs: panic = %v, wantPanic %v",
r, wantPanic)
}
}()

View File

@ -1,87 +0,0 @@
package helper
import (
"context"
"errors"
"io"
"os"
"slices"
"strconv"
"sync"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/helper/proc"
)
// BubblewrapName is the file name or path to bubblewrap.
var BubblewrapName = "bwrap"
type bubblewrap struct {
// final args fd of bwrap process
argsFd uintptr
// name of the command to run in bwrap
name string
lock sync.RWMutex
*helperCmd
}
func (b *bubblewrap) Start(ctx context.Context, stat bool) error {
b.lock.Lock()
defer b.lock.Unlock()
// Check for doubled Start calls before we defer failure cleanup. If the prior
// call to Start succeeded, we don't want to spuriously close its pipes.
if b.Cmd != nil && b.Cmd.Process != nil {
return errors.New("exec: already started")
}
args := b.finalise(ctx, stat)
b.Cmd.Args = slices.Grow(b.Cmd.Args, 4+len(args))
b.Cmd.Args = append(b.Cmd.Args, "--args", strconv.Itoa(int(b.argsFd)), "--", b.name)
b.Cmd.Args = append(b.Cmd.Args, args...)
return proc.Fulfill(ctx, b.Cmd, b.files, b.extraFiles)
}
// MustNewBwrap initialises a new Bwrap instance with wt as the null-terminated argument writer.
// If wt is nil, the child process spawned by bwrap will not get an argument pipe.
// Function argF returns an array of arguments passed directly to the child process.
func MustNewBwrap(
conf *bwrap.Config, name string,
wt io.WriterTo, argF func(argsFD, statFD int) []string,
extraFiles []*os.File,
syncFd *os.File,
) Helper {
b, err := NewBwrap(conf, name, wt, argF, extraFiles, syncFd)
if err != nil {
panic(err.Error())
} else {
return b
}
}
// NewBwrap initialises a new Bwrap instance with wt as the null-terminated argument writer.
// If wt is nil, the child process spawned by bwrap will not get an argument pipe.
// Function argF returns an array of arguments passed directly to the child process.
func NewBwrap(
conf *bwrap.Config, name string,
wt io.WriterTo, argF func(argsFd, statFd int) []string,
extraFiles []*os.File,
syncFd *os.File,
) (Helper, error) {
b := new(bubblewrap)
b.name = name
b.helperCmd = newHelperCmd(b, BubblewrapName, wt, argF, extraFiles)
if v, err := NewCheckedArgs(conf.Args(syncFd, b.extraFiles, &b.files)); err != nil {
return nil, err
} else {
f := proc.NewWriterTo(v)
b.argsFd = proc.InitFile(f, b.extraFiles)
b.files = append(b.files, f)
}
return b, nil
}

View File

@ -1,72 +0,0 @@
package bwrap
import (
"os"
"slices"
"git.gensokyo.uk/security/fortify/helper/proc"
)
type Builder interface {
Len() int
Append(args *[]string)
}
type FSBuilder interface {
Path() string
Builder
}
type FDBuilder interface {
proc.File
Builder
}
// Args returns a slice of bwrap args corresponding to c.
func (c *Config) Args(syncFd *os.File, extraFiles *proc.ExtraFilesPre, files *[]proc.File) (args []string) {
builders := []Builder{
c.boolArgs(),
c.intArgs(),
c.stringArgs(),
c.pairArgs(),
c.seccompArgs(),
newFile(SyncFd.String(), syncFd),
}
builders = slices.Grow(builders, len(c.Filesystem)+1)
for _, f := range c.Filesystem {
builders = append(builders, f)
}
builders = append(builders, c.Chmod)
argc := 0
fc := 0
for _, b := range builders {
l := b.Len()
if l < 1 {
continue
}
argc += l
if f, ok := b.(FDBuilder); ok {
fc++
proc.InitFile(f, extraFiles)
}
}
fc++ // allocate extra slot for stat fd
args = make([]string, 0, argc)
*files = slices.Grow(*files, fc)
for _, b := range builders {
if b.Len() < 1 {
continue
}
b.Append(&args)
if f, ok := b.(FDBuilder); ok {
*files = append(*files, f)
}
}
return
}

View File

@ -1,199 +0,0 @@
package bwrap
import (
"os"
)
/*
Bind binds mount src on host to dest in sandbox.
Bind(src, dest) bind mount host path readonly on sandbox
(--ro-bind SRC DEST).
Bind(src, dest, true) equal to ROBind but ignores non-existent host path
(--ro-bind-try SRC DEST).
Bind(src, dest, false, true) bind mount host path on sandbox.
(--bind SRC DEST).
Bind(src, dest, true, true) equal to Bind but ignores non-existent host path
(--bind-try SRC DEST).
Bind(src, dest, false, true, true) bind mount host path on sandbox, allowing device access
(--dev-bind SRC DEST).
Bind(src, dest, true, true, true) equal to DevBind but ignores non-existent host path
(--dev-bind-try SRC DEST).
*/
func (c *Config) Bind(src, dest string, opts ...bool) *Config {
var (
try bool
write bool
dev bool
)
if len(opts) > 0 {
try = opts[0]
}
if len(opts) > 1 {
write = opts[1]
}
if len(opts) > 2 {
dev = opts[2]
}
if dev {
if try {
c.Filesystem = append(c.Filesystem, &pairF{DevBindTry.String(), src, dest})
} else {
c.Filesystem = append(c.Filesystem, &pairF{DevBind.String(), src, dest})
}
return c
} else if write {
if try {
c.Filesystem = append(c.Filesystem, &pairF{BindTry.String(), src, dest})
} else {
c.Filesystem = append(c.Filesystem, &pairF{Bind.String(), src, dest})
}
return c
} else {
if try {
c.Filesystem = append(c.Filesystem, &pairF{ROBindTry.String(), src, dest})
} else {
c.Filesystem = append(c.Filesystem, &pairF{ROBind.String(), src, dest})
}
return c
}
}
// WriteFile copy from FD to destination DEST
// (--file FD DEST)
func (c *Config) WriteFile(name string, data []byte) *Config {
c.Filesystem = append(c.Filesystem, &DataConfig{Dest: name, Data: data, Type: DataWrite})
return c
}
/*
CopyBind copy from FD to file which is readonly bind-mounted on DEST
(--ro-bind-data FD DEST)
CopyBind(dest, payload, true) copy from FD to file which is bind-mounted on DEST
(--bind-data FD DEST)
*/
func (c *Config) CopyBind(dest string, payload []byte, opts ...bool) *Config {
var p *[]byte
c.CopyBindRef(dest, &p, opts...)
*p = payload
return c
}
// CopyBindRef is the same as CopyBind but writes the address of DataConfig.Data.
func (c *Config) CopyBindRef(dest string, payloadRef **[]byte, opts ...bool) *Config {
t := DataROBind
if len(opts) > 0 && opts[0] {
t = DataBind
}
d := &DataConfig{Dest: dest, Type: t}
*payloadRef = &d.Data
c.Filesystem = append(c.Filesystem, d)
return c
}
// Dir create dir in sandbox
// (--dir DEST)
func (c *Config) Dir(dest string) *Config {
c.Filesystem = append(c.Filesystem, &stringF{Dir.String(), dest})
return c
}
// RemountRO remount path as readonly; does not recursively remount
// (--remount-ro DEST)
func (c *Config) RemountRO(dest string) *Config {
c.Filesystem = append(c.Filesystem, &stringF{RemountRO.String(), dest})
return c
}
// Procfs mount new procfs in sandbox
// (--proc DEST)
func (c *Config) Procfs(dest string) *Config {
c.Filesystem = append(c.Filesystem, &stringF{Procfs.String(), dest})
return c
}
// DevTmpfs mount new dev in sandbox
// (--dev DEST)
func (c *Config) DevTmpfs(dest string) *Config {
c.Filesystem = append(c.Filesystem, &stringF{DevTmpfs.String(), dest})
return c
}
// Mqueue mount new mqueue in sandbox
// (--mqueue DEST)
func (c *Config) Mqueue(dest string) *Config {
c.Filesystem = append(c.Filesystem, &stringF{Mqueue.String(), dest})
return c
}
// Tmpfs mount new tmpfs in sandbox
// (--tmpfs DEST)
func (c *Config) Tmpfs(dest string, size int, perm ...os.FileMode) *Config {
tmpfs := &PermConfig[*TmpfsConfig]{Inner: &TmpfsConfig{Dir: dest}}
if size >= 0 {
tmpfs.Inner.Size = size
}
if len(perm) == 1 {
tmpfs.Mode = &perm[0]
}
c.Filesystem = append(c.Filesystem, tmpfs)
return c
}
// Overlay mount overlayfs on DEST, with writes going to an invisible tmpfs
// (--tmp-overlay DEST)
func (c *Config) Overlay(dest string, src ...string) *Config {
c.Filesystem = append(c.Filesystem, &OverlayConfig{Src: src, Dest: dest})
return c
}
// Join mount overlayfs read-only on DEST
// (--ro-overlay DEST)
func (c *Config) Join(dest string, src ...string) *Config {
c.Filesystem = append(c.Filesystem, &OverlayConfig{Src: src, Dest: dest, Persist: new([2]string)})
return c
}
// Persist mount overlayfs on DEST, with RWSRC as the host path for writes and
// WORKDIR an empty directory on the same filesystem as RWSRC
// (--overlay RWSRC WORKDIR DEST)
func (c *Config) Persist(dest, rwsrc, workdir string, src ...string) *Config {
if rwsrc == "" || workdir == "" {
panic("persist called without required paths")
}
c.Filesystem = append(c.Filesystem, &OverlayConfig{Src: src, Dest: dest, Persist: &[2]string{rwsrc, workdir}})
return c
}
// Symlink create symlink within sandbox
// (--symlink SRC DEST)
func (c *Config) Symlink(src, dest string, perm ...os.FileMode) *Config {
symlink := &PermConfig[SymlinkConfig]{Inner: SymlinkConfig{src, dest}}
if len(perm) == 1 {
symlink.Mode = &perm[0]
}
c.Filesystem = append(c.Filesystem, symlink)
return c
}
// SetUID sets custom uid in the sandbox, requires new user namespace (--uid UID).
func (c *Config) SetUID(uid int) *Config {
if uid >= 0 {
c.UID = &uid
}
return c
}
// SetGID sets custom gid in the sandbox, requires new user namespace (--gid GID).
func (c *Config) SetGID(gid int) *Config {
if gid >= 0 {
c.GID = &gid
}
return c
}

View File

@ -1,104 +0,0 @@
package bwrap
type Config struct {
// unshare every namespace we support by default if nil
// (--unshare-all)
Unshare *UnshareConfig `json:"unshare,omitempty"`
// retain the network namespace (can only combine with nil Unshare)
// (--share-net)
Net bool `json:"net"`
// disable further use of user namespaces inside sandbox and fail unless
// further use of user namespace inside sandbox is disabled if false
// (--disable-userns) (--assert-userns-disabled)
UserNS bool `json:"userns"`
// custom uid in the sandbox, requires new user namespace
// (--uid UID)
UID *int `json:"uid,omitempty"`
// custom gid in the sandbox, requires new user namespace
// (--gid GID)
GID *int `json:"gid,omitempty"`
// custom hostname in the sandbox, requires new uts namespace
// (--hostname NAME)
Hostname string `json:"hostname,omitempty"`
// change directory
// (--chdir DIR)
Chdir string `json:"chdir,omitempty"`
// unset all environment variables
// (--clearenv)
Clearenv bool `json:"clearenv"`
// set environment variable
// (--setenv VAR VALUE)
SetEnv map[string]string `json:"setenv,omitempty"`
// unset environment variables
// (--unsetenv VAR)
UnsetEnv []string `json:"unsetenv,omitempty"`
// take a lock on file while sandbox is running
// (--lock-file DEST)
LockFile []string `json:"lock_file,omitempty"`
// ordered filesystem args
Filesystem []FSBuilder `json:"filesystem,omitempty"`
// change permissions (must already exist)
// (--chmod OCTAL PATH)
Chmod ChmodConfig `json:"chmod,omitempty"`
// load and use seccomp rules from FD (not repeatable)
// (--seccomp FD)
Syscall *SyscallPolicy
// create a new terminal session
// (--new-session)
NewSession bool `json:"new_session"`
// kills with SIGKILL child process (COMMAND) when bwrap or bwrap's parent dies.
// (--die-with-parent)
DieWithParent bool `json:"die_with_parent"`
// do not install a reaper process with PID=1
// (--as-pid-1)
AsInit bool `json:"as_init"`
/* unmapped options include:
--unshare-user-try Create new user namespace if possible else continue by skipping it
--unshare-cgroup-try Create new cgroup namespace if possible else continue by skipping it
--userns FD Use this user namespace (cannot combine with --unshare-user)
--userns2 FD After setup switch to this user namespace, only useful with --userns
--pidns FD Use this pid namespace (as parent namespace if using --unshare-pid)
--bind-fd FD DEST Bind open directory or path fd on DEST
--ro-bind-fd FD DEST Bind open directory or path fd read-only on DEST
--exec-label LABEL Exec label for the sandbox
--file-label LABEL File label for temporary sandbox content
--add-seccomp-fd FD Load and use seccomp rules from FD (repeatable)
--block-fd FD Block on FD until some data to read is available
--userns-block-fd FD Block on FD until the user namespace is ready
--info-fd FD Write information about the running container to FD
--json-status-fd FD Write container status to FD as multiple JSON documents
--cap-add CAP Add cap CAP when running as privileged user
--cap-drop CAP Drop cap CAP when running as privileged user
among which --args is used internally for passing arguments */
}
type UnshareConfig struct {
// (--unshare-user)
// create new user namespace
User bool `json:"user"`
// (--unshare-ipc)
// create new ipc namespace
IPC bool `json:"ipc"`
// (--unshare-pid)
// create new pid namespace
PID bool `json:"pid"`
// (--unshare-net)
// create new network namespace
Net bool `json:"net"`
// (--unshare-uts)
// create new uts namespace
UTS bool `json:"uts"`
// (--unshare-cgroup)
// create new cgroup namespace
CGroup bool `json:"cgroup"`
}

View File

@ -1,257 +0,0 @@
package bwrap_test
import (
"log"
"os"
"slices"
"testing"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/helper/seccomp"
)
func TestConfig_Args(t *testing.T) {
seccomp.CPrintln = log.Println
t.Cleanup(func() { seccomp.CPrintln = nil })
testCases := []struct {
name string
conf *bwrap.Config
want []string
}{
{
"bind", (new(bwrap.Config)).
Bind("/etc", "/.fortify/etc").
Bind("/etc", "/.fortify/etc", true).
Bind("/run", "/.fortify/run", false, true).
Bind("/sys/devices", "/.fortify/sys/devices", true, true).
Bind("/dev/dri", "/.fortify/dev/dri", false, true, true).
Bind("/dev/dri", "/.fortify/dev/dri", true, true, true),
[]string{
"--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled",
// Bind("/etc", "/.fortify/etc")
"--ro-bind", "/etc", "/.fortify/etc",
// Bind("/etc", "/.fortify/etc", true)
"--ro-bind-try", "/etc", "/.fortify/etc",
// Bind("/run", "/.fortify/run", false, true)
"--bind", "/run", "/.fortify/run",
// Bind("/sys/devices", "/.fortify/sys/devices", true, true)
"--bind-try", "/sys/devices", "/.fortify/sys/devices",
// Bind("/dev/dri", "/.fortify/dev/dri", false, true, true)
"--dev-bind", "/dev/dri", "/.fortify/dev/dri",
// Bind("/dev/dri", "/.fortify/dev/dri", true, true, true)
"--dev-bind-try", "/dev/dri", "/.fortify/dev/dri",
},
},
{
"dir remount-ro proc dev mqueue", (new(bwrap.Config)).
Dir("/.fortify").
RemountRO("/home").
Procfs("/proc").
DevTmpfs("/dev").
Mqueue("/dev/mqueue"),
[]string{
"--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled",
// Dir("/.fortify")
"--dir", "/.fortify",
// RemountRO("/home")
"--remount-ro", "/home",
// Procfs("/proc")
"--proc", "/proc",
// DevTmpfs("/dev")
"--dev", "/dev",
// Mqueue("/dev/mqueue")
"--mqueue", "/dev/mqueue",
},
},
{
"tmpfs", (new(bwrap.Config)).
Tmpfs("/run/user", 8192).
Tmpfs("/run/dbus", 8192, 0755),
[]string{
"--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled",
// Tmpfs("/run/user", 8192)
"--size", "8192", "--tmpfs", "/run/user",
// Tmpfs("/run/dbus", 8192, 0755)
"--perms", "755", "--size", "8192", "--tmpfs", "/run/dbus",
},
},
{
"symlink", (new(bwrap.Config)).
Symlink("/.fortify/sbin/init", "/sbin/init").
Symlink("/.fortify/sbin/init", "/sbin/init", 0755),
[]string{
"--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled",
// Symlink("/.fortify/sbin/init", "/sbin/init")
"--symlink", "/.fortify/sbin/init", "/sbin/init",
// Symlink("/.fortify/sbin/init", "/sbin/init", 0755)
"--perms", "755", "--symlink", "/.fortify/sbin/init", "/sbin/init",
},
},
{
"overlayfs", (new(bwrap.Config)).
Overlay("/etc", "/etc").
Join("/.fortify/bin", "/bin", "/usr/bin", "/usr/local/bin").
Persist("/nix", "/data/data/org.chromium.Chromium/overlay/rwsrc", "/data/data/org.chromium.Chromium/workdir", "/data/app/org.chromium.Chromium/nix"),
[]string{
"--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled",
// Overlay("/etc", "/etc")
"--overlay-src", "/etc", "--tmp-overlay", "/etc",
// Join("/.fortify/bin", "/bin", "/usr/bin", "/usr/local/bin")
"--overlay-src", "/bin", "--overlay-src", "/usr/bin",
"--overlay-src", "/usr/local/bin", "--ro-overlay", "/.fortify/bin",
// Persist("/nix", "/data/data/org.chromium.Chromium/overlay/rwsrc", "/data/data/org.chromium.Chromium/workdir", "/data/app/org.chromium.Chromium/nix")
"--overlay-src", "/data/app/org.chromium.Chromium/nix",
"--overlay", "/data/data/org.chromium.Chromium/overlay/rwsrc", "/data/data/org.chromium.Chromium/workdir", "/nix",
},
},
{
"copy", (new(bwrap.Config)).
WriteFile("/.fortify/version", make([]byte, 8)).
CopyBind("/etc/group", make([]byte, 8)).
CopyBind("/etc/passwd", make([]byte, 8), true),
[]string{
"--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled",
// Write("/.fortify/version", make([]byte, 8))
"--file", "3", "/.fortify/version",
// CopyBind("/etc/group", make([]byte, 8))
"--ro-bind-data", "4", "/etc/group",
// CopyBind("/etc/passwd", make([]byte, 8), true)
"--bind-data", "5", "/etc/passwd",
},
},
{
"unshare", &bwrap.Config{Unshare: &bwrap.UnshareConfig{
User: false,
IPC: false,
PID: false,
Net: false,
UTS: false,
CGroup: false,
}},
[]string{"--disable-userns", "--assert-userns-disabled"},
},
{
"uid gid sync", (new(bwrap.Config)).
SetUID(1971).
SetGID(100),
[]string{
"--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled",
// SetUID(1971)
"--uid", "1971",
// SetGID(100)
"--gid", "100",
},
},
{
"hostname chdir setenv unsetenv lockfile chmod syscall", &bwrap.Config{
Hostname: "fortify",
Chdir: "/.fortify",
SetEnv: map[string]string{"FORTIFY_INIT": "/.fortify/sbin/init"},
UnsetEnv: []string{"HOME", "HOST"},
LockFile: []string{"/.fortify/lock"},
Syscall: new(bwrap.SyscallPolicy),
Chmod: map[string]os.FileMode{"/.fortify/sbin/init": 0755},
},
[]string{
"--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled",
// Hostname: "fortify"
"--hostname", "fortify",
// Chdir: "/.fortify"
"--chdir", "/.fortify",
// UnsetEnv: []string{"HOME", "HOST"}
"--unsetenv", "HOME",
"--unsetenv", "HOST",
// LockFile: []string{"/.fortify/lock"},
"--lock-file", "/.fortify/lock",
// SetEnv: map[string]string{"FORTIFY_INIT": "/.fortify/sbin/init"}
"--setenv", "FORTIFY_INIT", "/.fortify/sbin/init",
// Syscall: new(bwrap.SyscallPolicy),
"--seccomp", "3",
// Chmod: map[string]os.FileMode{"/.fortify/sbin/init": 0755}
"--chmod", "755", "/.fortify/sbin/init",
},
},
{
"xdg-dbus-proxy constraint sample", (&bwrap.Config{Clearenv: true, DieWithParent: true}).
Symlink("usr/bin", "/bin").
Symlink("var/home", "/home").
Symlink("usr/lib", "/lib").
Symlink("usr/lib64", "/lib64").
Symlink("run/media", "/media").
Symlink("var/mnt", "/mnt").
Symlink("var/opt", "/opt").
Symlink("sysroot/ostree", "/ostree").
Symlink("var/roothome", "/root").
Symlink("usr/sbin", "/sbin").
Symlink("var/srv", "/srv").
Bind("/run", "/run", false, true).
Bind("/tmp", "/tmp", false, true).
Bind("/var", "/var", false, true).
Bind("/run/user/1971/.dbus-proxy/", "/run/user/1971/.dbus-proxy/", false, true).
Bind("/boot", "/boot").
Bind("/dev", "/dev").
Bind("/proc", "/proc").
Bind("/sys", "/sys").
Bind("/sysroot", "/sysroot").
Bind("/usr", "/usr").
Bind("/etc", "/etc"),
[]string{
"--unshare-all", "--unshare-user",
"--disable-userns", "--assert-userns-disabled",
"--clearenv", "--die-with-parent",
"--symlink", "usr/bin", "/bin",
"--symlink", "var/home", "/home",
"--symlink", "usr/lib", "/lib",
"--symlink", "usr/lib64", "/lib64",
"--symlink", "run/media", "/media",
"--symlink", "var/mnt", "/mnt",
"--symlink", "var/opt", "/opt",
"--symlink", "sysroot/ostree", "/ostree",
"--symlink", "var/roothome", "/root",
"--symlink", "usr/sbin", "/sbin",
"--symlink", "var/srv", "/srv",
"--bind", "/run", "/run",
"--bind", "/tmp", "/tmp",
"--bind", "/var", "/var",
"--bind", "/run/user/1971/.dbus-proxy/", "/run/user/1971/.dbus-proxy/",
"--ro-bind", "/boot", "/boot",
"--ro-bind", "/dev", "/dev",
"--ro-bind", "/proc", "/proc",
"--ro-bind", "/sys", "/sys",
"--ro-bind", "/sysroot", "/sysroot",
"--ro-bind", "/usr", "/usr",
"--ro-bind", "/etc", "/etc",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := tc.conf.Args(nil, new(proc.ExtraFilesPre), new([]proc.File)); !slices.Equal(got, tc.want) {
t.Errorf("Args() = %#v, want %#v", got, tc.want)
}
})
}
// test persist validation
t.Run("invalid persist", func(t *testing.T) {
defer func() {
wantPanic := "persist called without required paths"
if r := recover(); r != wantPanic {
t.Errorf("Persist() panic = %v; wantPanic %v", r, wantPanic)
}
}()
(new(bwrap.Config)).Persist("/run", "", "")
})
}

View File

@ -1,85 +0,0 @@
package bwrap
import (
"fmt"
"strconv"
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/helper/seccomp"
)
type SyscallPolicy struct {
// disable fortify extensions
Compat bool `json:"compat"`
// deny development syscalls
DenyDevel bool `json:"deny_devel"`
// deny multiarch/emulation syscalls
Multiarch bool `json:"multiarch"`
// allow PER_LINUX32
Linux32 bool `json:"linux32"`
// allow AF_CAN
Can bool `json:"can"`
// allow AF_BLUETOOTH
Bluetooth bool `json:"bluetooth"`
}
func (c *Config) seccompArgs() FDBuilder {
// explicitly disable syscall filter
if c.Syscall == nil {
// nil File skips builder
return new(seccompBuilder)
}
var (
opts seccomp.SyscallOpts
optd []string
optCond = [...]struct {
v bool
o seccomp.SyscallOpts
d string
}{
{!c.Syscall.Compat, seccomp.FlagExt, "fortify"},
{!c.UserNS, seccomp.FlagDenyNS, "denyns"},
{c.NewSession, seccomp.FlagDenyTTY, "denytty"},
{c.Syscall.DenyDevel, seccomp.FlagDenyDevel, "denydevel"},
{c.Syscall.Multiarch, seccomp.FlagMultiarch, "multiarch"},
{c.Syscall.Linux32, seccomp.FlagLinux32, "linux32"},
{c.Syscall.Can, seccomp.FlagCan, "can"},
{c.Syscall.Bluetooth, seccomp.FlagBluetooth, "bluetooth"},
}
)
if seccomp.CPrintln != nil {
optd = make([]string, 1, len(optCond)+1)
optd[0] = "common"
}
for _, opt := range optCond {
if opt.v {
opts |= opt.o
if seccomp.CPrintln != nil {
optd = append(optd, opt.d)
}
}
}
if seccomp.CPrintln != nil {
seccomp.CPrintln(fmt.Sprintf("seccomp flags: %s", optd))
}
return &seccompBuilder{seccomp.NewFile(opts)}
}
type seccompBuilder struct{ proc.File }
func (s *seccompBuilder) Len() int {
if s == nil || s.File == nil {
return 0
}
return 2
}
func (s *seccompBuilder) Append(args *[]string) {
if s == nil || s.File == nil {
return
}
*args = append(*args, Seccomp.String(), strconv.Itoa(int(s.Fd())))
}

View File

@ -1,273 +0,0 @@
package bwrap
import (
"encoding/gob"
"fmt"
"io"
"os"
"strconv"
"git.gensokyo.uk/security/fortify/helper/proc"
)
func init() {
gob.Register(new(PermConfig[SymlinkConfig]))
gob.Register(new(PermConfig[*TmpfsConfig]))
gob.Register(new(OverlayConfig))
gob.Register(new(DataConfig))
}
type PositionalArg int
func (p PositionalArg) String() string { return positionalArgs[p] }
const (
Tmpfs PositionalArg = iota
Symlink
Bind
BindTry
DevBind
DevBindTry
ROBind
ROBindTry
Chmod
Dir
RemountRO
Procfs
DevTmpfs
Mqueue
Perms
Size
OverlaySrc
Overlay
TmpOverlay
ROOverlay
SyncFd
Seccomp
File
BindData
ROBindData
)
var positionalArgs = [...]string{
Tmpfs: "--tmpfs",
Symlink: "--symlink",
Bind: "--bind",
BindTry: "--bind-try",
DevBind: "--dev-bind",
DevBindTry: "--dev-bind-try",
ROBind: "--ro-bind",
ROBindTry: "--ro-bind-try",
Chmod: "--chmod",
Dir: "--dir",
RemountRO: "--remount-ro",
Procfs: "--proc",
DevTmpfs: "--dev",
Mqueue: "--mqueue",
Perms: "--perms",
Size: "--size",
OverlaySrc: "--overlay-src",
Overlay: "--overlay",
TmpOverlay: "--tmp-overlay",
ROOverlay: "--ro-overlay",
SyncFd: "--sync-fd",
Seccomp: "--seccomp",
File: "--file",
BindData: "--bind-data",
ROBindData: "--ro-bind-data",
}
type PermConfig[T FSBuilder] struct {
// set permissions of next argument
// (--perms OCTAL)
Mode *os.FileMode `json:"mode,omitempty"`
// path to get the new permission
// (--bind-data, --file, etc.)
Inner T `json:"path"`
}
func (p *PermConfig[T]) Path() string { return p.Inner.Path() }
func (p *PermConfig[T]) Len() int {
if p.Mode != nil {
return p.Inner.Len() + 2
} else {
return p.Inner.Len()
}
}
func (p *PermConfig[T]) Append(args *[]string) {
if p.Mode != nil {
*args = append(*args, Perms.String(), strconv.FormatInt(int64(*p.Mode), 8))
}
p.Inner.Append(args)
}
type TmpfsConfig struct {
// set size of tmpfs
// (--size BYTES)
Size int `json:"size,omitempty"`
// mount point of new tmpfs
// (--tmpfs DEST)
Dir string `json:"dir"`
}
func (t *TmpfsConfig) Path() string { return t.Dir }
func (t *TmpfsConfig) Len() int {
if t.Size > 0 {
return 4
} else {
return 2
}
}
func (t *TmpfsConfig) Append(args *[]string) {
if t.Size > 0 {
*args = append(*args, Size.String(), strconv.Itoa(t.Size))
}
*args = append(*args, Tmpfs.String(), t.Dir)
}
type OverlayConfig struct {
/*
read files from SRC in the following overlay
(--overlay-src SRC)
*/
Src []string `json:"src,omitempty"`
/*
mount overlayfs on DEST, with RWSRC as the host path for writes and
WORKDIR an empty directory on the same filesystem as RWSRC
(--overlay RWSRC WORKDIR DEST)
if nil, mount overlayfs on DEST, with writes going to an invisible tmpfs
(--tmp-overlay DEST)
if either strings are empty, mount overlayfs read-only on DEST
(--ro-overlay DEST)
*/
Persist *[2]string `json:"persist,omitempty"`
/*
--overlay RWSRC WORKDIR DEST
--tmp-overlay DEST
--ro-overlay DEST
*/
Dest string `json:"dest"`
}
func (o *OverlayConfig) Path() string { return o.Dest }
func (o *OverlayConfig) Len() int {
// (--tmp-overlay DEST) or (--ro-overlay DEST)
p := 2
// (--overlay RWSRC WORKDIR DEST)
if o.Persist != nil && o.Persist[0] != "" && o.Persist[1] != "" {
p = 4
}
return p + len(o.Src)*2
}
func (o *OverlayConfig) Append(args *[]string) {
// --overlay-src SRC
for _, src := range o.Src {
*args = append(*args, OverlaySrc.String(), src)
}
if o.Persist != nil {
if o.Persist[0] != "" && o.Persist[1] != "" {
// --overlay RWSRC WORKDIR
*args = append(*args, Overlay.String(), o.Persist[0], o.Persist[1])
} else {
// --ro-overlay
*args = append(*args, ROOverlay.String())
}
} else {
// --tmp-overlay
*args = append(*args, TmpOverlay.String())
}
// DEST
*args = append(*args, o.Dest)
}
type SymlinkConfig [2]string
func (s SymlinkConfig) Path() string { return s[1] }
func (s SymlinkConfig) Len() int { return 3 }
func (s SymlinkConfig) Append(args *[]string) { *args = append(*args, Symlink.String(), s[0], s[1]) }
type ChmodConfig map[string]os.FileMode
func (c ChmodConfig) Len() int { return len(c) }
func (c ChmodConfig) Append(args *[]string) {
for path, mode := range c {
*args = append(*args, Chmod.String(), strconv.FormatInt(int64(mode), 8), path)
}
}
const (
DataWrite = iota
DataBind
DataROBind
)
type DataConfig struct {
Dest string `json:"dest"`
Data []byte `json:"data,omitempty"`
Type int `json:"type"`
proc.File
}
func (d *DataConfig) Path() string { return d.Dest }
func (d *DataConfig) Len() int {
if d == nil || d.Data == nil {
return 0
}
return 3
}
func (d *DataConfig) Init(fd uintptr, v **os.File) uintptr {
if d.File != nil {
panic("file initialised twice")
}
d.File = proc.NewWriterTo(d)
return d.File.Init(fd, v)
}
func (d *DataConfig) WriteTo(w io.Writer) (int64, error) {
n, err := w.Write(d.Data)
return int64(n), err
}
func (d *DataConfig) Append(args *[]string) {
if d == nil || d.Data == nil {
return
}
var a PositionalArg
switch d.Type {
case DataWrite:
a = File
case DataBind:
a = BindData
case DataROBind:
a = ROBindData
default:
panic(fmt.Sprintf("invalid type %d", a))
}
*args = append(*args, a.String(), strconv.Itoa(int(d.Fd())), d.Dest)
}

View File

@ -1,249 +0,0 @@
package bwrap
import (
"slices"
"strconv"
)
/*
static boolean args
*/
type BoolArg int
func (b BoolArg) Unwrap() []string {
return boolArgs[b]
}
const (
UnshareAll BoolArg = iota
UnshareUser
UnshareIPC
UnsharePID
UnshareNet
UnshareUTS
UnshareCGroup
ShareNet
UserNS
Clearenv
NewSession
DieWithParent
AsInit
)
var boolArgs = [...][]string{
UnshareAll: {"--unshare-all", "--unshare-user"},
UnshareUser: {"--unshare-user"},
UnshareIPC: {"--unshare-ipc"},
UnsharePID: {"--unshare-pid"},
UnshareNet: {"--unshare-net"},
UnshareUTS: {"--unshare-uts"},
UnshareCGroup: {"--unshare-cgroup"},
ShareNet: {"--share-net"},
UserNS: {"--disable-userns", "--assert-userns-disabled"},
Clearenv: {"--clearenv"},
NewSession: {"--new-session"},
DieWithParent: {"--die-with-parent"},
AsInit: {"--as-pid-1"},
}
func (c *Config) boolArgs() Builder {
b := boolArg{
UserNS: !c.UserNS,
Clearenv: c.Clearenv,
NewSession: c.NewSession,
DieWithParent: c.DieWithParent,
AsInit: c.AsInit,
}
if c.Unshare == nil {
b[UnshareAll] = true
b[ShareNet] = c.Net
} else {
b[UnshareUser] = c.Unshare.User
b[UnshareIPC] = c.Unshare.IPC
b[UnsharePID] = c.Unshare.PID
b[UnshareNet] = c.Unshare.Net
b[UnshareUTS] = c.Unshare.UTS
b[UnshareCGroup] = c.Unshare.CGroup
}
return &b
}
type boolArg [len(boolArgs)]bool
func (b *boolArg) Len() (l int) {
for i, v := range b {
if v {
l += len(boolArgs[i])
}
}
return
}
func (b *boolArg) Append(args *[]string) {
for i, v := range b {
if v {
*args = append(*args, BoolArg(i).Unwrap()...)
}
}
}
/*
static integer args
*/
type IntArg int
func (i IntArg) Unwrap() string {
return intArgs[i]
}
const (
UID IntArg = iota
GID
)
var intArgs = [...]string{
UID: "--uid",
GID: "--gid",
}
func (c *Config) intArgs() Builder {
return &intArg{
UID: c.UID,
GID: c.GID,
}
}
type intArg [len(intArgs)]*int
func (n *intArg) Len() (l int) {
for _, v := range n {
if v != nil {
l += 2
}
}
return
}
func (n *intArg) Append(args *[]string) {
for i, v := range n {
if v != nil {
*args = append(*args, IntArg(i).Unwrap(), strconv.Itoa(*v))
}
}
}
/*
static string args
*/
type StringArg int
func (s StringArg) Unwrap() string {
return stringArgs[s]
}
const (
Hostname StringArg = iota
Chdir
UnsetEnv
LockFile
)
var stringArgs = [...]string{
Hostname: "--hostname",
Chdir: "--chdir",
UnsetEnv: "--unsetenv",
LockFile: "--lock-file",
}
func (c *Config) stringArgs() Builder {
n := stringArg{
UnsetEnv: c.UnsetEnv,
LockFile: c.LockFile,
}
if c.Hostname != "" {
n[Hostname] = []string{c.Hostname}
}
if c.Chdir != "" {
n[Chdir] = []string{c.Chdir}
}
return &n
}
type stringArg [len(stringArgs)][]string
func (s *stringArg) Len() (l int) {
for _, arg := range s {
l += len(arg) * 2
}
return
}
func (s *stringArg) Append(args *[]string) {
for i, arg := range s {
for _, v := range arg {
*args = append(*args, StringArg(i).Unwrap(), v)
}
}
}
/*
static pair args
*/
type PairArg int
func (p PairArg) Unwrap() string {
return pairArgs[p]
}
const (
SetEnv PairArg = iota
)
var pairArgs = [...]string{
SetEnv: "--setenv",
}
func (c *Config) pairArgs() Builder {
var n pairArg
n[SetEnv] = make([][2]string, len(c.SetEnv))
keys := make([]string, 0, len(c.SetEnv))
for k := range c.SetEnv {
keys = append(keys, k)
}
slices.Sort(keys)
for i, k := range keys {
n[SetEnv][i] = [2]string{k, c.SetEnv[k]}
}
return &n
}
type pairArg [len(pairArgs)][][2]string
func (p *pairArg) Len() (l int) {
for _, v := range p {
l += len(v) * 3
}
return
}
func (p *pairArg) Append(args *[]string) {
for i, arg := range p {
for _, v := range arg {
*args = append(*args, PairArg(i).Unwrap(), v[0], v[1])
}
}
}

View File

@ -1,52 +0,0 @@
package bwrap
import (
"context"
"encoding/gob"
"os"
"strconv"
"git.gensokyo.uk/security/fortify/helper/proc"
)
func init() {
gob.Register(new(pairF))
gob.Register(new(stringF))
}
type pairF [3]string
func (p *pairF) Path() string { return p[2] }
func (p *pairF) Len() int { return len(p) }
func (p *pairF) Append(args *[]string) { *args = append(*args, p[0], p[1], p[2]) }
type stringF [2]string
func (s stringF) Path() string { return s[1] }
func (s stringF) Len() int { return len(s) /* compiler replaces this with 2 */ }
func (s stringF) Append(args *[]string) { *args = append(*args, s[0], s[1]) }
func newFile(name string, f *os.File) FDBuilder { return &fileF{name: name, file: f} }
type fileF struct {
name string
file *os.File
proc.BaseFile
}
func (f *fileF) ErrCount() int { return 0 }
func (f *fileF) Fulfill(_ context.Context, _ func(error)) error { f.Set(f.file); return nil }
func (f *fileF) Len() int {
if f.file == nil {
return 0
}
return 2
}
func (f *fileF) Append(args *[]string) {
if f.file == nil {
return
}
*args = append(*args, f.name, strconv.Itoa(int(f.Fd())))
}

View File

@ -1,103 +0,0 @@
package helper_test
import (
"context"
"errors"
"os"
"strings"
"testing"
"time"
"git.gensokyo.uk/security/fortify/helper"
"git.gensokyo.uk/security/fortify/helper/bwrap"
)
func TestBwrap(t *testing.T) {
sc := &bwrap.Config{
Net: true,
Hostname: "localhost",
Chdir: "/nonexistent",
Clearenv: true,
NewSession: true,
DieWithParent: true,
AsInit: true,
}
t.Run("nonexistent bwrap name", func(t *testing.T) {
bubblewrapName := helper.BubblewrapName
helper.BubblewrapName = "/nonexistent"
t.Cleanup(func() {
helper.BubblewrapName = bubblewrapName
})
h := helper.MustNewBwrap(
sc, "fortify",
argsWt, argF,
nil, nil,
)
if err := h.Start(context.Background(), false); !errors.Is(err, os.ErrNotExist) {
t.Errorf("Start: error = %v, wantErr %v",
err, os.ErrNotExist)
}
})
t.Run("valid new helper nil check", func(t *testing.T) {
if got := helper.MustNewBwrap(
sc, "fortify",
argsWt, argF,
nil, nil,
); got == nil {
t.Errorf("MustNewBwrap(%#v, %#v, %#v) got nil",
sc, argsWt, "fortify")
return
}
})
t.Run("invalid bwrap config new helper panic", func(t *testing.T) {
defer func() {
wantPanic := "argument contains null character"
if r := recover(); r != wantPanic {
t.Errorf("MustNewBwrap: panic = %q, want %q",
r, wantPanic)
}
}()
helper.MustNewBwrap(
&bwrap.Config{Hostname: "\x00"}, "fortify",
nil, argF,
nil, nil,
)
})
t.Run("start without pipes", func(t *testing.T) {
helper.InternalReplaceExecCommand(t)
h := helper.MustNewBwrap(
sc, "crash-test-dummy",
nil, argFChecked,
nil, nil,
)
stdout, stderr := new(strings.Builder), new(strings.Builder)
h.Stdout(stdout).Stderr(stderr)
c, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := h.Start(c, false); err != nil {
t.Errorf("Start: error = %v",
err)
return
}
if err := h.Wait(); err != nil {
t.Errorf("Wait() err = %v stderr = %s",
err, stderr)
}
})
t.Run("implementation compliance", func(t *testing.T) {
testHelper(t, func() helper.Helper { return helper.MustNewBwrap(sc, "crash-test-dummy", argsWt, argF, nil, nil) })
})
}

84
helper/cmd.go Normal file
View File

@ -0,0 +1,84 @@
package helper
import (
"context"
"errors"
"io"
"os"
"os/exec"
"slices"
"sync"
"syscall"
"git.gensokyo.uk/security/hakurei/helper/proc"
)
// NewDirect initialises a new direct Helper instance with wt as the null-terminated argument writer.
// Function argF returns an array of arguments passed directly to the child process.
func NewDirect(
ctx context.Context,
name string,
wt io.WriterTo,
stat bool,
argF func(argsFd, statFd int) []string,
cmdF func(cmd *exec.Cmd),
extraFiles []*os.File,
) Helper {
d, args := newHelperCmd(ctx, name, wt, stat, argF, extraFiles)
d.Args = append(d.Args, args...)
if cmdF != nil {
cmdF(d.Cmd)
}
return d
}
func newHelperCmd(
ctx context.Context,
name string,
wt io.WriterTo,
stat bool,
argF func(argsFd, statFd int) []string,
extraFiles []*os.File,
) (cmd *helperCmd, args []string) {
cmd = new(helperCmd)
cmd.helperFiles, args = newHelperFiles(ctx, wt, stat, argF, extraFiles)
cmd.Cmd = exec.CommandContext(ctx, name)
cmd.Cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGTERM) }
cmd.WaitDelay = WaitDelay
return
}
// helperCmd provides a [exec.Cmd] wrapper around helper ipc.
type helperCmd struct {
mu sync.RWMutex
*helperFiles
*exec.Cmd
}
func (h *helperCmd) Start() error {
h.mu.Lock()
defer h.mu.Unlock()
// Check for doubled Start calls before we defer failure cleanup. If the prior
// call to Start succeeded, we don't want to spuriously close its pipes.
if h.Cmd != nil && h.Cmd.Process != nil {
return errors.New("helper: already started")
}
h.Env = slices.Grow(h.Env, 2)
if h.useArgsFd {
h.Env = append(h.Env, HakureiHelper+"=1")
} else {
h.Env = append(h.Env, HakureiHelper+"=0")
}
if h.useStatFd {
h.Env = append(h.Env, HakureiStatus+"=1")
// stat is populated on fulfill
h.Cancel = func() error { return h.stat.Close() }
} else {
h.Env = append(h.Env, HakureiStatus+"=0")
}
return proc.Fulfill(h.helperFiles.ctx, &h.ExtraFiles, h.Cmd.Start, h.files, h.extraFiles)
}

39
helper/cmd_test.go Normal file
View File

@ -0,0 +1,39 @@
package helper_test
import (
"context"
"errors"
"io"
"os"
"os/exec"
"testing"
"git.gensokyo.uk/security/hakurei/helper"
)
func TestCmd(t *testing.T) {
t.Run("start non-existent helper path", func(t *testing.T) {
h := helper.NewDirect(t.Context(), "/proc/nonexistent", argsWt, false, argF, nil, nil)
if err := h.Start(); !errors.Is(err, os.ErrNotExist) {
t.Errorf("Start: error = %v, wantErr %v",
err, os.ErrNotExist)
}
})
t.Run("valid new helper nil check", func(t *testing.T) {
if got := helper.NewDirect(t.Context(), "hakurei", argsWt, false, argF, nil, nil); got == nil {
t.Errorf("NewDirect(%q, %q) got nil",
argsWt, "hakurei")
return
}
})
t.Run("implementation compliance", func(t *testing.T) {
testHelper(t, func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper {
return helper.NewDirect(ctx, os.Args[0], argsWt, stat, argF, func(cmd *exec.Cmd) {
setOutput(&cmd.Stdout, &cmd.Stderr)
}, nil)
})
})
}

76
helper/container.go Normal file
View File

@ -0,0 +1,76 @@
package helper
import (
"context"
"errors"
"io"
"os"
"os/exec"
"slices"
"sync"
"git.gensokyo.uk/security/hakurei/helper/proc"
"git.gensokyo.uk/security/hakurei/sandbox"
)
// New initialises a Helper instance with wt as the null-terminated argument writer.
func New(
ctx context.Context,
name string,
wt io.WriterTo,
stat bool,
argF func(argsFd, statFd int) []string,
cmdF func(container *sandbox.Container),
extraFiles []*os.File,
) Helper {
var args []string
h := new(helperContainer)
h.helperFiles, args = newHelperFiles(ctx, wt, stat, argF, extraFiles)
h.Container = sandbox.New(ctx, name, args...)
h.WaitDelay = WaitDelay
if cmdF != nil {
cmdF(h.Container)
}
return h
}
// helperContainer provides a [sandbox.Container] wrapper around helper ipc.
type helperContainer struct {
started bool
mu sync.Mutex
*helperFiles
*sandbox.Container
}
func (h *helperContainer) Start() error {
h.mu.Lock()
defer h.mu.Unlock()
if h.started {
return errors.New("helper: already started")
}
h.started = true
h.Env = slices.Grow(h.Env, 2)
if h.useArgsFd {
h.Env = append(h.Env, HakureiHelper+"=1")
} else {
h.Env = append(h.Env, HakureiHelper+"=0")
}
if h.useStatFd {
h.Env = append(h.Env, HakureiStatus+"=1")
// stat is populated on fulfill
h.Cancel = func(*exec.Cmd) error { return h.stat.Close() }
} else {
h.Env = append(h.Env, HakureiStatus+"=0")
}
return proc.Fulfill(h.helperFiles.ctx, &h.ExtraFiles, func() error {
if err := h.Container.Start(); err != nil {
return err
}
return h.Container.Serve()
}, h.files, h.extraFiles)
}

57
helper/container_test.go Normal file
View File

@ -0,0 +1,57 @@
package helper_test
import (
"context"
"io"
"os"
"os/exec"
"testing"
"git.gensokyo.uk/security/hakurei/helper"
"git.gensokyo.uk/security/hakurei/internal"
"git.gensokyo.uk/security/hakurei/internal/hlog"
"git.gensokyo.uk/security/hakurei/sandbox"
)
func TestContainer(t *testing.T) {
t.Run("start empty container", func(t *testing.T) {
h := helper.New(t.Context(), "/nonexistent", argsWt, false, argF, nil, nil)
wantErr := "sandbox: starting an empty container"
if err := h.Start(); err == nil || err.Error() != wantErr {
t.Errorf("Start: error = %v, wantErr %q",
err, wantErr)
}
})
t.Run("valid new helper nil check", func(t *testing.T) {
if got := helper.New(t.Context(), "hakurei", argsWt, false, argF, nil, nil); got == nil {
t.Errorf("New(%q, %q) got nil",
argsWt, "hakurei")
return
}
})
t.Run("implementation compliance", func(t *testing.T) {
testHelper(t, func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper {
return helper.New(ctx, os.Args[0], argsWt, stat, argF, func(container *sandbox.Container) {
setOutput(&container.Stdout, &container.Stderr)
container.CommandContext = func(ctx context.Context) (cmd *exec.Cmd) {
return exec.CommandContext(ctx, os.Args[0], "-test.v",
"-test.run=TestHelperInit", "--", "init")
}
container.Bind("/", "/", 0)
container.Proc("/proc")
container.Dev("/dev")
}, nil)
})
})
}
func TestHelperInit(t *testing.T) {
if len(os.Args) != 5 || os.Args[4] != "init" {
return
}
sandbox.SetOutput(hlog.Output{})
sandbox.Init(hlog.Prepare, func(bool) { internal.InstallFmsg(false) })
}

View File

@ -1,40 +0,0 @@
package helper
import (
"context"
"errors"
"io"
"sync"
"git.gensokyo.uk/security/fortify/helper/proc"
)
// direct wraps *exec.Cmd and manages status and args fd.
// Args is always 3 and status if set is always 4.
type direct struct {
lock sync.RWMutex
*helperCmd
}
func (h *direct) Start(ctx context.Context, stat bool) error {
h.lock.Lock()
defer h.lock.Unlock()
// Check for doubled Start calls before we defer failure cleanup. If the prior
// call to Start succeeded, we don't want to spuriously close its pipes.
if h.Cmd != nil && h.Cmd.Process != nil {
return errors.New("exec: already started")
}
args := h.finalise(ctx, stat)
h.Cmd.Args = append(h.Cmd.Args, args...)
return proc.Fulfill(ctx, h.Cmd, h.files, h.extraFiles)
}
// New initialises a new direct Helper instance with wt as the null-terminated argument writer.
// Function argF returns an array of arguments passed directly to the child process.
func New(wt io.WriterTo, name string, argF func(argsFd, statFd int) []string) Helper {
d := new(direct)
d.helperCmd = newHelperCmd(d, name, wt, argF, nil)
return d
}

View File

@ -1,33 +0,0 @@
package helper_test
import (
"context"
"errors"
"os"
"testing"
"git.gensokyo.uk/security/fortify/helper"
)
func TestDirect(t *testing.T) {
t.Run("start non-existent helper path", func(t *testing.T) {
h := helper.New(argsWt, "/nonexistent", argF)
if err := h.Start(context.Background(), false); !errors.Is(err, os.ErrNotExist) {
t.Errorf("Start: error = %v, wantErr %v",
err, os.ErrNotExist)
}
})
t.Run("valid new helper nil check", func(t *testing.T) {
if got := helper.New(argsWt, "fortify", argF); got == nil {
t.Errorf("New(%q, %q) got nil",
argsWt, "fortify")
return
}
})
t.Run("implementation compliance", func(t *testing.T) {
testHelper(t, func() helper.Helper { return helper.New(argsWt, "crash-test-dummy", argF) })
})
}

View File

@ -1,4 +1,4 @@
// Package helper runs external helpers with optional sandboxing and manages their status/args pipes.
// Package helper runs external helpers with optional sandboxing.
package helper
import (
@ -6,82 +6,71 @@ import (
"fmt"
"io"
"os"
"os/exec"
"slices"
"syscall"
"time"
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/hakurei/helper/proc"
)
var (
WaitDelay = 2 * time.Second
)
var WaitDelay = 2 * time.Second
const (
// FortifyHelper is set to 1 when args fd is enabled and 0 otherwise.
FortifyHelper = "FORTIFY_HELPER"
// FortifyStatus is set to 1 when stat fd is enabled and 0 otherwise.
FortifyStatus = "FORTIFY_STATUS"
// HakureiHelper is set to 1 when args fd is enabled and 0 otherwise.
HakureiHelper = "HAKUREI_HELPER"
// HakureiStatus is set to 1 when stat fd is enabled and 0 otherwise.
HakureiStatus = "HAKUREI_STATUS"
)
type Helper interface {
// Stdin sets the standard input of Helper.
Stdin(r io.Reader) Helper
// Stdout sets the standard output of Helper.
Stdout(w io.Writer) Helper
// Stderr sets the standard error of Helper.
Stderr(w io.Writer) Helper
// SetEnv sets the environment of Helper.
SetEnv(env []string) Helper
// Start starts the helper process.
// A status pipe is passed to the helper if stat is true.
Start(ctx context.Context, stat bool) error
// Wait blocks until Helper exits and releases all its resources.
Start() error
// Wait blocks until Helper exits.
Wait() error
fmt.Stringer
}
func newHelperCmd(
h Helper, name string,
wt io.WriterTo, argF func(argsFd, statFd int) []string,
func newHelperFiles(
ctx context.Context,
wt io.WriterTo,
stat bool,
argF func(argsFd, statFd int) []string,
extraFiles []*os.File,
) (cmd *helperCmd) {
cmd = new(helperCmd)
) (hl *helperFiles, args []string) {
hl = new(helperFiles)
hl.ctx = ctx
hl.useArgsFd = wt != nil
hl.useStatFd = stat
cmd.r = h
cmd.name = name
cmd.extraFiles = new(proc.ExtraFilesPre)
hl.extraFiles = new(proc.ExtraFilesPre)
for _, f := range extraFiles {
_, v := cmd.extraFiles.Append()
_, v := hl.extraFiles.Append()
*v = f
}
argsFd := -1
if wt != nil {
if hl.useArgsFd {
f := proc.NewWriterTo(wt)
argsFd = int(proc.InitFile(f, cmd.extraFiles))
cmd.files = append(cmd.files, f)
cmd.hasArgsFd = true
argsFd = int(proc.InitFile(f, hl.extraFiles))
hl.files = append(hl.files, f)
}
cmd.argF = func(statFd int) []string { return argF(argsFd, statFd) }
statFd := -1
if hl.useStatFd {
f := proc.NewStat(&hl.stat)
statFd = int(proc.InitFile(f, hl.extraFiles))
hl.files = append(hl.files, f)
}
args = argF(argsFd, statFd)
return
}
// helperCmd wraps Cmd and implements methods shared across all Helper implementations.
type helperCmd struct {
// ref to parent
r Helper
// returns an array of arguments passed directly
// to the helper process
argF func(statFd int) []string
// helperFiles provides a generic wrapper around helper ipc.
type helperFiles struct {
// whether argsFd is present
hasArgsFd bool
useArgsFd bool
// whether statFd is present
useStatFd bool
// closes statFd
stat io.Closer
@ -90,45 +79,5 @@ type helperCmd struct {
// passed through to [proc.Fulfill] and [proc.InitFile]
extraFiles *proc.ExtraFilesPre
name string
stdin io.Reader
stdout, stderr io.Writer
env []string
*exec.Cmd
ctx context.Context
}
func (h *helperCmd) Stdin(r io.Reader) Helper { h.stdin = r; return h.r }
func (h *helperCmd) Stdout(w io.Writer) Helper { h.stdout = w; return h.r }
func (h *helperCmd) Stderr(w io.Writer) Helper { h.stderr = w; return h.r }
func (h *helperCmd) SetEnv(env []string) Helper { h.env = env; return h.r }
// finalise initialises the underlying [exec.Cmd] object.
func (h *helperCmd) finalise(ctx context.Context, stat bool) (args []string) {
h.Cmd = commandContext(ctx, h.name)
h.Cmd.Stdin, h.Cmd.Stdout, h.Cmd.Stderr = h.stdin, h.stdout, h.stderr
h.Cmd.Env = slices.Grow(h.env, 2)
if h.hasArgsFd {
h.Cmd.Env = append(h.Cmd.Env, FortifyHelper+"=1")
} else {
h.Cmd.Env = append(h.Cmd.Env, FortifyHelper+"=0")
}
h.Cmd.Cancel = func() error { return h.Cmd.Process.Signal(syscall.SIGTERM) }
h.Cmd.WaitDelay = WaitDelay
statFd := -1
if stat {
f := proc.NewStat(&h.stat)
statFd = int(proc.InitFile(f, h.extraFiles))
h.files = append(h.files, f)
h.Cmd.Env = append(h.Cmd.Env, FortifyStatus+"=1")
// stat is populated on fulfill
h.Cmd.Cancel = func() error { return h.stat.Close() }
} else {
h.Cmd.Env = append(h.Cmd.Env, FortifyStatus+"=0")
}
return h.argF(statFd)
}
var commandContext = exec.CommandContext

View File

@ -4,18 +4,20 @@ import (
"context"
"errors"
"fmt"
"io"
"os"
"strconv"
"strings"
"testing"
"time"
"git.gensokyo.uk/security/fortify/helper"
"git.gensokyo.uk/security/hakurei/helper"
)
var (
wantArgs = []string{
"unix:path=/run/dbus/system_bus_socket",
"/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/system_bus_socket",
"/tmp/hakurei.1971/12622d846cc3fe7b4c10359d01f0eb47/system_bus_socket",
"--filter",
"--talk=org.bluez",
"--talk=org.freedesktop.Avahi",
@ -35,7 +37,8 @@ func argF(argsFd, statFd int) []string {
}
func argFChecked(argsFd, statFd int) (args []string) {
args = make([]string, 0, 4)
args = make([]string, 0, 6)
args = append(args, "-test.run=TestHelperStub", "--")
if argsFd > -1 {
args = append(args, "--args", strconv.Itoa(argsFd))
}
@ -46,14 +49,15 @@ func argFChecked(argsFd, statFd int) (args []string) {
}
// this function tests an implementation of the helper.Helper interface
func testHelper(t *testing.T, createHelper func() helper.Helper) {
helper.InternalReplaceExecCommand(t)
func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper) {
oldWaitDelay := helper.WaitDelay
helper.WaitDelay = 16 * time.Second
t.Cleanup(func() { helper.WaitDelay = oldWaitDelay })
t.Run("start helper with status channel and wait", func(t *testing.T) {
h := createHelper()
stdout, stderr := new(strings.Builder), new(strings.Builder)
h.Stdout(stdout).Stderr(stderr)
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
stdout := new(strings.Builder)
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, os.Stderr }, true)
t.Run("wait not yet started helper", func(t *testing.T) {
defer func() {
@ -65,10 +69,8 @@ func testHelper(t *testing.T, createHelper func() helper.Helper) {
panic(fmt.Sprintf("unreachable: %v", h.Wait()))
})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
t.Log("starting helper stub")
if err := h.Start(ctx, true); err != nil {
if err := h.Start(); err != nil {
t.Errorf("Start: error = %v", err)
cancel()
return
@ -77,8 +79,8 @@ func testHelper(t *testing.T, createHelper func() helper.Helper) {
cancel()
t.Run("start already started helper", func(t *testing.T) {
wantErr := "exec: already started"
if err := h.Start(ctx, true); err != nil && err.Error() != wantErr {
wantErr := "helper: already started"
if err := h.Start(); err != nil && err.Error() != wantErr {
t.Errorf("Start: error = %v, wantErr %v",
err, wantErr)
return
@ -87,8 +89,8 @@ func testHelper(t *testing.T, createHelper func() helper.Helper) {
t.Log("waiting on helper")
if err := h.Wait(); !errors.Is(err, context.Canceled) {
t.Errorf("Wait() err = %v stderr = %s",
err, stderr)
t.Errorf("Wait: error = %v",
err)
}
t.Run("wait already finalised helper", func(t *testing.T) {
@ -100,34 +102,36 @@ func testHelper(t *testing.T, createHelper func() helper.Helper) {
}
})
if got := stdout.String(); !strings.HasPrefix(got, wantPayload) {
t.Errorf("Start: stdout = %v, want %v",
if got := trimStdout(stdout); got != wantPayload {
t.Errorf("Start: stdout = %q, want %q",
got, wantPayload)
}
})
t.Run("start helper and wait", func(t *testing.T) {
h := createHelper()
stdout, stderr := new(strings.Builder), new(strings.Builder)
h.Stdout(stdout).Stderr(stderr)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
stdout := new(strings.Builder)
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, os.Stderr }, false)
if err := h.Start(ctx, false); err != nil {
t.Errorf("Start() error = %v",
if err := h.Start(); err != nil {
t.Errorf("Start: error = %v",
err)
return
}
if err := h.Wait(); err != nil {
t.Errorf("Wait() err = %v stdout = %s stderr = %s",
err, stdout, stderr)
t.Errorf("Wait: error = %v stdout = %q",
err, stdout)
}
if got := stdout.String(); !strings.HasPrefix(got, wantPayload) {
t.Errorf("Start() stdout = %v, want %v",
if got := trimStdout(stdout); got != wantPayload {
t.Errorf("Start: stdout = %q, want %q",
got, wantPayload)
}
})
}
func trimStdout(stdout fmt.Stringer) string {
return strings.TrimPrefix(stdout.String(), "=== RUN TestHelperInit\n")
}

View File

@ -60,7 +60,10 @@ func (f *ExtraFilesPre) copy(e []*os.File) []*os.File {
}
// Fulfill calls the [File.Fulfill] method on all files, starts cmd and blocks until all fulfillment completes.
func Fulfill(ctx context.Context, cmd *exec.Cmd, files []File, extraFiles *ExtraFilesPre) (err error) {
func Fulfill(ctx context.Context,
v *[]*os.File, start func() error,
files []File, extraFiles *ExtraFilesPre,
) (err error) {
var ecs int
for _, o := range files {
ecs += o.ErrCount()
@ -77,8 +80,8 @@ func Fulfill(ctx context.Context, cmd *exec.Cmd, files []File, extraFiles *Extra
}
}
cmd.ExtraFiles = extraFiles.Files()
if err = cmd.Start(); err != nil {
*v = extraFiles.Files()
if err = start(); err != nil {
return
}

View File

@ -5,6 +5,7 @@ import (
"errors"
"io"
"os"
"runtime"
)
// NewWriterTo returns a [File] that receives content from wt on fulfillment.
@ -25,13 +26,20 @@ func (f *writeToFile) Fulfill(ctx context.Context, dispatchErr func(error)) erro
f.Set(r)
done := make(chan struct{})
go func() { _, err = f.wt.WriteTo(w); dispatchErr(err); dispatchErr(w.Close()); close(done) }()
go func() {
_, err = f.wt.WriteTo(w)
dispatchErr(err)
dispatchErr(w.Close())
close(done)
runtime.KeepAlive(r)
}()
go func() {
select {
case <-done:
dispatchErr(nil)
case <-ctx.Done():
dispatchErr(w.Close()) // this aborts WriteTo with file already closed
runtime.KeepAlive(r)
}
}()
@ -83,6 +91,7 @@ func (f *statFile) Fulfill(ctx context.Context, dispatchErr func(error)) error {
default:
panic("unreachable")
}
runtime.KeepAlive(w)
}()
go func() {
@ -91,6 +100,7 @@ func (f *statFile) Fulfill(ctx context.Context, dispatchErr func(error)) error {
dispatchErr(nil)
case <-ctx.Done():
dispatchErr(r.Close()) // this aborts Read with file already closed
runtime.KeepAlive(w)
}
}()

View File

@ -1,300 +0,0 @@
#ifndef _GNU_SOURCE
#define _GNU_SOURCE // CLONE_NEWUSER
#endif
#include "seccomp-export.h"
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
#include <errno.h>
#include <sys/syscall.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/personality.h>
#include <sched.h>
#if (SCMP_VER_MAJOR < 2) || \
(SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR < 5) || \
(SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR == 5 && SCMP_VER_MICRO < 1)
#error This package requires libseccomp >= v2.5.1
#endif
struct f_syscall_act {
int syscall;
int m_errno;
struct scmp_arg_cmp *arg;
};
#define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
#define SECCOMP_RULESET_ADD(ruleset) do { \
if (opts & F_VERBOSE) F_println("adding seccomp ruleset \"" #ruleset "\""); \
for (int i = 0; i < LEN(ruleset); i++) { \
assert(ruleset[i].m_errno == EPERM || ruleset[i].m_errno == ENOSYS); \
\
if (ruleset[i].arg) \
ret = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ruleset[i].m_errno), ruleset[i].syscall, 1, *ruleset[i].arg); \
else \
ret = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(ruleset[i].m_errno), ruleset[i].syscall, 0); \
\
if (ret == -EFAULT) { \
res = 4; \
goto out; \
} else if (ret < 0) { \
res = 5; \
errno = -ret; \
goto out; \
} \
} \
} while (0)
int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts opts) {
int32_t res = 0; // refer to resErr for meaning
int allow_multiarch = opts & F_MULTIARCH;
int allowed_personality = PER_LINUX;
if (opts & F_LINUX32)
allowed_personality = PER_LINUX32;
// flatpak commit 4c3bf179e2e4a2a298cd1db1d045adaf3f564532
struct f_syscall_act deny_common[] = {
// Block dmesg
{SCMP_SYS(syslog), EPERM},
// Useless old syscall
{SCMP_SYS(uselib), EPERM},
// Don't allow disabling accounting
{SCMP_SYS(acct), EPERM},
// Don't allow reading current quota use
{SCMP_SYS(quotactl), EPERM},
// Don't allow access to the kernel keyring
{SCMP_SYS(add_key), EPERM},
{SCMP_SYS(keyctl), EPERM},
{SCMP_SYS(request_key), EPERM},
// Scary VM/NUMA ops
{SCMP_SYS(move_pages), EPERM},
{SCMP_SYS(mbind), EPERM},
{SCMP_SYS(get_mempolicy), EPERM},
{SCMP_SYS(set_mempolicy), EPERM},
{SCMP_SYS(migrate_pages), EPERM},
};
// fortify: project-specific extensions
struct f_syscall_act deny_common_ext[] = {
// system calls for changing the system clock
{SCMP_SYS(adjtimex), EPERM},
{SCMP_SYS(clock_adjtime), EPERM},
{SCMP_SYS(clock_adjtime64), EPERM},
{SCMP_SYS(clock_settime), EPERM},
{SCMP_SYS(clock_settime64), EPERM},
{SCMP_SYS(settimeofday), EPERM},
// loading and unloading of kernel modules
{SCMP_SYS(delete_module), EPERM},
{SCMP_SYS(finit_module), EPERM},
{SCMP_SYS(init_module), EPERM},
// system calls for rebooting and reboot preparation
{SCMP_SYS(kexec_file_load), EPERM},
{SCMP_SYS(kexec_load), EPERM},
{SCMP_SYS(reboot), EPERM},
// system calls for enabling/disabling swap devices
{SCMP_SYS(swapoff), EPERM},
{SCMP_SYS(swapon), EPERM},
};
struct f_syscall_act deny_ns[] = {
// Don't allow subnamespace setups:
{SCMP_SYS(unshare), EPERM},
{SCMP_SYS(setns), EPERM},
{SCMP_SYS(mount), EPERM},
{SCMP_SYS(umount), EPERM},
{SCMP_SYS(umount2), EPERM},
{SCMP_SYS(pivot_root), EPERM},
{SCMP_SYS(chroot), EPERM},
#if defined(__s390__) || defined(__s390x__) || defined(__CRIS__)
// Architectures with CONFIG_CLONE_BACKWARDS2: the child stack
// and flags arguments are reversed so the flags come second
{SCMP_SYS(clone), EPERM, &SCMP_A1(SCMP_CMP_MASKED_EQ, CLONE_NEWUSER, CLONE_NEWUSER)},
#else
// Normally the flags come first
{SCMP_SYS(clone), EPERM, &SCMP_A0(SCMP_CMP_MASKED_EQ, CLONE_NEWUSER, CLONE_NEWUSER)},
#endif
// seccomp can't look into clone3()'s struct clone_args to check whether
// the flags are OK, so we have no choice but to block clone3().
// Return ENOSYS so user-space will fall back to clone().
// (CVE-2021-41133; see also https://github.com/moby/moby/commit/9f6b562d)
{SCMP_SYS(clone3), ENOSYS},
// New mount manipulation APIs can also change our VFS. There's no
// legitimate reason to do these in the sandbox, so block all of them
// rather than thinking about which ones might be dangerous.
// (CVE-2021-41133)
{SCMP_SYS(open_tree), ENOSYS},
{SCMP_SYS(move_mount), ENOSYS},
{SCMP_SYS(fsopen), ENOSYS},
{SCMP_SYS(fsconfig), ENOSYS},
{SCMP_SYS(fsmount), ENOSYS},
{SCMP_SYS(fspick), ENOSYS},
{SCMP_SYS(mount_setattr), ENOSYS},
};
// fortify: project-specific extensions
struct f_syscall_act deny_ns_ext[] = {
// changing file ownership
{SCMP_SYS(chown), EPERM},
{SCMP_SYS(chown32), EPERM},
{SCMP_SYS(fchown), EPERM},
{SCMP_SYS(fchown32), EPERM},
{SCMP_SYS(fchownat), EPERM},
{SCMP_SYS(lchown), EPERM},
{SCMP_SYS(lchown32), EPERM},
// system calls for changing user ID and group ID credentials
{SCMP_SYS(setgid), EPERM},
{SCMP_SYS(setgid32), EPERM},
{SCMP_SYS(setgroups), EPERM},
{SCMP_SYS(setgroups32), EPERM},
{SCMP_SYS(setregid), EPERM},
{SCMP_SYS(setregid32), EPERM},
{SCMP_SYS(setresgid), EPERM},
{SCMP_SYS(setresgid32), EPERM},
{SCMP_SYS(setresuid), EPERM},
{SCMP_SYS(setresuid32), EPERM},
{SCMP_SYS(setreuid), EPERM},
{SCMP_SYS(setreuid32), EPERM},
{SCMP_SYS(setuid), EPERM},
{SCMP_SYS(setuid32), EPERM},
};
struct f_syscall_act deny_tty[] = {
// Don't allow faking input to the controlling tty (CVE-2017-5226)
{SCMP_SYS(ioctl), EPERM, &SCMP_A1(SCMP_CMP_MASKED_EQ, 0xFFFFFFFFu, (int)TIOCSTI)},
// In the unlikely event that the controlling tty is a Linux virtual
// console (/dev/tty2 or similar), copy/paste operations have an effect
// similar to TIOCSTI (CVE-2023-28100)
{SCMP_SYS(ioctl), EPERM, &SCMP_A1(SCMP_CMP_MASKED_EQ, 0xFFFFFFFFu, (int)TIOCLINUX)},
};
struct f_syscall_act deny_devel[] = {
// Profiling operations; we expect these to be done by tools from outside
// the sandbox. In particular perf has been the source of many CVEs.
{SCMP_SYS(perf_event_open), EPERM},
// Don't allow you to switch to bsd emulation or whatnot
{SCMP_SYS(personality), EPERM, &SCMP_A0(SCMP_CMP_NE, allowed_personality)},
{SCMP_SYS(ptrace), EPERM}
};
struct f_syscall_act deny_emu[] = {
// modify_ldt is a historic source of interesting information leaks,
// so it's disabled as a hardening measure.
// However, it is required to run old 16-bit applications
// as well as some Wine patches, so it's allowed in multiarch.
{SCMP_SYS(modify_ldt), EPERM},
};
// fortify: project-specific extensions
struct f_syscall_act deny_emu_ext[] = {
{SCMP_SYS(subpage_prot), ENOSYS},
{SCMP_SYS(switch_endian), ENOSYS},
{SCMP_SYS(vm86), ENOSYS},
{SCMP_SYS(vm86old), ENOSYS},
};
// Blocklist all but unix, inet, inet6 and netlink
struct
{
int family;
f_syscall_opts flags_mask;
} socket_family_allowlist[] = {
// NOTE: Keep in numerical order
{ AF_UNSPEC, 0 },
{ AF_LOCAL, 0 },
{ AF_INET, 0 },
{ AF_INET6, 0 },
{ AF_NETLINK, 0 },
{ AF_CAN, F_CAN },
{ AF_BLUETOOTH, F_BLUETOOTH },
};
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
if (ctx == NULL) {
res = 1;
goto out;
} else
errno = 0;
int ret;
// We only really need to handle arches on multiarch systems.
// If only one arch is supported the default is fine
if (arch != 0) {
// This *adds* the target arch, instead of replacing the
// native one. This is not ideal, because we'd like to only
// allow the target arch, but we can't really disallow the
// native arch at this point, because then bubblewrap
// couldn't continue running.
ret = seccomp_arch_add(ctx, arch);
if (ret < 0 && ret != -EEXIST) {
res = 2;
errno = -ret;
goto out;
}
if (allow_multiarch && multiarch != 0) {
ret = seccomp_arch_add(ctx, multiarch);
if (ret < 0 && ret != -EEXIST) {
res = 3;
errno = -ret;
goto out;
}
}
}
SECCOMP_RULESET_ADD(deny_common);
if (opts & F_DENY_NS) SECCOMP_RULESET_ADD(deny_ns);
if (opts & F_DENY_TTY) SECCOMP_RULESET_ADD(deny_tty);
if (opts & F_DENY_DEVEL) SECCOMP_RULESET_ADD(deny_devel);
if (!allow_multiarch) SECCOMP_RULESET_ADD(deny_emu);
if (opts & F_EXT) {
SECCOMP_RULESET_ADD(deny_common_ext);
if (opts & F_DENY_NS) SECCOMP_RULESET_ADD(deny_ns_ext);
if (!allow_multiarch) SECCOMP_RULESET_ADD(deny_emu_ext);
}
// Socket filtering doesn't work on e.g. i386, so ignore failures here
// However, we need to user seccomp_rule_add_exact to avoid libseccomp doing
// something else: https://github.com/seccomp/libseccomp/issues/8
int last_allowed_family = -1;
for (int i = 0; i < LEN(socket_family_allowlist); i++) {
if (socket_family_allowlist[i].flags_mask != 0 &&
(socket_family_allowlist[i].flags_mask & opts) != socket_family_allowlist[i].flags_mask)
continue;
for (int disallowed = last_allowed_family + 1; disallowed < socket_family_allowlist[i].family; disallowed++) {
// Blocklist the in-between valid families
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_EQ, disallowed));
}
last_allowed_family = socket_family_allowlist[i].family;
}
// Blocklist the rest
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1));
ret = seccomp_export_bpf(ctx, fd);
if (ret != 0) {
res = 6;
errno = -ret;
goto out;
}
out:
if (ctx)
seccomp_release(ctx);
return res;
}

View File

@ -1,23 +0,0 @@
#include <stdint.h>
#include <seccomp.h>
#if (SCMP_VER_MAJOR < 2) || \
(SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR < 5) || \
(SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR == 5 && SCMP_VER_MICRO < 1)
#error This package requires libseccomp >= v2.5.1
#endif
typedef enum {
F_VERBOSE = 1 << 0,
F_EXT = 1 << 1,
F_DENY_NS = 1 << 2,
F_DENY_TTY = 1 << 3,
F_DENY_DEVEL = 1 << 4,
F_MULTIARCH = 1 << 5,
F_LINUX32 = 1 << 6,
F_CAN = 1 << 7,
F_BLUETOOTH = 1 << 8,
} f_syscall_opts;
extern void F_println(char *v);
int32_t f_export_bpf(int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts opts);

View File

@ -1,88 +0,0 @@
package seccomp
/*
#cgo linux pkg-config: --static libseccomp
#include "seccomp-export.h"
*/
import "C"
import (
"errors"
"fmt"
"runtime"
)
var CPrintln func(v ...any)
var resErr = [...]error{
0: nil,
1: errors.New("seccomp_init failed"),
2: errors.New("seccomp_arch_add failed"),
3: errors.New("seccomp_arch_add failed (multiarch)"),
4: errors.New("internal libseccomp failure"),
5: errors.New("seccomp_rule_add failed"),
6: errors.New("seccomp_export_bpf failed"),
}
type SyscallOpts = C.f_syscall_opts
const (
flagVerbose SyscallOpts = C.F_VERBOSE
// FlagExt are project-specific extensions.
FlagExt SyscallOpts = C.F_EXT
// FlagDenyNS denies namespace setup syscalls.
FlagDenyNS SyscallOpts = C.F_DENY_NS
// FlagDenyTTY denies faking input.
FlagDenyTTY SyscallOpts = C.F_DENY_TTY
// FlagDenyDevel denies development-related syscalls.
FlagDenyDevel SyscallOpts = C.F_DENY_DEVEL
// FlagMultiarch allows multiarch/emulation.
FlagMultiarch SyscallOpts = C.F_MULTIARCH
// FlagLinux32 sets PER_LINUX32.
FlagLinux32 SyscallOpts = C.F_LINUX32
// FlagCan allows AF_CAN.
FlagCan SyscallOpts = C.F_CAN
// FlagBluetooth allows AF_BLUETOOTH.
FlagBluetooth SyscallOpts = C.F_BLUETOOTH
)
func exportFilter(fd uintptr, opts SyscallOpts) error {
var (
arch C.uint32_t = 0
multiarch C.uint32_t = 0
)
switch runtime.GOARCH {
case "386":
arch = C.SCMP_ARCH_X86
case "amd64":
arch = C.SCMP_ARCH_X86_64
multiarch = C.SCMP_ARCH_X86
case "arm":
arch = C.SCMP_ARCH_ARM
case "arm64":
arch = C.SCMP_ARCH_AARCH64
multiarch = C.SCMP_ARCH_ARM
}
// this removes repeated transitions between C and Go execution
// when producing log output via F_println and CPrintln is nil
if CPrintln != nil {
opts |= flagVerbose
}
res, err := C.f_export_bpf(C.int(fd), arch, multiarch, opts)
if re := resErr[res]; re != nil {
if err == nil {
return re
}
return fmt.Errorf("%s: %v", re.Error(), err)
}
return err
}
//export F_println
func F_println(v *C.char) {
if CPrintln != nil {
CPrintln(C.GoString(v))
}
}

View File

@ -1,62 +1,33 @@
package helper
import (
"context"
"flag"
"fmt"
"io"
"os"
"os/exec"
"strconv"
"strings"
"syscall"
"testing"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/internal"
)
// InternalChildStub is an internal function but exported because it is cross-package;
// InternalHelperStub is an internal function but exported because it is cross-package;
// it is part of the implementation of the helper stub.
func InternalChildStub() {
func InternalHelperStub() {
// this test mocks the helper process
var ap, sp string
if v, ok := os.LookupEnv(FortifyHelper); !ok {
if v, ok := os.LookupEnv(HakureiHelper); !ok {
return
} else {
ap = v
}
if v, ok := os.LookupEnv(FortifyStatus); !ok {
panic(FortifyStatus)
if v, ok := os.LookupEnv(HakureiStatus); !ok {
panic(HakureiStatus)
} else {
sp = v
}
switch os.Args[3] {
case "bwrap":
bwrapStub()
default:
genericStub(flagRestoreFiles(4, ap, sp))
}
genericStub(flagRestoreFiles(3, ap, sp))
internal.Exit(0)
}
// InternalReplaceExecCommand is an internal function but exported because it is cross-package;
// it is part of the implementation of the helper stub.
func InternalReplaceExecCommand(t *testing.T) {
t.Cleanup(func() { commandContext = exec.CommandContext })
// replace execCommand to have the resulting *exec.Cmd launch TestHelperChildStub
commandContext = func(ctx context.Context, name string, arg ...string) *exec.Cmd {
// pass through nonexistent path
if name == "/nonexistent" && len(arg) == 0 {
return exec.CommandContext(ctx, name)
}
return exec.CommandContext(ctx, os.Args[0], append([]string{"-test.run=TestHelperChildStub", "--", name}, arg...)...)
}
os.Exit(0)
}
func newFile(fd int, name, p string) *os.File {
@ -133,42 +104,3 @@ func genericStub(argsFile, statFile *os.File) {
<-done
}
}
func bwrapStub() {
// the bwrap launcher does not launch with a typical sync fd
argsFile, _ := flagRestoreFiles(4, "1", "0")
// test args pipe behaviour
func() {
got, want := new(strings.Builder), new(strings.Builder)
if _, err := io.Copy(got, argsFile); err != nil {
panic("cannot read bwrap args: " + err.Error())
}
// hardcoded bwrap configuration used by test
sc := &bwrap.Config{
Net: true,
Hostname: "localhost",
Chdir: "/nonexistent",
Clearenv: true,
NewSession: true,
DieWithParent: true,
AsInit: true,
}
if _, err := MustNewCheckedArgs(sc.Args(nil, new(proc.ExtraFilesPre), new([]proc.File))).
WriteTo(want); err != nil {
panic("cannot read want: " + err.Error())
}
if len(flag.CommandLine.Args()) > 0 && flag.CommandLine.Args()[0] == "crash-test-dummy" && got.String() != want.String() {
panic("bad bwrap args\ngot: " + got.String() + "\nwant: " + want.String())
}
}()
if err := syscall.Exec(
os.Args[0],
append([]string{os.Args[0], "-test.run=TestHelperChildStub", "--"}, flag.CommandLine.Args()...),
os.Environ()); err != nil {
panic("cannot start general stub: " + err.Error())
}
}

View File

@ -3,9 +3,7 @@ package helper_test
import (
"testing"
"git.gensokyo.uk/security/fortify/helper"
"git.gensokyo.uk/security/hakurei/helper"
)
func TestHelperChildStub(t *testing.T) {
helper.InternalChildStub()
}
func TestHelperStub(t *testing.T) { helper.InternalHelperStub() }

83
hst/config.go Normal file
View File

@ -0,0 +1,83 @@
// Package hst exports shared types for invoking hakurei.
package hst
import (
"git.gensokyo.uk/security/hakurei/dbus"
"git.gensokyo.uk/security/hakurei/system"
)
const Tmp = "/.hakurei"
// Config is used to seal an app implementation.
type Config struct {
// reverse-DNS style arbitrary identifier string from config;
// passed to wayland security-context-v1 as application ID
// and used as part of defaults in dbus session proxy
ID string `json:"id"`
// absolute path to executable file
Path string `json:"path,omitempty"`
// final args passed to container init
Args []string `json:"args"`
// system services to make available in the container
Enablements system.Enablement `json:"enablements"`
// session D-Bus proxy configuration;
// nil makes session bus proxy assume built-in defaults
SessionBus *dbus.Config `json:"session_bus,omitempty"`
// system D-Bus proxy configuration;
// nil disables system bus proxy
SystemBus *dbus.Config `json:"system_bus,omitempty"`
// direct access to wayland socket; when this gets set no attempt is made to attach security-context-v1
// and the bare socket is mounted to the sandbox
DirectWayland bool `json:"direct_wayland,omitempty"`
// passwd username in container, defaults to passwd name of target uid or chronos
Username string `json:"username,omitempty"`
// absolute path to shell, empty for host shell
Shell string `json:"shell,omitempty"`
// absolute path to home directory in the init mount namespace
Data string `json:"data"`
// directory to enter and use as home in the container mount namespace, empty for Data
Dir string `json:"dir"`
// extra acl ops, dispatches before container init
ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
// numerical application id, used for init user namespace credentials
Identity int `json:"identity"`
// list of supplementary groups inherited by container processes
Groups []string `json:"groups"`
// abstract container configuration baseline
Container *ContainerConfig `json:"container"`
}
// ExtraPermConfig describes an acl update op.
type ExtraPermConfig struct {
Ensure bool `json:"ensure,omitempty"`
Path string `json:"path"`
Read bool `json:"r,omitempty"`
Write bool `json:"w,omitempty"`
Execute bool `json:"x,omitempty"`
}
func (e *ExtraPermConfig) String() string {
buf := make([]byte, 0, 5+len(e.Path))
buf = append(buf, '-', '-', '-')
if e.Ensure {
buf = append(buf, '+')
}
buf = append(buf, ':')
buf = append(buf, []byte(e.Path)...)
if e.Read {
buf[0] = 'r'
}
if e.Write {
buf[1] = 'w'
}
if e.Execute {
buf[2] = 'x'
}
return string(buf)
}

59
hst/container.go Normal file
View File

@ -0,0 +1,59 @@
package hst
import (
"git.gensokyo.uk/security/hakurei/sandbox/seccomp"
)
type (
// ContainerConfig describes the container configuration baseline to which the app implementation adds upon.
ContainerConfig struct {
// container hostname
Hostname string `json:"hostname,omitempty"`
// extra seccomp flags
Seccomp seccomp.FilterOpts `json:"seccomp"`
// allow ptrace and friends
Devel bool `json:"devel,omitempty"`
// allow userns creation in container
Userns bool `json:"userns,omitempty"`
// share host net namespace
Net bool `json:"net,omitempty"`
// allow dangerous terminal I/O
Tty bool `json:"tty,omitempty"`
// allow multiarch
Multiarch bool `json:"multiarch,omitempty"`
// initial process environment variables
Env map[string]string `json:"env"`
// map target user uid to privileged user uid in the user namespace
MapRealUID bool `json:"map_real_uid"`
// pass through all devices
Device bool `json:"device,omitempty"`
// container host filesystem bind mounts
Filesystem []*FilesystemConfig `json:"filesystem"`
// create symlinks inside container filesystem
Link [][2]string `json:"symlink"`
// read-only /etc directory
Etc string `json:"etc,omitempty"`
// automatically set up /etc symlinks
AutoEtc bool `json:"auto_etc"`
// cover these paths or create them if they do not already exist
Cover []string `json:"cover"`
}
// FilesystemConfig is an abstract representation of a bind mount.
FilesystemConfig struct {
// mount point in container, same as src if empty
Dst string `json:"dst,omitempty"`
// host filesystem path to make available to the container
Src string `json:"src"`
// do not mount filesystem read-only
Write bool `json:"write,omitempty"`
// do not disable device files
Device bool `json:"dev,omitempty"`
// fail if the bind mount cannot be established for any reason
Must bool `json:"require,omitempty"`
}
)

View File

@ -1,4 +1,4 @@
package fst
package hst
type Info struct {
User int `json:"user"`

91
hst/template.go Normal file
View File

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

140
hst/template_test.go Normal file
View File

@ -0,0 +1,140 @@
package hst_test
import (
"encoding/json"
"testing"
"git.gensokyo.uk/security/hakurei/hst"
)
func TestTemplate(t *testing.T) {
const want = `{
"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": 13,
"session_bus": {
"see": null,
"talk": [
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager"
],
"own": [
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"
],
"call": {
"org.freedesktop.portal.*": "*"
},
"broadcast": {
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"
},
"filter": true
},
"system_bus": {
"see": null,
"talk": [
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower"
],
"own": null,
"call": null,
"broadcast": null,
"filter": true
},
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"data": "/var/lib/hakurei/u0/org.chromium.Chromium",
"dir": "/data/data/org.chromium.Chromium",
"extra_perms": [
{
"ensure": true,
"path": "/var/lib/hakurei/u0",
"x": true
},
{
"path": "/var/lib/hakurei/u0/org.chromium.Chromium",
"r": true,
"w": true,
"x": true
}
],
"identity": 9,
"groups": [
"video",
"dialout",
"plugdev"
],
"container": {
"hostname": "localhost",
"seccomp": 32,
"devel": true,
"userns": true,
"net": true,
"tty": true,
"multiarch": true,
"env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
},
"map_real_uid": true,
"device": true,
"filesystem": [
{
"src": "/nix/store"
},
{
"src": "/run/current-system"
},
{
"src": "/run/opengl-driver"
},
{
"src": "/var/db/nix-channels"
},
{
"dst": "/data/data/org.chromium.Chromium",
"src": "/var/lib/hakurei/u0/org.chromium.Chromium",
"write": true,
"require": true
},
{
"src": "/dev/dri",
"dev": true
}
],
"symlink": [
[
"/run/user/65534",
"/run/user/150"
]
],
"etc": "/etc",
"auto_etc": true,
"cover": [
"/var/run/nscd"
]
}
}`
if p, err := json.MarshalIndent(hst.Template(), "", "\t"); err != nil {
t.Fatalf("cannot marshal: %v", err)
} else if s := string(p); s != want {
t.Fatalf("Template:\n%s\nwant:\n%s",
s, want)
}
}

View File

@ -1,79 +1,59 @@
// Package app defines the generic [App] interface.
package app
import (
"fmt"
"log"
"sync"
"syscall"
"time"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/sys"
"git.gensokyo.uk/security/hakurei/hst"
)
func New(os sys.State) (fst.App, error) {
a := new(app)
a.sys = os
type App interface {
// ID returns a copy of [ID] held by App.
ID() ID
id := new(fst.ID)
err := fst.NewAppID(id)
a.id = newID(id)
// Seal determines the outcome of config as a [SealedApp].
// The value of config might be overwritten and must not be used again.
Seal(config *hst.Config) (SealedApp, error)
return a, err
String() string
}
func MustNew(os sys.State) fst.App {
a, err := New(os)
if err != nil {
log.Fatalf("cannot create app: %v", err)
}
return a
type SealedApp interface {
// Run commits sealed system setup and starts the app process.
Run(rs *RunState) error
}
type app struct {
id *stringPair[fst.ID]
sys sys.State
// RunState stores the outcome of a call to [SealedApp.Run].
type RunState struct {
// Time is the exact point in time where the process was created.
// Location must be set to UTC.
//
// Time is nil if no process was ever created.
Time *time.Time
// RevertErr is stored by the deferred revert call.
RevertErr error
// WaitErr is the generic error value created by the standard library.
WaitErr error
*outcome
mu sync.RWMutex
syscall.WaitStatus
}
func (a *app) ID() fst.ID { a.mu.RLock(); defer a.mu.RUnlock(); return a.id.unwrap() }
func (a *app) String() string {
if a == nil {
return "(invalid app)"
// SetStart stores the current time in [RunState] once.
func (rs *RunState) SetStart() {
if rs.Time != nil {
panic("attempted to store time twice")
}
a.mu.RLock()
defer a.mu.RUnlock()
if a.outcome != nil {
if a.outcome.user.uid == nil {
return fmt.Sprintf("(sealed app %s with invalid uid)", a.id)
}
return fmt.Sprintf("(sealed app %s as uid %s)", a.id, a.outcome.user.uid)
}
return fmt.Sprintf("(unsealed app %s)", a.id)
now := time.Now().UTC()
rs.Time = &now
}
func (a *app) Seal(config *fst.Config) (fst.SealedApp, error) {
a.mu.Lock()
defer a.mu.Unlock()
if a.outcome != nil {
panic("app sealed twice")
}
if config == nil {
return nil, fmsg.WrapError(ErrConfig,
"attempted to seal app with nil config")
}
seal := new(outcome)
seal.id = a.id
err := seal.finalise(a.sys, config)
if err == nil {
a.outcome = seal
}
return seal, err
// Paths contains environment-dependent paths used by hakurei.
type Paths struct {
// path to shared directory (usually `/tmp/hakurei.%d`)
SharePath string `json:"share_path"`
// XDG_RUNTIME_DIR value (usually `/run/user/%d`)
RuntimePath string `json:"runtime_path"`
// application runtime directory (usually `/run/user/%d/hakurei`)
RunDirPath string `json:"run_dir_path"`
}

View File

@ -1,223 +0,0 @@
package app_test
import (
"git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/system"
)
var testCasesNixos = []sealTestCase{
{
"nixos chromium direct wayland", new(stubNixOS),
&fst.Config{
ID: "org.chromium.Chromium",
Command: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
Confinement: fst.ConfinementConfig{
AppID: 1, Groups: []string{}, Username: "u0_a1",
Outer: "/var/lib/persist/module/fortify/0/1",
Sandbox: &fst.SandboxConfig{
UserNS: true, Net: true, MapRealUID: true, DirectWayland: true, Env: nil, AutoEtc: true,
Filesystem: []*fst.FilesystemConfig{
{Src: "/bin", Must: true}, {Src: "/usr/bin", Must: true},
{Src: "/nix/store", Must: true}, {Src: "/run/current-system", Must: true},
{Src: "/sys/block"}, {Src: "/sys/bus"}, {Src: "/sys/class"}, {Src: "/sys/dev"}, {Src: "/sys/devices"},
{Src: "/run/opengl-driver", Must: true}, {Src: "/dev/dri", Device: true},
},
Override: []string{"/var/run/nscd"},
},
SystemBus: &dbus.Config{
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
Filter: true,
},
SessionBus: &dbus.Config{
Talk: []string{
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
"org.kde.kwalletd5", "org.kde.kwalletd6",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{}, Broadcast: map[string]string{},
Filter: true,
},
Enablements: system.EWayland.Mask() | system.EDBus.Mask() | system.EPulse.Mask(),
},
},
fst.ID{
0x8e, 0x2c, 0x76, 0xb0,
0x66, 0xda, 0xbe, 0x57,
0x4c, 0xf0, 0x73, 0xbd,
0xb4, 0x6e, 0xb5, 0xc1,
},
system.New(1000001).
Ensure("/tmp/fortify.1971", 0711).
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1", 0711).
Ephemeral(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/1", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/1", acl.Read, acl.Write, acl.Execute).
UpdatePermType(system.EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute).
Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse").
CopyFile(nil, "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
MustProxyDBus("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", &dbus.Config{
Talk: []string{
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
"org.kde.kwalletd5", "org.kde.kwalletd6",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{}, Broadcast: map[string]string{},
Filter: true,
}, "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower",
},
Filter: true,
}).
UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write).
UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write),
(&bwrap.Config{
Net: true,
UserNS: true,
Chdir: "/var/lib/persist/module/fortify/0/1",
Clearenv: true,
SetEnv: map[string]string{
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/1971/bus",
"DBUS_SYSTEM_BUS_ADDRESS": "unix:path=/run/dbus/system_bus_socket",
"HOME": "/var/lib/persist/module/fortify/0/1",
"PULSE_COOKIE": fst.Tmp + "/pulse-cookie",
"PULSE_SERVER": "unix:/run/user/1971/pulse/native",
"SHELL": "/run/current-system/sw/bin/zsh",
"TERM": "xterm-256color",
"USER": "u0_a1",
"WAYLAND_DISPLAY": "wayland-0",
"XDG_RUNTIME_DIR": "/run/user/1971",
"XDG_SESSION_CLASS": "user",
"XDG_SESSION_TYPE": "tty",
},
Chmod: make(bwrap.ChmodConfig),
NewSession: true,
DieWithParent: true,
AsInit: true,
}).SetUID(1971).SetGID(1971).
Procfs("/proc").
Tmpfs(fst.Tmp, 4096).
DevTmpfs("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin").
Bind("/usr/bin", "/usr/bin").
Bind("/nix/store", "/nix/store").
Bind("/run/current-system", "/run/current-system").
Bind("/sys/block", "/sys/block", true).
Bind("/sys/bus", "/sys/bus", true).
Bind("/sys/class", "/sys/class", true).
Bind("/sys/dev", "/sys/dev", true).
Bind("/sys/devices", "/sys/devices", true).
Bind("/run/opengl-driver", "/run/opengl-driver").
Bind("/dev/dri", "/dev/dri", true, true, true).
Bind("/etc", fst.Tmp+"/etc").
Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
Symlink(fst.Tmp+"/etc/default", "/etc/default").
Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/proc/mounts", "/etc/mtab").
Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
Symlink(fst.Tmp+"/etc/services", "/etc/services").
Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
Symlink(fst.Tmp+"/etc/static", "/etc/static").
Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/1971", 8388608).
Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", false, true).
Bind("/var/lib/persist/module/fortify/0/1", "/var/lib/persist/module/fortify/0/1", false, true).
CopyBind("/etc/passwd", []byte("u0_a1:x:1971:1971:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n")).
CopyBind("/etc/group", []byte("fortify:x:1971:\n")).
Bind("/run/user/1971/wayland-0", "/run/user/1971/wayland-0").
Bind("/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native").
CopyBind(fst.Tmp+"/pulse-cookie", nil).
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus").
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket").
Tmpfs("/var/run/nscd", 8192).
Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
Symlink("fortify", "/.fortify/sbin/init"),
},
}

View File

@ -1,394 +0,0 @@
package app_test
import (
"os"
"git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/system"
)
var testCasesPd = []sealTestCase{
{
"nixos permissive defaults no enablements", new(stubNixOS),
&fst.Config{
Command: make([]string, 0),
Confinement: fst.ConfinementConfig{
AppID: 0,
Username: "chronos",
Outer: "/home/chronos",
},
},
fst.ID{
0x4a, 0x45, 0x0b, 0x65,
0x96, 0xd7, 0xbc, 0x15,
0xbd, 0x01, 0x78, 0x0e,
0xb9, 0xa6, 0x07, 0xac,
},
system.New(1000000).
Ensure("/tmp/fortify.1971", 0711).
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac", 0711).
Ephemeral(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute),
(&bwrap.Config{
Net: true,
UserNS: true,
Clearenv: true,
Syscall: new(bwrap.SyscallPolicy),
Chdir: "/home/chronos",
SetEnv: map[string]string{
"HOME": "/home/chronos",
"SHELL": "/run/current-system/sw/bin/zsh",
"TERM": "xterm-256color",
"USER": "chronos",
"XDG_RUNTIME_DIR": "/run/user/65534",
"XDG_SESSION_CLASS": "user",
"XDG_SESSION_TYPE": "tty"},
Chmod: make(bwrap.ChmodConfig),
DieWithParent: true,
AsInit: true,
}).SetUID(65534).SetGID(65534).
Procfs("/proc").
Tmpfs(fst.Tmp, 4096).
DevTmpfs("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", false, true).
Bind("/boot", "/boot", false, true).
Bind("/home", "/home", false, true).
Bind("/lib", "/lib", false, true).
Bind("/lib64", "/lib64", false, true).
Bind("/nix", "/nix", false, true).
Bind("/root", "/root", false, true).
Bind("/run", "/run", false, true).
Bind("/srv", "/srv", false, true).
Bind("/sys", "/sys", false, true).
Bind("/usr", "/usr", false, true).
Bind("/var", "/var", false, true).
Bind("/dev/kvm", "/dev/kvm", true, true, true).
Tmpfs("/run/user/1971", 8192).
Tmpfs("/run/dbus", 8192).
Bind("/etc", fst.Tmp+"/etc").
Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
Symlink(fst.Tmp+"/etc/default", "/etc/default").
Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/proc/mounts", "/etc/mtab").
Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
Symlink(fst.Tmp+"/etc/services", "/etc/services").
Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
Symlink(fst.Tmp+"/etc/static", "/etc/static").
Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/65534", 8388608).
Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", false, true).
Bind("/home/chronos", "/home/chronos", false, true).
CopyBind("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
CopyBind("/etc/group", []byte("fortify:x:65534:\n")).
Tmpfs("/var/run/nscd", 8192).
Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
Symlink("fortify", "/.fortify/sbin/init"),
},
{
"nixos permissive defaults chromium", new(stubNixOS),
&fst.Config{
ID: "org.chromium.Chromium",
Command: []string{"/run/current-system/sw/bin/zsh", "-c", "exec chromium "},
Confinement: fst.ConfinementConfig{
AppID: 9,
Groups: []string{"video"},
Username: "chronos",
Outer: "/home/chronos",
SessionBus: &dbus.Config{
Talk: []string{
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{
"org.freedesktop.portal.*": "*",
},
Broadcast: map[string]string{
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
},
Filter: true,
},
SystemBus: &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower",
},
Filter: true,
},
Enablements: system.EWayland.Mask() | system.EDBus.Mask() | system.EPulse.Mask(),
},
},
fst.ID{
0xeb, 0xf0, 0x83, 0xd1,
0xb1, 0x75, 0x91, 0x17,
0x82, 0xd4, 0x13, 0x36,
0x9b, 0x64, 0xce, 0x7c,
},
system.New(1000009).
Ensure("/tmp/fortify.1971", 0711).
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c", 0711).
Ephemeral(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/9", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/9", acl.Read, acl.Write, acl.Execute).
Ensure("/tmp/fortify.1971/wayland", 0711).
Wayland(new(*os.File), "/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/1971/wayland-0", "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse").
CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
MustProxyDBus("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", &dbus.Config{
Talk: []string{
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{
"org.freedesktop.portal.*": "*",
},
Broadcast: map[string]string{
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
},
Filter: true,
}, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower",
},
Filter: true,
}).
UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write).
UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write),
(&bwrap.Config{
Net: true,
UserNS: true,
Chdir: "/home/chronos",
Clearenv: true,
Syscall: new(bwrap.SyscallPolicy),
SetEnv: map[string]string{
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/65534/bus",
"DBUS_SYSTEM_BUS_ADDRESS": "unix:path=/run/dbus/system_bus_socket",
"HOME": "/home/chronos",
"PULSE_COOKIE": fst.Tmp + "/pulse-cookie",
"PULSE_SERVER": "unix:/run/user/65534/pulse/native",
"SHELL": "/run/current-system/sw/bin/zsh",
"TERM": "xterm-256color",
"USER": "chronos",
"WAYLAND_DISPLAY": "wayland-0",
"XDG_RUNTIME_DIR": "/run/user/65534",
"XDG_SESSION_CLASS": "user",
"XDG_SESSION_TYPE": "tty",
},
Chmod: make(bwrap.ChmodConfig),
DieWithParent: true,
AsInit: true,
}).SetUID(65534).SetGID(65534).
Procfs("/proc").
Tmpfs(fst.Tmp, 4096).
DevTmpfs("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", false, true).
Bind("/boot", "/boot", false, true).
Bind("/home", "/home", false, true).
Bind("/lib", "/lib", false, true).
Bind("/lib64", "/lib64", false, true).
Bind("/nix", "/nix", false, true).
Bind("/root", "/root", false, true).
Bind("/run", "/run", false, true).
Bind("/srv", "/srv", false, true).
Bind("/sys", "/sys", false, true).
Bind("/usr", "/usr", false, true).
Bind("/var", "/var", false, true).
Bind("/dev/dri", "/dev/dri", true, true, true).
Bind("/dev/kvm", "/dev/kvm", true, true, true).
Tmpfs("/run/user/1971", 8192).
Tmpfs("/run/dbus", 8192).
Bind("/etc", fst.Tmp+"/etc").
Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
Symlink(fst.Tmp+"/etc/default", "/etc/default").
Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/proc/mounts", "/etc/mtab").
Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
Symlink(fst.Tmp+"/etc/services", "/etc/services").
Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
Symlink(fst.Tmp+"/etc/static", "/etc/static").
Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/65534", 8388608).
Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", false, true).
Bind("/home/chronos", "/home/chronos", false, true).
CopyBind("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
CopyBind("/etc/group", []byte("fortify:x:65534:\n")).
Bind("/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/65534/wayland-0").
Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native").
CopyBind(fst.Tmp+"/pulse-cookie", nil).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket").
Tmpfs("/var/run/nscd", 8192).
Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
Symlink("fortify", "/.fortify/sbin/init"),
},
}

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