Compare commits

..

247 Commits

Author SHA1 Message Date
12be7bc78e
release: 0.3.3
All checks were successful
Release / Create release (push) Successful in 34s
Test / Sandbox (push) Successful in 35s
Test / Fortify (push) Successful in 2m47s
Test / Sandbox (race detector) (push) Successful in 3m0s
Test / Create distribution (push) Successful in 20s
Test / Fpkg (push) Successful in 4m12s
Test / Fortify (race detector) (push) Successful in 4m18s
Test / Flake checks (push) Successful in 1m20s
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 / Fpkg (push) Successful in 35s
Test / Fortify (push) Successful in 40s
Test / Data race detector (push) Successful in 40s
Test / Flake checks (push) Successful in 58s
This is copied at runtime because it appears to be impossible to obtain this path in nix.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-26 02:06:11 +09:00
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 / Fortify (push) Successful in 2m33s
Test / Data race detector (push) Successful in 3m25s
Test / Fpkg (push) Successful in 38m26s
Test / Flake checks (push) Successful in 54s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-26 13:12:16 +09:00
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
c64b8163e7
app: separate instance from process state
All checks were successful
Test / Create distribution (push) Successful in 19s
Test / Run NixOS test (push) Successful in 1m59s
This works better for the implementation.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-16 12:15:26 +09:00
197 changed files with 11280 additions and 6785 deletions

View File

@ -5,25 +5,107 @@ on:
- pull_request
jobs:
test:
name: Run NixOS test
fortify:
name: Fortify
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.fortify
- name: Upload test output
uses: actions/upload-artifact@v3
with:
name: "nixos-vm-output"
name: "fortify-vm-output"
path: result/*
retention-days: 1
race:
name: Fortify (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: "fortify-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
fpkg:
name: Fpkg
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.fpkg
- name: Upload test output
uses: actions/upload-artifact@v3
with:
name: "fpkg-vm-output"
path: result/*
retention-days: 1
check:
name: Flake checks
needs:
- fortify
- race
- sandbox
- sandbox-race
- fpkg
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

69
acl/acl-update.c Normal file
View File

@ -0,0 +1,69 @@
#include "acl-update.h"
#include <stdlib.h>
#include <stdbool.h>
#include <sys/acl.h>
#include <acl/libacl.h>
int f_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;
acl_t acl;
acl_entry_t entry;
acl_tag_t tag_type;
void *qualifier_p;
acl_permset_t permset;
acl = acl_get_file(path_p, ACL_TYPE_ACCESS);
if (acl == NULL)
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)) {
if (acl_get_tag_type(entry, &tag_type) != 0)
return -1;
if (tag_type != ACL_USER)
continue;
qualifier_p = acl_get_qualifier(entry);
if (qualifier_p == NULL)
return -1;
v = *(uid_t *)qualifier_p == uid;
acl_free(qualifier_p);
if (!v)
continue;
acl_delete_entry(acl, entry);
}
if (plen == 0)
goto set;
if (acl_create_entry(&acl, &entry) != 0)
goto out;
if (acl_get_permset(entry, &permset) != 0)
goto out;
for (i = 0; i < plen; i++) {
if (acl_add_perm(permset, perms[i]) != 0)
goto out;
}
if (acl_set_tag_type(entry, ACL_USER) != 0)
goto out;
if (acl_set_qualifier(entry, (void *)&uid) != 0)
goto out;
set:
if (acl_calc_mask(&acl) != 0)
goto out;
if (acl_valid(acl) != 0)
goto out;
if (acl_set_file(path_p, ACL_TYPE_ACCESS, acl) == 0)
ret = 0;
out:
free((void *)path_p);
if (acl != NULL)
acl_free((void *)acl);
return ret;
}

3
acl/acl-update.h Normal file
View File

@ -0,0 +1,3 @@
#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);

View File

@ -1,19 +1,36 @@
// Package acl implements simple ACL manipulation via libacl.
package acl
type Perms []Perm
/*
#cgo linux pkg-config: --static libacl
func (ps Perms) String() string {
var s = []byte("---")
for _, p := range ps {
switch p {
case Read:
s[0] = 'r'
case Write:
s[1] = 'w'
case Execute:
s[2] = 'x'
}
#include "acl-update.h"
*/
import "C"
type Perm C.acl_perm_t
const (
Read Perm = C.ACL_READ
Write Perm = C.ACL_WRITE
Execute Perm = C.ACL_EXECUTE
)
// Update replaces ACL_USER entry with qualifier uid.
func Update(name string, uid int, perms ...Perm) error {
var p *Perm
if len(perms) > 0 {
p = &perms[0]
}
return string(s)
r, err := C.f_acl_update_file_by_uid(
C.CString(name),
C.uid_t(uid),
(*C.acl_perm_t)(p),
C.size_t(len(perms)),
)
if r == 0 {
return nil
}
return err
}

View File

