Compare commits

..

56 Commits

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-26 02:49:37 +09:00
89 changed files with 3368 additions and 2486 deletions

View File

@ -22,6 +22,57 @@ jobs:
path: result/* path: result/*
retention-days: 1 retention-days: 1
race:
name: Fortify (race detector)
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run NixOS test
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.race
- name: Upload test output
uses: actions/upload-artifact@v3
with:
name: "fortify-race-vm-output"
path: result/*
retention-days: 1
sandbox:
name: Sandbox
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run NixOS test
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.sandbox
- name: Upload test output
uses: actions/upload-artifact@v3
with:
name: "sandbox-vm-output"
path: result/*
retention-days: 1
sandbox-race:
name: Sandbox (race detector)
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run NixOS test
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.sandbox-race
- name: Upload test output
uses: actions/upload-artifact@v3
with:
name: "sandbox-race-vm-output"
path: result/*
retention-days: 1
fpkg: fpkg:
name: Fpkg name: Fpkg
runs-on: nix runs-on: nix
@ -39,29 +90,14 @@ jobs:
path: result/* path: result/*
retention-days: 1 retention-days: 1
race:
name: Data race detector
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run NixOS test
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.race
- name: Upload test output
uses: actions/upload-artifact@v3
with:
name: "fortify-race-vm-output"
path: result/*
retention-days: 1
check: check:
name: Flake checks name: Flake checks
needs: needs:
- fortify - fortify
- fpkg
- race - race
- sandbox
- sandbox-race
- fpkg
runs-on: nix runs-on: nix
steps: steps:
- name: Checkout - name: Checkout

View File

@ -19,7 +19,7 @@ type appInfo struct {
// passed through to [fst.Config] // passed through to [fst.Config]
ID string `json:"id"` ID string `json:"id"`
// passed through to [fst.Config] // passed through to [fst.Config]
AppID int `json:"app_id"` Identity int `json:"identity"`
// passed through to [fst.Config] // passed through to [fst.Config]
Groups []string `json:"groups,omitempty"` Groups []string `json:"groups,omitempty"`
// passed through to [fst.Config] // passed through to [fst.Config]
@ -29,7 +29,7 @@ type appInfo struct {
// passed through to [fst.Config] // passed through to [fst.Config]
Net bool `json:"net,omitempty"` Net bool `json:"net,omitempty"`
// passed through to [fst.Config] // passed through to [fst.Config]
Dev bool `json:"dev,omitempty"` Device bool `json:"dev,omitempty"`
// passed through to [fst.Config] // passed through to [fst.Config]
Tty bool `json:"tty,omitempty"` Tty bool `json:"tty,omitempty"`
// passed through to [fst.Config] // passed through to [fst.Config]
@ -64,56 +64,61 @@ type appInfo struct {
func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool) *fst.Config { func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool) *fst.Config {
config := &fst.Config{ config := &fst.Config{
ID: app.ID, ID: app.ID,
Path: argv[0], Path: argv[0],
Args: argv, Args: argv,
Confinement: fst.ConfinementConfig{
AppID: app.AppID, Enablements: app.Enablements,
Groups: app.Groups,
Username: "fortify", SystemBus: app.SystemBus,
Inner: path.Join("/data/data", app.ID), SessionBus: app.SessionBus,
Outer: pathSet.homeDir, DirectWayland: app.DirectWayland,
Sandbox: &fst.SandboxConfig{
Hostname: formatHostname(app.Name), Username: "fortify",
Devel: app.Devel, Shell: shellPath,
Userns: app.Userns, Data: pathSet.homeDir,
Net: app.Net, Dir: path.Join("/data/data", app.ID),
Dev: app.Dev,
Tty: app.Tty || flagDropShell, Identity: app.Identity,
MapRealUID: app.MapRealUID, Groups: app.Groups,
DirectWayland: app.DirectWayland,
Filesystem: []*fst.FilesystemConfig{ Container: &fst.ContainerConfig{
{Src: path.Join(pathSet.nixPath, "store"), Dst: "/nix/store", Must: true}, Hostname: formatHostname(app.Name),
{Src: pathSet.metaPath, Dst: path.Join(fst.Tmp, "app"), Must: true}, Devel: app.Devel,
{Src: "/etc/resolv.conf"}, Userns: app.Userns,
{Src: "/sys/block"}, Net: app.Net,
{Src: "/sys/bus"}, Device: app.Device,
{Src: "/sys/class"}, Tty: app.Tty || flagDropShell,
{Src: "/sys/dev"}, MapRealUID: app.MapRealUID,
{Src: "/sys/devices"}, Filesystem: []*fst.FilesystemConfig{
}, {Src: path.Join(pathSet.nixPath, "store"), Dst: "/nix/store", Must: true},
Link: [][2]string{ {Src: pathSet.metaPath, Dst: path.Join(fst.Tmp, "app"), Must: true},
{app.CurrentSystem, "/run/current-system"}, {Src: "/etc/resolv.conf"},
{"/run/current-system/sw/bin", "/bin"}, {Src: "/sys/block"},
{"/run/current-system/sw/bin", "/usr/bin"}, {Src: "/sys/bus"},
}, {Src: "/sys/class"},
Etc: path.Join(pathSet.cacheDir, "etc"), {Src: "/sys/dev"},
AutoEtc: true, {Src: "/sys/devices"},
}, },
ExtraPerms: []*fst.ExtraPermConfig{ Link: [][2]string{
{Path: dataHome, Execute: true}, {app.CurrentSystem, "/run/current-system"},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true}, {"/run/current-system/sw/bin", "/bin"},
{"/run/current-system/sw/bin", "/usr/bin"},
}, },
SystemBus: app.SystemBus, Etc: path.Join(pathSet.cacheDir, "etc"),
SessionBus: app.SessionBus, AutoEtc: true,
Enablements: app.Enablements, },
ExtraPerms: []*fst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
}, },
} }
if app.Multiarch { if app.Multiarch {
config.Confinement.Sandbox.Seccomp |= seccomp.FlagMultiarch config.Container.Seccomp |= seccomp.FilterMultiarch
} }
if app.Bluetooth { if app.Bluetooth {
config.Confinement.Sandbox.Seccomp |= seccomp.FlagBluetooth config.Container.Seccomp |= seccomp.FilterBluetooth
} }
return config return config
} }

View File

@ -31,7 +31,7 @@
'', '',
id ? name, id ? name,
app_id ? throw "app_id is required", identity ? throw "identity is required",
groups ? [ ], groups ? [ ],
userns ? false, userns ? false,
net ? true, net ? true,
@ -147,7 +147,7 @@ let
name name
version version
id id
app_id identity
launcher launcher
groups groups
userns userns

View File

@ -13,7 +13,7 @@ import (
"git.gensokyo.uk/security/fortify/command" "git.gensokyo.uk/security/fortify/command"
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/app" "git.gensokyo.uk/security/fortify/internal/app/instance"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/sys" "git.gensokyo.uk/security/fortify/internal/sys"
"git.gensokyo.uk/security/fortify/sandbox" "git.gensokyo.uk/security/fortify/sandbox"
@ -62,7 +62,7 @@ func main() {
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console"). Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next fortify action") 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 }) c.Command("shim", command.UsageInternal, func([]string) error { instance.ShimMain(); return errSuccess })
{ {
var ( var (
@ -157,11 +157,11 @@ func main() {
return errSuccess return errSuccess
} }
// AppID determines uid // identity determines uid
if a.AppID != bundle.AppID { if a.Identity != bundle.Identity {
cleanup() cleanup()
log.Printf("package %q app id %d differs from installed %d", log.Printf("package %q identity %d differs from installed %d",
pkgPath, bundle.AppID, a.AppID) pkgPath, bundle.Identity, a.Identity)
return syscall.EBADE return syscall.EBADE
} }
@ -292,7 +292,7 @@ func main() {
"--override-input nixpkgs path:/etc/nixpkgs " + "--override-input nixpkgs path:/etc/nixpkgs " +
"path:" + a.NixGL + "#nixVulkanNvidia", "path:" + a.NixGL + "#nixVulkanNvidia",
}, true, func(config *fst.Config) *fst.Config { }, true, func(config *fst.Config) *fst.Config {
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, []*fst.FilesystemConfig{ config.Container.Filesystem = append(config.Container.Filesystem, []*fst.FilesystemConfig{
{Src: "/etc/resolv.conf"}, {Src: "/etc/resolv.conf"},
{Src: "/sys/block"}, {Src: "/sys/block"},
{Src: "/sys/bus"}, {Src: "/sys/bus"},
@ -324,7 +324,7 @@ func main() {
*/ */
if a.GPU { if a.GPU {
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, config.Container.Filesystem = append(config.Container.Filesystem,
&fst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(fst.Tmp, "nixGL")}) &fst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(fst.Tmp, "nixGL")})
appendGPUFilesystem(config) appendGPUFilesystem(config)
} }

View File

