Compare commits

...

184 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
158 changed files with 8414 additions and 4843 deletions

View File

@ -23,7 +23,7 @@ jobs:
retention-days: 1
race:
name: Data race detector
name: Fortify (race detector)
runs-on: nix
steps:
- name: Checkout
@ -39,11 +39,65 @@ jobs:
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

View File

@ -4,12 +4,15 @@ 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 bundleInfo struct {
type appInfo struct {
Name string `json:"name"`
Version string `json:"version"`
@ -20,13 +23,15 @@ type bundleInfo struct {
// passed through to [fst.Config]
Groups []string `json:"groups,omitempty"`
// passed through to [fst.Config]
UserNS bool `json:"userns,omitempty"`
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]
NoNewSession bool `json:"no_new_session,omitempty"`
Tty bool `json:"tty,omitempty"`
// passed through to [fst.Config]
MapRealUID bool `json:"map_real_uid,omitempty"`
// passed through to [fst.Config]
@ -36,13 +41,11 @@ type bundleInfo struct {
// passed through to [fst.Config]
SessionBus *dbus.Config `json:"session_bus,omitempty"`
// passed through to [fst.Config]
Enablements system.Enablements `json:"enablements"`
Enablements system.Enablement `json:"enablements"`
// passed through inverted to [bwrap.SyscallPolicy]
Devel bool `json:"devel,omitempty"`
// passed through to [bwrap.SyscallPolicy]
// passed through to [fst.Config]
Multiarch bool `json:"multiarch,omitempty"`
// passed through to [bwrap.SyscallPolicy]
// passed through to [fst.Config]
Bluetooth bool `json:"bluetooth,omitempty"`
// allow gpu access within sandbox
@ -59,8 +62,65 @@ type bundleInfo struct {
ActivationPackage string `json:"activation_package"`
}
func loadBundleInfo(name string, beforeFail func()) *bundleInfo {
bundle := new(bundleInfo)
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)

View File

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

View File

@ -1,50 +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 shellPath = "/run/current-system/sw/bin/bash"
var (
errSuccess = errors.New("success")
std sys.State = new(sys.Std)
)
func init() {
fmsg.Prepare("fpkg")
if err := os.Setenv("SHELL", shellPath); err != nil {
log.Fatalf("cannot set $SHELL: %v", err)
}
}
func main() {
// early init path, skips root check and duplicate PR_SET_DUMPABLE
sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg)
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
}
if os.Geteuid() == 0 {
log.Fatal("this program must not run as root")
}
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer stop() // unreachable
var (
flagVerbose bool
flagDropShell bool
)
c := command.New(os.Stderr, log.Printf, "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")
)
func init() {
flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
/*
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
}
func main() {
fmsg.Prepare("fpkg")
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
}
flag.Parse()
fmsg.Store(flagVerbose)
// 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
}
args := flag.Args()
// 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.Fatal("invalid argument")
log.Println("invalid argument")
return syscall.EINVAL
}
switch args[0] {
case "install":
actionInstall(args[1:])
case "start":
actionStart(args[1:])
/*
Parse app metadata.
*/
default:
log.Fatal("invalid argument")
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
}
internal.Exit(0)
/*
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

@ -8,6 +8,7 @@ import (
"strconv"
"sync/atomic"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
@ -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,65 +1,28 @@
package main
import (
"encoding/json"
"errors"
"io"
"log"
"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()
log.Fatal("invalid fortify path, this copy of fpkg is not compiled correctly")
} else if r, w, err := os.Pipe(); err != nil {
beforeFail()
log.Fatalf("cannot pipe: %v", err)
if sa, err := a.Seal(config); err != nil {
fmsg.PrintBaseError(err, "cannot seal app:")
rs.ExitCode = 1
} else {
if fmsg.Load() {
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 {
if rs.ExitCode != 0 {
beforeFail()
log.Fatalf("cannot send configuration: %v", err)
}
}()
if err := cmd.Start(); err != nil {
beforeFail()
log.Fatalf("cannot start fortify: %v", err)
}
if err := cmd.Wait(); err != nil {
var exitError *exec.ExitError
if errors.As(err, &exitError) {
beforeFail()
internal.Exit(exitError.ExitCode())
} else {
beforeFail()
log.Fatalf("cannot wait: %v", err)
}
os.Exit(rs.ExitCode)
}
}

View File

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

View File

@ -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"
"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{
mustRunAppDropShell(ctx, updateConfig(&fst.Config{
ID: app.ID,
Command: []string{shellPath, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
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
Userns: true, // nix sandbox requires userns
Net: net,
Syscall: &bwrap.SyscallPolicy{Multiarch: true},
NoNewSession: dropShell,
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{
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,
Command: []string{shellPath, "-lc", strings.Join(command, " && ")},
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,
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{shellPath, "-l"}
fortifyApp(config, beforeFail)
config.Args = []string{shellPath, "-l"}
mustRunApp(ctx, config, beforeFail)
beforeFail()
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)
}

View File

@ -1,4 +1,5 @@
{
lib,
buildGoModule,
fortify ? abort "fortify package required",
}:
@ -15,5 +16,15 @@ buildGoModule {
go mod init fsu >& /dev/null
'';
ldflags = [ "-X main.Fmain=${fortify}/libexec/fortify" ];
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
}

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

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

129
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,7 +27,7 @@
nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
in
{
nixosModules.fortify = import ./nixos.nix;
nixosModules.fortify = import ./nixos.nix self.packages;
buildPackage = forAllSystems (
system:
@ -63,11 +63,19 @@
withRace = true;
};
sandbox = callPackage ./test/sandbox { inherit self; };
sandbox-race = callPackage ./test/sandbox {
inherit self;
withRace = true;
};
fpkg = callPackage ./cmd/fpkg/test { inherit system self; };
formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } ''
cd ${./.}
echo "running nixfmt..."
nixfmt --check .
nixfmt --width=256 --check .
touch $out
'';
@ -97,115 +105,62 @@
packages = forAllSystems (
system:
let
inherit (self.packages.${system}) fortify;
inherit (self.packages.${system}) fortify fsu;
pkgs = nixpkgsFor.${system};
in
{
default = self.packages.${system}.fortify;
default = fortify;
fortify = pkgs.pkgsStatic.callPackage ./package.nix {
inherit (pkgs) bubblewrap xdg-dbus-proxy glibc;
inherit (pkgs)
# passthru.buildInputs
go
gcc
# nativeBuildInputs
pkg-config
wayland-scanner
makeBinaryWrapper
# appPackages
glibc
xdg-dbus-proxy
# fpkg
zstd
gnutar
coreutils
;
};
fsu = pkgs.callPackage ./cmd/fsu/package.nix { inherit (self.packages.${system}) fortify; };
dist =
pkgs.runCommand "${fortify.name}-dist" { inherit (self.devShells.${system}.default) buildInputs; }
''
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 -R +w .
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
'';
fhs = pkgs.buildFHSEnv {
pname = "fortify-fhs";
inherit (fortify) version;
targetPkgs =
pkgs:
with pkgs;
[
go
gcc
pkg-config
wayland-scanner
]
++ (
with pkgs.pkgsStatic;
[
musl
libffi
libseccomp
acl
wayland
wayland-protocols
]
++ (with xorg; [
libxcb
libXau
libXdmcp
xorgproto
])
);
extraOutputsToInstall = [ "dev" ];
profile = ''
export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH"
'';
};
}
);
devShells = forAllSystems (
system:
let
inherit (self.packages.${system}) fortify fhs;
inherit (self.packages.${system}) 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 =
@ -214,7 +169,7 @@
specialArgs = {
inherit pkgs;
};
modules = [ ./options.nix ];
modules = [ (import ./options.nix self.packages) ];
};
cleanEval = lib.filterAttrsRecursive (n: _: n != "_module") eval;
in
@ -224,7 +179,7 @@
sed -i '/*Declared by:*/,+1 d' $out
'';
in
nixpkgsFor.${system}.mkShell {
pkgs.mkShell {
shellHook = ''
exec cat ${docText} > options.md
'';

View File

@ -2,7 +2,6 @@
package fst
import (
"context"
"time"
)
@ -19,7 +18,7 @@ type App interface {
type SealedApp interface {
// Run commits sealed system setup and starts the app process.
Run(ctx context.Context, rs *RunState) error
Run(rs *RunState) error
}
// RunState stores the outcome of a call to [SealedApp.Run].

View File

@ -2,7 +2,7 @@ package fst
import (
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
"git.gensokyo.uk/security/fortify/system"
)
@ -14,8 +14,11 @@ type Config struct {
// passed to wayland security-context-v1 as application ID
// and used as part of defaults in dbus session proxy
ID string `json:"id"`
// final argv, passed to init
Command []string `json:"command"`
// absolute path to executable file
Path string `json:"path,omitempty"`
// final args passed to container init
Args []string `json:"args"`
Confinement ConfinementConfig `json:"confinement"`
}
@ -26,13 +29,15 @@ 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 ops, runs after everything else
ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
@ -44,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 {
@ -76,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{
Path: "/run/current-system/sw/bin/chromium",
Args: []string{
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
@ -106,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
@ -134,7 +130,7 @@ func Template() *Config {
Link: [][2]string{{"/run/user/65534", "/run/user/150"}},
Etc: "/etc",
AutoEtc: true,
Override: []string{"/var/run/nscd"},
Cover: []string{"/var/run/nscd"},
},
ExtraPerms: []*ExtraPermConfig{
{Path: "/var/lib/fortify/u0", Ensure: true, Execute: true},
@ -160,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,50 +4,63 @@ 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/sandbox"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
)
// SandboxConfig describes resources made available to the sandbox.
type SandboxConfig struct {
// unix hostname within sandbox
type (
SandboxConfig struct {
// container hostname
Hostname string `json:"hostname,omitempty"`
// allow userns within sandbox
UserNS bool `json:"userns,omitempty"`
// share net namespace
// 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"`
// 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"`
// expose main process tty
Tty bool `json:"tty,omitempty"`
// allow multiarch
Multiarch bool `json:"multiarch,omitempty"`
// initial process environment variables
Env map[string]string `json:"env"`
// map target user uid to privileged user uid in the user namespace
MapRealUID bool `json:"map_real_uid"`
// 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"`
// final environment variables
Env map[string]string `json:"env"`
// sandbox host filesystem access
Filesystem []*FilesystemConfig `json:"filesystem"`
// symlinks created inside the sandbox
Link [][2]string `json:"symlink"`
// read-only /etc directory
Etc string `json:"etc,omitempty"`
// automatically set up /etc symlinks
AutoEtc bool `json:"auto_etc"`
// mount tmpfs over these paths,
// runs right before [ConfinementConfig.ExtraPerms]
Override []string `json:"override"`
// cover these paths or create them if they do not already exist
Cover []string `json:"cover"`
}
// SandboxSys encapsulates system functions used during the creation of [bwrap.Config].
type SandboxSys interface {
Geteuid() int
// 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)
@ -56,73 +69,88 @@ type SandboxSys interface {
Printf(format string, v ...any)
}
// Bwrap returns the address of the corresponding bwrap.Config to s.
// Note that remaining tmpfs entries must be queued by the caller prior to launch.
func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) {
// 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 {
sys.Println("syscall filter not configured, PROCEED WITH CAUTION")
}
if !s.MapRealUID {
// mapped uid defaults to 65534 to work around file ownership checks due to a bwrap limitation
*uid = 65534
} else {
// some programs fail to connect to dbus session running as a different uid, so a separate workaround
// is introduced to map priv-side caller uid in namespace
*uid = sys.Geteuid()
}
conf := (&bwrap.Config{
Net: s.Net,
UserNS: s.UserNS,
UID: uid,
GID: uid,
container := &sandbox.Params{
Hostname: s.Hostname,
Clearenv: true,
SetEnv: s.Env,
Ops: new(sandbox.Ops),
Seccomp: s.Seccomp,
}
if s.Multiarch {
container.Seccomp |= seccomp.FlagMultiarch
}
/* 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),
*container.Ops = slices.Grow(*container.Ops, 1<<8)
Syscall: s.Syscall,
NewSession: !s.NoNewSession,
DieWithParent: true,
AsInit: true,
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
}
// initialise unconditionally as Once cannot be justified
// for saving such a miniscule amount of memory
Chmod: make(bwrap.ChmodConfig),
}).
Procfs("/proc").
Tmpfs(Tmp, 4*1024)
if s.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 := 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 {
@ -148,7 +176,7 @@ func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) {
hidePathMatch := make([]bool, len(hidePaths))
for i := range hidePaths {
if err := evalSymlinks(sys, &hidePaths[i]); err != nil {
return nil, err
return nil, nil, err
}
}
@ -158,19 +186,19 @@ func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*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(sys, &srcH); err != nil {
return nil, err
return nil, nil, err
}
for i := range hidePaths {
@ -180,54 +208,69 @@ func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*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
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 := sys.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(sys SandboxSys, v *string) error {

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

84
helper/cmd.go Normal file
View File

@ -0,0 +1,84 @@
package helper
import (
"context"
"errors"
"io"
"os"
"os/exec"
"slices"
"sync"
"syscall"
"git.gensokyo.uk/security/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

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

View File

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

@ -1,6 +1,7 @@
package app
import (
"context"
"fmt"
"log"
"sync"
@ -10,9 +11,10 @@ import (
"git.gensokyo.uk/security/fortify/internal/sys"
)
func New(os sys.State) (fst.App, error) {
func New(ctx context.Context, os sys.State) (fst.App, error) {
a := new(app)
a.sys = os
a.ctx = ctx
id := new(fst.ID)
err := fst.NewAppID(id)
@ -21,8 +23,8 @@ func New(os sys.State) (fst.App, error) {
return a, err
}
func MustNew(os sys.State) fst.App {
a, err := New(os)
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)
}
@ -32,6 +34,7 @@ func MustNew(os sys.State) fst.App {
type app struct {
id *stringPair[fst.ID]
sys sys.State
ctx context.Context
*outcome
mu sync.RWMutex
@ -71,7 +74,7 @@ func (a *app) Seal(config *fst.Config) (fst.SealedApp, error) {
seal := new(outcome)
seal.id = a.id
err := seal.finalise(a.sys, config)
err := seal.finalise(a.ctx, a.sys, config)
if err == nil {
a.outcome = seal
}

View File

@ -4,7 +4,7 @@ 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/sandbox"
"git.gensokyo.uk/security/fortify/system"
)
@ -13,19 +13,19 @@ var testCasesNixos = []sealTestCase{
"nixos chromium direct wayland", new(stubNixOS),
&fst.Config{
ID: "org.chromium.Chromium",
Command: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
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, AutoEtc: true,
Userns: true, Net: true, MapRealUID: true, DirectWayland: true, Env: nil, AutoEtc: true,
Filesystem: []*fst.FilesystemConfig{
{Src: "/bin", Must: true}, {Src: "/usr/bin", Must: true},
{Src: "/nix/store", Must: true}, {Src: "/run/current-system", Must: true},
{Src: "/sys/block"}, {Src: "/sys/bus"}, {Src: "/sys/class"}, {Src: "/sys/dev"}, {Src: "/sys/devices"},
{Src: "/run/opengl-driver", Must: true}, {Src: "/dev/dri", Device: true},
},
Override: []string{"/var/run/nscd"},
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).
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1", 0711).
Ephemeral(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/1", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/1", acl.Read, acl.Write, acl.Execute).
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
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(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",
},
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),
},
Chmod: make(bwrap.ChmodConfig),
NewSession: true,
DieWithParent: true,
AsInit: true,
}).SetUID(1971).SetGID(1971).
Procfs("/proc").
Tmpfs(fst.Tmp, 4096).
DevTmpfs("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin").
Bind("/usr/bin", "/usr/bin").
Bind("/nix/store", "/nix/store").
Bind("/run/current-system", "/run/current-system").
Bind("/sys/block", "/sys/block", true).
Bind("/sys/bus", "/sys/bus", true).
Bind("/sys/class", "/sys/class", true).
Bind("/sys/dev", "/sys/dev", true).
Bind("/sys/devices", "/sys/devices", true).
Bind("/run/opengl-driver", "/run/opengl-driver").
Bind("/dev/dri", "/dev/dri", true, true, true).
Bind("/etc", fst.Tmp+"/etc").
Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
Symlink(fst.Tmp+"/etc/default", "/etc/default").
Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/proc/mounts", "/etc/mtab").
Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
Symlink(fst.Tmp+"/etc/services", "/etc/services").
Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
Symlink(fst.Tmp+"/etc/static", "/etc/static").
Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/1971", 8388608).
Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", false, true).
Bind("/var/lib/persist/module/fortify/0/1", "/var/lib/persist/module/fortify/0/1", false, true).
CopyBind("/etc/passwd", []byte("u0_a1:x:1971:1971:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n")).
CopyBind("/etc/group", []byte("fortify:x:1971:\n")).
Bind("/run/user/1971/wayland-0", "/run/user/1971/wayland-0").
Bind("/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native").
CopyBind(fst.Tmp+"/pulse-cookie", nil).
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus").
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket").
Tmpfs("/var/run/nscd", 8192).
Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
Symlink("fortify", "/.fortify/sbin/init"),
},
}

View File

@ -6,7 +6,7 @@ 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/sandbox"
"git.gensokyo.uk/security/fortify/system"
)
@ -14,7 +14,6 @@ var testCasesPd = []sealTestCase{
{
"nixos permissive defaults no enablements", new(stubNixOS),
&fst.Config{
Command: make([]string, 0),
Confinement: fst.ConfinementConfig{
AppID: 0,
Username: "chronos",
@ -29,142 +28,134 @@ var testCasesPd = []sealTestCase{
},
system.New(1000000).
Ensure("/tmp/fortify.1971", 0711).
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac", 0711).
Ephemeral(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute),
(&bwrap.Config{
Net: true,
UserNS: true,
Clearenv: true,
Syscall: new(bwrap.SyscallPolicy),
Chdir: "/home/chronos",
SetEnv: map[string]string{
"HOME": "/home/chronos",
"SHELL": "/run/current-system/sw/bin/zsh",
"TERM": "xterm-256color",
"USER": "chronos",
"XDG_RUNTIME_DIR": "/run/user/65534",
"XDG_SESSION_CLASS": "user",
"XDG_SESSION_TYPE": "tty"},
Chmod: make(bwrap.ChmodConfig),
DieWithParent: true,
AsInit: true,
}).SetUID(65534).SetGID(65534).
Procfs("/proc").
Tmpfs(fst.Tmp, 4096).
DevTmpfs("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", false, true).
Bind("/boot", "/boot", false, true).
Bind("/home", "/home", false, true).
Bind("/lib", "/lib", false, true).
Bind("/lib64", "/lib64", false, true).
Bind("/nix", "/nix", false, true).
Bind("/root", "/root", false, true).
Bind("/run", "/run", false, true).
Bind("/srv", "/srv", false, true).
Bind("/sys", "/sys", false, true).
Bind("/usr", "/usr", false, true).
Bind("/var", "/var", false, true).
Bind("/dev/kvm", "/dev/kvm", true, true, true).
Tmpfs("/run/user/1971", 8192).
Tmpfs("/run/dbus", 8192).
Bind("/etc", fst.Tmp+"/etc").
Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
Symlink(fst.Tmp+"/etc/default", "/etc/default").
Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/proc/mounts", "/etc/mtab").
Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
Symlink(fst.Tmp+"/etc/services", "/etc/services").
Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
Symlink(fst.Tmp+"/etc/static", "/etc/static").
Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/65534", 8388608).
Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", false, true).
Bind("/home/chronos", "/home/chronos", false, true).
CopyBind("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
CopyBind("/etc/group", []byte("fortify:x:65534:\n")).
Tmpfs("/var/run/nscd", 8192).
Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
Symlink("fortify", "/.fortify/sbin/init"),
&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 "},
Args: []string{"zsh", "-c", "exec chromium "},
Confinement: fst.ConfinementConfig{
AppID: 9,
Groups: []string{"video"},
@ -201,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{
@ -212,14 +203,13 @@ var testCasesPd = []sealTestCase{
},
system.New(1000009).
Ensure("/tmp/fortify.1971", 0711).
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c", 0711).
Ephemeral(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/9", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/9", acl.Read, acl.Write, acl.Execute).
Ensure("/tmp/fortify.1971/wayland", 0711).
Wayland(new(*os.File), "/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/1971/wayland-0", "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
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).
Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse").
CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
MustProxyDBus("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", &dbus.Config{
@ -254,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",
},
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),
},
Chmod: make(bwrap.ChmodConfig),
DieWithParent: true,
AsInit: true,
}).SetUID(65534).SetGID(65534).
Procfs("/proc").
Tmpfs(fst.Tmp, 4096).
DevTmpfs("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", false, true).
Bind("/boot", "/boot", false, true).
Bind("/home", "/home", false, true).
Bind("/lib", "/lib", false, true).
Bind("/lib64", "/lib64", false, true).
Bind("/nix", "/nix", false, true).
Bind("/root", "/root", false, true).
Bind("/run", "/run", false, true).
Bind("/srv", "/srv", false, true).
Bind("/sys", "/sys", false, true).
Bind("/usr", "/usr", false, true).
Bind("/var", "/var", false, true).
Bind("/dev/dri", "/dev/dri", true, true, true).
Bind("/dev/kvm", "/dev/kvm", true, true, true).
Tmpfs("/run/user/1971", 8192).
Tmpfs("/run/dbus", 8192).
Bind("/etc", fst.Tmp+"/etc").
Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
Symlink(fst.Tmp+"/etc/default", "/etc/default").
Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/proc/mounts", "/etc/mtab").
Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
Symlink(fst.Tmp+"/etc/services", "/etc/services").
Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
Symlink(fst.Tmp+"/etc/static", "/etc/static").
Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/65534", 8388608).
Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", false, true).
Bind("/home/chronos", "/home/chronos", false, true).
CopyBind("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
CopyBind("/etc/group", []byte("fortify:x:65534:\n")).
Bind("/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/65534/wayland-0").
Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native").
CopyBind(fst.Tmp+"/pulse-cookie", nil).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket").
Tmpfs("/var/run/nscd", 8192).
Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
Symlink("fortify", "/.fortify/sbin/init"),
},
}

View File

@ -17,7 +17,8 @@ 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)) }
@ -54,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))
}

View File

@ -8,9 +8,9 @@ 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/sys"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system"
)
@ -20,7 +20,7 @@ type sealTestCase struct {
config *fst.Config
id fst.ID
wantSys *system.I
wantBwrap *bwrap.Config
wantContainer *sandbox.Params
}
func TestApp(t *testing.T) {
@ -31,14 +31,14 @@ func TestApp(t *testing.T) {
a := app.NewWithID(tc.id, tc.os)
var (
gotSys *system.I
gotBwrap *bwrap.Config
gotContainer *sandbox.Params
)
if !t.Run("seal", func(t *testing.T) {
if sa, err := a.Seal(tc.config); err != nil {
t.Errorf("Seal: error = %v", err)
return
} else {
gotSys, gotBwrap = app.AppSystemBwrap(a, sa)
gotSys, gotContainer = app.AppIParams(a, sa)
}
}) {
return
@ -51,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,8 +2,8 @@ package app
import (
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/sys"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system"
)
@ -14,7 +14,7 @@ func NewWithID(id fst.ID, os sys.State) fst.App {
return a
}
func AppSystemBwrap(a fst.App, sa fst.SealedApp) (*system.I, *bwrap.Config) {
func AppIParams(a fst.App, sa fst.SealedApp) (*system.I, *sandbox.Params) {
v := a.(*app)
seal := sa.(*outcome)
if v.outcome != seal || v.id != seal.id {

View File

@ -1,18 +0,0 @@
package init0
import (
"os"
"path"
"git.gensokyo.uk/security/fortify/internal"
)
// 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()
internal.Exit(0)
}
}

View File

@ -1,165 +0,0 @@
package init0
import (
"errors"
"log"
"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.Prepare("init")
// setting this prevents ptrace
if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
}
if os.Getpid() != 1 {
log.Fatal("this process must run as pid 1")
}
// receive setup payload
var (
payload Payload
closeSetup func() error
)
if f, err := proc.Receive(Env, &payload); err != nil {
if errors.Is(err, proc.ErrInvalid) {
log.Fatal("invalid config descriptor")
}
if errors.Is(err, proc.ErrNotSet) {
log.Fatal("FORTIFY_INIT not set")
}
log.Fatalf("cannot decode init setup payload: %v", err)
} else {
fmsg.Store(payload.Verbose)
closeSetup = f
// child does not need to see this
if err = os.Unsetenv(Env); err != nil {
log.Printf("cannot unset %s: %v", Env, err)
// not fatal
} else {
fmsg.Verbose("received configuration")
}
}
// die with parent
if err := internal.PR_SET_PDEATHSIG__SIGKILL(); err != nil {
log.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 {
log.Fatalf("cannot start %q: %v", payload.Argv0, err)
}
fmsg.Suspend()
// close setup pipe as setup is now complete
if err := closeSetup(); err != nil {
log.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) {
log.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:
if fmsg.Resume() {
fmsg.Verbosef("terminating on %s after process start", s.String())
} else {
fmsg.Verbosef("terminating on %s", s.String())
}
internal.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:
internal.Exit(r)
case <-timeout:
log.Println("timeout exceeded waiting for lingering processes")
internal.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
}

View File

@ -3,16 +3,12 @@ package app
import (
"context"
"errors"
"fmt"
"log"
"os/exec"
"path/filepath"
"strings"
"time"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper"
"git.gensokyo.uk/security/fortify/internal/app/shim"
"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"
@ -20,7 +16,7 @@ import (
const shimSetupTimeout = 5 * time.Second
func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
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
@ -32,33 +28,15 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
panic("invalid state")
}
/*
resolve exec paths
*/
shimExec := [2]string{helper.BubblewrapName}
if len(seal.command) > 0 {
shimExec[1] = 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))
}
}
}
// 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(ctx); err != nil {
if err := seal.sys.Commit(seal.ctx); err != nil {
return err
}
store := state.NewMulti(seal.runDirPath)
@ -74,16 +52,16 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
revert app setup transaction
*/
rt, ec := new(system.Enablements), new(system.Criteria)
ec.Enablements = new(system.Enablements)
ec.Set(system.Process)
var rt system.Enablement
ec := system.Process
if states, err := c.Load(); err != nil {
// revert per-process state here to limit damage
return errors.Join(err, seal.sys.Revert(ec))
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.Set(system.User)
ec |= system.User
} else {
fmsg.Verbosef("found %d active launchers, cleaning up without globals", l)
}
@ -91,38 +69,23 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
// accumulate enablements of remaining launchers
for i, s := range states {
if s.Config != nil {
*rt |= s.Config.Confinement.Enablements
rt |= s.Config.Confinement.Enablements
} else {
log.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)
}
}
ec |= rt ^ (system.EWayland | system.EX11 | system.EDBus | system.EPulse)
if fmsg.Load() {
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.Verbose("reverting operations type", strings.Join(labels, ", "))
if ec > 0 {
fmsg.Verbose("reverting operations type", system.TypeString(ec))
}
}
err := seal.sys.Revert(ec)
if err != nil {
err = err.(RevertCompoundError)
}
return err
return seal.sys.Revert((*system.Criteria)(&ec))
}()
})
storeErr.Err = errors.Join(revertErr, store.Close())
storeErr.save([]error{revertErr, store.Close()})
rs.RevertErr = storeErr.equiv("error returned during cleanup:")
}()
@ -131,11 +94,10 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
*/
waitErr := make(chan error, 1)
cmd := new(shim.Shim)
cmd := new(shimProcess)
if startTime, err := cmd.Start(
seal.user.aid.String(),
seal.user.supp,
seal.bwrapSync,
); err != nil {
return err
} else {
@ -143,7 +105,7 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
rs.Time = startTime
}
c, cancel := context.WithTimeout(ctx, shimSetupTimeout)
ctx, cancel := context.WithTimeout(seal.ctx, shimSetupTimeout)
defer cancel()
go func() {
@ -152,10 +114,8 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
cancel()
}()
if err := cmd.Serve(c, &shim.Payload{
Argv: seal.command,
Exec: shimExec,
Bwrap: seal.container,
if err := cmd.Serve(ctx, &shimParams{
Container: seal.container,
Home: seal.user.data,
Verbose: fmsg.Load(),
@ -170,7 +130,9 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
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) })
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()) }
@ -195,86 +157,24 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
// 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.WaitFallback():
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 <-ctx.Done():
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 {
// dump dbus message buffer
seal.dbusMsg()
}
return earlyStoreErr.equiv("cannot save process state:")
}
// StateStoreError is returned for a failed state save
type StateStoreError struct {
// whether inner function was called
Inner bool
// returned by the Do method of [state.Store]
DoErr error
// returned by the Save/Destroy method of [state.Cursor]
InnerErr error
// stores an arbitrary error
Err error
}
// save saves exactly one arbitrary error in [StateStoreError].
func (e *StateStoreError) save(err error) {
if err == nil || e.Err != nil {
panic("invalid call to save")
}
e.Err = err
}
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()
}
// equiv nullifies e for values where this is reached
panic("unreachable")
}
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
}
// A RevertCompoundError encapsulates errors returned by
// the Revert method of [system.I].
type RevertCompoundError interface {
Error() string
Unwrap() []error
}