@ -47,7 +47,7 @@ func TestUpdatePerm(t *testing.T) {
})
t.Run("default clear mask", func(t *testing.T) {
if err := acl.UpdatePerm(testFilePath, uid); err != nil {
if err := acl.Update(testFilePath, uid); err != nil {
t.Fatalf("UpdatePerm: error = %v", err)
}
if cur = getfacl(t, testFilePath); len(cur) != 4 {
@ -56,7 +56,7 @@ func TestUpdatePerm(t *testing.T) {
})
t.Run("default clear consistency", func(t *testing.T) {
if err := acl.UpdatePerm(testFilePath, uid); err != nil {
if err := acl.Update(testFilePath, uid); err != nil {
t.Fatalf("UpdatePerm: error = %v", err)
}
if val := getfacl(t, testFilePath); !reflect.DeepEqual(val, cur) {
@ -76,7 +76,7 @@ func TestUpdatePerm(t *testing.T) {
func testUpdate(t *testing.T, testFilePath, name string, cur []*getFAclResp, val fAclPerm, perms ...acl.Perm) {
t.Run(name, func(t *testing.T) {
t.Cleanup(func() {
if err := acl.UpdatePerm(testFilePath, uid); err != nil {
if err := acl.Update(testFilePath, uid); err != nil {
t.Fatalf("UpdatePerm: error = %v", err)
}
if v := getfacl(t, testFilePath); !reflect.DeepEqual(v, cur) {
@ -84,7 +84,7 @@ func testUpdate(t *testing.T, testFilePath, name string, cur []*getFAclResp, val
}
})
if err := acl.UpdatePerm(testFilePath, uid, perms...); err != nil {
if err := acl.Update(testFilePath, uid, perms...); err != nil {
t.Fatalf("UpdatePerm: error = %v", err)
}
r := respByCred(getfacl(t, testFilePath), fAclTypeUser, cred)

196
acl/c.go
View File

@ -1,196 +0,0 @@
package acl
import "C"
import (
"errors"
"runtime"
"syscall"
"unsafe"
)
/*
#cgo linux pkg-config: --static libacl
#include <stdlib.h>
#include <sys/acl.h>
#include <acl/libacl.h>
static acl_t _go_acl_get_file(const char *path_p, acl_type_t type) {
acl_t acl = acl_get_file(path_p, type);
free((void *)path_p);
return acl;
}
static int _go_acl_set_file(const char *path_p, acl_type_t type, acl_t acl) {
if (acl_valid(acl) != 0) {
return -1;
}
int ret = acl_set_file(path_p, type, acl);
free((void *)path_p);
return ret;
}
*/
import "C"
func getFile(name string, t C.acl_type_t) (*ACL, error) {
a, err := C._go_acl_get_file(C.CString(name), t)
if errors.Is(err, syscall.ENODATA) {
err = nil
}
return newACL(a), err
}
func (acl *ACL) setFile(name string, t C.acl_type_t) error {
_, err := C._go_acl_set_file(C.CString(name), t, acl.acl)
return err
}
func newACL(a C.acl_t) *ACL {
acl := &ACL{a}
runtime.SetFinalizer(acl, (*ACL).free)
return acl
}
type ACL struct {
acl C.acl_t
}
func (acl *ACL) free() {
C.acl_free(unsafe.Pointer(acl.acl))
// no need for a finalizer anymore
runtime.SetFinalizer(acl, nil)
}
const (
Read = C.ACL_READ
Write = C.ACL_WRITE
Execute = C.ACL_EXECUTE
TypeDefault = C.ACL_TYPE_DEFAULT
TypeAccess = C.ACL_TYPE_ACCESS
UndefinedTag = C.ACL_UNDEFINED_TAG
UserObj = C.ACL_USER_OBJ
User = C.ACL_USER
GroupObj = C.ACL_GROUP_OBJ
Group = C.ACL_GROUP
Mask = C.ACL_MASK
Other = C.ACL_OTHER
)
type (
Perm C.acl_perm_t
)
func (acl *ACL) removeEntry(tt C.acl_tag_t, tq int) error {
var e C.acl_entry_t
// get first entry
if r, err := C.acl_get_entry(acl.acl, C.ACL_FIRST_ENTRY, &e); err != nil {
return err
} else if r == 0 {
// return on acl with no entries
return nil
}
for {
if r, err := C.acl_get_entry(acl.acl, C.ACL_NEXT_ENTRY, &e); err != nil {
return err
} else if r == 0 {
// return on drained acl
return nil
}
var (
q int
t C.acl_tag_t
)
// get current entry tag type
if _, err := C.acl_get_tag_type(e, &t); err != nil {
return err
}
// get current entry qualifier
if rq, err := C.acl_get_qualifier(e); err != nil {
// neither ACL_USER nor ACL_GROUP
if errors.Is(err, syscall.EINVAL) {
continue
}
return err
} else {
q = *(*int)(rq)
C.acl_free(rq)
}
// delete on match
if t == tt && q == tq {
_, err := C.acl_delete_entry(acl.acl, e)
return err
}
}
}
func UpdatePerm(name string, uid int, perms ...Perm) error {
// read acl from file
a, err := getFile(name, TypeAccess)
if err != nil {
return err
}
// free acl on return if get is successful
defer a.free()
// remove existing entry
if err = a.removeEntry(User, uid); err != nil {
return err
}
// create new entry if perms are passed
if len(perms) > 0 {
// create new acl entry
var e C.acl_entry_t
if _, err = C.acl_create_entry(&a.acl, &e); err != nil {
return err
}
// get perm set of new entry
var p C.acl_permset_t
if _, err = C.acl_get_permset(e, &p); err != nil {
return err
}
// add target perms
for _, perm := range perms {
if _, err = C.acl_add_perm(p, C.acl_perm_t(perm)); err != nil {
return err
}
}
// set perm set to new entry
if _, err = C.acl_set_permset(e, p); err != nil {
return err
}
// set user tag to new entry
if _, err = C.acl_set_tag_type(e, User); err != nil {
return err
}
// set qualifier (uid) to new entry
if _, err = C.acl_set_qualifier(e, unsafe.Pointer(&uid)); err != nil {
return err
}
}
// calculate mask after update
if _, err = C.acl_calc_mask(&a.acl); err != nil {
return err
}
// write acl to file
return a.setFile(name, TypeAccess)
}

18
acl/perms.go Normal file
View File

@ -0,0 +1,18 @@
package acl
type Perms []Perm
func (ps Perms) String() string {
var s = []byte("---")
for _, p := range ps {
switch p {
case Read:
s[0] = 'r'
case Write:
s[1] = 'w'
case Execute:
s[2] = 'x'
}
}
return string(s)
}

150
cmd/fpkg/app.go Normal file
View File

@ -0,0 +1,150 @@
package main
import (
"encoding/json"
"log"
"os"
"path"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
"git.gensokyo.uk/security/fortify/system"
)
type appInfo 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]
Devel bool `json:"devel,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]
Tty bool `json:"tty,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.Enablement `json:"enablements"`
// passed through to [fst.Config]
Multiarch bool `json:"multiarch,omitempty"`
// passed through to [fst.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) *fst.Config {
config := &fst.Config{
ID: app.ID,
Path: argv[0],
Args: argv,
Confinement: fst.ConfinementConfig{
AppID: app.AppID,
Groups: app.Groups,
Username: "fortify",
Inner: path.Join("/data/data", app.ID),
Outer: pathSet.homeDir,
Shell: shellPath,
Sandbox: &fst.SandboxConfig{
Hostname: formatHostname(app.Name),
Devel: app.Devel,
Userns: app.Userns,
Net: app.Net,
Dev: app.Dev,
Tty: app.Tty || flagDropShell,
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,
},
}
if app.Multiarch {
config.Confinement.Sandbox.Seccomp |= seccomp.FlagMultiarch
}
if app.Bluetooth {
config.Confinement.Sandbox.Seccomp |= seccomp.FlagBluetooth
}
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 "fortify-" + name
} else {
return h + "-" + name
}
}

View File

@ -7,6 +7,8 @@
{
lib,
stdenv,
closureInfo,
writeScript,
runtimeShell,
writeText,
@ -15,12 +17,15 @@
runCommand,
fetchFromGitHub,
zstd,
nix,
sqlite,
name ? throw "name is required",
version ? throw "version is required",
pname ? "${name}-${version}",
modules ? [ ],
nixosModules ? [ ],
script ? ''
exec "$SHELL" "$@"
'',
@ -72,6 +77,8 @@ let
etc.nixpkgs.source = nixpkgs.outPath;
systemPackages = [ pkgs.nix ];
};
imports = nixosModules;
};
nixos = nixpkgs.lib.nixosSystem {
inherit system;
@ -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"
'';
}

View File

@ -1,90 +0,0 @@
package main
import (
"encoding/json"
"os"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/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()
fmsg.Fatalf("cannot open bundle: %v", err)
} else if err = json.NewDecoder(f).Decode(&bundle); err != nil {
beforeFail()
fmsg.Fatalf("cannot parse bundle metadata: %v", err)
} else if err = f.Close(); err != nil {
fmsg.Printf("cannot close bundle metadata: %v", err)
// not fatal
}
if bundle.ID == "" {
beforeFail()
fmsg.Fatal("application identifier must not be empty")
}
return bundle
}
func formatHostname(name string) string {
if h, err := os.Hostname(); err != nil {
fmsg.Printf("cannot get hostname: %v", err)
return "fortify-" + name
} else {
return h + "-" + name
}
}

View File

@ -1,195 +0,0 @@
package main
import (
"encoding/json"
"flag"
"os"
"path"
"git.gensokyo.uk/security/fortify/fst"
"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 {
fmsg.Fatal("invalid argument")
}
pkgPath := args[0]
if !path.IsAbs(pkgPath) {
if dir, err := os.Getwd(); err != nil {
fmsg.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 {
fmsg.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()
fmsg.Fatalf("cannot access %q: %v", pathSet.metaPath, err)
panic("unreachable")
}
// did not modify app, clean installation condition met later
} else if s.IsDir() {
cleanup()
fmsg.Fatalf("metadata path %q is not a file", pathSet.metaPath)
panic("unreachable")
} else {
app = loadBundleInfo(pathSet.metaPath, cleanup)
if app.ID != bundle.ID {
cleanup()
fmsg.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()
fmsg.Printf("package %q is identical to local application %q", pkgPath, app.ID)
fmsg.Exit(0)
}
// AppID determines uid
if app.AppID != bundle.AppID {
cleanup()
fmsg.Fatalf("package %q app id %d differs from installed %d", pkgPath, bundle.AppID, app.AppID)
panic("unreachable")
}
// sec: should compare version string
fmsg.VPrintf("installing application %q version %q over local %q", bundle.ID, bundle.Version, app.Version)
} else {
fmsg.VPrintf("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()
fmsg.Fatalf("cannot create metadata file: %v", err)
panic("unreachable")
} else if err = json.NewEncoder(f).Encode(bundle); err != nil {
cleanup()
fmsg.Fatalf("cannot write metadata: %v", err)
panic("unreachable")
} else if err = f.Close(); err != nil {
fmsg.Printf("cannot close metadata file: %v", err)
// not fatal
}
if err := os.Rename(pathSet.metaPath+"~", pathSet.metaPath); err != nil {
cleanup()
fmsg.Fatalf("cannot rename metadata file: %v", err)
panic("unreachable")
}
cleanup()
}

View File

@ -1,48 +1,351 @@
package main
import (
"flag"
"context"
"encoding/json"
"errors"
"log"
"os"
"os/signal"
"path"
"syscall"
"git.gensokyo.uk/security/fortify/command"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/sys"
"git.gensokyo.uk/security/fortify/sandbox"
)
const shell = "/run/current-system/sw/bin/bash"
func init() {
if err := os.Setenv("SHELL", shell); err != nil {
fmsg.Fatalf("cannot set $SHELL: %v", err)
}
}
const shellPath = "/run/current-system/sw/bin/bash"
var (
flagVerbose bool
errSuccess = errors.New("success")
std sys.State = new(sys.Std)
)
func init() {
flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
fmsg.Prepare("fpkg")
if err := os.Setenv("SHELL", shellPath); err != nil {
log.Fatalf("cannot set $SHELL: %v", err)
}
}
func main() {
fmsg.SetPrefix("fpkg")
// early init path, skips root check and duplicate PR_SET_DUMPABLE
sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg)
flag.Parse()
fmsg.SetVerbose(flagVerbose)
args := flag.Args()
if len(args) < 1 {
fmsg.Fatal("invalid argument")
if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
// not fatal: this program runs as the privileged user
}
switch args[0] {
case "install":
actionInstall(args[1:])
case "start":
actionStart(args[1:])
default:
fmsg.Fatal("invalid argument")
if os.Geteuid() == 0 {
log.Fatal("this program must not run as root")
}
fmsg.Exit(0)
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, "fpkg", 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 fortify action")
c.Command("shim", command.UsageInternal, func([]string) error { app.ShimMain(); return errSuccess })
{
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 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.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
}
// AppID determines uid
if a.AppID != bundle.AppID {
cleanup()
log.Printf("package %q app id %d differs from installed %d",
pkgPath, bundle.AppID, a.AppID)
return syscall.EBADE
}
// sec: should compare version string
fmsg.Verbosef("installing application %q version %q over local %q",
bundle.ID, bundle.Version, a.Version)
} else {
fmsg.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=" + 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, 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 *fst.Config) *fst.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 *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
}, 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.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.
*/
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) {
fmsg.Verbosef("command returned %v", err)
if errors.Is(err, errSuccess) {
fmsg.BeforeExit()
os.Exit(0)
}
})
log.Fatal("unreachable")
}

View File

@ -1,12 +1,14 @@
package main
import (
"log"
"os"
"os/exec"
"path"
"strconv"
"sync/atomic"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
@ -25,8 +27,8 @@ func init() {
func lookPath(file string) string {
if p, err := exec.LookPath(file); err != nil {
fmsg.Fatalf("%s: command not found", file)
panic("unreachable")
log.Fatalf("%s: command not found", file)
return ""
} else {
return p
}
@ -35,15 +37,14 @@ func lookPath(file string) string {
var beforeRunFail = new(atomic.Pointer[func()])
func mustRun(name string, arg ...string) {
fmsg.VPrintf("spawning process: %q %q", name, arg)
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)()
}
fmsg.Fatalf("%s: %v", name, err)
panic("unreachable")
log.Fatalf("%s: %v", name, err)
}
}
@ -69,3 +70,32 @@ func pathSetByApp(id string) *appPathSet {
pathSet.nixPath = path.Join(pathSet.cacheDir, "nix")
return pathSet
}
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,70 +1,28 @@
package main
import (
"encoding/json"
"errors"
"io"
"context"
"os"
"os/exec"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
func mustRunApp(ctx context.Context, config *fst.Config, beforeFail func()) {
rs := new(fst.RunState)
a := app.MustNew(ctx, std)
var (
Fmain = compPoison
)
func fortifyApp(config *fst.Config, beforeFail func()) {
var (
cmd *exec.Cmd
st io.WriteCloser
)
if p, ok := internal.Path(Fmain); !ok {
beforeFail()
fmsg.Fatal("invalid fortify path, this copy of fpkg is not compiled correctly")
panic("unreachable")
} else if r, w, err := os.Pipe(); err != nil {
beforeFail()
fmsg.Fatalf("cannot pipe: %v", err)
panic("unreachable")
if sa, err := a.Seal(config); err != nil {
fmsg.PrintBaseError(err, "cannot seal app:")
rs.ExitCode = 1
} else {
if fmsg.Verbose() {
cmd = exec.Command(p, "-v", "app", "3")
} else {
cmd = exec.Command(p, "app", "3")
}
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.ExtraFiles = []*os.File{r}
st = w
// this updates ExitCode
app.PrintRunStateErr(rs, sa.Run(rs))
}
go func() {
if err := json.NewEncoder(st).Encode(config); err != nil {
beforeFail()
fmsg.Fatalf("cannot send configuration: %v", err)
panic("unreachable")
}
}()
if err := cmd.Start(); err != nil {
if rs.ExitCode != 0 {
beforeFail()
fmsg.Fatalf("cannot start fortify: %v", err)
panic("unreachable")
}
if err := cmd.Wait(); err != nil {
var exitError *exec.ExitError
if errors.As(err, &exitError) {
beforeFail()
fmsg.Exit(exitError.ExitCode())
panic("unreachable")
} else {
beforeFail()
fmsg.Fatalf("cannot wait: %v", err)
panic("unreachable")
}
os.Exit(rs.ExitCode)
}
}

View File

@ -1,177 +0,0 @@
package main
import (
"flag"
"path"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
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 {
fmsg.Fatal("invalid argument")
}
/*
Parse app metadata.
*/
id := args[0]
pathSet := pathSetByApp(id)
app := loadBundleInfo(pathSet.metaPath, func() {})
if app.ID != id {
fmsg.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() {})
fmsg.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

@ -0,0 +1,60 @@
{ 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.fortify = {
enable = true;
stateDir = "/var/lib/fortify";
users.alice = 0;
home-manager = _: _: { home.stateVersion = "23.05"; };
};
}

34
cmd/fpkg/test/default.nix Normal file
View File

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

48
cmd/fpkg/test/foot.nix Normal file
View File

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

108
cmd/fpkg/test/test.py Normal file
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 fortify ps > '/tmp/{name}.ps'")
machine.copy_from_vm(f"/tmp/{name}.ps", "")
swaymsg(f"exec fortify --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 fortify --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"fortify-{name}-" not in (config['args'][0]):
raise Exception(f"unexpected args {instance['config']['args']}")
if config['confinement']['enablements'] != enablements:
raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}")
start_all()
machine.wait_for_unit("multi-user.target")
# To check fortify's version:
print(machine.succeed("sudo -u alice -i fortify 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 fpkg directory:
machine.succeed("install -dm 0700 -o alice -g users /var/lib/fortify/1000")
# Install fpkg app:
swaymsg("exec fpkg -v install /etc/foot.pkg && touch /tmp/fpkg-install-done")
machine.wait_for_file("/tmp/fpkg-install-done")
# Start app (foot) with Wayland enablement:
swaymsg("exec fpkg -v start org.codeberg.dnkl.foot")
wait_for_window("fortify@machine-foot")
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
machine.wait_for_file("/tmp/fortify.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 fortify runDir contents:
print(machine.succeed("find /run/user/1000/fortify"))

View File

@ -1,21 +1,24 @@
package main
import (
"context"
"path"
"strings"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
)
func withNixDaemon(
ctx context.Context,
action string, command []string, net bool, updateConfig func(config *fst.Config) *fst.Config,
app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
app *appInfo, 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 && " +
mustRunAppDropShell(ctx, updateConfig(&fst.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
@ -31,12 +34,13 @@ func withNixDaemon(
Username: "fortify",
Inner: path.Join("/data/data", app.ID),
Outer: pathSet.homeDir,
Shell: shellPath,
Sandbox: &fst.SandboxConfig{
Hostname: formatHostname(app.Name) + "-" + action,
UserNS: true, // nix sandbox requires userns
Net: net,
Syscall: &bwrap.SyscallPolicy{Multiarch: true},
NoNewSession: dropShell,
Hostname: formatHostname(app.Name) + "-" + action,
Userns: true, // nix sandbox requires userns
Net: net,
Seccomp: seccomp.FlagMultiarch,
Tty: dropShell,
Filesystem: []*fst.FilesystemConfig{
{Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true},
},
@ -56,19 +60,24 @@ func withNixDaemon(
}), 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, " && ")},
func withCacheDir(
ctx context.Context,
action string, command []string, workDir string,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
mustRunAppDropShell(ctx, &fst.Config{
ID: app.ID,
Path: shellPath,
Args: []string{shellPath, "-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
Shell: shellPath,
Sandbox: &fst.SandboxConfig{
Hostname: formatHostname(app.Name) + "-" + action,
Syscall: &bwrap.SyscallPolicy{Multiarch: true},
NoNewSession: dropShell,
Hostname: formatHostname(app.Name) + "-" + action,
Seccomp: seccomp.FlagMultiarch,
Tty: dropShell,
Filesystem: []*fst.FilesystemConfig{
{Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true},
{Src: workDir, Dst: path.Join(fst.Tmp, "bundle"), Must: true},
@ -90,12 +99,12 @@ func withCacheDir(action string, command []string, workDir string, app *bundleIn
}, dropShell, beforeFail)
}
func fortifyAppDropShell(config *fst.Config, dropShell bool, beforeFail func()) {
func mustRunAppDropShell(ctx context.Context, config *fst.Config, dropShell bool, beforeFail func()) {
if dropShell {
config.Command = []string{shell, "-l"}
fortifyApp(config, beforeFail)
config.Args = []string{shellPath, "-l"}
mustRunApp(ctx, config, beforeFail)
beforeFail()
fmsg.Exit(0)
internal.Exit(0)
}
fortifyApp(config, beforeFail)
mustRunApp(ctx, config, beforeFail)
}

View File

@ -13,7 +13,6 @@ import (
)
const (
compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
fsuConfFile = "/etc/fsurc"
envShim = "FORTIFY_SHIM"
envAID = "FORTIFY_APP_ID"
@ -22,10 +21,6 @@ const (
PR_SET_NO_NEW_PRIVS = 0x26
)
var (
Fmain = compPoison
)
func main() {
log.SetFlags(0)
log.SetPrefix("fsu: ")
@ -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 {
} else if p != mustCheckPath(fmain) && p != mustCheckPath(fpkg) {
log.Fatal("this program must be started by fortify")
} else {
toolPath = p
}
// uid = 1000000 +
@ -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{"fortify", "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)
}

30
cmd/fsu/package.nix Normal file
View File

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

21
cmd/fsu/path.go Normal file
View File

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

65
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/fortify/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/fortify/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,14 +1,22 @@
package dbus_test
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"syscall"
"testing"
"time"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/helper"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/sandbox"
)
func TestNew(t *testing.T) {
@ -64,7 +72,7 @@ func TestProxy_Seal(t *testing.T) {
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 {
if err := p.Seal(tc[0].c, tc[1].c); (errors.Is(err, syscall.EINVAL)) != tc[0].wantErr {
t.Errorf("Seal(%p, %p) error = %v, wantErr %v",
tc[0].c, tc[1].c,
err, tc[0].wantErr)
@ -100,15 +108,20 @@ func TestProxy_Seal(t *testing.T) {
}
func TestProxy_Start_Wait_Close_String(t *testing.T) {
t.Run("sandboxed", func(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 })
testProxyStartWaitCloseString(t, true)
})
t.Run("direct", func(t *testing.T) {
testProxyStartWaitCloseString(t, false)
})
t.Run("direct", func(t *testing.T) { testProxyStartWaitCloseString(t, false) })
}
func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
func testProxyStartWaitCloseString(t *testing.T, useSandbox bool) {
for id, tc := range testCasePairs() {
// this test does not test errors
if tc[0].wantErr {
@ -125,14 +138,33 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
})
t.Run("proxy for "+id, func(t *testing.T) {
helper.InternalReplaceExecCommand(t)
overridePath(t)
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) {
t.Run("unsealed", func(t *testing.T) {
t.Run("string", func(t *testing.T) {
want := "(unsealed dbus proxy)"
if got := p.String(); got != want {
t.Errorf("String() = %v, want %v",
@ -141,16 +173,16 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
}
})
t.Run("unsealed start of "+id, func(t *testing.T) {
t.Run("start", func(t *testing.T) {
want := "proxy not sealed"
if err := p.Start(context.Background(), nil, sandbox); err == nil || err.Error() != want {
if err := p.Start(context.Background(), nil, useSandbox); 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("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",
@ -168,7 +200,7 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
}
})
t.Run("sealed behaviour of "+id, func(t *testing.T) {
t.Run("sealed", func(t *testing.T) {
want := strings.Join(append(tc[0].want, tc[1].want...), " ")
if got := p.String(); got != want {
t.Errorf("String() = %v, want %v",
@ -176,17 +208,20 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
return
}
t.Run("sealed start of "+id, func(t *testing.T) {
t.Run("start", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := p.Start(ctx, output, sandbox); err != nil {
if err := p.Start(ctx, output, useSandbox); err != nil {
t.Fatalf("Start(nil, nil) 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)
@ -194,7 +229,7 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
}
})
t.Run("started wait of "+id, func(t *testing.T) {
t.Run("wait", func(t *testing.T) {
p.Close()
if err := p.Wait(); err != nil {
t.Errorf("Wait() error = %v\noutput: %s",
@ -207,10 +242,10 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
}
}
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(fmsg.Output{})
sandbox.Init(fmsg.Prepare, internal.InstallFmsg)
}

178
dbus/proc.go Normal file
View File

@ -0,0 +1,178 @@
package dbus
import (
"context"
"errors"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"slices"
"strconv"
"strings"
"syscall"
"git.gensokyo.uk/security/fortify/helper"
"git.gensokyo.uk/security/fortify/ldd"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
)
// Start launches the D-Bus proxy.
func (p *Proxy) Start(ctx context.Context, output io.Writer, useSandbox bool) error {
p.lock.Lock()
defer p.lock.Unlock()
if p.seal == nil {
return errors.New("proxy not sealed")
}
var h helper.Helper
c, cancel := context.WithCancelCause(ctx)
if !useSandbox {
h = helper.NewDirect(c, p.name, p.seal, true, argF, func(cmd *exec.Cmd) {
if p.CmdF != nil {
p.CmdF(cmd)
}
if output != nil {
cmd.Stdout, cmd.Stderr = output, 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)
}
h = helper.New(
c, toolPath,
p.seal, true,
argF, func(container *sandbox.Container) {
container.Seccomp |= seccomp.FlagMultiarch
container.Hostname = "fortify-dbus"
container.CommandContext = p.CommandContext
if output != nil {
container.Stdout, container.Stderr = output, 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 _, as := range []string{p.session[0], p.system[0]} {
if len(as) > 0 && strings.HasPrefix(as, "unix:path=/") {
// leave / intact
upstreamPaths = append(upstreamPaths, path.Dir(as[10:]))
}
}
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.session[1]); path.IsAbs(d) {
sockDirPaths = append(sockDirPaths, d)
}
if d := path.Dir(p.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 := h.Start(); 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
}
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

@ -5,10 +5,10 @@ import (
"errors"
"fmt"
"io"
"os/exec"
"sync"
"git.gensokyo.uk/security/fortify/helper"
"git.gensokyo.uk/security/fortify/helper/bwrap"
)
// ProxyName is the file name or path to the proxy program.
@ -19,15 +19,18 @@ var ProxyName = "xdg-dbus-proxy"
// 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
CmdF func(any)
sysP bool
CommandContext func(ctx context.Context) (cmd *exec.Cmd)
FilterF func([]byte) []byte
seal io.WriterTo
lock sync.RWMutex
}

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

@ -6,6 +6,12 @@ import (
"git.gensokyo.uk/security/fortify/dbus"
)
const (
sampleHostPath = "/tmp/bus"
sampleHostAddr = "unix:path=" + sampleHostPath
sampleBindPath = "/tmp/proxied_bus"
)
var samples = []dbusTestCase{
{
"org.chromium.Chromium", &dbus.Config{
@ -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

@ -6,6 +6,4 @@ import (
"git.gensokyo.uk/security/fortify/helper"
)
func TestHelperChildStub(t *testing.T) {
helper.InternalChildStub()
}
func TestHelperStub(t *testing.T) { helper.InternalHelperStub() }

7
dist/release.sh vendored
View File

@ -10,9 +10,10 @@ cp -rv "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/fortify/internal.version=${VERSION}
-X git.gensokyo.uk/security/fortify/internal.fsu=/usr/bin/fsu
-X main.fmain=/usr/bin/fortify
-X main.fpkg=/usr/bin/fpkg" ./...
rm -f "./${out}.tar.gz" && tar -C dist -czf "${out}.tar.gz" "${pname}"
rm -rf "./${out}"

View File

@ -1,55 +0,0 @@
package main
import (
"errors"
"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) {
fmsg.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
fmsg.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
fmsg.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
fmsg.Println("invalid error type returned by revert:", ei)
} else {
// print inner *app.BaseError message
fmsg.Print(eb.Message())
}
}
}
}
}
}
func logBaseError(err error, message string) {
var e *fmsg.BaseError
if fmsg.AsBaseError(err, &e) {
fmsg.Print(e.Message())
} else {
fmsg.Println(message, err)
}
}

14
flake.lock generated
View File

@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1736373539,
"narHash": "sha256-dinzAqCjenWDxuy+MqUQq0I4zUSfaCvN9rzuCmgMZJY=",
"lastModified": 1742655702,
"narHash": "sha256-jbqlw4sPArFtNtA1s3kLg7/A4fzP4GLk9bGbtUJg0JQ=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "bd65bc3cde04c16755955630b344bc9e35272c56",
"rev": "0948aeedc296f964140d9429223c7e4a0702a1ff",
"type": "github"
},
"original": {
@ -23,16 +23,16 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1739333913,
"narHash": "sha256-JXt5FtySR+yBm5ny8zG/hX1IybF/7R66jZfXxXSb6wY=",
"lastModified": 1743231893,
"narHash": "sha256-tpJsHMUPEhEnzySoQxx7+kA+KUtgWqvlcUBqROYNNt0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "7d83f668aee9e41d574c398a9bb569047e8a3f5d",
"rev": "c570c1f5304493cafe133b8d843c7c1c4a10d3a6",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.11-small",
"ref": "nixos-24.11",
"repo": "nixpkgs",
"type": "github"
}

166
flake.nix
View File

@ -2,7 +2,7 @@
description = "fortify sandbox tool and nixos module";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11-small";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
home-manager = {
url = "github:nix-community/home-manager/release-24.11";
@ -27,12 +27,12 @@
nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
in
{
nixosModules.fortify = import ./nixos.nix;
nixosModules.fortify = import ./nixos.nix self.packages;
buildPackage = forAllSystems (
system:
nixpkgsFor.${system}.callPackage (
import ./bundle.nix {
import ./cmd/fpkg/build.nix {
inherit
nixpkgsFor
system
@ -57,18 +57,30 @@
;
in
{
check-formatting =
runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; }
''
cd ${./.}
fortify = callPackage ./test { inherit system self; };
race = callPackage ./test {
inherit system self;
withRace = true;
};
echo "running nixfmt..."
nixfmt --check .
sandbox = callPackage ./test/sandbox { inherit self; };
sandbox-race = callPackage ./test/sandbox {
inherit self;
withRace = true;
};
touch $out
'';
fpkg = callPackage ./cmd/fpkg/test { inherit system self; };
check-lint =
formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } ''
cd ${./.}
echo "running nixfmt..."
nixfmt --width=256 --check .
touch $out
'';
lint =
runCommandLocal "check-lint"
{
nativeBuildInputs = [
@ -87,120 +99,68 @@
touch $out
'';
nixos-tests = callPackage ./test.nix { inherit system self home-manager; };
}
);
packages = forAllSystems (
system:
let
inherit (self.packages.${system}) fortify;
inherit (self.packages.${system}) fortify fsu;
pkgs = nixpkgsFor.${system};
in
{
default = self.packages.${system}.fortify;
fortify = pkgs.callPackage ./package.nix { };
default = fortify;
fortify = pkgs.pkgsStatic.callPackage ./package.nix {
inherit (pkgs)
# passthru.buildInputs
go
gcc
dist =
pkgs.runCommand "${fortify.name}-dist" { inherit (self.devShells.${system}.default) buildInputs; }
''
# go requires XDG_CACHE_HOME for the build cache
export XDG_CACHE_HOME="$(mktemp -d)"
# nativeBuildInputs
pkg-config
wayland-scanner
makeBinaryWrapper
# get a different workdir as go does not like /build
cd $(mktemp -d) && cp -r ${fortify.src}/. . && chmod -R +w .
# appPackages
glibc
xdg-dbus-proxy
export FORTIFY_VERSION="v${fortify.version}"
./dist/release.sh && mkdir $out && cp -v "dist/fortify-$FORTIFY_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"
'';
# fpkg
zstd
gnutar
coreutils
;
};
fsu = pkgs.callPackage ./cmd/fsu/package.nix { inherit (self.packages.${system}) fortify; };
dist = pkgs.runCommand "${fortify.name}-dist" { buildInputs = fortify.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 +w cmd && cp -r ${fsu.src}/. cmd/fsu/ \
&& chmod -R +w .
export FORTIFY_VERSION="v${fortify.version}"
./dist/release.sh && mkdir $out && cp -v "dist/fortify-$FORTIFY_VERSION.tar.gz"* $out
'';
}
);
devShells = forAllSystems (
system:
let
inherit (self.packages.${system}) fortify fhs;
inherit (self.packages.${system}) fortify;
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 = fortify.targetPkgs; };
withPackage = pkgs.mkShell { buildInputs = [ fortify ] ++ fortify.targetPkgs; };
generateDoc =
let
pkgs = nixpkgsFor.${system};
inherit (pkgs) lib;
doc =
@ -209,7 +169,7 @@
specialArgs = {
inherit pkgs;
};
modules = [ ./options.nix ];
modules = [ (import ./options.nix self.packages) ];
};
cleanEval = lib.filterAttrsRecursive (n: _: n != "_module") eval;
in
@ -219,7 +179,7 @@
sed -i '/*Declared by:*/,+1 d' $out
'';
in
nixpkgsFor.${system}.mkShell {
pkgs.mkShell {
shellHook = ''
exec cat ${docText} > options.md
'';

47
fst/app.go Normal file
View File

@ -0,0 +1,47 @@
// Package fst exports shared fortify types.
package fst
import (
"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(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

@ -2,18 +2,23 @@ package fst
import (
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/system"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
"git.gensokyo.uk/security/fortify/system"
)
const Tmp = "/.fortify"
// Config is used to seal an app
type Config struct {
// application ID
// 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"`
// value passed through to the child process as its argv
Command []string `json:"command"`
// absolute path to executable file
Path string `json:"path,omitempty"`
// final args passed to container init
Args []string `json:"args"`
Confinement ConfinementConfig `json:"confinement"`
}
@ -24,15 +29,17 @@ type ConfinementConfig struct {
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
// passwd username in container, defaults to passwd name of target uid or chronos
Username string `json:"username,omitempty"`
// home directory in sandbox, empty for outer
// home directory in container, empty for outer
Inner string `json:"home_inner"`
// home directory in init namespace
Outer string `json:"home"`
// bwrap sandbox confinement configuration
// absolute path to shell, empty for host shell
Shell string `json:"shell,omitempty"`
// abstract sandbox configuration
Sandbox *SandboxConfig `json:"sandbox"`
// extra acl entries to append
// extra acl ops, runs after everything else
ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
// reference to a system D-Bus proxy configuration,
@ -42,8 +49,8 @@ type ConfinementConfig struct {
// 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"`
// system resources to expose to the container
Enablements system.Enablement `json:"enablements"`
}
type ExtraPermConfig struct {
@ -74,24 +81,12 @@ func (e *ExtraPermConfig) String() string {
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{
ID: "org.chromium.Chromium",
Path: "/run/current-system/sw/bin/chromium",
Args: []string{
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
@ -104,13 +99,16 @@ func Template() *Config {
Username: "chronos",
Outer: "/var/lib/persist/home/org.chromium.Chromium",
Inner: "/var/lib/fortify",
Shell: "/run/current-system/sw/bin/zsh",
Sandbox: &SandboxConfig{
Hostname: "localhost",
UserNS: true,
Devel: true,
Userns: true,
Net: true,
Dev: true,
Syscall: &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
NoNewSession: true,
Seccomp: seccomp.FlagMultiarch,
Tty: true,
Multiarch: true,
MapRealUID: true,
DirectWayland: false,
// example API credentials pulled from Google Chrome
@ -129,10 +127,10 @@ func Template() *Config {
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"},
Link: [][2]string{{"/run/user/65534", "/run/user/150"}},
Etc: "/etc",
AutoEtc: true,
Cover: []string{"/var/run/nscd"},
},
ExtraPerms: []*ExtraPermConfig{
{Path: "/var/lib/fortify/u0", Ensure: true, Execute: true},
@ -158,7 +156,7 @@ func Template() *Config {
Log: false,
Filter: true,
},
Enablements: system.EWayland.Mask() | system.EDBus.Mask() | system.EPulse.Mask(),
Enablements: system.EWayland | system.EDBus | system.EPulse,
},
}
}

View File

@ -4,111 +4,153 @@ import (
"errors"
"fmt"
"io/fs"
"maps"
"path"
"slices"
"syscall"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
)
// 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
DirectWayland bool `json:"direct_wayland,omitempty"`
type (
SandboxConfig struct {
// container hostname
Hostname string `json:"hostname,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"`
// paths to override by mounting tmpfs over them
Override []string `json:"override"`
}
// extra seccomp flags
Seccomp seccomp.SyscallOpts `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"`
// expose main process tty
Tty bool `json:"tty,omitempty"`
// allow multiarch
Multiarch bool `json:"multiarch,omitempty"`
// 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(os linux.System) (*bwrap.Config, error) {
// 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"`
// expose all devices
Dev bool `json:"dev,omitempty"`
// container host filesystem bind mounts
Filesystem []*FilesystemConfig `json:"filesystem"`
// create symlinks inside container filesystem
Link [][2]string `json:"symlink"`
// 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"`
// 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"`
}
// SandboxSys encapsulates system functions used during [sandbox.Container] initialisation.
SandboxSys interface {
Getuid() int
Getgid() int
Paths() Paths
ReadDir(name string) ([]fs.DirEntry, error)
EvalSymlinks(path string) (string, error)
Println(v ...any)
Printf(format string, v ...any)
}
// FilesystemConfig is a representation of [sandbox.BindMount].
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"`
}
)
// ToContainer initialises [sandbox.Params] via [SandboxConfig].
// Note that remaining container setup must be queued by the [App] implementation.
func (s *SandboxConfig) ToContainer(sys SandboxSys, uid, gid *int) (*sandbox.Params, map[string]string, error) {
if s == nil {
return nil, errors.New("nil sandbox config")
return nil, nil, syscall.EBADE
}
if s.Syscall == nil {
fmsg.VPrintln("syscall filter not configured, PROCEED WITH CAUTION")
}
var uid int
if !s.MapRealUID {
uid = 65534
} else {
uid = os.Geteuid()
}
conf := (&bwrap.Config{
Net: s.Net,
UserNS: s.UserNS,
container := &sandbox.Params{
Hostname: s.Hostname,
Clearenv: true,
SetEnv: s.Env,
Ops: new(sandbox.Ops),
Seccomp: s.Seccomp,
}
/* 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),
if s.Multiarch {
container.Seccomp |= seccomp.FlagMultiarch
}
Syscall: s.Syscall,
NewSession: !s.NoNewSession,
DieWithParent: true,
AsInit: true,
/* 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 */
*container.Ops = slices.Grow(*container.Ops, 1<<8)
// initialise unconditionally as Once cannot be justified
// for saving such a miniscule amount of memory
Chmod: make(bwrap.ChmodConfig),
}).
SetUID(uid).SetGID(uid).
Procfs("/proc").
Tmpfs(Tmp, 4*1024)
if s.Devel {
container.Flags |= sandbox.FAllowDevel
}
if s.Userns {
container.Flags |= sandbox.FAllowUserns
}
if s.Net {
container.Flags |= sandbox.FAllowNet
}
if s.Tty {
container.Flags |= sandbox.FAllowTTY
}
if s.MapRealUID {
/* some programs fail to connect to dbus session running as a different uid
so this workaround is introduced to map priv-side caller uid in container */
container.Uid = sys.Getuid()
*uid = container.Uid
container.Gid = sys.Getgid()
*gid = container.Gid
} else {
*uid = sandbox.OverflowUid()
*gid = sandbox.OverflowGid()
}
container.
Proc("/proc").
Tmpfs(Tmp, 1<<12, 0755)
if !s.Dev {
conf.DevTmpfs("/dev").Mqueue("/dev/mqueue")
container.Dev("/dev").Mqueue("/dev/mqueue")
} else {
conf.Bind("/dev", "/dev", false, true, true)
container.Bind("/dev", "/dev", sandbox.BindDevice)
}
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
/* retrieve paths and hide them if they're made available in the sandbox;
this feature tries to improve user experience of permissive defaults, and
to warn about issues in custom configuration; it is NOT a security feature
and should not be treated as such, ALWAYS be careful with what you bind */
var hidePaths []string
sc := os.Paths()
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
return nil, nil, err
} else {
// there is usually only one, do not preallocate
for _, entry := range entries {
@ -121,11 +163,11 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
// get parent dir of socket
dir := path.Dir(pair[1])
if dir == "." || dir == "/" {
fmsg.VPrintf("dbus socket %q is in an unusual location", pair[1])
sys.Printf("dbus socket %q is in an unusual location", pair[1])
}
hidePaths = append(hidePaths, dir)
} else {
fmsg.VPrintf("dbus socket %q is not absolute", pair[1])
sys.Printf("dbus socket %q is not absolute", pair[1])
}
}
}
@ -133,8 +175,8 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
}
hidePathMatch := make([]bool, len(hidePaths))
for i := range hidePaths {
if err := evalSymlinks(os, &hidePaths[i]); err != nil {
return nil, err
if err := evalSymlinks(sys, &hidePaths[i]); err != nil {
return nil, nil, err
}
}
@ -144,19 +186,19 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
}
if !path.IsAbs(c.Src) {
return nil, fmt.Errorf("src path %q is not absolute", c.Src)
return nil, 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)
return nil, nil, fmt.Errorf("dst path %q is not absolute", dest)
}
srcH := c.Src
if err := evalSymlinks(os, &srcH); err != nil {
return nil, err
if err := evalSymlinks(sys, &srcH); err != nil {
return nil, nil, err
}
for i := range hidePaths {
@ -166,62 +208,77 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) {
}
if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil {
return nil, err
return nil, nil, err
} else if ok {
hidePathMatch[i] = true
fmsg.VPrintf("hiding paths from %q", c.Src)
sys.Printf("hiding paths from %q", c.Src)
}
}
conf.Bind(c.Src, dest, !c.Must, c.Write, c.Device)
var flags int
if c.Write {
flags |= sandbox.BindWritable
}
if c.Device {
flags |= sandbox.BindDevice | sandbox.BindWritable
}
if !c.Must {
flags |= sandbox.BindOptional
}
container.Bind(c.Src, dest, flags)
}
// hide marked paths before setting up shares
// cover matched paths
for i, ok := range hidePathMatch {
if ok {
conf.Tmpfs(hidePaths[i], 8192)
container.Tmpfs(hidePaths[i], 1<<13, 0755)
}
}
for _, l := range s.Link {
conf.Symlink(l[0], l[1])
container.Link(l[0], l[1])
}
if s.AutoEtc {
etc := s.Etc
if etc == "" {
etc = "/etc"
// perf: this might work better if implemented as a setup op in container init
if !s.AutoEtc {
if s.Etc != "" {
container.Bind(s.Etc, "/etc", 0)
}
conf.Bind(etc, Tmp+"/etc")
} else {
etcPath := s.Etc
if etcPath == "" {
etcPath = "/etc"
}
container.Bind(etcPath, Tmp+"/etc", 0)
// link host /etc contents to prevent passwd/group from being overwritten
if d, err := os.ReadDir(etc); err != nil {
return nil, err
// link host /etc contents to prevent dropping passwd/group bind mounts
if d, err := sys.ReadDir(etcPath); err != nil {
return nil, nil, err
} else {
for _, ent := range d {
name := ent.Name()
switch name {
n := ent.Name()
switch n {
case "passwd":
case "group":
case "mtab":
conf.Symlink("/proc/mounts", "/etc/"+name)
container.Link("/proc/mounts", "/etc/"+n)
default:
conf.Symlink(Tmp+"/etc/"+name, "/etc/"+name)
container.Link(Tmp+"/etc/"+n, "/etc/"+n)
}
}
}
}
return conf, nil
return container, maps.Clone(s.Env), nil
}
func evalSymlinks(os linux.System, v *string) error {
if p, err := os.EvalSymlinks(*v); err != nil {
func evalSymlinks(sys SandboxSys, v *string) error {
if p, err := sys.EvalSymlinks(*v); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return err
}
fmsg.VPrintf("path %q does not yet exist", *v)
sys.Printf("path %q does not yet exist", *v)
} else {
*v = p
}

View File

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

2
go.mod
View File

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

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"
)
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,188 +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
}
}
// Write copy from FD to destination DEST
// (--file FD DEST)
func (c *Config) Write(dest string, payload []byte) *Config {
c.Filesystem = append(c.Filesystem, &DataConfig{Dest: dest, Data: payload, 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 {
t := DataROBind
if len(opts) > 0 && opts[0] {
t = DataBind
}
c.Filesystem = append(c.Filesystem, &DataConfig{Dest: dest, Data: payload, Type: t})
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 (
"os"
"slices"
"testing"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/helper/seccomp"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
func TestConfig_Args(t *testing.T) {
seccomp.CPrintln = fmsg.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)).
Write("/.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/fortify/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, FortifyHelper+"=1")
} else {
h.Env = append(h.Env, FortifyHelper+"=0")
}
if h.useStatFd {
h.Env = append(h.Env, FortifyStatus+"=1")
// stat is populated on fulfill
h.Cancel = func() error { return h.stat.Close() }
} else {
h.Env = append(h.Env, FortifyStatus+"=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/fortify/helper"
)
func TestCmd(t *testing.T) {
t.Run("start non-existent helper path", func(t *testing.T) {
h := helper.NewDirect(context.Background(), "/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(context.TODO(), "fortify", argsWt, false, argF, nil, nil); got == nil {
t.Errorf("NewDirect(%q, %q) got nil",
argsWt, "fortify")
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/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/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, FortifyHelper+"=1")
} else {
h.Env = append(h.Env, FortifyHelper+"=0")
}
if h.useStatFd {
h.Env = append(h.Env, FortifyStatus+"=1")
// stat is populated on fulfill
h.Cancel = func(*exec.Cmd) error { return h.stat.Close() }
} else {
h.Env = append(h.Env, FortifyStatus+"=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/fortify/helper"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/sandbox"
)
func TestContainer(t *testing.T) {
t.Run("start empty container", func(t *testing.T) {
h := helper.New(context.Background(), "/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(context.TODO(), "fortify", argsWt, false, argF, nil, nil); got == nil {
t.Errorf("New(%q, %q) got nil",
argsWt, "fortify")
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(fmsg.Output{})
sandbox.Init(fmsg.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,17 +6,12 @@ import (
"fmt"
"io"
"os"
"os/exec"
"slices"
"syscall"
"time"
"git.gensokyo.uk/security/fortify/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.
@ -26,62 +21,56 @@ const (
)
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,6 +4,8 @@ import (
"context"
"errors"
"fmt"
"io"
"os"
"strconv"
"strings"
"testing"
@ -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(context.Background(), 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)
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,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,25 +1,17 @@
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/fmsg"
)
// 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 {
@ -33,30 +25,9 @@ func InternalChildStub() {
sp = v
}
switch os.Args[3] {
case "bwrap":
bwrapStub()
default:
genericStub(flagRestoreFiles(4, ap, sp))
}
genericStub(flagRestoreFiles(3, ap, sp))
fmsg.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

@ -6,6 +6,4 @@ import (
"git.gensokyo.uk/security/fortify/helper"
)
func TestHelperChildStub(t *testing.T) {
helper.InternalChildStub()
}
func TestHelperStub(t *testing.T) { helper.InternalHelperStub() }

View File

@ -2,71 +2,81 @@ package app
import (
"context"
"fmt"
"log"
"sync"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/priv/shim"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/sys"
)
type App interface {
// ID returns a copy of App's unique ID.
ID() fst.ID
// Run sets up the system and runs the App.
Run(ctx context.Context, rs *RunState) error
func New(ctx context.Context, os sys.State) (fst.App, error) {
a := new(app)
a.sys = os
a.ctx = ctx
Seal(config *fst.Config) error
String() string
id := new(fst.ID)
err := fst.NewAppID(id)
a.id = newID(id)
return a, err
}
type RunState struct {
// Start is true if fsu is successfully started.
Start bool
// ExitCode is the value returned by shim.
ExitCode int
// WaitErr is error returned by the underlying wait syscall.
WaitErr error
func MustNew(ctx context.Context, os sys.State) fst.App {
a, err := New(ctx, os)
if err != nil {
log.Fatalf("cannot create app: %v", err)
}
return a
}
type app struct {
// application unique identifier
id *fst.ID
// operating system interface
os linux.System
// shim process manager
shim *shim.Shim
// child process related information
seal *appSeal
id *stringPair[fst.ID]
sys sys.State
ctx context.Context
lock sync.RWMutex
*outcome
mu sync.RWMutex
}
func (a *app) ID() fst.ID {
return *a.id
}
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 fortified app)"
return "(invalid app)"
}
a.lock.RLock()
defer a.lock.RUnlock()
a.mu.RLock()
defer a.mu.RUnlock()
if a.shim != nil {
return a.shim.String()
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)
}
if a.seal != nil {
return "(sealed fortified app as uid " + a.seal.sys.user.us + ")"
}
return "(unsealed fortified app)"
return fmt.Sprintf("(unsealed app %s)", a.id)
}
func New(os linux.System) (App, error) {
a := new(app)
a.id = new(fst.ID)
a.os = os
return a, fst.NewAppID(a.id)
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.ctx, a.sys, config)
if err == nil {
a.outcome = seal
}
return seal, err
}

View File

@ -4,28 +4,28 @@ 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/internal/system"
"git.gensokyo.uk/security/fortify/sandbox"
"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"},
ID: "org.chromium.Chromium",
Path: "/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,
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},
}, AutoEtc: true,
Override: []string{"/var/run/nscd"},
},
Cover: []string{"/var/run/nscd"},
},
SystemBus: &dbus.Config{
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
@ -45,7 +45,7 @@ var testCasesNixos = []sealTestCase{
Call: map[string]string{}, Broadcast: map[string]string{},
Filter: true,
},
Enablements: system.EWayland.Mask() | system.EDBus.Mask() | system.EPulse.Mask(),
Enablements: system.EWayland | system.EDBus | system.EPulse,
},
},
fst.ID{
@ -56,15 +56,15 @@ var testCasesNixos = []sealTestCase{
},
system.New(1000001).
Ensure("/tmp/fortify.1971", 0711).
Ephemeral(system.Process, "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1", 0711).
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).
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, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", acl.Execute).
UpdatePermType(system.EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute).
Ephemeral(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", acl.Execute).
Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse").
CopyFile("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/pulse-cookie", "/home/ophestra/xdg/config/pulse/cookie").
CopyFile(nil, "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
Ephemeral(system.Process, "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1", 0711).
MustProxyDBus("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", &dbus.Config{
Talk: []string{
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
@ -88,136 +88,133 @@ var testCasesNixos = []sealTestCase{
}).
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",
&sandbox.Params{
Uid: 1971,
Gid: 100,
Flags: sandbox.FAllowNet | sandbox.FAllowUserns,
Dir: "/var/lib/persist/module/fortify/0/1",
Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
Args: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
Env: []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").
Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", false, true).
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/1971", 8388608).
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").
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/pulse-cookie", fst.Tmp+"/pulse-cookie").
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"),
Ops: new(sandbox.Ops).
Proc("/proc").
Tmpfs(fst.Tmp, 4096, 0755).
Dev("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", 0).
Bind("/usr/bin", "/usr/bin", 0).
Bind("/nix/store", "/nix/store", 0).
Bind("/run/current-system", "/run/current-system", 0).
Bind("/sys/block", "/sys/block", sandbox.BindOptional).
Bind("/sys/bus", "/sys/bus", sandbox.BindOptional).
Bind("/sys/class", "/sys/class", sandbox.BindOptional).
Bind("/sys/dev", "/sys/dev", sandbox.BindOptional).
Bind("/sys/devices", "/sys/devices", sandbox.BindOptional).
Bind("/run/opengl-driver", "/run/opengl-driver", 0).
Bind("/dev/dri", "/dev/dri", sandbox.BindDevice|sandbox.BindWritable|sandbox.BindOptional).
Bind("/etc", fst.Tmp+"/etc", 0).
Link(fst.Tmp+"/etc/alsa", "/etc/alsa").
Link(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
Link(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
Link(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
Link(fst.Tmp+"/etc/default", "/etc/default").
Link(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
Link(fst.Tmp+"/etc/fonts", "/etc/fonts").
Link(fst.Tmp+"/etc/fstab", "/etc/fstab").
Link(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
Link(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
Link(fst.Tmp+"/etc/hostid", "/etc/hostid").
Link(fst.Tmp+"/etc/hostname", "/etc/hostname").
Link(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Link(fst.Tmp+"/etc/hosts", "/etc/hosts").
Link(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
Link(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
Link(fst.Tmp+"/etc/issue", "/etc/issue").
Link(fst.Tmp+"/etc/kbd", "/etc/kbd").
Link(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
Link(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
Link(fst.Tmp+"/etc/localtime", "/etc/localtime").
Link(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
Link(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
Link(fst.Tmp+"/etc/lvm", "/etc/lvm").
Link(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
Link(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
Link(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
Link(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
Link("/proc/mounts", "/etc/mtab").
Link(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
Link(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
Link(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
Link(fst.Tmp+"/etc/nix", "/etc/nix").
Link(fst.Tmp+"/etc/nixos", "/etc/nixos").
Link(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
Link(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
Link(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
Link(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
Link(fst.Tmp+"/etc/os-release", "/etc/os-release").
Link(fst.Tmp+"/etc/pam", "/etc/pam").
Link(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
Link(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
Link(fst.Tmp+"/etc/pki", "/etc/pki").
Link(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
Link(fst.Tmp+"/etc/profile", "/etc/profile").
Link(fst.Tmp+"/etc/protocols", "/etc/protocols").
Link(fst.Tmp+"/etc/qemu", "/etc/qemu").
Link(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
Link(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
Link(fst.Tmp+"/etc/rpc", "/etc/rpc").
Link(fst.Tmp+"/etc/samba", "/etc/samba").
Link(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
Link(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
Link(fst.Tmp+"/etc/services", "/etc/services").
Link(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
Link(fst.Tmp+"/etc/shadow", "/etc/shadow").
Link(fst.Tmp+"/etc/shells", "/etc/shells").
Link(fst.Tmp+"/etc/ssh", "/etc/ssh").
Link(fst.Tmp+"/etc/ssl", "/etc/ssl").
Link(fst.Tmp+"/etc/static", "/etc/static").
Link(fst.Tmp+"/etc/subgid", "/etc/subgid").
Link(fst.Tmp+"/etc/subuid", "/etc/subuid").
Link(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
Link(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
Link(fst.Tmp+"/etc/systemd", "/etc/systemd").
Link(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
Link(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
Link(fst.Tmp+"/etc/udev", "/etc/udev").
Link(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
Link(fst.Tmp+"/etc/UPower", "/etc/UPower").
Link(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
Link(fst.Tmp+"/etc/X11", "/etc/X11").
Link(fst.Tmp+"/etc/zfs", "/etc/zfs").
Link(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
Link(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
Link(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Tmpfs("/run/user", 4096, 0755).
Tmpfs("/run/user/1971", 8388608, 0700).
Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", sandbox.BindWritable).
Bind("/var/lib/persist/module/fortify/0/1", "/var/lib/persist/module/fortify/0/1", sandbox.BindWritable).
Place("/etc/passwd", []byte("u0_a1:x:1971:100:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n")).
Place("/etc/group", []byte("fortify:x:100:\n")).
Bind("/run/user/1971/wayland-0", "/run/user/1971/wayland-0", 0).
Bind("/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native", 0).
Place(fst.Tmp+"/pulse-cookie", nil).
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus", 0).
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket", 0).
Tmpfs("/var/run/nscd", 8192, 0755),
},
},
}

View File

@ -1,18 +1,19 @@
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/internal/system"
"git.gensokyo.uk/security/fortify/sandbox"
"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",
@ -27,142 +28,134 @@ var testCasesPd = []sealTestCase{
},
system.New(1000000).
Ensure("/tmp/fortify.1971", 0711).
Ephemeral(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac", 0711).
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).
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, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 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").
Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", false, true).
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/65534", 8388608).
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"),
Ensure("/tmp/fortify.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute),
&sandbox.Params{
Flags: sandbox.FAllowNet | sandbox.FAllowUserns | sandbox.FAllowTTY,
Dir: "/home/chronos",
Path: "/run/current-system/sw/bin/zsh",
Args: []string{"/run/current-system/sw/bin/zsh"},
Env: []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",
},
Ops: new(sandbox.Ops).
Proc("/proc").
Tmpfs(fst.Tmp, 4096, 0755).
Dev("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", sandbox.BindWritable).
Bind("/boot", "/boot", sandbox.BindWritable).
Bind("/home", "/home", sandbox.BindWritable).
Bind("/lib", "/lib", sandbox.BindWritable).
Bind("/lib64", "/lib64", sandbox.BindWritable).
Bind("/nix", "/nix", sandbox.BindWritable).
Bind("/root", "/root", sandbox.BindWritable).
Bind("/run", "/run", sandbox.BindWritable).
Bind("/srv", "/srv", sandbox.BindWritable).
Bind("/sys", "/sys", sandbox.BindWritable).
Bind("/usr", "/usr", sandbox.BindWritable).
Bind("/var", "/var", sandbox.BindWritable).
Bind("/dev/kvm", "/dev/kvm", sandbox.BindWritable|sandbox.BindDevice|sandbox.BindOptional).
Tmpfs("/run/user/1971", 8192, 0755).
Tmpfs("/run/dbus", 8192, 0755).
Bind("/etc", fst.Tmp+"/etc", 0).
Link(fst.Tmp+"/etc/alsa", "/etc/alsa").
Link(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
Link(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
Link(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
Link(fst.Tmp+"/etc/default", "/etc/default").
Link(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
Link(fst.Tmp+"/etc/fonts", "/etc/fonts").
Link(fst.Tmp+"/etc/fstab", "/etc/fstab").
Link(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
Link(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
Link(fst.Tmp+"/etc/hostid", "/etc/hostid").
Link(fst.Tmp+"/etc/hostname", "/etc/hostname").
Link(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Link(fst.Tmp+"/etc/hosts", "/etc/hosts").
Link(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
Link(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
Link(fst.Tmp+"/etc/issue", "/etc/issue").
Link(fst.Tmp+"/etc/kbd", "/etc/kbd").
Link(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
Link(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
Link(fst.Tmp+"/etc/localtime", "/etc/localtime").
Link(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
Link(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
Link(fst.Tmp+"/etc/lvm", "/etc/lvm").
Link(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
Link(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
Link(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
Link(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
Link("/proc/mounts", "/etc/mtab").
Link(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
Link(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
Link(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
Link(fst.Tmp+"/etc/nix", "/etc/nix").
Link(fst.Tmp+"/etc/nixos", "/etc/nixos").
Link(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
Link(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
Link(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
Link(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
Link(fst.Tmp+"/etc/os-release", "/etc/os-release").
Link(fst.Tmp+"/etc/pam", "/etc/pam").
Link(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
Link(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
Link(fst.Tmp+"/etc/pki", "/etc/pki").
Link(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
Link(fst.Tmp+"/etc/profile", "/etc/profile").
Link(fst.Tmp+"/etc/protocols", "/etc/protocols").
Link(fst.Tmp+"/etc/qemu", "/etc/qemu").
Link(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
Link(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
Link(fst.Tmp+"/etc/rpc", "/etc/rpc").
Link(fst.Tmp+"/etc/samba", "/etc/samba").
Link(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
Link(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
Link(fst.Tmp+"/etc/services", "/etc/services").
Link(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
Link(fst.Tmp+"/etc/shadow", "/etc/shadow").
Link(fst.Tmp+"/etc/shells", "/etc/shells").
Link(fst.Tmp+"/etc/ssh", "/etc/ssh").
Link(fst.Tmp+"/etc/ssl", "/etc/ssl").
Link(fst.Tmp+"/etc/static", "/etc/static").
Link(fst.Tmp+"/etc/subgid", "/etc/subgid").
Link(fst.Tmp+"/etc/subuid", "/etc/subuid").
Link(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
Link(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
Link(fst.Tmp+"/etc/systemd", "/etc/systemd").
Link(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
Link(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
Link(fst.Tmp+"/etc/udev", "/etc/udev").
Link(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
Link(fst.Tmp+"/etc/UPower", "/etc/UPower").
Link(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
Link(fst.Tmp+"/etc/X11", "/etc/X11").
Link(fst.Tmp+"/etc/zfs", "/etc/zfs").
Link(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
Link(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
Link(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Tmpfs("/run/user", 4096, 0755).
Tmpfs("/run/user/65534", 8388608, 0700).
Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", sandbox.BindWritable).
Bind("/home/chronos", "/home/chronos", sandbox.BindWritable).
Place("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
Place("/etc/group", []byte("fortify:x:65534:\n")).
Tmpfs("/var/run/nscd", 8192, 0755),
},
},
{
"nixos permissive defaults chromium", new(stubNixOS),
&fst.Config{
ID: "org.chromium.Chromium",
Command: []string{"/run/current-system/sw/bin/zsh", "-c", "exec chromium "},
ID: "org.chromium.Chromium",
Args: []string{"zsh", "-c", "exec chromium "},
Confinement: fst.ConfinementConfig{
AppID: 9,
Groups: []string{"video"},
@ -199,7 +192,7 @@ var testCasesPd = []sealTestCase{
},
Filter: true,
},
Enablements: system.EWayland.Mask() | system.EDBus.Mask() | system.EPulse.Mask(),
Enablements: system.EWayland | system.EDBus | system.EPulse,
},
},
fst.ID{
@ -210,16 +203,15 @@ var testCasesPd = []sealTestCase{
},
system.New(1000009).
Ensure("/tmp/fortify.1971", 0711).
Ephemeral(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c", 0711).
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).
Ephemeral(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c", 0711).
Wayland(new(*os.File), "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/1971/wayland-0", "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
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, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", acl.Execute).
Ensure("/tmp/fortify.1971/wayland", 0711).
Wayland("/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("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie", "/home/ophestra/xdg/config/pulse/cookie").
CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
MustProxyDBus("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", &dbus.Config{
Talk: []string{
"org.freedesktop.Notifications",
@ -252,141 +244,136 @@ var testCasesPd = []sealTestCase{
}).
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",
&sandbox.Params{
Flags: sandbox.FAllowNet | sandbox.FAllowUserns | sandbox.FAllowTTY,
Dir: "/home/chronos",
Path: "/run/current-system/sw/bin/zsh",
Args: []string{"zsh", "-c", "exec chromium "},
Env: []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").
Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", false, true).
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/65534", 8388608).
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").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/pulse-cookie", fst.Tmp+"/pulse-cookie").
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"),
Ops: new(sandbox.Ops).
Proc("/proc").
Tmpfs(fst.Tmp, 4096, 0755).
Dev("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", sandbox.BindWritable).
Bind("/boot", "/boot", sandbox.BindWritable).
Bind("/home", "/home", sandbox.BindWritable).
Bind("/lib", "/lib", sandbox.BindWritable).
Bind("/lib64", "/lib64", sandbox.BindWritable).
Bind("/nix", "/nix", sandbox.BindWritable).
Bind("/root", "/root", sandbox.BindWritable).
Bind("/run", "/run", sandbox.BindWritable).
Bind("/srv", "/srv", sandbox.BindWritable).
Bind("/sys", "/sys", sandbox.BindWritable).
Bind("/usr", "/usr", sandbox.BindWritable).
Bind("/var", "/var", sandbox.BindWritable).
Bind("/dev/dri", "/dev/dri", sandbox.BindWritable|sandbox.BindDevice|sandbox.BindOptional).
Bind("/dev/kvm", "/dev/kvm", sandbox.BindWritable|sandbox.BindDevice|sandbox.BindOptional).
Tmpfs("/run/user/1971", 8192, 0755).
Tmpfs("/run/dbus", 8192, 0755).
Bind("/etc", fst.Tmp+"/etc", 0).
Link(fst.Tmp+"/etc/alsa", "/etc/alsa").
Link(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
Link(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
Link(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
Link(fst.Tmp+"/etc/default", "/etc/default").
Link(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
Link(fst.Tmp+"/etc/fonts", "/etc/fonts").
Link(fst.Tmp+"/etc/fstab", "/etc/fstab").
Link(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
Link(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
Link(fst.Tmp+"/etc/hostid", "/etc/hostid").
Link(fst.Tmp+"/etc/hostname", "/etc/hostname").
Link(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Link(fst.Tmp+"/etc/hosts", "/etc/hosts").
Link(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
Link(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
Link(fst.Tmp+"/etc/issue", "/etc/issue").
Link(fst.Tmp+"/etc/kbd", "/etc/kbd").
Link(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
Link(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
Link(fst.Tmp+"/etc/localtime", "/etc/localtime").
Link(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
Link(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
Link(fst.Tmp+"/etc/lvm", "/etc/lvm").
Link(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
Link(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
Link(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
Link(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
Link("/proc/mounts", "/etc/mtab").
Link(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
Link(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
Link(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
Link(fst.Tmp+"/etc/nix", "/etc/nix").
Link(fst.Tmp+"/etc/nixos", "/etc/nixos").
Link(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
Link(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
Link(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
Link(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
Link(fst.Tmp+"/etc/os-release", "/etc/os-release").
Link(fst.Tmp+"/etc/pam", "/etc/pam").
Link(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
Link(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
Link(fst.Tmp+"/etc/pki", "/etc/pki").
Link(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
Link(fst.Tmp+"/etc/profile", "/etc/profile").
Link(fst.Tmp+"/etc/protocols", "/etc/protocols").
Link(fst.Tmp+"/etc/qemu", "/etc/qemu").
Link(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
Link(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
Link(fst.Tmp+"/etc/rpc", "/etc/rpc").
Link(fst.Tmp+"/etc/samba", "/etc/samba").
Link(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
Link(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
Link(fst.Tmp+"/etc/services", "/etc/services").
Link(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
Link(fst.Tmp+"/etc/shadow", "/etc/shadow").
Link(fst.Tmp+"/etc/shells", "/etc/shells").
Link(fst.Tmp+"/etc/ssh", "/etc/ssh").
Link(fst.Tmp+"/etc/ssl", "/etc/ssl").
Link(fst.Tmp+"/etc/static", "/etc/static").
Link(fst.Tmp+"/etc/subgid", "/etc/subgid").
Link(fst.Tmp+"/etc/subuid", "/etc/subuid").
Link(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
Link(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
Link(fst.Tmp+"/etc/systemd", "/etc/systemd").
Link(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
Link(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
Link(fst.Tmp+"/etc/udev", "/etc/udev").
Link(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
Link(fst.Tmp+"/etc/UPower", "/etc/UPower").
Link(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
Link(fst.Tmp+"/etc/X11", "/etc/X11").
Link(fst.Tmp+"/etc/zfs", "/etc/zfs").
Link(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
Link(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
Link(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Tmpfs("/run/user", 4096, 0755).
Tmpfs("/run/user/65534", 8388608, 0700).
Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", sandbox.BindWritable).
Bind("/home/chronos", "/home/chronos", sandbox.BindWritable).
Place("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
Place("/etc/group", []byte("fortify:x:65534:\n")).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/65534/wayland-0", 0).
Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native", 0).
Place(fst.Tmp+"/pulse-cookie", nil).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus", 0).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket", 0).
Tmpfs("/var/run/nscd", 8192, 0755),
},
},
}

View File

@ -3,10 +3,11 @@ package app_test
import (
"fmt"
"io/fs"
"log"
"os/user"
"strconv"
"git.gensokyo.uk/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/fst"
)
// fs methods are not implemented using a real FS
@ -16,13 +17,17 @@ type stubNixOS struct {
usernameErr map[string]error
}
func (s *stubNixOS) Geteuid() int { return 1971 }
func (s *stubNixOS) Getuid() int { return 1971 }
func (s *stubNixOS) Getgid() int { return 100 }
func (s *stubNixOS) TempDir() string { return "/tmp" }
func (s *stubNixOS) MustExecutable() string { return "/run/wrappers/bin/fortify" }
func (s *stubNixOS) Exit(code int) { panic("called exit on stub with code " + strconv.Itoa(code)) }
func (s *stubNixOS) EvalSymlinks(path string) (string, error) { return path, nil }
func (s *stubNixOS) Uid(aid int) (int, error) { return 1000000 + 0*10000 + aid, nil }
func (s *stubNixOS) Println(v ...any) { log.Println(v...) }
func (s *stubNixOS) Printf(format string, v ...any) { log.Printf(format, v...) }
func (s *stubNixOS) LookupEnv(key string) (string, bool) {
switch key {
case "SHELL":
@ -50,10 +55,8 @@ func (s *stubNixOS) LookPath(file string) (string, error) {
}
switch file {
case "sudo":
return "/run/wrappers/bin/sudo", nil
case "machinectl":
return "/home/ophestra/.nix-profile/bin/machinectl", nil
case "zsh":
return "/run/current-system/sw/bin/zsh", nil
default:
panic(fmt.Sprintf("attempted to look up unexpected executable %q", file))
}
@ -122,8 +125,8 @@ func (s *stubNixOS) Open(name string) (fs.File, error) {
}
}
func (s *stubNixOS) Paths() linux.Paths {
return linux.Paths{
func (s *stubNixOS) Paths() fst.Paths {
return fst.Paths{
SharePath: "/tmp/fortify.1971",
RuntimePath: "/run/user/1971",
RunDirPath: "/run/user/1971/fortify",

View File

@ -8,19 +8,19 @@ import (
"time"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/system"
"git.gensokyo.uk/security/fortify/internal/sys"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system"
)
type sealTestCase struct {
name string
os linux.System
config *fst.Config
id fst.ID
wantSys *system.I
wantBwrap *bwrap.Config
name string
os sys.State
config *fst.Config
id fst.ID
wantSys *system.I
wantContainer *sandbox.Params
}
func TestApp(t *testing.T) {
@ -29,17 +29,21 @@ func TestApp(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
a := app.NewWithID(tc.id, tc.os)
var (
gotSys *system.I
gotContainer *sandbox.Params
)
if !t.Run("seal", func(t *testing.T) {
if err := a.Seal(tc.config); err != nil {
if sa, err := a.Seal(tc.config); err != nil {
t.Errorf("Seal: error = %v", err)
return
} else {
gotSys, gotContainer = app.AppIParams(a, sa)
}
}) {
return
}
gotSys, gotBwrap := app.AppSystemBwrap(a)
t.Run("compare sys", func(t *testing.T) {
if !gotSys.Equal(tc.wantSys) {
t.Errorf("Seal: sys = %#v, want %#v",
@ -47,10 +51,10 @@ func TestApp(t *testing.T) {
}
})
t.Run("compare bwrap", func(t *testing.T) {
if !reflect.DeepEqual(gotBwrap, tc.wantBwrap) {
t.Errorf("seal: bwrap =\n%s\n, want\n%s",
mustMarshal(gotBwrap), mustMarshal(tc.wantBwrap))
t.Run("compare params", func(t *testing.T) {
if !reflect.DeepEqual(gotContainer, tc.wantContainer) {
t.Errorf("seal: params =\n%s\n, want\n%s",
mustMarshal(gotContainer), mustMarshal(tc.wantContainer))
}
})
})

179
internal/app/errors.go Normal file
View File

@ -0,0 +1,179 @@
package app
import (
"errors"
"log"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
func PrintRunStateErr(rs *fst.RunState, runErr error) {
if runErr != nil {
if rs.Time == nil {
fmsg.PrintBaseError(runErr, "cannot start app:")
} else {
var e *fmsg.BaseError
if !fmsg.AsBaseError(runErr, &e) {
log.Println("wait failed:", runErr)
} else {
// Wait only returns either *app.ProcessError or *app.StateStoreError wrapped in a *app.BaseError
var se *StateStoreError
if !errors.As(runErr, &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 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())
}
}
}
}
}
}
if rs.ExitCode == 0 {
rs.ExitCode = 126
}
}
if rs.RevertErr != nil {
var stateStoreError *StateStoreError
if !errors.As(rs.RevertErr, &stateStoreError) || stateStoreError == nil {
fmsg.PrintBaseError(rs.RevertErr, "generic fault during cleanup:")
goto out
}
if stateStoreError.Err != nil {
if len(stateStoreError.Err) == 2 {
if stateStoreError.Err[0] != nil {
if joinedErrs, ok := stateStoreError.Err[0].(interface{ Unwrap() []error }); !ok {
fmsg.PrintBaseError(stateStoreError.Err[0], "generic fault during revert:")
} else {
for _, err := range joinedErrs.Unwrap() {
if err != nil {
fmsg.PrintBaseError(err, "fault during revert:")
}
}
}
}
if stateStoreError.Err[1] != nil {
log.Printf("cannot close store: %v", stateStoreError.Err[1])
}
} else {
log.Printf("fault during cleanup: %v",
errors.Join(stateStoreError.Err...))
}
}
if stateStoreError.OpErr != nil {
log.Printf("blind revert due to store fault: %v",
stateStoreError.OpErr)
}
if stateStoreError.DoErr != nil {
fmsg.PrintBaseError(stateStoreError.DoErr, "state store operation unsuccessful:")
}
if stateStoreError.Inner && stateStoreError.InnerErr != nil {
fmsg.PrintBaseError(stateStoreError.InnerErr, "cannot destroy state entry:")
}
out:
if rs.ExitCode == 0 {
rs.ExitCode = 128
}
}
if rs.WaitErr != nil {
log.Println("inner wait failed:", rs.WaitErr)
}
}
// StateStoreError is returned for a failed state save
type StateStoreError struct {
// whether inner function was called
Inner bool
// returned by the Save/Destroy method of [state.Cursor]
InnerErr error
// returned by the Do method of [state.Store]
DoErr error
// stores an arbitrary store operation error
OpErr error
// stores arbitrary errors
Err []error
}
// save saves arbitrary errors in [StateStoreError] once.
func (e *StateStoreError) save(errs []error) {
if len(errs) == 0 || e.Err != nil {
panic("invalid call to save")
}
e.Err = errs
}
func (e *StateStoreError) equiv(a ...any) error {
if e.Inner && e.InnerErr == nil && e.DoErr == nil && e.OpErr == nil && errors.Join(e.Err...) == nil {
return nil
} else {
return fmsg.WrapErrorSuffix(e, a...)
}
}
func (e *StateStoreError) Error() string {
if e.Inner && e.InnerErr != nil {
return e.InnerErr.Error()
}
if e.DoErr != nil {
return e.DoErr.Error()
}
if e.OpErr != nil {
return e.OpErr.Error()
}
if err := errors.Join(e.Err...); err != nil {
return err.Error()
}
// equiv nullifies e for values where this is reached
panic("unreachable")
}
func (e *StateStoreError) Unwrap() (errs []error) {
errs = make([]error, 0, 3)
if e.InnerErr != nil {
errs = append(errs, e.InnerErr)
}
if e.DoErr != nil {
errs = append(errs, e.DoErr)
}
if e.OpErr != nil {
errs = append(errs, e.OpErr)
}
if err := errors.Join(e.Err...); err != nil {
errs = append(errs, err)
}
return
}
// A RevertCompoundError encapsulates errors returned by
// the Revert method of [system.I].
type RevertCompoundError interface {
Error() string
Unwrap() []error
}

View File

@ -2,19 +2,23 @@ package app
import (
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/system"
"git.gensokyo.uk/security/fortify/internal/sys"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system"
)
func NewWithID(id fst.ID, os linux.System) App {
func NewWithID(id fst.ID, os sys.State) fst.App {
a := new(app)
a.id = &id
a.os = os
a.id = newID(&id)
a.sys = os
return a
}
func AppSystemBwrap(a App) (*system.I, *bwrap.Config) {
func AppIParams(a fst.App, sa fst.SealedApp) (*system.I, *sandbox.Params) {
v := a.(*app)
return v.seal.sys.I, v.seal.sys.bwrap
seal := sa.(*outcome)
if v.outcome != seal || v.id != seal.id {
panic("broken app/outcome link")
}
return seal.sys, seal.container
}

180
internal/app/process.go Normal file
View File

@ -0,0 +1,180 @@
package app
import (
"context"
"errors"
"log"
"os/exec"
"time"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/state"
"git.gensokyo.uk/security/fortify/system"
)
const shimSetupTimeout = 5 * time.Second
func (seal *outcome) Run(rs *fst.RunState) error {
if !seal.f.CompareAndSwap(false, true) {
// run does much more than just starting a process; calling it twice, even if the first call fails, will result
// in inconsistent state that is impossible to clean up; return here to limit damage and hopefully give the
// other Run a chance to return
panic("attempted to run twice")
}
if rs == nil {
panic("invalid state")
}
// read comp values early to allow for early failure
fmsg.Verbosef("version %s", internal.Version())
fmsg.Verbosef("setuid helper at %s", internal.MustFsuPath())
/*
prepare/revert os state
*/
if err := seal.sys.Commit(seal.ctx); err != nil {
return err
}
store := state.NewMulti(seal.runDirPath)
deferredStoreFunc := func(c state.Cursor) error { return nil }
defer func() {
var revertErr error
storeErr := new(StateStoreError)
storeErr.Inner, storeErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) {
revertErr = func() error {
storeErr.InnerErr = deferredStoreFunc(c)
/*
revert app setup transaction
*/
var rt system.Enablement
ec := system.Process
if states, err := c.Load(); err != nil {
// revert per-process state here to limit damage
storeErr.OpErr = err
return seal.sys.Revert((*system.Criteria)(&ec))
} else {
if l := len(states); l == 0 {
fmsg.Verbose("no other launchers active, will clean up globals")
ec |= system.User
} else {
fmsg.Verbosef("found %d active launchers, cleaning up without globals", l)
}
// accumulate enablements of remaining launchers
for i, s := range states {
if s.Config != nil {
rt |= s.Config.Confinement.Enablements
} else {
log.Printf("state entry %d does not contain config", i)
}
}
}
ec |= rt ^ (system.EWayland | system.EX11 | system.EDBus | system.EPulse)
if fmsg.Load() {
if ec > 0 {
fmsg.Verbose("reverting operations type", system.TypeString(ec))
}
}
return seal.sys.Revert((*system.Criteria)(&ec))
}()
})
storeErr.save([]error{revertErr, store.Close()})
rs.RevertErr = storeErr.equiv("error returned during cleanup:")
}()
/*
shim process lifecycle
*/
waitErr := make(chan error, 1)
cmd := new(shimProcess)
if startTime, err := cmd.Start(
seal.user.aid.String(),
seal.user.supp,
); err != nil {
return err
} else {
// whether/when the fsu process was created
rs.Time = startTime
}
ctx, cancel := context.WithTimeout(seal.ctx, shimSetupTimeout)
defer cancel()
go func() {
waitErr <- cmd.Unwrap().Wait()
// cancel shim setup in case shim died before receiving payload
cancel()
}()
if err := cmd.Serve(ctx, &shimParams{
Container: seal.container,
Home: seal.user.data,
Verbose: fmsg.Load(),
}); err != nil {
return err
}
// shim accepted setup payload, create process state
sd := state.State{
ID: seal.id.unwrap(),
PID: cmd.Unwrap().Process.Pid,
Time: *rs.Time,
}
var earlyStoreErr = new(StateStoreError) // returned after blocking on waitErr
earlyStoreErr.Inner, earlyStoreErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) {
earlyStoreErr.InnerErr = c.Save(&sd, seal.ct)
})
// destroy defunct state entry
deferredStoreFunc = func(c state.Cursor) error { return c.Destroy(seal.id.unwrap()) }
select {
case err := <-waitErr: // block until fsu/shim returns
if err != nil {
var exitError *exec.ExitError
if !errors.As(err, &exitError) {
// should be unreachable
rs.WaitErr = err
}
// store non-zero return code
rs.ExitCode = exitError.ExitCode()
} else {
rs.ExitCode = cmd.Unwrap().ProcessState.ExitCode()
}
if fmsg.Load() {
fmsg.Verbosef("process %d exited with exit code %d", cmd.Unwrap().Process.Pid, rs.ExitCode)
}
// this is reached when a fault makes an already running shim impossible to continue execution
// however a kill signal could not be delivered (should actually always happen like that since fsu)
// the effects of this is similar to the alternative exit path and ensures shim death
case err := <-cmd.Fallback():
rs.ExitCode = 255
log.Printf("cannot terminate shim on faulted setup: %v", err)
// alternative exit path relying on shim behaviour on monitor process exit
case <-seal.ctx.Done():
fmsg.Verbose("alternative exit path selected")
}
fmsg.Resume()
if seal.sync != nil {
if err := seal.sync.Close(); err != nil {
log.Printf("cannot close wayland security context: %v", err)
}
}
if seal.dbusMsg != nil {
seal.dbusMsg()
}
return earlyStoreErr.equiv("cannot save process state:")
}

View File

@ -2,24 +2,48 @@ package app
import (
"bytes"
"context"
"encoding/gob"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path"
"regexp"
"strconv"
"slices"
"strings"
"sync/atomic"
"syscall"
"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/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/state"
"git.gensokyo.uk/security/fortify/internal/system"
"git.gensokyo.uk/security/fortify/internal/sys"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/sandbox/wl"
"git.gensokyo.uk/security/fortify/system"
)
const (
home = "HOME"
shell = "SHELL"
xdgConfigHome = "XDG_CONFIG_HOME"
xdgRuntimeDir = "XDG_RUNTIME_DIR"
xdgSessionClass = "XDG_SESSION_CLASS"
xdgSessionType = "XDG_SESSION_TYPE"
term = "TERM"
display = "DISPLAY"
pulseServer = "PULSE_SERVER"
pulseCookie = "PULSE_COOKIE"
dbusSessionBusAddress = "DBUS_SESSION_BUS_ADDRESS"
dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS"
)
var (
@ -27,182 +51,196 @@ var (
ErrUser = errors.New("invalid aid")
ErrHome = errors.New("invalid home directory")
ErrName = errors.New("invalid username")
ErrXDisplay = errors.New(display + " unset")
ErrPulseCookie = errors.New("pulse cookie not present")
ErrPulseSocket = errors.New("pulse socket not present")
ErrPulseMode = errors.New("unexpected pulse socket mode")
)
var posixUsername = regexp.MustCompilePOSIX("^[a-z_]([A-Za-z0-9_-]{0,31}|[A-Za-z0-9_-]{0,30}\\$)$")
// appSeal seals the application with child-related information
type appSeal struct {
// app unique ID string representation
id string
// outcome stores copies of various parts of [fst.Config]
type outcome struct {
// copied from initialising [app]
id *stringPair[fst.ID]
// copied from [sys.State] response
runDirPath string
// initial [fst.Config] gob stream for state data;
// this is prepared ahead of time as config is clobbered during seal creation
ct io.WriterTo
// dump dbus proxy message buffer
dbusMsg func()
// freedesktop application ID
fid string
// argv to start process with in the final confined environment
command []string
// persistent process state store
store state.Store
user fsuUser
sys *system.I
ctx context.Context
// process-specific share directory path
share string
// process-specific share directory path local to XDG_RUNTIME_DIR
shareLocal string
container *sandbox.Params
env map[string]string
sync *os.File
// pass-through enablement tracking from config
et system.Enablements
// initial config gob encoding buffer
ct io.WriterTo
// wayland socket direct access
directWayland bool
// extra UpdatePerm ops
extraPerms []*sealedExtraPerm
// prevents sharing from happening twice
shared bool
// seal system-level component
sys *appSealSys
linux.Paths
// protected by upstream mutex
f atomic.Bool
}
type sealedExtraPerm struct {
name string
perms acl.Perms
ensure bool
// shareHost holds optional share directory state that must not be accessed directly
type shareHost struct {
// whether XDG_RUNTIME_DIR is used post fsu
useRuntimeDir bool
// process-specific directory in tmpdir, empty if unused
sharePath string
// process-specific directory in XDG_RUNTIME_DIR, empty if unused
runtimeSharePath string
seal *outcome
sc fst.Paths
}
// Seal seals the app launch context
func (a *app) Seal(config *fst.Config) error {
a.lock.Lock()
defer a.lock.Unlock()
// ensureRuntimeDir must be called if direct access to paths within XDG_RUNTIME_DIR is required
func (share *shareHost) ensureRuntimeDir() {
if share.useRuntimeDir {
return
}
share.useRuntimeDir = true
share.seal.sys.Ensure(share.sc.RunDirPath, 0700)
share.seal.sys.UpdatePermType(system.User, share.sc.RunDirPath, acl.Execute)
share.seal.sys.Ensure(share.sc.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset
share.seal.sys.UpdatePermType(system.User, share.sc.RuntimePath, acl.Execute)
}
if a.seal != nil {
panic("app sealed twice")
// instance returns a process-specific share path within tmpdir
func (share *shareHost) instance() string {
if share.sharePath != "" {
return share.sharePath
}
share.sharePath = path.Join(share.sc.SharePath, share.seal.id.String())
share.seal.sys.Ephemeral(system.Process, share.sharePath, 0711)
return share.sharePath
}
// runtime returns a process-specific share path within XDG_RUNTIME_DIR
func (share *shareHost) runtime() string {
if share.runtimeSharePath != "" {
return share.runtimeSharePath
}
share.ensureRuntimeDir()
share.runtimeSharePath = path.Join(share.sc.RunDirPath, share.seal.id.String())
share.seal.sys.Ephemeral(system.Process, share.runtimeSharePath, 0700)
share.seal.sys.UpdatePerm(share.runtimeSharePath, acl.Execute)
return share.runtimeSharePath
}
// fsuUser stores post-fsu credentials and metadata
type fsuUser struct {
// application id
aid *stringPair[int]
// target uid resolved by fid:aid
uid *stringPair[int]
// supplementary group ids
supp []string
// home directory host path
data string
// app user home directory
home string
// passwd database username
username string
}
func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Config) error {
if seal.ctx != nil {
panic("finalise called twice")
}
seal.ctx = ctx
{
// encode initial configuration for state tracking
ct := new(bytes.Buffer)
if err := gob.NewEncoder(ct).Encode(config); err != nil {
return fmsg.WrapErrorSuffix(err,
"cannot encode initial config:")
}
seal.ct = ct
}
if config == nil {
return fmsg.WrapError(ErrConfig,
"attempted to seal app with nil config")
}
// create seal
seal := new(appSeal)
// encode initial configuration for state tracking
ct := new(bytes.Buffer)
if err := gob.NewEncoder(ct).Encode(config); err != nil {
return fmsg.WrapErrorSuffix(err,
"cannot encode initial config:")
}
seal.ct = ct
// fetch system constants
seal.Paths = a.os.Paths()
// pass through config values
seal.id = a.id.String()
seal.fid = config.ID
seal.command = config.Command
// create seal system component
seal.sys = new(appSealSys)
// mapped uid
if config.Confinement.Sandbox != nil && config.Confinement.Sandbox.MapRealUID {
seal.sys.mappedID = a.os.Geteuid()
} else {
seal.sys.mappedID = 65534
}
seal.sys.mappedIDString = strconv.Itoa(seal.sys.mappedID)
seal.sys.runtime = path.Join("/run/user", seal.sys.mappedIDString)
// validate uid and set user info
// allowed aid range 0 to 9999, this is checked again in fsu
if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 {
return fmsg.WrapError(ErrUser,
fmt.Sprintf("aid %d out of range", config.Confinement.AppID))
}
seal.sys.user = appUser{
aid: config.Confinement.AppID,
as: strconv.Itoa(config.Confinement.AppID),
seal.user = fsuUser{
aid: newInt(config.Confinement.AppID),
data: config.Confinement.Outer,
home: config.Confinement.Inner,
username: config.Confinement.Username,
}
if seal.sys.user.username == "" {
seal.sys.user.username = "chronos"
} else if !posixUsername.MatchString(seal.sys.user.username) ||
len(seal.sys.user.username) >= internal.Sysconf_SC_LOGIN_NAME_MAX() {
if seal.user.username == "" {
seal.user.username = "chronos"
} else if !posixUsername.MatchString(seal.user.username) ||
len(seal.user.username) >= internal.Sysconf_SC_LOGIN_NAME_MAX() {
return fmsg.WrapError(ErrName,
fmt.Sprintf("invalid user name %q", seal.sys.user.username))
fmt.Sprintf("invalid user name %q", seal.user.username))
}
if seal.sys.user.data == "" || !path.IsAbs(seal.sys.user.data) {
if seal.user.data == "" || !path.IsAbs(seal.user.data) {
return fmsg.WrapError(ErrHome,
fmt.Sprintf("invalid home directory %q", seal.sys.user.data))
fmt.Sprintf("invalid home directory %q", seal.user.data))
}
if seal.sys.user.home == "" {
seal.sys.user.home = seal.sys.user.data
if seal.user.home == "" {
seal.user.home = seal.user.data
}
// invoke fsu for full uid
if u, err := a.os.Uid(seal.sys.user.aid); err != nil {
return fmsg.WrapErrorSuffix(err,
"cannot obtain uid from fsu:")
if u, err := sys.Uid(seal.user.aid.unwrap()); err != nil {
return err
} else {
seal.sys.user.uid = u
seal.sys.user.us = strconv.Itoa(u)
seal.user.uid = newInt(u)
}
// resolve supplementary group ids from names
seal.sys.user.supp = make([]string, len(config.Confinement.Groups))
seal.user.supp = make([]string, len(config.Confinement.Groups))
for i, name := range config.Confinement.Groups {
if g, err := a.os.LookupGroup(name); err != nil {
if g, err := sys.LookupGroup(name); err != nil {
return fmsg.WrapError(err,
fmt.Sprintf("unknown group %q", name))
} else {
seal.sys.user.supp[i] = g.Gid
seal.user.supp[i] = g.Gid
}
}
// build extra perms
seal.extraPerms = make([]*sealedExtraPerm, len(config.Confinement.ExtraPerms))
for i, p := range config.Confinement.ExtraPerms {
if p == nil {
continue
// this also falls back to host path if encountering an invalid path
if !path.IsAbs(config.Confinement.Shell) {
config.Confinement.Shell = "/bin/sh"
if s, ok := sys.LookupEnv(shell); ok && path.IsAbs(s) {
config.Confinement.Shell = s
}
seal.extraPerms[i] = new(sealedExtraPerm)
seal.extraPerms[i].name = p.Path
seal.extraPerms[i].perms = make(acl.Perms, 0, 3)
if p.Read {
seal.extraPerms[i].perms = append(seal.extraPerms[i].perms, acl.Read)
}
if p.Write {
seal.extraPerms[i].perms = append(seal.extraPerms[i].perms, acl.Write)
}
if p.Execute {
seal.extraPerms[i].perms = append(seal.extraPerms[i].perms, acl.Execute)
}
seal.extraPerms[i].ensure = p.Ensure
}
// do not use the value of shell before this point
// map sandbox config to bwrap
// permissive defaults
if config.Confinement.Sandbox == nil {
fmsg.VPrintln("sandbox configuration not supplied, PROCEED WITH CAUTION")
fmsg.Verbose("sandbox configuration not supplied, PROCEED WITH CAUTION")
// fsu clears the environment so resolve paths early
if !path.IsAbs(config.Path) {
if len(config.Args) > 0 {
if p, err := sys.LookPath(config.Args[0]); err != nil {
return fmsg.WrapError(err, err.Error())
} else {
config.Path = p
}
} else {
config.Path = config.Confinement.Shell
}
}
// permissive defaults
conf := &fst.SandboxConfig{
UserNS: true,
Net: true,
Syscall: new(bwrap.SyscallPolicy),
NoNewSession: true,
AutoEtc: true,
Userns: true,
Net: true,
Tty: true,
AutoEtc: true,
}
// bind entries in /
if d, err := a.os.ReadDir("/"); err != nil {
if d, err := sys.ReadDir("/"); err != nil {
return err
} else {
b := make([]*fst.FilesystemConfig, 0, len(d))
@ -224,11 +262,11 @@ func (a *app) Seal(config *fst.Config) error {
// hide nscd from sandbox if present
nscd := "/var/run/nscd"
if _, err := a.os.Stat(nscd); !errors.Is(err, fs.ErrNotExist) {
conf.Override = append(conf.Override, nscd)
if _, err := sys.Stat(nscd); !errors.Is(err, fs.ErrNotExist) {
conf.Cover = append(conf.Cover, nscd)
}
// bind GPU stuff
if config.Confinement.Enablements.Has(system.EX11) || config.Confinement.Enablements.Has(system.EWayland) {
if config.Confinement.Enablements&(system.EX11|system.EWayland) != 0 {
conf.Filesystem = append(conf.Filesystem, &fst.FilesystemConfig{Src: "/dev/dri", Device: true})
}
// opportunistically bind kvm
@ -236,38 +274,288 @@ func (a *app) Seal(config *fst.Config) error {
config.Confinement.Sandbox = conf
}
seal.directWayland = config.Confinement.Sandbox.DirectWayland
if b, err := config.Confinement.Sandbox.Bwrap(a.os); err != nil {
return err
} else {
seal.sys.bwrap = b
}
seal.sys.override = config.Confinement.Sandbox.Override
if seal.sys.bwrap.SetEnv == nil {
seal.sys.bwrap.SetEnv = make(map[string]string)
var mapuid, mapgid *stringPair[int]
{
var uid, gid int
var err error
seal.container, seal.env, err = config.Confinement.Sandbox.ToContainer(sys, &uid, &gid)
if err != nil {
return fmsg.WrapErrorSuffix(err,
"cannot initialise container configuration:")
}
if !path.IsAbs(config.Path) {
return fmsg.WrapError(syscall.EINVAL,
"invalid program path")
}
if len(config.Args) == 0 {
config.Args = []string{config.Path}
}
seal.container.Path = config.Path
seal.container.Args = config.Args
mapuid = newInt(uid)
mapgid = newInt(gid)
if seal.env == nil {
seal.env = make(map[string]string, 1<<6)
}
}
// open process state store
// the simple store only starts holding an open file after first action
// store activity begins after Start is called and must end before Wait
seal.store = state.NewMulti(seal.RunDirPath)
// inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as mapped uid
innerRuntimeDir := path.Join("/run/user", mapuid.String())
seal.container.Tmpfs("/run/user", 1<<12, 0755)
seal.container.Tmpfs(innerRuntimeDir, 1<<23, 0700)
seal.env[xdgRuntimeDir] = innerRuntimeDir
seal.env[xdgSessionClass] = "user"
seal.env[xdgSessionType] = "tty"
// initialise system interface with full uid
seal.sys.I = system.New(seal.sys.user.uid)
share := &shareHost{seal: seal, sc: sys.Paths()}
seal.runDirPath = share.sc.RunDirPath
seal.sys = system.New(seal.user.uid.unwrap())
// pass through enablements
seal.et = config.Confinement.Enablements
// this method calls all share methods in sequence
if err := seal.setupShares([2]*dbus.Config{config.Confinement.SessionBus, config.Confinement.SystemBus}, a.os); err != nil {
return err
{
seal.sys.Ensure(share.sc.SharePath, 0711)
tmpdir := path.Join(share.sc.SharePath, "tmpdir")
seal.sys.Ensure(tmpdir, 0700)
seal.sys.UpdatePermType(system.User, tmpdir, acl.Execute)
tmpdirInst := path.Join(tmpdir, seal.user.aid.String())
seal.sys.Ensure(tmpdirInst, 01700)
seal.sys.UpdatePermType(system.User, tmpdirInst, acl.Read, acl.Write, acl.Execute)
// mount inner /tmp from share so it shares persistence and storage behaviour of host /tmp
seal.container.Bind(tmpdirInst, "/tmp", sandbox.BindWritable)
}
// verbose log seal information
fmsg.VPrintf("created application seal for uid %s (%s) groups: %v, command: %s",
seal.sys.user.us, seal.sys.user.username, config.Confinement.Groups, config.Command)
{
homeDir := "/var/empty"
if seal.user.home != "" {
homeDir = seal.user.home
}
username := "chronos"
if seal.user.username != "" {
username = seal.user.username
}
seal.container.Bind(seal.user.data, homeDir, sandbox.BindWritable)
seal.container.Dir = homeDir
seal.env["HOME"] = homeDir
seal.env["USER"] = username
seal.env[shell] = config.Confinement.Shell
seal.container.Place("/etc/passwd",
[]byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Fortify:"+homeDir+":"+config.Confinement.Shell+"\n"))
seal.container.Place("/etc/group",
[]byte("fortify:x:"+mapgid.String()+":\n"))
}
// pass TERM for proper terminal I/O in initial process
if t, ok := sys.LookupEnv(term); ok {
seal.env[term] = t
}
if config.Confinement.Enablements&system.EWayland != 0 {
// outer wayland socket (usually `/run/user/%d/wayland-%d`)
var socketPath string
if name, ok := sys.LookupEnv(wl.WaylandDisplay); !ok {
fmsg.Verbose(wl.WaylandDisplay + " is not set, assuming " + wl.FallbackName)
socketPath = path.Join(share.sc.RuntimePath, wl.FallbackName)
} else if !path.IsAbs(name) {
socketPath = path.Join(share.sc.RuntimePath, name)
} else {
socketPath = name
}
innerPath := path.Join(innerRuntimeDir, wl.FallbackName)
seal.env[wl.WaylandDisplay] = wl.FallbackName
if !config.Confinement.Sandbox.DirectWayland { // set up security-context-v1
appID := config.ID
if appID == "" {
// use instance ID in case app id is not set
appID = "uk.gensokyo.fortify." + seal.id.String()
}
// downstream socket paths
outerPath := path.Join(share.instance(), "wayland")
seal.sys.Wayland(&seal.sync, outerPath, socketPath, appID, seal.id.String())
seal.container.Bind(outerPath, innerPath, 0)
} else { // bind mount wayland socket (insecure)
fmsg.Verbose("direct wayland access, PROCEED WITH CAUTION")
share.ensureRuntimeDir()
seal.container.Bind(socketPath, innerPath, 0)
seal.sys.UpdatePermType(system.EWayland, socketPath, acl.Read, acl.Write, acl.Execute)
}
}
if config.Confinement.Enablements&system.EX11 != 0 {
if d, ok := sys.LookupEnv(display); !ok {
return fmsg.WrapError(ErrXDisplay,
"DISPLAY is not set")
} else {
seal.sys.ChangeHosts("#" + seal.user.uid.String())
seal.env[display] = d
seal.container.Bind("/tmp/.X11-unix", "/tmp/.X11-unix", 0)
}
}
if config.Confinement.Enablements&system.EPulse != 0 {
// PulseAudio runtime directory (usually `/run/user/%d/pulse`)
pulseRuntimeDir := path.Join(share.sc.RuntimePath, "pulse")
// PulseAudio socket (usually `/run/user/%d/pulse/native`)
pulseSocket := path.Join(pulseRuntimeDir, "native")
if _, err := sys.Stat(pulseRuntimeDir); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot access PulseAudio directory %q:", pulseRuntimeDir))
}
return fmsg.WrapError(ErrPulseSocket,
fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir))
}
if s, err := sys.Stat(pulseSocket); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot access PulseAudio socket %q:", pulseSocket))
}
return fmsg.WrapError(ErrPulseSocket,
fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pulseRuntimeDir))
} else {
if m := s.Mode(); m&0o006 != 0o006 {
return fmsg.WrapError(ErrPulseMode,
fmt.Sprintf("unexpected permissions on %q:", pulseSocket), m)
}
}
// hard link pulse socket into target-executable share
innerPulseRuntimeDir := path.Join(share.runtime(), "pulse")
innerPulseSocket := path.Join(innerRuntimeDir, "pulse", "native")
seal.sys.Link(pulseSocket, innerPulseRuntimeDir)
seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0)
seal.env[pulseServer] = "unix:" + innerPulseSocket
// publish current user's pulse cookie for target user
if src, err := discoverPulseCookie(sys); err != nil {
// not fatal
fmsg.Verbose(strings.TrimSpace(err.(*fmsg.BaseError).Message()))
} else {
innerDst := fst.Tmp + "/pulse-cookie"
seal.env[pulseCookie] = innerDst
var payload *[]byte
seal.container.PlaceP(innerDst, &payload)
seal.sys.CopyFile(payload, src, 256, 256)
}
}
if config.Confinement.Enablements&system.EDBus != 0 {
// ensure dbus session bus defaults
if config.Confinement.SessionBus == nil {
config.Confinement.SessionBus = dbus.NewConfig(config.ID, true, true)
}
// downstream socket paths
sharePath := share.instance()
sessionPath, systemPath := path.Join(sharePath, "bus"), path.Join(sharePath, "system_bus_socket")
// configure dbus proxy
if f, err := seal.sys.ProxyDBus(
config.Confinement.SessionBus, config.Confinement.SystemBus,
sessionPath, systemPath,
); err != nil {
return err
} else {
seal.dbusMsg = f
}
// share proxy sockets
sessionInner := path.Join(innerRuntimeDir, "bus")
seal.env[dbusSessionBusAddress] = "unix:path=" + sessionInner
seal.container.Bind(sessionPath, sessionInner, 0)
seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
if config.Confinement.SystemBus != nil {
systemInner := "/run/dbus/system_bus_socket"
seal.env[dbusSystemBusAddress] = "unix:path=" + systemInner
seal.container.Bind(systemPath, systemInner, 0)
seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write)
}
}
for _, dest := range config.Confinement.Sandbox.Cover {
seal.container.Tmpfs(dest, 1<<13, 0755)
}
// append ExtraPerms last
for _, p := range config.Confinement.ExtraPerms {
if p == nil {
continue
}
if p.Ensure {
seal.sys.Ensure(p.Path, 0700)
}
perms := make(acl.Perms, 0, 3)
if p.Read {
perms = append(perms, acl.Read)
}
if p.Write {
perms = append(perms, acl.Write)
}
if p.Execute {
perms = append(perms, acl.Execute)
}
seal.sys.UpdatePermType(system.User, p.Path, perms...)
}
// flatten and sort env for deterministic behaviour
seal.container.Env = make([]string, 0, len(seal.env))
for k, v := range seal.env {
if strings.IndexByte(k, '=') != -1 {
return fmsg.WrapError(syscall.EINVAL,
fmt.Sprintf("invalid environment variable %s", k))
}
seal.container.Env = append(seal.container.Env, k+"="+v)
}
slices.Sort(seal.container.Env)
fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s",
seal.user.uid, seal.user.username, config.Confinement.Groups, seal.container.Args)
// seal app and release lock
a.seal = seal
return nil
}
// discoverPulseCookie attempts various standard methods to discover the current user's PulseAudio authentication cookie
func discoverPulseCookie(sys sys.State) (string, error) {
if p, ok := sys.LookupEnv(pulseCookie); ok {
return p, nil
}
// dotfile $HOME/.pulse-cookie
if p, ok := sys.LookupEnv(home); ok {
p = path.Join(p, ".pulse-cookie")
if s, err := sys.Stat(p); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return p, fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot access PulseAudio cookie %q:", p))
}
// not found, try next method
} else if !s.IsDir() {
return p, nil
}
}
// $XDG_CONFIG_HOME/pulse/cookie
if p, ok := sys.LookupEnv(xdgConfigHome); ok {
p = path.Join(p, "pulse", "cookie")
if s, err := sys.Stat(p); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return p, fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot access PulseAudio cookie %q:", p))
}
// not found, try next method
} else if !s.IsDir() {
return p, nil
}
}
return "", fmsg.WrapError(ErrPulseCookie,
fmt.Sprintf("cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)",
pulseCookie, xdgConfigHome, home))
}

View File

@ -1,339 +0,0 @@
package app
import (
"errors"
"fmt"
"io/fs"
"path"
"strings"
"git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/system"
"git.gensokyo.uk/security/fortify/wl"
)
const (
home = "HOME"
shell = "SHELL"
xdgConfigHome = "XDG_CONFIG_HOME"
xdgRuntimeDir = "XDG_RUNTIME_DIR"
xdgSessionClass = "XDG_SESSION_CLASS"
xdgSessionType = "XDG_SESSION_TYPE"
term = "TERM"
display = "DISPLAY"
pulseServer = "PULSE_SERVER"
pulseCookie = "PULSE_COOKIE"
dbusSessionBusAddress = "DBUS_SESSION_BUS_ADDRESS"
dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS"
)
var (
ErrXDisplay = errors.New(display + " unset")
ErrPulseCookie = errors.New("pulse cookie not present")
ErrPulseSocket = errors.New("pulse socket not present")
ErrPulseMode = errors.New("unexpected pulse socket mode")
)
func (seal *appSeal) setupShares(bus [2]*dbus.Config, os linux.System) error {
if seal.shared {
panic("seal shared twice")
}
seal.shared = true
/*
Tmpdir-based share directory
*/
// ensure Share (e.g. `/tmp/fortify.%d`)
// acl is unnecessary as this directory is world executable
seal.sys.Ensure(seal.SharePath, 0711)
// ensure process-specific share (e.g. `/tmp/fortify.%d/%s`)
// acl is unnecessary as this directory is world executable
seal.share = path.Join(seal.SharePath, seal.id)
seal.sys.Ephemeral(system.Process, seal.share, 0711)
// ensure child tmpdir parent directory (e.g. `/tmp/fortify.%d/tmpdir`)
targetTmpdirParent := path.Join(seal.SharePath, "tmpdir")
seal.sys.Ensure(targetTmpdirParent, 0700)
seal.sys.UpdatePermType(system.User, targetTmpdirParent, acl.Execute)
// ensure child tmpdir (e.g. `/tmp/fortify.%d/tmpdir/%d`)
targetTmpdir := path.Join(targetTmpdirParent, seal.sys.user.as)
seal.sys.Ensure(targetTmpdir, 01700)
seal.sys.UpdatePermType(system.User, targetTmpdir, acl.Read, acl.Write, acl.Execute)
seal.sys.bwrap.Bind(targetTmpdir, "/tmp", false, true)
/*
XDG runtime directory
*/
// mount tmpfs on inner runtime (e.g. `/run/user/%d`)
seal.sys.bwrap.Tmpfs("/run/user", 1*1024*1024)
seal.sys.bwrap.Tmpfs(seal.sys.runtime, 8*1024*1024)
// point to inner runtime path `/run/user/%d`
seal.sys.bwrap.SetEnv[xdgRuntimeDir] = seal.sys.runtime
seal.sys.bwrap.SetEnv[xdgSessionClass] = "user"
seal.sys.bwrap.SetEnv[xdgSessionType] = "tty"
// ensure RunDir (e.g. `/run/user/%d/fortify`)
seal.sys.Ensure(seal.RunDirPath, 0700)
seal.sys.UpdatePermType(system.User, seal.RunDirPath, acl.Execute)
// ensure runtime directory ACL (e.g. `/run/user/%d`)
seal.sys.Ensure(seal.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset
seal.sys.UpdatePermType(system.User, seal.RuntimePath, acl.Execute)
// ensure process-specific share local to XDG_RUNTIME_DIR (e.g. `/run/user/%d/fortify/%s`)
seal.shareLocal = path.Join(seal.RunDirPath, seal.id)
seal.sys.Ephemeral(system.Process, seal.shareLocal, 0700)
seal.sys.UpdatePerm(seal.shareLocal, acl.Execute)
/*
Inner passwd database
*/
// look up shell
sh := "/bin/sh"
if s, ok := os.LookupEnv(shell); ok {
seal.sys.bwrap.SetEnv[shell] = s
sh = s
}
// bind home directory
homeDir := "/var/empty"
if seal.sys.user.home != "" {
homeDir = seal.sys.user.home
}
username := "chronos"
if seal.sys.user.username != "" {
username = seal.sys.user.username
}
seal.sys.bwrap.Bind(seal.sys.user.data, homeDir, false, true)
seal.sys.bwrap.Chdir = homeDir
seal.sys.bwrap.SetEnv["HOME"] = homeDir
seal.sys.bwrap.SetEnv["USER"] = username
// generate /etc/passwd and /etc/group
seal.sys.bwrap.CopyBind("/etc/passwd",
[]byte(username+":x:"+seal.sys.mappedIDString+":"+seal.sys.mappedIDString+":Fortify:"+homeDir+":"+sh+"\n"))
seal.sys.bwrap.CopyBind("/etc/group",
[]byte("fortify:x:"+seal.sys.mappedIDString+":\n"))
/*
Display servers
*/
// pass $TERM to launcher
if t, ok := os.LookupEnv(term); ok {
seal.sys.bwrap.SetEnv[term] = t
}
// set up wayland
if seal.et.Has(system.EWayland) {
var socketPath string
if name, ok := os.LookupEnv(wl.WaylandDisplay); !ok {
fmsg.VPrintln(wl.WaylandDisplay + " is not set, assuming " + wl.FallbackName)
socketPath = path.Join(seal.RuntimePath, wl.FallbackName)
} else if !path.IsAbs(name) {
socketPath = path.Join(seal.RuntimePath, name)
} else {
socketPath = name
}
innerPath := path.Join(seal.sys.runtime, wl.FallbackName)
seal.sys.bwrap.SetEnv[wl.WaylandDisplay] = wl.FallbackName
if !seal.directWayland { // set up security-context-v1
socketDir := path.Join(seal.SharePath, "wayland")
outerPath := path.Join(socketDir, seal.id)
seal.sys.Ensure(socketDir, 0711)
appID := seal.fid
if appID == "" {
// use instance ID in case app id is not set
appID = "uk.gensokyo.fortify." + seal.id
}
seal.sys.Wayland(outerPath, socketPath, appID, seal.id)
seal.sys.bwrap.Bind(outerPath, innerPath)
} else { // bind mount wayland socket (insecure)
fmsg.VPrintln("direct wayland access, PROCEED WITH CAUTION")
seal.sys.bwrap.Bind(socketPath, innerPath)
// ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`)
seal.sys.UpdatePermType(system.EWayland, socketPath, acl.Read, acl.Write, acl.Execute)
}
}
// set up X11
if seal.et.Has(system.EX11) {
// discover X11 and grant user permission via the `ChangeHosts` command
if d, ok := os.LookupEnv(display); !ok {
return fmsg.WrapError(ErrXDisplay,
"DISPLAY is not set")
} else {
seal.sys.ChangeHosts("#" + seal.sys.user.us)
seal.sys.bwrap.SetEnv[display] = d
seal.sys.bwrap.Bind("/tmp/.X11-unix", "/tmp/.X11-unix")
}
}
/*
PulseAudio server and authentication
*/
if seal.et.Has(system.EPulse) {
// check PulseAudio directory presence (e.g. `/run/user/%d/pulse`)
pd := path.Join(seal.RuntimePath, "pulse")
ps := path.Join(pd, "native")
if _, err := os.Stat(pd); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot access PulseAudio directory %q:", pd))
}
return fmsg.WrapError(ErrPulseSocket,
fmt.Sprintf("PulseAudio directory %q not found", pd))
}
// check PulseAudio socket permission (e.g. `/run/user/%d/pulse/native`)
if s, err := os.Stat(ps); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot access PulseAudio socket %q:", ps))
}
return fmsg.WrapError(ErrPulseSocket,
fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pd))
} else {
if m := s.Mode(); m&0o006 != 0o006 {
return fmsg.WrapError(ErrPulseMode,
fmt.Sprintf("unexpected permissions on %q:", ps), m)
}
}
// hard link pulse socket into target-executable share
psi := path.Join(seal.shareLocal, "pulse")
p := path.Join(seal.sys.runtime, "pulse", "native")
seal.sys.Link(ps, psi)
seal.sys.bwrap.Bind(psi, p)
seal.sys.bwrap.SetEnv[pulseServer] = "unix:" + p
// publish current user's pulse cookie for target user
if src, err := discoverPulseCookie(os); err != nil {
// not fatal
fmsg.VPrintln(strings.TrimSpace(err.(*fmsg.BaseError).Message()))
} else {
dst := path.Join(seal.share, "pulse-cookie")
innerDst := fst.Tmp + "/pulse-cookie"
seal.sys.bwrap.SetEnv[pulseCookie] = innerDst
seal.sys.CopyFile(dst, src)
seal.sys.bwrap.Bind(dst, innerDst)
}
}
/*
D-Bus proxy
*/
if seal.et.Has(system.EDBus) {
// ensure dbus session bus defaults
if bus[0] == nil {
bus[0] = dbus.NewConfig(seal.fid, true, true)
}
// downstream socket paths
sessionPath, systemPath := path.Join(seal.share, "bus"), path.Join(seal.share, "system_bus_socket")
// configure dbus proxy
if f, err := seal.sys.ProxyDBus(bus[0], bus[1], sessionPath, systemPath); err != nil {
return err
} else {
seal.dbusMsg = f
}
// share proxy sockets
sessionInner := path.Join(seal.sys.runtime, "bus")
seal.sys.bwrap.SetEnv[dbusSessionBusAddress] = "unix:path=" + sessionInner
seal.sys.bwrap.Bind(sessionPath, sessionInner)
seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
if bus[1] != nil {
systemInner := "/run/dbus/system_bus_socket"
seal.sys.bwrap.SetEnv[dbusSystemBusAddress] = "unix:path=" + systemInner
seal.sys.bwrap.Bind(systemPath, systemInner)
seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write)
}
}
/*
Miscellaneous
*/
// queue overriding tmpfs at the end of seal.sys.bwrap.Filesystem
for _, dest := range seal.sys.override {
seal.sys.bwrap.Tmpfs(dest, 8*1024)
}
// mount fortify in sandbox for init
seal.sys.bwrap.Bind(os.MustExecutable(), path.Join(fst.Tmp, "sbin/fortify"))
seal.sys.bwrap.Symlink("fortify", path.Join(fst.Tmp, "sbin/init"))
// append extra perms
for _, p := range seal.extraPerms {
if p == nil {
continue
}
if p.ensure {
seal.sys.Ensure(p.name, 0700)
}
seal.sys.UpdatePermType(system.User, p.name, p.perms...)
}
return nil
}
// discoverPulseCookie attempts various standard methods to discover the current user's PulseAudio authentication cookie
func discoverPulseCookie(os linux.System) (string, error) {
if p, ok := os.LookupEnv(pulseCookie); ok {
return p, nil
}
// dotfile $HOME/.pulse-cookie
if p, ok := os.LookupEnv(home); ok {
p = path.Join(p, ".pulse-cookie")
if s, err := os.Stat(p); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return p, fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot access PulseAudio cookie %q:", p))
}
// not found, try next method
} else if !s.IsDir() {
return p, nil
}
}
// $XDG_CONFIG_HOME/pulse/cookie
if p, ok := os.LookupEnv(xdgConfigHome); ok {
p = path.Join(p, "pulse", "cookie")
if s, err := os.Stat(p); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return p, fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot access PulseAudio cookie %q:", p))
}
// not found, try next method
} else if !s.IsDir() {
return p, nil
}
}
return "", fmsg.WrapError(ErrPulseCookie,
fmt.Sprintf("cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)",
pulseCookie, xdgConfigHome, home))
}

212
internal/app/shim.go Normal file
View File

@ -0,0 +1,212 @@
package app
import (
"context"
"encoding/gob"
"errors"
"log"
"os"
"os/exec"
"os/signal"
"strconv"
"strings"
"syscall"
"time"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/sandbox"
)
const shimEnv = "FORTIFY_SHIM"
type shimParams struct {
// finalised container params
Container *sandbox.Params
// path to outer home directory
Home string
// verbosity pass through
Verbose bool
}
// ShimMain is the main function of the shim process and runs as the unconstrained target user.
func ShimMain() {
fmsg.Prepare("shim")
if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
}
var (
params shimParams
closeSetup func() error
)
if f, err := sandbox.Receive(shimEnv, &params, nil); err != nil {
if errors.Is(err, sandbox.ErrInvalid) {
log.Fatal("invalid config descriptor")
}
if errors.Is(err, sandbox.ErrNotSet) {
log.Fatal("FORTIFY_SHIM not set")
}
log.Fatalf("cannot receive shim setup params: %v", err)
} else {
internal.InstallFmsg(params.Verbose)
closeSetup = f
}
if params.Container == nil || params.Container.Ops == nil {
log.Fatal("invalid container params")
}
// close setup socket
if err := closeSetup(); err != nil {
log.Printf("cannot close setup pipe: %v", err)
// not fatal
}
// ensure home directory as target user
if s, err := os.Stat(params.Home); err != nil {
if os.IsNotExist(err) {
if err = os.Mkdir(params.Home, 0700); err != nil {
log.Fatalf("cannot create home directory: %v", err)
}
} else {
log.Fatalf("cannot access home directory: %v", err)
}
// home directory is created, proceed
} else if !s.IsDir() {
log.Fatalf("path %q is not a directory", params.Home)
}
var name string
if len(params.Container.Args) > 0 {
name = params.Container.Args[0]
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop() // unreachable
container := sandbox.New(ctx, name)
container.Params = *params.Container
container.Stdin, container.Stdout, container.Stderr = os.Stdin, os.Stdout, os.Stderr
container.Cancel = func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) }
container.WaitDelay = 2 * time.Second
if err := container.Start(); err != nil {
fmsg.PrintBaseError(err, "cannot start container:")
os.Exit(1)
}
if err := container.Serve(); err != nil {
fmsg.PrintBaseError(err, "cannot configure container:")
}
if err := container.Wait(); err != nil {
var exitError *exec.ExitError
if !errors.As(err, &exitError) {
if errors.Is(err, context.Canceled) {
os.Exit(2)
}
log.Printf("wait: %v", err)
os.Exit(127)
}
os.Exit(exitError.ExitCode())
}
}
type shimProcess struct {
// user switcher process
cmd *exec.Cmd
// fallback exit notifier with error returned killing the process
killFallback chan error
// monitor to shim encoder
encoder *gob.Encoder
}
func (s *shimProcess) Unwrap() *exec.Cmd { return s.cmd }
func (s *shimProcess) Fallback() chan error { return s.killFallback }
func (s *shimProcess) String() string {
if s.cmd == nil {
return "(unused shim manager)"
}
return s.cmd.String()
}
func (s *shimProcess) Start(
aid string,
supp []string,
) (*time.Time, error) {
// prepare user switcher invocation
fsuPath := internal.MustFsuPath()
s.cmd = exec.Command(fsuPath)
// pass shim setup pipe
if fd, e, err := sandbox.Setup(&s.cmd.ExtraFiles); err != nil {
return nil, fmsg.WrapErrorSuffix(err,
"cannot create shim setup pipe:")
} else {
s.encoder = e
s.cmd.Env = []string{
shimEnv + "=" + strconv.Itoa(fd),
"FORTIFY_APP_ID=" + aid,
}
}
// format fsu supplementary groups
if len(supp) > 0 {
fmsg.Verbosef("attaching supplementary group ids %s", supp)
s.cmd.Env = append(s.cmd.Env, "FORTIFY_GROUPS="+strings.Join(supp, " "))
}
s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
s.cmd.Dir = "/"
fmsg.Verbose("starting shim via fsu:", s.cmd)
// withhold messages to stderr
fmsg.Suspend()
if err := s.cmd.Start(); err != nil {
return nil, fmsg.WrapErrorSuffix(err,
"cannot start fsu:")
}
startTime := time.Now().UTC()
return &startTime, nil
}
func (s *shimProcess) Serve(ctx context.Context, params *shimParams) error {
// kill shim if something goes wrong and an error is returned
s.killFallback = make(chan error, 1)
killShim := func() {
if err := s.cmd.Process.Signal(os.Interrupt); err != nil {
s.killFallback <- err
}
}
defer func() { killShim() }()
encodeErr := make(chan error)
go func() { encodeErr <- s.encoder.Encode(params) }()
select {
// encode return indicates setup completion
case err := <-encodeErr:
if err != nil {
return fmsg.WrapErrorSuffix(err,
"cannot transmit shim config:")
}
killShim = func() {}
return nil
// setup canceled before payload was accepted
case <-ctx.Done():
err := ctx.Err()
if errors.Is(err, context.Canceled) {
return fmsg.WrapError(syscall.ECANCELED,
"shim setup canceled")
}
if errors.Is(err, context.DeadlineExceeded) {
return fmsg.WrapError(syscall.ETIMEDOUT,
"deadline exceeded waiting for shim")
}
// unreachable
return err
}
}

View File

@ -1,266 +0,0 @@
package app
import (
"context"
"errors"
"fmt"
"os/exec"
"path/filepath"
"strings"
"time"
"git.gensokyo.uk/security/fortify/helper"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/priv/shim"
"git.gensokyo.uk/security/fortify/internal/state"
"git.gensokyo.uk/security/fortify/internal/system"
)
const shimSetupTimeout = 5 * time.Second
func (a *app) Run(ctx context.Context, rs *RunState) error {
a.lock.Lock()
defer a.lock.Unlock()
if rs == nil {
panic("attempted to pass nil state to run")
}
// resolve exec paths
shimExec := [2]string{helper.BubblewrapName}
if len(a.seal.command) > 0 {
shimExec[1] = a.seal.command[0]
}
for i, n := range shimExec {
if len(n) == 0 {
continue
}
if filepath.Base(n) == n {
if s, err := exec.LookPath(n); err == nil {
shimExec[i] = s
} else {
return fmsg.WrapError(err,
fmt.Sprintf("executable file %q not found in $PATH", n))
}
}
}
// startup will go ahead, commit system setup
if err := a.seal.sys.Commit(ctx); err != nil {
return err
}
a.seal.sys.needRevert = true
// start shim via manager
a.shim = new(shim.Shim)
waitErr := make(chan error, 1)
if startTime, err := a.shim.Start(
a.seal.sys.user.as,
a.seal.sys.user.supp,
a.seal.sys.Sync(),
); err != nil {
return err
} else {
// shim process created
rs.Start = true
shimSetupCtx, shimSetupCancel := context.WithDeadline(ctx, time.Now().Add(shimSetupTimeout))
defer shimSetupCancel()
// start waiting for shim
go func() {
waitErr <- a.shim.Unwrap().Wait()
// cancel shim setup in case shim died before receiving payload
shimSetupCancel()
}()
// send payload
if err = a.shim.Serve(shimSetupCtx, &shim.Payload{
Argv: a.seal.command,
Exec: shimExec,
Bwrap: a.seal.sys.bwrap,
Home: a.seal.sys.user.data,
Verbose: fmsg.Verbose(),
}); err != nil {
return err
}
// shim accepted setup payload, create process state
sd := state.State{
ID: *a.id,
PID: a.shim.Unwrap().Process.Pid,
Time: *startTime,
}
// register process state
var err0 = new(StateStoreError)
err0.Inner, err0.DoErr = a.seal.store.Do(a.seal.sys.user.aid, func(c state.Cursor) {
err0.InnerErr = c.Save(&sd, a.seal.ct)
})
a.seal.sys.saveState = true
if err = err0.equiv("cannot save process state:"); err != nil {
return err
}
}
select {
// wait for process and resolve exit code
case err := <-waitErr:
if err != nil {
var exitError *exec.ExitError
if !errors.As(err, &exitError) {
// should be unreachable
rs.WaitErr = err
}
// store non-zero return code
rs.ExitCode = exitError.ExitCode()
} else {
rs.ExitCode = a.shim.Unwrap().ProcessState.ExitCode()
}
if fmsg.Verbose() {
fmsg.VPrintf("process %d exited with exit code %d", a.shim.Unwrap().Process.Pid, rs.ExitCode)
}
// this is reached when a fault makes an already running shim impossible to continue execution
// however a kill signal could not be delivered (should actually always happen like that since fsu)
// the effects of this is similar to the alternative exit path and ensures shim death
case err := <-a.shim.WaitFallback():
rs.ExitCode = 255
fmsg.Printf("cannot terminate shim on faulted setup: %v", err)
// alternative exit path relying on shim behaviour on monitor process exit
case <-ctx.Done():
fmsg.VPrintln("alternative exit path selected")
}
// child process exited, resume output
fmsg.Resume()
// print queued up dbus messages
if a.seal.dbusMsg != nil {
a.seal.dbusMsg()
}
// update store and revert app setup transaction
e := new(StateStoreError)
e.Inner, e.DoErr = a.seal.store.Do(a.seal.sys.user.aid, func(b state.Cursor) {
e.InnerErr = func() error {
// destroy defunct state entry
if cmd := a.shim.Unwrap(); cmd != nil && a.seal.sys.saveState {
if err := b.Destroy(*a.id); err != nil {
return err
}
}
// enablements of remaining launchers
rt, ec := new(system.Enablements), new(system.Criteria)
ec.Enablements = new(system.Enablements)
ec.Set(system.Process)
if states, err := b.Load(); err != nil {
return err
} else {
if l := len(states); l == 0 {
// cleanup globals as the final launcher
fmsg.VPrintln("no other launchers active, will clean up globals")
ec.Set(system.User)
} else {
fmsg.VPrintf("found %d active launchers, cleaning up without globals", l)
}
// accumulate capabilities of other launchers
for i, s := range states {
if s.Config != nil {
*rt |= s.Config.Confinement.Enablements
} else {
fmsg.Printf("state entry %d does not contain config", i)
}
}
}
// invert accumulated enablements for cleanup
for i := system.Enablement(0); i < system.Enablement(system.ELen); i++ {
if !rt.Has(i) {
ec.Set(i)
}
}
if fmsg.Verbose() {
labels := make([]string, 0, system.ELen+1)
for i := system.Enablement(0); i < system.Enablement(system.ELen+2); i++ {
if ec.Has(i) {
labels = append(labels, system.TypeString(i))
}
}
if len(labels) > 0 {
fmsg.VPrintln("reverting operations labelled", strings.Join(labels, ", "))
}
}
if a.seal.sys.needRevert {
if err := a.seal.sys.Revert(ec); err != nil {
return err.(RevertCompoundError)
}
}
return nil
}()
})
e.Err = a.seal.store.Close()
return e.equiv("error returned during cleanup:", e)
}
// StateStoreError is returned for a failed state save
type StateStoreError struct {
// whether inner function was called
Inner bool
// error returned by state.Store Do method
DoErr error
// error returned by state.Backend Save method
InnerErr error
// any other errors needing to be tracked
Err error
}
func (e *StateStoreError) equiv(a ...any) error {
if e.Inner && e.DoErr == nil && e.InnerErr == nil && e.Err == nil {
return nil
} else {
return fmsg.WrapErrorSuffix(e, a...)
}
}
func (e *StateStoreError) Error() string {
if e.Inner && e.InnerErr != nil {
return e.InnerErr.Error()
}
if e.DoErr != nil {
return e.DoErr.Error()
}
if e.Err != nil {
return e.Err.Error()
}
return "(nil)"
}
func (e *StateStoreError) Unwrap() (errs []error) {
errs = make([]error, 0, 3)
if e.DoErr != nil {
errs = append(errs, e.DoErr)
}
if e.InnerErr != nil {
errs = append(errs, e.InnerErr)
}
if e.Err != nil {
errs = append(errs, e.Err)
}
return
}
type RevertCompoundError interface {
Error() string
Unwrap() []error
}

19
internal/app/strings.go Normal file
View File

@ -0,0 +1,19 @@
package app
import (
"strconv"
"git.gensokyo.uk/security/fortify/fst"
)
func newInt(v int) *stringPair[int] { return &stringPair[int]{v, strconv.Itoa(v)} }
func newID(id *fst.ID) *stringPair[fst.ID] { return &stringPair[fst.ID]{*id, id.String()} }
// stringPair stores a value and its string representation.
type stringPair[T comparable] struct {
v T
s string
}
func (s *stringPair[T]) unwrap() T { return s.v }
func (s *stringPair[T]) String() string { return s.s }

View File

@ -1,51 +0,0 @@
package app
import (
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/system"
)
// appSealSys encapsulates app seal behaviour with OS interactions
type appSealSys struct {
bwrap *bwrap.Config
// paths to override by mounting tmpfs over them
override []string
// default formatted XDG_RUNTIME_DIR of User
runtime string
// target user sealed from config
user appUser
// mapped uid and gid in user namespace
mappedID int
// string representation of mappedID
mappedIDString string
needRevert bool
saveState bool
*system.I
// protected by upstream mutex
}
type appUser struct {
// full uid resolved by fsu
uid int
// string representation of uid
us string
// supplementary group ids
supp []string
// application id
aid int
// string representation of aid
as string
// home directory host path
data string
// app user home directory
home string
// passwd database username
username string
}

View File

@ -3,10 +3,15 @@ package internal
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
var (
Version = compPoison
version = compPoison
)
// Check validates string value set at compile time.
func Check(s string) (string, bool) {
return s, s != compPoison && s != ""
// check validates string value set at compile time.
func check(s string) (string, bool) { return s, s != compPoison && s != "" }
func Version() string {
if v, ok := check(version); ok {
return v
}
return "impure"
}

9
internal/exit.go Normal file
View File

@ -0,0 +1,9 @@
package internal
import (
"os"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
func Exit(code int) { fmsg.BeforeExit(); os.Exit(code) }

View File

@ -1,98 +0,0 @@
package fmsg
import (
"os"
"sync"
"sync/atomic"
)
var (
wstate atomic.Bool
dropped atomic.Uint64
withhold = make(chan struct{}, 1)
msgbuf = make(chan dOp, 64) // these ops are tiny so a large buffer is allocated for withholding output
dequeueOnce sync.Once
queueSync sync.WaitGroup
)
func dequeue() {
go func() {
for {
select {
case op := <-msgbuf:
op.Do()
queueSync.Done()
case <-withhold:
<-withhold
}
}
}()
}
// queue submits ops to msgbuf but drops messages
// when the buffer is full and dequeue is withholding
func queue(op dOp) {
queueSync.Add(1)
select {
case msgbuf <- op:
default:
// send the op anyway if not withholding
// as dequeue will get to it eventually
if !wstate.Load() {
msgbuf <- op
} else {
queueSync.Done()
// increment dropped message count
dropped.Add(1)
}
}
}
type dOp interface{ Do() }
func Exit(code int) {
Resume() // resume here to avoid deadlock
queueSync.Wait()
os.Exit(code)
}
func Suspend() {
dequeueOnce.Do(dequeue)
if wstate.CompareAndSwap(false, true) {
queueSync.Wait()
withhold <- struct{}{}
}
}
func Resume() {
dequeueOnce.Do(dequeue)
if wstate.CompareAndSwap(true, false) {
withhold <- struct{}{}
if d := dropped.Swap(0); d != 0 {
Printf("dropped %d messages during withhold", d)
}
}
}
type dPrint []any
func (v dPrint) Do() {
std.Print(v...)
}
type dPrintf struct {
format string
v []any
}
func (d *dPrintf) Do() {
std.Printf(d.format, d.v...)
}
type dPrintln []any
func (v dPrintln) Do() {
std.Println(v...)
}

View File

@ -2,7 +2,9 @@ package fmsg
import (
"fmt"
"log"
"reflect"
"strings"
)
// baseError implements a basic error container
@ -70,3 +72,17 @@ func AsBaseError(err error, target **BaseError) bool {
*target = v.Convert(baseErrorType).Interface().(*BaseError)
return true
}
func PrintBaseError(err error, fallback string) {
var e *BaseError
if AsBaseError(err, &e) {
if msg := e.Message(); strings.TrimSpace(msg) != "" {
log.Print(msg)
return
}
Verbose("*"+fallback, err)
return
}
log.Println(fallback, err)
}

View File

@ -2,39 +2,85 @@
package fmsg
import (
"bytes"
"io"
"log"
"os"
"sync"
"sync/atomic"
"syscall"
)
var std = log.New(os.Stderr, "fortify: ", 0)
const (
bufSize = 4 * 1024
bufSizeMax = 16 * 1024 * 1024
)
func SetPrefix(prefix string) {
prefix += ": "
std.SetPrefix(prefix)
std.SetPrefix(prefix)
var o = &suspendable{w: os.Stderr}
// Prepare configures the system logger for [Suspend] and [Resume] to take effect.
func Prepare(prefix string) { log.SetPrefix(prefix + ": "); log.SetFlags(0); log.SetOutput(o) }
type suspendable struct {
w io.Writer
s atomic.Bool
buf bytes.Buffer
bufOnce sync.Once
bufMu sync.Mutex
dropped int
}
func Print(v ...any) {
dequeueOnce.Do(dequeue)
queue(dPrint(v))
func (s *suspendable) Write(p []byte) (n int, err error) {
if !s.s.Load() {
return s.w.Write(p)
}
s.bufOnce.Do(func() { s.prepareBuf() })
s.bufMu.Lock()
defer s.bufMu.Unlock()
if l := len(p); s.buf.Len()+l > bufSizeMax {
s.dropped += l
return 0, syscall.ENOMEM
}
return s.buf.Write(p)
}
func Printf(format string, v ...any) {
dequeueOnce.Do(dequeue)
queue(&dPrintf{format, v})
func (s *suspendable) prepareBuf() { s.buf.Grow(bufSize) }
func (s *suspendable) Suspend() bool { return o.s.CompareAndSwap(false, true) }
func (s *suspendable) Resume() (resumed bool, dropped uintptr, n int64, err error) {
if o.s.CompareAndSwap(true, false) {
o.bufMu.Lock()
defer o.bufMu.Unlock()
resumed = true
dropped = uintptr(o.dropped)
o.dropped = 0
n, err = io.Copy(s.w, &s.buf)
s.buf = bytes.Buffer{}
s.prepareBuf()
}
return
}
func Println(v ...any) {
dequeueOnce.Do(dequeue)
queue(dPrintln(v))
func Suspend() bool { return o.Suspend() }
func Resume() bool {
resumed, dropped, _, err := o.Resume()
if err != nil {
// probably going to result in an error as well,
// so this call is as good as unreachable
log.Printf("cannot dump buffer on resume: %v", err)
}
if resumed && dropped > 0 {
log.Fatalf("dropped %d bytes while output is suspended", dropped)
}
return resumed
}
func Fatal(v ...any) {
Print(v...)
Exit(1)
}
func Fatalf(format string, v ...any) {
Printf(format, v...)
Exit(1)
func BeforeExit() {
if Resume() {
log.Printf("beforeExit reached on suspended output")
}
}

12
internal/fmsg/msg.go Normal file
View File

@ -0,0 +1,12 @@
package fmsg
type Output struct{}
func (Output) IsVerbose() bool { return Load() }
func (Output) Verbose(v ...any) { Verbose(v...) }
func (Output) Verbosef(format string, v ...any) { Verbosef(format, v...) }
func (Output) WrapErr(err error, a ...any) error { return WrapError(err, a...) }
func (Output) PrintBaseErr(err error, fallback string) { PrintBaseError(err, fallback) }
func (Output) Suspend() { Suspend() }
func (Output) Resume() bool { return Resume() }
func (Output) BeforeExit() { BeforeExit() }

View File

@ -1,25 +1,23 @@
package fmsg
import "sync/atomic"
import (
"log"
"sync/atomic"
)
var verbose = new(atomic.Bool)
func Verbose() bool {
return verbose.Load()
}
func Load() bool { return verbose.Load() }
func Store(v bool) { verbose.Store(v) }
func SetVerbose(v bool) {
verbose.Store(v)
}
func VPrintf(format string, v ...any) {
func Verbosef(format string, v ...any) {
if verbose.Load() {
Printf(format, v...)
log.Printf(format, v...)
}
}
func VPrintln(v ...any) {
func Verbose(v ...any) {
if verbose.Load() {
Println(v...)
log.Println(v...)
}
}

17
internal/output.go Normal file
View File

@ -0,0 +1,17 @@
package internal
import (
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
"git.gensokyo.uk/security/fortify/system"
)
func InstallFmsg(verbose bool) {
fmsg.Store(verbose)
sandbox.SetOutput(fmsg.Output{})
system.SetOutput(fmsg.Output{})
if verbose {
seccomp.SetOutput(fmsg.Verbose)
}
}

View File

@ -1,11 +1,23 @@
package internal
import "path"
import (
"log"
"path"
var (
Fsu = compPoison
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
func Path(p string) (string, bool) {
return p, p != compPoison && p != "" && path.IsAbs(p)
var (
fsu = compPoison
)
func MustFsuPath() string {
if name, ok := checkPath(fsu); ok {
return name
}
fmsg.BeforeExit()
log.Fatal("invalid fsu path, this program is compiled incorrectly")
return compPoison
}
func checkPath(p string) (string, bool) { return p, p != compPoison && p != "" && path.IsAbs(p) }

View File

@ -1,20 +0,0 @@
package internal
import "syscall"
func PR_SET_DUMPABLE__SUID_DUMP_DISABLE() error {
// linux/sched/coredump.h
if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, 0, 0); errno != 0 {
return errno
}
return nil
}
func PR_SET_PDEATHSIG__SIGKILL() error {
if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGKILL), 0); errno != 0 {
return errno
}
return nil
}

View File

@ -1,18 +0,0 @@
package init0
import (
"os"
"path"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
// used by the parent process
// TryArgv0 calls [Main] if argv0 indicates the process is started from a file named "init".
func TryArgv0() {
if len(os.Args) > 0 && path.Base(os.Args[0]) == "init" {
Main()
fmsg.Exit(0)
}
}

View File

@ -1,164 +0,0 @@
package init0
import (
"errors"
"os"
"os/exec"
"os/signal"
"syscall"
"time"
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
const (
// time to wait for linger processes after death of initial process
residualProcessTimeout = 5 * time.Second
)
// everything beyond this point runs within pid namespace
// proceed with caution!
func Main() {
// sharing stdout with shim
// USE WITH CAUTION
fmsg.SetPrefix("init")
// setting this prevents ptrace
if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
fmsg.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
panic("unreachable")
}
if os.Getpid() != 1 {
fmsg.Fatal("this process must run as pid 1")
panic("unreachable")
}
// receive setup payload
var (
payload Payload
closeSetup func() error
)
if f, err := proc.Receive(Env, &payload); err != nil {
if errors.Is(err, proc.ErrInvalid) {
fmsg.Fatal("invalid config descriptor")
}
if errors.Is(err, proc.ErrNotSet) {
fmsg.Fatal("FORTIFY_INIT not set")
}
fmsg.Fatalf("cannot decode init setup payload: %v", err)
panic("unreachable")
} else {
fmsg.SetVerbose(payload.Verbose)
closeSetup = f
// child does not need to see this
if err = os.Unsetenv(Env); err != nil {
fmsg.Printf("cannot unset %s: %v", Env, err)
// not fatal
} else {
fmsg.VPrintln("received configuration")
}
}
// die with parent
if err := internal.PR_SET_PDEATHSIG__SIGKILL(); err != nil {
fmsg.Fatalf("prctl(PR_SET_PDEATHSIG, SIGKILL): %v", err)
}
cmd := exec.Command(payload.Argv0)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Args = payload.Argv
cmd.Env = os.Environ()
if err := cmd.Start(); err != nil {
fmsg.Fatalf("cannot start %q: %v", payload.Argv0, err)
}
fmsg.Suspend()
// close setup pipe as setup is now complete
if err := closeSetup(); err != nil {
fmsg.Println("cannot close setup pipe:", err)
// not fatal
}
sig := make(chan os.Signal, 2)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
type winfo struct {
wpid int
wstatus syscall.WaitStatus
}
info := make(chan winfo, 1)
done := make(chan struct{})
go func() {
var (
err error
wpid = -2
wstatus syscall.WaitStatus
)
// keep going until no child process is left
for wpid != -1 {
if err != nil {
break
}
if wpid != -2 {
info <- winfo{wpid, wstatus}
}
err = syscall.EINTR
for errors.Is(err, syscall.EINTR) {
wpid, err = syscall.Wait4(-1, &wstatus, 0, nil)
}
}
if !errors.Is(err, syscall.ECHILD) {
fmsg.Println("unexpected wait4 response:", err)
}
close(done)
}()
// closed after residualProcessTimeout has elapsed after initial process death
timeout := make(chan struct{})
r := 2
for {
select {
case s := <-sig:
fmsg.VPrintln("received", s.String())
fmsg.Resume() // output could still be withheld at this point, so resume is called
fmsg.Exit(0)
case w := <-info:
if w.wpid == cmd.Process.Pid {
// initial process exited, output is most likely available again
fmsg.Resume()
switch {
case w.wstatus.Exited():
r = w.wstatus.ExitStatus()
case w.wstatus.Signaled():
r = 128 + int(w.wstatus.Signal())
default:
r = 255
}
go func() {
time.Sleep(residualProcessTimeout)
close(timeout)
}()
}
case <-done:
fmsg.Exit(r)
case <-timeout:
fmsg.Println("timeout exceeded waiting for lingering processes")
fmsg.Exit(r)
}
}
}

View File

@ -1,13 +0,0 @@
package init0
const Env = "FORTIFY_INIT"
type Payload struct {
// target full exec path
Argv0 string
// child full argv
Argv []string
// verbosity pass through
Verbose bool
}

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