@ -72,7 +72,7 @@ func pathSetByApp(id string) *appPathSet {
} }
func appendGPUFilesystem(config *fst.Config) { func appendGPUFilesystem(config *fst.Config) {
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, []*fst.FilesystemConfig{ config.Container.Filesystem = append(config.Container.Filesystem, []*fst.FilesystemConfig{
// flatpak commit 763a686d874dd668f0236f911de00b80766ffe79 // flatpak commit 763a686d874dd668f0236f911de00b80766ffe79
{Src: "/dev/dri", Device: true}, {Src: "/dev/dri", Device: true},
// mali // mali

View File

@ -6,23 +6,24 @@ import (
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/app" "git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/app/instance"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
) )
func mustRunApp(ctx context.Context, config *fst.Config, beforeFail func()) { func mustRunApp(ctx context.Context, config *fst.Config, beforeFail func()) {
rs := new(fst.RunState) rs := new(app.RunState)
a := app.MustNew(ctx, std) a := instance.MustNew(instance.ISetuid, ctx, std)
var code int
if sa, err := a.Seal(config); err != nil { if sa, err := a.Seal(config); err != nil {
fmsg.PrintBaseError(err, "cannot seal app:") fmsg.PrintBaseError(err, "cannot seal app:")
rs.ExitCode = 1 code = 1
} else { } else {
// this updates ExitCode code = instance.PrintRunStateErr(instance.ISetuid, rs, sa.Run(rs))
app.PrintRunStateErr(rs, sa.Run(rs))
} }
if rs.ExitCode != 0 { if code != 0 {
beforeFail() beforeFail()
os.Exit(rs.ExitCode) os.Exit(code)
} }
} }

View File

@ -10,7 +10,7 @@ buildPackage {
name = "foot"; name = "foot";
inherit (foot) version; inherit (foot) version;
app_id = 2; identity = 2;
id = "org.codeberg.dnkl.foot"; id = "org.codeberg.dnkl.foot";
modules = [ modules = [

View File

@ -65,8 +65,8 @@ def check_state(name, enablements):
if len(config['args']) != 1 or not (config['args'][0].startswith("/nix/store/")) or f"fortify-{name}-" not in (config['args'][0]): 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']}") raise Exception(f"unexpected args {instance['config']['args']}")
if config['confinement']['enablements'] != enablements: if config['enablements'] != enablements:
raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}") raise Exception(f"unexpected enablements {instance['config']['enablements']}")
start_all() start_all()

View File

@ -16,7 +16,8 @@ func withNixDaemon(
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(), app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
) { ) {
mustRunAppDropShell(ctx, updateConfig(&fst.Config{ mustRunAppDropShell(ctx, updateConfig(&fst.Config{
ID: app.ID, ID: app.ID,
Path: shellPath, Path: shellPath,
Args: []string{shellPath, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " + Args: []string{shellPath, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
// start nix-daemon // start nix-daemon
@ -29,32 +30,34 @@ func withNixDaemon(
// terminate nix-daemon // terminate nix-daemon
" && pkill nix-daemon", " && pkill nix-daemon",
}, },
Confinement: fst.ConfinementConfig{
AppID: app.AppID, Username: "fortify",
Username: "fortify", Shell: shellPath,
Inner: path.Join("/data/data", app.ID), Data: pathSet.homeDir,
Outer: pathSet.homeDir, Dir: path.Join("/data/data", app.ID),
Sandbox: &fst.SandboxConfig{ ExtraPerms: []*fst.ExtraPermConfig{
Hostname: formatHostname(app.Name) + "-" + action, {Path: dataHome, Execute: true},
Userns: true, // nix sandbox requires userns {Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
Net: net, },
Seccomp: seccomp.FlagMultiarch,
Tty: dropShell, Identity: app.Identity,
Filesystem: []*fst.FilesystemConfig{
{Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true}, Container: &fst.ContainerConfig{
}, Hostname: formatHostname(app.Name) + "-" + action,
Link: [][2]string{ Userns: true, // nix sandbox requires userns
{app.CurrentSystem, "/run/current-system"}, Net: net,
{"/run/current-system/sw/bin", "/bin"}, Seccomp: seccomp.FilterMultiarch,
{"/run/current-system/sw/bin", "/usr/bin"}, Tty: dropShell,
}, Filesystem: []*fst.FilesystemConfig{
Etc: path.Join(pathSet.cacheDir, "etc"), {Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true},
AutoEtc: true,
}, },
ExtraPerms: []*fst.ExtraPermConfig{ Link: [][2]string{
{Path: dataHome, Execute: true}, {app.CurrentSystem, "/run/current-system"},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true}, {"/run/current-system/sw/bin", "/bin"},
{"/run/current-system/sw/bin", "/usr/bin"},
}, },
Etc: path.Join(pathSet.cacheDir, "etc"),
AutoEtc: true,
}, },
}), dropShell, beforeFail) }), dropShell, beforeFail)
} }
@ -64,35 +67,38 @@ func withCacheDir(
action string, command []string, workDir string, action string, command []string, workDir string,
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) { app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
mustRunAppDropShell(ctx, &fst.Config{ mustRunAppDropShell(ctx, &fst.Config{
ID: app.ID, ID: app.ID,
Path: shellPath, Path: shellPath,
Args: []string{shellPath, "-lc", strings.Join(command, " && ")}, Args: []string{shellPath, "-lc", strings.Join(command, " && ")},
Confinement: fst.ConfinementConfig{
AppID: app.AppID, Username: "nixos",
Username: "nixos", Shell: shellPath,
Inner: path.Join("/data/data", app.ID, "cache"), Data: pathSet.cacheDir, // this also ensures cacheDir via shim
Outer: pathSet.cacheDir, // this also ensures cacheDir via shim Dir: path.Join("/data/data", app.ID, "cache"),
Sandbox: &fst.SandboxConfig{ ExtraPerms: []*fst.ExtraPermConfig{
Hostname: formatHostname(app.Name) + "-" + action, {Path: dataHome, Execute: true},
Seccomp: seccomp.FlagMultiarch, {Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
Tty: dropShell, {Path: workDir, Execute: true},
Filesystem: []*fst.FilesystemConfig{ },
{Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true},
{Src: workDir, Dst: path.Join(fst.Tmp, "bundle"), Must: true}, Identity: app.Identity,
},
Link: [][2]string{ Container: &fst.ContainerConfig{
{app.CurrentSystem, "/run/current-system"}, Hostname: formatHostname(app.Name) + "-" + action,
{"/run/current-system/sw/bin", "/bin"}, Seccomp: seccomp.FilterMultiarch,
{"/run/current-system/sw/bin", "/usr/bin"}, Tty: dropShell,
}, Filesystem: []*fst.FilesystemConfig{
Etc: path.Join(workDir, "etc"), {Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true},
AutoEtc: true, {Src: workDir, Dst: path.Join(fst.Tmp, "bundle"), Must: true},
}, },
ExtraPerms: []*fst.ExtraPermConfig{ Link: [][2]string{
{Path: dataHome, Execute: true}, {app.CurrentSystem, "/run/current-system"},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true}, {"/run/current-system/sw/bin", "/bin"},
{Path: workDir, Execute: true}, {"/run/current-system/sw/bin", "/usr/bin"},
}, },
Etc: path.Join(workDir, "etc"),
AutoEtc: true,
}, },
}, dropShell, beforeFail) }, dropShell, beforeFail)
} }

View File

@ -8,6 +8,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
"syscall"
"testing" "testing"
"time" "time"
@ -71,7 +72,7 @@ func TestProxy_Seal(t *testing.T) {
for id, tc := range testCasePairs() { for id, tc := range testCasePairs() {
t.Run("create seal for "+id, func(t *testing.T) { t.Run("create seal for "+id, func(t *testing.T) {
p := dbus.New(tc[0].bus, tc[1].bus) 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", t.Errorf("Seal(%p, %p) error = %v, wantErr %v",
tc[0].c, tc[1].c, tc[0].c, tc[1].c,
err, tc[0].wantErr) err, tc[0].wantErr)

View File

@ -63,7 +63,7 @@ func (p *Proxy) Start(ctx context.Context, output io.Writer, useSandbox bool) er
c, toolPath, c, toolPath,
p.seal, true, p.seal, true,
argF, func(container *sandbox.Container) { argF, func(container *sandbox.Container) {
container.Seccomp |= seccomp.FlagMultiarch container.Seccomp |= seccomp.FilterMultiarch
container.Hostname = "fortify-dbus" container.Hostname = "fortify-dbus"
container.CommandContext = p.CommandContext container.CommandContext = p.CommandContext
if output != nil { if output != nil {

12
flake.lock generated
View File

@ -7,11 +7,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1742234739, "lastModified": 1742655702,
"narHash": "sha256-zFL6zsf/5OztR1NSNQF33dvS1fL/BzVUjabZq4qrtY4=", "narHash": "sha256-jbqlw4sPArFtNtA1s3kLg7/A4fzP4GLk9bGbtUJg0JQ=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "f6af7280a3390e65c2ad8fd059cdc303426cbd59", "rev": "0948aeedc296f964140d9429223c7e4a0702a1ff",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -23,11 +23,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1742512142, "lastModified": 1743231893,
"narHash": "sha256-8XfURTDxOm6+33swQJu/hx6xw1Tznl8vJJN5HwVqckg=", "narHash": "sha256-tpJsHMUPEhEnzySoQxx7+kA+KUtgWqvlcUBqROYNNt0=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "7105ae3957700a9646cc4b766f5815b23ed0c682", "rev": "c570c1f5304493cafe133b8d843c7c1c4a10d3a6",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -58,12 +58,19 @@
in in
{ {
fortify = callPackage ./test { inherit system self; }; fortify = callPackage ./test { inherit system self; };
fpkg = callPackage ./cmd/fpkg/test { inherit system self; };
race = callPackage ./test { race = callPackage ./test {
inherit system self; inherit system self;
withRace = true; 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 ]; } '' formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } ''
cd ${./.} cd ${./.}

View File

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

View File

@ -1,14 +1,14 @@
// Package fst exports shared fortify types.
package fst package fst
import ( import (
"git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
"git.gensokyo.uk/security/fortify/system" "git.gensokyo.uk/security/fortify/system"
) )
const Tmp = "/.fortify" const Tmp = "/.fortify"
// Config is used to seal an app // Config is used to seal an app implementation.
type Config struct { type Config struct {
// reverse-DNS style arbitrary identifier string from config; // reverse-DNS style arbitrary identifier string from config;
// passed to wayland security-context-v1 as application ID // passed to wayland security-context-v1 as application ID
@ -20,37 +20,40 @@ type Config struct {
// final args passed to container init // final args passed to container init
Args []string `json:"args"` Args []string `json:"args"`
Confinement ConfinementConfig `json:"confinement"` // system services to make available in the container
} Enablements system.Enablement `json:"enablements"`
// session D-Bus proxy configuration;
// nil makes session bus proxy assume built-in defaults
SessionBus *dbus.Config `json:"session_bus,omitempty"`
// system D-Bus proxy configuration;
// nil disables system bus proxy
SystemBus *dbus.Config `json:"system_bus,omitempty"`
// direct access to wayland socket; when this gets set no attempt is made to attach security-context-v1
// and the bare socket is mounted to the sandbox
DirectWayland bool `json:"direct_wayland,omitempty"`
// ConfinementConfig defines fortified child's confinement
type ConfinementConfig struct {
// numerical application id, determines uid in the init namespace
AppID int `json:"app_id"`
// list of supplementary groups to inherit
Groups []string `json:"groups"`
// passwd username in container, defaults to passwd name of target uid or chronos // passwd username in container, defaults to passwd name of target uid or chronos
Username string `json:"username,omitempty"` Username string `json:"username,omitempty"`
// home directory in container, empty for outer // absolute path to shell, empty for host shell
Inner string `json:"home_inner"` Shell string `json:"shell,omitempty"`
// home directory in init namespace // absolute path to home directory in the init mount namespace
Outer string `json:"home"` Data string `json:"data"`
// abstract sandbox configuration // directory to enter and use as home in the container mount namespace, empty for Data
Sandbox *SandboxConfig `json:"sandbox"` Dir string `json:"dir"`
// extra acl ops, runs after everything else // extra acl ops, dispatches before container init
ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"` ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
// reference to a system D-Bus proxy configuration, // numerical application id, used for init user namespace credentials
// nil value disables system bus proxy Identity int `json:"identity"`
SystemBus *dbus.Config `json:"system_bus,omitempty"` // list of supplementary groups inherited by container processes
// reference to a session D-Bus proxy configuration, Groups []string `json:"groups"`
// nil value makes session bus proxy assume built-in defaults
SessionBus *dbus.Config `json:"session_bus,omitempty"`
// system resources to expose to the container // abstract container configuration baseline
Enablements system.Enablement `json:"enablements"` Container *ContainerConfig `json:"container"`
} }
// ExtraPermConfig describes an acl update op.
type ExtraPermConfig struct { type ExtraPermConfig struct {
Ensure bool `json:"ensure,omitempty"` Ensure bool `json:"ensure,omitempty"`
Path string `json:"path"` Path string `json:"path"`
@ -78,82 +81,3 @@ func (e *ExtraPermConfig) String() string {
} }
return string(buf) return string(buf)
} }
// Template returns a fully populated instance of Config.
func Template() *Config {
return &Config{
ID: "org.chromium.Chromium",
Path: "/run/current-system/sw/bin/chromium",
Args: []string{
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland",
},
Confinement: ConfinementConfig{
AppID: 9,
Groups: []string{"video"},
Username: "chronos",
Outer: "/var/lib/persist/home/org.chromium.Chromium",
Inner: "/var/lib/fortify",
Sandbox: &SandboxConfig{
Hostname: "localhost",
Devel: true,
Userns: true,
Net: true,
Dev: true,
Seccomp: seccomp.FlagMultiarch,
Tty: true,
Multiarch: true,
MapRealUID: true,
DirectWayland: false,
// example API credentials pulled from Google Chrome
// DO NOT USE THESE IN A REAL BROWSER
Env: map[string]string{
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT",
},
Filesystem: []*FilesystemConfig{
{Src: "/nix/store"},
{Src: "/run/current-system"},
{Src: "/run/opengl-driver"},
{Src: "/var/db/nix-channels"},
{Src: "/var/lib/fortify/u0/org.chromium.Chromium",
Dst: "/data/data/org.chromium.Chromium", Write: true, Must: true},
{Src: "/dev/dri", Device: true},
},
Link: [][2]string{{"/run/user/65534", "/run/user/150"}},
Etc: "/etc",
AutoEtc: true,
Cover: []string{"/var/run/nscd"},
},
ExtraPerms: []*ExtraPermConfig{
{Path: "/var/lib/fortify/u0", Ensure: true, Execute: true},
{Path: "/var/lib/fortify/u0/org.chromium.Chromium", Read: true, Write: true, Execute: true},
},
SystemBus: &dbus.Config{
See: nil,
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
Own: nil,
Call: nil,
Broadcast: nil,
Log: false,
Filter: true,
},
SessionBus: &dbus.Config{
See: nil,
Talk: []string{"org.freedesktop.Notifications", "org.freedesktop.FileManager1", "org.freedesktop.ScreenSaver",
"org.freedesktop.secrets", "org.kde.kwalletd5", "org.kde.kwalletd6", "org.gnome.SessionManager"},
Own: []string{"org.chromium.Chromium.*", "org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"},
Call: map[string]string{"org.freedesktop.portal.*": "*"},
Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"},
Log: false,
Filter: true,
},
Enablements: system.EWayland | system.EDBus | system.EPulse,
},
}
}

59
fst/container.go Normal file
View File

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

View File

@ -1,282 +0,0 @@
package fst
import (
"errors"
"fmt"
"io/fs"
"maps"
"path"
"slices"
"syscall"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
)
// SandboxConfig describes resources made available to the sandbox.
type (
SandboxConfig struct {
// container hostname
Hostname string `json:"hostname,omitempty"`
// extra seccomp flags
Seccomp seccomp.SyscallOpts `json:"seccomp"`
// allow ptrace and friends
Devel bool `json:"devel,omitempty"`
// allow userns creation in container
Userns bool `json:"userns,omitempty"`
// share host net namespace
Net bool `json:"net,omitempty"`
// expose main process tty
Tty bool `json:"tty,omitempty"`
// allow multiarch
Multiarch bool `json:"multiarch,omitempty"`
// initial process environment variables
Env map[string]string `json:"env"`
// map target user uid to privileged user uid in the user namespace
MapRealUID bool `json:"map_real_uid"`
// expose all devices
Dev bool `json:"dev,omitempty"`
// container host filesystem bind mounts
Filesystem []*FilesystemConfig `json:"filesystem"`
// create symlinks inside container filesystem
Link [][2]string `json:"symlink"`
// direct access to wayland socket; when this gets set no attempt is made to attach security-context-v1
// and the bare socket is mounted to the sandbox
DirectWayland bool `json:"direct_wayland,omitempty"`
// read-only /etc directory
Etc string `json:"etc,omitempty"`
// automatically set up /etc symlinks
AutoEtc bool `json:"auto_etc"`
// cover these paths or create them if they do not already exist
Cover []string `json:"cover"`
}
// SandboxSys encapsulates system functions used during [sandbox.Container] initialisation.
SandboxSys interface {
Getuid() int
Getgid() int
Paths() Paths
ReadDir(name string) ([]fs.DirEntry, error)
EvalSymlinks(path string) (string, error)
Println(v ...any)
Printf(format string, v ...any)
}
// FilesystemConfig is a representation of [sandbox.BindMount].
FilesystemConfig struct {
// mount point in container, same as src if empty
Dst string `json:"dst,omitempty"`
// host filesystem path to make available to the container
Src string `json:"src"`
// do not mount filesystem read-only
Write bool `json:"write,omitempty"`
// do not disable device files
Device bool `json:"dev,omitempty"`
// fail if the bind mount cannot be established for any reason
Must bool `json:"require,omitempty"`
}
)
// ToContainer initialises [sandbox.Params] via [SandboxConfig].
// Note that remaining container setup must be queued by the [App] implementation.
func (s *SandboxConfig) ToContainer(sys SandboxSys, uid, gid *int) (*sandbox.Params, map[string]string, error) {
if s == nil {
return nil, nil, syscall.EBADE
}
container := &sandbox.Params{
Hostname: s.Hostname,
Ops: new(sandbox.Ops),
Seccomp: s.Seccomp,
}
/* this is only 4 KiB of memory on a 64-bit system,
permissive defaults on NixOS results in around 100 entries
so this capacity should eliminate copies for most setups */
*container.Ops = slices.Grow(*container.Ops, 1<<8)
if s.Devel {
container.Flags |= sandbox.FAllowDevel
}
if s.Userns {
container.Flags |= sandbox.FAllowUserns
}
if s.Net {
container.Flags |= sandbox.FAllowNet
}
if s.Tty {
container.Flags |= sandbox.FAllowTTY
}
if s.MapRealUID {
/* some programs fail to connect to dbus session running as a different uid
so this workaround is introduced to map priv-side caller uid in container */
container.Uid = sys.Getuid()
*uid = container.Uid
container.Gid = sys.Getgid()
*gid = container.Gid
} else {
*uid = sandbox.OverflowUid()
*gid = sandbox.OverflowGid()
}
container.
Proc("/proc").
Tmpfs(Tmp, 1<<12, 0755)
if !s.Dev {
container.Dev("/dev").Mqueue("/dev/mqueue")
} else {
container.Bind("/dev", "/dev", sandbox.BindDevice)
}
/* 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, nil, err
} else {
// there is usually only one, do not preallocate
for _, entry := range entries {
if entry.Method != "unix" {
continue
}
for _, pair := range entry.Values {
if pair[0] == "path" {
if path.IsAbs(pair[1]) {
// get parent dir of socket
dir := path.Dir(pair[1])
if dir == "." || dir == "/" {
sys.Printf("dbus socket %q is in an unusual location", pair[1])
}
hidePaths = append(hidePaths, dir)
} else {
sys.Printf("dbus socket %q is not absolute", pair[1])
}
}
}
}
}
hidePathMatch := make([]bool, len(hidePaths))
for i := range hidePaths {
if err := evalSymlinks(sys, &hidePaths[i]); err != nil {
return nil, nil, err
}
}
for _, c := range s.Filesystem {
if c == nil {
continue
}
if !path.IsAbs(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, nil, fmt.Errorf("dst path %q is not absolute", dest)
}
srcH := c.Src
if err := evalSymlinks(sys, &srcH); err != nil {
return nil, nil, err
}
for i := range hidePaths {
// skip matched entries
if hidePathMatch[i] {
continue
}
if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil {
return nil, nil, err
} else if ok {
hidePathMatch[i] = true
sys.Printf("hiding paths from %q", c.Src)
}
}
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)
}
// cover matched paths
for i, ok := range hidePathMatch {
if ok {
container.Tmpfs(hidePaths[i], 1<<13, 0755)
}
}
for _, l := range s.Link {
container.Link(l[0], l[1])
}
// 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)
}
} else {
etcPath := s.Etc
if etcPath == "" {
etcPath = "/etc"
}
container.Bind(etcPath, Tmp+"/etc", 0)
// 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 {
n := ent.Name()
switch n {
case "passwd":
case "group":
case "mtab":
container.Link("/proc/mounts", "/etc/"+n)
default:
container.Link(Tmp+"/etc/"+n, "/etc/"+n)
}
}
}
}
return container, maps.Clone(s.Env), nil
}
func evalSymlinks(sys SandboxSys, v *string) error {
if p, err := sys.EvalSymlinks(*v); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return err
}
sys.Printf("path %q does not yet exist", *v)
} else {
*v = p
}
return nil
}

91
fst/template.go Normal file
View File

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

140
fst/template_test.go Normal file
View File

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

View File

@ -1,38 +1,17 @@
package helper package helper
import ( import (
"errors" "bytes"
"io" "io"
"strings" "syscall"
) )
var ( type argsWt [][]byte
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
}
func (a argsWt) WriteTo(w io.Writer) (int64, error) { func (a argsWt) WriteTo(w io.Writer) (int64, error) {
// assuming already checked
nt := 0 nt := 0
// write null terminated arguments
for _, arg := range a { for _, arg := range a {
n, err := w.Write([]byte(arg + "\x00")) n, err := w.Write(arg)
nt += n nt += n
if err != nil { if err != nil {
@ -44,18 +23,32 @@ func (a argsWt) WriteTo(w io.Writer) (int64, error) {
} }
func (a argsWt) String() string { 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. // NewCheckedArgs returns a checked null-terminated argument writer for a copy of args.
// Callers must not retain any references to args. func NewCheckedArgs(args []string) (wt io.WriterTo, err error) {
func NewCheckedArgs(args []string) (io.WriterTo, error) { a := make(argsWt, len(args))
a := argsWt(args) for i, arg := range args {
return a, a.check() 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. // MustNewCheckedArgs returns a checked null-terminated argument writer for a copy of args.
// Callers must not retain any references to args. // If s contains a NUL byte this function panics instead of returning an error.
func MustNewCheckedArgs(args []string) io.WriterTo { func MustNewCheckedArgs(args []string) io.WriterTo {
a, err := NewCheckedArgs(args) a, err := NewCheckedArgs(args)
if err != nil { if err != nil {

View File

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

View File

@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"os"
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
@ -55,8 +56,8 @@ func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput f
t.Run("start helper with status channel and wait", func(t *testing.T) { t.Run("start helper with status channel and wait", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
stdout, stderr := new(strings.Builder), new(strings.Builder) stdout := new(strings.Builder)
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, stderr }, true) 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) { t.Run("wait not yet started helper", func(t *testing.T) {
defer func() { defer func() {
@ -88,8 +89,8 @@ func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput f
t.Log("waiting on helper") t.Log("waiting on helper")
if err := h.Wait(); !errors.Is(err, context.Canceled) { if err := h.Wait(); !errors.Is(err, context.Canceled) {
t.Errorf("Wait() err = %v stderr = %s", t.Errorf("Wait: error = %v",
err, stderr) err)
} }
t.Run("wait already finalised helper", func(t *testing.T) { t.Run("wait already finalised helper", func(t *testing.T) {
@ -101,8 +102,8 @@ func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput f
} }
}) })
if got := stderr.String(); got != wantPayload { if got := trimStdout(stdout); got != wantPayload {
t.Errorf("Start: stderr = %v, want %v", t.Errorf("Start: stdout = %q, want %q",
got, wantPayload) got, wantPayload)
} }
}) })
@ -110,23 +111,27 @@ func testHelper(t *testing.T, createHelper func(ctx context.Context, setOutput f
t.Run("start helper and wait", func(t *testing.T) { t.Run("start helper and wait", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
stdout, stderr := new(strings.Builder), new(strings.Builder) stdout := new(strings.Builder)
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, stderr }, false) h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, os.Stderr }, false)
if err := h.Start(); err != nil { if err := h.Start(); err != nil {
t.Errorf("Start() error = %v", t.Errorf("Start: error = %v",
err) err)
return return
} }
if err := h.Wait(); err != nil { if err := h.Wait(); err != nil {
t.Errorf("Wait() err = %v stdout = %s stderr = %s", t.Errorf("Wait: error = %v stdout = %q",
err, stdout, stderr) err, stdout)
} }
if got := stderr.String(); got != wantPayload { if got := trimStdout(stdout); got != wantPayload {
t.Errorf("Start() stderr = %v, want %v", t.Errorf("Start: stdout = %q, want %q",
got, wantPayload) got, wantPayload)
} }
}) })
} }
func trimStdout(stdout fmt.Stringer) string {
return strings.TrimPrefix(stdout.String(), "=== RUN TestHelperInit\n")
}

View File

@ -63,7 +63,7 @@ func flagRestoreFiles(offset int, ap, sp string) (argsFile, statFile *os.File) {
func genericStub(argsFile, statFile *os.File) { func genericStub(argsFile, statFile *os.File) {
if argsFile != nil { if argsFile != nil {
// this output is checked by parent // this output is checked by parent
if _, err := io.Copy(os.Stderr, argsFile); err != nil { if _, err := io.Copy(os.Stdout, argsFile); err != nil {
panic("cannot read args: " + err.Error()) panic("cannot read args: " + err.Error())
} }
} }

View File

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

View File

@ -1,219 +0,0 @@
package app_test
import (
"git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system"
)
var testCasesNixos = []sealTestCase{
{
"nixos chromium direct wayland", new(stubNixOS),
&fst.Config{
ID: "org.chromium.Chromium",
Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
Confinement: fst.ConfinementConfig{
AppID: 1, Groups: []string{}, Username: "u0_a1",
Outer: "/var/lib/persist/module/fortify/0/1",
Sandbox: &fst.SandboxConfig{
Userns: true, Net: true, MapRealUID: true, DirectWayland: true, Env: nil, 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},
},
Cover: []string{"/var/run/nscd"},
},
SystemBus: &dbus.Config{
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
Filter: true,
},
SessionBus: &dbus.Config{
Talk: []string{
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
"org.kde.kwalletd5", "org.kde.kwalletd6",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{}, Broadcast: map[string]string{},
Filter: true,
},
Enablements: system.EWayland | system.EDBus | system.EPulse,
},
},
fst.ID{
0x8e, 0x2c, 0x76, 0xb0,
0x66, 0xda, 0xbe, 0x57,
0x4c, 0xf0, 0x73, 0xbd,
0xb4, 0x6e, 0xb5, 0xc1,
},
system.New(1000001).
Ensure("/tmp/fortify.1971", 0711).
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1", 0711).
Ephemeral(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/1", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/1", acl.Read, acl.Write, acl.Execute).
UpdatePermType(system.EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute).
Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse").
CopyFile(nil, "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
MustProxyDBus("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", &dbus.Config{
Talk: []string{
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
"org.kde.kwalletd5", "org.kde.kwalletd6",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{}, Broadcast: map[string]string{},
Filter: true,
}, "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower",
},
Filter: true,
}).
UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write).
UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write),
&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",
"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, 0755).
Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", sandbox.BindWritable).
Bind("/var/lib/persist/module/fortify/0/1", "/var/lib/persist/module/fortify/0/1", sandbox.BindWritable).
Place("/etc/passwd", []byte("u0_a1:x:1971:100:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n")).
Place("/etc/group", []byte("fortify:x:100:\n")).
Bind("/run/user/1971/wayland-0", "/run/user/1971/wayland-0", 0).
Bind("/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native", 0).
Place(fst.Tmp+"/pulse-cookie", nil).
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus", 0).
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket", 0).
Tmpfs("/var/run/nscd", 8192, 0755),
},
},
}

View File

@ -1,382 +0,0 @@
package app_test
import (
"os"
"git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system"
)
var testCasesPd = []sealTestCase{
{
"nixos permissive defaults no enablements", new(stubNixOS),
&fst.Config{
Confinement: fst.ConfinementConfig{
AppID: 0,
Username: "chronos",
Outer: "/home/chronos",
},
},
fst.ID{
0x4a, 0x45, 0x0b, 0x65,
0x96, 0xd7, 0xbc, 0x15,
0xbd, 0x01, 0x78, 0x0e,
0xb9, 0xa6, 0x07, 0xac,
},
system.New(1000000).
Ensure("/tmp/fortify.1971", 0711).
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac", 0711).
Ephemeral(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute),
&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",
"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, 0755).
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",
Args: []string{"zsh", "-c", "exec chromium "},
Confinement: fst.ConfinementConfig{
AppID: 9,
Groups: []string{"video"},
Username: "chronos",
Outer: "/home/chronos",
SessionBus: &dbus.Config{
Talk: []string{
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{
"org.freedesktop.portal.*": "*",
},
Broadcast: map[string]string{
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
},
Filter: true,
},
SystemBus: &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower",
},
Filter: true,
},
Enablements: system.EWayland | system.EDBus | system.EPulse,
},
},
fst.ID{
0xeb, 0xf0, 0x83, 0xd1,
0xb1, 0x75, 0x91, 0x17,
0x82, 0xd4, 0x13, 0x36,
0x9b, 0x64, 0xce, 0x7c,
},
system.New(1000009).
Ensure("/tmp/fortify.1971", 0711).
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c", 0711).
Ephemeral(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/9", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/9", acl.Read, acl.Write, acl.Execute).
Ensure("/tmp/fortify.1971/wayland", 0711).
Wayland(new(*os.File), "/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/1971/wayland-0", "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse").
CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
MustProxyDBus("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", &dbus.Config{
Talk: []string{
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{
"org.freedesktop.portal.*": "*",
},
Broadcast: map[string]string{
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
},
Filter: true,
}, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower",
},
Filter: true,
}).
UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write).
UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write),
&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",
"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, 0755).
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/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/65534/wayland-0", 0).
Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native", 0).
Place(fst.Tmp+"/pulse-cookie", nil).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus", 0).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket", 0).
Tmpfs("/var/run/nscd", 8192, 0755),
},
},
}

View File

@ -1,4 +1,4 @@
package fst package app
import ( import (
"crypto/rand" "crypto/rand"

View File

@ -1,22 +1,22 @@
package fst_test package app_test
import ( import (
"errors" "errors"
"testing" "testing"
"git.gensokyo.uk/security/fortify/fst" . "git.gensokyo.uk/security/fortify/internal/app"
) )
func TestParseAppID(t *testing.T) { func TestParseAppID(t *testing.T) {
t.Run("bad length", func(t *testing.T) { t.Run("bad length", func(t *testing.T) {
if err := fst.ParseAppID(new(fst.ID), "meow"); !errors.Is(err, fst.ErrInvalidLength) { if err := ParseAppID(new(ID), "meow"); !errors.Is(err, ErrInvalidLength) {
t.Errorf("ParseAppID: error = %v, wantErr = %v", err, fst.ErrInvalidLength) t.Errorf("ParseAppID: error = %v, wantErr = %v", err, ErrInvalidLength)
} }
}) })
t.Run("bad byte", func(t *testing.T) { t.Run("bad byte", func(t *testing.T) {
wantErr := "invalid char '\\n' at byte 15" wantErr := "invalid char '\\n' at byte 15"
if err := fst.ParseAppID(new(fst.ID), "02bc7f8936b2af6\n\ne2535cd71ef0bb7"); err == nil || err.Error() != wantErr { if err := ParseAppID(new(ID), "02bc7f8936b2af6\n\ne2535cd71ef0bb7"); err == nil || err.Error() != wantErr {
t.Errorf("ParseAppID: error = %v, wantErr = %v", err, wantErr) t.Errorf("ParseAppID: error = %v, wantErr = %v", err, wantErr)
} }
}) })
@ -30,30 +30,30 @@ func TestParseAppID(t *testing.T) {
func FuzzParseAppID(f *testing.F) { func FuzzParseAppID(f *testing.F) {
for i := 0; i < 16; i++ { for i := 0; i < 16; i++ {
id := new(fst.ID) id := new(ID)
if err := fst.NewAppID(id); err != nil { if err := NewAppID(id); err != nil {
panic(err.Error()) panic(err.Error())
} }
f.Add(id[0], id[1], id[2], id[3], id[4], id[5], id[6], id[7], id[8], id[9], id[10], id[11], id[12], id[13], id[14], id[15]) f.Add(id[0], id[1], id[2], id[3], id[4], id[5], id[6], id[7], id[8], id[9], id[10], id[11], id[12], id[13], id[14], id[15])
} }
f.Fuzz(func(t *testing.T, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15 byte) { f.Fuzz(func(t *testing.T, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15 byte) {
testParseAppID(t, &fst.ID{b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15}) testParseAppID(t, &ID{b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15})
}) })
} }
func testParseAppIDWithRandom(t *testing.T) { func testParseAppIDWithRandom(t *testing.T) {
id := new(fst.ID) id := new(ID)
if err := fst.NewAppID(id); err != nil { if err := NewAppID(id); err != nil {
t.Fatalf("cannot generate app ID: %v", err) t.Fatalf("cannot generate app ID: %v", err)
} }
testParseAppID(t, id) testParseAppID(t, id)
} }
func testParseAppID(t *testing.T, id *fst.ID) { func testParseAppID(t *testing.T, id *ID) {
s := id.String() s := id.String()
got := new(fst.ID) got := new(ID)
if err := fst.ParseAppID(got, s); err != nil { if err := ParseAppID(got, s); err != nil {
t.Fatalf("cannot parse app ID: %v", err) t.Fatalf("cannot parse app ID: %v", err)
} }

View File

@ -0,0 +1,189 @@
package common
import (
"errors"
"fmt"
"io/fs"
"maps"
"path"
"syscall"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/sys"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
)
// in practice there should be less than 30 entries added by the runtime;
// allocating slightly more as a margin for future expansion
const preallocateOpsCount = 1 << 5
// NewContainer initialises [sandbox.Params] via [fst.ContainerConfig].
// Note that remaining container setup must be queued by the caller.
func NewContainer(s *fst.ContainerConfig, os sys.State, uid, gid *int) (*sandbox.Params, map[string]string, error) {
if s == nil {
return nil, nil, syscall.EBADE
}
container := &sandbox.Params{
Hostname: s.Hostname,
Seccomp: s.Seccomp,
}
{
ops := make(sandbox.Ops, 0, preallocateOpsCount+len(s.Filesystem)+len(s.Link)+len(s.Cover))
container.Ops = &ops
}
if s.Multiarch {
container.Seccomp |= seccomp.FilterMultiarch
}
if s.Devel {
container.Flags |= sandbox.FAllowDevel
}
if s.Userns {
container.Flags |= sandbox.FAllowUserns
}
if s.Net {
container.Flags |= sandbox.FAllowNet
}
if s.Tty {
container.Flags |= sandbox.FAllowTTY
}
if s.MapRealUID {
/* some programs fail to connect to dbus session running as a different uid
so this workaround is introduced to map priv-side caller uid in container */
container.Uid = os.Getuid()
*uid = container.Uid
container.Gid = os.Getgid()
*gid = container.Gid
} else {
*uid = sandbox.OverflowUid()
*gid = sandbox.OverflowGid()
}
container.
Proc("/proc").
Tmpfs(fst.Tmp, 1<<12, 0755)
if !s.Device {
container.Dev("/dev").Mqueue("/dev/mqueue")
} else {
container.Bind("/dev", "/dev", sandbox.BindWritable|sandbox.BindDevice)
}
/* retrieve paths and hide them if they're made available in the sandbox;
this feature tries to improve user experience of permissive defaults, and
to warn about issues in custom configuration; it is NOT a security feature
and should not be treated as such, ALWAYS be careful with what you bind */
var hidePaths []string
sc := os.Paths()
hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath)
_, systemBusAddr := dbus.Address()
if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
return nil, nil, err
} else {
// there is usually only one, do not preallocate
for _, entry := range entries {
if entry.Method != "unix" {
continue
}
for _, pair := range entry.Values {
if pair[0] == "path" {
if path.IsAbs(pair[1]) {
// get parent dir of socket
dir := path.Dir(pair[1])
if dir == "." || dir == "/" {
os.Printf("dbus socket %q is in an unusual location", pair[1])
}
hidePaths = append(hidePaths, dir)
} else {
os.Printf("dbus socket %q is not absolute", pair[1])
}
}
}
}
}
hidePathMatch := make([]bool, len(hidePaths))
for i := range hidePaths {
if err := evalSymlinks(os, &hidePaths[i]); err != nil {
return nil, nil, err
}
}
for _, c := range s.Filesystem {
if c == nil {
continue
}
if !path.IsAbs(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, nil, fmt.Errorf("dst path %q is not absolute", dest)
}
srcH := c.Src
if err := evalSymlinks(os, &srcH); err != nil {
return nil, nil, err
}
for i := range hidePaths {
// skip matched entries
if hidePathMatch[i] {
continue
}
if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil {
return nil, nil, err
} else if ok {
hidePathMatch[i] = true
os.Printf("hiding paths from %q", c.Src)
}
}
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)
}
// cover matched paths
for i, ok := range hidePathMatch {
if ok {
container.Tmpfs(hidePaths[i], 1<<13, 0755)
}
}
for _, l := range s.Link {
container.Link(l[0], l[1])
}
return container, maps.Clone(s.Env), nil
}
func evalSymlinks(os sys.State, v *string) error {
if p, err := os.EvalSymlinks(*v); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return err
}
os.Printf("path %q does not yet exist", *v)
} else {
*v = p
}
return nil
}

View File

@ -1,4 +1,4 @@
package fst package common
import ( import (
"path/filepath" "path/filepath"

View File

@ -1,4 +1,4 @@
package fst package common
import ( import (
"testing" "testing"

View File

@ -0,0 +1,17 @@
package instance
import (
"syscall"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/app/internal/setuid"
)
func PrintRunStateErr(whence int, rs *app.RunState, runErr error) (code int) {
switch whence {
case ISetuid:
return setuid.PrintRunStateErr(rs, runErr)
default:
panic(syscall.EINVAL)
}
}

View File

@ -0,0 +1,33 @@
// Package instance exposes cross-package implementation details and provides constructors for builtin implementations.
package instance
import (
"context"
"log"
"syscall"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/app/internal/setuid"
"git.gensokyo.uk/security/fortify/internal/sys"
)
const (
ISetuid = iota
)
func New(whence int, ctx context.Context, os sys.State) (app.App, error) {
switch whence {
case ISetuid:
return setuid.New(ctx, os)
default:
return nil, syscall.EINVAL
}
}
func MustNew(whence int, ctx context.Context, os sys.State) app.App {
a, err := New(whence, ctx, os)
if err != nil {
log.Fatalf("cannot create app: %v", err)
}
return a
}

View File

@ -0,0 +1,6 @@
package instance
import "git.gensokyo.uk/security/fortify/internal/app/internal/setuid"
// ShimMain is the main function of the shim process and runs as the unconstrained target user.
func ShimMain() { setuid.ShimMain() }

View File

@ -0,0 +1,74 @@
package setuid
import (
"context"
"fmt"
"sync"
"git.gensokyo.uk/security/fortify/fst"
. "git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/sys"
)
func New(ctx context.Context, os sys.State) (App, error) {
a := new(app)
a.sys = os
a.ctx = ctx
id := new(ID)
err := NewAppID(id)
a.id = newID(id)
return a, err
}
type app struct {
id *stringPair[ID]
sys sys.State
ctx context.Context
*outcome
mu sync.RWMutex
}
func (a *app) ID() ID { a.mu.RLock(); defer a.mu.RUnlock(); return a.id.unwrap() }
func (a *app) String() string {
if a == nil {
return "(invalid app)"
}
a.mu.RLock()
defer a.mu.RUnlock()
if a.outcome != nil {
if a.outcome.user.uid == nil {
return fmt.Sprintf("(sealed app %s with invalid uid)", a.id)
}
return fmt.Sprintf("(sealed app %s as uid %s)", a.id, a.outcome.user.uid)
}
return fmt.Sprintf("(unsealed app %s)", a.id)
}
func (a *app) Seal(config *fst.Config) (SealedApp, error) {
a.mu.Lock()
defer a.mu.Unlock()
if a.outcome != nil {
panic("app sealed twice")
}
if config == nil {
return nil, fmsg.WrapError(ErrConfig,
"attempted to seal app with nil config")
}
seal := new(outcome)
seal.id = a.id
err := seal.finalise(a.ctx, a.sys, config)
if err == nil {
a.outcome = seal
}
return seal, err
}

View File

@ -0,0 +1,145 @@
package setuid_test
import (
"git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system"
)
var testCasesNixos = []sealTestCase{
{
"nixos chromium direct wayland", new(stubNixOS),
&fst.Config{
ID: "org.chromium.Chromium",
Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
Enablements: system.EWayland | system.EDBus | system.EPulse,
Container: &fst.ContainerConfig{
Userns: true, Net: true, MapRealUID: 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},
},
Cover: []string{"/var/run/nscd"},
},
SystemBus: &dbus.Config{
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
Filter: true,
},
SessionBus: &dbus.Config{
Talk: []string{
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
"org.kde.kwalletd5", "org.kde.kwalletd6",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{}, Broadcast: map[string]string{},
Filter: true,
},
DirectWayland: true,
Username: "u0_a1",
Data: "/var/lib/persist/module/fortify/0/1",
Identity: 1, Groups: []string{},
},
app.ID{
0x8e, 0x2c, 0x76, 0xb0,
0x66, 0xda, 0xbe, 0x57,
0x4c, 0xf0, 0x73, 0xbd,
0xb4, 0x6e, 0xb5, 0xc1,
},
system.New(1000001).
Ensure("/tmp/fortify.1971", 0711).
Ensure("/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",
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
"org.kde.kwalletd5", "org.kde.kwalletd6",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{}, Broadcast: map[string]string{},
Filter: true,
}, "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower",
},
Filter: true,
}).
UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write).
UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write),
&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).
Etc("/etc", "8e2c76b066dabe574cf073bdb46eb5c1").
Tmpfs("/run/user", 4096, 0755).
Tmpfs("/run/user/1971", 8388608, 0700).
Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", sandbox.BindWritable).
Bind("/var/lib/persist/module/fortify/0/1", "/var/lib/persist/module/fortify/0/1", sandbox.BindWritable).
Place("/etc/passwd", []byte("u0_a1:x:1971:100:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n")).
Place("/etc/group", []byte("fortify:x:100:\n")).
Bind("/run/user/1971/wayland-0", "/run/user/1971/wayland-0", 0).
Bind("/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native", 0).
Place(fst.Tmp+"/pulse-cookie", nil).
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus", 0).
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket", 0).
Tmpfs("/var/run/nscd", 8192, 0755),
},
},
}

View File

@ -0,0 +1,216 @@
package setuid_test
import (
"os"
"git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system"
)
var testCasesPd = []sealTestCase{
{
"nixos permissive defaults no enablements", new(stubNixOS),
&fst.Config{Username: "chronos", Data: "/home/chronos"},
app.ID{
0x4a, 0x45, 0x0b, 0x65,
0x96, 0xd7, 0xbc, 0x15,
0xbd, 0x01, 0x78, 0x0e,
0xb9, 0xa6, 0x07, 0xac,
},
system.New(1000000).
Ensure("/tmp/fortify.1971", 0711).
Ensure("/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),
&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).
Etc("/etc", "4a450b6596d7bc15bd01780eb9a607ac").
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",
Args: []string{"zsh", "-c", "exec chromium "},
Identity: 9,
Groups: []string{"video"},
Username: "chronos",
Data: "/home/chronos",
SessionBus: &dbus.Config{
Talk: []string{
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{
"org.freedesktop.portal.*": "*",
},
Broadcast: map[string]string{
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
},
Filter: true,
},
SystemBus: &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower",
},
Filter: true,
},
Enablements: system.EWayland | system.EDBus | system.EPulse,
},
app.ID{
0xeb, 0xf0, 0x83, 0xd1,
0xb1, 0x75, 0x91, 0x17,
0x82, 0xd4, 0x13, 0x36,
0x9b, 0x64, 0xce, 0x7c,
},
system.New(1000009).
Ensure("/tmp/fortify.1971", 0711).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/9", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/9", acl.Read, acl.Write, acl.Execute).
Ephemeral(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c", 0711).
Wayland(new(*os.File), "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/1971/wayland-0", "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", acl.Execute).
Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse").
CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
MustProxyDBus("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", &dbus.Config{
Talk: []string{
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager",
},
Own: []string{
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*",
},
Call: map[string]string{
"org.freedesktop.portal.*": "*",
},
Broadcast: map[string]string{
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
},
Filter: true,
}, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", &dbus.Config{
Talk: []string{
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower",
},
Filter: true,
}).
UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write).
UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write),
&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).
Etc("/etc", "ebf083d1b175911782d413369b64ce7c").
Tmpfs("/run/user", 4096, 0755).
Tmpfs("/run/user/65534", 8388608, 0700).
Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", sandbox.BindWritable).
Bind("/home/chronos", "/home/chronos", sandbox.BindWritable).
Place("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
Place("/etc/group", []byte("fortify:x:65534:\n")).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/65534/wayland-0", 0).
Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native", 0).
Place(fst.Tmp+"/pulse-cookie", nil).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus", 0).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket", 0).
Tmpfs("/var/run/nscd", 8192, 0755),
},
},
}