View File

@ -2,6 +2,7 @@ package app
import (
"bytes"
"context"
"encoding/gob"
"errors"
"fmt"
@ -10,18 +11,20 @@ import (
"os"
"path"
"regexp"
"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/sys"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/sandbox/wl"
"git.gensokyo.uk/security/fortify/system"
"git.gensokyo.uk/security/fortify/wl"
)
const (
@ -65,23 +68,70 @@ type outcome struct {
// copied from [sys.State] response
runDirPath string
// passed through from [fst.Config]
command []string
// initial [fst.Config] gob stream for state data;
// this is prepared ahead of time as config is mutated during seal creation
// this is prepared ahead of time as config is clobbered during seal creation
ct io.WriterTo
// dump dbus proxy message buffer
dbusMsg func()
user fsuUser
sys *system.I
container *bwrap.Config
bwrapSync *os.File
ctx context.Context
container *sandbox.Params
env map[string]string
sync *os.File
f atomic.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
}
// 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)
}
// 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
@ -100,7 +150,12 @@ type fsuUser struct {
username string
}
func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
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)
@ -111,19 +166,12 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
seal.ct = ct
}
// pass through command slice; this value is never touched in the main process
seal.command = config.Command
// 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))
}
/*
Resolve post-fsu user state
*/
seal.user = fsuUser{
aid: newInt(config.Confinement.AppID),
data: config.Confinement.Outer,
@ -159,19 +207,36 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
}
}
/*
Resolve initial container state
*/
// 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
}
}
// do not use the value of shell before this point
// permissive defaults
if config.Confinement.Sandbox == nil {
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
}
}
conf := &fst.SandboxConfig{
UserNS: true,
Userns: true,
Net: true,
Syscall: new(bwrap.SyscallPolicy),
NoNewSession: true,
Tty: true,
AutoEtc: true,
}
// bind entries in /
@ -198,10 +263,10 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
// hide nscd from sandbox if present
nscd := "/var/run/nscd"
if _, err := sys.Stat(nscd); !errors.Is(err, fs.ErrNotExist) {
conf.Override = append(conf.Override, nscd)
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
@ -210,84 +275,57 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
config.Confinement.Sandbox = conf
}
var mapuid *stringPair[int]
var mapuid, mapgid *stringPair[int]
{
var uid int
var uid, gid int
var err error
seal.container, err = config.Confinement.Sandbox.Bwrap(sys, &uid)
seal.container, seal.env, err = config.Confinement.Sandbox.ToContainer(sys, &uid, &gid)
if err != nil {
return err
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)
if seal.container.SetEnv == nil {
seal.container.SetEnv = make(map[string]string)
mapgid = newInt(gid)
if seal.env == nil {
seal.env = make(map[string]string, 1<<6)
}
}
/*
Initialise externals
*/
sc := sys.Paths()
seal.runDirPath = sc.RunDirPath
seal.sys = system.New(seal.user.uid.unwrap())
seal.sys.IsVerbose = fmsg.Load
seal.sys.Verbose = fmsg.Verbose
seal.sys.Verbosef = fmsg.Verbosef
seal.sys.WrapErr = fmsg.WrapError
/*
Work directories
*/
// base fortify share path
seal.sys.Ensure(sc.SharePath, 0711)
// outer paths used by the main process
seal.sys.Ensure(sc.RunDirPath, 0700)
seal.sys.UpdatePermType(system.User, sc.RunDirPath, acl.Execute)
seal.sys.Ensure(sc.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset
seal.sys.UpdatePermType(system.User, sc.RuntimePath, acl.Execute)
// outer process-specific share directory
sharePath := path.Join(sc.SharePath, seal.id.String())
seal.sys.Ephemeral(system.Process, sharePath, 0711)
// similar to share but within XDG_RUNTIME_DIR
sharePathLocal := path.Join(sc.RunDirPath, seal.id.String())
seal.sys.Ephemeral(system.Process, sharePathLocal, 0700)
seal.sys.UpdatePerm(sharePathLocal, acl.Execute)
// inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as post-fsu user
// 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*1024*1024)
seal.container.Tmpfs(innerRuntimeDir, 8*1024*1024)
seal.container.SetEnv[xdgRuntimeDir] = innerRuntimeDir
seal.container.SetEnv[xdgSessionClass] = "user"
seal.container.SetEnv[xdgSessionType] = "tty"
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"
share := &shareHost{seal: seal, sc: sys.Paths()}
seal.runDirPath = share.sc.RunDirPath
seal.sys = system.New(seal.user.uid.unwrap())
// outer path for inner /tmp
{
tmpdir := path.Join(sc.SharePath, "tmpdir")
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)
tmpdirProc := path.Join(tmpdir, seal.user.aid.String())
seal.sys.Ensure(tmpdirProc, 01700)
seal.sys.UpdatePermType(system.User, tmpdirProc, acl.Read, acl.Write, acl.Execute)
seal.container.Bind(tmpdirProc, "/tmp", false, true)
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)
}
/*
Passwd database
*/
// look up shell
sh := "/bin/sh"
if s, ok := sys.LookupEnv(shell); ok {
seal.container.SetEnv[shell] = s
sh = s
}
// bind home directory
{
homeDir := "/var/empty"
if seal.user.home != "" {
homeDir = seal.user.home
@ -296,80 +334,70 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
if seal.user.username != "" {
username = seal.user.username
}
seal.container.Bind(seal.user.data, homeDir, false, true)
seal.container.Chdir = homeDir
seal.container.SetEnv["HOME"] = homeDir
seal.container.SetEnv["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
// generate /etc/passwd and /etc/group
seal.container.CopyBind("/etc/passwd",
[]byte(username+":x:"+mapuid.String()+":"+mapuid.String()+":Fortify:"+homeDir+":"+sh+"\n"))
seal.container.CopyBind("/etc/group",
[]byte("fortify:x:"+mapuid.String()+":\n"))
/*
Display servers
*/
// pass $TERM to launcher
if t, ok := sys.LookupEnv(term); ok {
seal.container.SetEnv[term] = t
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"))
}
// set up wayland
if config.Confinement.Enablements.Has(system.EWayland) {
// 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(sc.RuntimePath, wl.FallbackName)
socketPath = path.Join(share.sc.RuntimePath, wl.FallbackName)
} else if !path.IsAbs(name) {
socketPath = path.Join(sc.RuntimePath, name)
socketPath = path.Join(share.sc.RuntimePath, name)
} else {
socketPath = name
}
innerPath := path.Join(innerRuntimeDir, wl.FallbackName)
seal.container.SetEnv[wl.WaylandDisplay] = wl.FallbackName
seal.env[wl.WaylandDisplay] = wl.FallbackName
if !config.Confinement.Sandbox.DirectWayland { // set up security-context-v1
socketDir := path.Join(sc.SharePath, "wayland")
outerPath := path.Join(socketDir, seal.id.String())
seal.sys.Ensure(socketDir, 0711)
appID := config.ID
if appID == "" {
// use instance ID in case app id is not set
appID = "uk.gensokyo.fortify." + seal.id.String()
}
seal.sys.Wayland(&seal.bwrapSync, outerPath, socketPath, appID, seal.id.String())
seal.container.Bind(outerPath, innerPath)
// 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")
seal.container.Bind(socketPath, innerPath)
share.ensureRuntimeDir()
seal.container.Bind(socketPath, innerPath, 0)
seal.sys.UpdatePermType(system.EWayland, socketPath, acl.Read, acl.Write, acl.Execute)
}
}
// set up X11
if config.Confinement.Enablements.Has(system.EX11) {
// discover X11 and grant user permission via the `ChangeHosts` command
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.container.SetEnv[display] = d
seal.container.Bind("/tmp/.X11-unix", "/tmp/.X11-unix")
seal.env[display] = d
seal.container.Bind("/tmp/.X11-unix", "/tmp/.X11-unix", 0)
}
}
/*
PulseAudio server and authentication
*/
if config.Confinement.Enablements.Has(system.EPulse) {
if config.Confinement.Enablements&system.EPulse != 0 {
// PulseAudio runtime directory (usually `/run/user/%d/pulse`)
pulseRuntimeDir := path.Join(sc.RuntimePath, "pulse")
pulseRuntimeDir := path.Join(share.sc.RuntimePath, "pulse")
// PulseAudio socket (usually `/run/user/%d/pulse/native`)
pulseSocket := path.Join(pulseRuntimeDir, "native")
@ -397,11 +425,11 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
}
// hard link pulse socket into target-executable share
innerPulseRuntimeDir := path.Join(sharePathLocal, "pulse")
innerPulseRuntimeDir := path.Join(share.runtime(), "pulse")
innerPulseSocket := path.Join(innerRuntimeDir, "pulse", "native")
seal.sys.Link(pulseSocket, innerPulseRuntimeDir)
seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket)
seal.container.SetEnv[pulseServer] = "unix:" + innerPulseSocket
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 {
@ -409,24 +437,21 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
fmsg.Verbose(strings.TrimSpace(err.(*fmsg.BaseError).Message()))
} else {
innerDst := fst.Tmp + "/pulse-cookie"
seal.container.SetEnv[pulseCookie] = innerDst
payload := new([]byte)
seal.container.CopyBindRef(innerDst, &payload)
seal.env[pulseCookie] = innerDst
var payload *[]byte
seal.container.PlaceP(innerDst, &payload)
seal.sys.CopyFile(payload, src, 256, 256)
}
}
/*
D-Bus proxy
*/
if config.Confinement.Enablements.Has(system.EDBus) {
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
@ -441,24 +466,19 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
// share proxy sockets
sessionInner := path.Join(innerRuntimeDir, "bus")
seal.container.SetEnv[dbusSessionBusAddress] = "unix:path=" + sessionInner
seal.container.Bind(sessionPath, sessionInner)
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.container.SetEnv[dbusSystemBusAddress] = "unix:path=" + systemInner
seal.container.Bind(systemPath, systemInner)
seal.env[dbusSystemBusAddress] = "unix:path=" + systemInner
seal.container.Bind(systemPath, systemInner, 0)
seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write)
}
}
/*
Miscellaneous
*/
// queue overriding tmpfs at the end of seal.container.Filesystem
for _, dest := range config.Confinement.Sandbox.Override {
seal.container.Tmpfs(dest, 8*1024)
for _, dest := range config.Confinement.Sandbox.Cover {
seal.container.Tmpfs(dest, 1<<13, 0755)
}
// append ExtraPerms last
@ -484,12 +504,19 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
seal.sys.UpdatePermType(system.User, p.Path, perms...)
}
// mount fortify in sandbox for init
seal.container.Bind(sys.MustExecutable(), path.Join(fst.Tmp, "sbin/fortify"))
seal.container.Symlink("fortify", path.Join(fst.Tmp, "sbin/init"))
// 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, command: %s",
seal.user.uid, seal.user.username, config.Confinement.Groups, config.Command)
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)
return nil
}

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,153 +0,0 @@
package shim
import (
"context"
"errors"
"log"
"os"
"os/exec"
"os/signal"
"path"
"strconv"
"syscall"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper"
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/helper/seccomp"
"git.gensokyo.uk/security/fortify/internal"
init0 "git.gensokyo.uk/security/fortify/internal/app/init"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
// everything beyond this point runs as unconstrained target user
// proceed with caution!
func Main() {
// sharing stdout with fortify
// USE WITH CAUTION
fmsg.Prepare("shim")
// setting this prevents ptrace
if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
}
// receive setup payload
var (
payload Payload
closeSetup func() error
)
if f, err := proc.Receive(Env, &payload); err != nil {
if errors.Is(err, proc.ErrInvalid) {
log.Fatal("invalid config descriptor")
}
if errors.Is(err, proc.ErrNotSet) {
log.Fatal("FORTIFY_SHIM not set")
}
log.Fatalf("cannot decode shim setup payload: %v", err)
} else {
fmsg.Store(payload.Verbose)
closeSetup = f
}
if payload.Bwrap == nil {
log.Fatal("bwrap config not supplied")
}
// restore bwrap sync fd
var syncFd *os.File
if payload.Sync != nil {
syncFd = os.NewFile(*payload.Sync, "sync")
}
// close setup socket
if err := closeSetup(); err != nil {
log.Println("cannot close setup pipe:", err)
// not fatal
}
// ensure home directory as target user
if s, err := os.Stat(payload.Home); err != nil {
if os.IsNotExist(err) {
if err = os.Mkdir(payload.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("data path %q is not a directory", payload.Home)
}
var ic init0.Payload
// resolve argv0
ic.Argv = payload.Argv
if len(ic.Argv) > 0 {
// looked up from $PATH by parent
ic.Argv0 = payload.Exec[1]
} else {
// no argv, look up shell instead
var ok bool
if payload.Bwrap.SetEnv == nil {
log.Fatal("no command was specified and environment is unset")
}
if ic.Argv0, ok = payload.Bwrap.SetEnv["SHELL"]; !ok {
log.Fatal("no command was specified and $SHELL was unset")
}
ic.Argv = []string{ic.Argv0}
}
conf := payload.Bwrap
var extraFiles []*os.File
// serve setup payload
if fd, encoder, err := proc.Setup(&extraFiles); err != nil {
log.Fatalf("cannot pipe: %v", err)
} else {
conf.SetEnv[init0.Env] = strconv.Itoa(fd)
go func() {
fmsg.Verbose("transmitting config to init")
if err = encoder.Encode(&ic); err != nil {
log.Fatalf("cannot transmit init config: %v", err)
}
}()
}
helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent
if fmsg.Load() {
seccomp.CPrintln = log.Println
}
if b, err := helper.NewBwrap(
conf, path.Join(fst.Tmp, "sbin/init"),
nil, func(int, int) []string { return make([]string, 0) },
extraFiles,
syncFd,
); err != nil {
log.Fatalf("malformed sandbox config: %v", err)
} else {
b.Stdin(os.Stdin).Stdout(os.Stdout).Stderr(os.Stderr)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop() // unreachable
// run and pass through exit code
if err = b.Start(ctx, false); err != nil {
log.Fatalf("cannot start target process: %v", err)
} else if err = b.Wait(); err != nil {
var exitError *exec.ExitError
if !errors.As(err, &exitError) {
log.Printf("wait: %v", err)
internal.Exit(127)
panic("unreachable")
}
internal.Exit(exitError.ExitCode())
panic("unreachable")
}
}
}

View File

@ -1,139 +0,0 @@
package shim
import (
"context"
"encoding/gob"
"errors"
"os"
"os/exec"
"strconv"
"strings"
"time"
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
// used by the parent process
type Shim 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
// bwrap --sync-fd value
sync *uintptr
}
func (s *Shim) String() string {
if s.cmd == nil {
return "(unused shim manager)"
}
return s.cmd.String()
}
func (s *Shim) Unwrap() *exec.Cmd {
return s.cmd
}
func (s *Shim) WaitFallback() chan error {
return s.killFallback
}
func (s *Shim) Start(
// string representation of application id
aid string,
// string representation of supplementary group ids
supp []string,
// bwrap --sync-fd
syncFd *os.File,
) (*time.Time, error) {
// prepare user switcher invocation
var fsu string
if p, ok := internal.Path(internal.Fsu); !ok {
return nil, fmsg.WrapError(errors.New("bad fsu path"),
"invalid fsu path, this copy of fortify is not compiled correctly")
} else {
fsu = p
}
s.cmd = exec.Command(fsu)
// pass shim setup pipe
if fd, e, err := proc.Setup(&s.cmd.ExtraFiles); err != nil {
return nil, fmsg.WrapErrorSuffix(err,
"cannot create shim setup pipe:")
} else {
s.encoder = e
s.cmd.Env = []string{
Env + "=" + 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 = "/"
// pass sync fd if set
if syncFd != nil {
fd := proc.ExtraFile(s.cmd, syncFd)
s.sync = &fd
}
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 *Shim) Serve(ctx context.Context, payload *Payload) 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() }()
payload.Sync = s.sync
encodeErr := make(chan error)
go func() { encodeErr <- s.encoder.Encode(payload) }()
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(errors.New("shim setup canceled"),
"shim setup canceled")
}
if errors.Is(err, context.DeadlineExceeded) {
return fmsg.WrapError(errors.New("deadline exceeded waiting for shim"),
"deadline exceeded waiting for shim")
}
// unreachable
return err
}
}

