98 Commits

Author SHA1 Message Date
cb513bb1cd release: 0.1.2
All checks were successful
Release / Create release (push) Successful in 41s
Test / Sandbox (push) Successful in 40s
Test / Hakurei (push) Successful in 2m37s
Test / Create distribution (push) Successful in 24s
Test / Sandbox (race detector) (push) Successful in 3m29s
Test / Planterette (push) Successful in 3m5s
Test / Hakurei (race detector) (push) Successful in 2m27s
Test / Flake checks (push) Successful in 1m19s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-29 03:11:33 +09:00
f7bd28118c hst: configurable wait delay
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m58s
Test / Hakurei (push) Successful in 2m47s
Test / Sandbox (race detector) (push) Successful in 3m56s
Test / Planterette (push) Successful in 3m58s
Test / Hakurei (race detector) (push) Successful in 4m31s
Test / Flake checks (push) Successful in 1m17s
This is useful for programs that take a long time to clean up.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-29 03:06:49 +09:00
940ee00ffe container/init: configurable lingering process wait delay
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m57s
Test / Hakurei (push) Successful in 2m50s
Test / Planterette (push) Successful in 3m39s
Test / Sandbox (race detector) (push) Successful in 3m43s
Test / Hakurei (race detector) (push) Successful in 4m33s
Test / Flake checks (push) Successful in 1m16s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-29 02:38:17 +09:00
b43d104680 app: integrate interrupt forwarding
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m58s
Test / Hakurei (push) Successful in 2m53s
Test / Sandbox (race detector) (push) Successful in 3m53s
Test / Planterette (push) Successful in 3m53s
Test / Hakurei (race detector) (push) Successful in 4m31s
Test / Flake checks (push) Successful in 1m19s
This significantly increases usability of command line tools running through hakurei.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-29 02:23:06 +09:00
ddf48a6c22 app/shim: implement signal handler outcome in Go
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m53s
Test / Hakurei (push) Successful in 2m48s
Test / Planterette (push) Successful in 3m48s
Test / Sandbox (race detector) (push) Successful in 3m56s
Test / Hakurei (race detector) (push) Successful in 4m27s
Test / Flake checks (push) Successful in 1m13s
This needs to be done from the Go side eventually anyway to integrate the signal forwarding behaviour now supported by the container package.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-28 23:39:30 +09:00
a0f499e30a app/shim: separate signal handler implementation
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m57s
Test / Planterette (push) Successful in 3m44s
Test / Sandbox (race detector) (push) Successful in 3m50s
Test / Hakurei (race detector) (push) Successful in 4m25s
Test / Hakurei (push) Successful in 2m0s
Test / Flake checks (push) Successful in 1m19s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-28 21:52:53 +09:00
d6b07f12ff container: forward context cancellation
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m56s
Test / Hakurei (push) Successful in 2m47s
Test / Planterette (push) Successful in 3m40s
Test / Sandbox (race detector) (push) Successful in 3m45s
Test / Hakurei (race detector) (push) Successful in 4m29s
Test / Flake checks (push) Successful in 1m18s
This allows container processes to exit gracefully.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-28 01:45:38 +09:00
65fe09caf9 container: check cancel signal delivery
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m55s
Test / Hakurei (push) Successful in 2m50s
Test / Sandbox (race detector) (push) Successful in 3m46s
Test / Planterette (push) Successful in 3m52s
Test / Hakurei (race detector) (push) Successful in 4m28s
Test / Flake checks (push) Successful in 1m18s
This change also makes some parts of the test more robust.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-28 01:04:29 +09:00
a1e5f020f4 container: improve doc comments
All checks were successful
Test / Create distribution (push) Successful in 31s
Test / Sandbox (push) Successful in 2m3s
Test / Hakurei (push) Successful in 2m53s
Test / Sandbox (race detector) (push) Successful in 3m43s
Test / Planterette (push) Successful in 3m57s
Test / Hakurei (race detector) (push) Successful in 4m23s
Test / Flake checks (push) Successful in 1m10s
Putting them on the builder methods is more useful.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-27 12:27:42 +09:00
bd3fa53a55 container: access test case by index in helper
All checks were successful
Test / Create distribution (push) Successful in 24s
Test / Hakurei (push) Successful in 40s
Test / Sandbox (push) Successful in 38s
Test / Hakurei (race detector) (push) Successful in 41s
Test / Sandbox (race detector) (push) Successful in 38s
Test / Planterette (push) Successful in 39s
Test / Flake checks (push) Successful in 1m17s
This is more elegant and allows for much easier extension of the tests. Mountinfo is still serialised however due to libPaths nondeterminism.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-26 18:59:19 +09:00
625632c593 nix: update flake lock
All checks were successful
Test / Create distribution (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 50s
Test / Sandbox (push) Successful in 52s
Test / Planterette (push) Successful in 50s
Test / Hakurei (race detector) (push) Successful in 57s
Test / Hakurei (push) Successful in 59s
Test / Flake checks (push) Successful in 1m53s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-26 18:57:54 +09:00
e71ae3b8c5 container: remove custom cmd initialisation
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Hakurei (push) Successful in 45s
Test / Sandbox (push) Successful in 43s
Test / Hakurei (race detector) (push) Successful in 45s
Test / Sandbox (race detector) (push) Successful in 43s
Test / Planterette (push) Successful in 43s
Test / Flake checks (push) Successful in 1m27s
This part of the interface is very unintuitive and only used for testing, even in testing it is inelegant and can be done better.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-25 00:45:10 +09:00
9d7a19d162 container: use more reliable nonexistence
All checks were successful
Test / Create distribution (push) Successful in 45s
Test / Sandbox (push) Successful in 2m21s
Test / Hakurei (push) Successful in 3m8s
Test / Planterette (push) Successful in 3m55s
Test / Sandbox (race detector) (push) Successful in 4m6s
Test / Hakurei (race detector) (push) Successful in 4m41s
Test / Flake checks (push) Successful in 1m18s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-18 23:18:26 +09:00
6ba19a7ba5 release: 0.1.1
All checks were successful
Release / Create release (push) Successful in 41s
Test / Hakurei (push) Successful in 49s
Test / Sandbox (push) Successful in 40s
Test / Create distribution (push) Successful in 24s
Test / Planterette (push) Successful in 3m13s
Test / Sandbox (race detector) (push) Successful in 3m46s
Test / Hakurei (race detector) (push) Successful in 2m18s
Test / Flake checks (push) Successful in 1m21s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-09 05:42:31 +09:00
749a2779f5 test/sandbox: add arm64 constants
All checks were successful
Test / Create distribution (push) Successful in 24s
Test / Sandbox (push) Successful in 40s
Test / Hakurei (push) Successful in 42s
Test / Hakurei (race detector) (push) Successful in 42s
Test / Sandbox (race detector) (push) Successful in 38s
Test / Planterette (push) Successful in 40s
Test / Flake checks (push) Successful in 1m30s
Most of these are differences in qemu.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-09 05:36:35 +09:00
e574042d76 test/sandbox: verify seccomp on all test cases
All checks were successful
Test / Hakurei (push) Successful in 42s
Test / Sandbox (push) Successful in 39s
Test / Hakurei (race detector) (push) Successful in 41s
Test / Create distribution (push) Successful in 33s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Planterette (push) Successful in 41s
Test / Flake checks (push) Successful in 1m17s
This change also makes seccomp hashes cross-platform.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-09 04:21:35 +09:00
2b44493e8a test/sandbox: guard on testtool tag
All checks were successful
Test / Hakurei (push) Successful in 40s
Test / Create distribution (push) Successful in 31s
Test / Hakurei (race detector) (push) Successful in 41s
Test / Planterette (push) Successful in 40s
Test / Sandbox (push) Successful in 1m30s
Test / Sandbox (race detector) (push) Successful in 1m43s
Test / Flake checks (push) Successful in 1m11s
This tool should not show up when building hakurei normally.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-07 20:11:29 +09:00
c30dd4e630 test/sandbox/seccomp: remove uselib
All checks were successful
Test / Hakurei (push) Successful in 41s
Test / Create distribution (push) Successful in 32s
Test / Hakurei (race detector) (push) Successful in 41s
Test / Sandbox (push) Successful in 1m27s
Test / Sandbox (race detector) (push) Successful in 1m44s
Test / Flake checks (push) Successful in 1m12s
Test / Planterette (push) Successful in 40s
This syscall is not wired on all platforms. This test barely does anything anyway and seccomp is covered by the privileged test instrumentation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-07 15:28:55 +09:00
d90da1c8f5 container/seccomp: add arm64 constants
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m2s
Test / Hakurei (push) Successful in 2m52s
Test / Sandbox (race detector) (push) Successful in 3m9s
Test / Planterette (push) Successful in 3m40s
Test / Hakurei (race detector) (push) Successful in 4m28s
Test / Flake checks (push) Successful in 1m12s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-07 14:58:03 +09:00
5853d7700f container/seccomp: move bpf hashes
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m56s
Test / Sandbox (race detector) (push) Successful in 3m7s
Test / Planterette (push) Successful in 3m35s
Test / Hakurei (race detector) (push) Successful in 4m23s
Test / Hakurei (push) Successful in 2m7s
Test / Flake checks (push) Successful in 1m19s
Filter programs are different across platforms. This representation is also much more readable.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-07 14:41:47 +09:00
d5c7523726 container/init: fix prctl call
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m50s
Test / Hakurei (push) Successful in 2m43s
Test / Sandbox (race detector) (push) Successful in 3m11s
Test / Planterette (push) Successful in 3m35s
Test / Hakurei (race detector) (push) Successful in 4m21s
Test / Flake checks (push) Successful in 1m8s
This is a very silly typo. Luckily has no effect due to an upper layer doing PR_SET_NO_NEW_PRIVS already.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-07 14:06:14 +09:00
ddfcc51b91 container: move capset implementation
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m46s
Test / Hakurei (push) Successful in 2m50s
Test / Sandbox (race detector) (push) Successful in 3m4s
Test / Planterette (push) Successful in 3m35s
Test / Hakurei (race detector) (push) Successful in 4m21s
Test / Flake checks (push) Successful in 1m10s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-07 13:47:13 +09:00
8ebedbd88a container: move syscall constants
All checks were successful
Test / Create distribution (push) Successful in 31s
Test / Sandbox (push) Successful in 1m55s
Test / Hakurei (push) Successful in 2m45s
Test / Sandbox (race detector) (push) Successful in 3m6s
Test / Planterette (push) Successful in 3m33s
Test / Hakurei (race detector) (push) Successful in 4m20s
Test / Flake checks (push) Successful in 1m10s
These aren't missing from all targets.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-07 13:23:01 +09:00
84e8142a2d container/seccomp: move personality constants
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m50s
Test / Hakurei (push) Successful in 2m45s
Test / Sandbox (race detector) (push) Successful in 3m5s
Test / Planterette (push) Successful in 3m37s
Test / Hakurei (race detector) (push) Successful in 4m22s
Test / Flake checks (push) Successful in 1m8s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-07 12:44:32 +09:00
2c7b7ad845 container/seccomp: cross-platform sysnum cutoff
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m54s
Test / Hakurei (push) Successful in 2m47s
Test / Sandbox (race detector) (push) Successful in 3m5s
Test / Planterette (push) Successful in 3m30s
Test / Hakurei (race detector) (push) Successful in 4m20s
Test / Flake checks (push) Successful in 1m10s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-07 12:27:00 +09:00
72c2b66fc0 nix: cross-platform syscall wrapper
All checks were successful
Test / Create distribution (push) Successful in 42s
Test / Sandbox (push) Successful in 54s
Test / Sandbox (race detector) (push) Successful in 52s
Test / Planterette (push) Successful in 51s
Test / Hakurei (push) Successful in 1m1s
Test / Hakurei (race detector) (push) Successful in 59s
Test / Flake checks (push) Successful in 1m6s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-07 04:22:55 +09:00
356b42a406 container/init: use /proc/self as intermediate
All checks were successful
Test / Create distribution (push) Successful in 43s
Test / Sandbox (push) Successful in 2m19s
Test / Sandbox (race detector) (push) Successful in 4m11s
Test / Hakurei (race detector) (push) Successful in 5m28s
Test / Hakurei (push) Successful in 2m10s
Test / Planterette (push) Successful in 38m44s
Test / Flake checks (push) Successful in 3m0s
Setting up via /tmp is okay, /proc/self/fd makes a lot more sense though for reasons described in the comment.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-06 02:14:35 +09:00
d9b6d48e7c add miscellaneous badges
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m51s
Test / Hakurei (push) Successful in 2m45s
Test / Sandbox (race detector) (push) Successful in 3m11s
Test / Planterette (push) Successful in 3m37s
Test / Hakurei (race detector) (push) Successful in 4m20s
Test / Flake checks (push) Successful in 1m9s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-03 18:04:09 +09:00
087959e81b app: remove split implementation
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m56s
Test / Hakurei (push) Successful in 2m42s
Test / Sandbox (race detector) (push) Successful in 3m5s
Test / Planterette (push) Successful in 3m37s
Test / Hakurei (race detector) (push) Successful in 4m19s
Test / Flake checks (push) Successful in 1m7s
It is completely nonsensical and highly error-prone to have multiple implementations of this in the same build. This should be switched at compile time instead therefore the split packages are pointless.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-03 04:36:59 +09:00
e6967b8bbb release: 0.1.0
All checks were successful
Release / Create release (push) Successful in 39s
Test / Sandbox (push) Successful in 39s
Test / Hakurei (push) Successful in 1m9s
Test / Sandbox (race detector) (push) Successful in 2m58s
Test / Create distribution (push) Successful in 24s
Test / Planterette (push) Successful in 3m48s
Test / Hakurei (race detector) (push) Successful in 4m6s
Test / Flake checks (push) Successful in 1m15s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-03 03:42:58 +09:00
d2f9a9b83b treewide: migrate to hakurei.app
All checks were successful
Test / Create distribution (push) Successful in 24s
Test / Sandbox (push) Successful in 46s
Test / Hakurei (push) Successful in 2m9s
Test / Sandbox (race detector) (push) Successful in 3m14s
Test / Planterette (push) Successful in 3m41s
Test / Hakurei (race detector) (push) Successful in 3m40s
Test / Flake checks (push) Successful in 1m18s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-03 03:30:39 +09:00
1b5ecd9eaf container: move out of toplevel
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m52s
Test / Sandbox (race detector) (push) Successful in 3m14s
Test / Planterette (push) Successful in 3m36s
Test / Hakurei (race detector) (push) Successful in 4m31s
Test / Hakurei (push) Successful in 2m3s
Test / Flake checks (push) Successful in 1m13s
This allows slightly easier use of the vanity url. This also provides some disambiguation between low level containers and hakurei app containers.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-03 02:59:43 +09:00
82561d62b6 system: move system access packages
All checks were successful
Test / Create distribution (push) Successful in 31s
Test / Sandbox (push) Successful in 1m52s
Test / Hakurei (push) Successful in 3m3s
Test / Planterette (push) Successful in 3m38s
Test / Hakurei (race detector) (push) Successful in 4m48s
Test / Sandbox (race detector) (push) Successful in 1m14s
Test / Flake checks (push) Successful in 1m6s
These packages loosely belong in the "system" package and "system" provides high level wrappers for all of them.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 21:52:07 +09:00
eec021cc4b hakurei: move container helpers toplevel
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m1s
Test / Hakurei (push) Successful in 2m52s
Test / Sandbox (race detector) (push) Successful in 3m8s
Test / Planterette (push) Successful in 3m32s
Test / Hakurei (race detector) (push) Successful in 4m27s
Test / Flake checks (push) Successful in 1m9s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 21:31:29 +09:00
a1d98823f8 hakurei: move container toplevel
All checks were successful
Test / Create distribution (push) Successful in 31s
Test / Sandbox (push) Successful in 1m55s
Test / Hakurei (push) Successful in 2m47s
Test / Sandbox (race detector) (push) Successful in 3m16s
Test / Planterette (push) Successful in 3m32s
Test / Hakurei (race detector) (push) Successful in 4m25s
Test / Flake checks (push) Successful in 1m9s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 21:23:55 +09:00
255b77d91d cmd/hakurei: move command handlers
All checks were successful
Test / Create distribution (push) Successful in 31s
Test / Sandbox (push) Successful in 1m55s
Test / Hakurei (push) Successful in 2m49s
Test / Sandbox (race detector) (push) Successful in 3m8s
Test / Planterette (push) Successful in 3m32s
Test / Hakurei (race detector) (push) Successful in 4m31s
Test / Flake checks (push) Successful in 1m6s
The hakurei command is a bit ugly since it's also used for validating the command package. This alleviates some of the ugliness.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 20:59:17 +09:00
f84ec5a3f8 sandbox/wl: track generated files
All checks were successful
Test / Create distribution (push) Successful in 31s
Test / Sandbox (push) Successful in 1m54s
Test / Hakurei (push) Successful in 2m58s
Test / Sandbox (race detector) (push) Successful in 3m16s
Test / Planterette (push) Successful in 3m36s
Test / Hakurei (race detector) (push) Successful in 4m31s
Test / Flake checks (push) Successful in 1m9s
This allows the package to be imported.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 20:52:22 +09:00
eb22a8bcc1 cmd/hakurei: move to cmd
All checks were successful
Test / Create distribution (push) Successful in 31s
Test / Sandbox (push) Successful in 1m50s
Test / Hakurei (push) Successful in 3m2s
Test / Sandbox (race detector) (push) Successful in 3m18s
Test / Planterette (push) Successful in 3m36s
Test / Hakurei (race detector) (push) Successful in 4m35s
Test / Flake checks (push) Successful in 1m7s
Having it at the project root never made sense since the "ego" name was deprecated. This change finally addresses it.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 20:42:51 +09:00
31aef905fa sandbox: expose seccomp interface
All checks were successful
Test / Create distribution (push) Successful in 31s
Test / Sandbox (push) Successful in 1m59s
Test / Hakurei (push) Successful in 2m47s
Test / Sandbox (race detector) (push) Successful in 3m11s
Test / Planterette (push) Successful in 3m34s
Test / Hakurei (race detector) (push) Successful in 4m22s
Test / Flake checks (push) Successful in 1m8s
There's no point in artificially limiting and abstracting away these options. The higher level hakurei package is responsible for providing a secure baseline and sane defaults. The sandbox package should present everything to the caller.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 04:47:13 +09:00
a6887f7253 sandbox/seccomp: import dot for syscall
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m55s
Test / Sandbox (race detector) (push) Successful in 3m7s
Test / Planterette (push) Successful in 3m31s
Test / Hakurei (race detector) (push) Successful in 4m19s
Test / Hakurei (push) Successful in 1m57s
Test / Flake checks (push) Successful in 1m11s
This significantly increases readability in some places.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 02:30:35 +09:00
69bd581af7 sandbox/seccomp: append suffix to ops
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m50s
Test / Hakurei (push) Successful in 2m54s
Test / Sandbox (race detector) (push) Successful in 3m9s
Test / Planterette (push) Successful in 4m5s
Test / Hakurei (race detector) (push) Successful in 4m44s
Test / Flake checks (push) Successful in 1m31s
This avoids clashes with stdlib names to allow for . imports.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 01:09:04 +09:00
26b7afc890 sandbox/seccomp: prepare -> export
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m51s
Test / Sandbox (race detector) (push) Successful in 3m3s
Test / Planterette (push) Successful in 3m37s
Test / Hakurei (race detector) (push) Successful in 4m17s
Test / Hakurei (push) Successful in 2m12s
Test / Flake checks (push) Successful in 1m12s
Export makes a lot more sense, and also matches the libseccomp function.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 00:32:48 +09:00
d5532aade0 sandbox/seccomp: native rule slice in helpers
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m6s
Test / Hakurei (push) Successful in 2m49s
Test / Sandbox (race detector) (push) Successful in 3m8s
Test / Planterette (push) Successful in 3m33s
Test / Hakurei (race detector) (push) Successful in 4m16s
Test / Flake checks (push) Successful in 1m16s
These helper functions took FilterPreset as input for ease of integration. This moves them to []NativeRule.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 00:22:27 +09:00
0c5409aec7 sandbox/seccomp: native rule type alias
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m57s
Test / Hakurei (push) Successful in 2m49s
Test / Sandbox (race detector) (push) Successful in 3m4s
Test / Planterette (push) Successful in 3m39s
Test / Hakurei (race detector) (push) Successful in 4m20s
Test / Flake checks (push) Successful in 1m13s
This makes it easier to keep API stable.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-02 00:00:08 +09:00
1a8840bebc sandbox/seccomp: resolve rules natively
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m45s
Test / Hakurei (push) Successful in 2m49s
Test / Sandbox (race detector) (push) Successful in 3m1s
Test / Planterette (push) Successful in 3m31s
Test / Hakurei (race detector) (push) Successful in 4m18s
Test / Flake checks (push) Successful in 1m6s
This enables loading syscall filter policies from external cross-platform config files.

This also removes a significant amount of C code.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-01 22:11:32 +09:00
1fb453dffe sandbox/seccomp: extra constants
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m59s
Test / Hakurei (push) Successful in 2m44s
Test / Sandbox (race detector) (push) Successful in 3m1s
Test / Planterette (push) Successful in 3m33s
Test / Hakurei (race detector) (push) Successful in 4m20s
Test / Flake checks (push) Successful in 1m7s
These all resolve to pseudo syscall numbers in libseccomp, but are necessary anyway for other platforms.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-01 20:15:42 +09:00
e03d702d08 sandbox/seccomp: implement syscall lookup
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m51s
Test / Hakurei (push) Successful in 2m52s
Test / Sandbox (race detector) (push) Successful in 3m20s
Test / Planterette (push) Successful in 3m40s
Test / Hakurei (race detector) (push) Successful in 4m18s
Test / Flake checks (push) Successful in 1m10s
This uses the Go map and is verified against libseccomp.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-01 00:35:27 +09:00
241dc964a6 sandbox/seccomp: wire extra syscall
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m46s
Test / Hakurei (push) Successful in 2m48s
Test / Sandbox (race detector) (push) Successful in 3m6s
Test / Planterette (push) Successful in 40s
Test / Hakurei (race detector) (push) Successful in 2m39s
Test / Flake checks (push) Successful in 1m15s
These values are only useful for libseccomp.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-07-01 00:32:08 +09:00
8ef71e14d5 sandbox/seccomp: emit syscall constants
All checks were successful
Test / Create distribution (push) Successful in 44s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m8s
Test / Sandbox (race detector) (push) Successful in 3m18s
Test / Planterette (push) Successful in 3m55s
Test / Hakurei (race detector) (push) Successful in 4m37s
Test / Flake checks (push) Successful in 1m9s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-06-30 20:34:33 +09:00
972f4006f0 treewide: switch to hakurei.app
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m0s
Test / Hakurei (push) Successful in 2m49s
Test / Sandbox (race detector) (push) Successful in 3m12s
Test / Planterette (push) Successful in 3m35s
Test / Hakurei (race detector) (push) Successful in 4m22s
Test / Flake checks (push) Successful in 1m7s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-06-26 04:01:02 +09:00
9a8a047908 sandbox/seccomp: syscall name lookup table
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m58s
Test / Hakurei (push) Successful in 2m42s
Test / Sandbox (race detector) (push) Successful in 2m59s
Test / Planterette (push) Successful in 3m31s
Test / Hakurei (race detector) (push) Successful in 4m21s
Test / Flake checks (push) Successful in 1m9s
The script is from Go source of same name. The result is checked against libseccomp.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-06-26 03:49:07 +09:00
863bf69ad3 treewide: reapply clang-format
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m51s
Test / Hakurei (push) Successful in 2m49s
Test / Sandbox (race detector) (push) Successful in 2m58s
Test / Planterette (push) Successful in 3m37s
Test / Hakurei (race detector) (push) Successful in 4m15s
Test / Flake checks (push) Successful in 1m8s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-06-25 23:43:42 +09:00
0e957cc9c1 release: 0.0.2
All checks were successful
Release / Create release (push) Successful in 43s
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 40s
Test / Hakurei (push) Successful in 45s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Planterette (push) Successful in 1m41s
Test / Hakurei (race detector) (push) Successful in 1m44s
Test / Flake checks (push) Successful in 1m14s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-06-25 21:11:11 +09:00
aa454b158f cmd/planterette: remove hsu special case
All checks were successful
Test / Hakurei (push) Successful in 42s
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 40s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Sandbox (race detector) (push) Successful in 38s
Test / Planterette (push) Successful in 40s
Test / Flake checks (push) Successful in 1m15s
Remove special case and invoke hakurei out of process.

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

Still better than nothing though.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-31 04:41:08 +09:00
bdee0c3921 nix: update flake lock
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 5m6s
Test / Sandbox (race detector) (push) Successful in 5m12s
Test / Fortify (push) Successful in 6m5s
Test / Fortify (race detector) (push) Successful in 6m39s
Test / Fpkg (push) Successful in 9m53s
Test / Flake checks (push) Successful in 1m20s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-30 23:15:18 +09:00
227 changed files with 9457 additions and 6252 deletions

View File

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

View File

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

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

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

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

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

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

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

6
.gitignore vendored
View File

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

View File

@@ -1,4 +1,4 @@
Copyright (c) 2024 Ophestra Umiker
Copyright (c) 2024-2025 Ophestra
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

109
README.md
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,8 +2,6 @@ package main
import (
"context"
_ "embed"
"errors"
"fmt"
"io"
"log"
@@ -15,67 +13,28 @@ import (
"syscall"
"time"
"git.gensokyo.uk/security/fortify/command"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/state"
"git.gensokyo.uk/security/fortify/internal/sys"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system"
"hakurei.app/command"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/app"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/system"
"hakurei.app/system/dbus"
)
var (
errSuccess = errors.New("success")
//go:embed LICENSE
license string
)
func init() { fmsg.Prepare("fortify") }
var std sys.State = new(sys.Std)
func main() {
// early init path, skips root check and duplicate PR_SET_DUMPABLE
sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg)
if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
// not fatal: this program runs as the privileged user
}
if os.Geteuid() == 0 {
log.Fatal("this program must not run as root")
}
buildCommand(os.Stderr).MustParse(os.Args[1:], func(err error) {
fmsg.Verbosef("command returned %v", err)
if errors.Is(err, errSuccess) {
fmsg.BeforeExit()
os.Exit(0)
}
})
log.Fatal("unreachable")
}
func buildCommand(out io.Writer) command.Command {
var (
flagVerbose bool
flagJSON bool
)
c := command.New(out, log.Printf, "fortify", func([]string) error {
internal.InstallFmsg(flagVerbose)
return nil
}).
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output as JSON when applicable")
c := command.New(out, log.Printf, "hakurei", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")
c.Command("shim", command.UsageInternal, func([]string) error { app.ShimMain(); return errSuccess })
c.Command("app", "Launch app defined by the specified config file", func(args []string) error {
c.Command("app", "Load app from configuration file", func(args []string) error {
if len(args) < 1 {
log.Fatal("app requires at least 1 argument")
}
@@ -106,7 +65,7 @@ func buildCommand(out io.Writer) command.Command {
c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error {
// initialise config from flags
config := &fst.Config{
config := &hst.Config{
ID: fid,
Args: args,
}
@@ -122,19 +81,19 @@ func buildCommand(out io.Writer) command.Command {
passwdFunc = func() {
var us string
if uid, err := std.Uid(aid); err != nil {
fmsg.PrintBaseError(err, "cannot obtain uid from fsu:")
hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:")
os.Exit(1)
} else {
us = strconv.Itoa(uid)
}
if u, err := user.LookupId(us); err != nil {
fmsg.Verbosef("cannot look up uid %s", us)
hlog.Verbosef("cannot look up uid %s", us)
passwd = &user.User{
Uid: us,
Gid: us,
Username: "chronos",
Name: "Fortify",
Name: "Hakurei Permissive Default",
HomeDir: "/var/empty",
}
} else {
@@ -153,33 +112,33 @@ func buildCommand(out io.Writer) command.Command {
userName = passwd.Username
}
config.Confinement.AppID = aid
config.Confinement.Groups = groups
config.Confinement.Outer = homeDir
config.Confinement.Username = userName
config.Identity = aid
config.Groups = groups
config.Data = homeDir
config.Username = userName
if wayland {
config.Confinement.Enablements |= system.EWayland
config.Enablements |= system.EWayland
}
if x11 {
config.Confinement.Enablements |= system.EX11
config.Enablements |= system.EX11
}
if dBus {
config.Confinement.Enablements |= system.EDBus
config.Enablements |= system.EDBus
}
if pulse {
config.Confinement.Enablements |= system.EPulse
config.Enablements |= system.EPulse
}
// parse D-Bus config file from flags if applicable
if dBus {
if dbusConfigSession == "builtin" {
config.Confinement.SessionBus = dbus.NewConfig(fid, true, mpris)
config.SessionBus = dbus.NewConfig(fid, true, mpris)
} else {
if conf, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
log.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err)
} else {
config.Confinement.SessionBus = conf
config.SessionBus = conf
}
}
@@ -188,14 +147,14 @@ func buildCommand(out io.Writer) command.Command {
if conf, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil {
log.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err)
} else {
config.Confinement.SystemBus = conf
config.SystemBus = conf
}
}
// override log from configuration
if dbusVerbose {
config.Confinement.SessionBus.Log = true
config.Confinement.SystemBus.Log = true
config.SessionBus.Log = true
config.SystemBus.Log = true
}
}
@@ -204,46 +163,46 @@ func buildCommand(out io.Writer) command.Command {
panic("unreachable")
}).
Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"),
"Path to D-Bus proxy config file, or \"builtin\" for defaults").
"Path to session bus proxy config file, or \"builtin\" for defaults").
Flag(&dbusConfigSystem, "dbus-system", command.StringFlag("nil"),
"Path to system D-Bus proxy config file, or \"nil\" to disable").
"Path to system bus proxy config file, or \"nil\" to disable").
Flag(&mpris, "mpris", command.BoolFlag(false),
"Allow owning MPRIS D-Bus path, has no effect if custom config is available").
Flag(&dbusVerbose, "dbus-log", command.BoolFlag(false),
"Force logging in the D-Bus proxy").
"Force buffered logging in the D-Bus proxy").
Flag(&fid, "id", command.StringFlag(""),
"App ID, leave empty to disable security context app_id").
"Reverse-DNS style Application identifier, leave empty to inherit instance identifier").
Flag(&aid, "a", command.IntFlag(0),
"Fortify application ID").
"Application identity").
Flag(nil, "g", &groups,
"Groups inherited by the app process").
"Groups inherited by all container processes").
Flag(&homeDir, "d", command.StringFlag("os"),
"Application home directory").
"Container home directory").
Flag(&userName, "u", command.StringFlag("chronos"),
"Passwd name within sandbox").
"Passwd user name within sandbox").
Flag(&wayland, "wayland", command.BoolFlag(false),
"Allow Wayland connections").
"Enable connection to Wayland via security-context-v1").
Flag(&x11, "X", command.BoolFlag(false),
"Share X11 socket and allow connection").
"Enable direct connection to X11").
Flag(&dBus, "dbus", command.BoolFlag(false),
"Proxy D-Bus connection").
"Enable proxied connection to D-Bus").
Flag(&pulse, "pulse", command.BoolFlag(false),
"Share PulseAudio socket and cookie")
"Enable direct connection to PulseAudio")
}
var showFlagShort bool
c.NewCommand("show", "Show the contents of an app configuration", func(args []string) error {
c.NewCommand("show", "Show live or local app configuration", func(args []string) error {
switch len(args) {
case 0: // system
printShowSystem(os.Stdout, showFlagShort, flagJSON)
case 1: // instance
name := args[0]
config, instance := tryShort(name)
config, entry := tryShort(name)
if config == nil {
config = tryPath(name)
}
printShowInstance(os.Stdout, time.Now().UTC(), instance, config, showFlagShort, flagJSON)
printShowInstance(os.Stdout, time.Now().UTC(), entry, config, showFlagShort, flagJSON)
default:
log.Fatal("show requires 1 argument")
@@ -252,12 +211,12 @@ func buildCommand(out io.Writer) command.Command {
}).Flag(&showFlagShort, "short", command.BoolFlag(false), "Omit filesystem information")
var psFlagShort bool
c.NewCommand("ps", "List active apps and their state", func(args []string) error {
c.NewCommand("ps", "List active instances", func(args []string) error {
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(std.Paths().RunDirPath), psFlagShort, flagJSON)
return errSuccess
}).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id")
c.Command("version", "Show fortify version", func(args []string) error {
c.Command("version", "Display version information", func(args []string) error {
fmt.Println(internal.Version())
return errSuccess
})
@@ -268,7 +227,7 @@ func buildCommand(out io.Writer) command.Command {
})
c.Command("template", "Produce a config template", func(args []string) error {
printJSON(os.Stdout, false, fst.Template())
printJSON(os.Stdout, false, hst.Template())
return errSuccess
})
@@ -280,19 +239,19 @@ func buildCommand(out io.Writer) command.Command {
return c
}
func runApp(config *fst.Config) {
func runApp(config *hst.Config) {
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer stop() // unreachable
a := app.MustNew(ctx, std)
rs := new(fst.RunState)
rs := new(app.RunState)
if sa, err := a.Seal(config); err != nil {
fmsg.PrintBaseError(err, "cannot seal app:")
rs.ExitCode = 1
hlog.PrintBaseError(err, "cannot seal app:")
internal.Exit(1)
} else {
// this updates ExitCode
app.PrintRunStateErr(rs, sa.Run(rs))
internal.Exit(app.PrintRunStateErr(rs, sa.Run(rs)))
}
internal.Exit(rs.ExitCode)
*(*int)(nil) = 0 // not reached
}

View File

@@ -6,7 +6,7 @@ import (
"flag"
"testing"
"git.gensokyo.uk/security/fortify/command"
"hakurei.app/command"
)
func TestHelp(t *testing.T) {
@@ -17,14 +17,14 @@ func TestHelp(t *testing.T) {
}{
{
"main", []string{}, `
Usage: fortify [-h | --help] [-v] [--json] COMMAND [OPTIONS]
Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS]
Commands:
app Launch app defined by the specified config file
app Load app from configuration file
run Configure and start a permissive default sandbox
show Show the contents of an app configuration
ps List active apps and their state
version Show fortify version
show Show live or local app configuration
ps List active instances
version Display version information
license Show full license text
template Produce a config template
help Show this help message
@@ -33,34 +33,34 @@ Commands:
},
{
"run", []string{"run", "-h"}, `
Usage: fortify run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--wayland] [-X] [--dbus] [--pulse] COMMAND [OPTIONS]
Usage: hakurei run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--wayland] [-X] [--dbus] [--pulse] COMMAND [OPTIONS]
Flags:
-X Share X11 socket and allow connection
-X Enable direct connection to X11
-a int
Fortify application ID
Application identity
-d string
Application home directory (default "os")
Container home directory (default "os")
-dbus
Proxy D-Bus connection
Enable proxied connection to D-Bus
-dbus-config string
Path to D-Bus proxy config file, or "builtin" for defaults (default "builtin")
Path to session bus proxy config file, or "builtin" for defaults (default "builtin")
-dbus-log
Force logging in the D-Bus proxy
Force buffered logging in the D-Bus proxy
-dbus-system string
Path to system D-Bus proxy config file, or "nil" to disable (default "nil")
Path to system bus proxy config file, or "nil" to disable (default "nil")
-g value
Groups inherited by the app process
Groups inherited by all container processes
-id string
App ID, leave empty to disable security context app_id
Reverse-DNS style Application identifier, leave empty to inherit instance identifier
-mpris
Allow owning MPRIS D-Bus path, has no effect if custom config is available
-pulse
Share PulseAudio socket and cookie
Enable direct connection to PulseAudio
-u string
Passwd name within sandbox (default "chronos")
Passwd user name within sandbox (default "chronos")
-wayland
Allow Wayland connections
Enable connection to Wayland via security-context-v1
`,
},

51
cmd/hakurei/main.go Normal file
View File

@@ -0,0 +1,51 @@
package main
// this works around go:embed '..' limitation
//go:generate cp ../../LICENSE .
import (
_ "embed"
"errors"
"log"
"os"
"hakurei.app/container"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/internal/sys"
)
var (
errSuccess = errors.New("success")
//go:embed LICENSE
license string
)
func init() { hlog.Prepare("hakurei") }
var std sys.State = new(sys.Std)
func main() {
// early init path, skips root check and duplicate PR_SET_DUMPABLE
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
if err := container.SetDumpable(container.SUID_DUMP_DISABLE); err != nil {
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
// not fatal: this program runs as the privileged user
}
if os.Geteuid() == 0 {
log.Fatal("this program must not run as root")
}
buildCommand(os.Stderr).MustParse(os.Args[1:], func(err error) {
hlog.Verbosef("command returned %v", err)
if errors.Is(err, errSuccess) {
hlog.BeforeExit()
os.Exit(0)
}
// this catches faulty command handlers that fail to return before this point
})
log.Fatal("unreachable")
}

View File

@@ -10,19 +10,19 @@ import (
"strings"
"syscall"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/state"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
)
func tryPath(name string) (config *fst.Config) {
func tryPath(name string) (config *hst.Config) {
var r io.Reader
config = new(fst.Config)
config = new(hst.Config)
if name != "-" {
r = tryFd(name)
if r == nil {
fmsg.Verbose("load configuration from file")
hlog.Verbose("load configuration from file")
if f, err := os.Open(name); err != nil {
log.Fatalf("cannot access configuration file %q: %s", name, err)
@@ -51,11 +51,11 @@ func tryPath(name string) (config *fst.Config) {
func tryFd(name string) io.ReadCloser {
if v, err := strconv.Atoi(name); err != nil {
if !errors.Is(err, strconv.ErrSyntax) {
fmsg.Verbosef("name cannot be interpreted as int64: %v", err)
hlog.Verbosef("name cannot be interpreted as int64: %v", err)
}
return nil
} else {
fmsg.Verbosef("trying config stream from %d", v)
hlog.Verbosef("trying config stream from %d", v)
fd := uintptr(v)
if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 {
if errors.Is(errno, syscall.EBADF) {
@@ -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 *hst.Config, entry *state.State) {
likePrefix := false
if len(name) <= 32 {
likePrefix = true
@@ -85,7 +85,7 @@ func tryShort(name string) (config *fst.Config, instance *state.State) {
// try to match from state store
if likePrefix && len(name) >= 8 {
fmsg.Verbose("argument looks like prefix")
hlog.Verbose("argument looks like prefix")
s := state.NewMulti(std.Paths().RunDirPath)
if entries, err := state.Join(s); err != nil {
@@ -96,12 +96,12 @@ func tryShort(name string) (config *fst.Config, instance *state.State) {
v := id.String()
if strings.HasPrefix(v, name) {
// match, use config from this state entry
instance = entries[id]
config = instance.Config
entry = entries[id]
config = entry.Config
break
}
fmsg.Verbosef("instance %s skipped", v)
hlog.Verbosef("instance %s skipped", v)
}
}
}

View File

@@ -12,21 +12,21 @@ import (
"text/tabwriter"
"time"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/state"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/internal/hlog"
"hakurei.app/system/dbus"
)
func printShowSystem(output io.Writer, short, flagJSON bool) {
t := newPrinter(output)
defer t.MustFlush()
info := new(fst.Info)
info := new(hst.Info)
// get fid by querying uid of aid 0
if uid, err := std.Uid(0); err != nil {
fmsg.PrintBaseError(err, "cannot obtain uid from fsu:")
hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:")
os.Exit(1)
} else {
info.User = (uid / 10000) - 100
@@ -42,7 +42,7 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
func printShowInstance(
output io.Writer, now time.Time,
instance *state.State, config *fst.Config,
instance *state.State, config *hst.Config,
short, flagJSON bool) {
if flagJSON {
if instance != nil {
@@ -56,7 +56,7 @@ func printShowInstance(
t := newPrinter(output)
defer t.MustFlush()
if config.Confinement.Sandbox == nil {
if config.Container == nil {
mustPrint(output, "Warning: this configuration uses permissive defaults!\n\n")
}
@@ -69,19 +69,21 @@ func printShowInstance(
t.Printf("App\n")
if config.ID != "" {
t.Printf(" ID:\t%d (%s)\n", config.Confinement.AppID, config.ID)
t.Printf(" Identity:\t%d (%s)\n", config.Identity, config.ID)
} else {
t.Printf(" ID:\t%d\n", config.Confinement.AppID)
t.Printf(" Identity:\t%d\n", config.Identity)
}
t.Printf(" Enablements:\t%s\n", config.Confinement.Enablements.String())
if len(config.Confinement.Groups) > 0 {
t.Printf(" Groups:\t%q\n", config.Confinement.Groups)
t.Printf(" Enablements:\t%s\n", config.Enablements.String())
if len(config.Groups) > 0 {
t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", "))
}
t.Printf(" Directory:\t%s\n", config.Confinement.Outer)
if config.Confinement.Sandbox != nil {
sandbox := config.Confinement.Sandbox
if sandbox.Hostname != "" {
t.Printf(" Hostname:\t%q\n", sandbox.Hostname)
if config.Data != "" {
t.Printf(" Data:\t%s\n", config.Data)
}
if config.Container != nil {
container := config.Container
if container.Hostname != "" {
t.Printf(" Hostname:\t%s\n", container.Hostname)
}
flags := make([]string, 0, 7)
writeFlag := func(name string, value bool) {
@@ -89,38 +91,40 @@ func printShowInstance(
flags = append(flags, name)
}
}
writeFlag("userns", sandbox.Userns)
writeFlag("net", sandbox.Net)
writeFlag("dev", sandbox.Dev)
writeFlag("tty", sandbox.Tty)
writeFlag("mapuid", sandbox.MapRealUID)
writeFlag("directwl", sandbox.DirectWayland)
writeFlag("autoetc", sandbox.AutoEtc)
writeFlag("userns", container.Userns)
writeFlag("devel", container.Devel)
writeFlag("net", container.Net)
writeFlag("device", container.Device)
writeFlag("tty", container.Tty)
writeFlag("mapuid", container.MapRealUID)
writeFlag("directwl", config.DirectWayland)
writeFlag("autoetc", container.AutoEtc)
if len(flags) == 0 {
flags = append(flags, "none")
}
t.Printf(" Flags:\t%s\n", strings.Join(flags, " "))
etc := sandbox.Etc
etc := container.Etc
if etc == "" {
etc = "/etc"
}
t.Printf(" Etc:\t%s\n", etc)
if len(sandbox.Cover) > 0 {
t.Printf(" Cover:\t%s\n", strings.Join(sandbox.Cover, " "))
if len(container.Cover) > 0 {
t.Printf(" Cover:\t%s\n", strings.Join(container.Cover, " "))
}
// Env map[string]string `json:"env"`
// Link [][2]string `json:"symlink"`
t.Printf(" Path:\t%s\n", config.Path)
}
if len(config.Args) > 0 {
t.Printf(" Arguments:\t%s\n", strings.Join(config.Args, " "))
}
t.Printf(" Command:\t%s\n", strings.Join(config.Args, " "))
t.Printf("\n")
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")
for _, f := range config.Confinement.Sandbox.Filesystem {
for _, f := range config.Container.Filesystem {
if f == nil {
continue
}
@@ -148,9 +152,9 @@ func printShowInstance(
}
t.Printf("\n")
}
if len(config.Confinement.ExtraPerms) > 0 {
if len(config.ExtraPerms) > 0 {
t.Printf("Extra ACL\n")
for _, p := range config.Confinement.ExtraPerms {
for _, p := range config.ExtraPerms {
if p == nil {
continue
}
@@ -178,14 +182,14 @@ func printShowInstance(
t.Printf(" Broadcast:\t%q\n", c.Broadcast)
}
}
if config.Confinement.SessionBus != nil {
if config.SessionBus != nil {
t.Printf("Session bus\n")
printDBus(config.Confinement.SessionBus)
printDBus(config.SessionBus)
t.Printf("\n")
}
if config.Confinement.SystemBus != nil {
if config.SystemBus != nil {
t.Printf("System bus\n")
printDBus(config.Confinement.SystemBus)
printDBus(config.SystemBus)
t.Printf("\n")
}
}
@@ -247,22 +251,26 @@ func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON boo
t := newPrinter(output)
defer t.MustFlush()
t.Println("\tInstance\tPID\tApp\tUptime\tEnablements\tCommand")
t.Println("\tInstance\tPID\tApplication\tUptime")
for _, e := range exp {
var (
es = "(No confinement information)"
cs = "(No command information)"
as = "(No configuration information)"
)
if e.Config != nil {
es = e.Config.Confinement.Enablements.String()
cs = fmt.Sprintf("%q", e.Config.Args)
as = strconv.Itoa(e.Config.Confinement.AppID)
if len(e.s) != 1<<5 {
// unreachable
log.Printf("possible store corruption: invalid instance string %s", e.s)
continue
}
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 = "app.hakurei." + 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 {

View File

@@ -5,13 +5,13 @@ import (
"testing"
"time"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/state"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/system/dbus"
)
var (
testID = fst.ID{
testID = state.ID{
0x8e, 0x2c, 0x76, 0xb0,
0x66, 0xda, 0xbe, 0x57,
0x4c, 0xf0, 0x73, 0xbd,
@@ -20,7 +20,7 @@ var (
testState = &state.State{
ID: testID,
PID: 0xDEADBEEF,
Config: fst.Template(),
Config: hst.Template(),
Time: testAppTime,
}
testTime = time.Unix(3752, 1).UTC()
@@ -31,32 +31,33 @@ func Test_printShowInstance(t *testing.T) {
testCases := []struct {
name string
instance *state.State
config *fst.Config
config *hst.Config
short, json bool
want string
}{
{"config", nil, fst.Template(), false, false, `App
ID: 9 (org.chromium.Chromium)
{"config", nil, hst.Template(), false, false, `App
Identity: 9 (org.chromium.Chromium)
Enablements: wayland, dbus, pulseaudio
Groups: ["video"]
Directory: /var/lib/persist/home/org.chromium.Chromium
Hostname: "localhost"
Flags: userns net dev tty mapuid autoetc
Groups: video, dialout, plugdev
Data: /var/lib/hakurei/u0/org.chromium.Chromium
Hostname: localhost
Flags: userns devel net device tty mapuid autoetc
Etc: /etc
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
+/nix/store
+/run/current-system
+/run/opengl-driver
+/var/db/nix-channels
w*/var/lib/fortify/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
w*/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
d+/dev/dri
Extra ACL
--x+:/var/lib/fortify/u0
rwx:/var/lib/fortify/u0/org.chromium.Chromium
--x+:/var/lib/hakurei/u0
rwx:/var/lib/hakurei/u0/org.chromium.Chromium
Session bus
Filter: true
@@ -70,44 +71,38 @@ System bus
Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"]
`},
{"config pd", nil, new(fst.Config), false, false, `Warning: this configuration uses permissive defaults!
{"config pd", nil, new(hst.Config), false, false, `Warning: this configuration uses permissive defaults!
App
ID: 0
Identity: 0
Enablements: (no enablements)
Directory:
Command:
`},
{"config flag none", nil, &fst.Config{Confinement: fst.ConfinementConfig{Sandbox: new(fst.SandboxConfig)}}, false, false, `App
ID: 0
{"config flag none", nil, &hst.Config{Container: new(hst.ContainerConfig)}, false, false, `App
Identity: 0
Enablements: (no enablements)
Directory:
Flags: none
Etc: /etc
Command:
Path:
`},
{"config nil entries", nil, &fst.Config{Confinement: fst.ConfinementConfig{Sandbox: &fst.SandboxConfig{Filesystem: make([]*fst.FilesystemConfig, 1)}, ExtraPerms: make([]*fst.ExtraPermConfig, 1)}}, false, false, `App
ID: 0
{"config nil entries", nil, &hst.Config{Container: &hst.ContainerConfig{Filesystem: make([]*hst.FilesystemConfig, 1)}, ExtraPerms: make([]*hst.ExtraPermConfig, 1)}, false, false, `App
Identity: 0
Enablements: (no enablements)
Directory:
Flags: none
Etc: /etc
Command:
Path:
Filesystem
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, &hst.Config{SessionBus: &dbus.Config{See: []string{"org.example.test"}}}, false, false, `Warning: this configuration uses permissive defaults!
App
ID: 0
Identity: 0
Enablements: (no enablements)
Directory:
Command:
Session bus
Filter: false
@@ -115,32 +110,33 @@ Session bus
`},
{"instance", testState, fst.Template(), false, false, `State
{"instance", testState, hst.Template(), false, false, `State
Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (3735928559)
Uptime: 1h2m32s
App
ID: 9 (org.chromium.Chromium)
Identity: 9 (org.chromium.Chromium)
Enablements: wayland, dbus, pulseaudio
Groups: ["video"]
Directory: /var/lib/persist/home/org.chromium.Chromium
Hostname: "localhost"
Flags: userns net dev tty mapuid autoetc
Groups: video, dialout, plugdev
Data: /var/lib/hakurei/u0/org.chromium.Chromium
Hostname: localhost
Flags: userns devel net device tty mapuid autoetc
Etc: /etc
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
+/nix/store
+/run/current-system
+/run/opengl-driver
+/var/db/nix-channels
w*/var/lib/fortify/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
w*/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
d+/dev/dri
Extra ACL
--x+:/var/lib/fortify/u0
rwx:/var/lib/fortify/u0/org.chromium.Chromium
--x+:/var/lib/hakurei/u0
rwx:/var/lib/hakurei/u0/org.chromium.Chromium
Session bus
Filter: true
@@ -154,17 +150,15 @@ System bus
Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"]
`},
{"instance pd", testState, new(fst.Config), false, false, `Warning: this configuration uses permissive defaults!
{"instance pd", testState, new(hst.Config), false, false, `Warning: this configuration uses permissive defaults!
State
Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (3735928559)
Uptime: 1h2m32s
App
ID: 0
Identity: 0
Enablements: (no enablements)
Directory:
Command:
`},
@@ -200,214 +194,7 @@ App
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"confinement": {
"app_id": 9,
"groups": [
"video"
],
"username": "chronos",
"home_inner": "/var/lib/fortify",
"home": "/var/lib/persist/home/org.chromium.Chromium",
"sandbox": {
"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": {
"see": null,
"talk": [
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower"
],
"own": null,
"call": null,
"broadcast": null,
"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"
}
`},
{"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"
],
"confinement": {
"app_id": 9,
"groups": [
"video"
],
"username": "chronos",
"home_inner": "/var/lib/fortify",
"home": "/var/lib/persist/home/org.chromium.Chromium",
"sandbox": {
"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": {
"see": null,
"talk": [
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower"
],
"own": null,
"call": null,
"broadcast": null,
"filter": true
},
"enablements": 13,
"session_bus": {
"see": null,
"talk": [
@@ -432,7 +219,222 @@ App
},
"filter": true
},
"enablements": 13
"system_bus": {
"see": null,
"talk": [
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower"
],
"own": null,
"call": null,
"broadcast": null,
"filter": true
},
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"data": "/var/lib/hakurei/u0/org.chromium.Chromium",
"dir": "/data/data/org.chromium.Chromium",
"extra_perms": [
{
"ensure": true,
"path": "/var/lib/hakurei/u0",
"x": true
},
{
"path": "/var/lib/hakurei/u0/org.chromium.Chromium",
"r": true,
"w": true,
"x": true
}
],
"identity": 9,
"groups": [
"video",
"dialout",
"plugdev"
],
"container": {
"hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1,
"seccomp_presets": 1,
"seccomp_compat": true,
"devel": true,
"userns": true,
"net": true,
"tty": true,
"multiarch": true,
"env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
},
"map_real_uid": true,
"device": true,
"filesystem": [
{
"src": "/nix/store"
},
{
"src": "/run/current-system"
},
{
"src": "/run/opengl-driver"
},
{
"src": "/var/db/nix-channels"
},
{
"dst": "/data/data/org.chromium.Chromium",
"src": "/var/lib/hakurei/u0/org.chromium.Chromium",
"write": true,
"require": true
},
{
"src": "/dev/dri",
"dev": true
}
],
"symlink": [
[
"/run/user/65534",
"/run/user/150"
]
],
"etc": "/etc",
"auto_etc": true,
"cover": [
"/var/run/nscd"
]
}
},
"time": "1970-01-01T00:00:00.000000009Z"
}
`},
{"json config", nil, hst.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.*": "*"
},
"broadcast": {
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"
},
"filter": true
},
"system_bus": {
"see": null,
"talk": [
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower"
],
"own": null,
"call": null,
"broadcast": null,
"filter": true
},
"username": "chronos",
"shell": "/run/current-system/sw/bin/zsh",
"data": "/var/lib/hakurei/u0/org.chromium.Chromium",
"dir": "/data/data/org.chromium.Chromium",
"extra_perms": [
{
"ensure": true,
"path": "/var/lib/hakurei/u0",
"x": true
},
{
"path": "/var/lib/hakurei/u0/org.chromium.Chromium",
"r": true,
"w": true,
"x": true
}
],
"identity": 9,
"groups": [
"video",
"dialout",
"plugdev"
],
"container": {
"hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1,
"seccomp_presets": 1,
"seccomp_compat": true,
"devel": true,
"userns": true,
"net": true,
"tty": true,
"multiarch": true,
"env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
},
"map_real_uid": true,
"device": true,
"filesystem": [
{
"src": "/nix/store"
},
{
"src": "/run/current-system"
},
{
"src": "/run/opengl-driver"
},
{
"src": "/var/db/nix-channels"
},
{
"dst": "/data/data/org.chromium.Chromium",
"src": "/var/lib/hakurei/u0/org.chromium.Chromium",
"write": true,
"require": true
},
{
"src": "/dev/dri",
"dev": true
}
],
"symlink": [
[
"/run/user/65534",
"/run/user/150"
]
],
"etc": "/etc",
"auto_etc": true,
"cover": [
"/var/run/nscd"
]
}
}
`},
@@ -458,23 +460,19 @@ func Test_printPs(t *testing.T) {
short, json bool
want string
}{
{"no entries", make(state.Entries), false, false, ` Instance PID App Uptime Enablements Command
`},
{"no entries short", make(state.Entries), true, false, ``},
{"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
{"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"},
{"state corruption", state.Entries{state.ID{}: testState}, false, false, " Instance PID Application Uptime\n"},
{"valid pd", state.Entries{testID: &state.State{ID: testID, PID: 1 << 8, Config: new(hst.Config), Time: testAppTime}}, false, false, ` Instance PID Application Uptime
8e2c76b0 256 0 (app.hakurei.8e2c76b0) 1h2m32s
`},
{"valid", state.Entries{testID: testState}, false, false, ` Instance PID App Uptime Enablements Command
8e2c76b0 3735928559 9 1h2m32s wayland, dbus, pulseaudio ["chromium" "--ignore-gpu-blocklist" "--disable-smooth-scrolling" "--enable-features=UseOzonePlatform" "--ozone-platform=wayland"]
`},
{"valid short", state.Entries{testID: testState}, true, false, `8e2c76b0
{"valid", state.Entries{testID: testState}, false, false, ` Instance PID Application Uptime
8e2c76b0 3735928559 9 (org.chromium.Chromium) 1h2m32s
`},
{"valid short", state.Entries{testID: testState}, true, false, "8e2c76b0\n"},
{"valid json", state.Entries{testID: testState}, false, true, `{
"8e2c76b066dabe574cf073bdb46eb5c1": {
"instance": [
@@ -506,115 +504,119 @@ func Test_printPs(t *testing.T) {
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
],
"confinement": {
"app_id": 9,
"groups": [
"video"
"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"
],
"username": "chronos",
"home_inner": "/var/lib/fortify",
"home": "/var/lib/persist/home/org.chromium.Chromium",
"sandbox": {
"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"
]
"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/hakurei/u0/org.chromium.Chromium",
"dir": "/data/data/org.chromium.Chromium",
"extra_perms": [
{
"ensure": true,
"path": "/var/lib/hakurei/u0",
"x": true
},
{
"path": "/var/lib/hakurei/u0/org.chromium.Chromium",
"r": true,
"w": true,
"x": true
}
],
"identity": 9,
"groups": [
"video",
"dialout",
"plugdev"
],
"container": {
"hostname": "localhost",
"wait_delay": -1,
"seccomp_flags": 1,
"seccomp_presets": 1,
"seccomp_compat": true,
"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,
"path": "/var/lib/fortify/u0",
"x": true
"src": "/nix/store"
},
{
"path": "/var/lib/fortify/u0/org.chromium.Chromium",
"r": true,
"w": true,
"x": true
"src": "/run/current-system"
},
{
"src": "/run/opengl-driver"
},
{
"src": "/var/db/nix-channels"
},
{
"dst": "/data/data/org.chromium.Chromium",
"src": "/var/lib/hakurei/u0/org.chromium.Chromium",
"write": true,
"require": true
},
{
"src": "/dev/dri",
"dev": true
}
],
"system_bus": {
"see": null,
"talk": [
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower"
],
"own": null,
"call": null,
"broadcast": null,
"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
"symlink": [
[
"/run/user/65534",
"/run/user/150"
]
],
"etc": "/etc",
"auto_etc": true,
"cover": [
"/var/run/nscd"
]
}
},
"time": "1970-01-01T00:00:00.000000009Z"

View File

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

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

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

View File

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

View File

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

View File

@@ -8,8 +8,7 @@ import (
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
var (
fmain = compPoison
fpkg = compPoison
hmain = compPoison
)
func mustCheckPath(p string) string {

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

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

View File

@@ -31,7 +31,7 @@
'',
id ? name,
app_id ? throw "app_id is required",
identity ? throw "identity is required",
groups ? [ ],
userns ? false,
net ? true,
@@ -57,7 +57,7 @@ let
modules = modules ++ [
{
home = {
username = "fortify";
username = "hakurei";
homeDirectory = "/data/data/${id}";
stateVersion = "22.11";
};
@@ -65,7 +65,7 @@ let
];
};
launcher = writeScript "fortify-${pname}" ''
launcher = writeScript "hakurei-${pname}" ''
#!${runtimeShell} -el
${script}
'';
@@ -147,7 +147,7 @@ let
name
version
id
app_id
identity
launcher
groups
userns
@@ -215,15 +215,14 @@ stdenv.mkDerivation {
# create binary cache
closureInfo="${
closureInfo {
rootPaths =
[
homeManagerConfiguration.activationPackage
launcher
]
++ optionals gpu [
mesaWrappers
nixGL
];
rootPaths = [
homeManagerConfiguration.activationPackage
launcher
]
++ optionals gpu [
mesaWrappers
nixGL
];
}
}"
echo "copying application paths..."

View File

@@ -10,39 +10,26 @@ import (
"path"
"syscall"
"git.gensokyo.uk/security/fortify/command"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/sys"
"git.gensokyo.uk/security/fortify/sandbox"
"hakurei.app/command"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
)
const shellPath = "/run/current-system/sw/bin/bash"
var (
errSuccess = errors.New("success")
std sys.State = new(sys.Std)
)
func init() {
fmsg.Prepare("fpkg")
hlog.Prepare("planterette")
if err := os.Setenv("SHELL", shellPath); err != nil {
log.Fatalf("cannot set $SHELL: %v", err)
}
}
func main() {
// early init path, skips root check and duplicate PR_SET_DUMPABLE
sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg)
if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
// not fatal: this program runs as the privileged user
}
if os.Geteuid() == 0 {
log.Fatal("this program must not run as root")
}
@@ -55,14 +42,9 @@ func main() {
flagVerbose bool
flagDropShell bool
)
c := command.New(os.Stderr, log.Printf, "fpkg", func([]string) error {
internal.InstallFmsg(flagVerbose)
return nil
}).
c := command.New(os.Stderr, log.Printf, "planterette", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next fortify action")
c.Command("shim", command.UsageInternal, func([]string) error { app.ShimMain(); return errSuccess })
Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next hakurei action")
{
var (
@@ -84,7 +66,7 @@ func main() {
}
/*
Look up paths to programs started by fpkg.
Look up paths to programs started by planterette.
This is done here to ease error handling as cleanup is not yet required.
*/
@@ -100,7 +82,7 @@ func main() {
*/
var workDir string
if p, err := os.MkdirTemp("", "fpkg.*"); err != nil {
if p, err := os.MkdirTemp("", "planterette.*"); err != nil {
log.Printf("cannot create temporary directory: %v", err)
return err
} else {
@@ -157,19 +139,19 @@ func main() {
return errSuccess
}
// AppID determines uid
if a.AppID != bundle.AppID {
// identity determines uid
if a.Identity != bundle.Identity {
cleanup()
log.Printf("package %q app id %d differs from installed %d",
pkgPath, bundle.AppID, a.AppID)
log.Printf("package %q identity %d differs from installed %d",
pkgPath, bundle.Identity, a.Identity)
return syscall.EBADE
}
// sec: should compare version string
fmsg.Verbosef("installing application %q version %q over local %q",
hlog.Verbosef("installing application %q version %q over local %q",
bundle.ID, bundle.Version, a.Version)
} else {
fmsg.Verbosef("application %q clean installation", bundle.ID)
hlog.Verbosef("application %q clean installation", bundle.ID)
// sec: should install credentials
}
@@ -179,7 +161,7 @@ func main() {
withCacheDir(ctx, "install", []string{
// export inner bundle path in the environment
"export BUNDLE=" + fst.Tmp + "/bundle",
"export BUNDLE=" + hst.Tmp + "/bundle",
// replace inner /etc
"mkdir -p etc",
"chmod -R +w etc",
@@ -218,7 +200,7 @@ func main() {
"rm -rf .local/state/{nix,home-manager}",
// run activation script
bundle.ActivationPackage + "/activate",
}, false, func(config *fst.Config) *fst.Config { return config },
}, false, func(config *hst.Config) *hst.Config { return config },
bundle, pathSet, flagDropShellActivate, cleanup)
/*
@@ -291,8 +273,8 @@ func main() {
"--out-link /nix/.nixGL/auto/vulkan " +
"--override-input nixpkgs path:/etc/nixpkgs " +
"path:" + a.NixGL + "#nixVulkanNvidia",
}, true, func(config *fst.Config) *fst.Config {
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, []*fst.FilesystemConfig{
}, true, func(config *hst.Config) *hst.Config {
config.Container.Filesystem = append(config.Container.Filesystem, []*hst.FilesystemConfig{
{Src: "/etc/resolv.conf"},
{Src: "/sys/block"},
{Src: "/sys/bus"},
@@ -324,8 +306,8 @@ func main() {
*/
if a.GPU {
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem,
&fst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(fst.Tmp, "nixGL")})
config.Container.Filesystem = append(config.Container.Filesystem,
&hst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(hst.Tmp, "nixGL")})
appendGPUFilesystem(config)
}
@@ -341,9 +323,9 @@ func main() {
}
c.MustParse(os.Args[1:], func(err error) {
fmsg.Verbosef("command returned %v", err)
hlog.Verbosef("command returned %v", err)
if errors.Is(err, errSuccess) {
fmsg.BeforeExit()
hlog.BeforeExit()
os.Exit(0)
}
})

View File

@@ -8,8 +8,8 @@ import (
"strconv"
"sync/atomic"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"hakurei.app/hst"
"hakurei.app/internal/hlog"
)
var (
@@ -18,10 +18,10 @@ var (
func init() {
// dataHome
if p, ok := os.LookupEnv("FORTIFY_DATA_HOME"); ok {
if p, ok := os.LookupEnv("HAKUREI_DATA_HOME"); ok {
dataHome = p
} else {
dataHome = "/var/lib/fortify/" + strconv.Itoa(os.Getuid())
dataHome = "/var/lib/hakurei/" + strconv.Itoa(os.Getuid())
}
}
@@ -37,7 +37,7 @@ func lookPath(file string) string {
var beforeRunFail = new(atomic.Pointer[func()])
func mustRun(name string, arg ...string) {
fmsg.Verbosef("spawning process: %q %q", name, arg)
hlog.Verbosef("spawning process: %q %q", name, arg)
cmd := exec.Command(name, arg...)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
@@ -71,8 +71,8 @@ func pathSetByApp(id string) *appPathSet {
return pathSet
}
func appendGPUFilesystem(config *fst.Config) {
config.Confinement.Sandbox.Filesystem = append(config.Confinement.Sandbox.Filesystem, []*fst.FilesystemConfig{
func appendGPUFilesystem(config *hst.Config) {
config.Container.Filesystem = append(config.Container.Filesystem, []*hst.FilesystemConfig{
// flatpak commit 763a686d874dd668f0236f911de00b80766ffe79
{Src: "/dev/dri", Device: true},
// mali

60
cmd/planterette/proc.go Normal file
View File

@@ -0,0 +1,60 @@
package main
import (
"context"
"encoding/json"
"errors"
"io"
"log"
"os"
"os/exec"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
)
var hakureiPath = internal.MustHakureiPath()
func mustRunApp(ctx context.Context, config *hst.Config, beforeFail func()) {
var (
cmd *exec.Cmd
st io.WriteCloser
)
if r, w, err := os.Pipe(); err != nil {
beforeFail()
log.Fatalf("cannot pipe: %v", err)
} else {
if hlog.Load() {
cmd = exec.CommandContext(ctx, hakureiPath, "-v", "app", "3")
} else {
cmd = exec.CommandContext(ctx, hakureiPath, "app", "3")
}
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.ExtraFiles = []*os.File{r}
st = w
}
go func() {
if err := json.NewEncoder(st).Encode(config); err != nil {
beforeFail()
log.Fatalf("cannot send configuration: %v", err)
}
}()
if err := cmd.Start(); err != nil {
beforeFail()
log.Fatalf("cannot start hakurei: %v", err)
}
if err := cmd.Wait(); err != nil {
var exitError *exec.ExitError
if errors.As(err, &exitError) {
beforeFail()
internal.Exit(exitError.ExitCode())
} else {
beforeFail()
log.Fatalf("cannot wait: %v", err)
}
}
}

View File

@@ -50,11 +50,13 @@
];
};
environment.fortify = {
environment.hakurei = {
enable = true;
stateDir = "/var/lib/fortify";
stateDir = "/var/lib/hakurei";
users.alice = 0;
home-manager = _: _: { home.stateVersion = "23.05"; };
extraHomeConfig = {
home.stateVersion = "23.05";
};
};
}

View File

@@ -9,7 +9,7 @@ let
buildPackage = self.buildPackage.${system};
in
nixosTest {
name = "fpkg";
name = "planterette";
nodes.machine = {
environment.etc = {
"foot.pkg".source = callPackage ./foot.nix { inherit buildPackage; };
@@ -18,7 +18,7 @@ nixosTest {
imports = [
./configuration.nix
self.nixosModules.fortify
self.nixosModules.hakurei
self.inputs.home-manager.nixosModules.home-manager
];
};

View File

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

View File

@@ -47,50 +47,50 @@ def wait_for_window(pattern):
def collect_state_ui(name):
swaymsg(f"exec fortify ps > '/tmp/{name}.ps'")
swaymsg(f"exec hakurei ps > '/tmp/{name}.ps'")
machine.copy_from_vm(f"/tmp/{name}.ps", "")
swaymsg(f"exec fortify --json ps > '/tmp/{name}.json'")
swaymsg(f"exec hakurei --json ps > '/tmp/{name}.json'")
machine.copy_from_vm(f"/tmp/{name}.json", "")
machine.screenshot(name)
def check_state(name, enablements):
instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 fortify --json ps"))
instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei --json ps"))
if len(instances) != 1:
raise Exception(f"unexpected state length {len(instances)}")
instance = next(iter(instances.values()))
config = instance['config']
if len(config['args']) != 1 or not (config['args'][0].startswith("/nix/store/")) or f"fortify-{name}-" not in (config['args'][0]):
if len(config['args']) != 1 or not (config['args'][0].startswith("/nix/store/")) or f"hakurei-{name}-" not in (config['args'][0]):
raise Exception(f"unexpected args {instance['config']['args']}")
if config['confinement']['enablements'] != enablements:
raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}")
if config['enablements'] != enablements:
raise Exception(f"unexpected enablements {instance['config']['enablements']}")
start_all()
machine.wait_for_unit("multi-user.target")
# To check fortify's version:
print(machine.succeed("sudo -u alice -i fortify version"))
# To check hakurei's version:
print(machine.succeed("sudo -u alice -i hakurei version"))
# Wait for Sway to complete startup:
machine.wait_for_file("/run/user/1000/wayland-1")
machine.wait_for_file("/tmp/sway-ipc.sock")
# Prepare fpkg directory:
machine.succeed("install -dm 0700 -o alice -g users /var/lib/fortify/1000")
# Prepare planterette directory:
machine.succeed("install -dm 0700 -o alice -g users /var/lib/hakurei/1000")
# Install fpkg app:
swaymsg("exec fpkg -v install /etc/foot.pkg && touch /tmp/fpkg-install-done")
machine.wait_for_file("/tmp/fpkg-install-done")
# Install planterette app:
swaymsg("exec planterette -v install /etc/foot.pkg && touch /tmp/planterette-install-ok")
machine.wait_for_file("/tmp/planterette-install-ok")
# Start app (foot) with Wayland enablement:
swaymsg("exec fpkg -v start org.codeberg.dnkl.foot")
wait_for_window("fortify@machine-foot")
swaymsg("exec planterette -v start org.codeberg.dnkl.foot")
wait_for_window("hakurei@machine-foot")
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-client")
machine.wait_for_file("/tmp/hakurei.1000/tmpdir/2/success-client")
collect_state_ui("app_wayland")
check_state("foot", 13)
# Verify acl on XDG_RUNTIME_DIR:
@@ -104,5 +104,5 @@ machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/
swaymsg("exit", succeed=False)
machine.wait_for_file("/tmp/sway-exit-ok")
# Print fortify runDir contents:
print(machine.succeed("find /run/user/1000/fortify"))
# Print hakurei runDir contents:
print(machine.succeed("find /run/user/1000/hakurei"))

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

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

View File

@@ -3,7 +3,7 @@ package command_test
import (
"testing"
"git.gensokyo.uk/security/fortify/command"
"hakurei.app/command"
)
func TestBuild(t *testing.T) {

View File

@@ -10,7 +10,7 @@ import (
"strings"
"testing"
"git.gensokyo.uk/security/fortify/command"
"hakurei.app/command"
)
func TestParse(t *testing.T) {

View File

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

45
container/capability.go Normal file
View File

@@ -0,0 +1,45 @@
package container
import (
"syscall"
"unsafe"
)
const (
_LINUX_CAPABILITY_VERSION_3 = 0x20080522
PR_CAP_AMBIENT = 0x2f
PR_CAP_AMBIENT_RAISE = 0x2
PR_CAP_AMBIENT_CLEAR_ALL = 0x4
CAP_SYS_ADMIN = 0x15
CAP_SETPCAP = 0x8
)
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
}

View File

@@ -1,5 +1,5 @@
// Package sandbox implements unprivileged Linux container with hardening options useful for creating application sandboxes.
package sandbox
// Package container implements unprivileged Linux containers with built-in support for syscall filtering.
package container
import (
"context"
@@ -11,37 +11,21 @@ import (
"os/exec"
"path"
"strconv"
"syscall"
. "syscall"
"time"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
"hakurei.app/container/seccomp"
)
type HardeningFlags uintptr
const (
FSyscallCompat HardeningFlags = 1 << iota
FAllowDevel
FAllowUserns
FAllowTTY
FAllowNet
)
// Nonexistent is a path that cannot exist.
// /proc is chosen because a system with covered /proc is unsupported by this package.
Nonexistent = "/proc/nonexistent"
func (flags HardeningFlags) seccomp(opts seccomp.SyscallOpts) seccomp.SyscallOpts {
if flags&FSyscallCompat == 0 {
opts |= seccomp.FlagExt
}
if flags&FAllowDevel == 0 {
opts |= seccomp.FlagDenyDevel
}
if flags&FAllowUserns == 0 {
opts |= seccomp.FlagDenyNS
}
if flags&FAllowTTY == 0 {
opts |= seccomp.FlagDenyTTY
}
return opts
}
// CancelSignal is the signal expected by container init on context cancel.
// A custom [Container.Cancel] function must eventually deliver this signal.
CancelSignal = SIGTERM
)
type (
// Container represents a container environment being prepared or run.
@@ -55,9 +39,6 @@ type (
// with behaviour identical to its [exec.Cmd] counterpart.
ExtraFiles []*os.File
// Custom [exec.Cmd] initialisation function.
CommandContext func(ctx context.Context) (cmd *exec.Cmd)
// param encoder for shim and init
setup *gob.Encoder
// cancels cmd
@@ -85,6 +66,10 @@ type (
Path string
// Initial process argv.
Args []string
// Deliver SIGINT to the initial process on context cancellation.
ForwardCancel bool
// time to wait for linger processes after death of initial process
AdoptWaitDelay time.Duration
// Mapped Uid in user namespace.
Uid int
@@ -94,44 +79,43 @@ type (
Hostname string
// Sequential container setup ops.
*Ops
// Extra seccomp options.
Seccomp seccomp.SyscallOpts
// Seccomp system call filter rules.
SeccompRules []seccomp.NativeRule
// Extra seccomp flags.
SeccompFlags seccomp.ExportFlag
// Seccomp presets. Has no effect unless SeccompRules is zero-length.
SeccompPresets seccomp.FilterPreset
// Do not load seccomp program.
SeccompDisable bool
// Permission bits of newly created parent directories.
// The zero value is interpreted as 0755.
ParentPerm os.FileMode
// Do not syscall.Setsid.
RetainSession bool
// Do not [syscall.CLONE_NEWNET].
HostNet bool
// Retain CAP_SYS_ADMIN.
Privileged bool
Flags HardeningFlags
}
Ops []Op
Op interface {
early(params *Params) error
apply(params *Params) error
prefix() string
Is(op Op) bool
fmt.Stringer
}
)
// Start starts the container init. The init process blocks until Serve is called.
func (p *Container) Start() error {
if p.cmd != nil {
return errors.New("sandbox: already started")
return errors.New("container: already started")
}
if p.Ops == nil || len(*p.Ops) == 0 {
return errors.New("sandbox: starting an empty container")
return errors.New("container: starting an empty container")
}
ctx, cancel := context.WithCancel(p.ctx)
p.cancel = cancel
var cloneFlags uintptr = syscall.CLONE_NEWIPC |
syscall.CLONE_NEWUTS |
syscall.CLONE_NEWCGROUP
if p.Flags&FAllowNet == 0 {
cloneFlags |= syscall.CLONE_NEWNET
var cloneFlags uintptr = CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP
if !p.HostNet {
cloneFlags |= CLONE_NEWNET
}
// map to overflow id to work around ownership checks
@@ -142,29 +126,32 @@ func (p *Container) Start() error {
p.Gid = OverflowGid()
}
if p.CommandContext != nil {
p.cmd = p.CommandContext(ctx)
} else {
p.cmd = exec.CommandContext(ctx, MustExecutable())
p.cmd.Args = []string{"init"}
if !p.RetainSession {
p.SeccompPresets |= seccomp.PresetDenyTTY
}
if p.AdoptWaitDelay == 0 {
p.AdoptWaitDelay = 5 * time.Second
}
// to allow disabling this behaviour
if p.AdoptWaitDelay < 0 {
p.AdoptWaitDelay = 0
}
p.cmd = exec.CommandContext(ctx, MustExecutable())
p.cmd.Args = []string{initName}
p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
p.cmd.WaitDelay = p.WaitDelay
if p.Cancel != nil {
p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
} else {
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(syscall.SIGTERM) }
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(CancelSignal) }
}
p.cmd.Dir = "/"
p.cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: p.Flags&FAllowTTY == 0,
Pdeathsig: syscall.SIGKILL,
Cloneflags: cloneFlags |
syscall.CLONE_NEWUSER |
syscall.CLONE_NEWPID |
syscall.CLONE_NEWNS,
p.cmd.SysProcAttr = &SysProcAttr{
Setsid: !p.RetainSession,
Pdeathsig: SIGKILL,
Cloneflags: cloneFlags | CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS,
// remain privileged for setup
AmbientCaps: []uintptr{CAP_SYS_ADMIN, CAP_SETPCAP},
@@ -192,6 +179,8 @@ func (p *Container) Start() error {
return nil
}
// Serve serves [Container.Params] to the container init.
// Serve must only be called once.
func (p *Container) Serve() error {
if p.setup == nil {
panic("invalid serve")
@@ -202,7 +191,7 @@ func (p *Container) Serve() error {
if p.Path != "" && !path.IsAbs(p.Path) {
p.cancel()
return msg.WrapErr(syscall.EINVAL,
return msg.WrapErr(EINVAL,
fmt.Sprintf("invalid executable path %q", p.Path))
}
@@ -211,7 +200,7 @@ func (p *Container) Serve() error {
p.Path = os.Getenv("SHELL")
if !path.IsAbs(p.Path) {
p.cancel()
return msg.WrapErr(syscall.EBADE,
return msg.WrapErr(EBADE,
"no command specified and $SHELL is invalid")
}
p.name = path.Base(p.Path)
@@ -225,11 +214,16 @@ func (p *Container) Serve() error {
}
}
if p.SeccompRules == nil {
// do not transmit nil
p.SeccompRules = make([]seccomp.NativeRule, 0)
}
err := setup.Encode(
&initParams{
p.Params,
syscall.Getuid(),
syscall.Getgid(),
Getuid(),
Getgid(),
len(p.ExtraFiles),
msg.IsVerbose(),
},
@@ -240,11 +234,20 @@ func (p *Container) Serve() error {
return err
}
// Wait waits for the container init process to exit.
func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() }
func (p *Container) String() string {
return fmt.Sprintf("argv: %q, flags: %#x, seccomp: %#x",
p.Args, p.Flags, int(p.Flags.seccomp(p.Seccomp)))
return fmt.Sprintf("argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x",
p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets))
}
// ProcessState returns the address to os.ProcessState held by the underlying [exec.Cmd].
func (p *Container) ProcessState() *os.ProcessState {
if p.cmd == nil {
return nil
}
return p.cmd.ProcessState
}
func New(ctx context.Context, name string, args ...string) *Container {

368
container/container_test.go Normal file
View File

@@ -0,0 +1,368 @@
package container_test
import (
"bytes"
"context"
"encoding/gob"
"errors"
"fmt"
"log"
"os"
"os/exec"
"os/signal"
"strconv"
"strings"
"syscall"
"testing"
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/container/vfs"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
)
const (
ignore = "\x00"
ignoreV = -1
pathWantMnt = "/etc/hakurei/want-mnt"
)
var containerTestCases = []struct {
name string
filter bool
session bool
net bool
ops *container.Ops
mnt []*vfs.MountInfoEntry
uid int
gid int
rules []seccomp.NativeRule
flags seccomp.ExportFlag
presets seccomp.FilterPreset
}{
{"minimal", true, false, false,
new(container.Ops), nil,
1000, 100, nil, 0, seccomp.PresetStrict},
{"allow", true, true, true,
new(container.Ops), nil,
1000, 100, nil, 0, seccomp.PresetExt | seccomp.PresetDenyDevel},
{"no filter", false, true, true,
new(container.Ops), nil,
1000, 100, nil, 0, seccomp.PresetExt},
{"custom rules", true, true, true,
new(container.Ops), nil,
1, 31, []seccomp.NativeRule{{seccomp.ScmpSyscall(syscall.SYS_SETUID), seccomp.ScmpErrno(syscall.EPERM), nil}}, 0, seccomp.PresetExt},
{"tmpfs", true, false, false,
new(container.Ops).
Tmpfs(hst.Tmp, 0, 0755),
[]*vfs.MountInfoEntry{
ent("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
},
9, 9, nil, 0, seccomp.PresetStrict},
{"dev", true, true /* go test output is not a tty */, false,
new(container.Ops).
Dev("/dev").
Mqueue("/dev/mqueue"),
[]*vfs.MountInfoEntry{
ent("/", "/dev", "rw,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
ent("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
ent("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
},
1971, 100, nil, 0, seccomp.PresetStrict},
}
func TestContainer(t *testing.T) {
{
oldVerbose := hlog.Load()
oldOutput := container.GetOutput()
internal.InstallOutput(true)
t.Cleanup(func() { hlog.Store(oldVerbose) })
t.Cleanup(func() { container.SetOutput(oldOutput) })
}
t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) {
wantErr := context.Canceled
wantExitCode := 0
if err := c.Wait(); !errors.Is(err, wantErr) {
hlog.PrintBaseError(err, "wait:")
t.Errorf("Wait: error = %v, want %v", err, wantErr)
}
if ps := c.ProcessState(); ps == nil {
t.Errorf("ProcessState unexpectedly returned nil")
} else if code := ps.ExitCode(); code != wantExitCode {
t.Errorf("ExitCode: %d, want %d", code, wantExitCode)
}
}))
t.Run("forward", testContainerCancel(func(c *container.Container) {
c.ForwardCancel = true
}, func(t *testing.T, c *container.Container) {
var exitError *exec.ExitError
if err := c.Wait(); !errors.As(err, &exitError) {
hlog.PrintBaseError(err, "wait:")
t.Errorf("Wait: error = %v", err)
}
if code := exitError.ExitCode(); code != blockExitCodeInterrupt {
t.Errorf("ExitCode: %d, want %d", code, blockExitCodeInterrupt)
}
}))
for i, tc := range containerTestCases {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
defer cancel()
var libPaths []string
c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i))
c.Uid = tc.uid
c.Gid = tc.gid
c.Hostname = hostnameFromTestCase(tc.name)
c.Stdout, c.Stderr = os.Stdout, os.Stderr
c.WaitDelay = helperDefaultTimeout
*c.Ops = append(*c.Ops, *tc.ops...)
c.SeccompRules = tc.rules
c.SeccompFlags = tc.flags | seccomp.AllowMultiarch
c.SeccompPresets = tc.presets
c.SeccompDisable = !tc.filter
c.RetainSession = tc.session
c.HostNet = tc.net
c.
Tmpfs("/tmp", 0, 0755).
Place("/etc/hostname", []byte(c.Hostname))
// needs /proc to check mountinfo
c.Proc("/proc")
// mountinfo cannot be resolved directly by helper due to libPaths nondeterminism
mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
mnt = append(mnt,
ent("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
// Bind(os.Args[0], helperInnerPath, 0)
ent(ignore, helperInnerPath, "ro,nosuid,nodev,relatime", ignore, ignore, ignore),
)
for _, name := range libPaths {
// Bind(name, name, 0)
mnt = append(mnt, ent(ignore, name, "ro,nosuid,nodev,relatime", ignore, ignore, ignore))
}
mnt = append(mnt, tc.mnt...)
mnt = append(mnt,
// Tmpfs("/tmp", 0, 0755)
ent("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
// Place("/etc/hostname", []byte(hostname))
ent(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
// Proc("/proc")
ent("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"),
// Place(pathWantMnt, want.Bytes())
ent(ignore, pathWantMnt, "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
)
want := new(bytes.Buffer)
if err := gob.NewEncoder(want).Encode(mnt); err != nil {
t.Fatalf("cannot serialise expected mount points: %v", err)
}
c.Place(pathWantMnt, want.Bytes())
if err := c.Start(); err != nil {
hlog.PrintBaseError(err, "start:")
t.Fatalf("cannot start container: %v", err)
} else if err = c.Serve(); err != nil {
hlog.PrintBaseError(err, "serve:")
t.Errorf("cannot serve setup params: %v", err)
}
if err := c.Wait(); err != nil {
hlog.PrintBaseError(err, "wait:")
t.Fatalf("wait: %v", err)
}
})
}
}
func ent(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry {
return &vfs.MountInfoEntry{
ID: ignoreV,
Parent: ignoreV,
Devno: vfs.DevT{ignoreV, ignoreV},
Root: root,
Target: target,
VfsOptstr: vfsOptstr,
OptFields: []string{ignore},
FsType: fsType,
Source: source,
FsOptstr: fsOptstr,
}
}
func hostnameFromTestCase(name string) string {
return "test-" + strings.Join(strings.Fields(name), "-")
}
func testContainerCancel(
containerExtra func(c *container.Container),
waitCheck func(t *testing.T, c *container.Container),
) func(t *testing.T) {
return func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
c := helperNewContainer(ctx, "block")
c.Stdout, c.Stderr = os.Stdout, os.Stderr
c.WaitDelay = helperDefaultTimeout
if containerExtra != nil {
containerExtra(c)
}
ready := make(chan struct{})
if r, w, err := os.Pipe(); err != nil {
t.Fatalf("cannot pipe: %v", err)
} else {
c.ExtraFiles = append(c.ExtraFiles, w)
go func() {
defer close(ready)
if _, err = r.Read(make([]byte, 1)); err != nil {
panic(err.Error())
}
}()
}
if err := c.Start(); err != nil {
hlog.PrintBaseError(err, "start:")
t.Fatalf("cannot start container: %v", err)
} else if err = c.Serve(); err != nil {
hlog.PrintBaseError(err, "serve:")
t.Errorf("cannot serve setup params: %v", err)
}
<-ready
cancel()
waitCheck(t, c)
}
}
func TestContainerString(t *testing.T) {
c := container.New(t.Context(), "ldd", "/usr/bin/env")
c.SeccompFlags |= seccomp.AllowMultiarch
c.SeccompRules = seccomp.Preset(
seccomp.PresetExt|seccomp.PresetDenyNS|seccomp.PresetDenyTTY,
c.SeccompFlags)
c.SeccompPresets = seccomp.PresetStrict
want := `argv: ["ldd" "/usr/bin/env"], filter: true, rules: 65, flags: 0x1, presets: 0xf`
if got := c.String(); got != want {
t.Errorf("String: %s, want %s", got, want)
}
}
const (
blockExitCodeInterrupt = 2
)
func init() {
helperCommands = append(helperCommands, func(c command.Command) {
c.Command("block", command.UsageInternal, func(args []string) error {
if _, err := os.NewFile(3, "sync").Write([]byte{0}); err != nil {
return fmt.Errorf("write to sync pipe: %v", err)
}
{
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt)
go func() { <-sig; os.Exit(blockExitCodeInterrupt) }()
}
select {}
})
c.Command("container", command.UsageInternal, func(args []string) error {
if len(args) != 1 {
return syscall.EINVAL
}
tc := containerTestCases[0]
if i, err := strconv.Atoi(args[0]); err != nil {
return fmt.Errorf("cannot parse test case index: %v", err)
} else {
tc = containerTestCases[i]
}
if uid := syscall.Getuid(); uid != tc.uid {
return fmt.Errorf("uid: %d, want %d", uid, tc.uid)
}
if gid := syscall.Getgid(); gid != tc.gid {
return fmt.Errorf("gid: %d, want %d", gid, tc.gid)
}
wantHost := hostnameFromTestCase(tc.name)
if host, err := os.Hostname(); err != nil {
return fmt.Errorf("cannot get hostname: %v", err)
} else if host != wantHost {
return fmt.Errorf("hostname: %q, want %q", host, wantHost)
}
if p, err := os.ReadFile("/etc/hostname"); err != nil {
return fmt.Errorf("cannot read /etc/hostname: %v", err)
} else if string(p) != wantHost {
return fmt.Errorf("/etc/hostname: %q, want %q", string(p), wantHost)
}
{
var fail bool
var mnt []*vfs.MountInfoEntry
if f, err := os.Open(pathWantMnt); err != nil {
return fmt.Errorf("cannot open expected mount points: %v", err)
} else if err = gob.NewDecoder(f).Decode(&mnt); err != nil {
return fmt.Errorf("cannot parse expected mount points: %v", err)
} else if err = f.Close(); err != nil {
return fmt.Errorf("cannot close expected mount points: %v", err)
}
var d *vfs.MountInfoDecoder
if f, err := os.Open("/proc/self/mountinfo"); err != nil {
return fmt.Errorf("cannot open mountinfo: %v", err)
} else {
d = vfs.NewMountInfoDecoder(f)
}
i := 0
for cur := range d.Entries() {
if i == len(mnt) {
return fmt.Errorf("got more than %d entries", len(mnt))
}
// ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",relatime")
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",noatime")
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",relatime")
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime")
if !cur.EqualWithIgnore(mnt[i], "\x00") {
fail = true
log.Printf("[FAIL] %s", cur)
} else {
log.Printf("[ OK ] %s", cur)
}
i++
}
if err := d.Err(); err != nil {
return fmt.Errorf("cannot parse mountinfo: %v", err)
}
if i != len(mnt) {
return fmt.Errorf("got %d entries, want %d", i, len(mnt))
}
if fail {
return errors.New("one or more mountinfo entries do not match")
}
}
return nil
})
})
}

View File

@@ -1,4 +1,4 @@
package sandbox
package container
import (
"log"

View File

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

View File

@@ -1,4 +1,4 @@
package sandbox
package container
import (
"errors"
@@ -10,21 +10,31 @@ import (
"path"
"runtime"
"strconv"
"syscall"
. "syscall"
"time"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
"hakurei.app/container/seccomp"
)
const (
// time to wait for linger processes after death of initial process
residualProcessTimeout = 5 * time.Second
/* intermediate tmpfs mount point
// intermediate tmpfs mount point
basePath = "/tmp"
this path might seem like a weird choice, however there are many good reasons to use it:
- the contents of this path is never exposed to the container:
the tmpfs root established here effectively becomes anonymous after pivot_root
- it is safe to assume this path exists and is a directory:
this program will not work correctly without a proper /proc and neither will most others
- this path belongs to the container init:
the container init is not any more privileged or trusted than the rest of the container
- this path is only accessible by init and root:
the container init sets SUID_DUMP_DISABLE and terminates if that fails;
it should be noted that none of this should become relevant at any point since the resulting
intermediate root tmpfs should be effectively anonymous */
intermediateHostPath = "/proc/self/fd"
// setup params file descriptor
setupEnv = "FORTIFY_SETUP"
setupEnv = "HAKUREI_SETUP"
)
type initParams struct {
@@ -45,10 +55,6 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
log.Fatal("this process must run as pid 1")
}
/*
receive setup payload
*/
var (
params initParams
closeSetup func() error
@@ -60,7 +66,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
log.Fatal("invalid setup descriptor")
}
if errors.Is(err, ErrNotSet) {
log.Fatal("FORTIFY_SETUP not set")
log.Fatal("HAKUREI_SETUP not set")
}
log.Fatalf("cannot decode init setup payload: %v", err)
@@ -101,9 +107,9 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
}
oldmask := syscall.Umask(0)
oldmask := Umask(0)
if params.Hostname != "" {
if err := syscall.Sethostname([]byte(params.Hostname)); err != nil {
if err := Sethostname([]byte(params.Hostname)); err != nil {
log.Fatalf("cannot set hostname: %v", err)
}
}
@@ -111,13 +117,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
// cache sysctl before pivot_root
LastCap()
/*
set up mount points from intermediate root
*/
if err := syscall.Mount("", "/", "",
syscall.MS_SILENT|syscall.MS_SLAVE|syscall.MS_REC,
""); err != nil {
if err := Mount("", "/", "", MS_SILENT|MS_SLAVE|MS_REC, ""); err != nil {
log.Fatalf("cannot make / rslave: %v", err)
}
@@ -134,28 +134,25 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
}
}
if err := syscall.Mount("rootfs", basePath, "tmpfs",
syscall.MS_NODEV|syscall.MS_NOSUID,
""); err != nil {
if err := Mount("rootfs", intermediateHostPath, "tmpfs", MS_NODEV|MS_NOSUID, ""); err != nil {
log.Fatalf("cannot mount intermediate root: %v", err)
}
if err := os.Chdir(basePath); err != nil {
if err := os.Chdir(intermediateHostPath); err != nil {
log.Fatalf("cannot enter base path: %v", err)
}
if err := os.Mkdir(sysrootDir, 0755); err != nil {
log.Fatalf("%v", err)
}
if err := syscall.Mount(sysrootDir, sysrootDir, "",
syscall.MS_SILENT|syscall.MS_MGC_VAL|syscall.MS_BIND|syscall.MS_REC,
""); err != nil {
if err := Mount(sysrootDir, sysrootDir, "", MS_SILENT|MS_MGC_VAL|MS_BIND|MS_REC, ""); err != nil {
log.Fatalf("cannot bind sysroot: %v", err)
}
if err := os.Mkdir(hostDir, 0755); err != nil {
log.Fatalf("%v", err)
}
if err := syscall.PivotRoot(basePath, hostDir); err != nil {
// pivot_root uncovers intermediateHostPath in hostDir
if err := PivotRoot(intermediateHostPath, hostDir); err != nil {
log.Fatalf("cannot pivot into intermediate root: %v", err)
}
if err := os.Chdir("/"); err != nil {
@@ -173,23 +170,18 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
}
}
/*
pivot to sysroot
*/
if err := syscall.Mount(hostDir, hostDir, "",
syscall.MS_SILENT|syscall.MS_REC|syscall.MS_PRIVATE,
""); err != nil {
// setup requiring host root complete at this point
if err := Mount(hostDir, hostDir, "", MS_SILENT|MS_REC|MS_PRIVATE, ""); err != nil {
log.Fatalf("cannot make host root rprivate: %v", err)
}
if err := syscall.Unmount(hostDir, syscall.MNT_DETACH); err != nil {
if err := Unmount(hostDir, MNT_DETACH); err != nil {
log.Fatalf("cannot unmount host root: %v", err)
}
{
var fd int
if err := IgnoringEINTR(func() (err error) {
fd, err = syscall.Open("/", syscall.O_DIRECTORY|syscall.O_RDONLY, 0)
fd, err = Open("/", O_DIRECTORY|O_RDONLY, 0)
return
}); err != nil {
log.Fatalf("cannot open intermediate root: %v", err)
@@ -198,40 +190,36 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
log.Fatalf("%v", err)
}
if err := syscall.PivotRoot(".", "."); err != nil {
if err := PivotRoot(".", "."); err != nil {
log.Fatalf("cannot pivot into sysroot: %v", err)
}
if err := syscall.Fchdir(fd); err != nil {
if err := Fchdir(fd); err != nil {
log.Fatalf("cannot re-enter intermediate root: %v", err)
}
if err := syscall.Unmount(".", syscall.MNT_DETACH); err != nil {
if err := Unmount(".", MNT_DETACH); err != nil {
log.Fatalf("cannot unmount intemediate root: %v", err)
}
if err := os.Chdir("/"); err != nil {
log.Fatalf("%v", err)
}
if err := syscall.Close(fd); err != nil {
if err := Close(fd); err != nil {
log.Fatalf("cannot close intermediate root: %v", err)
}
}
/*
caps/securebits and seccomp filter
*/
if _, _, errno := syscall.Syscall(PR_SET_NO_NEW_PRIVS, 1, 0, 0); errno != 0 {
if _, _, errno := Syscall(SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 {
log.Fatalf("prctl(PR_SET_NO_NEW_PRIVS): %v", errno)
}
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0); errno != 0 {
if _, _, errno := 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 {
if _, _, errno := Syscall(SYS_PRCTL, PR_CAPBSET_DROP, i, 0); errno != 0 {
log.Fatalf("cannot drop capability from bonding set: %v", errno)
}
}
@@ -240,7 +228,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
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 {
if _, _, errno := Syscall(SYS_PRCTL, PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, CAP_SYS_ADMIN); errno != 0 {
log.Fatalf("cannot raise CAP_SYS_ADMIN: %v", errno)
}
}
@@ -251,23 +239,26 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
log.Fatalf("cannot capset: %v", err)
}
if err := seccomp.Load(params.Flags.seccomp(params.Seccomp)); err != nil {
log.Fatalf("cannot load syscall filter: %v", err)
if !params.SeccompDisable {
rules := params.SeccompRules
if len(rules) == 0 { // non-empty rules slice always overrides presets
msg.Verbosef("resolving presets %#x", params.SeccompPresets)
rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags)
}
if err := seccomp.Load(rules, params.SeccompFlags); err != nil {
log.Fatalf("cannot load syscall filter: %v", err)
}
msg.Verbosef("%d filter rules loaded", len(rules))
} else {
msg.Verbose("syscall filter not configured")
}
/*
pass through extra files
*/
extraFiles := make([]*os.File, params.Count)
for i := range extraFiles {
// setup fd is placed before all extra files
extraFiles[i] = os.NewFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i))
}
syscall.Umask(oldmask)
/*
prepare initial process
*/
Umask(oldmask)
cmd := exec.Command(params.Path)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
@@ -276,30 +267,20 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
cmd.ExtraFiles = extraFiles
cmd.Dir = params.Dir
msg.Verbosef("starting initial program %s", params.Path)
if err := cmd.Start(); err != nil {
log.Fatalf("%v", err)
}
msg.Suspend()
/*
close setup pipe
*/
if err := closeSetup(); err != nil {
log.Println("cannot close setup pipe:", err)
log.Printf("cannot close setup pipe: %v", err)
// not fatal
}
/*
perform init duties
*/
sig := make(chan os.Signal, 2)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
type winfo struct {
wpid int
wstatus syscall.WaitStatus
wstatus WaitStatus
}
info := make(chan winfo, 1)
done := make(chan struct{})
@@ -308,7 +289,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
var (
err error
wpid = -2
wstatus syscall.WaitStatus
wstatus WaitStatus
)
// keep going until no child process is left
@@ -321,18 +302,22 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
info <- winfo{wpid, wstatus}
}
err = syscall.EINTR
for errors.Is(err, syscall.EINTR) {
wpid, err = syscall.Wait4(-1, &wstatus, 0, nil)
err = EINTR
for errors.Is(err, EINTR) {
wpid, err = Wait4(-1, &wstatus, 0, nil)
}
}
if !errors.Is(err, syscall.ECHILD) {
log.Println("unexpected wait4 response:", err)
if !errors.Is(err, ECHILD) {
log.Printf("unexpected wait4 response: %v", err)
}
close(done)
}()
// handle signals to dump withheld messages
sig := make(chan os.Signal, 2)
signal.Notify(sig, os.Interrupt, CancelSignal)
// closed after residualProcessTimeout has elapsed after initial process death
timeout := make(chan struct{})
@@ -341,11 +326,17 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
select {
case s := <-sig:
if msg.Resume() {
msg.Verbosef("terminating on %s after process start", s.String())
msg.Verbosef("%s after process start", s.String())
} else {
msg.Verbosef("terminating on %s", s.String())
msg.Verbosef("got %s", s.String())
}
if s == CancelSignal && params.ForwardCancel && cmd.Process != nil {
msg.Verbose("forwarding context cancellation")
if err := cmd.Process.Signal(os.Interrupt); err != nil {
log.Printf("cannot forward cancellation: %v", err)
}
continue
}
msg.BeforeExit()
os.Exit(0)
case w := <-info:
if w.wpid == cmd.Process.Pid {
@@ -364,10 +355,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
msg.Verbosef("initial process exited with status %#x", w.wstatus)
}
go func() {
time.Sleep(residualProcessTimeout)
close(timeout)
}()
go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
}
case <-done:
msg.BeforeExit()
@@ -380,9 +368,11 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
}
}
const initName = "init"
// TryArgv0 calls [Init] if the last element of argv0 is "init".
func TryArgv0(v Msg, prepare func(prefix string), setVerbose func(verbose bool)) {
if len(os.Args) > 0 && path.Base(os.Args[0]) == "init" {
if len(os.Args) > 0 && path.Base(os.Args[0]) == initName {
msg = v
Init(prepare, setVerbose)
msg.BeforeExit()

69
container/init_test.go Normal file
View File

@@ -0,0 +1,69 @@
package container_test
import (
"context"
"log"
"os"
"testing"
"time"
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/internal"
"hakurei.app/internal/hlog"
"hakurei.app/ldd"
)
const (
envDoCheck = "HAKUREI_TEST_DO_CHECK"
helperDefaultTimeout = 5 * time.Second
helperInnerPath = "/usr/bin/helper"
)
var helperCommands []func(c command.Command)
func TestMain(m *testing.M) {
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
if os.Getenv(envDoCheck) == "1" {
c := command.New(os.Stderr, log.Printf, "helper", func(args []string) error {
log.SetFlags(0)
log.SetPrefix("helper: ")
return nil
})
for _, f := range helperCommands {
f(c)
}
c.MustParse(os.Args[1:], func(err error) {
if err != nil {
log.Fatal(err.Error())
}
})
return
}
os.Exit(m.Run())
}
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]string, args ...string) (c *container.Container) {
c = container.New(ctx, helperInnerPath, args...)
c.Env = append(c.Env, envDoCheck+"=1")
c.Bind(os.Args[0], helperInnerPath, 0)
// in case test has cgo enabled
if entries, err := ldd.Exec(ctx, os.Args[0]); err != nil {
log.Fatalf("ldd: %v", err)
} else {
*libPaths = ldd.Path(entries)
}
for _, name := range *libPaths {
c.Bind(name, name, 0)
}
return
}
func helperNewContainer(ctx context.Context, args ...string) (c *container.Container) {
return helperNewContainerLibPaths(ctx, new([]string), args...)
}

View File

@@ -1,13 +1,13 @@
package sandbox
package container
import (
"errors"
"fmt"
"os"
"path/filepath"
"syscall"
. "syscall"
"git.gensokyo.uk/security/fortify/sandbox/vfs"
"hakurei.app/container/vfs"
)
func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) error {
@@ -17,8 +17,7 @@ func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) err
msg.Verbosef("resolved %q on %q flags %#x", source, target, flags)
}
if err := syscall.Mount(source, target, "",
syscall.MS_SILENT|syscall.MS_BIND|flags&syscall.MS_REC, ""); err != nil {
if err := Mount(source, target, "", MS_SILENT|MS_BIND|flags&MS_REC, ""); err != nil {
return wrapErrSuffix(err,
fmt.Sprintf("cannot mount %q on %q:", source, target))
}
@@ -38,7 +37,7 @@ func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) err
{
var destFd int
if err := IgnoringEINTR(func() (err error) {
destFd, err = syscall.Open(targetFinal, O_PATH|syscall.O_CLOEXEC, 0)
destFd, err = Open(targetFinal, O_PATH|O_CLOEXEC, 0)
return
}); err != nil {
return wrapErrSuffix(err,
@@ -46,7 +45,7 @@ func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) err
}
if v, err := os.Readlink(p.fd(destFd)); err != nil {
return wrapErrSelf(err)
} else if err = syscall.Close(destFd); err != nil {
} else if err = Close(destFd); err != nil {
return wrapErrSuffix(err,
fmt.Sprintf("cannot close %q:", targetFinal))
} else {
@@ -54,11 +53,11 @@ func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) err
}
}
mf := syscall.MS_NOSUID | flags&syscall.MS_NODEV | flags&syscall.MS_RDONLY
mf := MS_NOSUID | flags&MS_NODEV | flags&MS_RDONLY
return hostProc.mountinfo(func(d *vfs.MountInfoDecoder) error {
n, err := d.Unfold(targetKFinal)
if err != nil {
if errors.Is(err, syscall.ESTALE) {
if errors.Is(err, ESTALE) {
return msg.WrapErr(err,
fmt.Sprintf("mount point %q never appeared in mountinfo", targetKFinal))
}
@@ -69,13 +68,13 @@ func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) err
if err = remountWithFlags(n, mf); err != nil {
return err
}
if flags&syscall.MS_REC == 0 {
if flags&MS_REC == 0 {
return nil
}
for cur := range n.Collective() {
err = remountWithFlags(cur, mf)
if err != nil && !errors.Is(err, syscall.EACCES) {
if err != nil && !errors.Is(err, EACCES) {
return err
}
}
@@ -91,9 +90,8 @@ func remountWithFlags(n *vfs.MountInfoNode, mf uintptr) error {
}
if kf&mf != mf {
return wrapErrSuffix(syscall.Mount("none", n.Clean, "",
syscall.MS_SILENT|syscall.MS_BIND|syscall.MS_REMOUNT|kf|mf,
""),
return wrapErrSuffix(
Mount("none", n.Clean, "", MS_SILENT|MS_BIND|MS_REMOUNT|kf|mf, ""),
fmt.Sprintf("cannot remount %q:", n.Clean))
}
return nil
@@ -108,8 +106,8 @@ func mountTmpfs(fsname, name string, size int, perm os.FileMode) error {
if size > 0 {
opt += fmt.Sprintf(",size=%d", size)
}
return wrapErrSuffix(syscall.Mount(fsname, target, "tmpfs",
syscall.MS_NOSUID|syscall.MS_NODEV, opt),
return wrapErrSuffix(
Mount(fsname, target, "tmpfs", MS_NOSUID|MS_NODEV, opt),
fmt.Sprintf("cannot mount tmpfs on %q:", name))
}

View File

@@ -1,4 +1,4 @@
package sandbox
package container
import (
"log"

500
container/ops.go Normal file
View File

@@ -0,0 +1,500 @@
package container
import (
"encoding/gob"
"fmt"
"math"
"os"
"path"
"path/filepath"
"slices"
"strings"
. "syscall"
"unsafe"
)
type (
Ops []Op
// Op is a generic setup step ran inside the container init.
// Implementations of this interface are sent as a stream of gobs.
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
}
)
// Grow grows the slice Ops points to using [slices.Grow].
func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) }
func init() { gob.Register(new(BindMountOp)) }
// Bind appends an [Op] that bind mounts host path [BindMountOp.Source] on container path [BindMountOp.Target].
func (f *Ops) Bind(source, target string, flags int) *Ops {
*f = append(*f, &BindMountOp{source, "", target, flags})
return f
}
type BindMountOp struct {
Source, SourceFinal, Target string
Flags int
}
const (
// BindOptional skips nonexistent host paths.
BindOptional = 1 << iota
// BindWritable mounts filesystem read-write.
BindWritable
// BindDevice allows access to devices (special files) on this filesystem.
BindDevice
)
func (b *BindMountOp) early(*Params) error {
if !path.IsAbs(b.Source) {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", b.Source))
}
if v, err := filepath.EvalSymlinks(b.Source); err != nil {
if os.IsNotExist(err) && b.Flags&BindOptional != 0 {
b.SourceFinal = "\x00"
return nil
}
return wrapErrSelf(err)
} else {
b.SourceFinal = v
return nil
}
}
func (b *BindMountOp) apply(*Params) error {
if b.SourceFinal == "\x00" {
if b.Flags&BindOptional == 0 {
// unreachable
return EBADE
}
return nil
}
if !path.IsAbs(b.SourceFinal) || !path.IsAbs(b.Target) {
return msg.WrapErr(EBADE, "path is not absolute")
}
source := toHost(b.SourceFinal)
target := toSysroot(b.Target)
// this perm value emulates bwrap behaviour as it clears bits from 0755 based on
// op->perms which is never set for any bind setup op so always results in 0700
if fi, err := os.Stat(source); err != nil {
return wrapErrSelf(err)
} else if fi.IsDir() {
if err = os.MkdirAll(target, 0700); err != nil {
return wrapErrSelf(err)
}
} else if err = ensureFile(target, 0444, 0700); err != nil {
return err
}
var flags uintptr = MS_REC
if b.Flags&BindWritable == 0 {
flags |= MS_RDONLY
}
if b.Flags&BindDevice == 0 {
flags |= MS_NODEV
}
return hostProc.bindMount(source, target, flags, b.SourceFinal == b.Target)
}
func (b *BindMountOp) Is(op Op) bool { vb, ok := op.(*BindMountOp); return ok && *b == *vb }
func (*BindMountOp) prefix() string { return "mounting" }
func (b *BindMountOp) String() string {
if b.Source == b.Target {
return fmt.Sprintf("%q flags %#x", b.Source, b.Flags)
}
return fmt.Sprintf("%q on %q flags %#x", b.Source, b.Target, b.Flags&BindWritable)
}
func init() { gob.Register(new(MountProcOp)) }
// Proc appends an [Op] that mounts a private instance of proc.
func (f *Ops) Proc(dest string) *Ops {
*f = append(*f, MountProcOp(dest))
return f
}
type MountProcOp string
func (p MountProcOp) early(*Params) error { return nil }
func (p MountProcOp) apply(params *Params) error {
v := string(p)
if !path.IsAbs(v) {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", v))
}
target := toSysroot(v)
if err := os.MkdirAll(target, params.ParentPerm); err != nil {
return wrapErrSelf(err)
}
return wrapErrSuffix(Mount("proc", target, "proc", MS_NOSUID|MS_NOEXEC|MS_NODEV, ""),
fmt.Sprintf("cannot mount proc on %q:", v))
}
func (p MountProcOp) Is(op Op) bool { vp, ok := op.(MountProcOp); return ok && p == vp }
func (MountProcOp) prefix() string { return "mounting" }
func (p MountProcOp) String() string { return fmt.Sprintf("proc on %q", string(p)) }
func init() { gob.Register(new(MountDevOp)) }
// Dev appends an [Op] that mounts a subset of host /dev.
func (f *Ops) Dev(dest string) *Ops {
*f = append(*f, MountDevOp(dest))
return f
}
type MountDevOp string
func (d MountDevOp) early(*Params) error { return nil }
func (d MountDevOp) apply(params *Params) error {
v := string(d)
if !path.IsAbs(v) {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", v))
}
target := toSysroot(v)
if err := mountTmpfs("devtmpfs", v, 0, params.ParentPerm); err != nil {
return err
}
for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} {
targetPath := toSysroot(path.Join(v, name))
if err := ensureFile(targetPath, 0444, params.ParentPerm); err != nil {
return err
}
if err := hostProc.bindMount(
toHost("/dev/"+name),
targetPath,
0,
true,
); err != nil {
return err
}
}
for i, name := range []string{"stdin", "stdout", "stderr"} {
if err := os.Symlink(
"/proc/self/fd/"+string(rune(i+'0')),
path.Join(target, name),
); err != nil {
return wrapErrSelf(err)
}
}
for _, pair := range [][2]string{
{"/proc/self/fd", "fd"},
{"/proc/kcore", "core"},
{"pts/ptmx", "ptmx"},
} {
if err := os.Symlink(pair[0], path.Join(target, pair[1])); err != nil {
return wrapErrSelf(err)
}
}
devPtsPath := path.Join(target, "pts")
for _, name := range []string{path.Join(target, "shm"), devPtsPath} {
if err := os.Mkdir(name, params.ParentPerm); err != nil {
return wrapErrSelf(err)
}
}
if err := Mount("devpts", devPtsPath, "devpts", MS_NOSUID|MS_NOEXEC,
"newinstance,ptmxmode=0666,mode=620"); err != nil {
return wrapErrSuffix(err,
fmt.Sprintf("cannot mount devpts on %q:", devPtsPath))
}
if params.RetainSession {
var buf [8]byte
if _, _, errno := Syscall(SYS_IOCTL, 1, TIOCGWINSZ, uintptr(unsafe.Pointer(&buf[0]))); errno == 0 {
consolePath := toSysroot(path.Join(v, "console"))
if err := ensureFile(consolePath, 0444, params.ParentPerm); err != nil {
return err
}
if name, err := os.Readlink(hostProc.stdout()); err != nil {
return wrapErrSelf(err)
} else if err = hostProc.bindMount(
toHost(name),
consolePath,
0,
false,
); err != nil {
return err
}
}
}
return nil
}
func (d MountDevOp) Is(op Op) bool { vd, ok := op.(MountDevOp); return ok && d == vd }
func (MountDevOp) prefix() string { return "mounting" }
func (d MountDevOp) String() string { return fmt.Sprintf("dev on %q", string(d)) }
func init() { gob.Register(new(MountMqueueOp)) }
// Mqueue appends an [Op] that mounts a private instance of mqueue.
func (f *Ops) Mqueue(dest string) *Ops {
*f = append(*f, MountMqueueOp(dest))
return f
}
type MountMqueueOp string
func (m MountMqueueOp) early(*Params) error { return nil }
func (m MountMqueueOp) apply(params *Params) error {
v := string(m)
if !path.IsAbs(v) {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", v))
}
target := toSysroot(v)
if err := os.MkdirAll(target, params.ParentPerm); err != nil {
return wrapErrSelf(err)
}
return wrapErrSuffix(Mount("mqueue", target, "mqueue", MS_NOSUID|MS_NOEXEC|MS_NODEV, ""),
fmt.Sprintf("cannot mount mqueue on %q:", v))
}
func (m MountMqueueOp) Is(op Op) bool { vm, ok := op.(MountMqueueOp); return ok && m == vm }
func (MountMqueueOp) prefix() string { return "mounting" }
func (m MountMqueueOp) String() string { return fmt.Sprintf("mqueue on %q", string(m)) }
func init() { gob.Register(new(MountTmpfsOp)) }
// Tmpfs appends an [Op] that mounts tmpfs on container path [MountTmpfsOp.Path].
func (f *Ops) Tmpfs(dest string, size int, perm os.FileMode) *Ops {
*f = append(*f, &MountTmpfsOp{dest, size, perm})
return f
}
type MountTmpfsOp struct {
Path string
Size int
Perm os.FileMode
}
func (t *MountTmpfsOp) early(*Params) error { return nil }
func (t *MountTmpfsOp) apply(*Params) error {
if !path.IsAbs(t.Path) {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", t.Path))
}
if t.Size < 0 || t.Size > math.MaxUint>>1 {
return msg.WrapErr(EBADE, fmt.Sprintf("size %d out of bounds", t.Size))
}
return mountTmpfs("tmpfs", t.Path, t.Size, t.Perm)
}
func (t *MountTmpfsOp) Is(op Op) bool { vt, ok := op.(*MountTmpfsOp); return ok && *t == *vt }
func (*MountTmpfsOp) prefix() string { return "mounting" }
func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }
func init() { gob.Register(new(SymlinkOp)) }
// Link appends an [Op] that creates a symlink in the container filesystem.
func (f *Ops) Link(target, linkName string) *Ops {
*f = append(*f, &SymlinkOp{target, linkName})
return f
}
type SymlinkOp [2]string
func (l *SymlinkOp) early(*Params) error {
if strings.HasPrefix(l[0], "*") {
l[0] = l[0][1:]
if !path.IsAbs(l[0]) {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", l[0]))
}
if name, err := os.Readlink(l[0]); err != nil {
return wrapErrSelf(err)
} else {
l[0] = name
}
}
return nil
}
func (l *SymlinkOp) apply(params *Params) error {
// symlink target is an arbitrary path value, so only validate link name here
if !path.IsAbs(l[1]) {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", l[1]))
}
target := toSysroot(l[1])
if err := os.MkdirAll(path.Dir(target), params.ParentPerm); err != nil {
return wrapErrSelf(err)
}
if err := os.Symlink(l[0], target); err != nil {
return wrapErrSelf(err)
}
return nil
}
func (l *SymlinkOp) Is(op Op) bool { vl, ok := op.(*SymlinkOp); return ok && *l == *vl }
func (*SymlinkOp) prefix() string { return "creating" }
func (l *SymlinkOp) String() string { return fmt.Sprintf("symlink on %q target %q", l[1], l[0]) }
func init() { gob.Register(new(MkdirOp)) }
// Mkdir appends an [Op] that creates a directory in the container filesystem.
func (f *Ops) Mkdir(dest string, perm os.FileMode) *Ops {
*f = append(*f, &MkdirOp{dest, perm})
return f
}
type MkdirOp struct {
Path string
Perm os.FileMode
}
func (m *MkdirOp) early(*Params) error { return nil }
func (m *MkdirOp) apply(*Params) error {
if !path.IsAbs(m.Path) {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", m.Path))
}
if err := os.MkdirAll(toSysroot(m.Path), m.Perm); err != nil {
return wrapErrSelf(err)
}
return nil
}
func (m *MkdirOp) Is(op Op) bool { vm, ok := op.(*MkdirOp); return ok && m == vm }
func (*MkdirOp) prefix() string { return "creating" }
func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) }
func init() { gob.Register(new(TmpfileOp)) }
// Place appends an [Op] that places a file in container path [TmpfileOp.Path] containing [TmpfileOp.Data].
func (f *Ops) Place(name string, data []byte) *Ops { *f = append(*f, &TmpfileOp{name, data}); return f }
// PlaceP is like Place but writes the address of [TmpfileOp.Data] to the pointer dataP points to.
func (f *Ops) PlaceP(name string, dataP **[]byte) *Ops {
t := &TmpfileOp{Path: name}
*dataP = &t.Data
*f = append(*f, t)
return f
}
type TmpfileOp struct {
Path string
Data []byte
}
func (t *TmpfileOp) early(*Params) error { return nil }
func (t *TmpfileOp) apply(params *Params) error {
if !path.IsAbs(t.Path) {
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", t.Path))
}
var tmpPath string
if f, err := os.CreateTemp("/", "tmp.*"); err != nil {
return wrapErrSelf(err)
} else if _, err = f.Write(t.Data); err != nil {
return wrapErrSuffix(err,
"cannot write to intermediate file:")
} else if err = f.Close(); err != nil {
return wrapErrSuffix(err,
"cannot close intermediate file:")
} else {
tmpPath = f.Name()
}
target := toSysroot(t.Path)
if err := ensureFile(target, 0444, params.ParentPerm); err != nil {
return err
} else if err = hostProc.bindMount(
tmpPath,
target,
MS_RDONLY|MS_NODEV,
false,
); err != nil {
return err
} else if err = os.Remove(tmpPath); err != nil {
return wrapErrSelf(err)
}
return nil
}
func (t *TmpfileOp) Is(op Op) bool {
vt, ok := op.(*TmpfileOp)
return ok && t.Path == vt.Path && slices.Equal(t.Data, vt.Data)
}
func (*TmpfileOp) prefix() string { return "placing" }
func (t *TmpfileOp) String() string {
return fmt.Sprintf("tmpfile %q (%d bytes)", t.Path, len(t.Data))
}
func init() { gob.Register(new(AutoEtcOp)) }
// Etc appends an [Op] that expands host /etc into a toplevel symlink mirror with /etc semantics.
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
func (f *Ops) Etc(host, prefix string) *Ops {
e := &AutoEtcOp{prefix}
f.Mkdir("/etc", 0755)
f.Bind(host, e.hostPath(), 0)
*f = append(*f, e)
return f
}
type AutoEtcOp struct{ Prefix string }
func (e *AutoEtcOp) early(*Params) error { return nil }
func (e *AutoEtcOp) 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 *AutoEtcOp) hostPath() string { return "/etc/" + e.hostRel() }
func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix }
func (e *AutoEtcOp) Is(op Op) bool {
ve, ok := op.(*AutoEtcOp)
return ok && ((e == nil && ve == nil) || (e != nil && ve != nil && *e == *ve))
}
func (*AutoEtcOp) prefix() string { return "setting up" }
func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }

View File

@@ -1,4 +1,4 @@
package sandbox
package container
var msg Msg = new(DefaultMsg)

View File

@@ -1,4 +1,4 @@
package sandbox
package container
import (
"encoding/gob"

View File

@@ -1,4 +1,4 @@
package sandbox
package container
import (
"errors"
@@ -10,7 +10,7 @@ import (
"strings"
"syscall"
"git.gensokyo.uk/security/fortify/sandbox/vfs"
"hakurei.app/container/vfs"
)
const (

View File

@@ -0,0 +1,24 @@
package seccomp_test
import . "hakurei.app/container/seccomp"
var bpfExpected = bpfLookup{
{AllowMultiarch | AllowCAN |
AllowBluetooth, PresetExt |
PresetDenyNS | PresetDenyTTY | PresetDenyDevel |
PresetLinux32}: toHash(
"e99dd345e195413473d3cbee07b4ed57b908bfa89ea2072fe93482847f50b5b758da17e74ca2bbc00813de49a2b9bf834c024ed48850be69b68a9a4c5f53a9db"),
{0, 0}: toHash(
"95ec69d017733e072160e0da80fdebecdf27ae8166f5e2a731270c98ea2d2946cb5231029063668af215879155da21aca79b070e04c0ee9acdf58f55cfa815a5"),
{0, PresetExt}: toHash(
"dc7f2e1c5e829b79ebb7efc759150f54a83a75c8df6fee4dce5dadc4736c585d4deebfeb3c7969af3a077e90b77bb4741db05d90997c8659b95891206ac9952d"),
{0, PresetStrict}: toHash(
"e880298df2bd6751d0040fc21bc0ed4c00f95dc0d7ba506c244d8b8cf6866dba8ef4a33296f287b66cccc1d78e97026597f84cc7dec1573e148960fbd35cd735"),
{0, PresetDenyNS | PresetDenyTTY | PresetDenyDevel}: toHash(
"39871b93ffafc8b979fcedc0b0c37b9e03922f5b02748dc5c3c17c92527f6e022ede1f48bff59246ea452c0d1de54827808b1a6f84f32bbde1aa02ae30eedcfa"),
{0, PresetExt | PresetDenyDevel}: toHash(
"c698b081ff957afe17a6d94374537d37f2a63f6f9dd75da7546542407a9e32476ebda3312ba7785d7f618542bcfaf27ca27dcc2dddba852069d28bcfe8cad39a"),
{0, PresetExt | PresetDenyNS | PresetDenyDevel}: toHash(
"0b76007476c1c9e25dbf674c29fdf609a1656a70063e49327654e1b5360ad3da06e1a3e32bf80e961c5516ad83d4b9e7e9bde876a93797e27627d2555c25858b"),
}

View File

@@ -0,0 +1,24 @@
package seccomp_test
import . "hakurei.app/container/seccomp"
var bpfExpected = bpfLookup{
{AllowMultiarch | AllowCAN |
AllowBluetooth, PresetExt |
PresetDenyNS | PresetDenyTTY | PresetDenyDevel |
PresetLinux32}: toHash(
"1431c013f2ddac3adae577821cb5d351b1514e7c754d62346ddffd31f46ea02fb368e46e3f8104f81019617e721fe687ddd83f1e79580622ccc991da12622170"),
{0, 0}: toHash(
"450c21210dbf124dfa7ae56d0130f9c2e24b26f5bce8795ee75766c75850438ff9e7d91c5e73d63bbe51a5d4b06c2a0791c4de2903b2b9805f16265318183235"),
{0, PresetExt}: toHash(
"d971d0f2d30f54ac920fc6d84df2be279e9fd28cf2d48be775d7fdbd790b750e1369401cd3bb8bcf9ba3adb91874fe9792d9e3f62209b8ee59c9fdd2ddd10c7b"),
{0, PresetStrict}: toHash(
"79318538a3dc851314b6bd96f10d5861acb2aa7e13cb8de0619d0f6a76709d67f01ef3fd67e195862b02f9711e5b769bc4d1eb4fc0dfc41a723c89c968a93297"),
{0, PresetDenyNS | PresetDenyTTY | PresetDenyDevel}: toHash(
"228286c2f5df8e44463be0a57b91977b7f38b63b09e5d98dfabe5c61545b8f9ac3e5ea3d86df55d7edf2ce61875f0a5a85c0ab82800bef178c42533e8bdc9a6c"),
{0, PresetExt | PresetDenyDevel}: toHash(
"433ce9b911282d6dcc8029319fb79b816b60d5a795ec8fc94344dd027614d68f023166a91bb881faaeeedd26e3d89474e141e5a69a97e93b8984ca8f14999980"),
{0, PresetExt | PresetDenyNS | PresetDenyDevel}: toHash(
"cf1f4dc87436ba8ec95d268b663a6397bb0b4a5ac64d8557e6cc529d8b0f6f65dad3a92b62ed29d85eee9c6dde1267757a4d0f86032e8a45ca1bceadfa34cf5e"),
}

View File

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

View File

@@ -0,0 +1,130 @@
#ifndef _GNU_SOURCE
#define _GNU_SOURCE /* CLONE_NEWUSER */
#endif
#include "libseccomp-helper.h"
#include <assert.h>
#include <errno.h>
#include <sys/socket.h>
#define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch,
uint32_t multiarch,
struct hakurei_syscall_rule *rules,
size_t rules_sz, hakurei_export_flag flags) {
int i;
int last_allowed_family;
int disallowed;
struct hakurei_syscall_rule *rule;
int32_t res = 0; /* refer to resPrefix for message */
/* Blocklist all but unix, inet, inet6 and netlink */
struct {
int family;
hakurei_export_flag flags_mask;
} socket_family_allowlist[] = {
/* NOTE: Keep in numerical order */
{AF_UNSPEC, 0},
{AF_LOCAL, 0},
{AF_INET, 0},
{AF_INET6, 0},
{AF_NETLINK, 0},
{AF_CAN, HAKUREI_EXPORT_CAN},
{AF_BLUETOOTH, HAKUREI_EXPORT_BLUETOOTH},
};
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
if (ctx == NULL) {
res = 1;
goto out;
} else
errno = 0;
/* We only really need to handle arches on multiarch systems.
* If only one arch is supported the default is fine */
if (arch != 0) {
/* This *adds* the target arch, instead of replacing the
* native one. This is not ideal, because we'd like to only
* allow the target arch, but we can't really disallow the
* native arch at this point, because then bubblewrap
* couldn't continue running. */
*ret_p = seccomp_arch_add(ctx, arch);
if (*ret_p < 0 && *ret_p != -EEXIST) {
res = 2;
goto out;
}
if (flags & HAKUREI_EXPORT_MULTIARCH && multiarch != 0) {
*ret_p = seccomp_arch_add(ctx, multiarch);
if (*ret_p < 0 && *ret_p != -EEXIST) {
res = 3;
goto out;
}
}
}
for (i = 0; i < rules_sz; i++) {
rule = &rules[i];
assert(rule->m_errno == EPERM || rule->m_errno == ENOSYS);
if (rule->arg)
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno),
rule->syscall, 1, *rule->arg);
else
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno),
rule->syscall, 0);
if (*ret_p == -EFAULT) {
res = 4;
goto out;
} else if (*ret_p < 0) {
res = 5;
goto out;
}
}
/* Socket filtering doesn't work on e.g. i386, so ignore failures here
* However, we need to user seccomp_rule_add_exact to avoid libseccomp doing
* something else: https://github.com/seccomp/libseccomp/issues/8 */
last_allowed_family = -1;
for (i = 0; i < LEN(socket_family_allowlist); i++) {
if (socket_family_allowlist[i].flags_mask != 0 &&
(socket_family_allowlist[i].flags_mask & flags) !=
socket_family_allowlist[i].flags_mask)
continue;
for (disallowed = last_allowed_family + 1;
disallowed < socket_family_allowlist[i].family; disallowed++) {
/* Blocklist the in-between valid families */
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT),
SCMP_SYS(socket), 1,
SCMP_A0(SCMP_CMP_EQ, disallowed));
}
last_allowed_family = socket_family_allowlist[i].family;
}
/* Blocklist the rest */
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1,
SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1));
if (fd < 0) {
*ret_p = seccomp_load(ctx);
if (*ret_p != 0) {
res = 7;
goto out;
}
} else {
*ret_p = seccomp_export_bpf(ctx, fd);
if (*ret_p != 0) {
res = 6;
goto out;
}
}
out:
if (ctx)
seccomp_release(ctx);
return res;
}

View File

@@ -0,0 +1,24 @@
#include <seccomp.h>
#include <stdint.h>
#if (SCMP_VER_MAJOR < 2) || (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR < 5) || \
(SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR == 5 && SCMP_VER_MICRO < 1)
#error This package requires libseccomp >= v2.5.1
#endif
typedef enum {
HAKUREI_EXPORT_MULTIARCH = 1 << 0,
HAKUREI_EXPORT_CAN = 1 << 1,
HAKUREI_EXPORT_BLUETOOTH = 1 << 2,
} hakurei_export_flag;
struct hakurei_syscall_rule {
int syscall;
int m_errno;
struct scmp_arg_cmp *arg;
};
int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch,
uint32_t multiarch,
struct hakurei_syscall_rule *rules,
size_t rules_sz, hakurei_export_flag flags);

View File

@@ -0,0 +1,194 @@
package seccomp
/*
#cgo linux pkg-config: --static libseccomp
#include <libseccomp-helper.h>
#include <sys/personality.h>
*/
import "C"
import (
"errors"
"fmt"
"runtime"
"syscall"
"unsafe"
)
const (
PER_LINUX = C.PER_LINUX
PER_LINUX32 = C.PER_LINUX32
)
var (
ErrInvalidRules = errors.New("invalid native rules slice")
)
// LibraryError represents a libseccomp error.
type LibraryError struct {
Prefix string
Seccomp syscall.Errno
Errno error
}
func (e *LibraryError) Error() string {
if e.Seccomp == 0 {
if e.Errno == nil {
panic("invalid libseccomp error")
}
return fmt.Sprintf("%s: %s", e.Prefix, e.Errno)
}
if e.Errno == nil {
return fmt.Sprintf("%s: %s", e.Prefix, e.Seccomp)
}
return fmt.Sprintf("%s: %s (%s)", e.Prefix, e.Seccomp, e.Errno)
}
func (e *LibraryError) Is(err error) bool {
if e == nil {
return err == nil
}
if ef, ok := err.(*LibraryError); ok {
return *e == *ef
}
return (e.Seccomp != 0 && errors.Is(err, e.Seccomp)) ||
(e.Errno != nil && errors.Is(err, e.Errno))
}
type (
ScmpSyscall = C.int
ScmpErrno = C.int
)
// A NativeRule specifies an arch-specific action taken by seccomp under certain conditions.
type NativeRule struct {
// Syscall is the arch-dependent syscall number to act against.
Syscall ScmpSyscall
// Errno is the errno value to return when the condition is satisfied.
Errno ScmpErrno
// Arg is the optional struct scmp_arg_cmp passed to libseccomp.
Arg *ScmpArgCmp
}
type ExportFlag = C.hakurei_export_flag
const (
// AllowMultiarch allows multiarch/emulation.
AllowMultiarch ExportFlag = C.HAKUREI_EXPORT_MULTIARCH
// AllowCAN allows AF_CAN.
AllowCAN ExportFlag = C.HAKUREI_EXPORT_CAN
// AllowBluetooth allows AF_BLUETOOTH.
AllowBluetooth ExportFlag = C.HAKUREI_EXPORT_BLUETOOTH
)
var resPrefix = [...]string{
0: "",
1: "seccomp_init failed",
2: "seccomp_arch_add failed",
3: "seccomp_arch_add failed (multiarch)",
4: "internal libseccomp failure",
5: "seccomp_rule_add failed",
6: "seccomp_export_bpf failed",
7: "seccomp_load failed",
}
// Export streams filter contents to fd, or installs it to the current process if fd < 0.
func Export(fd int, rules []NativeRule, flags ExportFlag) error {
if len(rules) == 0 {
return ErrInvalidRules
}
var (
arch C.uint32_t = 0
multiarch C.uint32_t = 0
)
switch runtime.GOARCH {
case "386":
arch = C.SCMP_ARCH_X86
case "amd64":
arch = C.SCMP_ARCH_X86_64
multiarch = C.SCMP_ARCH_X86
case "arm":
arch = C.SCMP_ARCH_ARM
case "arm64":
arch = C.SCMP_ARCH_AARCH64
multiarch = C.SCMP_ARCH_ARM
}
var ret C.int
rulesPinner := new(runtime.Pinner)
for i := range rules {
rule := &rules[i]
rulesPinner.Pin(rule)
if rule.Arg != nil {
rulesPinner.Pin(rule.Arg)
}
}
res, err := C.hakurei_export_filter(
&ret, C.int(fd),
arch, multiarch,
(*C.struct_hakurei_syscall_rule)(unsafe.Pointer(&rules[0])),
C.size_t(len(rules)),
flags,
)
rulesPinner.Unpin()
if prefix := resPrefix[res]; prefix != "" {
return &LibraryError{
prefix,
-syscall.Errno(ret),
err,
}
}
return err
}
// ScmpCompare is the equivalent of scmp_compare;
// Comparison operators
type ScmpCompare = C.enum_scmp_compare
const (
_SCMP_CMP_MIN = C._SCMP_CMP_MIN
// not equal
SCMP_CMP_NE = C.SCMP_CMP_NE
// less than
SCMP_CMP_LT = C.SCMP_CMP_LT
// less than or equal
SCMP_CMP_LE = C.SCMP_CMP_LE
// equal
SCMP_CMP_EQ = C.SCMP_CMP_EQ
// greater than or equal
SCMP_CMP_GE = C.SCMP_CMP_GE
// greater than
SCMP_CMP_GT = C.SCMP_CMP_GT
// masked equality
SCMP_CMP_MASKED_EQ = C.SCMP_CMP_MASKED_EQ
_SCMP_CMP_MAX = C._SCMP_CMP_MAX
)
// ScmpDatum is the equivalent of scmp_datum_t;
// Argument datum
type ScmpDatum uint64
// ScmpArgCmp is the equivalent of struct scmp_arg_cmp;
// Argument / Value comparison definition
type ScmpArgCmp struct {
// argument number, starting at 0
Arg C.uint
// the comparison op, e.g. SCMP_CMP_*
Op ScmpCompare
DatumA, DatumB ScmpDatum
}
// only used for testing
func syscallResolveName(s string) (trap int) {
v := C.CString(s)
trap = int(C.seccomp_syscall_resolve_name(v))
C.free(unsafe.Pointer(v))
return
}

View File

@@ -0,0 +1,94 @@
package seccomp_test
import (
"crypto/sha512"
"errors"
"io"
"slices"
"syscall"
"testing"
. "hakurei.app/container/seccomp"
)
func TestExport(t *testing.T) {
testCases := []struct {
name string
flags ExportFlag
presets FilterPreset
wantErr bool
}{
{"everything", AllowMultiarch | AllowCAN |
AllowBluetooth, PresetExt |
PresetDenyNS | PresetDenyTTY | PresetDenyDevel |
PresetLinux32, false},
{"compat", 0, 0, false},
{"base", 0, PresetExt, false},
{"strict", 0, PresetStrict, false},
{"strict compat", 0, PresetDenyNS | PresetDenyTTY | PresetDenyDevel, false},
{"hakurei default", 0, PresetExt | PresetDenyDevel, false},
{"hakurei tty", 0, PresetExt | PresetDenyNS | PresetDenyDevel, false},
}
buf := make([]byte, 8)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
e := New(Preset(tc.presets, tc.flags), tc.flags)
want := bpfExpected[bpfPreset{tc.flags, tc.presets}]
digest := sha512.New()
if _, err := io.CopyBuffer(digest, e, buf); (err != nil) != tc.wantErr {
t.Errorf("Exporter: error = %v, wantErr %v", err, tc.wantErr)
return
}
if err := e.Close(); err != nil {
t.Errorf("Close: error = %v", err)
}
if got := digest.Sum(nil); !slices.Equal(got, want) {
t.Fatalf("Export() hash = %x, want %x",
got, want)
return
}
})
}
t.Run("close without use", func(t *testing.T) {
e := New(Preset(0, 0), 0)
if err := e.Close(); !errors.Is(err, syscall.EINVAL) {
t.Errorf("Close: error = %v", err)
return
}
})
t.Run("close partial read", func(t *testing.T) {
e := New(Preset(0, 0), 0)
if _, err := e.Read(nil); err != nil {
t.Errorf("Read: error = %v", err)
return
}
// the underlying implementation uses buffered io, so the outcome of this is nondeterministic;
// that is not harmful however, so both outcomes are checked for here
if err := e.Close(); err != nil &&
(!errors.Is(err, syscall.ECANCELED) || !errors.Is(err, syscall.EBADF)) {
t.Errorf("Close: error = %v", err)
return
}
})
}
func BenchmarkExport(b *testing.B) {
buf := make([]byte, 8)
for b.Loop() {
e := New(
Preset(PresetExt|PresetDenyNS|PresetDenyTTY|PresetDenyDevel|PresetLinux32,
AllowMultiarch|AllowCAN|AllowBluetooth),
AllowMultiarch|AllowCAN|AllowBluetooth)
if _, err := io.CopyBuffer(io.Discard, e, buf); err != nil {
b.Fatalf("cannot export: %v", err)
}
if err := e.Close(); err != nil {
b.Fatalf("cannot close exporter: %v", err)
}
}
}

View File

@@ -0,0 +1,89 @@
#!/usr/bin/env perl
# Copyright 2009 The Go Authors. All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
use strict;
use POSIX ();
my $command = "mksysnum_linux.pl ". join(' ', @ARGV);
my $uname_arch = (POSIX::uname)[4];
my %syscall_cutoff_arch = (
"x86_64" => 302,
"aarch64" => 281,
);
print <<EOF;
// $command
// Code generated by the command above; DO NOT EDIT.
package seccomp
import . "syscall"
var syscallNum = map[string]int{
EOF
my $offset = 0;
my $state = -1;
sub fmt {
my ($name, $num) = @_;
if($num > 999){
# ignore deprecated syscalls that are no longer implemented
# https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/tree/include/uapi/asm-generic/unistd.h?id=refs/heads/master#n716
return;
}
(my $name_upper = $name) =~ y/a-z/A-Z/;
$num = $num + $offset;
if($num > $syscall_cutoff_arch{$uname_arch}){ # not wired in Go standard library
if($state < 0){
print " \"$name\": SYS_$name_upper,\n";
}
else{
print " SYS_$name_upper = $num;\n";
}
}
elsif($state < 0){
print " \"$name\": SYS_$name_upper,\n";
}
else{
return;
}
}
GENERATE:
my $prev;
open(GCC, "gcc -E -dD $ARGV[0] |") || die "can't run gcc";
while(<GCC>){
if(/^#define __NR_Linux\s+([0-9]+)/){
# mips/mips64: extract offset
$offset = $1;
}
elsif(/^#define __NR_syscalls\s+/) {
# ignore redefinitions of __NR_syscalls
}
elsif(/^#define __NR_(\w+)\s+([0-9]+)/){
$prev = $2;
fmt($1, $2);
}
elsif(/^#define __NR3264_(\w+)\s+([0-9]+)/){
$prev = $2;
fmt($1, $2);
}
elsif(/^#define __NR_(\w+)\s+\(\w+\+\s*([0-9]+)\)/){
fmt($1, $prev+$2)
}
elsif(/^#define __NR_(\w+)\s+\(__NR_Linux \+ ([0-9]+)/){
fmt($1, $2);
}
}
if($state < 0){
$state = $state + 1;
print "}\n\nconst (\n";
goto GENERATE;
}
print ")";

View File

@@ -0,0 +1,229 @@
package seccomp
/* flatpak commit 4c3bf179e2e4a2a298cd1db1d045adaf3f564532 */
import (
. "syscall"
)
type FilterPreset int
const (
// PresetExt are project-specific extensions.
PresetExt FilterPreset = 1 << iota
// PresetDenyNS denies namespace setup syscalls.
PresetDenyNS
// PresetDenyTTY denies faking input.
PresetDenyTTY
// PresetDenyDevel denies development-related syscalls.
PresetDenyDevel
// PresetLinux32 sets PER_LINUX32.
PresetLinux32
)
func Preset(presets FilterPreset, flags ExportFlag) (rules []NativeRule) {
allowedPersonality := PER_LINUX
if presets&PresetLinux32 != 0 {
allowedPersonality = PER_LINUX32
}
presetDevelFinal := presetDevel(ScmpDatum(allowedPersonality))
l := len(presetCommon)
if presets&PresetDenyNS != 0 {
l += len(presetNamespace)
}
if presets&PresetDenyTTY != 0 {
l += len(presetTTY)
}
if presets&PresetDenyDevel != 0 {
l += len(presetDevelFinal)
}
if flags&AllowMultiarch == 0 {
l += len(presetEmu)
}
if presets&PresetExt != 0 {
l += len(presetCommonExt)
if presets&PresetDenyNS != 0 {
l += len(presetNamespaceExt)
}
if flags&AllowMultiarch == 0 {
l += len(presetEmuExt)
}
}
rules = make([]NativeRule, 0, l)
rules = append(rules, presetCommon...)
if presets&PresetDenyNS != 0 {
rules = append(rules, presetNamespace...)
}
if presets&PresetDenyTTY != 0 {
rules = append(rules, presetTTY...)
}
if presets&PresetDenyDevel != 0 {
rules = append(rules, presetDevelFinal...)
}
if flags&AllowMultiarch == 0 {
rules = append(rules, presetEmu...)
}
if presets&PresetExt != 0 {
rules = append(rules, presetCommonExt...)
if presets&PresetDenyNS != 0 {
rules = append(rules, presetNamespaceExt...)
}
if flags&AllowMultiarch == 0 {
rules = append(rules, presetEmuExt...)
}
}
return
}
var (
presetCommon = []NativeRule{
/* Block dmesg */
{ScmpSyscall(SYS_SYSLOG), ScmpErrno(EPERM), nil},
/* Useless old syscall */
{ScmpSyscall(SYS_USELIB), ScmpErrno(EPERM), nil},
/* Don't allow disabling accounting */
{ScmpSyscall(SYS_ACCT), ScmpErrno(EPERM), nil},
/* Don't allow reading current quota use */
{ScmpSyscall(SYS_QUOTACTL), ScmpErrno(EPERM), nil},
/* Don't allow access to the kernel keyring */
{ScmpSyscall(SYS_ADD_KEY), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_KEYCTL), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_REQUEST_KEY), ScmpErrno(EPERM), nil},
/* Scary VM/NUMA ops */
{ScmpSyscall(SYS_MOVE_PAGES), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_MBIND), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_GET_MEMPOLICY), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SET_MEMPOLICY), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_MIGRATE_PAGES), ScmpErrno(EPERM), nil},
}
/* hakurei: project-specific extensions */
presetCommonExt = []NativeRule{
/* system calls for changing the system clock */
{ScmpSyscall(SYS_ADJTIMEX), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_CLOCK_ADJTIME), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_CLOCK_ADJTIME64), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_CLOCK_SETTIME), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_CLOCK_SETTIME64), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETTIMEOFDAY), ScmpErrno(EPERM), nil},
/* loading and unloading of kernel modules */
{ScmpSyscall(SYS_DELETE_MODULE), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_FINIT_MODULE), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_INIT_MODULE), ScmpErrno(EPERM), nil},
/* system calls for rebooting and reboot preparation */
{ScmpSyscall(SYS_KEXEC_FILE_LOAD), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_KEXEC_LOAD), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_REBOOT), ScmpErrno(EPERM), nil},
/* system calls for enabling/disabling swap devices */
{ScmpSyscall(SYS_SWAPOFF), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SWAPON), ScmpErrno(EPERM), nil},
}
presetNamespace = []NativeRule{
/* Don't allow subnamespace setups: */
{ScmpSyscall(SYS_UNSHARE), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETNS), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_MOUNT), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_UMOUNT), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_UMOUNT2), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_PIVOT_ROOT), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_CHROOT), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_CLONE), ScmpErrno(EPERM),
&ScmpArgCmp{cloneArg, SCMP_CMP_MASKED_EQ, CLONE_NEWUSER, CLONE_NEWUSER}},
/* seccomp can't look into clone3()'s struct clone_args to check whether
* the flags are OK, so we have no choice but to block clone3().
* Return ENOSYS so user-space will fall back to clone().
* (CVE-2021-41133; see also https://github.com/moby/moby/commit/9f6b562d)
*/
{ScmpSyscall(SYS_CLONE3), ScmpErrno(ENOSYS), nil},
/* New mount manipulation APIs can also change our VFS. There's no
* legitimate reason to do these in the sandbox, so block all of them
* rather than thinking about which ones might be dangerous.
* (CVE-2021-41133) */
{ScmpSyscall(SYS_OPEN_TREE), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_MOVE_MOUNT), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_FSOPEN), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_FSCONFIG), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_FSMOUNT), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_FSPICK), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_MOUNT_SETATTR), ScmpErrno(ENOSYS), nil},
}
/* hakurei: project-specific extensions */
presetNamespaceExt = []NativeRule{
/* changing file ownership */
{ScmpSyscall(SYS_CHOWN), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_CHOWN32), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_FCHOWN), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_FCHOWN32), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_FCHOWNAT), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_LCHOWN), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_LCHOWN32), ScmpErrno(EPERM), nil},
/* system calls for changing user ID and group ID credentials */
{ScmpSyscall(SYS_SETGID), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETGID32), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETGROUPS), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETGROUPS32), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETREGID), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETREGID32), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETRESGID), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETRESGID32), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETRESUID), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETRESUID32), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETREUID), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETREUID32), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETUID), ScmpErrno(EPERM), nil},
{ScmpSyscall(SYS_SETUID32), ScmpErrno(EPERM), nil},
}
presetTTY = []NativeRule{
/* Don't allow faking input to the controlling tty (CVE-2017-5226) */
{ScmpSyscall(SYS_IOCTL), ScmpErrno(EPERM),
&ScmpArgCmp{1, SCMP_CMP_MASKED_EQ, 0xFFFFFFFF, TIOCSTI}},
/* In the unlikely event that the controlling tty is a Linux virtual
* console (/dev/tty2 or similar), copy/paste operations have an effect
* similar to TIOCSTI (CVE-2023-28100) */
{ScmpSyscall(SYS_IOCTL), ScmpErrno(EPERM),
&ScmpArgCmp{1, SCMP_CMP_MASKED_EQ, 0xFFFFFFFF, TIOCLINUX}},
}
presetEmu = []NativeRule{
/* modify_ldt is a historic source of interesting information leaks,
* so it's disabled as a hardening measure.
* However, it is required to run old 16-bit applications
* as well as some Wine patches, so it's allowed in multiarch. */
{ScmpSyscall(SYS_MODIFY_LDT), ScmpErrno(EPERM), nil},
}
/* hakurei: project-specific extensions */
presetEmuExt = []NativeRule{
{ScmpSyscall(SYS_SUBPAGE_PROT), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_SWITCH_ENDIAN), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_VM86), ScmpErrno(ENOSYS), nil},
{ScmpSyscall(SYS_VM86OLD), ScmpErrno(ENOSYS), nil},
}
)
func presetDevel(allowedPersonality ScmpDatum) []NativeRule {
return []NativeRule{
/* Profiling operations; we expect these to be done by tools from outside
* the sandbox. In particular perf has been the source of many CVEs. */
{ScmpSyscall(SYS_PERF_EVENT_OPEN), ScmpErrno(EPERM), nil},
/* Don't allow you to switch to bsd emulation or whatnot */
{ScmpSyscall(SYS_PERSONALITY), ScmpErrno(EPERM),
&ScmpArgCmp{0, SCMP_CMP_NE, allowedPersonality, 0}},
{ScmpSyscall(SYS_PTRACE), ScmpErrno(EPERM), nil},
}
}

View File

@@ -0,0 +1,7 @@
//go:build s390 || s390x
package seccomp
/* Architectures with CONFIG_CLONE_BACKWARDS2: the child stack
* and flags arguments are reversed so the flags come second */
const cloneArg = 1

View File

@@ -0,0 +1,6 @@
//go:build !s390 && !s390x
package seccomp
/* Normally the flags come first */
const cloneArg = 0

View File

@@ -5,14 +5,18 @@ import (
"errors"
"syscall"
"git.gensokyo.uk/security/fortify/helper/proc"
"hakurei.app/helper/proc"
)
const (
PresetStrict = PresetExt | PresetDenyNS | PresetDenyTTY | PresetDenyDevel
)
// New returns an inactive Encoder instance.
func New(opts SyscallOpts) *Encoder { return &Encoder{newExporter(opts)} }
func New(rules []NativeRule, flags ExportFlag) *Encoder { return &Encoder{newExporter(rules, flags)} }
// Load loads a filter into the kernel.
func Load(opts SyscallOpts) error { return buildFilter(-1, opts) }
func Load(rules []NativeRule, flags ExportFlag) error { return Export(-1, rules, flags) }
/*
An Encoder writes a BPF program to an output stream.
@@ -42,17 +46,20 @@ func (e *Encoder) Close() error {
}
// NewFile returns an instance of exporter implementing [proc.File].
func NewFile(opts SyscallOpts) proc.File { return &File{opts: opts} }
func NewFile(rules []NativeRule, flags ExportFlag) proc.File {
return &File{rules: rules, flags: flags}
}
// File implements [proc.File] and provides access to the read end of exporter pipe.
type File struct {
opts SyscallOpts
rules []NativeRule
flags ExportFlag
proc.BaseFile
}
func (f *File) ErrCount() int { return 2 }
func (f *File) Fulfill(ctx context.Context, dispatchErr func(error)) error {
e := newExporter(f.opts)
e := newExporter(f.rules, f.flags)
if err := e.prepare(); err != nil {
return err
}

View File

@@ -1,3 +1,4 @@
// Package seccomp provides high level wrappers around libseccomp.
package seccomp
import (
@@ -7,8 +8,9 @@ import (
)
type exporter struct {
opts SyscallOpts
r, w *os.File
rules []NativeRule
flags ExportFlag
r, w *os.File
prepareOnce sync.Once
prepareErr error
@@ -28,7 +30,7 @@ func (e *exporter) prepare() error {
ec := make(chan error, 1)
go func(fd uintptr) {
ec <- buildFilter(int(fd), e.opts)
ec <- Export(int(fd), e.rules, e.flags)
close(ec)
_ = e.closeWrite()
runtime.KeepAlive(e.w)
@@ -53,6 +55,6 @@ func (e *exporter) closeWrite() error {
return e.closeErr
}
func newExporter(opts SyscallOpts) *exporter {
return &exporter{opts: opts}
func newExporter(rules []NativeRule, flags ExportFlag) *exporter {
return &exporter{rules: rules, flags: flags}
}

View File

@@ -6,7 +6,7 @@ import (
"syscall"
"testing"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
"hakurei.app/container/seccomp"
)
func TestLibraryError(t *testing.T) {

View File

@@ -0,0 +1,28 @@
package seccomp
import "iter"
// Syscalls returns an iterator over all wired syscalls.
func Syscalls() iter.Seq2[string, int] {
return func(yield func(string, int) bool) {
for name, num := range syscallNum {
if !yield(name, num) {
return
}
}
for name, num := range syscallNumExtra {
if !yield(name, num) {
return
}
}
}
}
// SyscallResolveName resolves a syscall number from its string representation.
func SyscallResolveName(name string) (num int, ok bool) {
if num, ok = syscallNum[name]; ok {
return
}
num, ok = syscallNumExtra[name]
return
}

View File

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

View File

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

View File

@@ -0,0 +1,459 @@
// mksysnum_linux.pl /usr/include/asm/unistd_64.h
// Code generated by the command above; DO NOT EDIT.
package seccomp
import . "syscall"
var syscallNum = map[string]int{
"read": SYS_READ,
"write": SYS_WRITE,
"open": SYS_OPEN,
"close": SYS_CLOSE,
"stat": SYS_STAT,
"fstat": SYS_FSTAT,
"lstat": SYS_LSTAT,
"poll": SYS_POLL,
"lseek": SYS_LSEEK,
"mmap": SYS_MMAP,
"mprotect": SYS_MPROTECT,
"munmap": SYS_MUNMAP,
"brk": SYS_BRK,
"rt_sigaction": SYS_RT_SIGACTION,
"rt_sigprocmask": SYS_RT_SIGPROCMASK,
"rt_sigreturn": SYS_RT_SIGRETURN,
"ioctl": SYS_IOCTL,
"pread64": SYS_PREAD64,
"pwrite64": SYS_PWRITE64,
"readv": SYS_READV,
"writev": SYS_WRITEV,
"access": SYS_ACCESS,
"pipe": SYS_PIPE,
"select": SYS_SELECT,
"sched_yield": SYS_SCHED_YIELD,
"mremap": SYS_MREMAP,
"msync": SYS_MSYNC,
"mincore": SYS_MINCORE,
"madvise": SYS_MADVISE,
"shmget": SYS_SHMGET,
"shmat": SYS_SHMAT,
"shmctl": SYS_SHMCTL,
"dup": SYS_DUP,
"dup2": SYS_DUP2,
"pause": SYS_PAUSE,
"nanosleep": SYS_NANOSLEEP,
"getitimer": SYS_GETITIMER,
"alarm": SYS_ALARM,
"setitimer": SYS_SETITIMER,
"getpid": SYS_GETPID,
"sendfile": SYS_SENDFILE,
"socket": SYS_SOCKET,
"connect": SYS_CONNECT,
"accept": SYS_ACCEPT,
"sendto": SYS_SENDTO,
"recvfrom": SYS_RECVFROM,
"sendmsg": SYS_SENDMSG,
"recvmsg": SYS_RECVMSG,
"shutdown": SYS_SHUTDOWN,
"bind": SYS_BIND,
"listen": SYS_LISTEN,
"getsockname": SYS_GETSOCKNAME,
"getpeername": SYS_GETPEERNAME,
"socketpair": SYS_SOCKETPAIR,
"setsockopt": SYS_SETSOCKOPT,
"getsockopt": SYS_GETSOCKOPT,
"clone": SYS_CLONE,
"fork": SYS_FORK,
"vfork": SYS_VFORK,
"execve": SYS_EXECVE,
"exit": SYS_EXIT,
"wait4": SYS_WAIT4,
"kill": SYS_KILL,
"uname": SYS_UNAME,
"semget": SYS_SEMGET,
"semop": SYS_SEMOP,
"semctl": SYS_SEMCTL,
"shmdt": SYS_SHMDT,
"msgget": SYS_MSGGET,
"msgsnd": SYS_MSGSND,
"msgrcv": SYS_MSGRCV,
"msgctl": SYS_MSGCTL,
"fcntl": SYS_FCNTL,
"flock": SYS_FLOCK,
"fsync": SYS_FSYNC,
"fdatasync": SYS_FDATASYNC,
"truncate": SYS_TRUNCATE,
"ftruncate": SYS_FTRUNCATE,
"getdents": SYS_GETDENTS,
"getcwd": SYS_GETCWD,
"chdir": SYS_CHDIR,
"fchdir": SYS_FCHDIR,
"rename": SYS_RENAME,
"mkdir": SYS_MKDIR,
"rmdir": SYS_RMDIR,
"creat": SYS_CREAT,
"link": SYS_LINK,
"unlink": SYS_UNLINK,
"symlink": SYS_SYMLINK,
"readlink": SYS_READLINK,
"chmod": SYS_CHMOD,
"fchmod": SYS_FCHMOD,
"chown": SYS_CHOWN,
"fchown": SYS_FCHOWN,
"lchown": SYS_LCHOWN,
"umask": SYS_UMASK,
"gettimeofday": SYS_GETTIMEOFDAY,
"getrlimit": SYS_GETRLIMIT,
"getrusage": SYS_GETRUSAGE,
"sysinfo": SYS_SYSINFO,
"times": SYS_TIMES,
"ptrace": SYS_PTRACE,
"getuid": SYS_GETUID,
"syslog": SYS_SYSLOG,
"getgid": SYS_GETGID,
"setuid": SYS_SETUID,
"setgid": SYS_SETGID,
"geteuid": SYS_GETEUID,
"getegid": SYS_GETEGID,
"setpgid": SYS_SETPGID,
"getppid": SYS_GETPPID,
"getpgrp": SYS_GETPGRP,
"setsid": SYS_SETSID,
"setreuid": SYS_SETREUID,
"setregid": SYS_SETREGID,
"getgroups": SYS_GETGROUPS,
"setgroups": SYS_SETGROUPS,
"setresuid": SYS_SETRESUID,
"getresuid": SYS_GETRESUID,
"setresgid": SYS_SETRESGID,
"getresgid": SYS_GETRESGID,
"getpgid": SYS_GETPGID,
"setfsuid": SYS_SETFSUID,
"setfsgid": SYS_SETFSGID,
"getsid": SYS_GETSID,
"capget": SYS_CAPGET,
"capset": SYS_CAPSET,
"rt_sigpending": SYS_RT_SIGPENDING,
"rt_sigtimedwait": SYS_RT_SIGTIMEDWAIT,
"rt_sigqueueinfo": SYS_RT_SIGQUEUEINFO,
"rt_sigsuspend": SYS_RT_SIGSUSPEND,
"sigaltstack": SYS_SIGALTSTACK,
"utime": SYS_UTIME,
"mknod": SYS_MKNOD,
"uselib": SYS_USELIB,
"personality": SYS_PERSONALITY,
"ustat": SYS_USTAT,
"statfs": SYS_STATFS,
"fstatfs": SYS_FSTATFS,
"sysfs": SYS_SYSFS,
"getpriority": SYS_GETPRIORITY,
"setpriority": SYS_SETPRIORITY,
"sched_setparam": SYS_SCHED_SETPARAM,
"sched_getparam": SYS_SCHED_GETPARAM,
"sched_setscheduler": SYS_SCHED_SETSCHEDULER,
"sched_getscheduler": SYS_SCHED_GETSCHEDULER,
"sched_get_priority_max": SYS_SCHED_GET_PRIORITY_MAX,
"sched_get_priority_min": SYS_SCHED_GET_PRIORITY_MIN,
"sched_rr_get_interval": SYS_SCHED_RR_GET_INTERVAL,
"mlock": SYS_MLOCK,
"munlock": SYS_MUNLOCK,
"mlockall": SYS_MLOCKALL,
"munlockall": SYS_MUNLOCKALL,
"vhangup": SYS_VHANGUP,
"modify_ldt": SYS_MODIFY_LDT,
"pivot_root": SYS_PIVOT_ROOT,
"_sysctl": SYS__SYSCTL,
"prctl": SYS_PRCTL,
"arch_prctl": SYS_ARCH_PRCTL,
"adjtimex": SYS_ADJTIMEX,
"setrlimit": SYS_SETRLIMIT,
"chroot": SYS_CHROOT,
"sync": SYS_SYNC,
"acct": SYS_ACCT,
"settimeofday": SYS_SETTIMEOFDAY,
"mount": SYS_MOUNT,
"umount2": SYS_UMOUNT2,
"swapon": SYS_SWAPON,
"swapoff": SYS_SWAPOFF,
"reboot": SYS_REBOOT,
"sethostname": SYS_SETHOSTNAME,
"setdomainname": SYS_SETDOMAINNAME,
"iopl": SYS_IOPL,
"ioperm": SYS_IOPERM,
"create_module": SYS_CREATE_MODULE,
"init_module": SYS_INIT_MODULE,
"delete_module": SYS_DELETE_MODULE,
"get_kernel_syms": SYS_GET_KERNEL_SYMS,
"query_module": SYS_QUERY_MODULE,
"quotactl": SYS_QUOTACTL,
"nfsservctl": SYS_NFSSERVCTL,
"getpmsg": SYS_GETPMSG,
"putpmsg": SYS_PUTPMSG,
"afs_syscall": SYS_AFS_SYSCALL,
"tuxcall": SYS_TUXCALL,
"security": SYS_SECURITY,
"gettid": SYS_GETTID,
"readahead": SYS_READAHEAD,
"setxattr": SYS_SETXATTR,
"lsetxattr": SYS_LSETXATTR,
"fsetxattr": SYS_FSETXATTR,
"getxattr": SYS_GETXATTR,
"lgetxattr": SYS_LGETXATTR,
"fgetxattr": SYS_FGETXATTR,
"listxattr": SYS_LISTXATTR,
"llistxattr": SYS_LLISTXATTR,
"flistxattr": SYS_FLISTXATTR,
"removexattr": SYS_REMOVEXATTR,
"lremovexattr": SYS_LREMOVEXATTR,
"fremovexattr": SYS_FREMOVEXATTR,
"tkill": SYS_TKILL,
"time": SYS_TIME,
"futex": SYS_FUTEX,
"sched_setaffinity": SYS_SCHED_SETAFFINITY,
"sched_getaffinity": SYS_SCHED_GETAFFINITY,
"set_thread_area": SYS_SET_THREAD_AREA,
"io_setup": SYS_IO_SETUP,
"io_destroy": SYS_IO_DESTROY,
"io_getevents": SYS_IO_GETEVENTS,
"io_submit": SYS_IO_SUBMIT,
"io_cancel": SYS_IO_CANCEL,
"get_thread_area": SYS_GET_THREAD_AREA,
"lookup_dcookie": SYS_LOOKUP_DCOOKIE,
"epoll_create": SYS_EPOLL_CREATE,
"epoll_ctl_old": SYS_EPOLL_CTL_OLD,
"epoll_wait_old": SYS_EPOLL_WAIT_OLD,
"remap_file_pages": SYS_REMAP_FILE_PAGES,
"getdents64": SYS_GETDENTS64,
"set_tid_address": SYS_SET_TID_ADDRESS,
"restart_syscall": SYS_RESTART_SYSCALL,
"semtimedop": SYS_SEMTIMEDOP,
"fadvise64": SYS_FADVISE64,
"timer_create": SYS_TIMER_CREATE,
"timer_settime": SYS_TIMER_SETTIME,
"timer_gettime": SYS_TIMER_GETTIME,
"timer_getoverrun": SYS_TIMER_GETOVERRUN,
"timer_delete": SYS_TIMER_DELETE,
"clock_settime": SYS_CLOCK_SETTIME,
"clock_gettime": SYS_CLOCK_GETTIME,
"clock_getres": SYS_CLOCK_GETRES,
"clock_nanosleep": SYS_CLOCK_NANOSLEEP,
"exit_group": SYS_EXIT_GROUP,
"epoll_wait": SYS_EPOLL_WAIT,
"epoll_ctl": SYS_EPOLL_CTL,
"tgkill": SYS_TGKILL,
"utimes": SYS_UTIMES,
"vserver": SYS_VSERVER,
"mbind": SYS_MBIND,
"set_mempolicy": SYS_SET_MEMPOLICY,
"get_mempolicy": SYS_GET_MEMPOLICY,
"mq_open": SYS_MQ_OPEN,
"mq_unlink": SYS_MQ_UNLINK,
"mq_timedsend": SYS_MQ_TIMEDSEND,
"mq_timedreceive": SYS_MQ_TIMEDRECEIVE,
"mq_notify": SYS_MQ_NOTIFY,
"mq_getsetattr": SYS_MQ_GETSETATTR,
"kexec_load": SYS_KEXEC_LOAD,
"waitid": SYS_WAITID,
"add_key": SYS_ADD_KEY,
"request_key": SYS_REQUEST_KEY,
"keyctl": SYS_KEYCTL,
"ioprio_set": SYS_IOPRIO_SET,
"ioprio_get": SYS_IOPRIO_GET,
"inotify_init": SYS_INOTIFY_INIT,
"inotify_add_watch": SYS_INOTIFY_ADD_WATCH,
"inotify_rm_watch": SYS_INOTIFY_RM_WATCH,
"migrate_pages": SYS_MIGRATE_PAGES,
"openat": SYS_OPENAT,
"mkdirat": SYS_MKDIRAT,
"mknodat": SYS_MKNODAT,
"fchownat": SYS_FCHOWNAT,
"futimesat": SYS_FUTIMESAT,
"newfstatat": SYS_NEWFSTATAT,
"unlinkat": SYS_UNLINKAT,
"renameat": SYS_RENAMEAT,
"linkat": SYS_LINKAT,
"symlinkat": SYS_SYMLINKAT,
"readlinkat": SYS_READLINKAT,
"fchmodat": SYS_FCHMODAT,
"faccessat": SYS_FACCESSAT,
"pselect6": SYS_PSELECT6,
"ppoll": SYS_PPOLL,
"unshare": SYS_UNSHARE,
"set_robust_list": SYS_SET_ROBUST_LIST,
"get_robust_list": SYS_GET_ROBUST_LIST,
"splice": SYS_SPLICE,
"tee": SYS_TEE,
"sync_file_range": SYS_SYNC_FILE_RANGE,
"vmsplice": SYS_VMSPLICE,
"move_pages": SYS_MOVE_PAGES,
"utimensat": SYS_UTIMENSAT,
"epoll_pwait": SYS_EPOLL_PWAIT,
"signalfd": SYS_SIGNALFD,
"timerfd_create": SYS_TIMERFD_CREATE,
"eventfd": SYS_EVENTFD,
"fallocate": SYS_FALLOCATE,
"timerfd_settime": SYS_TIMERFD_SETTIME,
"timerfd_gettime": SYS_TIMERFD_GETTIME,
"accept4": SYS_ACCEPT4,
"signalfd4": SYS_SIGNALFD4,
"eventfd2": SYS_EVENTFD2,
"epoll_create1": SYS_EPOLL_CREATE1,
"dup3": SYS_DUP3,
"pipe2": SYS_PIPE2,
"inotify_init1": SYS_INOTIFY_INIT1,
"preadv": SYS_PREADV,
"pwritev": SYS_PWRITEV,
"rt_tgsigqueueinfo": SYS_RT_TGSIGQUEUEINFO,
"perf_event_open": SYS_PERF_EVENT_OPEN,
"recvmmsg": SYS_RECVMMSG,
"fanotify_init": SYS_FANOTIFY_INIT,
"fanotify_mark": SYS_FANOTIFY_MARK,
"prlimit64": SYS_PRLIMIT64,
"name_to_handle_at": SYS_NAME_TO_HANDLE_AT,
"open_by_handle_at": SYS_OPEN_BY_HANDLE_AT,
"clock_adjtime": SYS_CLOCK_ADJTIME,
"syncfs": SYS_SYNCFS,
"sendmmsg": SYS_SENDMMSG,
"setns": SYS_SETNS,
"getcpu": SYS_GETCPU,
"process_vm_readv": SYS_PROCESS_VM_READV,
"process_vm_writev": SYS_PROCESS_VM_WRITEV,
"kcmp": SYS_KCMP,
"finit_module": SYS_FINIT_MODULE,
"sched_setattr": SYS_SCHED_SETATTR,
"sched_getattr": SYS_SCHED_GETATTR,
"renameat2": SYS_RENAMEAT2,
"seccomp": SYS_SECCOMP,
"getrandom": SYS_GETRANDOM,
"memfd_create": SYS_MEMFD_CREATE,
"kexec_file_load": SYS_KEXEC_FILE_LOAD,
"bpf": SYS_BPF,
"execveat": SYS_EXECVEAT,
"userfaultfd": SYS_USERFAULTFD,
"membarrier": SYS_MEMBARRIER,
"mlock2": SYS_MLOCK2,
"copy_file_range": SYS_COPY_FILE_RANGE,
"preadv2": SYS_PREADV2,
"pwritev2": SYS_PWRITEV2,
"pkey_mprotect": SYS_PKEY_MPROTECT,
"pkey_alloc": SYS_PKEY_ALLOC,
"pkey_free": SYS_PKEY_FREE,
"statx": SYS_STATX,
"io_pgetevents": SYS_IO_PGETEVENTS,
"rseq": SYS_RSEQ,
"uretprobe": SYS_URETPROBE,
"pidfd_send_signal": SYS_PIDFD_SEND_SIGNAL,
"io_uring_setup": SYS_IO_URING_SETUP,
"io_uring_enter": SYS_IO_URING_ENTER,
"io_uring_register": SYS_IO_URING_REGISTER,
"open_tree": SYS_OPEN_TREE,
"move_mount": SYS_MOVE_MOUNT,
"fsopen": SYS_FSOPEN,
"fsconfig": SYS_FSCONFIG,
"fsmount": SYS_FSMOUNT,
"fspick": SYS_FSPICK,
"pidfd_open": SYS_PIDFD_OPEN,
"clone3": SYS_CLONE3,
"close_range": SYS_CLOSE_RANGE,
"openat2": SYS_OPENAT2,
"pidfd_getfd": SYS_PIDFD_GETFD,
"faccessat2": SYS_FACCESSAT2,
"process_madvise": SYS_PROCESS_MADVISE,
"epoll_pwait2": SYS_EPOLL_PWAIT2,
"mount_setattr": SYS_MOUNT_SETATTR,
"quotactl_fd": SYS_QUOTACTL_FD,
"landlock_create_ruleset": SYS_LANDLOCK_CREATE_RULESET,
"landlock_add_rule": SYS_LANDLOCK_ADD_RULE,
"landlock_restrict_self": SYS_LANDLOCK_RESTRICT_SELF,
"memfd_secret": SYS_MEMFD_SECRET,
"process_mrelease": SYS_PROCESS_MRELEASE,
"futex_waitv": SYS_FUTEX_WAITV,
"set_mempolicy_home_node": SYS_SET_MEMPOLICY_HOME_NODE,
"cachestat": SYS_CACHESTAT,
"fchmodat2": SYS_FCHMODAT2,
"map_shadow_stack": SYS_MAP_SHADOW_STACK,
"futex_wake": SYS_FUTEX_WAKE,
"futex_wait": SYS_FUTEX_WAIT,
"futex_requeue": SYS_FUTEX_REQUEUE,
"statmount": SYS_STATMOUNT,
"listmount": SYS_LISTMOUNT,
"lsm_get_self_attr": SYS_LSM_GET_SELF_ATTR,
"lsm_set_self_attr": SYS_LSM_SET_SELF_ATTR,
"lsm_list_modules": SYS_LSM_LIST_MODULES,
"mseal": SYS_MSEAL,
}
const (
SYS_NAME_TO_HANDLE_AT = 303
SYS_OPEN_BY_HANDLE_AT = 304
SYS_CLOCK_ADJTIME = 305
SYS_SYNCFS = 306
SYS_SENDMMSG = 307
SYS_SETNS = 308
SYS_GETCPU = 309
SYS_PROCESS_VM_READV = 310
SYS_PROCESS_VM_WRITEV = 311
SYS_KCMP = 312
SYS_FINIT_MODULE = 313
SYS_SCHED_SETATTR = 314
SYS_SCHED_GETATTR = 315
SYS_RENAMEAT2 = 316
SYS_SECCOMP = 317
SYS_GETRANDOM = 318
SYS_MEMFD_CREATE = 319
SYS_KEXEC_FILE_LOAD = 320
SYS_BPF = 321
SYS_EXECVEAT = 322
SYS_USERFAULTFD = 323
SYS_MEMBARRIER = 324
SYS_MLOCK2 = 325
SYS_COPY_FILE_RANGE = 326
SYS_PREADV2 = 327
SYS_PWRITEV2 = 328
SYS_PKEY_MPROTECT = 329
SYS_PKEY_ALLOC = 330
SYS_PKEY_FREE = 331
SYS_STATX = 332
SYS_IO_PGETEVENTS = 333
SYS_RSEQ = 334
SYS_URETPROBE = 335
SYS_PIDFD_SEND_SIGNAL = 424
SYS_IO_URING_SETUP = 425
SYS_IO_URING_ENTER = 426
SYS_IO_URING_REGISTER = 427
SYS_OPEN_TREE = 428
SYS_MOVE_MOUNT = 429
SYS_FSOPEN = 430
SYS_FSCONFIG = 431
SYS_FSMOUNT = 432
SYS_FSPICK = 433
SYS_PIDFD_OPEN = 434
SYS_CLONE3 = 435
SYS_CLOSE_RANGE = 436
SYS_OPENAT2 = 437
SYS_PIDFD_GETFD = 438
SYS_FACCESSAT2 = 439
SYS_PROCESS_MADVISE = 440
SYS_EPOLL_PWAIT2 = 441
SYS_MOUNT_SETATTR = 442
SYS_QUOTACTL_FD = 443
SYS_LANDLOCK_CREATE_RULESET = 444
SYS_LANDLOCK_ADD_RULE = 445
SYS_LANDLOCK_RESTRICT_SELF = 446
SYS_MEMFD_SECRET = 447
SYS_PROCESS_MRELEASE = 448
SYS_FUTEX_WAITV = 449
SYS_SET_MEMPOLICY_HOME_NODE = 450
SYS_CACHESTAT = 451
SYS_FCHMODAT2 = 452
SYS_MAP_SHADOW_STACK = 453
SYS_FUTEX_WAKE = 454
SYS_FUTEX_WAIT = 455
SYS_FUTEX_REQUEUE = 456
SYS_STATMOUNT = 457
SYS_LISTMOUNT = 458
SYS_LSM_GET_SELF_ATTR = 459
SYS_LSM_SET_SELF_ATTR = 460
SYS_LSM_LIST_MODULES = 461
SYS_MSEAL = 462
)

View File

@@ -0,0 +1,382 @@
// mksysnum_linux.pl /usr/include/asm/unistd_64.h
// Code generated by the command above; DO NOT EDIT.
package seccomp
import . "syscall"
var syscallNum = map[string]int{
"io_setup": SYS_IO_SETUP,
"io_destroy": SYS_IO_DESTROY,
"io_submit": SYS_IO_SUBMIT,
"io_cancel": SYS_IO_CANCEL,
"io_getevents": SYS_IO_GETEVENTS,
"setxattr": SYS_SETXATTR,
"lsetxattr": SYS_LSETXATTR,
"fsetxattr": SYS_FSETXATTR,
"getxattr": SYS_GETXATTR,
"lgetxattr": SYS_LGETXATTR,
"fgetxattr": SYS_FGETXATTR,
"listxattr": SYS_LISTXATTR,
"llistxattr": SYS_LLISTXATTR,
"flistxattr": SYS_FLISTXATTR,
"removexattr": SYS_REMOVEXATTR,
"lremovexattr": SYS_LREMOVEXATTR,
"fremovexattr": SYS_FREMOVEXATTR,
"getcwd": SYS_GETCWD,
"lookup_dcookie": SYS_LOOKUP_DCOOKIE,
"eventfd2": SYS_EVENTFD2,
"epoll_create1": SYS_EPOLL_CREATE1,
"epoll_ctl": SYS_EPOLL_CTL,
"epoll_pwait": SYS_EPOLL_PWAIT,
"dup": SYS_DUP,
"dup3": SYS_DUP3,
"fcntl": SYS_FCNTL,
"inotify_init1": SYS_INOTIFY_INIT1,
"inotify_add_watch": SYS_INOTIFY_ADD_WATCH,
"inotify_rm_watch": SYS_INOTIFY_RM_WATCH,
"ioctl": SYS_IOCTL,
"ioprio_set": SYS_IOPRIO_SET,
"ioprio_get": SYS_IOPRIO_GET,
"flock": SYS_FLOCK,
"mknodat": SYS_MKNODAT,
"mkdirat": SYS_MKDIRAT,
"unlinkat": SYS_UNLINKAT,
"symlinkat": SYS_SYMLINKAT,
"linkat": SYS_LINKAT,
"renameat": SYS_RENAMEAT,
"umount2": SYS_UMOUNT2,
"mount": SYS_MOUNT,
"pivot_root": SYS_PIVOT_ROOT,
"nfsservctl": SYS_NFSSERVCTL,
"statfs": SYS_STATFS,
"fstatfs": SYS_FSTATFS,
"truncate": SYS_TRUNCATE,
"ftruncate": SYS_FTRUNCATE,
"fallocate": SYS_FALLOCATE,
"faccessat": SYS_FACCESSAT,
"chdir": SYS_CHDIR,
"fchdir": SYS_FCHDIR,
"chroot": SYS_CHROOT,
"fchmod": SYS_FCHMOD,
"fchmodat": SYS_FCHMODAT,
"fchownat": SYS_FCHOWNAT,
"fchown": SYS_FCHOWN,
"openat": SYS_OPENAT,
"close": SYS_CLOSE,
"vhangup": SYS_VHANGUP,
"pipe2": SYS_PIPE2,
"quotactl": SYS_QUOTACTL,
"getdents64": SYS_GETDENTS64,
"lseek": SYS_LSEEK,
"read": SYS_READ,
"write": SYS_WRITE,
"readv": SYS_READV,
"writev": SYS_WRITEV,
"pread64": SYS_PREAD64,
"pwrite64": SYS_PWRITE64,
"preadv": SYS_PREADV,
"pwritev": SYS_PWRITEV,
"sendfile": SYS_SENDFILE,
"pselect6": SYS_PSELECT6,
"ppoll": SYS_PPOLL,
"signalfd4": SYS_SIGNALFD4,
"vmsplice": SYS_VMSPLICE,
"splice": SYS_SPLICE,
"tee": SYS_TEE,
"readlinkat": SYS_READLINKAT,
"newfstatat": SYS_NEWFSTATAT,
"fstat": SYS_FSTAT,
"sync": SYS_SYNC,
"fsync": SYS_FSYNC,
"fdatasync": SYS_FDATASYNC,
"sync_file_range": SYS_SYNC_FILE_RANGE,
"timerfd_create": SYS_TIMERFD_CREATE,
"timerfd_settime": SYS_TIMERFD_SETTIME,
"timerfd_gettime": SYS_TIMERFD_GETTIME,
"utimensat": SYS_UTIMENSAT,
"acct": SYS_ACCT,
"capget": SYS_CAPGET,
"capset": SYS_CAPSET,
"personality": SYS_PERSONALITY,
"exit": SYS_EXIT,
"exit_group": SYS_EXIT_GROUP,
"waitid": SYS_WAITID,
"set_tid_address": SYS_SET_TID_ADDRESS,
"unshare": SYS_UNSHARE,
"futex": SYS_FUTEX,
"set_robust_list": SYS_SET_ROBUST_LIST,
"get_robust_list": SYS_GET_ROBUST_LIST,
"nanosleep": SYS_NANOSLEEP,
"getitimer": SYS_GETITIMER,
"setitimer": SYS_SETITIMER,
"kexec_load": SYS_KEXEC_LOAD,
"init_module": SYS_INIT_MODULE,
"delete_module": SYS_DELETE_MODULE,
"timer_create": SYS_TIMER_CREATE,
"timer_gettime": SYS_TIMER_GETTIME,
"timer_getoverrun": SYS_TIMER_GETOVERRUN,
"timer_settime": SYS_TIMER_SETTIME,
"timer_delete": SYS_TIMER_DELETE,
"clock_settime": SYS_CLOCK_SETTIME,
"clock_gettime": SYS_CLOCK_GETTIME,
"clock_getres": SYS_CLOCK_GETRES,
"clock_nanosleep": SYS_CLOCK_NANOSLEEP,
"syslog": SYS_SYSLOG,
"ptrace": SYS_PTRACE,
"sched_setparam": SYS_SCHED_SETPARAM,
"sched_setscheduler": SYS_SCHED_SETSCHEDULER,
"sched_getscheduler": SYS_SCHED_GETSCHEDULER,
"sched_getparam": SYS_SCHED_GETPARAM,
"sched_setaffinity": SYS_SCHED_SETAFFINITY,
"sched_getaffinity": SYS_SCHED_GETAFFINITY,
"sched_yield": SYS_SCHED_YIELD,
"sched_get_priority_max": SYS_SCHED_GET_PRIORITY_MAX,
"sched_get_priority_min": SYS_SCHED_GET_PRIORITY_MIN,
"sched_rr_get_interval": SYS_SCHED_RR_GET_INTERVAL,
"restart_syscall": SYS_RESTART_SYSCALL,
"kill": SYS_KILL,
"tkill": SYS_TKILL,
"tgkill": SYS_TGKILL,
"sigaltstack": SYS_SIGALTSTACK,
"rt_sigsuspend": SYS_RT_SIGSUSPEND,
"rt_sigaction": SYS_RT_SIGACTION,
"rt_sigprocmask": SYS_RT_SIGPROCMASK,
"rt_sigpending": SYS_RT_SIGPENDING,
"rt_sigtimedwait": SYS_RT_SIGTIMEDWAIT,
"rt_sigqueueinfo": SYS_RT_SIGQUEUEINFO,
"rt_sigreturn": SYS_RT_SIGRETURN,
"setpriority": SYS_SETPRIORITY,
"getpriority": SYS_GETPRIORITY,
"reboot": SYS_REBOOT,
"setregid": SYS_SETREGID,
"setgid": SYS_SETGID,
"setreuid": SYS_SETREUID,
"setuid": SYS_SETUID,
"setresuid": SYS_SETRESUID,
"getresuid": SYS_GETRESUID,
"setresgid": SYS_SETRESGID,
"getresgid": SYS_GETRESGID,
"setfsuid": SYS_SETFSUID,
"setfsgid": SYS_SETFSGID,
"times": SYS_TIMES,
"setpgid": SYS_SETPGID,
"getpgid": SYS_GETPGID,
"getsid": SYS_GETSID,
"setsid": SYS_SETSID,
"getgroups": SYS_GETGROUPS,
"setgroups": SYS_SETGROUPS,
"uname": SYS_UNAME,
"sethostname": SYS_SETHOSTNAME,
"setdomainname": SYS_SETDOMAINNAME,
"getrlimit": SYS_GETRLIMIT,
"setrlimit": SYS_SETRLIMIT,
"getrusage": SYS_GETRUSAGE,
"umask": SYS_UMASK,
"prctl": SYS_PRCTL,
"getcpu": SYS_GETCPU,
"gettimeofday": SYS_GETTIMEOFDAY,
"settimeofday": SYS_SETTIMEOFDAY,
"adjtimex": SYS_ADJTIMEX,
"getpid": SYS_GETPID,
"getppid": SYS_GETPPID,
"getuid": SYS_GETUID,
"geteuid": SYS_GETEUID,
"getgid": SYS_GETGID,
"getegid": SYS_GETEGID,
"gettid": SYS_GETTID,
"sysinfo": SYS_SYSINFO,
"mq_open": SYS_MQ_OPEN,
"mq_unlink": SYS_MQ_UNLINK,
"mq_timedsend": SYS_MQ_TIMEDSEND,
"mq_timedreceive": SYS_MQ_TIMEDRECEIVE,
"mq_notify": SYS_MQ_NOTIFY,
"mq_getsetattr": SYS_MQ_GETSETATTR,
"msgget": SYS_MSGGET,
"msgctl": SYS_MSGCTL,
"msgrcv": SYS_MSGRCV,
"msgsnd": SYS_MSGSND,
"semget": SYS_SEMGET,
"semctl": SYS_SEMCTL,
"semtimedop": SYS_SEMTIMEDOP,
"semop": SYS_SEMOP,
"shmget": SYS_SHMGET,
"shmctl": SYS_SHMCTL,
"shmat": SYS_SHMAT,
"shmdt": SYS_SHMDT,
"socket": SYS_SOCKET,
"socketpair": SYS_SOCKETPAIR,
"bind": SYS_BIND,
"listen": SYS_LISTEN,
"accept": SYS_ACCEPT,
"connect": SYS_CONNECT,
"getsockname": SYS_GETSOCKNAME,
"getpeername": SYS_GETPEERNAME,
"sendto": SYS_SENDTO,
"recvfrom": SYS_RECVFROM,
"setsockopt": SYS_SETSOCKOPT,
"getsockopt": SYS_GETSOCKOPT,
"shutdown": SYS_SHUTDOWN,
"sendmsg": SYS_SENDMSG,
"recvmsg": SYS_RECVMSG,
"readahead": SYS_READAHEAD,
"brk": SYS_BRK,
"munmap": SYS_MUNMAP,
"mremap": SYS_MREMAP,
"add_key": SYS_ADD_KEY,
"request_key": SYS_REQUEST_KEY,
"keyctl": SYS_KEYCTL,
"clone": SYS_CLONE,
"execve": SYS_EXECVE,
"mmap": SYS_MMAP,
"fadvise64": SYS_FADVISE64,
"swapon": SYS_SWAPON,
"swapoff": SYS_SWAPOFF,
"mprotect": SYS_MPROTECT,
"msync": SYS_MSYNC,
"mlock": SYS_MLOCK,
"munlock": SYS_MUNLOCK,
"mlockall": SYS_MLOCKALL,
"munlockall": SYS_MUNLOCKALL,
"mincore": SYS_MINCORE,
"madvise": SYS_MADVISE,
"remap_file_pages": SYS_REMAP_FILE_PAGES,
"mbind": SYS_MBIND,
"get_mempolicy": SYS_GET_MEMPOLICY,
"set_mempolicy": SYS_SET_MEMPOLICY,
"migrate_pages": SYS_MIGRATE_PAGES,
"move_pages": SYS_MOVE_PAGES,
"rt_tgsigqueueinfo": SYS_RT_TGSIGQUEUEINFO,
"perf_event_open": SYS_PERF_EVENT_OPEN,
"accept4": SYS_ACCEPT4,
"recvmmsg": SYS_RECVMMSG,
"wait4": SYS_WAIT4,
"prlimit64": SYS_PRLIMIT64,
"fanotify_init": SYS_FANOTIFY_INIT,
"fanotify_mark": SYS_FANOTIFY_MARK,
"name_to_handle_at": SYS_NAME_TO_HANDLE_AT,
"open_by_handle_at": SYS_OPEN_BY_HANDLE_AT,
"clock_adjtime": SYS_CLOCK_ADJTIME,
"syncfs": SYS_SYNCFS,
"setns": SYS_SETNS,
"sendmmsg": SYS_SENDMMSG,
"process_vm_readv": SYS_PROCESS_VM_READV,
"process_vm_writev": SYS_PROCESS_VM_WRITEV,
"kcmp": SYS_KCMP,
"finit_module": SYS_FINIT_MODULE,
"sched_setattr": SYS_SCHED_SETATTR,
"sched_getattr": SYS_SCHED_GETATTR,
"renameat2": SYS_RENAMEAT2,
"seccomp": SYS_SECCOMP,
"getrandom": SYS_GETRANDOM,
"memfd_create": SYS_MEMFD_CREATE,
"bpf": SYS_BPF,
"execveat": SYS_EXECVEAT,
"userfaultfd": SYS_USERFAULTFD,
"membarrier": SYS_MEMBARRIER,
"mlock2": SYS_MLOCK2,
"copy_file_range": SYS_COPY_FILE_RANGE,
"preadv2": SYS_PREADV2,
"pwritev2": SYS_PWRITEV2,
"pkey_mprotect": SYS_PKEY_MPROTECT,
"pkey_alloc": SYS_PKEY_ALLOC,
"pkey_free": SYS_PKEY_FREE,
"statx": SYS_STATX,
"io_pgetevents": SYS_IO_PGETEVENTS,
"rseq": SYS_RSEQ,
"kexec_file_load": SYS_KEXEC_FILE_LOAD,
"pidfd_send_signal": SYS_PIDFD_SEND_SIGNAL,
"io_uring_setup": SYS_IO_URING_SETUP,
"io_uring_enter": SYS_IO_URING_ENTER,
"io_uring_register": SYS_IO_URING_REGISTER,
"open_tree": SYS_OPEN_TREE,
"move_mount": SYS_MOVE_MOUNT,
"fsopen": SYS_FSOPEN,
"fsconfig": SYS_FSCONFIG,
"fsmount": SYS_FSMOUNT,
"fspick": SYS_FSPICK,
"pidfd_open": SYS_PIDFD_OPEN,
"clone3": SYS_CLONE3,
"close_range": SYS_CLOSE_RANGE,
"openat2": SYS_OPENAT2,
"pidfd_getfd": SYS_PIDFD_GETFD,
"faccessat2": SYS_FACCESSAT2,
"process_madvise": SYS_PROCESS_MADVISE,
"epoll_pwait2": SYS_EPOLL_PWAIT2,
"mount_setattr": SYS_MOUNT_SETATTR,
"quotactl_fd": SYS_QUOTACTL_FD,
"landlock_create_ruleset": SYS_LANDLOCK_CREATE_RULESET,
"landlock_add_rule": SYS_LANDLOCK_ADD_RULE,
"landlock_restrict_self": SYS_LANDLOCK_RESTRICT_SELF,
"memfd_secret": SYS_MEMFD_SECRET,
"process_mrelease": SYS_PROCESS_MRELEASE,
"futex_waitv": SYS_FUTEX_WAITV,
"set_mempolicy_home_node": SYS_SET_MEMPOLICY_HOME_NODE,
"cachestat": SYS_CACHESTAT,
"fchmodat2": SYS_FCHMODAT2,
"map_shadow_stack": SYS_MAP_SHADOW_STACK,
"futex_wake": SYS_FUTEX_WAKE,
"futex_wait": SYS_FUTEX_WAIT,
"futex_requeue": SYS_FUTEX_REQUEUE,
"statmount": SYS_STATMOUNT,
"listmount": SYS_LISTMOUNT,
"lsm_get_self_attr": SYS_LSM_GET_SELF_ATTR,
"lsm_set_self_attr": SYS_LSM_SET_SELF_ATTR,
"lsm_list_modules": SYS_LSM_LIST_MODULES,
"mseal": SYS_MSEAL,
}
const (
SYS_USERFAULTFD = 282
SYS_MEMBARRIER = 283
SYS_MLOCK2 = 284
SYS_COPY_FILE_RANGE = 285
SYS_PREADV2 = 286
SYS_PWRITEV2 = 287
SYS_PKEY_MPROTECT = 288
SYS_PKEY_ALLOC = 289
SYS_PKEY_FREE = 290
SYS_STATX = 291
SYS_IO_PGETEVENTS = 292
SYS_RSEQ = 293
SYS_KEXEC_FILE_LOAD = 294
SYS_PIDFD_SEND_SIGNAL = 424
SYS_IO_URING_SETUP = 425
SYS_IO_URING_ENTER = 426
SYS_IO_URING_REGISTER = 427
SYS_OPEN_TREE = 428
SYS_MOVE_MOUNT = 429
SYS_FSOPEN = 430
SYS_FSCONFIG = 431
SYS_FSMOUNT = 432
SYS_FSPICK = 433
SYS_PIDFD_OPEN = 434
SYS_CLONE3 = 435
SYS_CLOSE_RANGE = 436
SYS_OPENAT2 = 437
SYS_PIDFD_GETFD = 438
SYS_FACCESSAT2 = 439
SYS_PROCESS_MADVISE = 440
SYS_EPOLL_PWAIT2 = 441
SYS_MOUNT_SETATTR = 442
SYS_QUOTACTL_FD = 443
SYS_LANDLOCK_CREATE_RULESET = 444
SYS_LANDLOCK_ADD_RULE = 445
SYS_LANDLOCK_RESTRICT_SELF = 446
SYS_MEMFD_SECRET = 447
SYS_PROCESS_MRELEASE = 448
SYS_FUTEX_WAITV = 449
SYS_SET_MEMPOLICY_HOME_NODE = 450
SYS_CACHESTAT = 451
SYS_FCHMODAT2 = 452
SYS_MAP_SHADOW_STACK = 453
SYS_FUTEX_WAKE = 454
SYS_FUTEX_WAIT = 455
SYS_FUTEX_REQUEUE = 456
SYS_STATMOUNT = 457
SYS_LISTMOUNT = 458
SYS_LSM_GET_SELF_ATTR = 459
SYS_LSM_SET_SELF_ATTR = 460
SYS_LSM_LIST_MODULES = 461
SYS_MSEAL = 462
)

View File

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

35
container/syscall.go Normal file
View File

@@ -0,0 +1,35 @@
package container
import (
"syscall"
)
const (
SUID_DUMP_DISABLE = iota
SUID_DUMP_USER
)
func SetDumpable(dumpable uintptr) error {
// linux/sched/coredump.h
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, dumpable, 0); errno != 0 {
return errno
}
return nil
}
// IgnoringEINTR makes a function call and repeats it if it returns an
// EINTR error. This appears to be required even though we install all
// signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846.
// Also #20400 and #36644 are issues in which a signal handler is
// installed without setting SA_RESTART. None of these are the common case,
// but there are enough of them that it seems that we can't avoid
// an EINTR loop.
func IgnoringEINTR(fn func() error) error {
for {
err := fn()
if err != syscall.EINTR {
return err
}
}
}

View File

@@ -0,0 +1,7 @@
package container
const (
O_PATH = 0x200000
PR_SET_NO_NEW_PRIVS = 0x26
)

View File

@@ -1,4 +1,4 @@
package sandbox
package container
import (
"bytes"

View File

@@ -3,7 +3,7 @@ package vfs_test
import (
"testing"
"git.gensokyo.uk/security/fortify/sandbox/vfs"
"hakurei.app/container/vfs"
)
func TestUnmangle(t *testing.T) {

View File

@@ -12,7 +12,7 @@ import (
"syscall"
"testing"
"git.gensokyo.uk/security/fortify/sandbox/vfs"
"hakurei.app/container/vfs"
)
func TestMountInfo(t *testing.T) {

View File

@@ -8,7 +8,7 @@ import (
"syscall"
"testing"
"git.gensokyo.uk/security/fortify/sandbox/vfs"
"hakurei.app/container/vfs"
)
func TestUnfold(t *testing.T) {

View File

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

View File

@@ -1,13 +0,0 @@
package dbus
import "io"
// CompareTestNew provides TestNew with comparison access to unexported Proxy fields.
func (p *Proxy) CompareTestNew(session, system [2]string) bool {
return session == p.session && system == p.system
}
// AccessTestProxySeal provides TestProxy_Seal with access to unexported Proxy seal field.
func (p *Proxy) AccessTestProxySeal() io.WriterTo {
return p.seal
}

View File

@@ -1,178 +0,0 @@
package dbus
import (
"context"
"errors"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"slices"
"strconv"
"strings"
"syscall"
"git.gensokyo.uk/security/fortify/helper"
"git.gensokyo.uk/security/fortify/ldd"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
)
// Start launches the D-Bus proxy.
func (p *Proxy) Start(ctx context.Context, output io.Writer, useSandbox bool) error {
p.lock.Lock()
defer p.lock.Unlock()
if p.seal == nil {
return errors.New("proxy not sealed")
}
var h helper.Helper
c, cancel := context.WithCancelCause(ctx)
if !useSandbox {
h = helper.NewDirect(c, p.name, p.seal, true, argF, func(cmd *exec.Cmd) {
if p.CmdF != nil {
p.CmdF(cmd)
}
if output != nil {
cmd.Stdout, cmd.Stderr = output, output
}
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
cmd.Env = make([]string, 0)
}, nil)
} else {
toolPath := p.name
if filepath.Base(p.name) == p.name {
if s, err := exec.LookPath(p.name); err != nil {
return err
} else {
toolPath = s
}
}
var libPaths []string
if entries, err := ldd.ExecFilter(ctx, p.CommandContext, p.FilterF, toolPath); err != nil {
return err
} else {
libPaths = ldd.Path(entries)
}
h = helper.New(
c, toolPath,
p.seal, true,
argF, func(container *sandbox.Container) {
container.Seccomp |= seccomp.FlagMultiarch
container.Hostname = "fortify-dbus"
container.CommandContext = p.CommandContext
if output != nil {
container.Stdout, container.Stderr = output, output
}
if p.CmdF != nil {
p.CmdF(container)
}
// these lib paths are unpredictable, so mount them first so they cannot cover anything
for _, name := range libPaths {
container.Bind(name, name, 0)
}
// upstream bus directories
upstreamPaths := make([]string, 0, 2)
for _, as := range []string{p.session[0], p.system[0]} {
if len(as) > 0 && strings.HasPrefix(as, "unix:path=/") {
// leave / intact
upstreamPaths = append(upstreamPaths, path.Dir(as[10:]))
}
}
slices.Sort(upstreamPaths)
upstreamPaths = slices.Compact(upstreamPaths)
for _, name := range upstreamPaths {
container.Bind(name, name, 0)
}
// parent directories of bind paths
sockDirPaths := make([]string, 0, 2)
if d := path.Dir(p.session[1]); path.IsAbs(d) {
sockDirPaths = append(sockDirPaths, d)
}
if d := path.Dir(p.system[1]); path.IsAbs(d) {
sockDirPaths = append(sockDirPaths, d)
}
slices.Sort(sockDirPaths)
sockDirPaths = slices.Compact(sockDirPaths)
for _, name := range sockDirPaths {
container.Bind(name, name, sandbox.BindWritable)
}
// xdg-dbus-proxy bin path
binPath := path.Dir(toolPath)
container.Bind(binPath, binPath, 0)
}, nil)
}
if err := h.Start(); err != nil {
cancel(err)
return err
}
p.helper = h
p.ctx = c
p.cancel = cancel
return nil
}
var proxyClosed = errors.New("proxy closed")
// Wait blocks until xdg-dbus-proxy exits and releases resources.
func (p *Proxy) Wait() error {
p.lock.RLock()
defer p.lock.RUnlock()
if p.helper == nil {
return errors.New("dbus: not started")
}
errs := make([]error, 3)
errs[0] = p.helper.Wait()
if p.cancel == nil &&
errors.Is(errs[0], context.Canceled) &&
errors.Is(context.Cause(p.ctx), proxyClosed) {
errs[0] = nil
}
// ensure socket removal so ephemeral directory is empty at revert
if err := os.Remove(p.session[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
errs[1] = err
}
if p.sysP {
if err := os.Remove(p.system[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
errs[2] = err
}
}
return errors.Join(errs...)
}
// Close cancels the context passed to the helper instance attached to xdg-dbus-proxy.
func (p *Proxy) Close() {
p.lock.Lock()
defer p.lock.Unlock()
if p.cancel == nil {
panic("dbus: not started")
}
p.cancel(proxyClosed)
p.cancel = nil
}
func argF(argsFd, statFd int) []string {
if statFd == -1 {
return []string{"--args=" + strconv.Itoa(argsFd)}
} else {
return []string{"--args=" + strconv.Itoa(argsFd), "--fd=" + strconv.Itoa(statFd)}
}
}

View File

@@ -1,98 +0,0 @@
package dbus
import (
"context"
"errors"
"fmt"
"io"
"os/exec"
"sync"
"git.gensokyo.uk/security/fortify/helper"
)
// ProxyName is the file name or path to the proxy program.
// Overriding ProxyName will only affect Proxy instance created after the change.
var ProxyName = "xdg-dbus-proxy"
// Proxy holds references to a xdg-dbus-proxy process, and should never be copied.
// Once sealed, configuration changes will no longer be possible and attempting to do so will result in a panic.
type Proxy struct {
helper helper.Helper
ctx context.Context
cancel context.CancelCauseFunc
name string
session [2]string
system [2]string
CmdF func(any)
sysP bool
CommandContext func(ctx context.Context) (cmd *exec.Cmd)
FilterF func([]byte) []byte
seal io.WriterTo
lock sync.RWMutex
}
func (p *Proxy) Session() [2]string { return p.session }
func (p *Proxy) System() [2]string { return p.system }
func (p *Proxy) Sealed() bool { p.lock.RLock(); defer p.lock.RUnlock(); return p.seal != nil }
var (
ErrConfig = errors.New("no configuration to seal")
)
func (p *Proxy) String() string {
if p == nil {
return "(invalid dbus proxy)"
}
p.lock.RLock()
defer p.lock.RUnlock()
if p.helper != nil {
return p.helper.String()
}
if p.seal != nil {
return p.seal.(fmt.Stringer).String()
}
return "(unsealed dbus proxy)"
}
// Seal seals the Proxy instance.
func (p *Proxy) Seal(session, system *Config) error {
p.lock.Lock()
defer p.lock.Unlock()
if p.seal != nil {
panic("dbus proxy sealed twice")
}
if session == nil && system == nil {
return ErrConfig
}
var args []string
if session != nil {
args = append(args, session.Args(p.session)...)
}
if system != nil {
args = append(args, system.Args(p.system)...)
p.sysP = true
}
if seal, err := helper.NewCheckedArgs(args); err != nil {
return err
} else {
p.seal = seal
}
return nil
}
// New returns a reference to a new unsealed Proxy.
func New(session, system [2]string) *Proxy {
return &Proxy{name: ProxyName, session: session, system: system}
}

View File

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

82
dist/comp/_hakurei vendored Normal file
View File

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

12
dist/install.sh vendored
View File

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

18
dist/release.sh vendored
View File

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

16
flake.lock generated
View File

@@ -7,32 +7,32 @@
]
},
"locked": {
"lastModified": 1742234739,
"narHash": "sha256-zFL6zsf/5OztR1NSNQF33dvS1fL/BzVUjabZq4qrtY4=",
"lastModified": 1753479839,
"narHash": "sha256-E/rPVh7vyPMJUFl2NAew+zibNGfVbANr8BP8nLRbLkQ=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "f6af7280a3390e65c2ad8fd059cdc303426cbd59",
"rev": "0b9bf983db4d064764084cd6748efb1ab8297d1e",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "release-24.11",
"ref": "release-25.05",
"repo": "home-manager",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1742512142,
"narHash": "sha256-8XfURTDxOm6+33swQJu/hx6xw1Tznl8vJJN5HwVqckg=",
"lastModified": 1753345091,
"narHash": "sha256-CdX2Rtvp5I8HGu9swBmYuq+ILwRxpXdJwlpg8jvN4tU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "7105ae3957700a9646cc4b766f5815b23ed0c682",
"rev": "3ff0e34b1383648053bba8ed03f201d3466f90c9",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.11",
"ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}

View File

@@ -1,11 +1,11 @@
{
description = "fortify sandbox tool and nixos module";
description = "hakurei container tool and nixos module";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
home-manager = {
url = "github:nix-community/home-manager/release-24.11";
url = "github:nix-community/home-manager/release-25.05";
inputs.nixpkgs.follows = "nixpkgs";
};
};
@@ -27,12 +27,12 @@
nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
in
{
nixosModules.fortify = import ./nixos.nix self.packages;
nixosModules.hakurei = import ./nixos.nix self.packages;
buildPackage = forAllSystems (
system:
nixpkgsFor.${system}.callPackage (
import ./cmd/fpkg/build.nix {
import ./cmd/planterette/build.nix {
inherit
nixpkgsFor
system
@@ -57,7 +57,7 @@
;
in
{
fortify = callPackage ./test { inherit system self; };
hakurei = callPackage ./test { inherit system self; };
race = callPackage ./test {
inherit system self;
withRace = true;
@@ -69,7 +69,7 @@
withRace = true;
};
fpkg = callPackage ./cmd/fpkg/test { inherit system self; };
planterette = callPackage ./cmd/planterette/test { inherit system self; };
formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } ''
cd ${./.}
@@ -105,12 +105,12 @@
packages = forAllSystems (
system:
let
inherit (self.packages.${system}) fortify fsu;
inherit (self.packages.${system}) hakurei hsu;
pkgs = nixpkgsFor.${system};
in
{
default = fortify;
fortify = pkgs.pkgsStatic.callPackage ./package.nix {
default = hakurei;
hakurei = pkgs.pkgsStatic.callPackage ./package.nix {
inherit (pkgs)
# passthru.buildInputs
go
@@ -125,26 +125,26 @@
glibc
xdg-dbus-proxy
# fpkg
# planterette
zstd
gnutar
coreutils
;
};
fsu = pkgs.callPackage ./cmd/fsu/package.nix { inherit (self.packages.${system}) fortify; };
hsu = pkgs.callPackage ./cmd/hsu/package.nix { inherit (self.packages.${system}) hakurei; };
dist = pkgs.runCommand "${fortify.name}-dist" { buildInputs = fortify.targetPkgs ++ [ pkgs.pkgsStatic.musl ]; } ''
dist = pkgs.runCommand "${hakurei.name}-dist" { buildInputs = hakurei.targetPkgs ++ [ pkgs.pkgsStatic.musl ]; } ''
# go requires XDG_CACHE_HOME for the build cache
export XDG_CACHE_HOME="$(mktemp -d)"
# get a different workdir as go does not like /build
cd $(mktemp -d) \
&& cp -r ${fortify.src}/. . \
&& chmod +w cmd && cp -r ${fsu.src}/. cmd/fsu/ \
&& cp -r ${hakurei.src}/. . \
&& chmod +w cmd && cp -r ${hsu.src}/. cmd/hsu/ \
&& chmod -R +w .
export FORTIFY_VERSION="v${fortify.version}"
./dist/release.sh && mkdir $out && cp -v "dist/fortify-$FORTIFY_VERSION.tar.gz"* $out
export HAKUREI_VERSION="v${hakurei.version}"
./dist/release.sh && mkdir $out && cp -v "dist/hakurei-$HAKUREI_VERSION.tar.gz"* $out
'';
}
);
@@ -152,12 +152,12 @@
devShells = forAllSystems (
system:
let
inherit (self.packages.${system}) fortify;
inherit (self.packages.${system}) hakurei;
pkgs = nixpkgsFor.${system};
in
{
default = pkgs.mkShell { buildInputs = fortify.targetPkgs; };
withPackage = pkgs.mkShell { buildInputs = [ fortify ] ++ fortify.targetPkgs; };
default = pkgs.mkShell { buildInputs = hakurei.targetPkgs; };
withPackage = pkgs.mkShell { buildInputs = [ hakurei ] ++ hakurei.targetPkgs; };
generateDoc =
let
@@ -174,7 +174,7 @@
cleanEval = lib.filterAttrsRecursive (n: _: n != "_module") eval;
in
pkgs.nixosOptionsDoc { inherit (cleanEval) options; };
docText = pkgs.runCommand "fortify-module-docs.md" { } ''
docText = pkgs.runCommand "hakurei-module-docs.md" { } ''
cat ${doc.optionsCommonMark} > $out
sed -i '/*Declared by:*/,+1 d' $out
'';
@@ -184,6 +184,24 @@
exec cat ${docText} > options.md
'';
};
generateSyscallTable =
let
GOARCH = {
x86_64-linux = "amd64";
aarch64-linux = "arm64";
};
in
pkgs.mkShell {
shellHook = "exec ${pkgs.writeShellScript "generate-syscall-table" ''
set -e
${pkgs.perl}/bin/perl \
container/seccomp/mksysnum_linux.pl \
${pkgs.linuxHeaders}/include/asm/unistd_64.h | \
${pkgs.go}/bin/gofmt > \
container/seccomp/syscall_linux_${GOARCH.${system}}.go
''}";
};
}
);
};

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,159 +0,0 @@
package fst
import (
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
"git.gensokyo.uk/security/fortify/system"
)
const Tmp = "/.fortify"
// Config is used to seal an app
type Config struct {
// reverse-DNS style arbitrary identifier string from config;
// passed to wayland security-context-v1 as application ID
// and used as part of defaults in dbus session proxy
ID string `json:"id"`
// absolute path to executable file
Path string `json:"path,omitempty"`
// final args passed to container init
Args []string `json:"args"`
Confinement ConfinementConfig `json:"confinement"`
}
// ConfinementConfig defines fortified child's confinement
type ConfinementConfig struct {
// numerical application id, determines uid in the init namespace
AppID int `json:"app_id"`
// list of supplementary groups to inherit
Groups []string `json:"groups"`
// passwd username in container, defaults to passwd name of target uid or chronos
Username string `json:"username,omitempty"`
// home directory in container, empty for outer
Inner string `json:"home_inner"`
// home directory in init namespace
Outer string `json:"home"`
// abstract sandbox configuration
Sandbox *SandboxConfig `json:"sandbox"`
// extra acl ops, runs after everything else
ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
// reference to a system D-Bus proxy configuration,
// nil value disables system bus proxy
SystemBus *dbus.Config `json:"system_bus,omitempty"`
// reference to a session D-Bus proxy configuration,
// nil value makes session bus proxy assume built-in defaults
SessionBus *dbus.Config `json:"session_bus,omitempty"`
// system resources to expose to the container
Enablements system.Enablement `json:"enablements"`
}
type ExtraPermConfig struct {
Ensure bool `json:"ensure,omitempty"`
Path string `json:"path"`
Read bool `json:"r,omitempty"`
Write bool `json:"w,omitempty"`
Execute bool `json:"x,omitempty"`
}
func (e *ExtraPermConfig) String() string {
buf := make([]byte, 0, 5+len(e.Path))
buf = append(buf, '-', '-', '-')
if e.Ensure {
buf = append(buf, '+')
}
buf = append(buf, ':')
buf = append(buf, []byte(e.Path)...)
if e.Read {
buf[0] = 'r'
}
if e.Write {
buf[1] = 'w'
}
if e.Execute {
buf[2] = 'x'
}
return string(buf)
}
// 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,
},
}
}

View File

@@ -1,286 +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,
}
if s.Multiarch {
container.Seccomp |= seccomp.FlagMultiarch
}
/* this is only 4 KiB of memory on a 64-bit system,
permissive defaults on NixOS results in around 100 entries
so this capacity should eliminate copies for most setups */
*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
}

4
go.mod
View File

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

View File

@@ -7,7 +7,7 @@ import (
"syscall"
"testing"
"git.gensokyo.uk/security/fortify/helper"
"hakurei.app/helper"
)
func TestArgsString(t *testing.T) {

View File

@@ -10,7 +10,7 @@ import (
"sync"
"syscall"
"git.gensokyo.uk/security/fortify/helper/proc"
"hakurei.app/helper/proc"
)
// NewDirect initialises a new direct Helper instance with wt as the null-terminated argument writer.
@@ -67,17 +67,17 @@ func (h *helperCmd) Start() error {
h.Env = slices.Grow(h.Env, 2)
if h.useArgsFd {
h.Env = append(h.Env, FortifyHelper+"=1")
h.Env = append(h.Env, HakureiHelper+"=1")
} else {
h.Env = append(h.Env, FortifyHelper+"=0")
h.Env = append(h.Env, HakureiHelper+"=0")
}
if h.useStatFd {
h.Env = append(h.Env, FortifyStatus+"=1")
h.Env = append(h.Env, HakureiStatus+"=1")
// stat is populated on fulfill
h.Cancel = func() error { return h.stat.Close() }
} else {
h.Env = append(h.Env, FortifyStatus+"=0")
h.Env = append(h.Env, HakureiStatus+"=0")
}
return proc.Fulfill(h.helperFiles.ctx, &h.ExtraFiles, h.Cmd.Start, h.files, h.extraFiles)

View File

@@ -8,12 +8,13 @@ import (
"os/exec"
"testing"
"git.gensokyo.uk/security/fortify/helper"
"hakurei.app/container"
"hakurei.app/helper"
)
func TestCmd(t *testing.T) {
t.Run("start non-existent helper path", func(t *testing.T) {
h := helper.NewDirect(context.Background(), "/proc/nonexistent", argsWt, false, argF, nil, nil)
h := helper.NewDirect(t.Context(), container.Nonexistent, argsWt, false, argF, nil, nil)
if err := h.Start(); !errors.Is(err, os.ErrNotExist) {
t.Errorf("Start: error = %v, wantErr %v",
@@ -22,9 +23,9 @@ func TestCmd(t *testing.T) {
})
t.Run("valid new helper nil check", func(t *testing.T) {
if got := helper.NewDirect(context.TODO(), "fortify", argsWt, false, argF, nil, nil); got == nil {
if got := helper.NewDirect(t.Context(), "hakurei", argsWt, false, argF, nil, nil); got == nil {
t.Errorf("NewDirect(%q, %q) got nil",
argsWt, "fortify")
argsWt, "hakurei")
return
}
})

View File

@@ -9,8 +9,8 @@ import (
"slices"
"sync"
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/sandbox"
"hakurei.app/container"
"hakurei.app/helper/proc"
)
// New initialises a Helper instance with wt as the null-terminated argument writer.
@@ -20,13 +20,13 @@ func New(
wt io.WriterTo,
stat bool,
argF func(argsFd, statFd int) []string,
cmdF func(container *sandbox.Container),
cmdF func(z *container.Container),
extraFiles []*os.File,
) Helper {
var args []string
h := new(helperContainer)
h.helperFiles, args = newHelperFiles(ctx, wt, stat, argF, extraFiles)
h.Container = sandbox.New(ctx, name, args...)
h.Container = container.New(ctx, name, args...)
h.WaitDelay = WaitDelay
if cmdF != nil {
cmdF(h.Container)
@@ -40,7 +40,7 @@ type helperContainer struct {
mu sync.Mutex
*helperFiles
*sandbox.Container
*container.Container
}
func (h *helperContainer) Start() error {
@@ -54,17 +54,17 @@ func (h *helperContainer) Start() error {
h.Env = slices.Grow(h.Env, 2)
if h.useArgsFd {
h.Env = append(h.Env, FortifyHelper+"=1")
h.Env = append(h.Env, HakureiHelper+"=1")
} else {
h.Env = append(h.Env, FortifyHelper+"=0")
h.Env = append(h.Env, HakureiHelper+"=0")
}
if h.useStatFd {
h.Env = append(h.Env, FortifyStatus+"=1")
h.Env = append(h.Env, HakureiStatus+"=1")
// stat is populated on fulfill
h.Cancel = func(*exec.Cmd) error { return h.stat.Close() }
} else {
h.Env = append(h.Env, FortifyStatus+"=0")
h.Env = append(h.Env, HakureiStatus+"=0")
}
return proc.Fulfill(h.helperFiles.ctx, &h.ExtraFiles, func() error {

View File

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

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