Compare commits

...

39 Commits

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

@ -73,6 +73,7 @@ func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool
Username: "fortify", Username: "fortify",
Inner: path.Join("/data/data", app.ID), Inner: path.Join("/data/data", app.ID),
Outer: pathSet.homeDir, Outer: pathSet.homeDir,
Shell: shellPath,
Sandbox: &fst.SandboxConfig{ Sandbox: &fst.SandboxConfig{
Hostname: formatHostname(app.Name), Hostname: formatHostname(app.Name),
Devel: app.Devel, Devel: app.Devel,

View File

@ -34,6 +34,7 @@ func withNixDaemon(
Username: "fortify", Username: "fortify",
Inner: path.Join("/data/data", app.ID), Inner: path.Join("/data/data", app.ID),
Outer: pathSet.homeDir, Outer: pathSet.homeDir,
Shell: shellPath,
Sandbox: &fst.SandboxConfig{ Sandbox: &fst.SandboxConfig{
Hostname: formatHostname(app.Name) + "-" + action, Hostname: formatHostname(app.Name) + "-" + action,
Userns: true, // nix sandbox requires userns Userns: true, // nix sandbox requires userns
@ -72,6 +73,7 @@ func withCacheDir(
Username: "nixos", Username: "nixos",
Inner: path.Join("/data/data", app.ID, "cache"), Inner: path.Join("/data/data", app.ID, "cache"),
Outer: pathSet.cacheDir, // this also ensures cacheDir via shim Outer: pathSet.cacheDir, // this also ensures cacheDir via shim
Shell: shellPath,
Sandbox: &fst.SandboxConfig{ Sandbox: &fst.SandboxConfig{
Hostname: formatHostname(app.Name) + "-" + action, Hostname: formatHostname(app.Name) + "-" + action,
Seccomp: seccomp.FlagMultiarch, Seccomp: seccomp.FlagMultiarch,

View File

@ -8,6 +8,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
"syscall"
"testing" "testing"
"time" "time"
@ -71,7 +72,7 @@ func TestProxy_Seal(t *testing.T) {
for id, tc := range testCasePairs() { for id, tc := range testCasePairs() {
t.Run("create seal for "+id, func(t *testing.T) { t.Run("create seal for "+id, func(t *testing.T) {
p := dbus.New(tc[0].bus, tc[1].bus) p := dbus.New(tc[0].bus, tc[1].bus)
if err := p.Seal(tc[0].c, tc[1].c); (errors.Is(err, helper.ErrContainsNull)) != tc[0].wantErr { if err := p.Seal(tc[0].c, tc[1].c); (errors.Is(err, syscall.EINVAL)) != tc[0].wantErr {
t.Errorf("Seal(%p, %p) error = %v, wantErr %v", t.Errorf("Seal(%p, %p) error = %v, wantErr %v",
tc[0].c, tc[1].c, tc[0].c, tc[1].c,
err, tc[0].wantErr) err, tc[0].wantErr)

12
flake.lock generated
View File

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

View File

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

View File

@ -35,6 +35,8 @@ type ConfinementConfig struct {
Inner string `json:"home_inner"` Inner string `json:"home_inner"`
// home directory in init namespace // home directory in init namespace
Outer string `json:"home"` Outer string `json:"home"`
// absolute path to shell, empty for host shell
Shell string `json:"shell,omitempty"`
// abstract sandbox configuration // abstract sandbox configuration
Sandbox *SandboxConfig `json:"sandbox"` Sandbox *SandboxConfig `json:"sandbox"`
// extra acl ops, runs after everything else // extra acl ops, runs after everything else
@ -97,6 +99,7 @@ func Template() *Config {
Username: "chronos", Username: "chronos",
Outer: "/var/lib/persist/home/org.chromium.Chromium", Outer: "/var/lib/persist/home/org.chromium.Chromium",
Inner: "/var/lib/fortify", Inner: "/var/lib/fortify",
Shell: "/run/current-system/sw/bin/zsh",
Sandbox: &SandboxConfig{ Sandbox: &SandboxConfig{
Hostname: "localhost", Hostname: "localhost",
Devel: true, Devel: true,

View File

@ -97,6 +97,10 @@ func (s *SandboxConfig) ToContainer(sys SandboxSys, uid, gid *int) (*sandbox.Par
Seccomp: s.Seccomp, Seccomp: s.Seccomp,
} }
if s.Multiarch {
container.Seccomp |= seccomp.FlagMultiarch
}
/* this is only 4 KiB of memory on a 64-bit system, /* this is only 4 KiB of memory on a 64-bit system,
permissive defaults on NixOS results in around 100 entries permissive defaults on NixOS results in around 100 entries
so this capacity should eliminate copies for most setups */ so this capacity should eliminate copies for most setups */

View File

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

View File

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

View File

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

View File

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

View File

@ -56,15 +56,15 @@ var testCasesNixos = []sealTestCase{
}, },
system.New(1000001). system.New(1000001).
Ensure("/tmp/fortify.1971", 0711). Ensure("/tmp/fortify.1971", 0711).
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1", 0711).
Ephemeral(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute). Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/1", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/1", acl.Read, acl.Write, acl.Execute). Ensure("/tmp/fortify.1971/tmpdir/1", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/1", acl.Read, acl.Write, acl.Execute).
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
UpdatePermType(system.EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute). UpdatePermType(system.EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute).
Ephemeral(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", acl.Execute).
Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse"). Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse").
CopyFile(nil, "/home/ophestra/xdg/config/pulse/cookie", 256, 256). CopyFile(nil, "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
Ephemeral(system.Process, "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1", 0711).
MustProxyDBus("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", &dbus.Config{ MustProxyDBus("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", &dbus.Config{
Talk: []string{ Talk: []string{
"org.freedesktop.FileManager1", "org.freedesktop.Notifications", "org.freedesktop.FileManager1", "org.freedesktop.Notifications",
@ -101,6 +101,7 @@ var testCasesNixos = []sealTestCase{
"HOME=/var/lib/persist/module/fortify/0/1", "HOME=/var/lib/persist/module/fortify/0/1",
"PULSE_COOKIE=" + fst.Tmp + "/pulse-cookie", "PULSE_COOKIE=" + fst.Tmp + "/pulse-cookie",
"PULSE_SERVER=unix:/run/user/1971/pulse/native", "PULSE_SERVER=unix:/run/user/1971/pulse/native",
"SHELL=/run/current-system/sw/bin/zsh",
"TERM=xterm-256color", "TERM=xterm-256color",
"USER=u0_a1", "USER=u0_a1",
"WAYLAND_DISPLAY=wayland-0", "WAYLAND_DISPLAY=wayland-0",
@ -203,7 +204,7 @@ var testCasesNixos = []sealTestCase{
Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv"). Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc"). Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Tmpfs("/run/user", 4096, 0755). Tmpfs("/run/user", 4096, 0755).
Tmpfs("/run/user/1971", 8388608, 0755). Tmpfs("/run/user/1971", 8388608, 0700).
Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", sandbox.BindWritable). Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", sandbox.BindWritable).
Bind("/var/lib/persist/module/fortify/0/1", "/var/lib/persist/module/fortify/0/1", sandbox.BindWritable). Bind("/var/lib/persist/module/fortify/0/1", "/var/lib/persist/module/fortify/0/1", sandbox.BindWritable).
Place("/etc/passwd", []byte("u0_a1:x:1971:100:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n")). Place("/etc/passwd", []byte("u0_a1:x:1971:100:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n")).

View File

@ -28,10 +28,6 @@ var testCasesPd = []sealTestCase{
}, },
system.New(1000000). system.New(1000000).
Ensure("/tmp/fortify.1971", 0711). Ensure("/tmp/fortify.1971", 0711).
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac", 0711).
Ephemeral(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute). Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute), Ensure("/tmp/fortify.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute),
&sandbox.Params{ &sandbox.Params{
@ -41,6 +37,7 @@ var testCasesPd = []sealTestCase{
Args: []string{"/run/current-system/sw/bin/zsh"}, Args: []string{"/run/current-system/sw/bin/zsh"},
Env: []string{ Env: []string{
"HOME=/home/chronos", "HOME=/home/chronos",
"SHELL=/run/current-system/sw/bin/zsh",
"TERM=xterm-256color", "TERM=xterm-256color",
"USER=chronos", "USER=chronos",
"XDG_RUNTIME_DIR=/run/user/65534", "XDG_RUNTIME_DIR=/run/user/65534",
@ -146,7 +143,7 @@ var testCasesPd = []sealTestCase{
Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv"). Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc"). Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Tmpfs("/run/user", 4096, 0755). Tmpfs("/run/user", 4096, 0755).
Tmpfs("/run/user/65534", 8388608, 0755). Tmpfs("/run/user/65534", 8388608, 0700).
Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", sandbox.BindWritable). Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", sandbox.BindWritable).
Bind("/home/chronos", "/home/chronos", sandbox.BindWritable). Bind("/home/chronos", "/home/chronos", sandbox.BindWritable).
Place("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")). Place("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
@ -206,14 +203,13 @@ var testCasesPd = []sealTestCase{
}, },
system.New(1000009). system.New(1000009).
Ensure("/tmp/fortify.1971", 0711). Ensure("/tmp/fortify.1971", 0711).
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c", 0711).
Ephemeral(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute). Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/9", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/9", acl.Read, acl.Write, acl.Execute). Ensure("/tmp/fortify.1971/tmpdir/9", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/9", acl.Read, acl.Write, acl.Execute).
Ensure("/tmp/fortify.1971/wayland", 0711). Ephemeral(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c", 0711).
Wayland(new(*os.File), "/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/1971/wayland-0", "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"). Wayland(new(*os.File), "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/1971/wayland-0", "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", acl.Execute).
Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse"). Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse").
CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 256, 256). CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
MustProxyDBus("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", &dbus.Config{ MustProxyDBus("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", &dbus.Config{
@ -259,6 +255,7 @@ var testCasesPd = []sealTestCase{
"HOME=/home/chronos", "HOME=/home/chronos",
"PULSE_COOKIE=" + fst.Tmp + "/pulse-cookie", "PULSE_COOKIE=" + fst.Tmp + "/pulse-cookie",
"PULSE_SERVER=unix:/run/user/65534/pulse/native", "PULSE_SERVER=unix:/run/user/65534/pulse/native",
"SHELL=/run/current-system/sw/bin/zsh",
"TERM=xterm-256color", "TERM=xterm-256color",
"USER=chronos", "USER=chronos",
"WAYLAND_DISPLAY=wayland-0", "WAYLAND_DISPLAY=wayland-0",
@ -366,12 +363,12 @@ var testCasesPd = []sealTestCase{
Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv"). Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc"). Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Tmpfs("/run/user", 4096, 0755). Tmpfs("/run/user", 4096, 0755).
Tmpfs("/run/user/65534", 8388608, 0755). Tmpfs("/run/user/65534", 8388608, 0700).
Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", sandbox.BindWritable). Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", sandbox.BindWritable).
Bind("/home/chronos", "/home/chronos", sandbox.BindWritable). Bind("/home/chronos", "/home/chronos", sandbox.BindWritable).
Place("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")). Place("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
Place("/etc/group", []byte("fortify:x:65534:\n")). Place("/etc/group", []byte("fortify:x:65534:\n")).
Bind("/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/65534/wayland-0", 0). Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/65534/wayland-0", 0).
Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native", 0). Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native", 0).
Place(fst.Tmp+"/pulse-cookie", nil). Place(fst.Tmp+"/pulse-cookie", nil).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus", 0). Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus", 0).

View File

@ -8,7 +8,6 @@ import (
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
"maps"
"os" "os"
"path" "path"
"regexp" "regexp"
@ -86,6 +85,53 @@ type outcome struct {
f atomic.Bool f atomic.Bool
} }
// shareHost holds optional share directory state that must not be accessed directly
type shareHost struct {
// whether XDG_RUNTIME_DIR is used post fsu
useRuntimeDir bool
// process-specific directory in tmpdir, empty if unused
sharePath string
// process-specific directory in XDG_RUNTIME_DIR, empty if unused
runtimeSharePath string
seal *outcome
sc fst.Paths
}
// ensureRuntimeDir must be called if direct access to paths within XDG_RUNTIME_DIR is required
func (share *shareHost) ensureRuntimeDir() {
if share.useRuntimeDir {
return
}
share.useRuntimeDir = true
share.seal.sys.Ensure(share.sc.RunDirPath, 0700)
share.seal.sys.UpdatePermType(system.User, share.sc.RunDirPath, acl.Execute)
share.seal.sys.Ensure(share.sc.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset
share.seal.sys.UpdatePermType(system.User, share.sc.RuntimePath, acl.Execute)
}
// instance returns a process-specific share path within tmpdir
func (share *shareHost) instance() string {
if share.sharePath != "" {
return share.sharePath
}
share.sharePath = path.Join(share.sc.SharePath, share.seal.id.String())
share.seal.sys.Ephemeral(system.Process, share.sharePath, 0711)
return share.sharePath
}
// runtime returns a process-specific share path within XDG_RUNTIME_DIR
func (share *shareHost) runtime() string {
if share.runtimeSharePath != "" {
return share.runtimeSharePath
}
share.ensureRuntimeDir()
share.runtimeSharePath = path.Join(share.sc.RunDirPath, share.seal.id.String())
share.seal.sys.Ephemeral(system.Process, share.runtimeSharePath, 0700)
share.seal.sys.UpdatePerm(share.runtimeSharePath, acl.Execute)
return share.runtimeSharePath
}
// fsuUser stores post-fsu credentials and metadata // fsuUser stores post-fsu credentials and metadata
type fsuUser struct { type fsuUser struct {
// application id // application id
@ -110,11 +156,6 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
} }
seal.ctx = ctx seal.ctx = ctx
shellPath := "/bin/sh"
if s, ok := sys.LookupEnv(shell); ok && path.IsAbs(s) {
shellPath = s
}
{ {
// encode initial configuration for state tracking // encode initial configuration for state tracking
ct := new(bytes.Buffer) ct := new(bytes.Buffer)
@ -131,10 +172,6 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
fmt.Sprintf("aid %d out of range", config.Confinement.AppID)) fmt.Sprintf("aid %d out of range", config.Confinement.AppID))
} }
/*
Resolve post-fsu user state
*/
seal.user = fsuUser{ seal.user = fsuUser{
aid: newInt(config.Confinement.AppID), aid: newInt(config.Confinement.AppID),
data: config.Confinement.Outer, data: config.Confinement.Outer,
@ -170,9 +207,14 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
} }
} }
/* // this also falls back to host path if encountering an invalid path
Resolve initial container state if !path.IsAbs(config.Confinement.Shell) {
*/ config.Confinement.Shell = "/bin/sh"
if s, ok := sys.LookupEnv(shell); ok && path.IsAbs(s) {
config.Confinement.Shell = s
}
}
// do not use the value of shell before this point
// permissive defaults // permissive defaults
if config.Confinement.Sandbox == nil { if config.Confinement.Sandbox == nil {
@ -187,7 +229,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
config.Path = p config.Path = p
} }
} else { } else {
config.Path = shellPath config.Path = config.Confinement.Shell
} }
} }
@ -255,85 +297,56 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
mapuid = newInt(uid) mapuid = newInt(uid)
mapgid = newInt(gid) mapgid = newInt(gid)
if seal.env == nil { if seal.env == nil {
seal.env = make(map[string]string) seal.env = make(map[string]string, 1<<6)
} }
} }
/* // inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as mapped uid
Initialise externals
*/
sc := sys.Paths()
seal.runDirPath = sc.RunDirPath
seal.sys = system.New(seal.user.uid.unwrap())
/*
Work directories
*/
// base fortify share path
seal.sys.Ensure(sc.SharePath, 0711)
// outer paths used by the main process
seal.sys.Ensure(sc.RunDirPath, 0700)
seal.sys.UpdatePermType(system.User, sc.RunDirPath, acl.Execute)
seal.sys.Ensure(sc.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset
seal.sys.UpdatePermType(system.User, sc.RuntimePath, acl.Execute)
// outer process-specific share directory
sharePath := path.Join(sc.SharePath, seal.id.String())
seal.sys.Ephemeral(system.Process, sharePath, 0711)
// similar to share but within XDG_RUNTIME_DIR
sharePathLocal := path.Join(sc.RunDirPath, seal.id.String())
seal.sys.Ephemeral(system.Process, sharePathLocal, 0700)
seal.sys.UpdatePerm(sharePathLocal, acl.Execute)
// inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as post-fsu user
innerRuntimeDir := path.Join("/run/user", mapuid.String()) innerRuntimeDir := path.Join("/run/user", mapuid.String())
seal.container.Tmpfs("/run/user", 1<<12, 0755) seal.container.Tmpfs("/run/user", 1<<12, 0755)
seal.container.Tmpfs(innerRuntimeDir, 1<<23, 0755) seal.container.Tmpfs(innerRuntimeDir, 1<<23, 0700)
seal.env[xdgRuntimeDir] = innerRuntimeDir seal.env[xdgRuntimeDir] = innerRuntimeDir
seal.env[xdgSessionClass] = "user" seal.env[xdgSessionClass] = "user"
seal.env[xdgSessionType] = "tty" seal.env[xdgSessionType] = "tty"
// outer path for inner /tmp share := &shareHost{seal: seal, sc: sys.Paths()}
seal.runDirPath = share.sc.RunDirPath
seal.sys = system.New(seal.user.uid.unwrap())
{ {
tmpdir := path.Join(sc.SharePath, "tmpdir") seal.sys.Ensure(share.sc.SharePath, 0711)
tmpdir := path.Join(share.sc.SharePath, "tmpdir")
seal.sys.Ensure(tmpdir, 0700) seal.sys.Ensure(tmpdir, 0700)
seal.sys.UpdatePermType(system.User, tmpdir, acl.Execute) seal.sys.UpdatePermType(system.User, tmpdir, acl.Execute)
tmpdirInst := path.Join(tmpdir, seal.user.aid.String()) tmpdirInst := path.Join(tmpdir, seal.user.aid.String())
seal.sys.Ensure(tmpdirInst, 01700) seal.sys.Ensure(tmpdirInst, 01700)
seal.sys.UpdatePermType(system.User, tmpdirInst, acl.Read, acl.Write, acl.Execute) seal.sys.UpdatePermType(system.User, tmpdirInst, acl.Read, acl.Write, acl.Execute)
// mount inner /tmp from share so it shares persistence and storage behaviour of host /tmp
seal.container.Bind(tmpdirInst, "/tmp", sandbox.BindWritable) seal.container.Bind(tmpdirInst, "/tmp", sandbox.BindWritable)
} }
/* {
Passwd database homeDir := "/var/empty"
*/ if seal.user.home != "" {
homeDir = seal.user.home
}
username := "chronos"
if seal.user.username != "" {
username = seal.user.username
}
seal.container.Bind(seal.user.data, homeDir, sandbox.BindWritable)
seal.container.Dir = homeDir
seal.env["HOME"] = homeDir
seal.env["USER"] = username
seal.env[shell] = config.Confinement.Shell
homeDir := "/var/empty" seal.container.Place("/etc/passwd",
if seal.user.home != "" { []byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Fortify:"+homeDir+":"+config.Confinement.Shell+"\n"))
homeDir = seal.user.home seal.container.Place("/etc/group",
[]byte("fortify:x:"+mapgid.String()+":\n"))
} }
username := "chronos"
if seal.user.username != "" {
username = seal.user.username
}
seal.container.Bind(seal.user.data, homeDir, sandbox.BindWritable)
seal.container.Dir = homeDir
seal.env["HOME"] = homeDir
seal.env["USER"] = username
seal.container.Place("/etc/passwd", // pass TERM for proper terminal I/O in initial process
[]byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Fortify:"+homeDir+":"+shellPath+"\n"))
seal.container.Place("/etc/group",
[]byte("fortify:x:"+mapgid.String()+":\n"))
/*
Display servers
*/
// pass $TERM for proper terminal I/O in shell
if t, ok := sys.LookupEnv(term); ok { if t, ok := sys.LookupEnv(term); ok {
seal.env[term] = t seal.env[term] = t
} }
@ -343,9 +356,9 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
var socketPath string var socketPath string
if name, ok := sys.LookupEnv(wl.WaylandDisplay); !ok { if name, ok := sys.LookupEnv(wl.WaylandDisplay); !ok {
fmsg.Verbose(wl.WaylandDisplay + " is not set, assuming " + wl.FallbackName) fmsg.Verbose(wl.WaylandDisplay + " is not set, assuming " + wl.FallbackName)
socketPath = path.Join(sc.RuntimePath, wl.FallbackName) socketPath = path.Join(share.sc.RuntimePath, wl.FallbackName)
} else if !path.IsAbs(name) { } else if !path.IsAbs(name) {
socketPath = path.Join(sc.RuntimePath, name) socketPath = path.Join(share.sc.RuntimePath, name)
} else { } else {
socketPath = name socketPath = name
} }
@ -354,18 +367,18 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
seal.env[wl.WaylandDisplay] = wl.FallbackName seal.env[wl.WaylandDisplay] = wl.FallbackName
if !config.Confinement.Sandbox.DirectWayland { // set up security-context-v1 if !config.Confinement.Sandbox.DirectWayland { // set up security-context-v1
socketDir := path.Join(sc.SharePath, "wayland")
outerPath := path.Join(socketDir, seal.id.String())
seal.sys.Ensure(socketDir, 0711)
appID := config.ID appID := config.ID
if appID == "" { if appID == "" {
// use instance ID in case app id is not set // use instance ID in case app id is not set
appID = "uk.gensokyo.fortify." + seal.id.String() appID = "uk.gensokyo.fortify." + seal.id.String()
} }
// downstream socket paths
outerPath := path.Join(share.instance(), "wayland")
seal.sys.Wayland(&seal.sync, outerPath, socketPath, appID, seal.id.String()) seal.sys.Wayland(&seal.sync, outerPath, socketPath, appID, seal.id.String())
seal.container.Bind(outerPath, innerPath, 0) seal.container.Bind(outerPath, innerPath, 0)
} else { // bind mount wayland socket (insecure) } else { // bind mount wayland socket (insecure)
fmsg.Verbose("direct wayland access, PROCEED WITH CAUTION") fmsg.Verbose("direct wayland access, PROCEED WITH CAUTION")
share.ensureRuntimeDir()
seal.container.Bind(socketPath, innerPath, 0) seal.container.Bind(socketPath, innerPath, 0)
seal.sys.UpdatePermType(system.EWayland, socketPath, acl.Read, acl.Write, acl.Execute) seal.sys.UpdatePermType(system.EWayland, socketPath, acl.Read, acl.Write, acl.Execute)
} }
@ -382,13 +395,9 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
} }
} }
/*
PulseAudio server and authentication
*/
if config.Confinement.Enablements&system.EPulse != 0 { if config.Confinement.Enablements&system.EPulse != 0 {
// PulseAudio runtime directory (usually `/run/user/%d/pulse`) // PulseAudio runtime directory (usually `/run/user/%d/pulse`)
pulseRuntimeDir := path.Join(sc.RuntimePath, "pulse") pulseRuntimeDir := path.Join(share.sc.RuntimePath, "pulse")
// PulseAudio socket (usually `/run/user/%d/pulse/native`) // PulseAudio socket (usually `/run/user/%d/pulse/native`)
pulseSocket := path.Join(pulseRuntimeDir, "native") pulseSocket := path.Join(pulseRuntimeDir, "native")
@ -416,7 +425,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
} }
// hard link pulse socket into target-executable share // hard link pulse socket into target-executable share
innerPulseRuntimeDir := path.Join(sharePathLocal, "pulse") innerPulseRuntimeDir := path.Join(share.runtime(), "pulse")
innerPulseSocket := path.Join(innerRuntimeDir, "pulse", "native") innerPulseSocket := path.Join(innerRuntimeDir, "pulse", "native")
seal.sys.Link(pulseSocket, innerPulseRuntimeDir) seal.sys.Link(pulseSocket, innerPulseRuntimeDir)
seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0) seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0)
@ -435,10 +444,6 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
} }
} }
/*
D-Bus proxy
*/
if config.Confinement.Enablements&system.EDBus != 0 { if config.Confinement.Enablements&system.EDBus != 0 {
// ensure dbus session bus defaults // ensure dbus session bus defaults
if config.Confinement.SessionBus == nil { if config.Confinement.SessionBus == nil {
@ -446,6 +451,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
} }
// downstream socket paths // downstream socket paths
sharePath := share.instance()
sessionPath, systemPath := path.Join(sharePath, "bus"), path.Join(sharePath, "system_bus_socket") sessionPath, systemPath := path.Join(sharePath, "bus"), path.Join(sharePath, "system_bus_socket")
// configure dbus proxy // configure dbus proxy
@ -471,10 +477,6 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
} }
} }
/*
Miscellaneous
*/
for _, dest := range config.Confinement.Sandbox.Cover { for _, dest := range config.Confinement.Sandbox.Cover {
seal.container.Tmpfs(dest, 1<<13, 0755) seal.container.Tmpfs(dest, 1<<13, 0755)
} }
@ -504,7 +506,13 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
// flatten and sort env for deterministic behaviour // flatten and sort env for deterministic behaviour
seal.container.Env = make([]string, 0, len(seal.env)) seal.container.Env = make([]string, 0, len(seal.env))
maps.All(seal.env)(func(k string, v string) bool { seal.container.Env = append(seal.container.Env, k+"="+v); return true }) for k, v := range seal.env {
if strings.IndexByte(k, '=') != -1 {
return fmsg.WrapError(syscall.EINVAL,
fmt.Sprintf("invalid environment variable %s", k))
}
seal.container.Env = append(seal.container.Env, k+"="+v)
}
slices.Sort(seal.container.Env) slices.Sort(seal.container.Env)
fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s", fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s",

View File

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

View File

@ -47,7 +47,7 @@ type State interface {
Uid(aid int) (int, error) Uid(aid int) (int, error)
} }
// CopyPaths is a generic implementation of [System.Paths]. // CopyPaths is a generic implementation of [fst.Paths].
func CopyPaths(os State, v *fst.Paths) { func CopyPaths(os State, v *fst.Paths) {
v.SharePath = path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Getuid())) v.SharePath = path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Getuid()))

View File

@ -88,11 +88,15 @@ in
conf = { conf = {
inherit (app) id; inherit (app) id;
path = pkgs.writeScript "${app.name}-start" '' path =
#!${pkgs.zsh}${pkgs.zsh.shellPath} if app.path == null then
${script} pkgs.writeScript "${app.name}-start" ''
''; #!${pkgs.zsh}${pkgs.zsh.shellPath}
args = [ "${app.name}-start" ]; ${script}
''
else
app.path;
args = if app.args == null then [ "${app.name}-start" ] else app.args;
confinement = { confinement = {
app_id = aid; app_id = aid;
@ -197,9 +201,11 @@ in
${copy "${pkg}/share/icons"} ${copy "${pkg}/share/icons"}
${copy "${pkg}/share/man"} ${copy "${pkg}/share/man"}
substituteInPlace $out/share/applications/* \ if test -d "$out/share/applications"; then
--replace-warn '${pkg}/bin/' "" \ substituteInPlace $out/share/applications/* \
--replace-warn '${pkg}/libexec/' "" --replace-warn '${pkg}/bin/' "" \
--replace-warn '${pkg}/libexec/' ""
fi
'' ''
) )
++ acc ++ acc

View File

@ -35,7 +35,7 @@ package
*Default:* *Default:*
` <derivation fortify-static-x86_64-unknown-linux-musl-0.3.0> ` ` <derivation fortify-static-x86_64-unknown-linux-musl-0.3.3> `
@ -73,6 +73,25 @@ list of package
## environment\.fortify\.apps\.\*\.args
Custom args\.
Setting this to null will default to script name\.
*Type:*
null or (list of string)
*Default:*
` null `
## environment\.fortify\.apps\.\*\.capability\.dbus ## environment\.fortify\.apps\.\*\.capability\.dbus
@ -486,6 +505,25 @@ boolean
## environment\.fortify\.apps\.\*\.path
Custom executable path\.
Setting this to null will default to the start script\.
*Type:*
null or string
*Default:*
` null `
## environment\.fortify\.apps\.\*\.script ## environment\.fortify\.apps\.\*\.script
@ -606,7 +644,7 @@ package
*Default:* *Default:*
` <derivation fortify-fsu-0.3.0> ` ` <derivation fortify-fsu-0.3.3> `

View File

@ -94,6 +94,24 @@ in
''; '';
}; };
path = mkOption {
type = nullOr str;
default = null;
description = ''
Custom executable path.
Setting this to null will default to the start script.
'';
};
args = mkOption {
type = nullOr (listOf str);
default = null;
description = ''
Custom args.
Setting this to null will default to script name.
'';
};
script = mkOption { script = mkOption {
type = nullOr str; type = nullOr str;
default = null; default = null;

View File

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

View File

@ -77,7 +77,9 @@ func printShowInstance(
if len(config.Confinement.Groups) > 0 { if len(config.Confinement.Groups) > 0 {
t.Printf(" Groups:\t%q\n", config.Confinement.Groups) t.Printf(" Groups:\t%q\n", config.Confinement.Groups)
} }
t.Printf(" Directory:\t%s\n", config.Confinement.Outer) if config.Confinement.Outer != "" {
t.Printf(" Directory:\t%s\n", config.Confinement.Outer)
}
if config.Confinement.Sandbox != nil { if config.Confinement.Sandbox != nil {
sandbox := config.Confinement.Sandbox sandbox := config.Confinement.Sandbox
if sandbox.Hostname != "" { if sandbox.Hostname != "" {
@ -114,7 +116,12 @@ func printShowInstance(
// Env map[string]string `json:"env"` // Env map[string]string `json:"env"`
// Link [][2]string `json:"symlink"` // Link [][2]string `json:"symlink"`
} }
t.Printf(" Command:\t%s\n", strings.Join(config.Args, " ")) if config.Confinement.Sandbox != nil {
t.Printf(" Path:\t%s\n", config.Path)
}
if len(config.Args) > 0 {
t.Printf(" Arguments:\t%s\n", strings.Join(config.Args, " "))
}
t.Printf("\n") t.Printf("\n")
if !short { if !short {
@ -247,22 +254,18 @@ func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON boo
t := newPrinter(output) t := newPrinter(output)
defer t.MustFlush() defer t.MustFlush()
t.Println("\tInstance\tPID\tApp\tUptime\tEnablements\tCommand") t.Println("\tInstance\tPID\tApplication\tUptime")
for _, e := range exp { for _, e := range exp {
var ( as := "(No configuration information)"
es = "(No confinement information)"
cs = "(No command information)"
as = "(No configuration information)"
)
if e.Config != nil { if e.Config != nil {
es = e.Config.Confinement.Enablements.String()
cs = fmt.Sprintf("%q", e.Config.Args)
as = strconv.Itoa(e.Config.Confinement.AppID) as = strconv.Itoa(e.Config.Confinement.AppID)
if e.Config.ID != "" {
as += " (" + e.Config.ID + ")"
}
} }
t.Printf("\t%s\t%d\t%s\t%s\t%s\t%s\n", 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(), strings.TrimPrefix(es, ", "), cs) e.s[:8], e.PID, as, now.Sub(e.Time).Round(time.Second).String())
} }
t.Println()
} }
type expandedStateEntry struct { type expandedStateEntry struct {

View File

@ -44,7 +44,8 @@ func Test_printShowInstance(t *testing.T) {
Flags: userns net dev tty mapuid autoetc Flags: userns net dev tty mapuid autoetc
Etc: /etc Etc: /etc
Cover: /var/run/nscd Cover: /var/run/nscd
Command: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland Path: /run/current-system/sw/bin/chromium
Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
Filesystem Filesystem
+/nix/store +/nix/store
@ -75,26 +76,22 @@ System bus
App App
ID: 0 ID: 0
Enablements: (no enablements) Enablements: (no enablements)
Directory:
Command:
`}, `},
{"config flag none", nil, &fst.Config{Confinement: fst.ConfinementConfig{Sandbox: new(fst.SandboxConfig)}}, false, false, `App {"config flag none", nil, &fst.Config{Confinement: fst.ConfinementConfig{Sandbox: new(fst.SandboxConfig)}}, false, false, `App
ID: 0 ID: 0
Enablements: (no enablements) Enablements: (no enablements)
Directory:
Flags: none Flags: none
Etc: /etc Etc: /etc
Command: Path:
`}, `},
{"config nil entries", nil, &fst.Config{Confinement: fst.ConfinementConfig{Sandbox: &fst.SandboxConfig{Filesystem: make([]*fst.FilesystemConfig, 1)}, ExtraPerms: make([]*fst.ExtraPermConfig, 1)}}, false, false, `App {"config nil entries", nil, &fst.Config{Confinement: fst.ConfinementConfig{Sandbox: &fst.SandboxConfig{Filesystem: make([]*fst.FilesystemConfig, 1)}, ExtraPerms: make([]*fst.ExtraPermConfig, 1)}}, false, false, `App
ID: 0 ID: 0
Enablements: (no enablements) Enablements: (no enablements)
Directory:
Flags: none Flags: none
Etc: /etc Etc: /etc
Command: Path:
Filesystem Filesystem
@ -106,8 +103,6 @@ Extra ACL
App App
ID: 0 ID: 0
Enablements: (no enablements) Enablements: (no enablements)
Directory:
Command:
Session bus Session bus
Filter: false Filter: false
@ -128,7 +123,8 @@ App
Flags: userns net dev tty mapuid autoetc Flags: userns net dev tty mapuid autoetc
Etc: /etc Etc: /etc
Cover: /var/run/nscd Cover: /var/run/nscd
Command: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland Path: /run/current-system/sw/bin/chromium
Arguments: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
Filesystem Filesystem
+/nix/store +/nix/store
@ -163,8 +159,6 @@ State
App App
ID: 0 ID: 0
Enablements: (no enablements) Enablements: (no enablements)
Directory:
Command:
`}, `},
@ -208,6 +202,7 @@ App
"username": "chronos", "username": "chronos",
"home_inner": "/var/lib/fortify", "home_inner": "/var/lib/fortify",
"home": "/var/lib/persist/home/org.chromium.Chromium", "home": "/var/lib/persist/home/org.chromium.Chromium",
"shell": "/run/current-system/sw/bin/zsh",
"sandbox": { "sandbox": {
"hostname": "localhost", "hostname": "localhost",
"seccomp": 32, "seccomp": 32,
@ -332,6 +327,7 @@ App
"username": "chronos", "username": "chronos",
"home_inner": "/var/lib/fortify", "home_inner": "/var/lib/fortify",
"home": "/var/lib/persist/home/org.chromium.Chromium", "home": "/var/lib/persist/home/org.chromium.Chromium",
"shell": "/run/current-system/sw/bin/zsh",
"sandbox": { "sandbox": {
"hostname": "localhost", "hostname": "localhost",
"seccomp": 32, "seccomp": 32,
@ -458,20 +454,16 @@ func Test_printPs(t *testing.T) {
short, json bool short, json bool
want string want string
}{ }{
{"no entries", make(state.Entries), false, false, ` Instance PID App Uptime Enablements Command {"no entries", make(state.Entries), false, false, ` Instance PID Application Uptime
`}, `},
{"no entries short", make(state.Entries), true, false, ``}, {"no entries short", make(state.Entries), true, false, ``},
{"nil instance", state.Entries{testID: nil}, false, false, ` Instance PID App Uptime Enablements Command {"nil instance", state.Entries{testID: nil}, false, false, ` Instance PID Application Uptime
`}, `},
{"state corruption", state.Entries{fst.ID{}: testState}, false, false, ` Instance PID App Uptime Enablements Command {"state corruption", state.Entries{fst.ID{}: testState}, false, false, ` Instance PID Application Uptime
`}, `},
{"valid", state.Entries{testID: testState}, false, false, ` Instance PID App Uptime Enablements Command {"valid", state.Entries{testID: testState}, false, false, ` Instance PID Application Uptime
8e2c76b0 3735928559 9 1h2m32s wayland, dbus, pulseaudio ["chromium" "--ignore-gpu-blocklist" "--disable-smooth-scrolling" "--enable-features=UseOzonePlatform" "--ozone-platform=wayland"] 8e2c76b0 3735928559 9 (org.chromium.Chromium) 1h2m32s
`}, `},
{"valid short", state.Entries{testID: testState}, true, false, `8e2c76b0 {"valid short", state.Entries{testID: testState}, true, false, `8e2c76b0
`}, `},
@ -514,6 +506,7 @@ func Test_printPs(t *testing.T) {
"username": "chronos", "username": "chronos",
"home_inner": "/var/lib/fortify", "home_inner": "/var/lib/fortify",
"home": "/var/lib/persist/home/org.chromium.Chromium", "home": "/var/lib/persist/home/org.chromium.Chromium",
"shell": "/run/current-system/sw/bin/zsh",
"sandbox": { "sandbox": {
"hostname": "localhost", "hostname": "localhost",
"seccomp": 32, "seccomp": 32,

View File

@ -99,19 +99,11 @@ type (
// Permission bits of newly created parent directories. // Permission bits of newly created parent directories.
// The zero value is interpreted as 0755. // The zero value is interpreted as 0755.
ParentPerm os.FileMode ParentPerm os.FileMode
// Retain CAP_SYS_ADMIN.
Privileged bool
Flags HardeningFlags Flags HardeningFlags
} }
Ops []Op
Op interface {
early(params *Params) error
apply(params *Params) error
prefix() string
Is(op Op) bool
fmt.Stringer
}
) )
func (p *Container) Start() error { func (p *Container) Start() error {
@ -165,7 +157,7 @@ func (p *Container) Start() error {
syscall.CLONE_NEWNS, syscall.CLONE_NEWNS,
// remain privileged for setup // remain privileged for setup
AmbientCaps: []uintptr{CAP_SYS_ADMIN}, AmbientCaps: []uintptr{CAP_SYS_ADMIN, CAP_SETPCAP},
UseCgroupFD: p.Cgroup != nil, UseCgroupFD: p.Cgroup != nil,
} }

View File

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

View File

@ -13,6 +13,22 @@ import (
"unsafe" "unsafe"
) )
type (
Ops []Op
Op interface {
// early is called in host root.
early(params *Params) error
// apply is called in intermediate root.
apply(params *Params) error
prefix() string
Is(op Op) bool
fmt.Stringer
}
)
func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) }
func init() { gob.Register(new(BindMount)) } func init() { gob.Register(new(BindMount)) }
// BindMount bind mounts host path Source on container path Target. // BindMount bind mounts host path Source on container path Target.

View File

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

View File

@ -73,6 +73,16 @@ func TestExport(t *testing.T) {
0x80, 0x8b, 0x1a, 0x6f, 0x84, 0xf3, 0x2b, 0xbd, 0x80, 0x8b, 0x1a, 0x6f, 0x84, 0xf3, 0x2b, 0xbd,
0xe1, 0xaa, 0x02, 0xae, 0x30, 0xee, 0xdc, 0xfa, 0xe1, 0xaa, 0x02, 0xae, 0x30, 0xee, 0xdc, 0xfa,
}, false}, }, false},
{"fortify default", seccomp.FlagExt | seccomp.FlagDenyDevel, []byte{
0xc6, 0x98, 0xb0, 0x81, 0xff, 0x95, 0x7a, 0xfe,
0x17, 0xa6, 0xd9, 0x43, 0x74, 0x53, 0x7d, 0x37,
0xf2, 0xa6, 0x3f, 0x6f, 0x9d, 0xd7, 0x5d, 0xa7,
0x54, 0x65, 0x42, 0x40, 0x7a, 0x9e, 0x32, 0x47,
0x6e, 0xbd, 0xa3, 0x31, 0x2b, 0xa7, 0x78, 0x5d,
0x7f, 0x61, 0x85, 0x42, 0xbc, 0xfa, 0xf2, 0x7c,
0xa2, 0x7d, 0xcc, 0x2d, 0xdd, 0xba, 0x85, 0x20,
0x69, 0xd2, 0x8b, 0xcf, 0xe8, 0xca, 0xd3, 0x9a,
}, false},
} }
buf := make([]byte, 8) buf := make([]byte, 8)

View File

@ -1,3 +1,4 @@
// Package seccomp provides filter presets and high level wrappers around libseccomp.
package seccomp package seccomp
/* /*

View File

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

47
sandbox/sysctl.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,39 @@
{ {
writeShellScript, lib,
writeText, nixosTest,
callPackage,
version, self,
withRace ? false,
}: }:
name: want:
writeShellScript "fortify-${name}-check-sandbox-script" '' nixosTest {
set -e name = "fortify-sandbox" + (if withRace then "-race" else "");
${callPackage ./assert.nix { inherit version; }}/bin/test \ nodes.machine =
${writeText "fortify-${name}-want.json" (builtins.toJSON want)} { options, pkgs, ... }:
touch /tmp/sandbox-ok {
'' # Run with Go race detector:
environment.fortify = lib.mkIf withRace rec {
# race detector does not support static linking
package = (pkgs.callPackage ../../package.nix { }).overrideAttrs (previousAttrs: {
GOFLAGS = previousAttrs.GOFLAGS ++ [ "-race" ];
});
fsuPackage = options.environment.fortify.fsuPackage.default.override { fortify = package; };
};
imports = [
./configuration.nix
self.nixosModules.fortify
self.inputs.home-manager.nixosModules.home-manager
];
};
# adapted from nixos sway integration tests
# testScriptWithTypes:49: error: Cannot call function of unknown type
# (machine.succeed if succeed else machine.execute)(
# ^
# Found 1 error in 1 file (checked 1 source file)
skipTypeCheck = true;
testScript = builtins.readFile ./test.py;
}

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

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

View File

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

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

@ -0,0 +1,71 @@
import json
import shlex
q = shlex.quote
def swaymsg(command: str = "", succeed=True, type="command"):
assert command != "" or type != "command", "Must specify command or type"
shell = q(f"swaymsg -t {q(type)} -- {q(command)}")
with machine.nested(
f"sending swaymsg {shell!r}" + " (allowed to fail)" * (not succeed)
):
ret = (machine.succeed if succeed else machine.execute)(
f"su - alice -c {shell}"
)
# execute also returns a status code, but disregard.
if not succeed:
_, ret = ret
if not succeed and not ret:
return None
parsed = json.loads(ret)
return parsed
start_all()
machine.wait_for_unit("multi-user.target")
# To check fortify's version:
print(machine.succeed("sudo -u alice -i fortify version"))
# Wait for Sway to complete startup:
machine.wait_for_file("/run/user/1000/wayland-1")
machine.wait_for_file("/tmp/sway-ipc.sock")
# Check seccomp outcome:
swaymsg("exec fortify run cat")
pid = int(machine.wait_until_succeeds("pgrep -U 1000000 -x cat", timeout=5))
print(machine.succeed(f"fortify-test filter {pid} c698b081ff957afe17a6d94374537d37f2a63f6f9dd75da7546542407a9e32476ebda3312ba7785d7f618542bcfaf27ca27dcc2dddba852069d28bcfe8cad39a &>/dev/stdout", timeout=5))
machine.succeed(f"kill -TERM {pid}")
# Verify capabilities/securebits in user namespace:
print(machine.succeed("sudo -u alice -i fortify run capsh --print"))
print(machine.succeed("sudo -u alice -i fortify run capsh --has-no-new-privs"))
print(machine.fail("sudo -u alice -i fortify run capsh --has-a=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i fortify run capsh --has-b=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i fortify run capsh --has-i=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i fortify run capsh --has-p=CAP_SYS_ADMIN"))
print(machine.fail("sudo -u alice -i fortify run umount -R /dev"))
# Check sandbox outcome:
check_offset = 0
def check_sandbox(name):
global check_offset
check_offset += 1
swaymsg(f"exec script /dev/null -E always -qec check-sandbox-{name}")
machine.wait_for_file(f"/tmp/fortify.1000/tmpdir/{check_offset}/sandbox-ok", timeout=15)
check_sandbox("preset")
check_sandbox("tty")
check_sandbox("mapuid")
# Exit Sway and verify process exit status 0:
swaymsg("exit", succeed=False)
machine.wait_for_file("/tmp/sway-exit-ok")
# Print fortify runDir contents:
print(machine.succeed("find /run/user/1000/fortify"))

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

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

View File

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

View File

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