View File

@ -1,23 +0,0 @@
package shim
import (
"git.gensokyo.uk/security/fortify/helper/bwrap"
)
const Env = "FORTIFY_SHIM"
type Payload struct {
// child full argv
Argv []string
// bwrap, target full exec path
Exec [2]string
// bwrap config
Bwrap *bwrap.Config
// path to outer home directory
Home string
// sync fd
Sync *uintptr
// verbosity pass through
Verbose bool
}

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

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

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

@ -33,10 +33,10 @@ func (s *multiStore) Do(aid int, f func(c Cursor)) (bool, error) {
// load or initialise new backend
b := new(multiBackend)
b.lock.Lock()
if v, ok := s.backends.LoadOrStore(aid, b); ok {
b = v.(*multiBackend)
} else {
b.lock.Lock()
b.path = path.Join(s.base, strconv.Itoa(aid))
// ensure directory

View File

@ -96,7 +96,7 @@ func testStore(t *testing.T, s state.Store) {
} else {
slices.Sort(aids)
want := []int{0, 1}
if slices.Compare(aids, want) != 0 {
if !slices.Equal(aids, want) {
t.Fatalf("List() = %#v, want %#v", aids, want)
}
}

View File

@ -12,8 +12,10 @@ import (
// State provides safe interaction with operating system state.
type State interface {
// Geteuid provides [os.Geteuid].
Geteuid() int
// Getuid provides [os.Getuid].
Getuid() int
// Getgid provides [os.Getgid].
Getgid() int
// LookupEnv provides [os.LookupEnv].
LookupEnv(key string) (string, bool)
// TempDir provides [os.TempDir].
@ -45,9 +47,9 @@ type State interface {
Uid(aid int) (int, error)
}
// CopyPaths is a generic implementation of [System.Paths].
// CopyPaths is a generic implementation of [fst.Paths].
func CopyPaths(os State, v *fst.Paths) {
v.SharePath = path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Geteuid()))
v.SharePath = path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Getuid()))
fmsg.Verbosef("process share directory at %q", v.SharePath)

View File

@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"io/fs"
"log"
"os"
"os/exec"
"os/user"
@ -16,6 +15,7 @@ import (
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/sandbox"
)
// Std implements System using the standard library.
@ -31,11 +31,12 @@ type Std struct {
uidMu sync.RWMutex
}
func (s *Std) Geteuid() int { return os.Geteuid() }
func (s *Std) Getuid() int { return os.Getuid() }
func (s *Std) Getgid() int { return os.Getgid() }
func (s *Std) LookupEnv(key string) (string, bool) { return os.LookupEnv(key) }
func (s *Std) TempDir() string { return os.TempDir() }
func (s *Std) LookPath(file string) (string, error) { return exec.LookPath(file) }
func (s *Std) MustExecutable() string { return internal.MustExecutable() }
func (s *Std) MustExecutable() string { return sandbox.MustExecutable() }
func (s *Std) LookupGroup(name string) (*user.Group, error) { return user.LookupGroup(name) }
func (s *Std) ReadDir(name string) ([]os.DirEntry, error) { return os.ReadDir(name) }
func (s *Std) Stat(name string) (fs.FileInfo, error) { return os.Stat(name) }
@ -79,14 +80,10 @@ func (s *Std) Uid(aid int) (int, error) {
defer func() { s.uidCopy[aid] = u }()
u.uid = -1
if fsu, ok := internal.Check(internal.Fsu); !ok {
fmsg.BeforeExit()
log.Fatal("invalid fsu path, this copy of fortify is not compiled correctly")
// unreachable
return 0, syscall.EBADE
} else {
cmd := exec.Command(fsu)
cmd.Path = fsu
fsuPath := internal.MustFsuPath()
cmd := exec.Command(fsuPath)
cmd.Path = fsuPath
cmd.Stderr = os.Stderr // pass through fatal messages
cmd.Env = []string{"FORTIFY_APP_ID=" + strconv.Itoa(aid)}
cmd.Dir = "/"
@ -103,8 +100,7 @@ func (s *Std) Uid(aid int) (int, error) {
} else if errors.As(u.err, &exitError) && exitError != nil && exitError.ExitCode() == 1 {
u.err = fmsg.WrapError(syscall.EACCES, "") // fsu prints to stderr in this case
} else if os.IsNotExist(u.err) {
u.err = fmsg.WrapError(os.ErrNotExist, fmt.Sprintf("the setuid helper is missing: %s", fsu))
u.err = fmsg.WrapError(os.ErrNotExist, fmt.Sprintf("the setuid helper is missing: %s", fsuPath))
}
return u.uid, u.err
}
}

View File

@ -3,56 +3,56 @@ package ldd
import (
"bytes"
"context"
"io"
"os"
"os/exec"
"time"
"git.gensokyo.uk/security/fortify/helper"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/sandbox"
)
const lddTimeout = 2 * time.Second
var (
msgStatic = []byte("Not a valid dynamic program")
msgStaticGlibc = []byte("not a dynamic executable")
)
func Exec(ctx context.Context, p string) ([]*Entry, error) {
var h helper.Helper
if toolPath, err := exec.LookPath("ldd"); err != nil {
return nil, err
} else if h, err = helper.NewBwrap(
(&bwrap.Config{
Hostname: "fortify-ldd",
Chdir: "/",
Syscall: &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
NewSession: true,
DieWithParent: true,
}).Bind("/", "/").DevTmpfs("/dev"), toolPath,
nil, func(_, _ int) []string { return []string{p} },
nil, nil,
); err != nil {
return nil, err
}
stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
h.Stdout(stdout).Stderr(stderr)
func Exec(ctx context.Context, p string) ([]*Entry, error) { return ExecFilter(ctx, nil, nil, p) }
func ExecFilter(ctx context.Context,
commandContext func(context.Context) *exec.Cmd,
f func([]byte) []byte,
p string) ([]*Entry, error) {
c, cancel := context.WithTimeout(ctx, lddTimeout)
defer cancel()
if err := h.Start(c, false); err != nil {
container := sandbox.New(c, "ldd", p)
container.CommandContext = commandContext
container.Hostname = "fortify-ldd"
stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
container.Stdout = stdout
container.Stderr = stderr
container.Bind("/", "/", 0).Proc("/proc").Dev("/dev")
if err := container.Start(); err != nil {
return nil, err
}
if err := h.Wait(); err != nil {
defer func() { _, _ = io.Copy(os.Stderr, stderr) }()
if err := container.Serve(); err != nil {
return nil, err
}
if err := container.Wait(); err != nil {
m := stderr.Bytes()
if bytes.Contains(m, msgStaticGlibc) {
if bytes.Contains(m, append([]byte(p+": "), msgStatic...)) ||
bytes.Contains(m, msgStaticGlibc) {
return nil, nil
}
_, _ = os.Stderr.Write(m)
return nil, err
}
return Parse(stdout)
v := stdout.Bytes()
if f != nil {
v = f(v)
}
return Parse(v)
}

View File

@ -2,7 +2,6 @@
package ldd
import (
"fmt"
"math"
"path"
"strconv"
@ -15,8 +14,8 @@ type Entry struct {
Location uint64 `json:"location"`
}
func Parse(stdout fmt.Stringer) ([]*Entry, error) {
payload := strings.Split(strings.TrimSpace(stdout.String()), "\n")
func Parse(p []byte) ([]*Entry, error) {
payload := strings.Split(strings.TrimSpace(string(p)), "\n")
result := make([]*Entry, len(payload))
for i, ent := range payload {

View File

@ -3,7 +3,6 @@ package ldd_test
import (
"errors"
"reflect"
"strings"
"testing"
"git.gensokyo.uk/security/fortify/ldd"
@ -34,10 +33,7 @@ libzstd.so.1 => /usr/lib/libzstd.so.1 7ff71bfd2000
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
stdout := new(strings.Builder)
stdout.WriteString(tc.out)
if _, err := ldd.Parse(stdout); !errors.Is(err, tc.wantErr) {
if _, err := ldd.Parse([]byte(tc.out)); !errors.Is(err, tc.wantErr) {
t.Errorf("Parse() error = %v, wantErr %v", err, tc.wantErr)
}
})
@ -111,10 +107,7 @@ libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7ff71c0a4000)`,
}
for _, tc := range testCases {
t.Run(tc.file, func(t *testing.T) {
stdout := new(strings.Builder)
stdout.WriteString(tc.out)
if got, err := ldd.Parse(stdout); err != nil {
if got, err := ldd.Parse([]byte(tc.out)); err != nil {
t.Errorf("Parse() error = %v", err)
} else if !reflect.DeepEqual(got, tc.want) {
t.Errorf("Parse() got = %#v, want %#v", got, tc.want)

21
ldd/path.go Normal file
View File

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

94
main.go
View File

@ -18,14 +18,12 @@ import (
"git.gensokyo.uk/security/fortify/command"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/seccomp"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/app"
init0 "git.gensokyo.uk/security/fortify/internal/app/init"
"git.gensokyo.uk/security/fortify/internal/app/shim"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/state"
"git.gensokyo.uk/security/fortify/internal/sys"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system"
)
@ -41,10 +39,10 @@ func init() { fmsg.Prepare("fortify") }
var std sys.State = new(sys.Std)
func main() {
// early init argv0 check, skips root check and duplicate PR_SET_DUMPABLE
init0.TryArgv0()
// early init path, skips root check and duplicate PR_SET_DUMPABLE
sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg)
if err := internal.PR_SET_DUMPABLE__SUID_DUMP_DISABLE(); err != nil {
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
}
@ -68,10 +66,15 @@ func buildCommand(out io.Writer) command.Command {
flagVerbose bool
flagJSON bool
)
c := command.New(out, log.Printf, "fortify", func([]string) error { fmsg.Store(flagVerbose); return nil }).
c := command.New(out, log.Printf, "fortify", func([]string) error {
internal.InstallFmsg(flagVerbose)
return nil
}).
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output as JSON when applicable")
c.Command("shim", command.UsageInternal, func([]string) error { app.ShimMain(); return errSuccess })
c.Command("app", "Launch app defined by the specified config file", func(args []string) error {
if len(args) < 1 {
log.Fatal("app requires at least 1 argument")
@ -79,10 +82,9 @@ func buildCommand(out io.Writer) command.Command {
// config extraArgs...
config := tryPath(args[0])
config.Command = append(config.Command, args[1:]...)
config.Args = append(config.Args, args[1:]...)
// invoke app
runApp(app.MustNew(std), config)
runApp(config)
panic("unreachable")
})
@ -98,14 +100,15 @@ func buildCommand(out io.Writer) command.Command {
groups command.RepeatableFlag
homeDir string
userName string
enablements [system.ELen]bool
wayland, x11, dBus, pulse bool
)
c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error {
// initialise config from flags
config := &fst.Config{
ID: fid,
Command: args,
Args: args,
}
if aid < 0 || aid > 9999 {
@ -155,15 +158,21 @@ func buildCommand(out io.Writer) command.Command {
config.Confinement.Outer = homeDir
config.Confinement.Username = userName
// enablements from flags
for i := system.Enablement(0); i < system.Enablement(system.ELen); i++ {
if enablements[i] {
config.Confinement.Enablements.Set(i)
if wayland {
config.Confinement.Enablements |= system.EWayland
}
if x11 {
config.Confinement.Enablements |= system.EX11
}
if dBus {
config.Confinement.Enablements |= system.EDBus
}
if pulse {
config.Confinement.Enablements |= system.EPulse
}
// parse D-Bus config file from flags if applicable
if enablements[system.EDBus] {
if dBus {
if dbusConfigSession == "builtin" {
config.Confinement.SessionBus = dbus.NewConfig(fid, true, mpris)
} else {
@ -191,7 +200,7 @@ func buildCommand(out io.Writer) command.Command {
}
// invoke app
runApp(app.MustNew(std), config)
runApp(config)
panic("unreachable")
}).
Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"),
@ -212,13 +221,13 @@ func buildCommand(out io.Writer) command.Command {
"Application home directory").
Flag(&userName, "u", command.StringFlag("chronos"),
"Passwd name within sandbox").
Flag(&enablements[system.EWayland], "wayland", command.BoolFlag(false),
Flag(&wayland, "wayland", command.BoolFlag(false),
"Allow Wayland connections").
Flag(&enablements[system.EX11], "X", command.BoolFlag(false),
Flag(&x11, "X", command.BoolFlag(false),
"Share X11 socket and allow connection").
Flag(&enablements[system.EDBus], "dbus", command.BoolFlag(false),
Flag(&dBus, "dbus", command.BoolFlag(false),
"Proxy D-Bus connection").
Flag(&enablements[system.EPulse], "pulse", command.BoolFlag(false),
Flag(&pulse, "pulse", command.BoolFlag(false),
"Share PulseAudio socket and cookie")
}
@ -249,11 +258,7 @@ func buildCommand(out io.Writer) command.Command {
}).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id")
c.Command("version", "Show fortify version", func(args []string) error {
if v, ok := internal.Check(internal.Version); ok {
fmt.Println(v)
} else {
fmt.Println("impure")
}
fmt.Println(internal.Version())
return errSuccess
})
@ -272,45 +277,22 @@ func buildCommand(out io.Writer) command.Command {
return errSuccess
})
// internal commands
c.Command("shim", command.UsageInternal, func([]string) error { shim.Main(); return errSuccess })
c.Command("init", command.UsageInternal, func([]string) error { init0.Main(); return errSuccess })
return c
}
func runApp(a fst.App, config *fst.Config) {
rs := new(fst.RunState)
func runApp(config *fst.Config) {
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer stop() // unreachable
a := app.MustNew(ctx, std)
if fmsg.Load() {
seccomp.CPrintln = log.Println
}
rs := new(fst.RunState)
if sa, err := a.Seal(config); err != nil {
fmsg.PrintBaseError(err, "cannot seal app:")
internal.Exit(1)
} else if err = sa.Run(ctx, rs); err != nil {
if rs.Time == nil {
fmsg.PrintBaseError(err, "cannot start app:")
rs.ExitCode = 1
} else {
logWaitError(err)
}
if rs.ExitCode == 0 {
rs.ExitCode = 126
}
}
if rs.RevertErr != nil {
fmsg.PrintBaseError(rs.RevertErr, "generic error returned during cleanup:")
if rs.ExitCode == 0 {
rs.ExitCode = 128
}
}
if rs.WaitErr != nil {
log.Println("inner wait failed:", rs.WaitErr)
// this updates ExitCode
app.PrintRunStateErr(rs, sa.Run(rs))
}
internal.Exit(rs.ExitCode)
}

View File

@ -1,3 +1,4 @@
packages:
{
lib,
pkgs,
@ -26,7 +27,7 @@ let
in
{
imports = [ ./options.nix ];
imports = [ (import ./options.nix packages) ];
config = mkIf cfg.enable {
security.wrappers.fsu = {
@ -77,29 +78,26 @@ in
};
in
{
session_bus =
if app.dbus.session != null then
(app.dbus.session (extendDBusDefault app.id))
else
(extendDBusDefault app.id default);
session_bus = if app.dbus.session != null then (app.dbus.session (extendDBusDefault app.id)) else (extendDBusDefault app.id default);
system_bus = app.dbus.system;
};
command = if app.command == null then app.name else app.command;
script = if app.script == null then ("exec " + command + " $@") else app.script;
enablements =
with app.capability;
(if wayland then 1 else 0)
+ (if x11 then 2 else 0)
+ (if dbus then 4 else 0)
+ (if pulse then 8 else 0);
enablements = with app.capability; (if wayland then 1 else 0) + (if x11 then 2 else 0) + (if dbus then 4 else 0) + (if pulse then 8 else 0);
isGraphical = if app.gpu != null then app.gpu else app.capability.wayland || app.capability.x11;
conf = {
inherit (app) id;
command = [
(pkgs.writeScript "${app.name}-start" ''
path =
if app.path == null then
pkgs.writeScript "${app.name}-start" ''
#!${pkgs.zsh}${pkgs.zsh.shellPath}
${script}
'')
];
''
else
app.path;
args = if app.args == null then [ "${app.name}-start" ] else app.args;
confinement = {
app_id = aid;
inherit (app) groups;
@ -107,18 +105,17 @@ in
home = getsubhome fid aid;
sandbox = {
inherit (app)
devel
userns
net
dev
tty
multiarch
env
;
syscall = {
inherit (app) compat multiarch bluetooth;
deny_devel = !app.devel;
};
map_real_uid = app.mapRealUid;
no_new_session = app.tty;
direct_wayland = app.insecureWayland;
filesystem =
let
bind = src: { inherit src; };
@ -135,7 +132,6 @@ in
(mustBind "/bin")
(mustBind "/usr/bin")
(mustBind "/nix/store")
(mustBind "/run/current-system")
(bind "/sys/block")
(bind "/sys/bus")
(bind "/sys/class")
@ -146,8 +142,7 @@ in
(mustBind "/nix/var")
(bind "/var/db/nix-channels")
]
++ optionals (if app.gpu != null then app.gpu else app.capability.wayland || app.capability.x11) [
(bind "/run/opengl-driver")
++ optionals isGraphical [
(devBind "/dev/dri")
(devBind "/dev/nvidiactl")
(devBind "/dev/nvidia-modeset")
@ -157,17 +152,38 @@ in
]
++ app.extraPaths;
auto_etc = true;
override = [ "/var/run/nscd" ];
cover = [ "/var/run/nscd" ];
symlink =
[
[
"*/run/current-system"
"/run/current-system"
]
]
++ optionals (isGraphical && config.hardware.graphics.enable) (
[
[
config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver"."L+".argument
"/run/opengl-driver"
]
]
++ optionals (app.multiarch && config.hardware.graphics.enable32Bit) [
[
config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver-32"."L+".argument
/run/opengl-driver-32
]
]
);
};
inherit enablements;
inherit (dbusConfig) session_bus system_bus;
};
};
in
pkgs.writeShellScriptBin app.name ''
exec fortify${
if app.verbose then " -v" else ""
} app ${pkgs.writeText "fortify-${app.name}.json" (builtins.toJSON conf)} $@
exec fortify${if app.verbose then " -v" else ""} app ${pkgs.writeText "fortify-${app.name}.json" (builtins.toJSON conf)} $@
''
) cfg.apps;
in
@ -185,9 +201,11 @@ in
${copy "${pkg}/share/icons"}
${copy "${pkg}/share/man"}
if test -d "$out/share/applications"; then
substituteInPlace $out/share/applications/* \
--replace-warn '${pkg}/bin/' "" \
--replace-warn '${pkg}/libexec/' ""
fi
''
)
++ acc

View File

@ -21,7 +21,6 @@ boolean
## environment\.fortify\.package
@ -36,8 +35,7 @@ package
*Default:*
` <derivation fortify-static-x86_64-unknown-linux-musl-0.2.18> `
` <derivation fortify-static-x86_64-unknown-linux-musl-0.3.3> `
@ -57,7 +55,6 @@ list of (submodule)
## environment\.fortify\.apps\.\*\.packages
@ -76,28 +73,22 @@ list of package
## environment\.fortify\.apps\.\*\.bluetooth
## environment\.fortify\.apps\.\*\.args
Whether to enable AF_BLUETOOTH socket operations\.
Custom args\.
Setting this to null will default to script name\.
*Type:*
boolean
null or (list of string)
*Default:*
` false `
*Example:*
` true `
` null `
@ -119,7 +110,6 @@ boolean
## environment\.fortify\.apps\.\*\.capability\.pulse
@ -138,7 +128,6 @@ boolean
## environment\.fortify\.apps\.\*\.capability\.wayland
@ -157,7 +146,6 @@ boolean
## environment\.fortify\.apps\.\*\.capability\.x11
@ -176,7 +164,6 @@ boolean
## environment\.fortify\.apps\.\*\.command
@ -197,31 +184,6 @@ null or string
## environment\.fortify\.apps\.\*\.compat
Whether to enable disable syscall filter extensions\.
*Type:*
boolean
*Default:*
` false `
*Example:*
` true `
## environment\.fortify\.apps\.\*\.dbus\.session
@ -241,7 +203,6 @@ null or (function that evaluates to a(n) anything)
## environment\.fortify\.apps\.\*\.dbus\.system
@ -261,7 +222,6 @@ null or anything
## environment\.fortify\.apps\.\*\.dev
@ -285,12 +245,11 @@ boolean
## environment\.fortify\.apps\.\*\.devel
Whether to enable development kernel APIs\.
Whether to enable debugging-related kernel interfaces\.
@ -309,7 +268,6 @@ boolean
## environment\.fortify\.apps\.\*\.env
@ -328,7 +286,6 @@ null or (attribute set of string)
## environment\.fortify\.apps\.\*\.extraConfig
@ -347,7 +304,6 @@ anything
## environment\.fortify\.apps\.\*\.extraPaths
@ -366,7 +322,6 @@ list of anything
## environment\.fortify\.apps\.\*\.gpu
@ -386,7 +341,6 @@ null or boolean
## environment\.fortify\.apps\.\*\.groups
@ -405,7 +359,6 @@ list of string
## environment\.fortify\.apps\.\*\.id
@ -424,7 +377,6 @@ null or string
## environment\.fortify\.apps\.\*\.insecureWayland
@ -448,7 +400,6 @@ boolean
## environment\.fortify\.apps\.\*\.mapRealUid
@ -472,12 +423,11 @@ boolean
## environment\.fortify\.apps\.\*\.multiarch
Whether to enable multiarch kernel support\.
Whether to enable multiarch kernel-level support\.
@ -496,7 +446,6 @@ boolean
## environment\.fortify\.apps\.\*\.name
@ -510,7 +459,6 @@ string
## environment\.fortify\.apps\.\*\.net
@ -534,12 +482,11 @@ boolean
## environment\.fortify\.apps\.\*\.nix
Whether to enable nix daemon\.
Whether to enable nix daemon access\.
@ -558,6 +505,24 @@ boolean
## environment\.fortify\.apps\.\*\.path
Custom executable path\.
Setting this to null will default to the start script\.
*Type:*
null or string
*Default:*
` null `
## environment\.fortify\.apps\.\*\.script
@ -577,7 +542,6 @@ null or string
## environment\.fortify\.apps\.\*\.share
@ -597,7 +561,6 @@ null or package
## environment\.fortify\.apps\.\*\.tty
@ -621,12 +584,11 @@ boolean
## environment\.fortify\.apps\.\*\.userns
Whether to enable user namespace\.
Whether to enable user namespace creation\.
@ -645,7 +607,6 @@ boolean
## environment\.fortify\.apps\.\*\.verbose
@ -669,7 +630,6 @@ boolean
## environment\.fortify\.fsuPackage
@ -684,8 +644,7 @@ package
*Default:*
` <derivation fortify-fsu-0.2.18> `
` <derivation fortify-fsu-0.3.3> `
@ -702,7 +661,6 @@ function that evaluates to a(n) function that evaluates to a(n) attribute set of
## environment\.fortify\.stateDir
@ -716,7 +674,6 @@ string
## environment\.fortify\.users
@ -729,4 +686,3 @@ Users allowed to spawn fortify apps and their corresponding fortify fid\.
attribute set of integer between 0 and 99 (both inclusive)

View File

@ -1,10 +1,8 @@
packages:
{ lib, pkgs, ... }:
let
inherit (lib) types mkOption mkEnableOption;
fortify = pkgs.pkgsStatic.callPackage ./package.nix {
inherit (pkgs) bubblewrap xdg-dbus-proxy glibc;
};
in
{
@ -14,13 +12,13 @@ in
package = mkOption {
type = types.package;
default = fortify;
default = packages.${pkgs.system}.fortify;
description = "The fortify package to use.";
};
fsuPackage = mkOption {
type = types.package;
default = pkgs.callPackage ./cmd/fsu/package.nix { inherit fortify; };
default = packages.${pkgs.system}.fsu;
description = "The fsu package to use.";
};
@ -96,6 +94,24 @@ in
'';
};
path = mkOption {
type = nullOr str;
default = null;
description = ''
Custom executable path.
Setting this to null will default to the start script.
'';
};
args = mkOption {
type = nullOr (listOf str);
default = null;
description = ''
Custom args.
Setting this to null will default to script name.
'';
};
script = mkOption {
type = nullOr str;
default = null;
@ -150,21 +166,19 @@ in
'';
};
nix = mkEnableOption "nix daemon";
userns = mkEnableOption "user namespace";
mapRealUid = mkEnableOption "mapping to priv-user uid";
dev = mkEnableOption "access to all devices";
devel = mkEnableOption "debugging-related kernel interfaces";
userns = mkEnableOption "user namespace creation";
tty = mkEnableOption "access to the controlling terminal";
insecureWayland = mkEnableOption "direct access to the Wayland socket";
multiarch = mkEnableOption "multiarch kernel-level support";
net = mkEnableOption "network access" // {
default = true;
};
compat = mkEnableOption "disable syscall filter extensions";
devel = mkEnableOption "development kernel APIs";
multiarch = mkEnableOption "multiarch kernel support";
bluetooth = mkEnableOption "AF_BLUETOOTH socket operations";
nix = mkEnableOption "nix daemon access";
mapRealUid = mkEnableOption "mapping to priv-user uid";
dev = mkEnableOption "access to all devices";
insecureWayland = mkEnableOption "direct access to the Wayland socket";
gpu = mkOption {
type = nullOr bool;

View File

@ -4,7 +4,6 @@
buildGoModule,
makeBinaryWrapper,
xdg-dbus-proxy,
bubblewrap,
pkg-config,
libffi,
libseccomp,
@ -14,21 +13,30 @@
wayland-scanner,
xorg,
# for fpkg
zstd,
gnutar,
coreutils,
# for passthru.buildInputs
go,
gcc,
# for check
util-linux,
glibc, # for ldd
withStatic ? stdenv.hostPlatform.isStatic,
}:
buildGoModule rec {
pname = "fortify";
version = "0.2.18";
version = "0.3.3";
src = builtins.path {
name = "${pname}-src";
path = lib.cleanSource ./.;
filter =
path: type:
!(type == "regular" && lib.hasSuffix ".nix" path)
&& !(type == "directory" && lib.hasSuffix "/cmd/fsu" path);
filter = path: type: !(type == "regular" && (lib.hasSuffix ".nix" path || lib.hasSuffix ".py" path)) && !(type == "directory" && lib.hasSuffix "/test" path) && !(type == "directory" && lib.hasSuffix "/cmd/fsu" path);
};
vendorHash = null;
@ -39,17 +47,15 @@ buildGoModule rec {
ldflags ++ [ "-X git.gensokyo.uk/security/fortify/internal.${name}=${value}" ]
)
(
[
"-s -w"
]
[ "-s -w" ]
++ lib.optionals withStatic [
"-linkmode external"
"-extldflags \"-static\""
]
)
{
Version = "v${version}";
Fsu = "/run/wrappers/bin/fsu";
version = "v${version}";
fsu = "/run/wrappers/bin/fsu";
};
# nix build environment does not allow acls
@ -79,19 +85,42 @@ buildGoModule rec {
HOME="$(mktemp -d)" PATH="${pkg-config}/bin:$PATH" go generate ./...
'';
postInstall = ''
postInstall =
let
appPackages = [
glibc
xdg-dbus-proxy
];
in
''
install -D --target-directory=$out/share/zsh/site-functions comp/*
mkdir "$out/libexec"
mv "$out"/bin/* "$out/libexec/"
makeBinaryWrapper "$out/libexec/fortify" "$out/bin/fortify" \
--inherit-argv0 --prefix PATH : ${lib.makeBinPath appPackages}
makeBinaryWrapper "$out/libexec/fpkg" "$out/bin/fpkg" \
--inherit-argv0 --prefix PATH : ${
lib.makeBinPath [
glibc
bubblewrap
xdg-dbus-proxy
lib.makeBinPath (
appPackages
++ [
zstd
gnutar
coreutils
]
)
}
'';
passthru.targetPkgs =
[
go
gcc
xorg.xorgproto
util-linux
]
++ buildInputs
++ nativeBuildInputs;
}

View File

@ -50,9 +50,12 @@ func tryPath(name string) (config *fst.Config) {
func tryFd(name string) io.ReadCloser {
if v, err := strconv.Atoi(name); err != nil {
if !errors.Is(err, strconv.ErrSyntax) {
fmsg.Verbosef("name cannot be interpreted as int64: %v", err)
}
return nil
} else {
fmsg.Verbosef("trying config stream from %d", v)
fd := uintptr(v)
if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 {
if errors.Is(errno, syscall.EBADF) {

View File

@ -77,7 +77,9 @@ func printShowInstance(
if len(config.Confinement.Groups) > 0 {
t.Printf(" Groups:\t%q\n", config.Confinement.Groups)
}
if config.Confinement.Outer != "" {
t.Printf(" Directory:\t%s\n", config.Confinement.Outer)
}
if config.Confinement.Sandbox != nil {
sandbox := config.Confinement.Sandbox
if sandbox.Hostname != "" {
@ -89,10 +91,10 @@ func printShowInstance(
flags = append(flags, name)
}
}
writeFlag("userns", sandbox.UserNS)
writeFlag("userns", sandbox.Userns)
writeFlag("net", sandbox.Net)
writeFlag("dev", sandbox.Dev)
writeFlag("tty", sandbox.NoNewSession)
writeFlag("tty", sandbox.Tty)
writeFlag("mapuid", sandbox.MapRealUID)
writeFlag("directwl", sandbox.DirectWayland)
writeFlag("autoetc", sandbox.AutoEtc)
@ -107,14 +109,19 @@ func printShowInstance(
}
t.Printf(" Etc:\t%s\n", etc)
if len(sandbox.Override) > 0 {
t.Printf(" Overrides:\t%s\n", strings.Join(sandbox.Override, " "))
if len(sandbox.Cover) > 0 {
t.Printf(" Cover:\t%s\n", strings.Join(sandbox.Cover, " "))
}
// Env map[string]string `json:"env"`
// Link [][2]string `json:"symlink"`
}
t.Printf(" Command:\t%s\n", strings.Join(config.Command, " "))
if config.Confinement.Sandbox != nil {
t.Printf(" Path:\t%s\n", config.Path)
}
if len(config.Args) > 0 {
t.Printf(" Arguments:\t%s\n", strings.Join(config.Args, " "))
}
t.Printf("\n")
if !short {
@ -247,22 +254,18 @@ func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON boo
t := newPrinter(output)
defer t.MustFlush()
t.Println("\tInstance\tPID\tApp\tUptime\tEnablements\tCommand")
t.Println("\tInstance\tPID\tApplication\tUptime")
for _, e := range exp {
var (
es = "(No confinement information)"
cs = "(No command information)"
as = "(No configuration information)"
)
as := "(No configuration information)"
if e.Config != nil {
es = e.Config.Confinement.Enablements.String()
cs = fmt.Sprintf("%q", e.Config.Command)
as = strconv.Itoa(e.Config.Confinement.AppID)
if e.Config.ID != "" {
as += " (" + e.Config.ID + ")"
}
t.Printf("\t%s\t%d\t%s\t%s\t%s\t%s\n",
e.s[:8], e.PID, as, now.Sub(e.Time).Round(time.Second).String(), strings.TrimPrefix(es, ", "), cs)
}
t.Println()
t.Printf("\t%s\t%d\t%s\t%s\n",
e.s[:8], e.PID, as, now.Sub(e.Time).Round(time.Second).String())
}
}
type expandedStateEntry struct {

View File

@ -37,14 +37,15 @@ func Test_printShowInstance(t *testing.T) {
}{
{"config", nil, fst.Template(), false, false, `App
ID: 9 (org.chromium.Chromium)
Enablements: Wayland, D-Bus, PulseAudio
Enablements: wayland, dbus, pulseaudio
Groups: ["video"]
Directory: /var/lib/persist/home/org.chromium.Chromium
Hostname: "localhost"
Flags: userns net dev tty mapuid autoetc
Etc: /etc
Overrides: /var/run/nscd
Command: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
Cover: /var/run/nscd
Path: /run/current-system/sw/bin/chromium
Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
Filesystem
+/nix/store
@ -74,27 +75,23 @@ System bus
App
ID: 0
Enablements: (No enablements)
Directory:
Command:
Enablements: (no enablements)
`},
{"config flag none", nil, &fst.Config{Confinement: fst.ConfinementConfig{Sandbox: new(fst.SandboxConfig)}}, false, false, `App
ID: 0
Enablements: (No enablements)
Directory:
Enablements: (no enablements)
Flags: none
Etc: /etc
Command:
Path:
`},
{"config nil entries", nil, &fst.Config{Confinement: fst.ConfinementConfig{Sandbox: &fst.SandboxConfig{Filesystem: make([]*fst.FilesystemConfig, 1)}, ExtraPerms: make([]*fst.ExtraPermConfig, 1)}}, false, false, `App
ID: 0
Enablements: (No enablements)
Directory:
Enablements: (no enablements)
Flags: none
Etc: /etc
Command:
Path:
Filesystem
@ -105,9 +102,7 @@ Extra ACL
App
ID: 0
Enablements: (No enablements)
Directory:
Command:
Enablements: (no enablements)
Session bus
Filter: false
@ -121,14 +116,15 @@ Session bus
App
ID: 9 (org.chromium.Chromium)
Enablements: Wayland, D-Bus, PulseAudio
Enablements: wayland, dbus, pulseaudio
Groups: ["video"]
Directory: /var/lib/persist/home/org.chromium.Chromium
Hostname: "localhost"
Flags: userns net dev tty mapuid autoetc
Etc: /etc
Overrides: /var/run/nscd
Command: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
Cover: /var/run/nscd
Path: /run/current-system/sw/bin/chromium
Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
Filesystem
+/nix/store
@ -162,9 +158,7 @@ State
App
ID: 0
Enablements: (No enablements)
Directory:
Command:
Enablements: (no enablements)
`},
@ -192,7 +186,8 @@ App
"pid": 3735928559,
"config": {
"id": "org.chromium.Chromium",
"command": [
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
@ -207,26 +202,22 @@ App
"username": "chronos",
"home_inner": "/var/lib/fortify",
"home": "/var/lib/persist/home/org.chromium.Chromium",
"shell": "/run/current-system/sw/bin/zsh",
"sandbox": {
"hostname": "localhost",
"seccomp": 32,
"devel": true,
"userns": true,
"net": true,
"dev": true,
"syscall": {
"compat": false,
"deny_devel": true,
"tty": true,
"multiarch": true,
"linux32": false,
"can": false,
"bluetooth": false
},
"no_new_session": true,
"map_real_uid": true,
"env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
},
"map_real_uid": true,
"dev": true,
"filesystem": [
{
"src": "/nix/store"
@ -259,7 +250,7 @@ App
],
"etc": "/etc",
"auto_etc": true,
"override": [
"cover": [
"/var/run/nscd"
]
},
@ -320,7 +311,8 @@ App
`},
{"json config", nil, fst.Template(), false, true, `{
"id": "org.chromium.Chromium",
"command": [
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
@ -335,26 +327,22 @@ App
"username": "chronos",
"home_inner": "/var/lib/fortify",
"home": "/var/lib/persist/home/org.chromium.Chromium",
"shell": "/run/current-system/sw/bin/zsh",
"sandbox": {
"hostname": "localhost",
"seccomp": 32,
"devel": true,
"userns": true,
"net": true,
"dev": true,
"syscall": {
"compat": false,
"deny_devel": true,
"tty": true,
"multiarch": true,
"linux32": false,
"can": false,
"bluetooth": false
},
"no_new_session": true,
"map_real_uid": true,
"env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
},
"map_real_uid": true,
"dev": true,
"filesystem": [
{
"src": "/nix/store"
@ -387,7 +375,7 @@ App
],
"etc": "/etc",
"auto_etc": true,
"override": [
"cover": [
"/var/run/nscd"
]
},
@ -466,20 +454,16 @@ func Test_printPs(t *testing.T) {
short, json bool
want string
}{
{"no entries", make(state.Entries), false, false, ` Instance PID App Uptime Enablements Command
{"no entries", make(state.Entries), false, false, ` Instance PID Application Uptime
`},
{"no entries short", make(state.Entries), true, false, ``},
{"nil instance", state.Entries{testID: nil}, false, false, ` Instance PID App Uptime Enablements Command
{"nil instance", state.Entries{testID: nil}, false, false, ` Instance PID Application Uptime
`},
{"state corruption", state.Entries{fst.ID{}: testState}, false, false, ` Instance PID App Uptime Enablements Command
{"state corruption", state.Entries{fst.ID{}: testState}, false, false, ` Instance PID Application Uptime
`},
{"valid", state.Entries{testID: testState}, false, false, ` Instance PID App Uptime Enablements Command
8e2c76b0 3735928559 9 1h2m32s Wayland, D-Bus, PulseAudio ["chromium" "--ignore-gpu-blocklist" "--disable-smooth-scrolling" "--enable-features=UseOzonePlatform" "--ozone-platform=wayland"]
{"valid", state.Entries{testID: testState}, false, false, ` Instance PID Application Uptime
8e2c76b0 3735928559 9 (org.chromium.Chromium) 1h2m32s
`},
{"valid short", state.Entries{testID: testState}, true, false, `8e2c76b0
`},
@ -506,7 +490,8 @@ func Test_printPs(t *testing.T) {
"pid": 3735928559,
"config": {
"id": "org.chromium.Chromium",
"command": [
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
@ -521,26 +506,22 @@ func Test_printPs(t *testing.T) {
"username": "chronos",
"home_inner": "/var/lib/fortify",
"home": "/var/lib/persist/home/org.chromium.Chromium",
"shell": "/run/current-system/sw/bin/zsh",
"sandbox": {
"hostname": "localhost",
"seccomp": 32,
"devel": true,
"userns": true,
"net": true,
"dev": true,
"syscall": {
"compat": false,
"deny_devel": true,
"tty": true,
"multiarch": true,
"linux32": false,
"can": false,
"bluetooth": false
},
"no_new_session": true,
"map_real_uid": true,
"env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
},
"map_real_uid": true,
"dev": true,
"filesystem": [
{
"src": "/nix/store"
@ -573,7 +554,7 @@ func Test_printPs(t *testing.T) {
],
"etc": "/etc",
"auto_etc": true,
"override": [
"cover": [
"/var/run/nscd"
]
},

244
sandbox/container.go Normal file
View File

@ -0,0 +1,244 @@
// Package sandbox implements unprivileged Linux container with hardening options useful for creating application sandboxes.
package sandbox
import (
"context"
"encoding/gob"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path"
"strconv"
"syscall"
"time"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
)
type HardeningFlags uintptr
const (
FSyscallCompat HardeningFlags = 1 << iota
FAllowDevel
FAllowUserns
FAllowTTY
FAllowNet
)
func (flags HardeningFlags) seccomp(opts seccomp.SyscallOpts) seccomp.SyscallOpts {
if flags&FSyscallCompat == 0 {
opts |= seccomp.FlagExt
}
if flags&FAllowDevel == 0 {
opts |= seccomp.FlagDenyDevel
}
if flags&FAllowUserns == 0 {
opts |= seccomp.FlagDenyNS
}
if flags&FAllowTTY == 0 {
opts |= seccomp.FlagDenyTTY
}
return opts
}
type (
// Container represents a container environment being prepared or run.
// None of [Container] methods are safe for concurrent use.
Container struct {
// Name of initial process in the container.
name string
// Cgroup fd, nil to disable.
Cgroup *int
// ExtraFiles passed through to initial process in the container,
// with behaviour identical to its [exec.Cmd] counterpart.
ExtraFiles []*os.File
// Custom [exec.Cmd] initialisation function.
CommandContext func(ctx context.Context) (cmd *exec.Cmd)
// param encoder for shim and init
setup *gob.Encoder
// cancels cmd
cancel context.CancelFunc
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
Cancel func(cmd *exec.Cmd) error
WaitDelay time.Duration
cmd *exec.Cmd
ctx context.Context
Params
}
// Params holds container configuration and is safe to serialise.
Params struct {
// Working directory in the container.
Dir string
// Initial process environment.
Env []string
// Absolute path of initial process in the container. Overrides name.
Path string
// Initial process argv.
Args []string
// Mapped Uid in user namespace.
Uid int
// Mapped Gid in user namespace.
Gid int
// Hostname value in UTS namespace.
Hostname string
// Sequential container setup ops.
*Ops
// Extra seccomp options.
Seccomp seccomp.SyscallOpts
// Permission bits of newly created parent directories.
// The zero value is interpreted as 0755.
ParentPerm os.FileMode
// Retain CAP_SYS_ADMIN.
Privileged bool
Flags HardeningFlags
}
)
func (p *Container) Start() error {
if p.cmd != nil {
return errors.New("sandbox: already started")
}
if p.Ops == nil || len(*p.Ops) == 0 {
return errors.New("sandbox: starting an empty container")
}
ctx, cancel := context.WithCancel(p.ctx)
p.cancel = cancel
var cloneFlags uintptr = syscall.CLONE_NEWIPC |
syscall.CLONE_NEWUTS |
syscall.CLONE_NEWCGROUP
if p.Flags&FAllowNet == 0 {
cloneFlags |= syscall.CLONE_NEWNET
}
// map to overflow id to work around ownership checks
if p.Uid < 1 {
p.Uid = OverflowUid()
}
if p.Gid < 1 {
p.Gid = OverflowGid()
}
if p.CommandContext != nil {
p.cmd = p.CommandContext(ctx)
} else {
p.cmd = exec.CommandContext(ctx, MustExecutable())
p.cmd.Args = []string{"init"}
}
p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
p.cmd.WaitDelay = p.WaitDelay
if p.Cancel != nil {
p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
} else {
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(syscall.SIGTERM) }
}
p.cmd.Dir = "/"
p.cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: p.Flags&FAllowTTY == 0,
Pdeathsig: syscall.SIGKILL,
Cloneflags: cloneFlags |
syscall.CLONE_NEWUSER |
syscall.CLONE_NEWPID |
syscall.CLONE_NEWNS,
// remain privileged for setup
AmbientCaps: []uintptr{CAP_SYS_ADMIN, CAP_SETPCAP},
UseCgroupFD: p.Cgroup != nil,
}
if p.cmd.SysProcAttr.UseCgroupFD {
p.cmd.SysProcAttr.CgroupFD = *p.Cgroup
}
// place setup pipe before user supplied extra files, this is later restored by init
if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil {
return wrapErrSuffix(err,
"cannot create shim setup pipe:")
} else {
p.setup = e
p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
}
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...)
msg.Verbose("starting container init")
if err := p.cmd.Start(); err != nil {
return msg.WrapErr(err, err.Error())
}
return nil
}
func (p *Container) Serve() error {
if p.setup == nil {
panic("invalid serve")
}
setup := p.setup
p.setup = nil
if p.Path != "" && !path.IsAbs(p.Path) {
p.cancel()
return msg.WrapErr(syscall.EINVAL,
fmt.Sprintf("invalid executable path %q", p.Path))
}
if p.Path == "" {
if p.name == "" {
p.Path = os.Getenv("SHELL")
if !path.IsAbs(p.Path) {
p.cancel()
return msg.WrapErr(syscall.EBADE,
"no command specified and $SHELL is invalid")
}
p.name = path.Base(p.Path)
} else if path.IsAbs(p.name) {
p.Path = p.name
} else if v, err := exec.LookPath(p.name); err != nil {
p.cancel()
return msg.WrapErr(err, err.Error())
} else {
p.Path = v
}
}
err := setup.Encode(
&initParams{
p.Params,
syscall.Getuid(),
syscall.Getgid(),
len(p.ExtraFiles),
msg.IsVerbose(),
},
)
if err != nil {
p.cancel()
}
return err
}
func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() }
func (p *Container) String() string {
return fmt.Sprintf("argv: %q, flags: %#x, seccomp: %#x",
p.Args, p.Flags, int(p.Flags.seccomp(p.Seccomp)))
}
func New(ctx context.Context, name string, args ...string) *Container {
return &Container{name: name, ctx: ctx,
Params: Params{Args: append([]string{name}, args...), Dir: "/", Ops: new(Ops)},
}
}

255
sandbox/container_test.go Normal file
View File

@ -0,0 +1,255 @@
package sandbox_test
import (
"bytes"
"context"
"encoding/gob"
"log"
"os"
"os/exec"
"strings"
"syscall"
"testing"
"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/ldd"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
"git.gensokyo.uk/security/fortify/sandbox/vfs"
)
const (
ignore = "\x00"
ignoreV = -1
)
func TestContainer(t *testing.T) {
{
oldVerbose := fmsg.Load()
oldOutput := sandbox.GetOutput()
internal.InstallFmsg(true)
t.Cleanup(func() { fmsg.Store(oldVerbose) })
t.Cleanup(func() { sandbox.SetOutput(oldOutput) })
}
testCases := []struct {
name string
flags sandbox.HardeningFlags
ops *sandbox.Ops
mnt []*vfs.MountInfoEntry
host string
}{
{"minimal", 0, new(sandbox.Ops), nil, "test-minimal"},
{"allow", sandbox.FAllowUserns | sandbox.FAllowNet | sandbox.FAllowTTY,
new(sandbox.Ops), nil, "test-minimal"},
{"tmpfs", 0,
new(sandbox.Ops).
Tmpfs(fst.Tmp, 0, 0755),
[]*vfs.MountInfoEntry{
e("/", fst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
}, "test-tmpfs"},
{"dev", sandbox.FAllowTTY, // go test output is not a tty
new(sandbox.Ops).
Dev("/dev").
Mqueue("/dev/mqueue"),
[]*vfs.MountInfoEntry{
e("/", "/dev", "rw,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
e("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
e("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
}, ""},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
container := sandbox.New(ctx, "/usr/bin/sandbox.test", "-test.v",
"-test.run=TestHelperCheckContainer", "--", "check", tc.host)
container.Uid = 1000
container.Gid = 100
container.Hostname = tc.host
container.CommandContext = commandContext
container.Flags |= tc.flags
container.Stdout, container.Stderr = os.Stdout, os.Stderr
container.Ops = tc.ops
if container.Args[5] == "" {
if name, err := os.Hostname(); err != nil {
t.Fatalf("cannot get hostname: %v", err)
} else {
container.Args[5] = name
}
}
container.
Tmpfs("/tmp", 0, 0755).
Bind(os.Args[0], os.Args[0], 0).
Mkdir("/usr/bin", 0755).
Link(os.Args[0], "/usr/bin/sandbox.test").
Place("/etc/hostname", []byte(container.Args[5]))
// in case test has cgo enabled
var libPaths []string
if entries, err := ldd.ExecFilter(ctx,
commandContext,
func(v []byte) []byte {
return bytes.SplitN(v, []byte("TestHelperInit\n"), 2)[1]
}, os.Args[0]); err != nil {
log.Fatalf("ldd: %v", err)
} else {
libPaths = ldd.Path(entries)
}
for _, name := range libPaths {
container.Bind(name, name, 0)
}
// needs /proc to check mountinfo
container.Proc("/proc")
mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
mnt = append(mnt, e("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore))
mnt = append(mnt, tc.mnt...)
mnt = append(mnt,
e("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
e(ignore, os.Args[0], "ro,nosuid,nodev,relatime", ignore, ignore, ignore),
e(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
)
for _, name := range libPaths {
mnt = append(mnt, e(ignore, name, "ro,nosuid,nodev,relatime", ignore, ignore, ignore))
}
mnt = append(mnt, e("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"))
want := new(bytes.Buffer)
if err := gob.NewEncoder(want).Encode(mnt); err != nil {
t.Fatalf("cannot serialise expected mount points: %v", err)
}
container.Stdin = want
if err := container.Start(); err != nil {
fmsg.PrintBaseError(err, "start:")
t.Fatalf("cannot start container: %v", err)
} else if err = container.Serve(); err != nil {
fmsg.PrintBaseError(err, "serve:")
t.Errorf("cannot serve setup params: %v", err)
}
if err := container.Wait(); err != nil {
fmsg.PrintBaseError(err, "wait:")
t.Fatalf("wait: %v", err)
}
})
}
}
func e(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry {
return &vfs.MountInfoEntry{
ID: ignoreV,
Parent: ignoreV,
Devno: vfs.DevT{ignoreV, ignoreV},
Root: root,
Target: target,
VfsOptstr: vfsOptstr,
OptFields: []string{ignore},
FsType: fsType,
Source: source,
FsOptstr: fsOptstr,
}
}
func TestContainerString(t *testing.T) {
container := sandbox.New(context.TODO(), "ldd", "/usr/bin/env")
container.Flags |= sandbox.FAllowDevel
container.Seccomp |= seccomp.FlagMultiarch
want := `argv: ["ldd" "/usr/bin/env"], flags: 0x2, seccomp: 0x2e`
if got := container.String(); got != want {
t.Errorf("String: %s, want %s", got, want)
}
}
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)
}
func TestHelperCheckContainer(t *testing.T) {
if len(os.Args) != 6 || os.Args[4] != "check" {
return
}
t.Run("user", func(t *testing.T) {
if uid := syscall.Getuid(); uid != 1000 {
t.Errorf("Getuid: %d, want 1000", uid)
}
if gid := syscall.Getgid(); gid != 100 {
t.Errorf("Getgid: %d, want 100", gid)
}
})
t.Run("hostname", func(t *testing.T) {
if name, err := os.Hostname(); err != nil {
t.Fatalf("cannot get hostname: %v", err)
} else if name != os.Args[5] {
t.Errorf("Hostname: %q, want %q", name, os.Args[5])
}
if p, err := os.ReadFile("/etc/hostname"); err != nil {
t.Fatalf("%v", err)
} else if string(p) != os.Args[5] {
t.Errorf("/etc/hostname: %q, want %q", string(p), os.Args[5])
}
})
t.Run("mount", func(t *testing.T) {
var mnt []*vfs.MountInfoEntry
if err := gob.NewDecoder(os.Stdin).Decode(&mnt); err != nil {
t.Fatalf("cannot receive expected mount points: %v", err)
}
var d *vfs.MountInfoDecoder
if f, err := os.Open("/proc/self/mountinfo"); err != nil {
t.Fatalf("cannot open mountinfo: %v", err)
} else {
d = vfs.NewMountInfoDecoder(f)
}
i := 0
for cur := range d.Entries() {
if i == len(mnt) {
t.Errorf("got more than %d entries", len(mnt))
break
}
// ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",relatime")
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",noatime")
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",relatime")
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime")
if !cur.EqualWithIgnore(mnt[i], "\x00") {
t.Errorf("[FAIL] %s", cur)
} else {
t.Logf("[ OK ] %s", cur)
}
i++
}
if err := d.Err(); err != nil {
t.Errorf("cannot parse mountinfo: %v", err)
}
if i != len(mnt) {
t.Errorf("got %d entries, want %d", i, len(mnt))
}
})
}
func commandContext(ctx context.Context) *exec.Cmd {
return exec.CommandContext(ctx, os.Args[0], "-test.v",
"-test.run=TestHelperInit", "--", "init")
}

View File

@ -1,11 +1,9 @@
package internal
package sandbox
import (
"log"
"os"
"sync"
"git.gensokyo.uk/security/fortify/internal/fmsg"
)
var (
@ -15,7 +13,7 @@ var (
func copyExecutable() {
if name, err := os.Executable(); err != nil {
fmsg.BeforeExit()
msg.BeforeExit()
log.Fatalf("cannot read executable path: %v", err)
} else {
executable = name

View File

@ -0,0 +1,17 @@
package sandbox_test
import (
"os"
"testing"
"git.gensokyo.uk/security/fortify/sandbox"
)
func TestExecutable(t *testing.T) {
for i := 0; i < 16; i++ {
if got := sandbox.MustExecutable(); got != os.Args[0] {
t.Errorf("MustExecutable: %q, want %q",
got, os.Args[0])
}
}
}

362
sandbox/init.go Normal file
View File

@ -0,0 +1,362 @@
package sandbox
import (
"errors"
"fmt"
"log"
"os"
"os/exec"
"os/signal"
"path"
"runtime"
"strconv"
"syscall"
"time"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
)
const (
// time to wait for linger processes after death of initial process
residualProcessTimeout = 5 * time.Second
// intermediate tmpfs mount point
basePath = "/tmp"
// setup params file descriptor
setupEnv = "FORTIFY_SETUP"
)
type initParams struct {
Params
HostUid, HostGid int
// extra files count
Count int
// verbosity pass through
Verbose bool
}
func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
runtime.LockOSThread()
prepare("init")
if os.Getpid() != 1 {
log.Fatal("this process must run as pid 1")
}
var (
params initParams
closeSetup func() error
setupFile *os.File
offsetSetup int
)
if f, err := Receive(setupEnv, &params, &setupFile); err != nil {
if errors.Is(err, ErrInvalid) {
log.Fatal("invalid setup descriptor")
}
if errors.Is(err, ErrNotSet) {
log.Fatal("FORTIFY_SETUP not set")
}
log.Fatalf("cannot decode init setup payload: %v", err)
} else {
if params.Ops == nil {
log.Fatal("invalid setup parameters")
}
if params.ParentPerm == 0 {
params.ParentPerm = 0755
}
setVerbose(params.Verbose)
msg.Verbose("received setup parameters")
closeSetup = f
offsetSetup = int(setupFile.Fd() + 1)
}
// write uid/gid map here so parent does not need to set dumpable
if err := SetDumpable(SUID_DUMP_USER); err != nil {
log.Fatalf("cannot set SUID_DUMP_USER: %s", err)
}
if err := os.WriteFile("/proc/self/uid_map",
append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...),
0); err != nil {
log.Fatalf("%v", err)
}
if err := os.WriteFile("/proc/self/setgroups",
[]byte("deny\n"),
0); err != nil && !os.IsNotExist(err) {
log.Fatalf("%v", err)
}
if err := os.WriteFile("/proc/self/gid_map",
append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...),
0); err != nil {
log.Fatalf("%v", err)
}
if err := SetDumpable(SUID_DUMP_DISABLE); err != nil {
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
}
oldmask := syscall.Umask(0)
if params.Hostname != "" {
if err := syscall.Sethostname([]byte(params.Hostname)); err != nil {
log.Fatalf("cannot set hostname: %v", err)
}
}
// cache sysctl before pivot_root
LastCap()
if err := syscall.Mount("", "/", "",
syscall.MS_SILENT|syscall.MS_SLAVE|syscall.MS_REC,
""); err != nil {
log.Fatalf("cannot make / rslave: %v", err)
}
for i, op := range *params.Ops {
if op == nil {
log.Fatalf("invalid op %d", i)
}
if err := op.early(&params.Params); err != nil {
msg.PrintBaseErr(err,
fmt.Sprintf("cannot prepare op %d:", i))
msg.BeforeExit()
os.Exit(1)
}
}
if err := syscall.Mount("rootfs", basePath, "tmpfs",
syscall.MS_NODEV|syscall.MS_NOSUID,
""); err != nil {
log.Fatalf("cannot mount intermediate root: %v", err)
}
if err := os.Chdir(basePath); err != nil {
log.Fatalf("cannot enter base path: %v", err)
}
if err := os.Mkdir(sysrootDir, 0755); err != nil {
log.Fatalf("%v", err)
}
if err := syscall.Mount(sysrootDir, sysrootDir, "",
syscall.MS_SILENT|syscall.MS_MGC_VAL|syscall.MS_BIND|syscall.MS_REC,
""); err != nil {
log.Fatalf("cannot bind sysroot: %v", err)
}
if err := os.Mkdir(hostDir, 0755); err != nil {
log.Fatalf("%v", err)
}
// pivot_root uncovers basePath in hostDir
if err := syscall.PivotRoot(basePath, hostDir); err != nil {
log.Fatalf("cannot pivot into intermediate root: %v", err)
}
if err := os.Chdir("/"); err != nil {
log.Fatalf("%v", err)
}
for i, op := range *params.Ops {
// ops already checked during early setup
msg.Verbosef("%s %s", op.prefix(), op)
if err := op.apply(&params.Params); err != nil {
msg.PrintBaseErr(err,
fmt.Sprintf("cannot apply op %d:", i))
msg.BeforeExit()
os.Exit(1)
}
}
// setup requiring host root complete at this point
if err := syscall.Mount(hostDir, hostDir, "",
syscall.MS_SILENT|syscall.MS_REC|syscall.MS_PRIVATE,
""); err != nil {
log.Fatalf("cannot make host root rprivate: %v", err)
}
if err := syscall.Unmount(hostDir, syscall.MNT_DETACH); err != nil {
log.Fatalf("cannot unmount host root: %v", err)
}
{
var fd int
if err := IgnoringEINTR(func() (err error) {
fd, err = syscall.Open("/", syscall.O_DIRECTORY|syscall.O_RDONLY, 0)
return
}); err != nil {
log.Fatalf("cannot open intermediate root: %v", err)
}
if err := os.Chdir(sysrootPath); err != nil {
log.Fatalf("%v", err)
}
if err := syscall.PivotRoot(".", "."); err != nil {
log.Fatalf("cannot pivot into sysroot: %v", err)
}
if err := syscall.Fchdir(fd); err != nil {
log.Fatalf("cannot re-enter intermediate root: %v", err)
}
if err := syscall.Unmount(".", syscall.MNT_DETACH); err != nil {
log.Fatalf("cannot unmount intemediate root: %v", err)
}
if err := os.Chdir("/"); err != nil {
log.Fatalf("%v", err)
}
if err := syscall.Close(fd); err != nil {
log.Fatalf("cannot close intermediate root: %v", err)
}
}
if _, _, errno := syscall.Syscall(PR_SET_NO_NEW_PRIVS, 1, 0, 0); errno != 0 {
log.Fatalf("prctl(PR_SET_NO_NEW_PRIVS): %v", errno)
}
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0); errno != 0 {
log.Fatalf("cannot clear the ambient capability set: %v", errno)
}
for i := uintptr(0); i <= LastCap(); i++ {
if params.Privileged && i == CAP_SYS_ADMIN {
continue
}
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_CAPBSET_DROP, i, 0); errno != 0 {
log.Fatalf("cannot drop capability from bonding set: %v", errno)
}
}
var keep [2]uint32
if params.Privileged {
keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN)
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, CAP_SYS_ADMIN); errno != 0 {
log.Fatalf("cannot raise CAP_SYS_ADMIN: %v", errno)
}
}
if err := capset(
&capHeader{_LINUX_CAPABILITY_VERSION_3, 0},
&[2]capData{{0, keep[0], keep[0]}, {0, keep[1], keep[1]}},
); err != nil {
log.Fatalf("cannot capset: %v", err)
}
if err := seccomp.Load(params.Flags.seccomp(params.Seccomp)); err != nil {
log.Fatalf("cannot load syscall filter: %v", err)
}
extraFiles := make([]*os.File, params.Count)
for i := range extraFiles {
// setup fd is placed before all extra files
extraFiles[i] = os.NewFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i))
}
syscall.Umask(oldmask)
cmd := exec.Command(params.Path)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Args = params.Args
cmd.Env = params.Env
cmd.ExtraFiles = extraFiles
cmd.Dir = params.Dir
if err := cmd.Start(); err != nil {
log.Fatalf("%v", err)
}
msg.Suspend()
if err := closeSetup(); err != nil {
log.Println("cannot close setup pipe:", err)
// not fatal
}
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) {
log.Println("unexpected wait4 response:", err)
}
close(done)
}()
// handle signals to dump withheld messages
sig := make(chan os.Signal, 2)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
// closed after residualProcessTimeout has elapsed after initial process death
timeout := make(chan struct{})
r := 2
for {
select {
case s := <-sig:
if msg.Resume() {
msg.Verbosef("terminating on %s after process start", s.String())
} else {
msg.Verbosef("terminating on %s", s.String())
}
os.Exit(0)
case w := <-info:
if w.wpid == cmd.Process.Pid {
// initial process exited, output is most likely available again
msg.Resume()
switch {
case w.wstatus.Exited():
r = w.wstatus.ExitStatus()
msg.Verbosef("initial process exited with code %d", w.wstatus.ExitStatus())
case w.wstatus.Signaled():
r = 128 + int(w.wstatus.Signal())
msg.Verbosef("initial process exited with signal %s", w.wstatus.Signal())
default:
r = 255
msg.Verbosef("initial process exited with status %#x", w.wstatus)
}
go func() {
time.Sleep(residualProcessTimeout)
close(timeout)
}()
}
case <-done:
msg.BeforeExit()
os.Exit(r)
case <-timeout:
log.Println("timeout exceeded waiting for lingering processes")
msg.BeforeExit()
os.Exit(r)
}
}
}
// TryArgv0 calls [Init] if the last element of argv0 is "init".
func TryArgv0(v Msg, prepare func(prefix string), setVerbose func(verbose bool)) {
if len(os.Args) > 0 && path.Base(os.Args[0]) == "init" {
msg = v
Init(prepare, setVerbose)
msg.BeforeExit()
os.Exit(0)
}
}

125
sandbox/mount.go Normal file
View File

@ -0,0 +1,125 @@
package sandbox
import (
"errors"
"fmt"
"os"
"path/filepath"
"syscall"
"git.gensokyo.uk/security/fortify/sandbox/vfs"
)
func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) error {
if eq {
msg.Verbosef("resolved %q flags %#x", target, flags)
} else {
msg.Verbosef("resolved %q on %q flags %#x", source, target, flags)
}
if err := syscall.Mount(source, target, "",
syscall.MS_SILENT|syscall.MS_BIND|flags&syscall.MS_REC, ""); err != nil {
return wrapErrSuffix(err,
fmt.Sprintf("cannot mount %q on %q:", source, target))
}
var targetFinal string
if v, err := filepath.EvalSymlinks(target); err != nil {
return wrapErrSelf(err)
} else {
targetFinal = v
if targetFinal != target {
msg.Verbosef("target resolves to %q", targetFinal)
}
}
// final target path according to the kernel through proc
var targetKFinal string
{
var destFd int
if err := IgnoringEINTR(func() (err error) {
destFd, err = syscall.Open(targetFinal, O_PATH|syscall.O_CLOEXEC, 0)
return
}); err != nil {
return wrapErrSuffix(err,
fmt.Sprintf("cannot open %q:", targetFinal))
}
if v, err := os.Readlink(p.fd(destFd)); err != nil {
return wrapErrSelf(err)
} else if err = syscall.Close(destFd); err != nil {
return wrapErrSuffix(err,
fmt.Sprintf("cannot close %q:", targetFinal))
} else {
targetKFinal = v
}
}
mf := syscall.MS_NOSUID | flags&syscall.MS_NODEV | flags&syscall.MS_RDONLY
return hostProc.mountinfo(func(d *vfs.MountInfoDecoder) error {
n, err := d.Unfold(targetKFinal)
if err != nil {
if errors.Is(err, syscall.ESTALE) {
return msg.WrapErr(err,
fmt.Sprintf("mount point %q never appeared in mountinfo", targetKFinal))
}
return wrapErrSuffix(err,
"cannot unfold mount hierarchy:")
}
if err = remountWithFlags(n, mf); err != nil {
return err
}
if flags&syscall.MS_REC == 0 {
return nil
}
for cur := range n.Collective() {
err = remountWithFlags(cur, mf)
if err != nil && !errors.Is(err, syscall.EACCES) {
return err
}
}
return nil
})
}
func remountWithFlags(n *vfs.MountInfoNode, mf uintptr) error {
kf, unmatched := n.Flags()
if len(unmatched) != 0 {
msg.Verbosef("unmatched vfs options: %q", unmatched)
}
if kf&mf != mf {
return wrapErrSuffix(syscall.Mount("none", n.Clean, "",
syscall.MS_SILENT|syscall.MS_BIND|syscall.MS_REMOUNT|kf|mf,
""),
fmt.Sprintf("cannot remount %q:", n.Clean))
}
return nil
}
func mountTmpfs(fsname, name string, size int, perm os.FileMode) error {
target := toSysroot(name)
if err := os.MkdirAll(target, parentPerm(perm)); err != nil {
return wrapErrSelf(err)
}
opt := fmt.Sprintf("mode=%#o", perm)
if size > 0 {
opt += fmt.Sprintf(",size=%d", size)
}
return wrapErrSuffix(syscall.Mount(fsname, target, "tmpfs",
syscall.MS_NOSUID|syscall.MS_NODEV, opt),
fmt.Sprintf("cannot mount tmpfs on %q:", name))
}
func parentPerm(perm os.FileMode) os.FileMode {
pperm := 0755
if perm&0070 == 0 {
pperm &= ^0050
}
if perm&0007 == 0 {
pperm &= ^0005
}
return os.FileMode(pperm)
}

43
sandbox/msg.go Normal file
View File

@ -0,0 +1,43 @@
package sandbox
import (
"log"
"sync/atomic"
)
type Msg interface {
IsVerbose() bool
Verbose(v ...any)
Verbosef(format string, v ...any)
WrapErr(err error, a ...any) error
PrintBaseErr(err error, fallback string)
Suspend()
Resume() bool
BeforeExit()
}
type DefaultMsg struct{ inactive atomic.Bool }
func (msg *DefaultMsg) IsVerbose() bool { return true }
func (msg *DefaultMsg) Verbose(v ...any) {
if !msg.inactive.Load() {
log.Println(v...)
}
}
func (msg *DefaultMsg) Verbosef(format string, v ...any) {
if !msg.inactive.Load() {
log.Printf(format, v...)
}
}
func (msg *DefaultMsg) WrapErr(err error, a ...any) error {
log.Println(a...)
return err
}
func (msg *DefaultMsg) PrintBaseErr(err error, fallback string) { log.Println(fallback, err) }
func (msg *DefaultMsg) Suspend() { msg.inactive.Store(true) }
func (msg *DefaultMsg) Resume() bool { return msg.inactive.CompareAndSwap(true, false) }
func (msg *DefaultMsg) BeforeExit() {}

442
sandbox/ops.go Normal file
View File

@ -0,0 +1,442 @@
package sandbox
import (
"encoding/gob"
"fmt"
"math"
"os"
"path"
"path/filepath"
"slices"
"strings"
"syscall"
"unsafe"
)
type (
Ops []Op
Op interface {
// early is called in host root.
early(params *Params) error
// apply is called in intermediate root.
apply(params *Params) error
prefix() string
Is(op Op) bool
fmt.Stringer
}
)
func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) }
func init() { gob.Register(new(BindMount)) }
// BindMount bind mounts host path Source on container path Target.
type BindMount struct {
Source, SourceFinal, Target string
Flags int
}
const (
BindOptional = 1 << iota
BindWritable
BindDevice
)
func (b *BindMount) early(*Params) error {
if !path.IsAbs(b.Source) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", b.Source))
}
if v, err := filepath.EvalSymlinks(b.Source); err != nil {
if os.IsNotExist(err) && b.Flags&BindOptional != 0 {
b.SourceFinal = "\x00"
return nil
}
return wrapErrSelf(err)
} else {
b.SourceFinal = v
return nil
}
}
func (b *BindMount) apply(*Params) error {
if b.SourceFinal == "\x00" {
if b.Flags&BindOptional == 0 {
// unreachable
return syscall.EBADE
}
return nil
}
if !path.IsAbs(b.SourceFinal) || !path.IsAbs(b.Target) {
return msg.WrapErr(syscall.EBADE,
"path is not absolute")
}
source := toHost(b.SourceFinal)
target := toSysroot(b.Target)
// this perm value emulates bwrap behaviour as it clears bits from 0755 based on
// op->perms which is never set for any bind setup op so always results in 0700
if fi, err := os.Stat(source); err != nil {
return wrapErrSelf(err)
} else if fi.IsDir() {
if err = os.MkdirAll(target, 0700); err != nil {
return wrapErrSelf(err)
}
} else if err = ensureFile(target, 0444, 0700); err != nil {
return err
}
var flags uintptr = syscall.MS_REC
if b.Flags&BindWritable == 0 {
flags |= syscall.MS_RDONLY
}
if b.Flags&BindDevice == 0 {
flags |= syscall.MS_NODEV
}
return hostProc.bindMount(source, target, flags, b.SourceFinal == b.Target)
}
func (b *BindMount) Is(op Op) bool { vb, ok := op.(*BindMount); return ok && *b == *vb }
func (*BindMount) prefix() string { return "mounting" }
func (b *BindMount) String() string {
if b.Source == b.Target {
return fmt.Sprintf("%q flags %#x", b.Source, b.Flags)
}
return fmt.Sprintf("%q on %q flags %#x", b.Source, b.Target, b.Flags&BindWritable)
}
func (f *Ops) Bind(source, target string, flags int) *Ops {
*f = append(*f, &BindMount{source, "", target, flags})
return f
}
func init() { gob.Register(new(MountProc)) }
// MountProc mounts a private instance of proc.
type MountProc string
func (p MountProc) early(*Params) error { return nil }
func (p MountProc) apply(params *Params) error {
v := string(p)
if !path.IsAbs(v) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", v))
}
target := toSysroot(v)
if err := os.MkdirAll(target, params.ParentPerm); err != nil {
return wrapErrSelf(err)
}
return wrapErrSuffix(syscall.Mount("proc", target, "proc",
syscall.MS_NOSUID|syscall.MS_NOEXEC|syscall.MS_NODEV, ""),
fmt.Sprintf("cannot mount proc on %q:", v))
}
func (p MountProc) Is(op Op) bool { vp, ok := op.(MountProc); return ok && p == vp }
func (MountProc) prefix() string { return "mounting" }
func (p MountProc) String() string { return fmt.Sprintf("proc on %q", string(p)) }
func (f *Ops) Proc(dest string) *Ops {
*f = append(*f, MountProc(dest))
return f
}
func init() { gob.Register(new(MountDev)) }
// MountDev mounts part of host dev.
type MountDev string
func (d MountDev) early(*Params) error { return nil }
func (d MountDev) apply(params *Params) error {
v := string(d)
if !path.IsAbs(v) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", v))
}
target := toSysroot(v)
if err := mountTmpfs("devtmpfs", v, 0, params.ParentPerm); err != nil {
return err
}
for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} {
targetPath := toSysroot(path.Join(v, name))
if err := ensureFile(targetPath, 0444, params.ParentPerm); err != nil {
return err
}
if err := hostProc.bindMount(
toHost("/dev/"+name),
targetPath,
0,
true,
); err != nil {
return err
}
}
for i, name := range []string{"stdin", "stdout", "stderr"} {
if err := os.Symlink(
"/proc/self/fd/"+string(rune(i+'0')),
path.Join(target, name),
); err != nil {
return wrapErrSelf(err)
}
}
for _, pair := range [][2]string{
{"/proc/self/fd", "fd"},
{"/proc/kcore", "core"},
{"pts/ptmx", "ptmx"},
} {
if err := os.Symlink(pair[0], path.Join(target, pair[1])); err != nil {
return wrapErrSelf(err)
}
}
devPtsPath := path.Join(target, "pts")
for _, name := range []string{path.Join(target, "shm"), devPtsPath} {
if err := os.Mkdir(name, params.ParentPerm); err != nil {
return wrapErrSelf(err)
}
}
if err := syscall.Mount("devpts", devPtsPath, "devpts",
syscall.MS_NOSUID|syscall.MS_NOEXEC,
"newinstance,ptmxmode=0666,mode=620"); err != nil {
return wrapErrSuffix(err,
fmt.Sprintf("cannot mount devpts on %q:", devPtsPath))
}
if params.Flags&FAllowTTY != 0 {
var buf [8]byte
if _, _, errno := syscall.Syscall(
syscall.SYS_IOCTL, 1, syscall.TIOCGWINSZ,
uintptr(unsafe.Pointer(&buf[0])),
); errno == 0 {
consolePath := toSysroot(path.Join(v, "console"))
if err := ensureFile(consolePath, 0444, params.ParentPerm); err != nil {
return err
}
if name, err := os.Readlink(hostProc.stdout()); err != nil {
return wrapErrSelf(err)
} else if err = hostProc.bindMount(
toHost(name),
consolePath,
0,
false,
); err != nil {
return err
}
}
}
return nil
}
func (d MountDev) Is(op Op) bool { vd, ok := op.(MountDev); return ok && d == vd }
func (MountDev) prefix() string { return "mounting" }
func (d MountDev) String() string { return fmt.Sprintf("dev on %q", string(d)) }
func (f *Ops) Dev(dest string) *Ops {
*f = append(*f, MountDev(dest))
return f
}
func init() { gob.Register(new(MountMqueue)) }
// MountMqueue mounts a private mqueue instance on container Path.
type MountMqueue string
func (m MountMqueue) early(*Params) error { return nil }
func (m MountMqueue) apply(params *Params) error {
v := string(m)
if !path.IsAbs(v) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", v))
}
target := toSysroot(v)
if err := os.MkdirAll(target, params.ParentPerm); err != nil {
return wrapErrSelf(err)
}
return wrapErrSuffix(syscall.Mount("mqueue", target, "mqueue",
syscall.MS_NOSUID|syscall.MS_NOEXEC|syscall.MS_NODEV, ""),
fmt.Sprintf("cannot mount mqueue on %q:", v))
}
func (m MountMqueue) Is(op Op) bool { vm, ok := op.(MountMqueue); return ok && m == vm }
func (MountMqueue) prefix() string { return "mounting" }
func (m MountMqueue) String() string { return fmt.Sprintf("mqueue on %q", string(m)) }
func (f *Ops) Mqueue(dest string) *Ops {
*f = append(*f, MountMqueue(dest))
return f
}
func init() { gob.Register(new(MountTmpfs)) }
// MountTmpfs mounts tmpfs on container Path.
type MountTmpfs struct {
Path string
Size int
Perm os.FileMode
}
func (t *MountTmpfs) early(*Params) error { return nil }
func (t *MountTmpfs) apply(*Params) error {
if !path.IsAbs(t.Path) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", t.Path))
}
if t.Size < 0 || t.Size > math.MaxUint>>1 {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("size %d out of bounds", t.Size))
}
return mountTmpfs("tmpfs", t.Path, t.Size, t.Perm)
}
func (t *MountTmpfs) Is(op Op) bool { vt, ok := op.(*MountTmpfs); return ok && *t == *vt }
func (*MountTmpfs) prefix() string { return "mounting" }
func (t *MountTmpfs) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }
func (f *Ops) Tmpfs(dest string, size int, perm os.FileMode) *Ops {
*f = append(*f, &MountTmpfs{dest, size, perm})
return f
}
func init() { gob.Register(new(Symlink)) }
// Symlink creates a symlink in the container filesystem.
type Symlink [2]string
func (l *Symlink) early(*Params) error {
if strings.HasPrefix(l[0], "*") {
l[0] = l[0][1:]
if !path.IsAbs(l[0]) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", l[0]))
}
if name, err := os.Readlink(l[0]); err != nil {
return wrapErrSelf(err)
} else {
l[0] = name
}
}
return nil
}
func (l *Symlink) apply(params *Params) error {
// symlink target is an arbitrary path value, so only validate link name here
if !path.IsAbs(l[1]) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", l[1]))
}
target := toSysroot(l[1])
if err := os.MkdirAll(path.Dir(target), params.ParentPerm); err != nil {
return wrapErrSelf(err)
}
if err := os.Symlink(l[0], target); err != nil {
return wrapErrSelf(err)
}
return nil
}
func (l *Symlink) Is(op Op) bool { vl, ok := op.(*Symlink); return ok && *l == *vl }
func (*Symlink) prefix() string { return "creating" }
func (l *Symlink) String() string { return fmt.Sprintf("symlink on %q target %q", l[1], l[0]) }
func (f *Ops) Link(target, linkName string) *Ops {
*f = append(*f, &Symlink{target, linkName})
return f
}
func init() { gob.Register(new(Mkdir)) }
// Mkdir creates a directory in the container filesystem.
type Mkdir struct {
Path string
Perm os.FileMode
}
func (m *Mkdir) early(*Params) error { return nil }
func (m *Mkdir) apply(*Params) error {
if !path.IsAbs(m.Path) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", m.Path))
}
if err := os.MkdirAll(toSysroot(m.Path), m.Perm); err != nil {
return wrapErrSelf(err)
}
return nil
}
func (m *Mkdir) Is(op Op) bool { vm, ok := op.(*Mkdir); return ok && m == vm }
func (*Mkdir) prefix() string { return "creating" }
func (m *Mkdir) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) }
func (f *Ops) Mkdir(dest string, perm os.FileMode) *Ops {
*f = append(*f, &Mkdir{dest, perm})
return f
}
func init() { gob.Register(new(Tmpfile)) }
// Tmpfile places a file in container Path containing Data.
type Tmpfile struct {
Path string
Data []byte
}
func (t *Tmpfile) early(*Params) error { return nil }
func (t *Tmpfile) apply(params *Params) error {
if !path.IsAbs(t.Path) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", t.Path))
}
var tmpPath string
if f, err := os.CreateTemp("/", "tmp.*"); err != nil {
return wrapErrSelf(err)
} else if _, err = f.Write(t.Data); err != nil {
return wrapErrSuffix(err,
"cannot write to intermediate file:")
} else if err = f.Close(); err != nil {
return wrapErrSuffix(err,
"cannot close intermediate file:")
} else {
tmpPath = f.Name()
}
target := toSysroot(t.Path)
if err := ensureFile(target, 0444, params.ParentPerm); err != nil {
return err
} else if err = hostProc.bindMount(
tmpPath,
target,
syscall.MS_RDONLY|syscall.MS_NODEV,
false,
); err != nil {
return err
} else if err = os.Remove(tmpPath); err != nil {
return wrapErrSelf(err)
}
return nil
}
func (t *Tmpfile) Is(op Op) bool {
vt, ok := op.(*Tmpfile)
return ok && t.Path == vt.Path && slices.Equal(t.Data, vt.Data)
}
func (*Tmpfile) prefix() string { return "placing" }
func (t *Tmpfile) String() string {
return fmt.Sprintf("tmpfile %q (%d bytes)", t.Path, len(t.Data))
}
func (f *Ops) Place(name string, data []byte) *Ops { *f = append(*f, &Tmpfile{name, data}); return f }
func (f *Ops) PlaceP(name string, dataP **[]byte) *Ops {
t := &Tmpfile{Path: name}
*dataP = &t.Data
*f = append(*f, t)
return f
}

26
sandbox/output.go Normal file
View File

@ -0,0 +1,26 @@
package sandbox
var msg Msg = new(DefaultMsg)
func GetOutput() Msg { return msg }
func SetOutput(v Msg) {
if v == nil {
msg = new(DefaultMsg)
} else {
msg = v
}
}
func wrapErrSuffix(err error, a ...any) error {
if err == nil {
return nil
}
return msg.WrapErr(err, append(a, err)...)
}
func wrapErrSelf(err error) error {
if err == nil {
return nil
}
return msg.WrapErr(err, err.Error())
}

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