View File

@ -1,4 +1,4 @@
package app_test package setuid_test
import ( import (
"fmt" "fmt"
@ -7,7 +7,7 @@ import (
"os/user" "os/user"
"strconv" "strconv"
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/internal/app"
) )
// fs methods are not implemented using a real FS // fs methods are not implemented using a real FS
@ -125,8 +125,8 @@ func (s *stubNixOS) Open(name string) (fs.File, error) {
} }
} }
func (s *stubNixOS) Paths() fst.Paths { func (s *stubNixOS) Paths() app.Paths {
return fst.Paths{ return app.Paths{
SharePath: "/tmp/fortify.1971", SharePath: "/tmp/fortify.1971",
RuntimePath: "/run/user/1971", RuntimePath: "/run/user/1971",
RunDirPath: "/run/user/1971/fortify", RunDirPath: "/run/user/1971/fortify",

View File

@ -1,4 +1,4 @@
package app_test package setuid_test
import ( import (
"encoding/json" "encoding/json"
@ -9,6 +9,7 @@ import (
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/app" "git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/app/internal/setuid"
"git.gensokyo.uk/security/fortify/internal/sys" "git.gensokyo.uk/security/fortify/internal/sys"
"git.gensokyo.uk/security/fortify/sandbox" "git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system" "git.gensokyo.uk/security/fortify/system"
@ -18,7 +19,7 @@ type sealTestCase struct {
name string name string
os sys.State os sys.State
config *fst.Config config *fst.Config
id fst.ID id app.ID
wantSys *system.I wantSys *system.I
wantContainer *sandbox.Params wantContainer *sandbox.Params
} }
@ -28,7 +29,7 @@ func TestApp(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
a := app.NewWithID(tc.id, tc.os) a := setuid.NewWithID(tc.id, tc.os)
var ( var (
gotSys *system.I gotSys *system.I
gotContainer *sandbox.Params gotContainer *sandbox.Params
@ -38,7 +39,7 @@ func TestApp(t *testing.T) {
t.Errorf("Seal: error = %v", err) t.Errorf("Seal: error = %v", err)
return return
} else { } else {
gotSys, gotContainer = app.AppIParams(a, sa) gotSys, gotContainer = setuid.AppIParams(a, sa)
} }
}) { }) {
return return

View File

@ -1,14 +1,16 @@
package app package setuid
import ( import (
"errors" "errors"
"log" "log"
"git.gensokyo.uk/security/fortify/fst" . "git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
) )
func PrintRunStateErr(rs *fst.RunState, runErr error) { func PrintRunStateErr(rs *RunState, runErr error) (code int) {
code = rs.ExitStatus()
if runErr != nil { if runErr != nil {
if rs.Time == nil { if rs.Time == nil {
fmsg.PrintBaseError(runErr, "cannot start app:") fmsg.PrintBaseError(runErr, "cannot start app:")
@ -49,8 +51,8 @@ func PrintRunStateErr(rs *fst.RunState, runErr error) {
} }
} }
if rs.ExitCode == 0 { if code == 0 {
rs.ExitCode = 126 code = 126
} }
} }
@ -97,13 +99,14 @@ func PrintRunStateErr(rs *fst.RunState, runErr error) {
} }
out: out:
if rs.ExitCode == 0 { if code == 0 {
rs.ExitCode = 128 code = 128
} }
} }
if rs.WaitErr != nil { if rs.WaitErr != nil {
log.Println("inner wait failed:", rs.WaitErr) fmsg.Verbosef("wait: %v", rs.WaitErr)
} }
return
} }
// StateStoreError is returned for a failed state save // StateStoreError is returned for a failed state save
@ -121,7 +124,7 @@ type StateStoreError struct {
} }
// save saves arbitrary errors in [StateStoreError] once. // save saves arbitrary errors in [StateStoreError] once.
func (e *StateStoreError) save(errs []error) { func (e *StateStoreError) save(errs ...error) {
if len(errs) == 0 || e.Err != nil { if len(errs) == 0 || e.Err != nil {
panic("invalid call to save") panic("invalid call to save")
} }

View File

@ -1,20 +1,20 @@
package app package setuid
import ( import (
"git.gensokyo.uk/security/fortify/fst" . "git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/sys" "git.gensokyo.uk/security/fortify/internal/sys"
"git.gensokyo.uk/security/fortify/sandbox" "git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system" "git.gensokyo.uk/security/fortify/system"
) )
func NewWithID(id fst.ID, os sys.State) fst.App { func NewWithID(id ID, os sys.State) App {
a := new(app) a := new(app)
a.id = newID(&id) a.id = newID(&id)
a.sys = os a.sys = os
return a return a
} }
func AppIParams(a fst.App, sa fst.SealedApp) (*system.I, *sandbox.Params) { func AppIParams(a App, sa SealedApp) (*system.I, *sandbox.Params) {
v := a.(*app) v := a.(*app)
seal := sa.(*outcome) seal := sa.(*outcome)
if v.outcome != seal || v.id != seal.id { if v.outcome != seal || v.id != seal.id {

View File

@ -0,0 +1,195 @@
package setuid
import (
"context"
"encoding/gob"
"errors"
"log"
"os"
"os/exec"
"strconv"
"strings"
"syscall"
"time"
"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/state"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system"
)
const shimWaitTimeout = 5 * time.Second
func (seal *outcome) Run(rs *RunState) error {
if !seal.f.CompareAndSwap(false, true) {
// run does much more than just starting a process; calling it twice, even if the first call fails, will result
// in inconsistent state that is impossible to clean up; return here to limit damage and hopefully give the
// other Run a chance to return
return errors.New("outcome: attempted to run twice")
}
if rs == nil {
panic("invalid state")
}
// read comp value early to allow for early failure
fsuPath := internal.MustFsuPath()
if err := seal.sys.Commit(seal.ctx); err != nil {
return err
}
store := state.NewMulti(seal.runDirPath)
deferredStoreFunc := func(c state.Cursor) error { return nil } // noop until state in store
defer func() {
var revertErr error
storeErr := new(StateStoreError)
storeErr.Inner, storeErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) {
revertErr = func() error {
storeErr.InnerErr = deferredStoreFunc(c)
var rt system.Enablement
ec := system.Process
if states, err := c.Load(); err != nil {
// revert per-process state here to limit damage
storeErr.OpErr = err
return seal.sys.Revert((*system.Criteria)(&ec))
} else {
if l := len(states); l == 0 {
ec |= system.User
} else {
fmsg.Verbosef("found %d instances, cleaning up without user-scoped operations", l)
}
// accumulate enablements of remaining launchers
for i, s := range states {
if s.Config != nil {
rt |= s.Config.Enablements
} else {
log.Printf("state entry %d does not contain config", i)
}
}
}
ec |= rt ^ (system.EWayland | system.EX11 | system.EDBus | system.EPulse)
if fmsg.Load() {
if ec > 0 {
fmsg.Verbose("reverting operations scope", system.TypeString(ec))
}
}
return seal.sys.Revert((*system.Criteria)(&ec))
}()
})
storeErr.save(revertErr, store.Close())
rs.RevertErr = storeErr.equiv("error during cleanup:")
}()
ctx, cancel := context.WithCancel(seal.ctx)
defer cancel()
cmd := exec.CommandContext(ctx, fsuPath)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Dir = "/" // container init enters final working directory
// shim runs in the same session as monitor; see shim.go for behaviour
cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGCONT) }
var e *gob.Encoder
if fd, encoder, err := sandbox.Setup(&cmd.ExtraFiles); err != nil {
return fmsg.WrapErrorSuffix(err,
"cannot create shim setup pipe:")
} else {
e = encoder
cmd.Env = []string{
// passed through to shim by fsu
shimEnv + "=" + strconv.Itoa(fd),
// interpreted by fsu
"FORTIFY_APP_ID=" + seal.user.aid.String(),
}
}
if len(seal.user.supp) > 0 {
fmsg.Verbosef("attaching supplementary group ids %s", seal.user.supp)
// interpreted by fsu
cmd.Env = append(cmd.Env, "FORTIFY_GROUPS="+strings.Join(seal.user.supp, " "))
}
fmsg.Verbosef("setuid helper at %s", fsuPath)
fmsg.Suspend()
if err := cmd.Start(); err != nil {
return fmsg.WrapErrorSuffix(err,
"cannot start setuid wrapper:")
}
rs.SetStart()
// this prevents blocking forever on an early failure
waitErr, setupErr := make(chan error, 1), make(chan error, 1)
go func() { waitErr <- cmd.Wait(); cancel() }()
go func() { setupErr <- e.Encode(&shimParams{os.Getpid(), seal.container, seal.user.data, fmsg.Load()}) }()
select {
case err := <-setupErr:
if err != nil {
fmsg.Resume()
return fmsg.WrapErrorSuffix(err,
"cannot transmit shim config:")
}
case <-ctx.Done():
fmsg.Resume()
return fmsg.WrapError(syscall.ECANCELED,
"shim setup canceled")
}
// returned after blocking on waitErr
var earlyStoreErr = new(StateStoreError)
{
// shim accepted setup payload, create process state
sd := state.State{
ID: seal.id.unwrap(),
PID: cmd.Process.Pid,
Time: *rs.Time,
}
earlyStoreErr.Inner, earlyStoreErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) {
earlyStoreErr.InnerErr = c.Save(&sd, seal.ct)
})
}
// state in store at this point, destroy defunct state entry on return
deferredStoreFunc = func(c state.Cursor) error { return c.Destroy(seal.id.unwrap()) }
waitTimeout := make(chan struct{})
go func() { <-seal.ctx.Done(); time.Sleep(shimWaitTimeout); close(waitTimeout) }()
select {
case rs.WaitErr = <-waitErr:
rs.WaitStatus = cmd.ProcessState.Sys().(syscall.WaitStatus)
if fmsg.Load() {
switch {
case rs.Exited():
fmsg.Verbosef("process %d exited with code %d", cmd.Process.Pid, rs.ExitStatus())
case rs.CoreDump():
fmsg.Verbosef("process %d dumped core", cmd.Process.Pid)
case rs.Signaled():
fmsg.Verbosef("process %d got %s", cmd.Process.Pid, rs.Signal())
default:
fmsg.Verbosef("process %d exited with status %#x", cmd.Process.Pid, rs.WaitStatus)
}
}
case <-waitTimeout:
rs.WaitErr = syscall.ETIMEDOUT
fmsg.Resume()
log.Printf("process %d did not terminate", cmd.Process.Pid)
}
fmsg.Resume()
if seal.sync != nil {
if err := seal.sync.Close(); err != nil {
log.Printf("cannot close wayland security context: %v", err)
}
}
if seal.dbusMsg != nil {
seal.dbusMsg()
}
return earlyStoreErr.equiv("cannot save process state:")
}

View File

@ -1,4 +1,4 @@
package app package setuid
import ( import (
"bytes" "bytes"
@ -8,7 +8,6 @@ import (
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
"maps"
"os" "os"
"path" "path"
"regexp" "regexp"
@ -21,6 +20,8 @@ import (
"git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal"
. "git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/app/instance/common"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/sys" "git.gensokyo.uk/security/fortify/internal/sys"
"git.gensokyo.uk/security/fortify/sandbox" "git.gensokyo.uk/security/fortify/sandbox"
@ -65,7 +66,7 @@ var posixUsername = regexp.MustCompilePOSIX("^[a-z_]([A-Za-z0-9_-]{0,31}|[A-Za-z
// outcome stores copies of various parts of [fst.Config] // outcome stores copies of various parts of [fst.Config]
type outcome struct { type outcome struct {
// copied from initialising [app] // copied from initialising [app]
id *stringPair[fst.ID] id *stringPair[ID]
// copied from [sys.State] response // copied from [sys.State] response
runDirPath string runDirPath string
@ -86,6 +87,53 @@ type outcome struct {
f atomic.Bool 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 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 // fsuUser stores post-fsu credentials and metadata
type fsuUser struct { type fsuUser struct {
// application id // application id
@ -110,11 +158,6 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
} }
seal.ctx = ctx seal.ctx = ctx
shellPath := "/bin/sh"
if s, ok := sys.LookupEnv(shell); ok && path.IsAbs(s) {
shellPath = s
}
{ {
// encode initial configuration for state tracking // encode initial configuration for state tracking
ct := new(bytes.Buffer) ct := new(bytes.Buffer)
@ -126,20 +169,16 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
} }
// allowed aid range 0 to 9999, this is checked again in fsu // allowed aid range 0 to 9999, this is checked again in fsu
if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 { if config.Identity < 0 || config.Identity > 9999 {
return fmsg.WrapError(ErrUser, return fmsg.WrapError(ErrUser,
fmt.Sprintf("aid %d out of range", config.Confinement.AppID)) fmt.Sprintf("identity %d out of range", config.Identity))
} }
/*
Resolve post-fsu user state
*/
seal.user = fsuUser{ seal.user = fsuUser{
aid: newInt(config.Confinement.AppID), aid: newInt(config.Identity),
data: config.Confinement.Outer, data: config.Data,
home: config.Confinement.Inner, home: config.Dir,
username: config.Confinement.Username, username: config.Username,
} }
if seal.user.username == "" { if seal.user.username == "" {
seal.user.username = "chronos" seal.user.username = "chronos"
@ -160,8 +199,8 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
} else { } else {
seal.user.uid = newInt(u) seal.user.uid = newInt(u)
} }
seal.user.supp = make([]string, len(config.Confinement.Groups)) seal.user.supp = make([]string, len(config.Groups))
for i, name := range config.Confinement.Groups { for i, name := range config.Groups {
if g, err := sys.LookupGroup(name); err != nil { if g, err := sys.LookupGroup(name); err != nil {
return fmsg.WrapError(err, return fmsg.WrapError(err,
fmt.Sprintf("unknown group %q", name)) fmt.Sprintf("unknown group %q", name))
@ -170,13 +209,18 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
} }
} }
/* // this also falls back to host path if encountering an invalid path
Resolve initial container state if !path.IsAbs(config.Shell) {
*/ config.Shell = "/bin/sh"
if s, ok := sys.LookupEnv(shell); ok && path.IsAbs(s) {
config.Shell = s
}
}
// do not use the value of shell before this point
// permissive defaults // permissive defaults
if config.Confinement.Sandbox == nil { if config.Container == nil {
fmsg.Verbose("sandbox configuration not supplied, PROCEED WITH CAUTION") fmsg.Verbose("container configuration not supplied, PROCEED WITH CAUTION")
// fsu clears the environment so resolve paths early // fsu clears the environment so resolve paths early
if !path.IsAbs(config.Path) { if !path.IsAbs(config.Path) {
@ -187,11 +231,11 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
config.Path = p config.Path = p
} }
} else { } else {
config.Path = shellPath config.Path = config.Shell
} }
} }
conf := &fst.SandboxConfig{ conf := &fst.ContainerConfig{
Userns: true, Userns: true,
Net: true, Net: true,
Tty: true, Tty: true,
@ -224,20 +268,20 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
conf.Cover = append(conf.Cover, nscd) conf.Cover = append(conf.Cover, nscd)
} }
// bind GPU stuff // bind GPU stuff
if config.Confinement.Enablements&(system.EX11|system.EWayland) != 0 { if config.Enablements&(system.EX11|system.EWayland) != 0 {
conf.Filesystem = append(conf.Filesystem, &fst.FilesystemConfig{Src: "/dev/dri", Device: true}) conf.Filesystem = append(conf.Filesystem, &fst.FilesystemConfig{Src: "/dev/dri", Device: true})
} }
// opportunistically bind kvm // opportunistically bind kvm
conf.Filesystem = append(conf.Filesystem, &fst.FilesystemConfig{Src: "/dev/kvm", Device: true}) conf.Filesystem = append(conf.Filesystem, &fst.FilesystemConfig{Src: "/dev/kvm", Device: true})
config.Confinement.Sandbox = conf config.Container = conf
} }
var mapuid, mapgid *stringPair[int] var mapuid, mapgid *stringPair[int]
{ {
var uid, gid int var uid, gid int
var err error var err error
seal.container, seal.env, err = config.Confinement.Sandbox.ToContainer(sys, &uid, &gid) seal.container, seal.env, err = common.NewContainer(config.Container, sys, &uid, &gid)
if err != nil { if err != nil {
return fmsg.WrapErrorSuffix(err, return fmsg.WrapErrorSuffix(err,
"cannot initialise container configuration:") "cannot initialise container configuration:")
@ -255,97 +299,80 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
mapuid = newInt(uid) mapuid = newInt(uid)
mapgid = newInt(gid) mapgid = newInt(gid)
if seal.env == nil { if seal.env == nil {
seal.env = make(map[string]string) seal.env = make(map[string]string, 1<<6)
} }
} }
/* if !config.Container.AutoEtc {
Initialise externals if config.Container.Etc != "" {
*/ seal.container.Bind(config.Container.Etc, "/etc", 0)
}
} else {
etcPath := config.Container.Etc
if etcPath == "" {
etcPath = "/etc"
}
seal.container.Etc(etcPath, seal.id.String())
}
sc := sys.Paths() // inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as mapped uid
seal.runDirPath = sc.RunDirPath
seal.sys = system.New(seal.user.uid.unwrap())
/*
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
innerRuntimeDir := path.Join("/run/user", mapuid.String()) innerRuntimeDir := path.Join("/run/user", mapuid.String())
seal.container.Tmpfs("/run/user", 1<<12, 0755) seal.container.Tmpfs("/run/user", 1<<12, 0755)
seal.container.Tmpfs(innerRuntimeDir, 1<<23, 0755) seal.container.Tmpfs(innerRuntimeDir, 1<<23, 0700)
seal.env[xdgRuntimeDir] = innerRuntimeDir seal.env[xdgRuntimeDir] = innerRuntimeDir
seal.env[xdgSessionClass] = "user" seal.env[xdgSessionClass] = "user"
seal.env[xdgSessionType] = "tty" seal.env[xdgSessionType] = "tty"
// outer path for inner /tmp share := &shareHost{seal: seal, sc: sys.Paths()}
seal.runDirPath = share.sc.RunDirPath
seal.sys = system.New(seal.user.uid.unwrap())
{ {
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.Ensure(tmpdir, 0700)
seal.sys.UpdatePermType(system.User, tmpdir, acl.Execute) seal.sys.UpdatePermType(system.User, tmpdir, acl.Execute)
tmpdirInst := path.Join(tmpdir, seal.user.aid.String()) tmpdirInst := path.Join(tmpdir, seal.user.aid.String())
seal.sys.Ensure(tmpdirInst, 01700) seal.sys.Ensure(tmpdirInst, 01700)
seal.sys.UpdatePermType(system.User, tmpdirInst, acl.Read, acl.Write, acl.Execute) 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) seal.container.Bind(tmpdirInst, "/tmp", sandbox.BindWritable)
} }
/* {
Passwd database homeDir := "/var/empty"
*/ if seal.user.home != "" {
homeDir = seal.user.home
}
username := "chronos"
if seal.user.username != "" {
username = seal.user.username
}
seal.container.Bind(seal.user.data, homeDir, sandbox.BindWritable)
seal.container.Dir = homeDir
seal.env["HOME"] = homeDir
seal.env["USER"] = username
seal.env[shell] = config.Shell
homeDir := "/var/empty" seal.container.Place("/etc/passwd",
if seal.user.home != "" { []byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Fortify:"+homeDir+":"+config.Shell+"\n"))
homeDir = seal.user.home seal.container.Place("/etc/group",
[]byte("fortify:x:"+mapgid.String()+":\n"))
} }
username := "chronos"
if seal.user.username != "" {
username = seal.user.username
}
seal.container.Bind(seal.user.data, homeDir, sandbox.BindWritable)
seal.container.Dir = homeDir
seal.env["HOME"] = homeDir
seal.env["USER"] = username
seal.container.Place("/etc/passwd", // pass TERM for proper terminal I/O in initial process
[]byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Fortify:"+homeDir+":"+shellPath+"\n"))
seal.container.Place("/etc/group",
[]byte("fortify:x:"+mapgid.String()+":\n"))
/*
Display servers
*/
// pass $TERM for proper terminal I/O in shell
if t, ok := sys.LookupEnv(term); ok { if t, ok := sys.LookupEnv(term); ok {
seal.env[term] = t seal.env[term] = t
} }
if config.Confinement.Enablements&system.EWayland != 0 { if config.Enablements&system.EWayland != 0 {
// outer wayland socket (usually `/run/user/%d/wayland-%d`) // outer wayland socket (usually `/run/user/%d/wayland-%d`)
var socketPath string var socketPath string
if name, ok := sys.LookupEnv(wl.WaylandDisplay); !ok { if name, ok := sys.LookupEnv(wl.WaylandDisplay); !ok {
fmsg.Verbose(wl.WaylandDisplay + " is not set, assuming " + wl.FallbackName) 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) { } else if !path.IsAbs(name) {
socketPath = path.Join(sc.RuntimePath, name) socketPath = path.Join(share.sc.RuntimePath, name)
} else { } else {
socketPath = name socketPath = name
} }
@ -353,25 +380,25 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
innerPath := path.Join(innerRuntimeDir, wl.FallbackName) innerPath := path.Join(innerRuntimeDir, wl.FallbackName)
seal.env[wl.WaylandDisplay] = wl.FallbackName seal.env[wl.WaylandDisplay] = wl.FallbackName
if !config.Confinement.Sandbox.DirectWayland { // set up security-context-v1 if !config.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 appID := config.ID
if appID == "" { if appID == "" {
// use instance ID in case app id is not set // use instance ID in case app id is not set
appID = "uk.gensokyo.fortify." + seal.id.String() appID = "uk.gensokyo.fortify." + seal.id.String()
} }
// downstream socket paths
outerPath := path.Join(share.instance(), "wayland")
seal.sys.Wayland(&seal.sync, outerPath, socketPath, appID, seal.id.String()) seal.sys.Wayland(&seal.sync, outerPath, socketPath, appID, seal.id.String())
seal.container.Bind(outerPath, innerPath, 0) seal.container.Bind(outerPath, innerPath, 0)
} else { // bind mount wayland socket (insecure) } else { // bind mount wayland socket (insecure)
fmsg.Verbose("direct wayland access, PROCEED WITH CAUTION") fmsg.Verbose("direct wayland access, PROCEED WITH CAUTION")
share.ensureRuntimeDir()
seal.container.Bind(socketPath, innerPath, 0) seal.container.Bind(socketPath, innerPath, 0)
seal.sys.UpdatePermType(system.EWayland, socketPath, acl.Read, acl.Write, acl.Execute) seal.sys.UpdatePermType(system.EWayland, socketPath, acl.Read, acl.Write, acl.Execute)
} }
} }
if config.Confinement.Enablements&system.EX11 != 0 { if config.Enablements&system.EX11 != 0 {
if d, ok := sys.LookupEnv(display); !ok { if d, ok := sys.LookupEnv(display); !ok {
return fmsg.WrapError(ErrXDisplay, return fmsg.WrapError(ErrXDisplay,
"DISPLAY is not set") "DISPLAY is not set")
@ -382,13 +409,9 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
} }
} }
/* if config.Enablements&system.EPulse != 0 {
PulseAudio server and authentication
*/
if config.Confinement.Enablements&system.EPulse != 0 {
// PulseAudio runtime directory (usually `/run/user/%d/pulse`) // 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`) // PulseAudio socket (usually `/run/user/%d/pulse/native`)
pulseSocket := path.Join(pulseRuntimeDir, "native") pulseSocket := path.Join(pulseRuntimeDir, "native")
@ -416,7 +439,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
} }
// hard link pulse socket into target-executable share // 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") innerPulseSocket := path.Join(innerRuntimeDir, "pulse", "native")
seal.sys.Link(pulseSocket, innerPulseRuntimeDir) seal.sys.Link(pulseSocket, innerPulseRuntimeDir)
seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0) seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0)
@ -435,22 +458,19 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
} }
} }
/* if config.Enablements&system.EDBus != 0 {
D-Bus proxy
*/
if config.Confinement.Enablements&system.EDBus != 0 {
// ensure dbus session bus defaults // ensure dbus session bus defaults
if config.Confinement.SessionBus == nil { if config.SessionBus == nil {
config.Confinement.SessionBus = dbus.NewConfig(config.ID, true, true) config.SessionBus = dbus.NewConfig(config.ID, true, true)
} }
// downstream socket paths // downstream socket paths
sharePath := share.instance()
sessionPath, systemPath := path.Join(sharePath, "bus"), path.Join(sharePath, "system_bus_socket") sessionPath, systemPath := path.Join(sharePath, "bus"), path.Join(sharePath, "system_bus_socket")
// configure dbus proxy // configure dbus proxy
if f, err := seal.sys.ProxyDBus( if f, err := seal.sys.ProxyDBus(
config.Confinement.SessionBus, config.Confinement.SystemBus, config.SessionBus, config.SystemBus,
sessionPath, systemPath, sessionPath, systemPath,
); err != nil { ); err != nil {
return err return err
@ -463,7 +483,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
seal.env[dbusSessionBusAddress] = "unix:path=" + sessionInner seal.env[dbusSessionBusAddress] = "unix:path=" + sessionInner
seal.container.Bind(sessionPath, sessionInner, 0) seal.container.Bind(sessionPath, sessionInner, 0)
seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write) seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
if config.Confinement.SystemBus != nil { if config.SystemBus != nil {
systemInner := "/run/dbus/system_bus_socket" systemInner := "/run/dbus/system_bus_socket"
seal.env[dbusSystemBusAddress] = "unix:path=" + systemInner seal.env[dbusSystemBusAddress] = "unix:path=" + systemInner
seal.container.Bind(systemPath, systemInner, 0) seal.container.Bind(systemPath, systemInner, 0)
@ -471,16 +491,12 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
} }
} }
/* for _, dest := range config.Container.Cover {
Miscellaneous
*/
for _, dest := range config.Confinement.Sandbox.Cover {
seal.container.Tmpfs(dest, 1<<13, 0755) seal.container.Tmpfs(dest, 1<<13, 0755)
} }
// append ExtraPerms last // append ExtraPerms last
for _, p := range config.Confinement.ExtraPerms { for _, p := range config.ExtraPerms {
if p == nil { if p == nil {
continue continue
} }
@ -504,11 +520,19 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
// flatten and sort env for deterministic behaviour // flatten and sort env for deterministic behaviour
seal.container.Env = make([]string, 0, len(seal.env)) seal.container.Env = make([]string, 0, len(seal.env))
maps.All(seal.env)(func(k string, v string) bool { seal.container.Env = append(seal.container.Env, k+"="+v); return true }) 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) slices.Sort(seal.container.Env)
fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s", if fmsg.Load() {
seal.user.uid, seal.user.username, config.Confinement.Groups, seal.container.Args) fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s, ops: %d",
seal.user.uid, seal.user.username, config.Groups, seal.container.Args, len(*seal.container.Ops))
}
return nil return nil
} }

View File

@ -1,26 +1,78 @@
package app package setuid
import ( import (
"context" "context"
"encoding/gob"
"errors" "errors"
"log" "log"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"strconv"
"strings"
"syscall" "syscall"
"time" "time"
"git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/sandbox" "git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
) )
/*
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <signal.h>
static pid_t f_shim_param_ppid = -1;
// this cannot unblock fmsg since Go code is not async-signal-safe
static void f_shim_sigaction(int sig, siginfo_t *si, void *ucontext) {
if (sig != SIGCONT || si == NULL) {
// unreachable
fprintf(stderr, "sigaction: sa_sigaction got invalid siginfo\n");
return;
}
// monitor requests shim exit
if (si->si_pid == f_shim_param_ppid)
exit(254);
fprintf(stderr, "sigaction: got SIGCONT from process %d\n", si->si_pid);
// shim orphaned before monitor delivers a signal
if (getppid() != f_shim_param_ppid)
exit(3);
}
void f_shim_setup_cont_signal(pid_t ppid) {
struct sigaction new_action = {0}, old_action = {0};
if (sigaction(SIGCONT, NULL, &old_action) != 0)
return;
if (old_action.sa_handler != SIG_DFL) {
errno = ENOTRECOVERABLE;
return;
}
new_action.sa_sigaction = f_shim_sigaction;
if (sigemptyset(&new_action.sa_mask) != 0)
return;
new_action.sa_flags = SA_ONSTACK | SA_SIGINFO;
if (sigaction(SIGCONT, &new_action, NULL) != 0)
return;
errno = 0;
f_shim_param_ppid = ppid;
}
*/
import "C"
const shimEnv = "FORTIFY_SHIM" const shimEnv = "FORTIFY_SHIM"
type shimParams struct { type shimParams struct {
// monitor pid, checked against ppid in signal handler
Monitor int
// finalised container params // finalised container params
Container *sandbox.Params Container *sandbox.Params
// path to outer home directory // path to outer home directory
@ -54,6 +106,16 @@ func ShimMain() {
} else { } else {
internal.InstallFmsg(params.Verbose) internal.InstallFmsg(params.Verbose)
closeSetup = f closeSetup = f
// the Go runtime does not expose siginfo_t so SIGCONT is handled in C to check si_pid
if _, err = C.f_shim_setup_cont_signal(C.pid_t(params.Monitor)); err != nil {
log.Fatalf("cannot install SIGCONT handler: %v", err)
}
// pdeath_signal delivery is checked as if the dying process called kill(2), see kernel/exit.c
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGCONT), 0); errno != 0 {
log.Fatalf("cannot set parent-death signal: %v", errno)
}
} }
if params.Container == nil || params.Container.Ops == nil { if params.Container == nil || params.Container.Ops == nil {
@ -100,6 +162,11 @@ func ShimMain() {
if err := container.Serve(); err != nil { if err := container.Serve(); err != nil {
fmsg.PrintBaseError(err, "cannot configure container:") fmsg.PrintBaseError(err, "cannot configure container:")
} }
if err := seccomp.Load(seccomp.PresetCommon); err != nil {
log.Fatalf("cannot load syscall filter: %v", err)
}
if err := container.Wait(); err != nil { if err := container.Wait(); err != nil {
var exitError *exec.ExitError var exitError *exec.ExitError
if !errors.As(err, &exitError) { if !errors.As(err, &exitError) {
@ -112,101 +179,3 @@ func ShimMain() {
os.Exit(exitError.ExitCode()) 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,13 +1,13 @@
package app package setuid
import ( import (
"strconv" "strconv"
"git.gensokyo.uk/security/fortify/fst" . "git.gensokyo.uk/security/fortify/internal/app"
) )
func newInt(v int) *stringPair[int] { return &stringPair[int]{v, strconv.Itoa(v)} } func newInt(v int) *stringPair[int] { return &stringPair[int]{v, strconv.Itoa(v)} }
func newID(id *fst.ID) *stringPair[fst.ID] { return &stringPair[fst.ID]{*id, id.String()} } func newID(id *ID) *stringPair[ID] { return &stringPair[ID]{*id, id.String()} }
// stringPair stores a value and its string representation. // stringPair stores a value and its string representation.
type stringPair[T comparable] struct { type stringPair[T comparable] struct {

View File

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

View File

@ -14,6 +14,7 @@ import (
"syscall" "syscall"
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
) )
@ -33,10 +34,10 @@ func (s *multiStore) Do(aid int, f func(c Cursor)) (bool, error) {
// load or initialise new backend // load or initialise new backend
b := new(multiBackend) b := new(multiBackend)
b.lock.Lock()
if v, ok := s.backends.LoadOrStore(aid, b); ok { if v, ok := s.backends.LoadOrStore(aid, b); ok {
b = v.(*multiBackend) b = v.(*multiBackend)
} else { } else {
b.lock.Lock()
b.path = path.Join(s.base, strconv.Itoa(aid)) b.path = path.Join(s.base, strconv.Itoa(aid))
// ensure directory // ensure directory
@ -129,7 +130,7 @@ type multiBackend struct {
lock sync.RWMutex lock sync.RWMutex
} }
func (b *multiBackend) filename(id *fst.ID) string { func (b *multiBackend) filename(id *app.ID) string {
return path.Join(b.path, id.String()) return path.Join(b.path, id.String())
} }
@ -189,8 +190,8 @@ func (b *multiBackend) load(decode bool) (Entries, error) {
return nil, fmt.Errorf("unexpected directory %q in store", e.Name()) return nil, fmt.Errorf("unexpected directory %q in store", e.Name())
} }
id := new(fst.ID) id := new(app.ID)
if err := fst.ParseAppID(id, e.Name()); err != nil { if err := app.ParseAppID(id, e.Name()); err != nil {
return nil, err return nil, err
} }
@ -335,7 +336,7 @@ func (b *multiBackend) encodeState(w io.WriteSeeker, state *State, configWriter
return err return err
} }
func (b *multiBackend) Destroy(id fst.ID) error { func (b *multiBackend) Destroy(id app.ID) error {
b.lock.Lock() b.lock.Lock()
defer b.lock.Unlock() defer b.lock.Unlock()

View File

@ -6,11 +6,12 @@ import (
"time" "time"
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/app"
) )
var ErrNoConfig = errors.New("state does not contain config") var ErrNoConfig = errors.New("state does not contain config")
type Entries map[fst.ID]*State type Entries map[app.ID]*State
type Store interface { type Store interface {
// Do calls f exactly once and ensures store exclusivity until f returns. // Do calls f exactly once and ensures store exclusivity until f returns.
@ -29,7 +30,7 @@ type Store interface {
// Cursor provides access to the store // Cursor provides access to the store
type Cursor interface { type Cursor interface {
Save(state *State, configWriter io.WriterTo) error Save(state *State, configWriter io.WriterTo) error
Destroy(id fst.ID) error Destroy(id app.ID) error
Load() (Entries, error) Load() (Entries, error)
Len() (int, error) Len() (int, error)
} }
@ -37,7 +38,7 @@ type Cursor interface {
// State is a fortify process's state // State is a fortify process's state
type State struct { type State struct {
// fortify instance id // fortify instance id
ID fst.ID `json:"instance"` ID app.ID `json:"instance"`
// child process PID value // child process PID value
PID int `json:"pid"` PID int `json:"pid"`
// sealed app configuration // sealed app configuration

View File

@ -11,6 +11,7 @@ import (
"time" "time"
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/state" "git.gensokyo.uk/security/fortify/internal/state"
) )
@ -133,7 +134,7 @@ func testStore(t *testing.T, s state.Store) {
} }
func makeState(t *testing.T, s *state.State, ct io.Writer) { func makeState(t *testing.T, s *state.State, ct io.Writer) {
if err := fst.NewAppID(&s.ID); err != nil { if err := app.NewAppID(&s.ID); err != nil {
t.Fatalf("cannot create dummy state: %v", err) t.Fatalf("cannot create dummy state: %v", err)
} }
if err := gob.NewEncoder(ct).Encode(fst.Template()); err != nil { if err := gob.NewEncoder(ct).Encode(fst.Template()); err != nil {

View File

@ -6,7 +6,7 @@ import (
"path" "path"
"strconv" "strconv"
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
) )
@ -41,14 +41,14 @@ type State interface {
Printf(format string, v ...any) Printf(format string, v ...any)
// Paths returns a populated [Paths] struct. // Paths returns a populated [Paths] struct.
Paths() fst.Paths Paths() app.Paths
// Uid invokes fsu and returns target uid. // Uid invokes fsu and returns target uid.
// Any errors returned by Uid is already wrapped [fmsg.BaseError]. // Any errors returned by Uid is already wrapped [fmsg.BaseError].
Uid(aid int) (int, error) 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) { func CopyPaths(os State, v *app.Paths) {
v.SharePath = path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Getuid())) v.SharePath = path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Getuid()))
fmsg.Verbosef("process share directory at %q", v.SharePath) fmsg.Verbosef("process share directory at %q", v.SharePath)

View File

@ -12,15 +12,15 @@ import (
"sync" "sync"
"syscall" "syscall"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal" "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/fmsg"
"git.gensokyo.uk/security/fortify/sandbox" "git.gensokyo.uk/security/fortify/sandbox"
) )
// Std implements System using the standard library. // Std implements System using the standard library.
type Std struct { type Std struct {
paths fst.Paths paths app.Paths
pathsOnce sync.Once pathsOnce sync.Once
uidOnce sync.Once uidOnce sync.Once
@ -48,7 +48,7 @@ func (s *Std) Printf(format string, v ...any) { fmsg.Verbosef(form
const xdgRuntimeDir = "XDG_RUNTIME_DIR" const xdgRuntimeDir = "XDG_RUNTIME_DIR"
func (s *Std) Paths() fst.Paths { func (s *Std) Paths() app.Paths {
s.pathsOnce.Do(func() { CopyPaths(s, &s.paths) }) s.pathsOnce.Do(func() { CopyPaths(s, &s.paths) })
return s.paths return s.paths
} }

45
main.go
View File

@ -20,6 +20,7 @@ import (
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/app" "git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/app/instance"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/state" "git.gensokyo.uk/security/fortify/internal/state"
"git.gensokyo.uk/security/fortify/internal/sys" "git.gensokyo.uk/security/fortify/internal/sys"
@ -73,7 +74,7 @@ func buildCommand(out io.Writer) command.Command {
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console"). Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output as JSON when applicable") 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("shim", command.UsageInternal, func([]string) error { instance.ShimMain(); return errSuccess })
c.Command("app", "Launch app defined by the specified config file", func(args []string) error { c.Command("app", "Launch app defined by the specified config file", func(args []string) error {
if len(args) < 1 { if len(args) < 1 {
@ -153,33 +154,33 @@ func buildCommand(out io.Writer) command.Command {
userName = passwd.Username userName = passwd.Username
} }
config.Confinement.AppID = aid config.Identity = aid
config.Confinement.Groups = groups config.Groups = groups
config.Confinement.Outer = homeDir config.Data = homeDir
config.Confinement.Username = userName config.Username = userName
if wayland { if wayland {
config.Confinement.Enablements |= system.EWayland config.Enablements |= system.EWayland
} }
if x11 { if x11 {
config.Confinement.Enablements |= system.EX11 config.Enablements |= system.EX11
} }
if dBus { if dBus {
config.Confinement.Enablements |= system.EDBus config.Enablements |= system.EDBus
} }
if pulse { if pulse {
config.Confinement.Enablements |= system.EPulse config.Enablements |= system.EPulse
} }
// parse D-Bus config file from flags if applicable // parse D-Bus config file from flags if applicable
if dBus { if dBus {
if dbusConfigSession == "builtin" { if dbusConfigSession == "builtin" {
config.Confinement.SessionBus = dbus.NewConfig(fid, true, mpris) config.SessionBus = dbus.NewConfig(fid, true, mpris)
} else { } else {
if conf, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil { if conf, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
log.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err) log.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err)
} else { } else {
config.Confinement.SessionBus = conf config.SessionBus = conf
} }
} }
@ -188,14 +189,14 @@ func buildCommand(out io.Writer) command.Command {
if conf, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil { if conf, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil {
log.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err) log.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err)
} else { } else {
config.Confinement.SystemBus = conf config.SystemBus = conf
} }
} }
// override log from configuration // override log from configuration
if dbusVerbose { if dbusVerbose {
config.Confinement.SessionBus.Log = true config.SessionBus.Log = true
config.Confinement.SystemBus.Log = true config.SystemBus.Log = true
} }
} }
@ -239,11 +240,11 @@ func buildCommand(out io.Writer) command.Command {
case 1: // instance case 1: // instance
name := args[0] name := args[0]
config, instance := tryShort(name) config, entry := tryShort(name)
if config == nil { if config == nil {
config = tryPath(name) config = tryPath(name)
} }
printShowInstance(os.Stdout, time.Now().UTC(), instance, config, showFlagShort, flagJSON) printShowInstance(os.Stdout, time.Now().UTC(), entry, config, showFlagShort, flagJSON)
default: default:
log.Fatal("show requires 1 argument") log.Fatal("show requires 1 argument")
@ -284,15 +285,15 @@ func runApp(config *fst.Config) {
ctx, stop := signal.NotifyContext(context.Background(), ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM) syscall.SIGINT, syscall.SIGTERM)
defer stop() // unreachable defer stop() // unreachable
a := app.MustNew(ctx, std) a := instance.MustNew(instance.ISetuid, ctx, std)
rs := new(fst.RunState) rs := new(app.RunState)
if sa, err := a.Seal(config); err != nil { if sa, err := a.Seal(config); err != nil {
fmsg.PrintBaseError(err, "cannot seal app:") fmsg.PrintBaseError(err, "cannot seal app:")
rs.ExitCode = 1 internal.Exit(1)
} else { } else {
// this updates ExitCode internal.Exit(instance.PrintRunStateErr(instance.ISetuid, rs, sa.Run(rs)))
app.PrintRunStateErr(rs, sa.Run(rs))
} }
internal.Exit(rs.ExitCode)
*(*int)(nil) = 0 // not reached
} }

169
nixos.nix
View File

@ -88,94 +88,101 @@ in
conf = { conf = {
inherit (app) id; inherit (app) id;
path = pkgs.writeScript "${app.name}-start" ''
#!${pkgs.zsh}${pkgs.zsh.shellPath}
${script}
'';
args = [ "${app.name}-start" ];
confinement = { path =
app_id = aid; if app.path == null then
inherit (app) groups; pkgs.writeScript "${app.name}-start" ''
username = getsubname fid aid; #!${pkgs.zsh}${pkgs.zsh.shellPath}
home = getsubhome fid aid; ${script}
sandbox = { ''
inherit (app) else
devel app.path;
userns args = if app.args == null then [ "${app.name}-start" ] else app.args;
net
dev
tty
multiarch
env
;
map_real_uid = app.mapRealUid;
direct_wayland = app.insecureWayland;
filesystem = inherit enablements;
let
bind = src: { inherit src; }; inherit (dbusConfig) session_bus system_bus;
mustBind = src: { direct_wayland = app.insecureWayland;
inherit src;
require = true; username = getsubname fid aid;
}; data = getsubhome fid aid;
devBind = src: {
inherit src; identity = aid;
dev = true; inherit (app) groups;
};
in container = {
inherit (app)
devel
userns
net
device
tty
multiarch
env
;
map_real_uid = app.mapRealUid;
filesystem =
let
bind = src: { inherit src; };
mustBind = src: {
inherit src;
require = true;
};
devBind = src: {
inherit src;
dev = true;
};
in
[
(mustBind "/bin")
(mustBind "/usr/bin")
(mustBind "/nix/store")
(bind "/sys/block")
(bind "/sys/bus")
(bind "/sys/class")
(bind "/sys/dev")
(bind "/sys/devices")
]
++ optionals app.nix [
(mustBind "/nix/var")
(bind "/var/db/nix-channels")
]
++ optionals isGraphical [
(devBind "/dev/dri")
(devBind "/dev/nvidiactl")
(devBind "/dev/nvidia-modeset")
(devBind "/dev/nvidia-uvm")
(devBind "/dev/nvidia-uvm-tools")
(devBind "/dev/nvidia0")
]
++ app.extraPaths;
auto_etc = true;
cover = [ "/var/run/nscd" ];
symlink =
[
[ [
(mustBind "/bin") "*/run/current-system"
(mustBind "/usr/bin") "/run/current-system"
(mustBind "/nix/store")
(bind "/sys/block")
(bind "/sys/bus")
(bind "/sys/class")
(bind "/sys/dev")
(bind "/sys/devices")
] ]
++ optionals app.nix [ ]
(mustBind "/nix/var") ++ optionals (isGraphical && config.hardware.graphics.enable) (
(bind "/var/db/nix-channels")
]
++ optionals isGraphical [
(devBind "/dev/dri")
(devBind "/dev/nvidiactl")
(devBind "/dev/nvidia-modeset")
(devBind "/dev/nvidia-uvm")
(devBind "/dev/nvidia-uvm-tools")
(devBind "/dev/nvidia0")
]
++ app.extraPaths;
auto_etc = true;
cover = [ "/var/run/nscd" ];
symlink =
[ [
[ [
"*/run/current-system" config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver"."L+".argument
"/run/current-system" "/run/opengl-driver"
] ]
] ]
++ optionals (isGraphical && config.hardware.graphics.enable) ( ++ optionals (app.multiarch && config.hardware.graphics.enable32Bit) [
[ [
[ config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver-32"."L+".argument
config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver"."L+".argument /run/opengl-driver-32
"/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 in
pkgs.writeShellScriptBin app.name '' pkgs.writeShellScriptBin app.name ''
@ -197,9 +204,11 @@ in
${copy "${pkg}/share/icons"} ${copy "${pkg}/share/icons"}
${copy "${pkg}/share/man"} ${copy "${pkg}/share/man"}
substituteInPlace $out/share/applications/* \ if test -d "$out/share/applications"; then
--replace-warn '${pkg}/bin/' "" \ substituteInPlace $out/share/applications/* \
--replace-warn '${pkg}/libexec/' "" --replace-warn '${pkg}/bin/' "" \
--replace-warn '${pkg}/libexec/' ""
fi
'' ''
) )
++ acc ++ acc

View File

@ -35,7 +35,7 @@ package
*Default:* *Default:*
` <derivation fortify-static-x86_64-unknown-linux-musl-0.3.0> ` ` <derivation fortify-static-x86_64-unknown-linux-musl-0.4.0> `
@ -73,6 +73,25 @@ list of package
## environment\.fortify\.apps\.\*\.args
Custom args\.
Setting this to null will default to script name\.
*Type:*
null or (list of string)
*Default:*
` null `
## environment\.fortify\.apps\.\*\.capability\.dbus ## environment\.fortify\.apps\.\*\.capability\.dbus
@ -203,11 +222,11 @@ null or anything
## environment\.fortify\.apps\.\*\.dev ## environment\.fortify\.apps\.\*\.devel
Whether to enable access to all devices\. Whether to enable debugging-related kernel interfaces\.
@ -226,11 +245,11 @@ boolean
## environment\.fortify\.apps\.\*\.devel ## environment\.fortify\.apps\.\*\.device
Whether to enable debugging-related kernel interfaces\. Whether to enable access to all devices\.
@ -486,6 +505,25 @@ 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 ## environment\.fortify\.apps\.\*\.script
@ -606,7 +644,7 @@ package
*Default:* *Default:*
` <derivation fortify-fsu-0.3.0> ` ` <derivation fortify-fsu-0.4.0> `

View File

@ -94,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 { script = mkOption {
type = nullOr str; type = nullOr str;
default = null; default = null;
@ -159,7 +177,7 @@ in
nix = mkEnableOption "nix daemon access"; nix = mkEnableOption "nix daemon access";
mapRealUid = mkEnableOption "mapping to priv-user uid"; mapRealUid = mkEnableOption "mapping to priv-user uid";
dev = mkEnableOption "access to all devices"; device = mkEnableOption "access to all devices";
insecureWayland = mkEnableOption "direct access to the Wayland socket"; insecureWayland = mkEnableOption "direct access to the Wayland socket";
gpu = mkOption { gpu = mkOption {

View File

@ -31,7 +31,7 @@
buildGoModule rec { buildGoModule rec {
pname = "fortify"; pname = "fortify";
version = "0.3.0"; version = "0.4.0";
src = builtins.path { src = builtins.path {
name = "${pname}-src"; name = "${pname}-src";

View File

@ -67,7 +67,7 @@ func tryFd(name string) io.ReadCloser {
} }
} }
func tryShort(name string) (config *fst.Config, instance *state.State) { func tryShort(name string) (config *fst.Config, entry *state.State) {
likePrefix := false likePrefix := false
if len(name) <= 32 { if len(name) <= 32 {
likePrefix = true likePrefix = true
@ -96,8 +96,8 @@ func tryShort(name string) (config *fst.Config, instance *state.State) {
v := id.String() v := id.String()
if strings.HasPrefix(v, name) { if strings.HasPrefix(v, name) {
// match, use config from this state entry // match, use config from this state entry
instance = entries[id] entry = entries[id]
config = instance.Config config = entry.Config
break break
} }

View File

@ -56,7 +56,7 @@ func printShowInstance(
t := newPrinter(output) t := newPrinter(output)
defer t.MustFlush() defer t.MustFlush()
if config.Confinement.Sandbox == nil { if config.Container == nil {
mustPrint(output, "Warning: this configuration uses permissive defaults!\n\n") mustPrint(output, "Warning: this configuration uses permissive defaults!\n\n")
} }
@ -69,19 +69,21 @@ func printShowInstance(
t.Printf("App\n") t.Printf("App\n")
if config.ID != "" { if config.ID != "" {
t.Printf(" ID:\t%d (%s)\n", config.Confinement.AppID, config.ID) t.Printf(" ID:\t%d (%s)\n", config.Identity, config.ID)
} else { } else {
t.Printf(" ID:\t%d\n", config.Confinement.AppID) t.Printf(" ID:\t%d\n", config.Identity)
} }
t.Printf(" Enablements:\t%s\n", config.Confinement.Enablements.String()) t.Printf(" Enablements:\t%s\n", config.Enablements.String())
if len(config.Confinement.Groups) > 0 { if len(config.Groups) > 0 {
t.Printf(" Groups:\t%q\n", config.Confinement.Groups) t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", "))
} }
t.Printf(" Directory:\t%s\n", config.Confinement.Outer) if config.Data != "" {
if config.Confinement.Sandbox != nil { t.Printf(" Data:\t%s\n", config.Data)
sandbox := config.Confinement.Sandbox }
if sandbox.Hostname != "" { if config.Container != nil {
t.Printf(" Hostname:\t%q\n", sandbox.Hostname) container := config.Container
if container.Hostname != "" {
t.Printf(" Hostname:\t%s\n", container.Hostname)
} }
flags := make([]string, 0, 7) flags := make([]string, 0, 7)
writeFlag := func(name string, value bool) { writeFlag := func(name string, value bool) {
@ -89,38 +91,40 @@ func printShowInstance(
flags = append(flags, name) flags = append(flags, name)
} }
} }
writeFlag("userns", sandbox.Userns) writeFlag("userns", container.Userns)
writeFlag("net", sandbox.Net) writeFlag("devel", container.Devel)
writeFlag("dev", sandbox.Dev) writeFlag("net", container.Net)
writeFlag("tty", sandbox.Tty) writeFlag("device", container.Device)
writeFlag("mapuid", sandbox.MapRealUID) writeFlag("tty", container.Tty)
writeFlag("directwl", sandbox.DirectWayland) writeFlag("mapuid", container.MapRealUID)
writeFlag("autoetc", sandbox.AutoEtc) writeFlag("directwl", config.DirectWayland)
writeFlag("autoetc", container.AutoEtc)
if len(flags) == 0 { if len(flags) == 0 {
flags = append(flags, "none") flags = append(flags, "none")
} }
t.Printf(" Flags:\t%s\n", strings.Join(flags, " ")) t.Printf(" Flags:\t%s\n", strings.Join(flags, " "))
etc := sandbox.Etc etc := container.Etc
if etc == "" { if etc == "" {
etc = "/etc" etc = "/etc"
} }
t.Printf(" Etc:\t%s\n", etc) t.Printf(" Etc:\t%s\n", etc)
if len(sandbox.Cover) > 0 { if len(container.Cover) > 0 {
t.Printf(" Cover:\t%s\n", strings.Join(sandbox.Cover, " ")) t.Printf(" Cover:\t%s\n", strings.Join(container.Cover, " "))
} }
// Env map[string]string `json:"env"` t.Printf(" Path:\t%s\n", config.Path)
// Link [][2]string `json:"symlink"` }
if len(config.Args) > 0 {
t.Printf(" Arguments:\t%s\n", strings.Join(config.Args, " "))
} }
t.Printf(" Command:\t%s\n", strings.Join(config.Args, " "))
t.Printf("\n") t.Printf("\n")
if !short { if !short {
if config.Confinement.Sandbox != nil && len(config.Confinement.Sandbox.Filesystem) > 0 { if config.Container != nil && len(config.Container.Filesystem) > 0 {
t.Printf("Filesystem\n") t.Printf("Filesystem\n")
for _, f := range config.Confinement.Sandbox.Filesystem { for _, f := range config.Container.Filesystem {
if f == nil { if f == nil {
continue continue
} }
@ -148,9 +152,9 @@ func printShowInstance(
} }
t.Printf("\n") t.Printf("\n")
} }
if len(config.Confinement.ExtraPerms) > 0 { if len(config.ExtraPerms) > 0 {
t.Printf("Extra ACL\n") t.Printf("Extra ACL\n")
for _, p := range config.Confinement.ExtraPerms { for _, p := range config.ExtraPerms {
if p == nil { if p == nil {
continue continue
} }
@ -178,14 +182,14 @@ func printShowInstance(
t.Printf(" Broadcast:\t%q\n", c.Broadcast) t.Printf(" Broadcast:\t%q\n", c.Broadcast)
} }
} }
if config.Confinement.SessionBus != nil { if config.SessionBus != nil {
t.Printf("Session bus\n") t.Printf("Session bus\n")
printDBus(config.Confinement.SessionBus) printDBus(config.SessionBus)
t.Printf("\n") t.Printf("\n")
} }
if config.Confinement.SystemBus != nil { if config.SystemBus != nil {
t.Printf("System bus\n") t.Printf("System bus\n")
printDBus(config.Confinement.SystemBus) printDBus(config.SystemBus)
t.Printf("\n") t.Printf("\n")
} }
} }
@ -247,22 +251,26 @@ func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON boo
t := newPrinter(output) t := newPrinter(output)
defer t.MustFlush() defer t.MustFlush()
t.Println("\tInstance\tPID\tApp\tUptime\tEnablements\tCommand") t.Println("\tInstance\tPID\tApplication\tUptime")
for _, e := range exp { for _, e := range exp {
var ( if len(e.s) != 1<<5 {
es = "(No confinement information)" // unreachable
cs = "(No command information)" log.Printf("possible store corruption: invalid instance string %s", e.s)
as = "(No configuration information)" continue
)
if e.Config != nil {
es = e.Config.Confinement.Enablements.String()
cs = fmt.Sprintf("%q", e.Config.Args)
as = strconv.Itoa(e.Config.Confinement.AppID)
} }
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) as := "(No configuration information)"
if e.Config != nil {
as = strconv.Itoa(e.Config.Identity)
id := e.Config.ID
if id == "" {
id = "uk.gensokyo.fortify." + e.s[:8]
}
as += " (" + id + ")"
}
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())
} }
t.Println()
} }
type expandedStateEntry struct { type expandedStateEntry struct {

View File

@ -7,11 +7,12 @@ import (
"git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/state" "git.gensokyo.uk/security/fortify/internal/state"
) )
var ( var (
testID = fst.ID{ testID = app.ID{
0x8e, 0x2c, 0x76, 0xb0, 0x8e, 0x2c, 0x76, 0xb0,
0x66, 0xda, 0xbe, 0x57, 0x66, 0xda, 0xbe, 0x57,
0x4c, 0xf0, 0x73, 0xbd, 0x4c, 0xf0, 0x73, 0xbd,
@ -38,13 +39,14 @@ func Test_printShowInstance(t *testing.T) {
{"config", nil, fst.Template(), false, false, `App {"config", nil, fst.Template(), false, false, `App
ID: 9 (org.chromium.Chromium) ID: 9 (org.chromium.Chromium)
Enablements: wayland, dbus, pulseaudio Enablements: wayland, dbus, pulseaudio
Groups: ["video"] Groups: video, dialout, plugdev
Directory: /var/lib/persist/home/org.chromium.Chromium Data: /var/lib/fortify/u0/org.chromium.Chromium
Hostname: "localhost" Hostname: localhost
Flags: userns net dev tty mapuid autoetc Flags: userns devel net device tty mapuid autoetc
Etc: /etc Etc: /etc
Cover: /var/run/nscd Cover: /var/run/nscd
Command: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland Path: /run/current-system/sw/bin/chromium
Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
Filesystem Filesystem
+/nix/store +/nix/store
@ -75,39 +77,33 @@ System bus
App App
ID: 0 ID: 0
Enablements: (no enablements) Enablements: (no enablements)
Directory:
Command:
`}, `},
{"config flag none", nil, &fst.Config{Confinement: fst.ConfinementConfig{Sandbox: new(fst.SandboxConfig)}}, false, false, `App {"config flag none", nil, &fst.Config{Container: new(fst.ContainerConfig)}, false, false, `App
ID: 0 ID: 0
Enablements: (no enablements) Enablements: (no enablements)
Directory:
Flags: none Flags: none
Etc: /etc 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 {"config nil entries", nil, &fst.Config{Container: &fst.ContainerConfig{Filesystem: make([]*fst.FilesystemConfig, 1)}, ExtraPerms: make([]*fst.ExtraPermConfig, 1)}, false, false, `App
ID: 0 ID: 0
Enablements: (no enablements) Enablements: (no enablements)
Directory:
Flags: none Flags: none
Etc: /etc Etc: /etc
Command: Path:
Filesystem Filesystem
Extra ACL Extra ACL
`}, `},
{"config pd dbus see", nil, &fst.Config{Confinement: fst.ConfinementConfig{SessionBus: &dbus.Config{See: []string{"org.example.test"}}}}, false, false, `Warning: this configuration uses permissive defaults! {"config pd dbus see", nil, &fst.Config{SessionBus: &dbus.Config{See: []string{"org.example.test"}}}, false, false, `Warning: this configuration uses permissive defaults!
App App
ID: 0 ID: 0
Enablements: (no enablements) Enablements: (no enablements)
Directory:
Command:
Session bus Session bus
Filter: false Filter: false
@ -122,13 +118,14 @@ Session bus
App App
ID: 9 (org.chromium.Chromium) ID: 9 (org.chromium.Chromium)
Enablements: wayland, dbus, pulseaudio Enablements: wayland, dbus, pulseaudio
Groups: ["video"] Groups: video, dialout, plugdev
Directory: /var/lib/persist/home/org.chromium.Chromium Data: /var/lib/fortify/u0/org.chromium.Chromium
Hostname: "localhost" Hostname: localhost
Flags: userns net dev tty mapuid autoetc Flags: userns devel net device tty mapuid autoetc
Etc: /etc Etc: /etc
Cover: /var/run/nscd Cover: /var/run/nscd
Command: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland Path: /run/current-system/sw/bin/chromium
Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
Filesystem Filesystem
+/nix/store +/nix/store
@ -163,8 +160,6 @@ State
App App
ID: 0 ID: 0
Enablements: (no enablements) Enablements: (no enablements)
Directory:
Command:
`}, `},
@ -200,139 +195,67 @@ App
"--enable-features=UseOzonePlatform", "--enable-features=UseOzonePlatform",
"--ozone-platform=wayland" "--ozone-platform=wayland"
], ],
"confinement": { "enablements": 13,
"app_id": 9, "session_bus": {
"groups": [ "see": null,
"video" "talk": [
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager"
], ],
"username": "chronos", "own": [
"home_inner": "/var/lib/fortify", "org.chromium.Chromium.*",
"home": "/var/lib/persist/home/org.chromium.Chromium", "org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"sandbox": { "org.mpris.MediaPlayer2.chromium.*"
"hostname": "localhost",
"seccomp": 32,
"devel": true,
"userns": true,
"net": true,
"tty": true,
"multiarch": true,
"env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
},
"map_real_uid": true,
"dev": true,
"filesystem": [
{
"src": "/nix/store"
},
{
"src": "/run/current-system"
},
{
"src": "/run/opengl-driver"
},
{
"src": "/var/db/nix-channels"
},
{
"dst": "/data/data/org.chromium.Chromium",
"src": "/var/lib/fortify/u0/org.chromium.Chromium",
"write": true,
"require": true
},
{
"src": "/dev/dri",
"dev": true
}
],
"symlink": [
[
"/run/user/65534",
"/run/user/150"
]
],
"etc": "/etc",
"auto_etc": true,
"cover": [
"/var/run/nscd"
]
},
"extra_perms": [
{
"ensure": true,
"path": "/var/lib/fortify/u0",
"x": true
},
{
"path": "/var/lib/fortify/u0/org.chromium.Chromium",
"r": true,
"w": true,
"x": true
}
], ],
"system_bus": { "call": {
"see": null, "org.freedesktop.portal.*": "*"
"talk": [
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower"
],
"own": null,
"call": null,
"broadcast": null,
"filter": true
}, },
"session_bus": { "broadcast": {
"see": null, "org.freedesktop.portal.*": "@/org/freedesktop/portal/*"
"talk": [
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager"
],
"own": [
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"
],
"call": {
"org.freedesktop.portal.*": "*"
},
"broadcast": {
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"
},
"filter": true
}, },
"enablements": 13 "filter": true
} },
}, "system_bus": {
"time": "1970-01-01T00:00:00.000000009Z" "see": null,
} "talk": [
`}, "org.bluez",
{"json config", nil, fst.Template(), false, true, `{ "org.freedesktop.Avahi",
"id": "org.chromium.Chromium", "org.freedesktop.UPower"
"path": "/run/current-system/sw/bin/chromium", ],
"args": [ "own": null,
"chromium", "call": null,
"--ignore-gpu-blocklist", "broadcast": null,
"--disable-smooth-scrolling", "filter": true
"--enable-features=UseOzonePlatform", },
"--ozone-platform=wayland"
],
"confinement": {
"app_id": 9,
"groups": [
"video"
],
"username": "chronos", "username": "chronos",
"home_inner": "/var/lib/fortify", "shell": "/run/current-system/sw/bin/zsh",
"home": "/var/lib/persist/home/org.chromium.Chromium", "data": "/var/lib/fortify/u0/org.chromium.Chromium",
"sandbox": { "dir": "/data/data/org.chromium.Chromium",
"extra_perms": [
{
"ensure": true,
"path": "/var/lib/fortify/u0",
"x": true
},
{
"path": "/var/lib/fortify/u0/org.chromium.Chromium",
"r": true,
"w": true,
"x": true
}
],
"identity": 9,
"groups": [
"video",
"dialout",
"plugdev"
],
"container": {
"hostname": "localhost", "hostname": "localhost",
"seccomp": 32, "seccomp": 32,
"devel": true, "devel": true,
@ -346,7 +269,7 @@ App
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT" "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
}, },
"map_real_uid": true, "map_real_uid": true,
"dev": true, "device": true,
"filesystem": [ "filesystem": [
{ {
"src": "/nix/store" "src": "/nix/store"
@ -382,57 +305,131 @@ App
"cover": [ "cover": [
"/var/run/nscd" "/var/run/nscd"
] ]
}
},
"time": "1970-01-01T00:00:00.000000009Z"
}
`},
{"json config", nil, fst.Template(), false, true, `{
"id": "org.chromium.Chromium",
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"enablements": 13,
"session_bus": {
"see": null,
"talk": [
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager"
],
"own": [
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"
],
"call": {
"org.freedesktop.portal.*": "*"
}, },
"extra_perms": [ "broadcast": {
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"
},
"filter": true
},
"system_bus": {
"see": null,
"talk": [
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower"
],
"own": null,
"call": null,
"broadcast": null,
"filter": true
},
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"data": "/var/lib/fortify/u0/org.chromium.Chromium",
"dir": "/data/data/org.chromium.Chromium",
"extra_perms": [
{
"ensure": true,
"path": "/var/lib/fortify/u0",
"x": true
},
{
"path": "/var/lib/fortify/u0/org.chromium.Chromium",
"r": true,
"w": true,
"x": true
}
],
"identity": 9,
"groups": [
"video",
"dialout",
"plugdev"
],
"container": {
"hostname": "localhost",
"seccomp": 32,
"devel": true,
"userns": true,
"net": true,
"tty": true,
"multiarch": true,
"env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
},
"map_real_uid": true,
"device": true,
"filesystem": [
{ {
"ensure": true, "src": "/nix/store"
"path": "/var/lib/fortify/u0",
"x": true
}, },
{ {
"path": "/var/lib/fortify/u0/org.chromium.Chromium", "src": "/run/current-system"
"r": true, },
"w": true, {
"x": true "src": "/run/opengl-driver"
},
{
"src": "/var/db/nix-channels"
},
{
"dst": "/data/data/org.chromium.Chromium",
"src": "/var/lib/fortify/u0/org.chromium.Chromium",
"write": true,
"require": true
},
{
"src": "/dev/dri",
"dev": true
} }
], ],
"system_bus": { "symlink": [
"see": null, [
"talk": [ "/run/user/65534",
"org.bluez", "/run/user/150"
"org.freedesktop.Avahi", ]
"org.freedesktop.UPower" ],
], "etc": "/etc",
"own": null, "auto_etc": true,
"call": null, "cover": [
"broadcast": null, "/var/run/nscd"
"filter": true ]
},
"session_bus": {
"see": null,
"talk": [
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager"
],
"own": [
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"
],
"call": {
"org.freedesktop.portal.*": "*"
},
"broadcast": {
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"
},
"filter": true
},
"enablements": 13
} }
} }
`}, `},
@ -458,23 +455,19 @@ func Test_printPs(t *testing.T) {
short, json bool short, json bool
want string 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\n"},
{"no entries short", make(state.Entries), true, false, ""},
`}, {"nil instance", state.Entries{testID: nil}, false, false, " Instance PID Application Uptime\n"},
{"no entries short", make(state.Entries), true, false, ``}, {"state corruption", state.Entries{app.ID{}: testState}, false, false, " Instance PID Application Uptime\n"},
{"nil instance", state.Entries{testID: nil}, false, false, ` Instance PID App Uptime Enablements Command
`},
{"state corruption", state.Entries{fst.ID{}: testState}, false, false, ` Instance PID App Uptime Enablements Command
{"valid pd", state.Entries{testID: &state.State{ID: testID, PID: 1 << 8, Config: new(fst.Config), Time: testAppTime}}, false, false, ` Instance PID Application Uptime
8e2c76b0 256 0 (uk.gensokyo.fortify.8e2c76b0) 1h2m32s
`}, `},
{"valid", state.Entries{testID: testState}, false, false, ` Instance PID App Uptime Enablements Command {"valid", state.Entries{testID: testState}, false, false, ` Instance PID Application Uptime
8e2c76b0 3735928559 9 1h2m32s wayland, dbus, pulseaudio ["chromium" "--ignore-gpu-blocklist" "--disable-smooth-scrolling" "--enable-features=UseOzonePlatform" "--ozone-platform=wayland"] 8e2c76b0 3735928559 9 (org.chromium.Chromium) 1h2m32s
`},
{"valid short", state.Entries{testID: testState}, true, false, `8e2c76b0
`}, `},
{"valid short", state.Entries{testID: testState}, true, false, "8e2c76b0\n"},
{"valid json", state.Entries{testID: testState}, false, true, `{ {"valid json", state.Entries{testID: testState}, false, true, `{
"8e2c76b066dabe574cf073bdb46eb5c1": { "8e2c76b066dabe574cf073bdb46eb5c1": {
"instance": [ "instance": [
@ -506,115 +499,116 @@ func Test_printPs(t *testing.T) {
"--enable-features=UseOzonePlatform", "--enable-features=UseOzonePlatform",
"--ozone-platform=wayland" "--ozone-platform=wayland"
], ],
"confinement": { "enablements": 13,
"app_id": 9, "session_bus": {
"groups": [ "see": null,
"video" "talk": [
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager"
], ],
"username": "chronos", "own": [
"home_inner": "/var/lib/fortify", "org.chromium.Chromium.*",
"home": "/var/lib/persist/home/org.chromium.Chromium", "org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"sandbox": { "org.mpris.MediaPlayer2.chromium.*"
"hostname": "localhost", ],
"seccomp": 32, "call": {
"devel": true, "org.freedesktop.portal.*": "*"
"userns": true,
"net": true,
"tty": true,
"multiarch": true,
"env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
},
"map_real_uid": true,
"dev": true,
"filesystem": [
{
"src": "/nix/store"
},
{
"src": "/run/current-system"
},
{
"src": "/run/opengl-driver"
},
{
"src": "/var/db/nix-channels"
},
{
"dst": "/data/data/org.chromium.Chromium",
"src": "/var/lib/fortify/u0/org.chromium.Chromium",
"write": true,
"require": true
},
{
"src": "/dev/dri",
"dev": true
}
],
"symlink": [
[
"/run/user/65534",
"/run/user/150"
]
],
"etc": "/etc",
"auto_etc": true,
"cover": [
"/var/run/nscd"
]
}, },
"extra_perms": [ "broadcast": {
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"
},
"filter": true
},
"system_bus": {
"see": null,
"talk": [
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower"
],
"own": null,
"call": null,
"broadcast": null,
"filter": true
},
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"data": "/var/lib/fortify/u0/org.chromium.Chromium",
"dir": "/data/data/org.chromium.Chromium",
"extra_perms": [
{
"ensure": true,
"path": "/var/lib/fortify/u0",
"x": true
},
{
"path": "/var/lib/fortify/u0/org.chromium.Chromium",
"r": true,
"w": true,
"x": true
}
],
"identity": 9,
"groups": [
"video",
"dialout",
"plugdev"
],
"container": {
"hostname": "localhost",
"seccomp": 32,
"devel": true,
"userns": true,
"net": true,
"tty": true,
"multiarch": true,
"env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
},
"map_real_uid": true,
"device": true,
"filesystem": [
{ {
"ensure": true, "src": "/nix/store"
"path": "/var/lib/fortify/u0",
"x": true
}, },
{ {
"path": "/var/lib/fortify/u0/org.chromium.Chromium", "src": "/run/current-system"
"r": true, },
"w": true, {
"x": true "src": "/run/opengl-driver"
},
{
"src": "/var/db/nix-channels"
},
{
"dst": "/data/data/org.chromium.Chromium",
"src": "/var/lib/fortify/u0/org.chromium.Chromium",
"write": true,
"require": true
},
{
"src": "/dev/dri",
"dev": true
} }
], ],
"system_bus": { "symlink": [
"see": null, [
"talk": [ "/run/user/65534",
"org.bluez", "/run/user/150"
"org.freedesktop.Avahi", ]
"org.freedesktop.UPower" ],
], "etc": "/etc",
"own": null, "auto_etc": true,
"call": null, "cover": [
"broadcast": null, "/var/run/nscd"
"filter": true ]
},
"session_bus": {
"see": null,
"talk": [
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager"
],
"own": [
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"
],
"call": {
"org.freedesktop.portal.*": "*"
},
"broadcast": {
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"
},
"filter": true
},
"enablements": 13
} }
}, },
"time": "1970-01-01T00:00:00.000000009Z" "time": "1970-01-01T00:00:00.000000009Z"

View File

@ -27,18 +27,18 @@ const (
FAllowNet FAllowNet
) )
func (flags HardeningFlags) seccomp(opts seccomp.SyscallOpts) seccomp.SyscallOpts { func (flags HardeningFlags) seccomp(opts seccomp.FilterOpts) seccomp.FilterOpts {
if flags&FSyscallCompat == 0 { if flags&FSyscallCompat == 0 {
opts |= seccomp.FlagExt opts |= seccomp.FilterExt
} }
if flags&FAllowDevel == 0 { if flags&FAllowDevel == 0 {
opts |= seccomp.FlagDenyDevel opts |= seccomp.FilterDenyDevel
} }
if flags&FAllowUserns == 0 { if flags&FAllowUserns == 0 {
opts |= seccomp.FlagDenyNS opts |= seccomp.FilterDenyNS
} }
if flags&FAllowTTY == 0 { if flags&FAllowTTY == 0 {
opts |= seccomp.FlagDenyTTY opts |= seccomp.FilterDenyTTY
} }
return opts return opts
} }
@ -95,23 +95,15 @@ type (
// Sequential container setup ops. // Sequential container setup ops.
*Ops *Ops
// Extra seccomp options. // Extra seccomp options.
Seccomp seccomp.SyscallOpts Seccomp seccomp.FilterOpts
// Permission bits of newly created parent directories. // Permission bits of newly created parent directories.
// The zero value is interpreted as 0755. // The zero value is interpreted as 0755.
ParentPerm os.FileMode ParentPerm os.FileMode
// Retain CAP_SYS_ADMIN.
Privileged bool
Flags HardeningFlags Flags HardeningFlags
} }
Ops []Op
Op interface {
early(params *Params) error
apply(params *Params) error
prefix() string
Is(op Op) bool
fmt.Stringer
}
) )
func (p *Container) Start() error { func (p *Container) Start() error {
@ -165,7 +157,7 @@ func (p *Container) Start() error {
syscall.CLONE_NEWNS, syscall.CLONE_NEWNS,
// remain privileged for setup // remain privileged for setup
AmbientCaps: []uintptr{CAP_SYS_ADMIN}, AmbientCaps: []uintptr{CAP_SYS_ADMIN, CAP_SETPCAP},
UseCgroupFD: p.Cgroup != nil, UseCgroupFD: p.Cgroup != nil,
} }

View File

@ -164,7 +164,7 @@ func e(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoE
func TestContainerString(t *testing.T) { func TestContainerString(t *testing.T) {
container := sandbox.New(context.TODO(), "ldd", "/usr/bin/env") container := sandbox.New(context.TODO(), "ldd", "/usr/bin/env")
container.Flags |= sandbox.FAllowDevel container.Flags |= sandbox.FAllowDevel
container.Seccomp |= seccomp.FlagMultiarch container.Seccomp |= seccomp.FilterMultiarch
want := `argv: ["ldd" "/usr/bin/env"], flags: 0x2, seccomp: 0x2e` want := `argv: ["ldd" "/usr/bin/env"], flags: 0x2, seccomp: 0x2e`
if got := container.String(); got != want { if got := container.String(); got != want {
t.Errorf("String: %s, want %s", got, want) t.Errorf("String: %s, want %s", got, want)

View File

@ -45,10 +45,6 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
log.Fatal("this process must run as pid 1") log.Fatal("this process must run as pid 1")
} }
/*
receive setup payload
*/
var ( var (
params initParams params initParams
closeSetup func() error closeSetup func() error
@ -108,9 +104,8 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
} }
} }
/* // cache sysctl before pivot_root
set up mount points from intermediate root LastCap()
*/
if err := syscall.Mount("", "/", "", if err := syscall.Mount("", "/", "",
syscall.MS_SILENT|syscall.MS_SLAVE|syscall.MS_REC, syscall.MS_SILENT|syscall.MS_SLAVE|syscall.MS_REC,
@ -152,6 +147,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
if err := os.Mkdir(hostDir, 0755); err != nil { if err := os.Mkdir(hostDir, 0755); err != nil {
log.Fatalf("%v", err) log.Fatalf("%v", err)
} }
// pivot_root uncovers basePath in hostDir
if err := syscall.PivotRoot(basePath, hostDir); err != nil { if err := syscall.PivotRoot(basePath, hostDir); err != nil {
log.Fatalf("cannot pivot into intermediate root: %v", err) log.Fatalf("cannot pivot into intermediate root: %v", err)
} }
@ -170,10 +166,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
} }
} }
/* // setup requiring host root complete at this point
pivot to sysroot
*/
if err := syscall.Mount(hostDir, hostDir, "", if err := syscall.Mount(hostDir, hostDir, "",
syscall.MS_SILENT|syscall.MS_REC|syscall.MS_PRIVATE, syscall.MS_SILENT|syscall.MS_REC|syscall.MS_PRIVATE,
""); err != nil { ""); err != nil {
@ -213,33 +206,48 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
} }
} }
/* if _, _, errno := syscall.Syscall(PR_SET_NO_NEW_PRIVS, 1, 0, 0); errno != 0 {
load seccomp filter log.Fatalf("prctl(PR_SET_NO_NEW_PRIVS): %v", errno)
*/
if _, _, err := syscall.Syscall(PR_SET_NO_NEW_PRIVS, 1, 0, 0); err != 0 {
log.Fatalf("prctl(PR_SET_NO_NEW_PRIVS): %v", err)
} }
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 { if err := seccomp.Load(params.Flags.seccomp(params.Seccomp)); err != nil {
log.Fatalf("cannot load syscall filter: %v", err) log.Fatalf("cannot load syscall filter: %v", err)
} }
/* at this point CAP_SYS_ADMIN can be dropped, however it is kept for now as it does not increase attack surface */
/*
pass through extra files
*/
extraFiles := make([]*os.File, params.Count) extraFiles := make([]*os.File, params.Count)
for i := range extraFiles { for i := range extraFiles {
// setup fd is placed before all extra files
extraFiles[i] = os.NewFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i)) extraFiles[i] = os.NewFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i))
} }
syscall.Umask(oldmask) syscall.Umask(oldmask)
/*
prepare initial process
*/
cmd := exec.Command(params.Path) cmd := exec.Command(params.Path)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Args = params.Args cmd.Args = params.Args
@ -252,22 +260,11 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
} }
msg.Suspend() msg.Suspend()
/*
close setup pipe
*/
if err := closeSetup(); err != nil { if err := closeSetup(); err != nil {
log.Println("cannot close setup pipe:", err) log.Println("cannot close setup pipe:", err)
// not fatal // not fatal
} }
/*
perform init duties
*/
sig := make(chan os.Signal, 2)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
type winfo struct { type winfo struct {
wpid int wpid int
wstatus syscall.WaitStatus wstatus syscall.WaitStatus
@ -304,6 +301,10 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
close(done) 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 // closed after residualProcessTimeout has elapsed after initial process death
timeout := make(chan struct{}) timeout := make(chan struct{})
@ -316,7 +317,6 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
} else { } else {
msg.Verbosef("terminating on %s", s.String()) msg.Verbosef("terminating on %s", s.String())
} }
msg.BeforeExit()
os.Exit(0) os.Exit(0)
case w := <-info: case w := <-info:
if w.wpid == cmd.Process.Pid { if w.wpid == cmd.Process.Pid {

View File

@ -13,6 +13,22 @@ import (
"unsafe" "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)) } func init() { gob.Register(new(BindMount)) }
// BindMount bind mounts host path Source on container path Target. // BindMount bind mounts host path Source on container path Target.
@ -424,3 +440,60 @@ func (f *Ops) PlaceP(name string, dataP **[]byte) *Ops {
*f = append(*f, t) *f = append(*f, t)
return f return f
} }
func init() { gob.Register(new(AutoEtc)) }
// AutoEtc expands host /etc into a toplevel symlink mirror with /etc semantics.
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
type AutoEtc struct{ Prefix string }
func (e *AutoEtc) early(*Params) error { return nil }
func (e *AutoEtc) apply(*Params) error {
const target = sysrootPath + "/etc/"
rel := e.hostRel() + "/"
if err := os.MkdirAll(target, 0755); err != nil {
return wrapErrSelf(err)
}
if d, err := os.ReadDir(toSysroot(e.hostPath())); err != nil {
return wrapErrSelf(err)
} else {
for _, ent := range d {
n := ent.Name()
switch n {
case ".host":
case "passwd":
case "group":
case "mtab":
if err = os.Symlink("/proc/mounts", target+n); err != nil {
return wrapErrSelf(err)
}
default:
if err = os.Symlink(rel+n, target+n); err != nil {
return wrapErrSelf(err)
}
}
}
}
return nil
}
func (e *AutoEtc) hostPath() string { return "/etc/" + e.hostRel() }
func (e *AutoEtc) hostRel() string { return ".host/" + e.Prefix }
func (e *AutoEtc) Is(op Op) bool {
ve, ok := op.(*AutoEtc)
return ok && ((e == nil && ve == nil) || (e != nil && ve != nil && *e == *ve))
}
func (*AutoEtc) prefix() string { return "setting up" }
func (e *AutoEtc) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }
func (f *Ops) Etc(host, prefix string) *Ops {
e := &AutoEtc{prefix}
f.Mkdir("/etc", 0755)
f.Bind(host, e.hostPath(), 0)
*f = append(*f, e)
return f
}

View File

@ -1,37 +0,0 @@
package sandbox
import (
"bytes"
"log"
"os"
"strconv"
"sync"
)
var (
ofUid int
ofGid int
ofOnce sync.Once
)
const (
ofUidPath = "/proc/sys/kernel/overflowuid"
ofGidPath = "/proc/sys/kernel/overflowgid"
)
func mustReadOverflow() {
if v, err := os.ReadFile(ofUidPath); err != nil {
log.Fatalf("cannot read %q: %v", ofUidPath, err)
} else if ofUid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
log.Fatalf("cannot interpret %q: %v", ofUidPath, err)
}
if v, err := os.ReadFile(ofGidPath); err != nil {
log.Fatalf("cannot read %q: %v", ofGidPath, err)
} else if ofGid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
log.Fatalf("cannot interpret %q: %v", ofGidPath, err)
}
}
func OverflowUid() int { ofOnce.Do(mustReadOverflow); return ofUid }
func OverflowGid() int { ofOnce.Do(mustReadOverflow); return ofGid }

View File

@ -8,11 +8,16 @@ import (
"git.gensokyo.uk/security/fortify/helper/proc" "git.gensokyo.uk/security/fortify/helper/proc"
) )
const (
PresetStrict = FilterExt | FilterDenyNS | FilterDenyTTY | FilterDenyDevel
PresetCommon = PresetStrict | FilterMultiarch
)
// New returns an inactive Encoder instance. // New returns an inactive Encoder instance.
func New(opts SyscallOpts) *Encoder { return &Encoder{newExporter(opts)} } func New(opts FilterOpts) *Encoder { return &Encoder{newExporter(opts)} }
// Load loads a filter into the kernel. // Load loads a filter into the kernel.
func Load(opts SyscallOpts) error { return buildFilter(-1, opts) } func Load(opts FilterOpts) error { return buildFilter(-1, opts) }
/* /*
An Encoder writes a BPF program to an output stream. An Encoder writes a BPF program to an output stream.
@ -42,11 +47,11 @@ func (e *Encoder) Close() error {
} }
// NewFile returns an instance of exporter implementing [proc.File]. // NewFile returns an instance of exporter implementing [proc.File].
func NewFile(opts SyscallOpts) proc.File { return &File{opts: opts} } func NewFile(opts FilterOpts) proc.File { return &File{opts: opts} }
// File implements [proc.File] and provides access to the read end of exporter pipe. // File implements [proc.File] and provides access to the read end of exporter pipe.
type File struct { type File struct {
opts SyscallOpts opts FilterOpts
proc.BaseFile proc.BaseFile
} }

View File

@ -7,7 +7,7 @@ import (
) )
type exporter struct { type exporter struct {
opts SyscallOpts opts FilterOpts
r, w *os.File r, w *os.File
prepareOnce sync.Once prepareOnce sync.Once
@ -53,6 +53,6 @@ func (e *exporter) closeWrite() error {
return e.closeErr return e.closeErr
} }
func newExporter(opts SyscallOpts) *exporter { func newExporter(opts FilterOpts) *exporter {
return &exporter{opts: opts} return &exporter{opts: opts}
} }

View File

@ -14,7 +14,7 @@ import (
func TestExport(t *testing.T) { func TestExport(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
opts seccomp.SyscallOpts opts seccomp.FilterOpts
want []byte want []byte
wantErr bool wantErr bool
}{ }{
@ -28,7 +28,7 @@ func TestExport(t *testing.T) {
0xa7, 0x9b, 0x07, 0x0e, 0x04, 0xc0, 0xee, 0x9a, 0xa7, 0x9b, 0x07, 0x0e, 0x04, 0xc0, 0xee, 0x9a,
0xcd, 0xf5, 0x8f, 0x55, 0xcf, 0xa8, 0x15, 0xa5, 0xcd, 0xf5, 0x8f, 0x55, 0xcf, 0xa8, 0x15, 0xa5,
}, false}, }, false},
{"base", seccomp.FlagExt, []byte{ {"base", seccomp.FilterExt, []byte{
0xdc, 0x7f, 0x2e, 0x1c, 0x5e, 0x82, 0x9b, 0x79, 0xdc, 0x7f, 0x2e, 0x1c, 0x5e, 0x82, 0x9b, 0x79,
0xeb, 0xb7, 0xef, 0xc7, 0x59, 0x15, 0x0f, 0x54, 0xeb, 0xb7, 0xef, 0xc7, 0x59, 0x15, 0x0f, 0x54,
0xa8, 0x3a, 0x75, 0xc8, 0xdf, 0x6f, 0xee, 0x4d, 0xa8, 0x3a, 0x75, 0xc8, 0xdf, 0x6f, 0xee, 0x4d,
@ -38,10 +38,10 @@ func TestExport(t *testing.T) {
0x1d, 0xb0, 0x5d, 0x90, 0x99, 0x7c, 0x86, 0x59, 0x1d, 0xb0, 0x5d, 0x90, 0x99, 0x7c, 0x86, 0x59,
0xb9, 0x58, 0x91, 0x20, 0x6a, 0xc9, 0x95, 0x2d, 0xb9, 0x58, 0x91, 0x20, 0x6a, 0xc9, 0x95, 0x2d,
}, false}, }, false},
{"everything", seccomp.FlagExt | {"everything", seccomp.FilterExt |
seccomp.FlagDenyNS | seccomp.FlagDenyTTY | seccomp.FlagDenyDevel | seccomp.FilterDenyNS | seccomp.FilterDenyTTY | seccomp.FilterDenyDevel |
seccomp.FlagMultiarch | seccomp.FlagLinux32 | seccomp.FlagCan | seccomp.FilterMultiarch | seccomp.FilterLinux32 | seccomp.FilterCan |
seccomp.FlagBluetooth, []byte{ seccomp.FilterBluetooth, []byte{
0xe9, 0x9d, 0xd3, 0x45, 0xe1, 0x95, 0x41, 0x34, 0xe9, 0x9d, 0xd3, 0x45, 0xe1, 0x95, 0x41, 0x34,
0x73, 0xd3, 0xcb, 0xee, 0x07, 0xb4, 0xed, 0x57, 0x73, 0xd3, 0xcb, 0xee, 0x07, 0xb4, 0xed, 0x57,
0xb9, 0x08, 0xbf, 0xa8, 0x9e, 0xa2, 0x07, 0x2f, 0xb9, 0x08, 0xbf, 0xa8, 0x9e, 0xa2, 0x07, 0x2f,
@ -51,8 +51,7 @@ func TestExport(t *testing.T) {
0x4c, 0x02, 0x4e, 0xd4, 0x88, 0x50, 0xbe, 0x69, 0x4c, 0x02, 0x4e, 0xd4, 0x88, 0x50, 0xbe, 0x69,
0xb6, 0x8a, 0x9a, 0x4c, 0x5f, 0x53, 0xa9, 0xdb, 0xb6, 0x8a, 0x9a, 0x4c, 0x5f, 0x53, 0xa9, 0xdb,
}, false}, }, false},
{"strict", seccomp.FlagExt | {"strict", seccomp.PresetStrict, []byte{
seccomp.FlagDenyNS | seccomp.FlagDenyTTY | seccomp.FlagDenyDevel, []byte{
0xe8, 0x80, 0x29, 0x8d, 0xf2, 0xbd, 0x67, 0x51, 0xe8, 0x80, 0x29, 0x8d, 0xf2, 0xbd, 0x67, 0x51,
0xd0, 0x04, 0x0f, 0xc2, 0x1b, 0xc0, 0xed, 0x4c, 0xd0, 0x04, 0x0f, 0xc2, 0x1b, 0xc0, 0xed, 0x4c,
0x00, 0xf9, 0x5d, 0xc0, 0xd7, 0xba, 0x50, 0x6c, 0x00, 0xf9, 0x5d, 0xc0, 0xd7, 0xba, 0x50, 0x6c,
@ -63,7 +62,7 @@ func TestExport(t *testing.T) {
0x14, 0x89, 0x60, 0xfb, 0xd3, 0x5c, 0xd7, 0x35, 0x14, 0x89, 0x60, 0xfb, 0xd3, 0x5c, 0xd7, 0x35,
}, false}, }, false},
{"strict compat", 0 | {"strict compat", 0 |
seccomp.FlagDenyNS | seccomp.FlagDenyTTY | seccomp.FlagDenyDevel, []byte{ seccomp.FilterDenyNS | seccomp.FilterDenyTTY | seccomp.FilterDenyDevel, []byte{
0x39, 0x87, 0x1b, 0x93, 0xff, 0xaf, 0xc8, 0xb9, 0x39, 0x87, 0x1b, 0x93, 0xff, 0xaf, 0xc8, 0xb9,
0x79, 0xfc, 0xed, 0xc0, 0xb0, 0xc3, 0x7b, 0x9e, 0x79, 0xfc, 0xed, 0xc0, 0xb0, 0xc3, 0x7b, 0x9e,
0x03, 0x92, 0x2f, 0x5b, 0x02, 0x74, 0x8d, 0xc5, 0x03, 0x92, 0x2f, 0x5b, 0x02, 0x74, 0x8d, 0xc5,
@ -73,6 +72,16 @@ func TestExport(t *testing.T) {
0x80, 0x8b, 0x1a, 0x6f, 0x84, 0xf3, 0x2b, 0xbd, 0x80, 0x8b, 0x1a, 0x6f, 0x84, 0xf3, 0x2b, 0xbd,
0xe1, 0xaa, 0x02, 0xae, 0x30, 0xee, 0xdc, 0xfa, 0xe1, 0xaa, 0x02, 0xae, 0x30, 0xee, 0xdc, 0xfa,
}, false}, }, false},
{"fortify default", seccomp.FilterExt | seccomp.FilterDenyDevel, []byte{
0xc6, 0x98, 0xb0, 0x81, 0xff, 0x95, 0x7a, 0xfe,
0x17, 0xa6, 0xd9, 0x43, 0x74, 0x53, 0x7d, 0x37,
0xf2, 0xa6, 0x3f, 0x6f, 0x9d, 0xd7, 0x5d, 0xa7,
0x54, 0x65, 0x42, 0x40, 0x7a, 0x9e, 0x32, 0x47,
0x6e, 0xbd, 0xa3, 0x31, 0x2b, 0xa7, 0x78, 0x5d,
0x7f, 0x61, 0x85, 0x42, 0xbc, 0xfa, 0xf2, 0x7c,
0xa2, 0x7d, 0xcc, 0x2d, 0xdd, 0xba, 0x85, 0x20,
0x69, 0xd2, 0x8b, 0xcf, 0xe8, 0xca, 0xd3, 0x9a,
}, false},
} }
buf := make([]byte, 8) buf := make([]byte, 8)
@ -128,10 +137,10 @@ func TestExport(t *testing.T) {
func BenchmarkExport(b *testing.B) { func BenchmarkExport(b *testing.B) {
buf := make([]byte, 8) buf := make([]byte, 8)
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
e := seccomp.New(seccomp.FlagExt | e := seccomp.New(seccomp.FilterExt |
seccomp.FlagDenyNS | seccomp.FlagDenyTTY | seccomp.FlagDenyDevel | seccomp.FilterDenyNS | seccomp.FilterDenyTTY | seccomp.FilterDenyDevel |
seccomp.FlagMultiarch | seccomp.FlagLinux32 | seccomp.FlagCan | seccomp.FilterMultiarch | seccomp.FilterLinux32 | seccomp.FilterCan |
seccomp.FlagBluetooth) seccomp.FilterBluetooth)
if _, err := io.CopyBuffer(io.Discard, e, buf); err != nil { if _, err := io.CopyBuffer(io.Discard, e, buf); err != nil {
b.Fatalf("cannot export: %v", err) b.Fatalf("cannot export: %v", err)
} }

View File

@ -22,8 +22,8 @@ func GetOutput() func(v ...any) {
} }
} }
//export F_println //export f_println
func F_println(v *C.char) { func f_println(v *C.char) {
if fp := printlnP.Load(); fp != nil { if fp := printlnP.Load(); fp != nil {
(*fp)(C.GoString(v)) (*fp)(C.GoString(v))
} }

View File

@ -28,7 +28,7 @@ struct f_syscall_act {
#define LEN(arr) (sizeof(arr) / sizeof((arr)[0])) #define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
#define SECCOMP_RULESET_ADD(ruleset) do { \ #define SECCOMP_RULESET_ADD(ruleset) do { \
if (opts & F_VERBOSE) F_println("adding seccomp ruleset \"" #ruleset "\""); \ if (opts & F_VERBOSE) f_println("adding seccomp ruleset \"" #ruleset "\""); \
for (int i = 0; i < LEN(ruleset); i++) { \ for (int i = 0; i < LEN(ruleset); i++) { \
assert(ruleset[i].m_errno == EPERM || ruleset[i].m_errno == ENOSYS); \ assert(ruleset[i].m_errno == EPERM || ruleset[i].m_errno == ENOSYS); \
\ \
@ -47,7 +47,7 @@ struct f_syscall_act {
} \ } \
} while (0) } while (0)
int32_t f_build_filter(int *ret_p, int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts opts) { int32_t f_build_filter(int *ret_p, int fd, uint32_t arch, uint32_t multiarch, f_filter_opts opts) {
int32_t res = 0; // refer to resErr for meaning int32_t res = 0; // refer to resErr for meaning
int allow_multiarch = opts & F_MULTIARCH; int allow_multiarch = opts & F_MULTIARCH;
int allowed_personality = PER_LINUX; int allowed_personality = PER_LINUX;
@ -209,7 +209,7 @@ int32_t f_build_filter(int *ret_p, int fd, uint32_t arch, uint32_t multiarch, f_
struct struct
{ {
int family; int family;
f_syscall_opts flags_mask; f_filter_opts flags_mask;
} socket_family_allowlist[] = { } socket_family_allowlist[] = {
// NOTE: Keep in numerical order // NOTE: Keep in numerical order
{ AF_UNSPEC, 0 }, { AF_UNSPEC, 0 },

View File

@ -17,7 +17,7 @@ typedef enum {
F_LINUX32 = 1 << 6, F_LINUX32 = 1 << 6,
F_CAN = 1 << 7, F_CAN = 1 << 7,
F_BLUETOOTH = 1 << 8, F_BLUETOOTH = 1 << 8,
} f_syscall_opts; } f_filter_opts;
extern void F_println(char *v); extern void f_println(char *v);
int32_t f_build_filter(int *ret_p, int fd, uint32_t arch, uint32_t multiarch, f_syscall_opts opts); int32_t f_build_filter(int *ret_p, int fd, uint32_t arch, uint32_t multiarch, f_filter_opts opts);

View File

@ -1,3 +1,4 @@
// Package seccomp provides filter presets and high level wrappers around libseccomp.
package seccomp package seccomp
/* /*
@ -6,6 +7,7 @@ package seccomp
#include "seccomp-build.h" #include "seccomp-build.h"
*/ */
import "C" import "C"
import ( import (
"errors" "errors"
"fmt" "fmt"
@ -55,29 +57,29 @@ var resPrefix = [...]string{
7: "seccomp_load failed", 7: "seccomp_load failed",
} }
type SyscallOpts = C.f_syscall_opts type FilterOpts = C.f_filter_opts
const ( const (
flagVerbose SyscallOpts = C.F_VERBOSE filterVerbose FilterOpts = C.F_VERBOSE
// FlagExt are project-specific extensions. // FilterExt are project-specific extensions.
FlagExt SyscallOpts = C.F_EXT FilterExt FilterOpts = C.F_EXT
// FlagDenyNS denies namespace setup syscalls. // FilterDenyNS denies namespace setup syscalls.
FlagDenyNS SyscallOpts = C.F_DENY_NS FilterDenyNS FilterOpts = C.F_DENY_NS
// FlagDenyTTY denies faking input. // FilterDenyTTY denies faking input.
FlagDenyTTY SyscallOpts = C.F_DENY_TTY FilterDenyTTY FilterOpts = C.F_DENY_TTY
// FlagDenyDevel denies development-related syscalls. // FilterDenyDevel denies development-related syscalls.
FlagDenyDevel SyscallOpts = C.F_DENY_DEVEL FilterDenyDevel FilterOpts = C.F_DENY_DEVEL
// FlagMultiarch allows multiarch/emulation. // FilterMultiarch allows multiarch/emulation.
FlagMultiarch SyscallOpts = C.F_MULTIARCH FilterMultiarch FilterOpts = C.F_MULTIARCH
// FlagLinux32 sets PER_LINUX32. // FilterLinux32 sets PER_LINUX32.
FlagLinux32 SyscallOpts = C.F_LINUX32 FilterLinux32 FilterOpts = C.F_LINUX32
// FlagCan allows AF_CAN. // FilterCan allows AF_CAN.
FlagCan SyscallOpts = C.F_CAN FilterCan FilterOpts = C.F_CAN
// FlagBluetooth allows AF_BLUETOOTH. // FilterBluetooth allows AF_BLUETOOTH.
FlagBluetooth SyscallOpts = C.F_BLUETOOTH FilterBluetooth FilterOpts = C.F_BLUETOOTH
) )
func buildFilter(fd int, opts SyscallOpts) error { func buildFilter(fd int, opts FilterOpts) error {
var ( var (
arch C.uint32_t = 0 arch C.uint32_t = 0
multiarch C.uint32_t = 0 multiarch C.uint32_t = 0
@ -98,7 +100,7 @@ func buildFilter(fd int, opts SyscallOpts) error {
// this removes repeated transitions between C and Go execution // this removes repeated transitions between C and Go execution
// when producing log output via F_println and CPrintln is nil // when producing log output via F_println and CPrintln is nil
if fp := printlnP.Load(); fp != nil { if fp := printlnP.Load(); fp != nil {
opts |= flagVerbose opts |= filterVerbose
} }
var ret C.int var ret C.int

View File

@ -1,11 +1,17 @@
package sandbox package sandbox
import "syscall" import (
"syscall"
"unsafe"
)
const ( const (
O_PATH = 0x200000 O_PATH = 0x200000
PR_SET_NO_NEW_PRIVS = 0x26 PR_SET_NO_NEW_PRIVS = 0x26
CAP_SYS_ADMIN = 0x15
CAP_SYS_ADMIN = 0x15
CAP_SETPCAP = 0x8
) )
const ( const (
@ -15,13 +21,49 @@ const (
func SetDumpable(dumpable uintptr) error { func SetDumpable(dumpable uintptr) error {
// linux/sched/coredump.h // linux/sched/coredump.h
if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, dumpable, 0); errno != 0 { if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, dumpable, 0); errno != 0 {
return errno return errno
} }
return nil return nil
} }
const (
_LINUX_CAPABILITY_VERSION_3 = 0x20080522
PR_CAP_AMBIENT = 0x2f
PR_CAP_AMBIENT_RAISE = 0x2
PR_CAP_AMBIENT_CLEAR_ALL = 0x4
)
type (
capHeader struct {
version uint32
pid int32
}
capData struct {
effective uint32
permitted uint32
inheritable uint32
}
)
// See CAP_TO_INDEX in linux/capability.h:
func capToIndex(cap uintptr) uintptr { return cap >> 5 }
// See CAP_TO_MASK in linux/capability.h:
func capToMask(cap uintptr) uint32 { return 1 << uint(cap&31) }
func capset(hdrp *capHeader, datap *[2]capData) error {
if _, _, errno := syscall.Syscall(syscall.SYS_CAPSET,
uintptr(unsafe.Pointer(hdrp)),
uintptr(unsafe.Pointer(&datap[0])), 0); errno != 0 {
return errno
}
return nil
}
// IgnoringEINTR makes a function call and repeats it if it returns an // IgnoringEINTR makes a function call and repeats it if it returns an
// EINTR error. This appears to be required even though we install all // EINTR error. This appears to be required even though we install all
// signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846. // signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846.

47
sandbox/sysctl.go Normal file
View File

@ -0,0 +1,47 @@
package sandbox
import (
"bytes"
"log"
"os"
"strconv"
"sync"
)
var (
kernelOverflowuid int
kernelOverflowgid int
kernelCapLastCap int
sysctlOnce sync.Once
)
const (
kernelOverflowuidPath = "/proc/sys/kernel/overflowuid"
kernelOverflowgidPath = "/proc/sys/kernel/overflowgid"
kernelCapLastCapPath = "/proc/sys/kernel/cap_last_cap"
)
func mustReadSysctl() {
if v, err := os.ReadFile(kernelOverflowuidPath); err != nil {
log.Fatalf("cannot read %q: %v", kernelOverflowuidPath, err)
} else if kernelOverflowuid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
log.Fatalf("cannot interpret %q: %v", kernelOverflowuidPath, err)
}
if v, err := os.ReadFile(kernelOverflowgidPath); err != nil {
log.Fatalf("cannot read %q: %v", kernelOverflowgidPath, err)
} else if kernelOverflowgid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
log.Fatalf("cannot interpret %q: %v", kernelOverflowgidPath, err)
}
if v, err := os.ReadFile(kernelCapLastCapPath); err != nil {
log.Fatalf("cannot read %q: %v", kernelCapLastCapPath, err)
} else if kernelCapLastCap, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil {
log.Fatalf("cannot interpret %q: %v", kernelCapLastCapPath, err)
}
}
func OverflowUid() int { sysctlOnce.Do(mustReadSysctl); return kernelOverflowuid }
func OverflowGid() int { sysctlOnce.Do(mustReadSysctl); return kernelOverflowgid }
func LastCap() uintptr { sysctlOnce.Do(mustReadSysctl); return uintptr(kernelCapLastCap) }

View File

@ -4,12 +4,6 @@
config, config,
... ...
}: }:
let
testCases = import ./sandbox/case {
inherit (pkgs) lib callPackage foot;
inherit (config.environment.fortify.package) version;
};
in
{ {
users.users = { users.users = {
alice = { alice = {
@ -108,10 +102,6 @@ in
home-manager = _: _: { home.stateVersion = "23.05"; }; home-manager = _: _: { home.stateVersion = "23.05"; };
apps = [ apps = [
testCases.preset
testCases.tty
testCases.mapuid
{ {
name = "ne-foot"; name = "ne-foot";
verbose = true; verbose = true;

View File

@ -7,10 +7,15 @@ in the public sandbox/vfs package. Files in this package are excluded by the bui
package sandbox package sandbox
import ( import (
"crypto/sha512"
"encoding/hex"
"encoding/json" "encoding/json"
"errors"
"io/fs" "io/fs"
"log" "log"
"os" "os"
"syscall"
"time"
) )
var ( var (
@ -23,6 +28,7 @@ func printf(format string, v ...any) { printfFunc(format, v...) }
func fatalf(format string, v ...any) { fatalfFunc(format, v...) } func fatalf(format string, v ...any) { fatalfFunc(format, v...) }
type TestCase struct { type TestCase struct {
Env []string `json:"env"`
FS *FS `json:"fs"` FS *FS `json:"fs"`
Mount []*MountinfoEntry `json:"mount"` Mount []*MountinfoEntry `json:"mount"`
Seccomp bool `json:"seccomp"` Seccomp bool `json:"seccomp"`
@ -34,13 +40,46 @@ type T struct {
MountsPath string MountsPath string
} }
func (t *T) MustCheckFile(wantFilePath string) { func (t *T) MustCheckFile(wantFilePath, markerPath string) {
var want *TestCase var want *TestCase
mustDecode(wantFilePath, &want) mustDecode(wantFilePath, &want)
t.MustCheck(want) t.MustCheck(want)
if _, err := os.Create(markerPath); err != nil {
fatalf("cannot create success marker: %v", err)
}
} }
func (t *T) MustCheck(want *TestCase) { func (t *T) MustCheck(want *TestCase) {
if want.Env != nil {
var (
fail bool
i int
got string
)
for i, got = range os.Environ() {
if i == len(want.Env) {
fatalf("got more than %d environment variables", len(want.Env))
}
if got != want.Env[i] {
fail = true
printf("[FAIL] %s", got)
} else {
printf("[ OK ] %s", got)
}
}
i++
if i != len(want.Env) {
fatalf("got %d environment variables, want %d", i, len(want.Env))
}
if fail {
fatalf("[FAIL] some environment variables did not match")
}
} else {
printf("[SKIP] skipping environ check")
}
if want.FS != nil && t.FS != nil { if want.FS != nil && t.FS != nil {
if err := want.FS.Compare(".", t.FS); err != nil { if err := want.FS.Compare(".", t.FS); err != nil {
fatalf("%v", err) fatalf("%v", err)
@ -82,7 +121,7 @@ func (t *T) MustCheck(want *TestCase) {
} }
if want.Seccomp { if want.Seccomp {
if TrySyscalls() != nil { if trySyscalls() != nil {
os.Exit(1) os.Exit(1)
} }
} else { } else {
@ -90,6 +129,81 @@ func (t *T) MustCheck(want *TestCase) {
} }
} }
func MustCheckFilter(pid int, want string) {
err := CheckFilter(pid, want)
if err == nil {
return
}
var perr *ptraceError
if !errors.As(err, &perr) {
fatalf("%s", err)
}
switch perr.op {
case "PTRACE_ATTACH":
fatalf("cannot attach to process %d: %v", pid, err)
case "PTRACE_SECCOMP_GET_FILTER":
if perr.errno == syscall.ENOENT {
fatalf("seccomp filter not installed for process %d", pid)
}
fatalf("cannot get filter: %v", err)
default:
fatalf("cannot check filter: %v", err)
}
*(*int)(nil) = 0 // not reached
}
func CheckFilter(pid int, want string) error {
if err := ptraceAttach(pid); err != nil {
return err
}
defer func() {
if err := ptraceDetach(pid); err != nil {
printf("cannot detach from process %d: %v", pid, err)
}
}()
h := sha512.New()
{
getFilter:
buf, err := getFilter[[8]byte](pid, 0)
/* this is not how ESRCH should be handled: the manpage advises the
use of waitpid, however that is not applicable for attaching to an
arbitrary process, and spawning target process here is not easily
possible under the current testing framework;
despite checking for /proc/pid/status indicating state t (tracing stop),
it does not appear to be directly related to the internal state used to
determine whether a process is ready to accept ptrace operations, it also
introduces a TOCTOU that is irrelevant in the testing vm; this behaviour
is kept anyway as it reduces the average iterations required here;
since this code is only ever compiled into the test program, whatever
implications this ugliness might have should not hurt anyone */
if errors.Is(err, syscall.ESRCH) {
time.Sleep(100 * time.Millisecond)
goto getFilter
}
if err != nil {
return err
}
for _, b := range buf {
h.Write(b[:])
}
}
if got := hex.EncodeToString(h.Sum(nil)); got != want {
printf("[FAIL] %s", got)
return syscall.ENOTRECOVERABLE
} else {
printf("[ OK ] %s", got)
return nil
}
}
func mustDecode(wantFilePath string, v any) { func mustDecode(wantFilePath string, v any) {
if f, err := os.Open(wantFilePath); err != nil { if f, err := os.Open(wantFilePath); err != nil {
fatalf("cannot open %q: %v", wantFilePath, err) fatalf("cannot open %q: %v", wantFilePath, err)

View File

@ -1,30 +0,0 @@
{
writeText,
buildGoModule,
pkg-config,
util-linux,
version,
}:
buildGoModule {
pname = "check-sandbox";
inherit version;
src = ../.;
vendorHash = null;
buildInputs = [ util-linux ];
nativeBuildInputs = [ pkg-config ];
preBuild = ''
go mod init git.gensokyo.uk/security/fortify/test >& /dev/null
cp ${writeText "main.go" ''
package main
import "os"
import "git.gensokyo.uk/security/fortify/test/sandbox"
func main() { (&sandbox.T{FS: os.DirFS("/")}).MustCheckFile(os.Args[1]) }
''} main.go
'';
}

View File

@ -1,10 +1,4 @@
{ lib: testProgram:
lib,
callPackage,
foot,
version,
}:
let let
fs = mode: dir: data: { fs = mode: dir: data: {
mode = lib.fromHexString mode; mode = lib.fromHexString mode;
@ -29,8 +23,6 @@ let
; ;
}; };
checkSandbox = callPackage ../. { inherit version; };
callTestCase = callTestCase =
path: path:
let let
@ -45,14 +37,19 @@ let
{ {
name = "check-sandbox-${tc.name}"; name = "check-sandbox-${tc.name}";
verbose = true; verbose = true;
inherit (tc) tty mapRealUid; inherit (tc) tty device mapRealUid;
share = foot; share = testProgram;
packages = [ ]; packages = [ ];
command = builtins.toString (checkSandbox tc.name tc.want); path = "${testProgram}/bin/fortify-test";
args = [
"test"
(toString (builtins.toFile "fortify-${tc.name}-want.json" (builtins.toJSON tc.want)))
];
}; };
in in
{ {
preset = callTestCase ./preset.nix; preset = callTestCase ./preset.nix;
tty = callTestCase ./tty.nix; tty = callTestCase ./tty.nix;
mapuid = callTestCase ./mapuid.nix; mapuid = callTestCase ./mapuid.nix;
device = callTestCase ./device.nix;
} }

View File

@ -0,0 +1,203 @@
{
fs,
ent,
ignore,
}:
{
name = "device";
tty = false;
device = true;
mapRealUid = false;
want = {
env = [
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus"
"HOME=/var/lib/fortify/u0/a4"
"PULSE_SERVER=unix:/run/user/65534/pulse/native"
"SHELL=/run/current-system/sw/bin/bash"
"TERM=linux"
"USER=u0_a4"
"WAYLAND_DISPLAY=wayland-0"
"XDG_RUNTIME_DIR=/run/user/65534"
"XDG_SESSION_CLASS=user"
"XDG_SESSION_TYPE=tty"
];
fs = fs "dead" {
".fortify" = fs "800001ed" { } null;
bin = fs "800001ed" { sh = fs "80001ff" null null; } null;
dev = fs "800001ed" null null;
etc = fs "800001ed" {
".clean" = fs "80001ff" null null;
".host" = fs "800001c0" null null;
".updated" = fs "80001ff" null null;
"NIXOS" = fs "80001ff" null null;
"X11" = fs "80001ff" null null;
"alsa" = fs "80001ff" null null;
"bashrc" = fs "80001ff" null null;
"binfmt.d" = fs "80001ff" null null;
"dbus-1" = fs "80001ff" null null;
"default" = fs "80001ff" null null;
"dhcpcd.exit-hook" = fs "80001ff" null null;
"fonts" = fs "80001ff" null null;
"fstab" = fs "80001ff" null null;
"fsurc" = fs "80001ff" null null;
"fuse.conf" = fs "80001ff" null null;
"group" = fs "180" null "fortify:x:65534:\n";
"host.conf" = fs "80001ff" null null;
"hostname" = fs "80001ff" null null;
"hosts" = fs "80001ff" null null;
"inputrc" = fs "80001ff" null null;
"issue" = fs "80001ff" null null;
"kbd" = fs "80001ff" null null;
"locale.conf" = fs "80001ff" null null;
"login.defs" = fs "80001ff" null null;
"lsb-release" = fs "80001ff" null null;
"lvm" = fs "80001ff" null null;
"machine-id" = fs "80001ff" null null;
"man_db.conf" = fs "80001ff" null null;
"modprobe.d" = fs "80001ff" null null;
"modules-load.d" = fs "80001ff" null null;
"mtab" = fs "80001ff" null null;
"nanorc" = fs "80001ff" null null;
"netgroup" = fs "80001ff" null null;
"nix" = fs "80001ff" null null;
"nixos" = fs "80001ff" null null;
"nscd.conf" = fs "80001ff" null null;
"nsswitch.conf" = fs "80001ff" null null;
"os-release" = fs "80001ff" null null;
"pam" = fs "80001ff" null null;
"pam.d" = fs "80001ff" null null;
"passwd" = fs "180" null "u0_a4:x:65534:65534:Fortify:/var/lib/fortify/u0/a4:/run/current-system/sw/bin/bash\n";
"pipewire" = fs "80001ff" null null;
"pki" = fs "80001ff" null null;
"polkit-1" = fs "80001ff" null null;
"profile" = fs "80001ff" null null;
"protocols" = fs "80001ff" null null;
"resolv.conf" = fs "80001ff" null null;
"resolvconf.conf" = fs "80001ff" null null;
"rpc" = fs "80001ff" null null;
"services" = fs "80001ff" null null;
"set-environment" = fs "80001ff" null null;
"shadow" = fs "80001ff" null null;
"shells" = fs "80001ff" null null;
"ssh" = fs "80001ff" null null;
"ssl" = fs "80001ff" null null;
"static" = fs "80001ff" null null;
"subgid" = fs "80001ff" null null;
"subuid" = fs "80001ff" null null;
"sudoers" = fs "80001ff" null null;
"sway" = fs "80001ff" null null;
"sysctl.d" = fs "80001ff" null null;
"systemd" = fs "80001ff" null null;
"terminfo" = fs "80001ff" null null;
"tmpfiles.d" = fs "80001ff" null null;
"udev" = fs "80001ff" null null;
"vconsole.conf" = fs "80001ff" null null;
"xdg" = fs "80001ff" null null;
"zoneinfo" = fs "80001ff" null null;
} null;
nix = fs "800001c0" { store = fs "801001fd" null null; } null;
proc = fs "8000016d" null null;
run = fs "800001ed" {
current-system = fs "80001ff" null null;
opengl-driver = fs "80001ff" null null;
user = fs "800001ed" {
"65534" = fs "800001c0" {
bus = fs "10001fd" null null;
pulse = fs "800001c0" { native = fs "10001b6" null null; } null;
wayland-0 = fs "1000038" null null;
} null;
} null;
} null;
sys = fs "800001c0" {
block = fs "800001ed" {
fd0 = fs "80001ff" null null;
loop0 = fs "80001ff" null null;
loop1 = fs "80001ff" null null;
loop2 = fs "80001ff" null null;
loop3 = fs "80001ff" null null;
loop4 = fs "80001ff" null null;
loop5 = fs "80001ff" null null;
loop6 = fs "80001ff" null null;
loop7 = fs "80001ff" null null;
sr0 = fs "80001ff" null null;
vda = fs "80001ff" null null;
} null;
bus = fs "800001ed" null null;
class = fs "800001ed" null null;
dev = fs "800001ed" {
block = fs "800001ed" null null;
char = fs "800001ed" null null;
} null;
devices = fs "800001ed" null null;
} null;
tmp = fs "800001f8" { } null;
usr = fs "800001c0" { bin = fs "800001ed" { env = fs "80001ff" null null; } null; } null;
var = fs "800001c0" {
lib = fs "800001c0" {
fortify = fs "800001c0" {
u0 = fs "800001c0" {
a4 = fs "800001c0" {
".cache" = fs "800001ed" { ".keep" = fs "80001ff" null ""; } null;
".config" = fs "800001ed" { "environment.d" = fs "800001ed" { "10-home-manager.conf" = fs "80001ff" null null; } null; } null;
".local" = fs "800001ed" {
state = fs "800001ed" {
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;
nix = fs "800001ed" {
profiles = fs "800001ed" {
home-manager = fs "80001ff" null null;
home-manager-1-link = fs "80001ff" null null;
profile = fs "80001ff" null null;
profile-1-link = fs "80001ff" null null;
} null;
} null;
} null;
} null;
".nix-defexpr" = fs "800001ed" {
channels = fs "80001ff" null null;
channels_root = fs "80001ff" null null;
} null;
".nix-profile" = fs "80001ff" null null;
} null;
} null;
} null;
} null;
run = fs "800001ed" { nscd = fs "800001ed" { } null; } null;
} null;
} null;
mount = [
(ent "/sysroot" "/" "rw,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000004,gid=1000004")
(ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw")
(ent "/" "/.fortify" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000004,gid=1000004")
(ent "/" "/dev" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,gid=3,mode=620,ptmxmode=666")
(ent "/" "/dev/shm" "rw,nosuid,nodev" "tmpfs" "tmpfs" ignore)
(ent "/" ignore ignore ignore ignore ignore) # order not deterministic
(ent "/" ignore ignore ignore ignore ignore)
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
(ent "/block" "/sys/block" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/bus" "/sys/bus" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/class" "/sys/class" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000004,gid=1000004")
(ent "/" "/run/user/65534" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8192k,mode=700,uid=1000004,gid=1000004")
(ent "/tmp/fortify.1000/tmpdir/4" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/lib/fortify/u0/a4" "/var/lib/fortify/u0/a4" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000004,gid=1000004")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000004,gid=1000004")
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/var/run/nscd" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8k,mode=755,uid=1000004,gid=1000004")
];
seccomp = true;
};
}

View File

@ -6,13 +6,25 @@
{ {
name = "mapuid"; name = "mapuid";
tty = false; tty = false;
device = false;
mapRealUid = true; mapRealUid = true;
want = { want = {
env = [
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus"
"HOME=/var/lib/fortify/u0/a3"
"PULSE_SERVER=unix:/run/user/1000/pulse/native"
"SHELL=/run/current-system/sw/bin/bash"
"TERM=linux"
"USER=u0_a3"
"WAYLAND_DISPLAY=wayland-0"
"XDG_RUNTIME_DIR=/run/user/1000"
"XDG_SESSION_CLASS=user"
"XDG_SESSION_TYPE=tty"
];
fs = fs "dead" { fs = fs "dead" {
".fortify" = fs "800001ed" { ".fortify" = fs "800001ed" { } null;
etc = fs "800001ed" null null;
} null;
bin = fs "800001ed" { sh = fs "80001ff" null null; } null; bin = fs "800001ed" { sh = fs "80001ff" null null; } null;
dev = fs "800001ed" { dev = fs "800001ed" {
core = fs "80001ff" null null; core = fs "80001ff" null null;
@ -41,6 +53,7 @@
} null; } null;
etc = fs "800001ed" { etc = fs "800001ed" {
".clean" = fs "80001ff" null null; ".clean" = fs "80001ff" null null;
".host" = fs "800001c0" null null;
".updated" = fs "80001ff" null null; ".updated" = fs "80001ff" null null;
"NIXOS" = fs "80001ff" null null; "NIXOS" = fs "80001ff" null null;
"X11" = fs "80001ff" null null; "X11" = fs "80001ff" null null;
@ -84,7 +97,6 @@
"pki" = fs "80001ff" null null; "pki" = fs "80001ff" null null;
"polkit-1" = fs "80001ff" null null; "polkit-1" = fs "80001ff" null null;
"profile" = fs "80001ff" null null; "profile" = fs "80001ff" null null;
"profiles" = fs "80001ff" null null;
"protocols" = fs "80001ff" null null; "protocols" = fs "80001ff" null null;
"resolv.conf" = fs "80001ff" null null; "resolv.conf" = fs "80001ff" null null;
"resolvconf.conf" = fs "80001ff" null null; "resolvconf.conf" = fs "80001ff" null null;
@ -115,7 +127,7 @@
current-system = fs "80001ff" null null; current-system = fs "80001ff" null null;
opengl-driver = fs "80001ff" null null; opengl-driver = fs "80001ff" null null;
user = fs "800001ed" { user = fs "800001ed" {
"1000" = fs "800001ed" { "1000" = fs "800001c0" {
bus = fs "10001fd" null null; bus = fs "10001fd" null null;
pulse = fs "800001c0" { native = fs "10001b6" null null; } null; pulse = fs "800001c0" { native = fs "10001b6" null null; } null;
wayland-0 = fs "1000038" null null; wayland-0 = fs "1000038" null null;
@ -201,9 +213,9 @@
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") (ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") (ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) (ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/etc" "/.fortify/etc" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000003,gid=1000003") (ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000003,gid=1000003")
(ent "/" "/run/user/1000" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8192k,mode=755,uid=1000003,gid=1000003") (ent "/" "/run/user/1000" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8192k,mode=700,uid=1000003,gid=1000003")
(ent "/tmp/fortify.1000/tmpdir/3" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/tmp/fortify.1000/tmpdir/3" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/lib/fortify/u0/a3" "/var/lib/fortify/u0/a3" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/var/lib/fortify/u0/a3" "/var/lib/fortify/u0/a3" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000003,gid=1000003") (ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000003,gid=1000003")

View File

@ -6,13 +6,25 @@
{ {
name = "preset"; name = "preset";
tty = false; tty = false;
device = false;
mapRealUid = false; mapRealUid = false;
want = { want = {
env = [
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus"
"HOME=/var/lib/fortify/u0/a1"
"PULSE_SERVER=unix:/run/user/65534/pulse/native"
"SHELL=/run/current-system/sw/bin/bash"
"TERM=linux"
"USER=u0_a1"
"WAYLAND_DISPLAY=wayland-0"
"XDG_RUNTIME_DIR=/run/user/65534"
"XDG_SESSION_CLASS=user"
"XDG_SESSION_TYPE=tty"
];
fs = fs "dead" { fs = fs "dead" {
".fortify" = fs "800001ed" { ".fortify" = fs "800001ed" { } null;
etc = fs "800001ed" null null;
} null;
bin = fs "800001ed" { sh = fs "80001ff" null null; } null; bin = fs "800001ed" { sh = fs "80001ff" null null; } null;
dev = fs "800001ed" { dev = fs "800001ed" {
core = fs "80001ff" null null; core = fs "80001ff" null null;
@ -41,6 +53,7 @@
} null; } null;
etc = fs "800001ed" { etc = fs "800001ed" {
".clean" = fs "80001ff" null null; ".clean" = fs "80001ff" null null;
".host" = fs "800001c0" null null;
".updated" = fs "80001ff" null null; ".updated" = fs "80001ff" null null;
"NIXOS" = fs "80001ff" null null; "NIXOS" = fs "80001ff" null null;
"X11" = fs "80001ff" null null; "X11" = fs "80001ff" null null;
@ -84,7 +97,6 @@
"pki" = fs "80001ff" null null; "pki" = fs "80001ff" null null;
"polkit-1" = fs "80001ff" null null; "polkit-1" = fs "80001ff" null null;
"profile" = fs "80001ff" null null; "profile" = fs "80001ff" null null;
"profiles" = fs "80001ff" null null;
"protocols" = fs "80001ff" null null; "protocols" = fs "80001ff" null null;
"resolv.conf" = fs "80001ff" null null; "resolv.conf" = fs "80001ff" null null;
"resolvconf.conf" = fs "80001ff" null null; "resolvconf.conf" = fs "80001ff" null null;
@ -115,7 +127,7 @@
current-system = fs "80001ff" null null; current-system = fs "80001ff" null null;
opengl-driver = fs "80001ff" null null; opengl-driver = fs "80001ff" null null;
user = fs "800001ed" { user = fs "800001ed" {
"65534" = fs "800001ed" { "65534" = fs "800001c0" {
bus = fs "10001fd" null null; bus = fs "10001fd" null null;
pulse = fs "800001c0" { native = fs "10001b6" null null; } null; pulse = fs "800001c0" { native = fs "10001b6" null null; } null;
wayland-0 = fs "1000038" null null; wayland-0 = fs "1000038" null null;
@ -201,9 +213,9 @@
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") (ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") (ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) (ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/etc" "/.fortify/etc" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000001,gid=1000001") (ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000001,gid=1000001")
(ent "/" "/run/user/65534" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8192k,mode=755,uid=1000001,gid=1000001") (ent "/" "/run/user/65534" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8192k,mode=700,uid=1000001,gid=1000001")
(ent "/tmp/fortify.1000/tmpdir/1" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/tmp/fortify.1000/tmpdir/1" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/lib/fortify/u0/a1" "/var/lib/fortify/u0/a1" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/var/lib/fortify/u0/a1" "/var/lib/fortify/u0/a1" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000001,gid=1000001") (ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000001,gid=1000001")

View File

@ -6,13 +6,25 @@
{ {
name = "tty"; name = "tty";
tty = true; tty = true;
device = false;
mapRealUid = false; mapRealUid = false;
want = { want = {
env = [
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus"
"HOME=/var/lib/fortify/u0/a2"
"PULSE_SERVER=unix:/run/user/65534/pulse/native"
"SHELL=/run/current-system/sw/bin/bash"
"TERM=linux"
"USER=u0_a2"
"WAYLAND_DISPLAY=wayland-0"
"XDG_RUNTIME_DIR=/run/user/65534"
"XDG_SESSION_CLASS=user"
"XDG_SESSION_TYPE=tty"
];
fs = fs "dead" { fs = fs "dead" {
".fortify" = fs "800001ed" { ".fortify" = fs "800001ed" { } null;
etc = fs "800001ed" null null;
} null;
bin = fs "800001ed" { sh = fs "80001ff" null null; } null; bin = fs "800001ed" { sh = fs "80001ff" null null; } null;
dev = fs "800001ed" { dev = fs "800001ed" {
console = fs "4200190" null null; console = fs "4200190" null null;
@ -42,6 +54,7 @@
} null; } null;
etc = fs "800001ed" { etc = fs "800001ed" {
".clean" = fs "80001ff" null null; ".clean" = fs "80001ff" null null;
".host" = fs "800001c0" null null;
".updated" = fs "80001ff" null null; ".updated" = fs "80001ff" null null;
"NIXOS" = fs "80001ff" null null; "NIXOS" = fs "80001ff" null null;
"X11" = fs "80001ff" null null; "X11" = fs "80001ff" null null;
@ -85,7 +98,6 @@
"pki" = fs "80001ff" null null; "pki" = fs "80001ff" null null;
"polkit-1" = fs "80001ff" null null; "polkit-1" = fs "80001ff" null null;
"profile" = fs "80001ff" null null; "profile" = fs "80001ff" null null;
"profiles" = fs "80001ff" null null;
"protocols" = fs "80001ff" null null; "protocols" = fs "80001ff" null null;
"resolv.conf" = fs "80001ff" null null; "resolv.conf" = fs "80001ff" null null;
"resolvconf.conf" = fs "80001ff" null null; "resolvconf.conf" = fs "80001ff" null null;
@ -116,7 +128,7 @@
current-system = fs "80001ff" null null; current-system = fs "80001ff" null null;
opengl-driver = fs "80001ff" null null; opengl-driver = fs "80001ff" null null;
user = fs "800001ed" { user = fs "800001ed" {
"65534" = fs "800001ed" { "65534" = fs "800001c0" {
bus = fs "10001fd" null null; bus = fs "10001fd" null null;
pulse = fs "800001c0" { native = fs "10001b6" null null; } null; pulse = fs "800001c0" { native = fs "10001b6" null null; } null;
wayland-0 = fs "1000038" null null; wayland-0 = fs "1000038" null null;
@ -203,9 +215,9 @@
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") (ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") (ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) (ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/etc" "/.fortify/etc" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000002,gid=1000002") (ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000002,gid=1000002")
(ent "/" "/run/user/65534" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8192k,mode=755,uid=1000002,gid=1000002") (ent "/" "/run/user/65534" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8192k,mode=700,uid=1000002,gid=1000002")
(ent "/tmp/fortify.1000/tmpdir/2" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/tmp/fortify.1000/tmpdir/2" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/lib/fortify/u0/a2" "/var/lib/fortify/u0/a2" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw") (ent "/var/lib/fortify/u0/a2" "/var/lib/fortify/u0/a2" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000002,gid=1000002") (ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000002,gid=1000002")

View File

@ -0,0 +1,77 @@
{
lib,
pkgs,
config,
...
}:
let
testProgram = pkgs.callPackage ./tool/package.nix { inherit (config.environment.fortify.package) version; };
testCases = import ./case lib testProgram;
in
{
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 = {
systemPackages = with pkgs; [
# For checking seccomp outcome:
testProgram
];
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.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 performance:
"-smp 8"
];
environment.fortify = {
enable = true;
stateDir = "/var/lib/fortify";
users.alice = 0;
home-manager = _: _: { home.stateVersion = "23.05"; };
apps = with testCases; [
preset
tty
mapuid
device
];
};
}

View File

@ -1,14 +1,39 @@
{ {
writeShellScript, lib,
writeText, nixosTest,
callPackage,
version, self,
withRace ? false,
}: }:
name: want:
writeShellScript "fortify-${name}-check-sandbox-script" '' nixosTest {
set -e name = "fortify-sandbox" + (if withRace then "-race" else "");
${callPackage ./assert.nix { inherit version; }}/bin/test \ nodes.machine =
${writeText "fortify-${name}-want.json" (builtins.toJSON want)} { options, pkgs, ... }:
touch /tmp/sandbox-ok {
'' # Run with Go race detector:
environment.fortify = lib.mkIf withRace rec {
# race detector does not support static linking
package = (pkgs.callPackage ../../package.nix { }).overrideAttrs (previousAttrs: {
GOFLAGS = previousAttrs.GOFLAGS ++ [ "-race" ];
});
fsuPackage = options.environment.fortify.fsuPackage.default.override { fortify = package; };
};
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;
}

119
test/sandbox/ptrace.go Normal file
View File

@ -0,0 +1,119 @@
package sandbox
import (
"bufio"
"fmt"
"io"
"os"
"strings"
"syscall"
"time"
"unsafe"
)
const (
NULL = 0
PTRACE_ATTACH = 16
PTRACE_DETACH = 17
PTRACE_SECCOMP_GET_FILTER = 0x420c
)
type ptraceError struct {
op string
errno syscall.Errno
}
func (p *ptraceError) Error() string { return fmt.Sprintf("%s: %v", p.op, p.errno) }
func (p *ptraceError) Unwrap() error {
if p.errno == 0 {
return nil
}
return p.errno
}
func ptrace(op uintptr, pid, addr int, data unsafe.Pointer) (r uintptr, errno syscall.Errno) {
r, _, errno = syscall.Syscall6(syscall.SYS_PTRACE, op, uintptr(pid), uintptr(addr), uintptr(data), NULL, NULL)
return
}
func ptraceAttach(pid int) error {
const (
statePrefix = "State:"
stateSuffix = "t (tracing stop)"
)
var r io.ReadSeekCloser
if f, err := os.Open(fmt.Sprintf("/proc/%d/status", pid)); err != nil {
return err
} else {
r = f
}
if _, errno := ptrace(PTRACE_ATTACH, pid, 0, nil); errno != 0 {
return &ptraceError{"PTRACE_ATTACH", errno}
}
// ugly! but there does not appear to be another way
for {
time.Sleep(10 * time.Millisecond)
if _, err := r.Seek(0, io.SeekStart); err != nil {
return err
}
s := bufio.NewScanner(r)
var found bool
for s.Scan() {
found = strings.HasPrefix(s.Text(), statePrefix)
if found {
break
}
}
if err := s.Err(); err != nil {
return err
}
if !found {
return syscall.EBADE
}
if strings.HasSuffix(s.Text(), stateSuffix) {
break
}
}
return nil
}
func ptraceDetach(pid int) error {
if _, errno := ptrace(PTRACE_DETACH, pid, 0, nil); errno != 0 {
return &ptraceError{"PTRACE_DETACH", errno}
}
return nil
}
type sockFilter struct { /* Filter block */
code uint16 /* Actual filter code */
jt uint8 /* Jump true */
jf uint8 /* Jump false */
k uint32 /* Generic multiuse field */
}
func getFilter[T comparable](pid, index int) ([]T, error) {
if s := unsafe.Sizeof(*new(T)); s != 8 {
panic(fmt.Sprintf("invalid filter block size %d", s))
}
var buf []T
if n, errno := ptrace(PTRACE_SECCOMP_GET_FILTER, pid, index, nil); errno != 0 {
return nil, &ptraceError{"PTRACE_SECCOMP_GET_FILTER", errno}
} else {
buf = make([]T, n)
}
if _, errno := ptrace(PTRACE_SECCOMP_GET_FILTER, pid, index, unsafe.Pointer(&buf[0])); errno != 0 {
return nil, &ptraceError{"PTRACE_SECCOMP_GET_FILTER", errno}
}
return buf, nil
}

View File

@ -10,9 +10,7 @@ import (
*/ */
import "C" import "C"
const NULL = 0 func trySyscalls() error {
func TrySyscalls() error {
testCases := []struct { testCases := []struct {
name string name string
errno syscall.Errno errno syscall.Errno

72
test/sandbox/test.py Normal file
View File

@ -0,0 +1,72 @@
import json
import shlex
q = shlex.quote
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
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")
# Check seccomp outcome:
swaymsg("exec fortify run cat")
pid = int(machine.wait_until_succeeds("pgrep -U 1000000 -x cat", timeout=5))
print(machine.succeed(f"fortify-test filter {pid} c698b081ff957afe17a6d94374537d37f2a63f6f9dd75da7546542407a9e32476ebda3312ba7785d7f618542bcfaf27ca27dcc2dddba852069d28bcfe8cad39a &>/dev/stdout", timeout=5))
machine.succeed(f"kill -TERM {pid}")
# Verify capabilities/securebits in user namespace:
print(machine.succeed("sudo -u alice -i fortify run capsh --print"))
print(machine.succeed("sudo -u alice -i fortify run capsh --has-no-new-privs"))
print(machine.fail("sudo -u alice -i fortify run capsh --has-a=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i fortify run capsh --has-b=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i fortify run capsh --has-i=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i fortify run capsh --has-p=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i fortify run umount -R /dev"))
# Check sandbox outcome:
check_offset = 0
def check_sandbox(name):
global check_offset
check_offset += 1
swaymsg(f"exec script /dev/null -E always -qec check-sandbox-{name}")
machine.wait_for_file(f"/tmp/fortify.1000/tmpdir/{check_offset}/sandbox-ok", timeout=15)
check_sandbox("preset")
check_sandbox("tty")
check_sandbox("mapuid")
check_sandbox("device")
# 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"))

39
test/sandbox/tool/main.go Normal file
View File

@ -0,0 +1,39 @@
package main
import (
"log"
"os"
"strconv"
"strings"
"git.gensokyo.uk/security/fortify/test/sandbox"
)
func main() {
log.SetFlags(0)
log.SetPrefix("test: ")
if len(os.Args) < 2 {
log.Fatal("invalid argument")
}
switch os.Args[1] {
case "filter":
if len(os.Args) != 4 {
log.Fatal("invalid argument")
}
if pid, err := strconv.Atoi(strings.TrimSpace(os.Args[2])); err != nil {
log.Fatalf("%s", err)
} else if pid < 1 {
log.Fatalf("%d out of range", pid)
} else {
sandbox.MustCheckFilter(pid, os.Args[3])
return
}
default:
(&sandbox.T{FS: os.DirFS("/")}).MustCheckFile(os.Args[1], "/tmp/sandbox-ok")
return
}
}

View File

@ -0,0 +1,30 @@
{
lib,
buildGoModule,
pkg-config,
util-linux,
version,
}:
buildGoModule rec {
pname = "check-sandbox";
inherit version;
src = builtins.path {
name = "${pname}-src";
path = lib.cleanSource ../.;
filter = path: type: (type == "directory") || (type == "regular" && lib.hasSuffix ".go" path);
};
vendorHash = null;
buildInputs = [ util-linux ];
nativeBuildInputs = [ pkg-config ];
preBuild = ''
go mod init git.gensokyo.uk/security/fortify/test/sandbox >& /dev/null
'';
postInstall = ''
mv $out/bin/tool $out/bin/fortify-test
'';
}

View File

@ -69,8 +69,8 @@ def check_state(name, enablements):
if len(config['args']) != 1 or config['args'][0] != command: if len(config['args']) != 1 or config['args'][0] != command:
raise Exception(f"unexpected args {config['args']}") raise Exception(f"unexpected args {config['args']}")
if config['confinement']['enablements'] != enablements: if config['enablements'] != enablements:
raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}") raise Exception(f"unexpected enablements {instance['config']['enablements']}")
def fortify(command): def fortify(command):
@ -105,19 +105,9 @@ if denyOutput != "fsu: uid 1001 is not in the fsurc file\n":
if denyOutputVerbose != "fsu: uid 1001 is not in the fsurc file\nfortify: *cannot obtain uid from fsu: permission denied\n": if denyOutputVerbose != "fsu: uid 1001 is not in the fsurc file\nfortify: *cannot obtain uid from fsu: permission denied\n":
raise Exception(f"unexpected deny verbose output:\n{denyOutputVerbose}") raise Exception(f"unexpected deny verbose output:\n{denyOutputVerbose}")
# Check sandbox outcome:
check_offset = 0 check_offset = 0
def check_sandbox(name):
global check_offset
check_offset += 1
swaymsg(f"exec script /dev/null -E always -qec check-sandbox-{name}")
machine.wait_for_file(f"/tmp/fortify.1000/tmpdir/{check_offset}/sandbox-ok", timeout=15)
check_sandbox("preset")
check_sandbox("tty")
check_sandbox("mapuid")
def aid(offset): def aid(offset):
return 1+check_offset+offset return 1+check_offset+offset
@ -179,6 +169,16 @@ machine.send_chars("exit\n")
machine.wait_for_file("/tmp/p0-exit-ok", timeout=15) machine.wait_for_file("/tmp/p0-exit-ok", timeout=15)
machine.fail("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000000") machine.fail("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000000")
# Check interrupt shim behaviour:
swaymsg("exec sh -c 'ne-foot; echo -n $? > /tmp/monitor-exit-code'")
wait_for_window(f"u0_a{aid(0)}@machine")
machine.succeed("pkill -INT -f 'fortify -v app '")
machine.wait_until_fails("pgrep foot", timeout=5)
machine.wait_for_file("/tmp/monitor-exit-code")
interrupt_exit_code = int(machine.succeed("cat /tmp/monitor-exit-code"))
if interrupt_exit_code != 254:
raise Exception(f"unexpected exit code {interrupt_exit_code}")
# Start app (foot) with Wayland enablement: # Start app (foot) with Wayland enablement:
swaymsg("exec ne-foot") swaymsg("exec ne-foot")
wait_for_window(f"u0_a{aid(0)}@machine") wait_for_window(f"u0_a{aid(0)}@machine")
@ -186,25 +186,11 @@ machine.send_chars("clear; wayland-info && touch /tmp/client-ok\n")
machine.wait_for_file(tmpdir_path(0, "client-ok"), timeout=15) machine.wait_for_file(tmpdir_path(0, "client-ok"), timeout=15)
collect_state_ui("foot_wayland") collect_state_ui("foot_wayland")
check_state("ne-foot", 1) check_state("ne-foot", 1)
# Verify acl on XDG_RUNTIME_DIR: # Verify lack of acl on XDG_RUNTIME_DIR:
print(machine.succeed(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {aid(0) + 1000000}")) machine.fail(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {aid(0) + 1000000}")
machine.send_chars("exit\n") machine.send_chars("exit\n")
machine.wait_until_fails("pgrep foot", timeout=5) machine.wait_until_fails("pgrep foot", timeout=5)
# Verify acl cleanup on XDG_RUNTIME_DIR: machine.fail(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {aid(0) + 1000000}", timeout=5)
machine.wait_until_fails(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {aid(0) + 1000000}", timeout=5)
# Start app (foot) with Wayland enablement from a terminal:
swaymsg("exec foot $SHELL -c '(ne-foot) & sleep 1 && fortify show $(fortify ps --short) && touch /tmp/ps-show-ok && cat'")
wait_for_window(f"u0_a{aid(0)}@machine")
machine.send_chars("clear; wayland-info && touch /tmp/term-ok\n")
machine.wait_for_file(tmpdir_path(0, "term-ok"), timeout=15)
machine.wait_for_file("/tmp/ps-show-ok", timeout=5)
collect_state_ui("foot_wayland_term")
check_state("ne-foot", 1)
machine.send_chars("exit\n")
wait_for_window("foot")
machine.send_key("ctrl-c")
machine.wait_until_fails("pgrep foot", timeout=5)
# Test PulseAudio (fortify does not support PipeWire yet): # Test PulseAudio (fortify does not support PipeWire yet):
swaymsg("exec pa-foot") swaymsg("exec pa-foot")
@ -243,6 +229,22 @@ machine.wait_until_fails(f"getfacl --absolute-names --omit-header --numeric /run
# Test syscall filter: # Test syscall filter:
print(machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 strace-failure")) print(machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 strace-failure"))
# Start app (foot) with Wayland enablement from a terminal:
swaymsg("exec foot $SHELL -c '(ne-foot) & disown && exec $SHELL'")
wait_for_window(f"u0_a{aid(0)}@machine")
machine.send_chars("clear; wayland-info && touch /tmp/term-ok\n")
machine.wait_for_file(tmpdir_path(0, "term-ok"), timeout=15)
machine.send_key("alt-h")
machine.send_chars("clear; fortify show $(fortify ps --short) && touch /tmp/ps-show-ok && exec cat\n")
machine.wait_for_file("/tmp/ps-show-ok", timeout=5)
collect_state_ui("foot_wayland_term")
check_state("ne-foot", 1)
machine.send_key("alt-l")
machine.send_chars("exit\n")
wait_for_window("alice@machine")
machine.send_key("ctrl-c")
machine.wait_until_fails("pgrep foot", timeout=5)
# Exit Sway and verify process exit status 0: # Exit Sway and verify process exit status 0:
swaymsg("exit", succeed=False) swaymsg("exit", succeed=False)
machine.wait_for_file("/tmp/sway-exit-ok") machine.wait_for_file("/tmp/sway-exit-ok")