Compare commits

..

46 Commits

Author SHA1 Message Date
5c4058d5ac
app: run in native sandbox
All checks were successful
Test / Create distribution (push) Successful in 20s
Test / Fortify (push) Successful in 2m5s
Test / Fpkg (push) Successful in 3m0s
Test / Data race detector (push) Successful in 4m12s
Test / Flake checks (push) Successful in 1m4s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-25 01:52:49 +09:00
e732dca762
wl: fix sync pipe keepalive
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m30s
Test / Fpkg (push) Successful in 3m36s
Test / Data race detector (push) Successful in 4m14s
Test / Flake checks (push) Successful in 59s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-25 01:33:37 +09:00
a9adcd914b
fortify/parse: omit try fd fallthrough message
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m33s
Test / Fpkg (push) Successful in 3m28s
Test / Data race detector (push) Successful in 4m12s
Test / Flake checks (push) Successful in 57s
This reduces noise in verbose output.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-25 01:21:11 +09:00
3dd4ff29c8
test/sandbox: check mount table length
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Fpkg (push) Successful in 37s
Test / Fortify (push) Successful in 2m20s
Test / Data race detector (push) Successful in 2m51s
Test / Flake checks (push) Successful in 1m0s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-24 16:36:53 +09:00
61d86c5e10
test/sandbox: fix stdout tty check
All checks were successful
Test / Create distribution (push) Successful in 27s
Test / Fpkg (push) Successful in 37s
Test / Fortify (push) Successful in 2m22s
Test / Data race detector (push) Successful in 2m57s
Test / Flake checks (push) Successful in 56s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-24 16:23:50 +09:00
d097eaa28f
test/sandbox: unquote fail messages
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m31s
Test / Fpkg (push) Successful in 3m22s
Test / Data race detector (push) Successful in 4m22s
Test / Flake checks (push) Successful in 57s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-24 16:03:53 +09:00
ad3576c164
sandbox: resolve tty name
All checks were successful
Test / Create distribution (push) Successful in 19s
Test / Fortify (push) Successful in 2m17s
Test / Fpkg (push) Successful in 3m15s
Test / Data race detector (push) Successful in 4m10s
Test / Flake checks (push) Successful in 56s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-24 16:03:07 +09:00
b989a4601a
test/sandbox: fail on mismatched mount entry
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Fpkg (push) Successful in 34s
Test / Fortify (push) Successful in 2m26s
Test / Data race detector (push) Successful in 2m47s
Test / Flake checks (push) Successful in 57s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-24 13:43:32 +09:00
a11237b158
sandbox/vfs: add doc comments
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m42s
Test / Fpkg (push) Successful in 3m40s
Test / Data race detector (push) Successful in 4m15s
Test / Flake checks (push) Successful in 55s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-24 13:21:55 +09:00
40f00d570e
sandbox: set mkdir perm
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m34s
Test / Fpkg (push) Successful in 3m26s
Test / Data race detector (push) Successful in 4m7s
Test / Flake checks (push) Successful in 57s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-24 12:51:39 +09:00
0eb1bc6301
test/sandbox: verify outcome via mountinfo
All checks were successful
Test / Fpkg (push) Successful in 36s
Test / Create distribution (push) Successful in 4m56s
Test / Fortify (push) Successful in 6m33s
Test / Data race detector (push) Successful in 7m3s
Test / Flake checks (push) Successful in 54s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-24 01:42:38 +09:00
1eb837eab8
test/sandbox: warn about misuse in doc comment
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Fpkg (push) Successful in 34s
Test / Fortify (push) Successful in 2m16s
Test / Data race detector (push) Successful in 2m45s
Test / Flake checks (push) Successful in 59s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-23 23:28:28 +09:00
0a4e633db2
nix: filter test from source
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m42s
Test / Fpkg (push) Successful in 3m52s
Test / Data race detector (push) Successful in 4m19s
Test / Flake checks (push) Successful in 54s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-23 22:20:19 +09:00
e8809125d4
sandbox: verify outcome via mountinfo
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m39s
Test / Fpkg (push) Successful in 3m29s
Test / Data race detector (push) Successful in 4m17s
Test / Flake checks (push) Successful in 1m6s
This contains much more information than /proc/mounts and allows for more fields to be checked. This also removes the dependency on the test package.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-23 22:17:36 +09:00
806ce18c0a
test/sandbox: check mapuid outcome
All checks were successful
Test / Create distribution (push) Successful in 27s
Test / Fpkg (push) Successful in 37s
Test / Fortify (push) Successful in 2m23s
Test / Data race detector (push) Successful in 2m50s
Test / Flake checks (push) Successful in 55s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-23 17:56:07 +09:00
b71d2bf534
test/sandbox: check tty outcome
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Fpkg (push) Successful in 34s
Test / Fortify (push) Successful in 2m21s
Test / Data race detector (push) Successful in 2m48s
Test / Flake checks (push) Successful in 54s
This makes no difference currently but has different behaviour in the native sandbox.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-23 17:28:57 +09:00
46059b1840
test/sandbox: print mismatching file content
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Fpkg (push) Successful in 34s
Test / Fortify (push) Successful in 2m3s
Test / Data race detector (push) Successful in 2m32s
Test / Flake checks (push) Successful in 51s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-23 17:24:52 +09:00
d2c329bcea
test: format path aid offsets
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Fpkg (push) Successful in 36s
Test / Fortify (push) Successful in 2m12s
Test / Data race detector (push) Successful in 2m41s
Test / Flake checks (push) Successful in 51s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-23 17:21:14 +09:00
2d379b5a38
test/sandbox: pass want file as argument
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Fpkg (push) Successful in 33s
Test / Fortify (push) Successful in 2m7s
Test / Data race detector (push) Successful in 2m36s
Test / Flake checks (push) Successful in 49s
This avoids building the check program multiple times.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-23 15:00:59 +09:00
75e0c5d406
test/sandbox: parse full test case
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m37s
Test / Fpkg (push) Successful in 3m52s
Test / Data race detector (push) Successful in 4m12s
Test / Flake checks (push) Successful in 50s
This makes declaring multiple tests much cleaner.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-23 14:53:50 +09:00
770b37ae16
sandbox/vfs: match MS_NOSYMFOLLOW flag
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m27s
Test / Fpkg (push) Successful in 3m33s
Test / Data race detector (push) Successful in 4m10s
Test / Flake checks (push) Successful in 52s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-23 13:57:30 +09:00
c638193268
sandbox: apply vfs options to bind mounts
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m41s
Test / Fpkg (push) Successful in 3m45s
Test / Data race detector (push) Successful in 4m11s
Test / Flake checks (push) Successful in 56s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-23 05:27:57 +09:00
8c3a817881
sandbox/vfs: unfold mount hierarchy
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m33s
Test / Fpkg (push) Successful in 3m31s
Test / Data race detector (push) Successful in 4m11s
Test / Flake checks (push) Successful in 53s
This presents all visible mount points under path. This is useful for applying extra vfs options to bind mounts.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-23 05:23:31 +09:00
e2fce321c1
sandbox/vfs: expose mountinfo line scanning
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m38s
Test / Fpkg (push) Successful in 3m30s
Test / Data race detector (push) Successful in 4m9s
Test / Flake checks (push) Successful in 51s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-23 02:46:58 +09:00
241702ae3a
go: 1.23
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m48s
Test / Data race detector (push) Successful in 4m22s
Test / Fpkg (push) Successful in 1h28m30s
Test / Flake checks (push) Successful in 49s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-22 18:20:06 +09:00
d21d9c5b1d
sandbox/vfs: parse vfs options
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m36s
Test / Fpkg (push) Successful in 3m20s
Test / Data race detector (push) Successful in 4m4s
Test / Flake checks (push) Successful in 50s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-21 17:12:10 +09:00
a70daf2250
sandbox: resolve inverted flags in op
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m5s
Test / Data race detector (push) Successful in 2m30s
Test / Fpkg (push) Successful in 2m48s
Test / Flake checks (push) Successful in 48s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-21 12:58:38 +09:00
632b18addd
test/sandbox: rename misleading bind destination
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Fpkg (push) Successful in 34s
Test / Fortify (push) Successful in 2m15s
Test / Data race detector (push) Successful in 2m49s
Test / Flake checks (push) Successful in 59s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-21 12:56:11 +09:00
a57a7a6a16
test/sandbox: check type handling host_passthrough
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m30s
Test / Fpkg (push) Successful in 3m30s
Test / Data race detector (push) Successful in 4m20s
Test / Flake checks (push) Successful in 52s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-21 12:21:08 +09:00
5098b12e4a
sandbox/vfs: count mountinfo entries
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m39s
Test / Fpkg (push) Successful in 3m37s
Test / Data race detector (push) Successful in 4m10s
Test / Flake checks (push) Successful in 52s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-21 12:14:33 +09:00
9ddf5794dd
sandbox/vfs: implement proc_pid_mountinfo(5) parser
All checks were successful
Test / Create distribution (push) Successful in 28s
Test / Fortify (push) Successful in 2m43s
Test / Fpkg (push) Successful in 3m38s
Test / Data race detector (push) Successful in 4m11s
Test / Flake checks (push) Successful in 53s
Test cases are mostly taken from util-linux. This implementation is more correct and slightly faster than the one found in github:kubernetes/utils.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-21 00:35:49 +09:00
b74a08dda9
sandbox: prepare ops early
All checks were successful
Test / Create distribution (push) Successful in 24s
Test / Fortify (push) Successful in 2m27s
Test / Fpkg (push) Successful in 3m33s
Test / Data race detector (push) Successful in 4m9s
Test / Flake checks (push) Successful in 53s
Some setup code needs to run in host root. This change allows that to happen.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-18 02:17:46 +09:00
1b9408864f
sandbox: pass cmd to cancel function
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m35s
Test / Fpkg (push) Successful in 3m36s
Test / Data race detector (push) Successful in 4m11s
Test / Flake checks (push) Successful in 49s
This is not usually in scope otherwise.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 22:36:39 +09:00
cc89dbdf63
sandbox: place files with content
All checks were successful
Test / Create distribution (push) Successful in 24s
Test / Fortify (push) Successful in 2m35s
Test / Fpkg (push) Successful in 3m35s
Test / Data race detector (push) Successful in 4m7s
Test / Flake checks (push) Successful in 47s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 22:13:22 +09:00
228f3301f2
sandbox: create directories
All checks were successful
Test / Create distribution (push) Successful in 24s
Test / Fortify (push) Successful in 2m29s
Test / Fpkg (push) Successful in 3m30s
Test / Data race detector (push) Successful in 4m2s
Test / Flake checks (push) Successful in 48s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 22:03:06 +09:00
07181138e5
sandbox/mount: pass absolute path
All checks were successful
Test / Create distribution (push) Successful in 24s
Test / Fortify (push) Successful in 2m31s
Test / Fpkg (push) Successful in 3m24s
Test / Data race detector (push) Successful in 4m6s
Test / Flake checks (push) Successful in 48s
This should never be used unless there is a good reason to, like using a file in the intermediate root.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 21:53:31 +09:00
816b372f14
sandbox: cancel process on serve error
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m32s
Test / Fpkg (push) Successful in 3m28s
Test / Data race detector (push) Successful in 4m4s
Test / Flake checks (push) Successful in 49s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 21:49:45 +09:00
d7eddd54a2
sandbox: rename params struct
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m33s
Test / Fpkg (push) Successful in 3m27s
Test / Data race detector (push) Successful in 4m3s
Test / Flake checks (push) Successful in 59s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 21:45:08 +09:00
7c063833e0
internal/sys: wrap getuid/getgid
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m34s
Test / Fpkg (push) Successful in 3m26s
Test / Data race detector (push) Successful in 4m8s
Test / Flake checks (push) Successful in 50s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 17:10:03 +09:00
af3619d440
sandbox: create symlinks
All checks were successful
Test / Create distribution (push) Successful in 24s
Test / Fortify (push) Successful in 2m30s
Test / Fpkg (push) Successful in 3m21s
Test / Data race detector (push) Successful in 4m3s
Test / Flake checks (push) Successful in 48s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 16:37:56 +09:00
528674cb6e
sandbox/init: fail early on nil op
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m27s
Test / Fpkg (push) Successful in 3m21s
Test / Data race detector (push) Successful in 4m3s
Test / Flake checks (push) Successful in 49s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 16:17:03 +09:00
70c9757e26
sandbox/mount: rename device flag
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m28s
Test / Fpkg (push) Successful in 3m30s
Test / Data race detector (push) Successful in 4m5s
Test / Flake checks (push) Successful in 51s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 16:10:55 +09:00
c83a7e2efc
sandbox: mount container /dev/mqueue
All checks were successful
Test / Create distribution (push) Successful in 24s
Test / Fortify (push) Successful in 2m26s
Test / Fpkg (push) Successful in 3m21s
Test / Data race detector (push) Successful in 4m0s
Test / Flake checks (push) Successful in 49s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 15:42:40 +09:00
904208b87f
sandbox: unwrap path string
All checks were successful
Test / Create distribution (push) Successful in 24s
Test / Fortify (push) Successful in 2m35s
Test / Fpkg (push) Successful in 3m21s
Test / Data race detector (push) Successful in 4m9s
Test / Flake checks (push) Successful in 50s
Mount proc and dev takes no additional parameters.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 15:33:20 +09:00
007b52d81f
sandbox/seccomp: check for both partial read outcomes
All checks were successful
Test / Create distribution (push) Successful in 24s
Test / Fortify (push) Successful in 2m28s
Test / Fpkg (push) Successful in 3m17s
Test / Data race detector (push) Successful in 4m1s
Test / Flake checks (push) Successful in 47s
This eliminates intermittent test failures.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 12:51:21 +09:00
3385538142
nix: clean up flake outputs
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Fpkg (push) Successful in 32s
Test / Fortify (push) Successful in 2m0s
Test / Data race detector (push) Successful in 2m32s
Test / Flake checks (push) Successful in 48s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-17 12:26:19 +09:00
69 changed files with 3433 additions and 1920 deletions

View File

@ -4,12 +4,15 @@ import (
"encoding/json" "encoding/json"
"log" "log"
"os" "os"
"path"
"git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
"git.gensokyo.uk/security/fortify/system" "git.gensokyo.uk/security/fortify/system"
) )
type bundleInfo struct { type appInfo struct {
Name string `json:"name"` Name string `json:"name"`
Version string `json:"version"` Version string `json:"version"`
@ -20,13 +23,15 @@ type bundleInfo struct {
// passed through to [fst.Config] // passed through to [fst.Config]
Groups []string `json:"groups,omitempty"` Groups []string `json:"groups,omitempty"`
// passed through to [fst.Config] // passed through to [fst.Config]
UserNS bool `json:"userns,omitempty"` Devel bool `json:"devel,omitempty"`
// passed through to [fst.Config]
Userns bool `json:"userns,omitempty"`
// passed through to [fst.Config] // passed through to [fst.Config]
Net bool `json:"net,omitempty"` Net bool `json:"net,omitempty"`
// passed through to [fst.Config] // passed through to [fst.Config]
Dev bool `json:"dev,omitempty"` Dev bool `json:"dev,omitempty"`
// passed through to [fst.Config] // passed through to [fst.Config]
NoNewSession bool `json:"no_new_session,omitempty"` Tty bool `json:"tty,omitempty"`
// passed through to [fst.Config] // passed through to [fst.Config]
MapRealUID bool `json:"map_real_uid,omitempty"` MapRealUID bool `json:"map_real_uid,omitempty"`
// passed through to [fst.Config] // passed through to [fst.Config]
@ -38,11 +43,9 @@ type bundleInfo struct {
// passed through to [fst.Config] // passed through to [fst.Config]
Enablements system.Enablements `json:"enablements"` Enablements system.Enablements `json:"enablements"`
// passed through inverted to [bwrap.SyscallPolicy] // passed through to [fst.Config]
Devel bool `json:"devel,omitempty"`
// passed through to [bwrap.SyscallPolicy]
Multiarch bool `json:"multiarch,omitempty"` Multiarch bool `json:"multiarch,omitempty"`
// passed through to [bwrap.SyscallPolicy] // passed through to [fst.Config]
Bluetooth bool `json:"bluetooth,omitempty"` Bluetooth bool `json:"bluetooth,omitempty"`
// allow gpu access within sandbox // allow gpu access within sandbox
@ -59,8 +62,64 @@ type bundleInfo struct {
ActivationPackage string `json:"activation_package"` ActivationPackage string `json:"activation_package"`
} }
func loadBundleInfo(name string, beforeFail func()) *bundleInfo { func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool) *fst.Config {
bundle := new(bundleInfo) config := &fst.Config{
ID: app.ID,
Path: argv[0],
Args: argv,
Confinement: fst.ConfinementConfig{
AppID: app.AppID,
Groups: app.Groups,
Username: "fortify",
Inner: path.Join("/data/data", app.ID),
Outer: pathSet.homeDir,
Sandbox: &fst.SandboxConfig{
Hostname: formatHostname(app.Name),
Devel: app.Devel,
Userns: app.Userns,
Net: app.Net,
Dev: app.Dev,
Tty: app.Tty || flagDropShell,
MapRealUID: app.MapRealUID,
DirectWayland: app.DirectWayland,
Filesystem: []*fst.FilesystemConfig{
{Src: path.Join(pathSet.nixPath, "store"), Dst: "/nix/store", Must: true},
{Src: pathSet.metaPath, Dst: path.Join(fst.Tmp, "app"), Must: true},
{Src: "/etc/resolv.conf"},
{Src: "/sys/block"},
{Src: "/sys/bus"},
{Src: "/sys/class"},
{Src: "/sys/dev"},
{Src: "/sys/devices"},
},
Link: [][2]string{
{app.CurrentSystem, "/run/current-system"},
{"/run/current-system/sw/bin", "/bin"},
{"/run/current-system/sw/bin", "/usr/bin"},
},
Etc: path.Join(pathSet.cacheDir, "etc"),
AutoEtc: true,
},
ExtraPerms: []*fst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
},
SystemBus: app.SystemBus,
SessionBus: app.SessionBus,
Enablements: app.Enablements,
},
}
if app.Multiarch {
config.Confinement.Sandbox.Seccomp |= seccomp.FlagMultiarch
}
if app.Bluetooth {
config.Confinement.Sandbox.Seccomp |= seccomp.FlagBluetooth
}
return config
}
func loadAppInfo(name string, beforeFail func()) *appInfo {
bundle := new(appInfo)
if f, err := os.Open(name); err != nil { if f, err := os.Open(name); err != nil {
beforeFail() beforeFail()
log.Fatalf("cannot open bundle: %v", err) log.Fatalf("cannot open bundle: %v", err)

View File

@ -12,9 +12,7 @@ import (
"git.gensokyo.uk/security/fortify/command" "git.gensokyo.uk/security/fortify/command"
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/app/init0"
"git.gensokyo.uk/security/fortify/internal/app/shim" "git.gensokyo.uk/security/fortify/internal/app/shim"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/sys" "git.gensokyo.uk/security/fortify/internal/sys"
@ -39,7 +37,6 @@ func init() {
func main() { func main() {
// early init path, skips root check and duplicate PR_SET_DUMPABLE // early init path, skips root check and duplicate PR_SET_DUMPABLE
sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg) sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg)
init0.TryArgv0()
if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil { if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err) log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
@ -65,9 +62,7 @@ func main() {
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console"). Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next fortify action") Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next fortify action")
// internal commands
c.Command("shim", command.UsageInternal, func([]string) error { shim.Main(); return errSuccess }) c.Command("shim", command.UsageInternal, func([]string) error { shim.Main(); return errSuccess })
c.Command("init", command.UsageInternal, func([]string) error { init0.Main(); return errSuccess })
{ {
var ( var (
@ -124,7 +119,7 @@ func main() {
Parse bundle and app metadata, do pre-install checks. Parse bundle and app metadata, do pre-install checks.
*/ */
bundle := loadBundleInfo(path.Join(workDir, "bundle.json"), cleanup) bundle := loadAppInfo(path.Join(workDir, "bundle.json"), cleanup)
pathSet := pathSetByApp(bundle.ID) pathSet := pathSetByApp(bundle.ID)
app := bundle app := bundle
@ -140,7 +135,7 @@ func main() {
log.Printf("metadata path %q is not a file", pathSet.metaPath) log.Printf("metadata path %q is not a file", pathSet.metaPath)
return syscall.EBADMSG return syscall.EBADMSG
} else { } else {
app = loadBundleInfo(pathSet.metaPath, cleanup) app = loadAppInfo(pathSet.metaPath, cleanup)
if app.ID != bundle.ID { if app.ID != bundle.ID {
cleanup() cleanup()
log.Printf("app %q claims to have identifier %q", log.Printf("app %q claims to have identifier %q",
@ -273,7 +268,7 @@ func main() {
id := args[0] id := args[0]
pathSet := pathSetByApp(id) pathSet := pathSetByApp(id)
app := loadBundleInfo(pathSet.metaPath, func() {}) app := loadAppInfo(pathSet.metaPath, func() {})
if app.ID != id { if app.ID != id {
log.Printf("app %q claims to have identifier %q", id, app.ID) log.Printf("app %q claims to have identifier %q", id, app.ID)
return syscall.EBADE return syscall.EBADE
@ -322,51 +317,7 @@ func main() {
} }
argv = append(argv, args[1:]...) argv = append(argv, args[1:]...)
config := &fst.Config{ config := app.toFst(pathSet, argv, flagDropShell)
ID: app.ID,
Command: argv,
Confinement: fst.ConfinementConfig{
AppID: app.AppID,
Groups: app.Groups,
Username: "fortify",
Inner: path.Join("/data/data", app.ID),
Outer: pathSet.homeDir,
Sandbox: &fst.SandboxConfig{
Hostname: formatHostname(app.Name),
UserNS: app.UserNS,
Net: app.Net,
Dev: app.Dev,
Syscall: &bwrap.SyscallPolicy{DenyDevel: !app.Devel, Multiarch: app.Multiarch, Bluetooth: app.Bluetooth},
NoNewSession: app.NoNewSession || flagDropShell,
MapRealUID: app.MapRealUID,
DirectWayland: app.DirectWayland,
Filesystem: []*fst.FilesystemConfig{
{Src: path.Join(pathSet.nixPath, "store"), Dst: "/nix/store", Must: true},
{Src: pathSet.metaPath, Dst: path.Join(fst.Tmp, "app"), Must: true},
{Src: "/etc/resolv.conf"},
{Src: "/sys/block"},
{Src: "/sys/bus"},
{Src: "/sys/class"},
{Src: "/sys/dev"},
{Src: "/sys/devices"},
},
Link: [][2]string{
{app.CurrentSystem, "/run/current-system"},
{"/run/current-system/sw/bin", "/bin"},
{"/run/current-system/sw/bin", "/usr/bin"},
},
Etc: path.Join(pathSet.cacheDir, "etc"),
AutoEtc: true,
},
ExtraPerms: []*fst.ExtraPermConfig{
{Path: dataHome, Execute: true},
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
},
SystemBus: app.SystemBus,
SessionBus: app.SessionBus,
Enablements: app.Enablements,
},
}
/* /*
Expose GPU devices. Expose GPU devices.

View File

@ -11,14 +11,14 @@ import (
func mustRunApp(ctx context.Context, config *fst.Config, beforeFail func()) { func mustRunApp(ctx context.Context, config *fst.Config, beforeFail func()) {
rs := new(fst.RunState) rs := new(fst.RunState)
a := app.MustNew(std) a := app.MustNew(ctx, std)
if sa, err := a.Seal(config); err != nil { if sa, err := a.Seal(config); err != nil {
fmsg.PrintBaseError(err, "cannot seal app:") fmsg.PrintBaseError(err, "cannot seal app:")
rs.ExitCode = 1 rs.ExitCode = 1
} else { } else {
// this updates ExitCode // this updates ExitCode
app.PrintRunStateErr(rs, sa.Run(ctx, rs)) app.PrintRunStateErr(rs, sa.Run(rs))
} }
if rs.ExitCode != 0 { if rs.ExitCode != 0 {

View File

@ -62,8 +62,8 @@ def check_state(name, enablements):
config = instance['config'] config = instance['config']
if len(config['command']) != 1 or not (config['command'][0].startswith("/nix/store/")) or f"fortify-{name}-" not in (config['command'][0]): if len(config['args']) != 1 or not (config['args'][0].startswith("/nix/store/")) or f"fortify-{name}-" not in (config['args'][0]):
raise Exception(f"unexpected command {instance['config']['command']}") raise Exception(f"unexpected args {instance['config']['args']}")
if config['confinement']['enablements'] != enablements: if config['confinement']['enablements'] != enablements:
raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}") raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}")

View File

@ -6,18 +6,19 @@ import (
"strings" "strings"
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
) )
func withNixDaemon( func withNixDaemon(
ctx context.Context, ctx context.Context,
action string, command []string, net bool, updateConfig func(config *fst.Config) *fst.Config, action string, command []string, net bool, updateConfig func(config *fst.Config) *fst.Config,
app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func(), app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
) { ) {
mustRunAppDropShell(ctx, updateConfig(&fst.Config{ mustRunAppDropShell(ctx, updateConfig(&fst.Config{
ID: app.ID, ID: app.ID,
Command: []string{shellPath, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " + Path: shellPath,
Args: []string{shellPath, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
// start nix-daemon // start nix-daemon
"nix-daemon --store / & " + "nix-daemon --store / & " +
// wait for socket to appear // wait for socket to appear
@ -35,10 +36,10 @@ func withNixDaemon(
Outer: pathSet.homeDir, Outer: pathSet.homeDir,
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
Net: net, Net: net,
Syscall: &bwrap.SyscallPolicy{Multiarch: true}, Seccomp: seccomp.FlagMultiarch,
NoNewSession: dropShell, Tty: dropShell,
Filesystem: []*fst.FilesystemConfig{ Filesystem: []*fst.FilesystemConfig{
{Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true}, {Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true},
}, },
@ -61,10 +62,11 @@ func withNixDaemon(
func withCacheDir( func withCacheDir(
ctx context.Context, ctx context.Context,
action string, command []string, workDir string, action string, command []string, workDir string,
app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) { app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
mustRunAppDropShell(ctx, &fst.Config{ mustRunAppDropShell(ctx, &fst.Config{
ID: app.ID, ID: app.ID,
Command: []string{shellPath, "-lc", strings.Join(command, " && ")}, Path: shellPath,
Args: []string{shellPath, "-lc", strings.Join(command, " && ")},
Confinement: fst.ConfinementConfig{ Confinement: fst.ConfinementConfig{
AppID: app.AppID, AppID: app.AppID,
Username: "nixos", Username: "nixos",
@ -72,8 +74,8 @@ func withCacheDir(
Outer: pathSet.cacheDir, // this also ensures cacheDir via shim Outer: pathSet.cacheDir, // this also ensures cacheDir via shim
Sandbox: &fst.SandboxConfig{ Sandbox: &fst.SandboxConfig{
Hostname: formatHostname(app.Name) + "-" + action, Hostname: formatHostname(app.Name) + "-" + action,
Syscall: &bwrap.SyscallPolicy{Multiarch: true}, Seccomp: seccomp.FlagMultiarch,
NoNewSession: dropShell, Tty: dropShell,
Filesystem: []*fst.FilesystemConfig{ Filesystem: []*fst.FilesystemConfig{
{Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true}, {Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true},
{Src: workDir, Dst: path.Join(fst.Tmp, "bundle"), Must: true}, {Src: workDir, Dst: path.Join(fst.Tmp, "bundle"), Must: true},
@ -97,7 +99,7 @@ func withCacheDir(
func mustRunAppDropShell(ctx context.Context, config *fst.Config, dropShell bool, beforeFail func()) { func mustRunAppDropShell(ctx context.Context, config *fst.Config, dropShell bool, beforeFail func()) {
if dropShell { if dropShell {
config.Command = []string{shellPath, "-l"} config.Args = []string{shellPath, "-l"}
mustRunApp(ctx, config, beforeFail) mustRunApp(ctx, config, beforeFail)
beforeFail() beforeFail()
internal.Exit(0) internal.Exit(0)

12
flake.lock generated
View File

@ -7,11 +7,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1739757849, "lastModified": 1742234739,
"narHash": "sha256-Gs076ot1YuAAsYVcyidLKUMIc4ooOaRGO0PqTY7sBzA=", "narHash": "sha256-zFL6zsf/5OztR1NSNQF33dvS1fL/BzVUjabZq4qrtY4=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "9d3d080aec2a35e05a15cedd281c2384767c2cfe", "rev": "f6af7280a3390e65c2ad8fd059cdc303426cbd59",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -23,11 +23,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1741445498, "lastModified": 1742512142,
"narHash": "sha256-F5Em0iv/CxkN5mZ9hRn3vPknpoWdcdCyR0e4WklHwiE=", "narHash": "sha256-8XfURTDxOm6+33swQJu/hx6xw1Tznl8vJJN5HwVqckg=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "52e3095f6d812b91b22fb7ad0bfc1ab416453634", "rev": "7105ae3957700a9646cc4b766f5815b23ed0c682",
"type": "github" "type": "github"
}, },
"original": { "original": {

100
flake.nix
View File

@ -27,7 +27,7 @@
nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; }); nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
in in
{ {
nixosModules.fortify = import ./nixos.nix; nixosModules.fortify = import ./nixos.nix self.packages;
buildPackage = forAllSystems ( buildPackage = forAllSystems (
system: system:
@ -105,9 +105,21 @@
default = fortify; default = fortify;
fortify = pkgs.pkgsStatic.callPackage ./package.nix { fortify = pkgs.pkgsStatic.callPackage ./package.nix {
inherit (pkgs) inherit (pkgs)
# passthru.buildInputs
go
gcc
# nativeBuildInputs
pkg-config
wayland-scanner
makeBinaryWrapper
# appPackages
glibc
bubblewrap bubblewrap
xdg-dbus-proxy xdg-dbus-proxy
glibc
# fpkg
zstd zstd
gnutar gnutar
coreutils coreutils
@ -115,7 +127,7 @@
}; };
fsu = pkgs.callPackage ./cmd/fsu/package.nix { inherit (self.packages.${system}) fortify; }; fsu = pkgs.callPackage ./cmd/fsu/package.nix { inherit (self.packages.${system}) fortify; };
dist = pkgs.runCommand "${fortify.name}-dist" { inherit (self.devShells.${system}.default) buildInputs; } '' dist = pkgs.runCommand "${fortify.name}-dist" { buildInputs = fortify.targetPkgs ++ [ pkgs.pkgsStatic.musl ]; } ''
# go requires XDG_CACHE_HOME for the build cache # go requires XDG_CACHE_HOME for the build cache
export XDG_CACHE_HOME="$(mktemp -d)" export XDG_CACHE_HOME="$(mktemp -d)"
@ -128,93 +140,21 @@
export FORTIFY_VERSION="v${fortify.version}" export FORTIFY_VERSION="v${fortify.version}"
./dist/release.sh && mkdir $out && cp -v "dist/fortify-$FORTIFY_VERSION.tar.gz"* $out ./dist/release.sh && mkdir $out && cp -v "dist/fortify-$FORTIFY_VERSION.tar.gz"* $out
''; '';
fhs = pkgs.buildFHSEnv {
pname = "fortify-fhs";
inherit (fortify) version;
targetPkgs =
pkgs:
with pkgs;
[
go
gcc
pkg-config
wayland-scanner
]
++ (
with pkgs.pkgsStatic;
[
musl
libffi
libseccomp
acl
wayland
wayland-protocols
]
++ (with xorg; [
libxcb
libXau
libXdmcp
xorgproto
])
);
extraOutputsToInstall = [ "dev" ];
profile = ''
export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH"
'';
};
} }
); );
devShells = forAllSystems ( devShells = forAllSystems (
system: system:
let let
inherit (self.packages.${system}) fortify fhs; inherit (self.packages.${system}) fortify;
pkgs = nixpkgsFor.${system}; pkgs = nixpkgsFor.${system};
in in
{ {
default = pkgs.mkShell { default = pkgs.mkShell { buildInputs = fortify.targetPkgs; };
buildInputs = withPackage = pkgs.mkShell { buildInputs = [ fortify ] ++ fortify.targetPkgs; };
with pkgs;
[
go
gcc
]
# buildInputs
++ (
with pkgsStatic;
[
musl
libffi
libseccomp
acl
wayland
wayland-protocols
]
++ (with xorg; [
libxcb
libXau
libXdmcp
])
)
# nativeBuildInputs
++ [
pkg-config
wayland-scanner
makeBinaryWrapper
];
};
fhs = fhs.env;
withPackage = nixpkgsFor.${system}.mkShell {
buildInputs = [ self.packages.${system}.fortify ] ++ self.devShells.${system}.default.buildInputs;
};
generateDoc = generateDoc =
let let
pkgs = nixpkgsFor.${system};
inherit (pkgs) lib; inherit (pkgs) lib;
doc = doc =
@ -223,7 +163,7 @@
specialArgs = { specialArgs = {
inherit pkgs; inherit pkgs;
}; };
modules = [ ./options.nix ]; modules = [ (import ./options.nix self.packages) ];
}; };
cleanEval = lib.filterAttrsRecursive (n: _: n != "_module") eval; cleanEval = lib.filterAttrsRecursive (n: _: n != "_module") eval;
in in
@ -233,7 +173,7 @@
sed -i '/*Declared by:*/,+1 d' $out sed -i '/*Declared by:*/,+1 d' $out
''; '';
in in
nixpkgsFor.${system}.mkShell { pkgs.mkShell {
shellHook = '' shellHook = ''
exec cat ${docText} > options.md exec cat ${docText} > options.md
''; '';

View File

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

View File

@ -2,7 +2,7 @@ package fst
import ( import (
"git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/helper/bwrap" "git.gensokyo.uk/security/fortify/sandbox/seccomp"
"git.gensokyo.uk/security/fortify/system" "git.gensokyo.uk/security/fortify/system"
) )
@ -14,8 +14,11 @@ type Config struct {
// passed to wayland security-context-v1 as application ID // passed to wayland security-context-v1 as application ID
// and used as part of defaults in dbus session proxy // and used as part of defaults in dbus session proxy
ID string `json:"id"` ID string `json:"id"`
// final argv, passed to init
Command []string `json:"command"` // absolute path to executable file
Path string `json:"path,omitempty"`
// final args passed to container init
Args []string `json:"args"`
Confinement ConfinementConfig `json:"confinement"` Confinement ConfinementConfig `json:"confinement"`
} }
@ -26,13 +29,13 @@ type ConfinementConfig struct {
AppID int `json:"app_id"` AppID int `json:"app_id"`
// list of supplementary groups to inherit // list of supplementary groups to inherit
Groups []string `json:"groups"` Groups []string `json:"groups"`
// passwd username in the sandbox, defaults to passwd name of target uid or chronos // passwd username in container, defaults to passwd name of target uid or chronos
Username string `json:"username,omitempty"` Username string `json:"username,omitempty"`
// home directory in sandbox, empty for outer // home directory in container, empty for outer
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"`
// bwrap sandbox confinement 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
ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"` ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
@ -44,7 +47,7 @@ type ConfinementConfig struct {
// nil value makes session bus proxy assume built-in defaults // nil value makes session bus proxy assume built-in defaults
SessionBus *dbus.Config `json:"session_bus,omitempty"` SessionBus *dbus.Config `json:"session_bus,omitempty"`
// system resources to expose to the sandbox // system resources to expose to the container
Enablements system.Enablements `json:"enablements"` Enablements system.Enablements `json:"enablements"`
} }
@ -76,24 +79,12 @@ func (e *ExtraPermConfig) String() string {
return string(buf) return string(buf)
} }
type FilesystemConfig struct {
// mount point in sandbox, same as src if empty
Dst string `json:"dst,omitempty"`
// host filesystem path to make available to sandbox
Src string `json:"src"`
// write access
Write bool `json:"write,omitempty"`
// device access
Device bool `json:"dev,omitempty"`
// fail if mount fails
Must bool `json:"require,omitempty"`
}
// Template returns a fully populated instance of Config. // Template returns a fully populated instance of Config.
func Template() *Config { func Template() *Config {
return &Config{ return &Config{
ID: "org.chromium.Chromium", ID: "org.chromium.Chromium",
Command: []string{ Path: "/run/current-system/sw/bin/chromium",
Args: []string{
"chromium", "chromium",
"--ignore-gpu-blocklist", "--ignore-gpu-blocklist",
"--disable-smooth-scrolling", "--disable-smooth-scrolling",
@ -108,11 +99,13 @@ func Template() *Config {
Inner: "/var/lib/fortify", Inner: "/var/lib/fortify",
Sandbox: &SandboxConfig{ Sandbox: &SandboxConfig{
Hostname: "localhost", Hostname: "localhost",
UserNS: true, Devel: true,
Userns: true,
Net: true, Net: true,
Dev: true, Dev: true,
Syscall: &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true}, Seccomp: seccomp.FlagMultiarch,
NoNewSession: true, Tty: true,
Multiarch: true,
MapRealUID: true, MapRealUID: true,
DirectWayland: false, DirectWayland: false,
// example API credentials pulled from Google Chrome // example API credentials pulled from Google Chrome
@ -134,7 +127,7 @@ func Template() *Config {
Link: [][2]string{{"/run/user/65534", "/run/user/150"}}, Link: [][2]string{{"/run/user/65534", "/run/user/150"}},
Etc: "/etc", Etc: "/etc",
AutoEtc: true, AutoEtc: true,
Override: []string{"/var/run/nscd"}, Cover: []string{"/var/run/nscd"},
}, },
ExtraPerms: []*ExtraPermConfig{ ExtraPerms: []*ExtraPermConfig{
{Path: "/var/lib/fortify/u0", Ensure: true, Execute: true}, {Path: "/var/lib/fortify/u0", Ensure: true, Execute: true},

View File

@ -4,125 +4,149 @@ import (
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
"maps"
"path" "path"
"slices"
"syscall"
"git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/helper/bwrap" "git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
) )
// SandboxConfig describes resources made available to the sandbox. // SandboxConfig describes resources made available to the sandbox.
type SandboxConfig struct { type (
// unix hostname within sandbox SandboxConfig struct {
// container hostname
Hostname string `json:"hostname,omitempty"` Hostname string `json:"hostname,omitempty"`
// allow userns within sandbox
UserNS bool `json:"userns,omitempty"` // extra seccomp flags
// share net namespace Seccomp seccomp.SyscallOpts `json:"seccomp"`
// allow ptrace and friends
Devel bool `json:"devel,omitempty"`
// allow userns creation in container
Userns bool `json:"userns,omitempty"`
// share host net namespace
Net bool `json:"net,omitempty"` Net bool `json:"net,omitempty"`
// share all devices // expose main process tty
Dev bool `json:"dev,omitempty"` Tty bool `json:"tty,omitempty"`
// seccomp syscall filter policy // allow multiarch
Syscall *bwrap.SyscallPolicy `json:"syscall"` Multiarch bool `json:"multiarch,omitempty"`
// do not run in new session
NoNewSession bool `json:"no_new_session,omitempty"` // initial process environment variables
Env map[string]string `json:"env"`
// map target user uid to privileged user uid in the user namespace // map target user uid to privileged user uid in the user namespace
MapRealUID bool `json:"map_real_uid"` MapRealUID bool `json:"map_real_uid"`
// expose all devices
Dev bool `json:"dev,omitempty"`
// container host filesystem bind mounts
Filesystem []*FilesystemConfig `json:"filesystem"`
// create symlinks inside container filesystem
Link [][2]string `json:"symlink"`
// direct access to wayland socket; when this gets set no attempt is made to attach security-context-v1 // direct access to wayland socket; when this gets set no attempt is made to attach security-context-v1
// and the bare socket is mounted to the sandbox // and the bare socket is mounted to the sandbox
DirectWayland bool `json:"direct_wayland,omitempty"` DirectWayland bool `json:"direct_wayland,omitempty"`
// final environment variables
Env map[string]string `json:"env"`
// sandbox host filesystem access
Filesystem []*FilesystemConfig `json:"filesystem"`
// symlinks created inside the sandbox
Link [][2]string `json:"symlink"`
// read-only /etc directory // read-only /etc directory
Etc string `json:"etc,omitempty"` Etc string `json:"etc,omitempty"`
// automatically set up /etc symlinks // automatically set up /etc symlinks
AutoEtc bool `json:"auto_etc"` AutoEtc bool `json:"auto_etc"`
// mount tmpfs over these paths, // cover these paths or create them if they do not already exist
// runs right before [ConfinementConfig.ExtraPerms] Cover []string `json:"cover"`
Override []string `json:"override"` }
}
// SandboxSys encapsulates system functions used during the creation of [bwrap.Config]. // SandboxSys encapsulates system functions used during [sandbox.Container] initialisation.
type SandboxSys interface { SandboxSys interface {
Geteuid() int Getuid() int
Getgid() int
Paths() Paths Paths() Paths
ReadDir(name string) ([]fs.DirEntry, error) ReadDir(name string) ([]fs.DirEntry, error)
EvalSymlinks(path string) (string, error) EvalSymlinks(path string) (string, error)
Println(v ...any) Println(v ...any)
Printf(format string, v ...any) Printf(format string, v ...any)
} }
// Bwrap returns the address of the corresponding bwrap.Config to s. // FilesystemConfig is a representation of [sandbox.BindMount].
// Note that remaining tmpfs entries must be queued by the caller prior to launch. FilesystemConfig struct {
func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) { // mount point in container, same as src if empty
Dst string `json:"dst,omitempty"`
// host filesystem path to make available to the container
Src string `json:"src"`
// do not mount filesystem read-only
Write bool `json:"write,omitempty"`
// do not disable device files
Device bool `json:"dev,omitempty"`
// fail if the bind mount cannot be established for any reason
Must bool `json:"require,omitempty"`
}
)
// ToContainer initialises [sandbox.Params] via [SandboxConfig].
// Note that remaining container setup must be queued by the [App] implementation.
func (s *SandboxConfig) ToContainer(sys SandboxSys, uid, gid *int) (*sandbox.Params, map[string]string, error) {
if s == nil { if s == nil {
return nil, errors.New("nil sandbox config") return nil, nil, syscall.EBADE
} }
if s.Syscall == nil { container := &sandbox.Params{
sys.Println("syscall filter not configured, PROCEED WITH CAUTION")
}
if !s.MapRealUID {
// mapped uid defaults to 65534 to work around file ownership checks due to a bwrap limitation
*uid = 65534
} else {
// some programs fail to connect to dbus session running as a different uid, so a separate workaround
// is introduced to map priv-side caller uid in namespace
*uid = sys.Geteuid()
}
conf := (&bwrap.Config{
Net: s.Net,
UserNS: s.UserNS,
UID: uid,
GID: uid,
Hostname: s.Hostname, Hostname: s.Hostname,
Clearenv: true, Ops: new(sandbox.Ops),
SetEnv: s.Env, Seccomp: s.Seccomp,
}
/* 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 */
Filesystem: make([]bwrap.FSBuilder, 0, 256), *container.Ops = slices.Grow(*container.Ops, 1<<8)
Syscall: s.Syscall, if s.Devel {
NewSession: !s.NoNewSession, container.Flags |= sandbox.FAllowDevel
DieWithParent: true, }
AsInit: true, if s.Userns {
container.Flags |= sandbox.FAllowUserns
}
if s.Net {
container.Flags |= sandbox.FAllowNet
}
if s.Tty {
container.Flags |= sandbox.FAllowTTY
}
// initialise unconditionally as Once cannot be justified if s.MapRealUID {
// for saving such a miniscule amount of memory /* some programs fail to connect to dbus session running as a different uid
Chmod: make(bwrap.ChmodConfig), so this workaround is introduced to map priv-side caller uid in container */
}). container.Uid = sys.Getuid()
Procfs("/proc"). *uid = container.Uid
Tmpfs(Tmp, 4*1024) container.Gid = sys.Getgid()
*gid = container.Gid
} else {
*uid = sandbox.OverflowUid()
*gid = sandbox.OverflowGid()
}
container.
Proc("/proc").
Tmpfs(Tmp, 1<<12, 0755)
if !s.Dev { if !s.Dev {
conf.DevTmpfs("/dev").Mqueue("/dev/mqueue") container.Dev("/dev").Mqueue("/dev/mqueue")
} else { } else {
conf.Bind("/dev", "/dev", false, true, true) container.Bind("/dev", "/dev", sandbox.BindDevice)
} }
if !s.AutoEtc { /* retrieve paths and hide them if they're made available in the sandbox;
if s.Etc == "" { this feature tries to improve user experience of permissive defaults, and
conf.Dir("/etc") to warn about issues in custom configuration; it is NOT a security feature
} else { and should not be treated as such, ALWAYS be careful with what you bind */
conf.Bind(s.Etc, "/etc")
}
}
// retrieve paths and hide them if they're made available in the sandbox
var hidePaths []string var hidePaths []string
sc := sys.Paths() sc := sys.Paths()
hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath) hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath)
_, systemBusAddr := dbus.Address() _, systemBusAddr := dbus.Address()
if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil { if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
return nil, err return nil, nil, err
} else { } else {
// there is usually only one, do not preallocate // there is usually only one, do not preallocate
for _, entry := range entries { for _, entry := range entries {
@ -148,7 +172,7 @@ func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) {
hidePathMatch := make([]bool, len(hidePaths)) hidePathMatch := make([]bool, len(hidePaths))
for i := range hidePaths { for i := range hidePaths {
if err := evalSymlinks(sys, &hidePaths[i]); err != nil { if err := evalSymlinks(sys, &hidePaths[i]); err != nil {
return nil, err return nil, nil, err
} }
} }
@ -158,19 +182,19 @@ func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) {
} }
if !path.IsAbs(c.Src) { if !path.IsAbs(c.Src) {
return nil, fmt.Errorf("src path %q is not absolute", c.Src) return nil, nil, fmt.Errorf("src path %q is not absolute", c.Src)
} }
dest := c.Dst dest := c.Dst
if c.Dst == "" { if c.Dst == "" {
dest = c.Src dest = c.Src
} else if !path.IsAbs(dest) { } else if !path.IsAbs(dest) {
return nil, fmt.Errorf("dst path %q is not absolute", dest) return nil, nil, fmt.Errorf("dst path %q is not absolute", dest)
} }
srcH := c.Src srcH := c.Src
if err := evalSymlinks(sys, &srcH); err != nil { if err := evalSymlinks(sys, &srcH); err != nil {
return nil, err return nil, nil, err
} }
for i := range hidePaths { for i := range hidePaths {
@ -180,54 +204,71 @@ func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) {
} }
if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil { if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil {
return nil, err return nil, nil, err
} else if ok { } else if ok {
hidePathMatch[i] = true hidePathMatch[i] = true
sys.Printf("hiding paths from %q", c.Src) sys.Printf("hiding paths from %q", c.Src)
} }
} }
conf.Bind(c.Src, dest, !c.Must, c.Write, c.Device) var flags int
if c.Write {
flags |= sandbox.BindWritable
}
if c.Device {
flags |= sandbox.BindDevice | sandbox.BindWritable
}
if !c.Must {
flags |= sandbox.BindOptional
}
container.Bind(c.Src, dest, flags)
} }
// hide marked paths before setting up shares // cover matched paths
for i, ok := range hidePathMatch { for i, ok := range hidePathMatch {
if ok { if ok {
conf.Tmpfs(hidePaths[i], 8192) container.Tmpfs(hidePaths[i], 1<<13, 0755)
} }
} }
for _, l := range s.Link { for _, l := range s.Link {
conf.Symlink(l[0], l[1]) container.Link(l[0], l[1])
} }
if s.AutoEtc { // perf: this might work better if implemented as a setup op in container init
etc := s.Etc if !s.AutoEtc {
if etc == "" { if s.Etc != "" {
etc = "/etc" container.Bind(s.Etc, "/etc", 0)
} }
conf.Bind(etc, Tmp+"/etc") } else {
etcPath := s.Etc
if etcPath == "" {
etcPath = "/etc"
}
container.
Bind(etcPath, Tmp+"/etc", 0).
Mkdir("/etc", 0700)
// link host /etc contents to prevent passwd/group from being overwritten // link host /etc contents to prevent dropping passwd/group bind mounts
if d, err := sys.ReadDir(etc); err != nil { if d, err := sys.ReadDir(etcPath); err != nil {
return nil, err return nil, nil, err
} else { } else {
for _, ent := range d { for _, ent := range d {
name := ent.Name() n := ent.Name()
switch name { switch n {
case "passwd": case "passwd":
case "group": case "group":
case "mtab": case "mtab":
conf.Symlink("/proc/mounts", "/etc/"+name) container.Link("/proc/mounts", "/etc/"+n)
default: default:
conf.Symlink(Tmp+"/etc/"+name, "/etc/"+name) container.Link(Tmp+"/etc/"+n, "/etc/"+n)
} }
} }
} }
} }
return conf, nil return container, maps.Clone(s.Env), nil
} }
func evalSymlinks(sys SandboxSys, v *string) error { func evalSymlinks(sys SandboxSys, v *string) error {

2
go.mod
View File

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

View File

@ -5,6 +5,7 @@ import (
"errors" "errors"
"io" "io"
"os" "os"
"os/exec"
"slices" "slices"
"sync" "sync"
@ -61,7 +62,7 @@ func (h *helperContainer) Start() error {
h.Env = append(h.Env, FortifyStatus+"=1") h.Env = append(h.Env, FortifyStatus+"=1")
// stat is populated on fulfill // stat is populated on fulfill
h.Cancel = func() error { return h.stat.Close() } h.Cancel = func(*exec.Cmd) error { return h.stat.Close() }
} else { } else {
h.Env = append(h.Env, FortifyStatus+"=0") h.Env = append(h.Env, FortifyStatus+"=0")
} }

View File

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

View File

@ -4,7 +4,7 @@ import (
"git.gensokyo.uk/security/fortify/acl" "git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap" "git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system" "git.gensokyo.uk/security/fortify/system"
) )
@ -13,19 +13,19 @@ var testCasesNixos = []sealTestCase{
"nixos chromium direct wayland", new(stubNixOS), "nixos chromium direct wayland", new(stubNixOS),
&fst.Config{ &fst.Config{
ID: "org.chromium.Chromium", ID: "org.chromium.Chromium",
Command: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"}, Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
Confinement: fst.ConfinementConfig{ Confinement: fst.ConfinementConfig{
AppID: 1, Groups: []string{}, Username: "u0_a1", AppID: 1, Groups: []string{}, Username: "u0_a1",
Outer: "/var/lib/persist/module/fortify/0/1", Outer: "/var/lib/persist/module/fortify/0/1",
Sandbox: &fst.SandboxConfig{ Sandbox: &fst.SandboxConfig{
UserNS: true, Net: true, MapRealUID: true, DirectWayland: true, Env: nil, AutoEtc: true, Userns: true, Net: true, MapRealUID: true, DirectWayland: true, Env: nil, AutoEtc: true,
Filesystem: []*fst.FilesystemConfig{ Filesystem: []*fst.FilesystemConfig{
{Src: "/bin", Must: true}, {Src: "/usr/bin", Must: true}, {Src: "/bin", Must: true}, {Src: "/usr/bin", Must: true},
{Src: "/nix/store", Must: true}, {Src: "/run/current-system", Must: true}, {Src: "/nix/store", Must: true}, {Src: "/run/current-system", Must: true},
{Src: "/sys/block"}, {Src: "/sys/bus"}, {Src: "/sys/class"}, {Src: "/sys/dev"}, {Src: "/sys/devices"}, {Src: "/sys/block"}, {Src: "/sys/bus"}, {Src: "/sys/class"}, {Src: "/sys/dev"}, {Src: "/sys/devices"},
{Src: "/run/opengl-driver", Must: true}, {Src: "/dev/dri", Device: true}, {Src: "/run/opengl-driver", Must: true}, {Src: "/dev/dri", Device: true},
}, },
Override: []string{"/var/run/nscd"}, Cover: []string{"/var/run/nscd"},
}, },
SystemBus: &dbus.Config{ SystemBus: &dbus.Config{
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"}, Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
@ -88,136 +88,133 @@ var testCasesNixos = []sealTestCase{
}). }).
UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write). UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write).
UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write), UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write),
(&bwrap.Config{ &sandbox.Params{
Net: true, Uid: 1971,
UserNS: true, Gid: 100,
Chdir: "/var/lib/persist/module/fortify/0/1", Flags: sandbox.FAllowNet | sandbox.FAllowUserns,
Clearenv: true, Dir: "/var/lib/persist/module/fortify/0/1",
SetEnv: map[string]string{ Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/1971/bus", Args: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
"DBUS_SYSTEM_BUS_ADDRESS": "unix:path=/run/dbus/system_bus_socket", Env: []string{
"HOME": "/var/lib/persist/module/fortify/0/1", "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus",
"PULSE_COOKIE": fst.Tmp + "/pulse-cookie", "DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
"PULSE_SERVER": "unix:/run/user/1971/pulse/native", "HOME=/var/lib/persist/module/fortify/0/1",
"SHELL": "/run/current-system/sw/bin/zsh", "PULSE_COOKIE=" + fst.Tmp + "/pulse-cookie",
"TERM": "xterm-256color", "PULSE_SERVER=unix:/run/user/1971/pulse/native",
"USER": "u0_a1", "TERM=xterm-256color",
"WAYLAND_DISPLAY": "wayland-0", "USER=u0_a1",
"XDG_RUNTIME_DIR": "/run/user/1971", "WAYLAND_DISPLAY=wayland-0",
"XDG_SESSION_CLASS": "user", "XDG_RUNTIME_DIR=/run/user/1971",
"XDG_SESSION_TYPE": "tty", "XDG_SESSION_CLASS=user",
"XDG_SESSION_TYPE=tty",
},
Ops: new(sandbox.Ops).
Proc("/proc").
Tmpfs(fst.Tmp, 4096, 0755).
Dev("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", 0).
Bind("/usr/bin", "/usr/bin", 0).
Bind("/nix/store", "/nix/store", 0).
Bind("/run/current-system", "/run/current-system", 0).
Bind("/sys/block", "/sys/block", sandbox.BindOptional).
Bind("/sys/bus", "/sys/bus", sandbox.BindOptional).
Bind("/sys/class", "/sys/class", sandbox.BindOptional).
Bind("/sys/dev", "/sys/dev", sandbox.BindOptional).
Bind("/sys/devices", "/sys/devices", sandbox.BindOptional).
Bind("/run/opengl-driver", "/run/opengl-driver", 0).
Bind("/dev/dri", "/dev/dri", sandbox.BindDevice|sandbox.BindWritable|sandbox.BindOptional).
Bind("/etc", fst.Tmp+"/etc", 0).
Mkdir("/etc", 0700).
Link(fst.Tmp+"/etc/alsa", "/etc/alsa").
Link(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
Link(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
Link(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
Link(fst.Tmp+"/etc/default", "/etc/default").
Link(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
Link(fst.Tmp+"/etc/fonts", "/etc/fonts").
Link(fst.Tmp+"/etc/fstab", "/etc/fstab").
Link(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
Link(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
Link(fst.Tmp+"/etc/hostid", "/etc/hostid").
Link(fst.Tmp+"/etc/hostname", "/etc/hostname").
Link(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Link(fst.Tmp+"/etc/hosts", "/etc/hosts").
Link(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
Link(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
Link(fst.Tmp+"/etc/issue", "/etc/issue").
Link(fst.Tmp+"/etc/kbd", "/etc/kbd").
Link(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
Link(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
Link(fst.Tmp+"/etc/localtime", "/etc/localtime").
Link(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
Link(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
Link(fst.Tmp+"/etc/lvm", "/etc/lvm").
Link(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
Link(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
Link(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
Link(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
Link("/proc/mounts", "/etc/mtab").
Link(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
Link(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
Link(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
Link(fst.Tmp+"/etc/nix", "/etc/nix").
Link(fst.Tmp+"/etc/nixos", "/etc/nixos").
Link(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
Link(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
Link(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
Link(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
Link(fst.Tmp+"/etc/os-release", "/etc/os-release").
Link(fst.Tmp+"/etc/pam", "/etc/pam").
Link(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
Link(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
Link(fst.Tmp+"/etc/pki", "/etc/pki").
Link(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
Link(fst.Tmp+"/etc/profile", "/etc/profile").
Link(fst.Tmp+"/etc/protocols", "/etc/protocols").
Link(fst.Tmp+"/etc/qemu", "/etc/qemu").
Link(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
Link(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
Link(fst.Tmp+"/etc/rpc", "/etc/rpc").
Link(fst.Tmp+"/etc/samba", "/etc/samba").
Link(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
Link(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
Link(fst.Tmp+"/etc/services", "/etc/services").
Link(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
Link(fst.Tmp+"/etc/shadow", "/etc/shadow").
Link(fst.Tmp+"/etc/shells", "/etc/shells").
Link(fst.Tmp+"/etc/ssh", "/etc/ssh").
Link(fst.Tmp+"/etc/ssl", "/etc/ssl").
Link(fst.Tmp+"/etc/static", "/etc/static").
Link(fst.Tmp+"/etc/subgid", "/etc/subgid").
Link(fst.Tmp+"/etc/subuid", "/etc/subuid").
Link(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
Link(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
Link(fst.Tmp+"/etc/systemd", "/etc/systemd").
Link(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
Link(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
Link(fst.Tmp+"/etc/udev", "/etc/udev").
Link(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
Link(fst.Tmp+"/etc/UPower", "/etc/UPower").
Link(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
Link(fst.Tmp+"/etc/X11", "/etc/X11").
Link(fst.Tmp+"/etc/zfs", "/etc/zfs").
Link(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
Link(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
Link(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Tmpfs("/run/user", 4096, 0755).
Tmpfs("/run/user/1971", 8388608, 0755).
Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", sandbox.BindWritable).
Bind("/var/lib/persist/module/fortify/0/1", "/var/lib/persist/module/fortify/0/1", sandbox.BindWritable).
Place("/etc/passwd", []byte("u0_a1:x:1971:100:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n")).
Place("/etc/group", []byte("fortify:x:100:\n")).
Bind("/run/user/1971/wayland-0", "/run/user/1971/wayland-0", 0).
Bind("/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native", 0).
Place(fst.Tmp+"/pulse-cookie", nil).
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus", 0).
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket", 0).
Tmpfs("/var/run/nscd", 8192, 0755),
}, },
Chmod: make(bwrap.ChmodConfig),
NewSession: true,
DieWithParent: true,
AsInit: true,
}).SetUID(1971).SetGID(1971).
Procfs("/proc").
Tmpfs(fst.Tmp, 4096).
DevTmpfs("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin").
Bind("/usr/bin", "/usr/bin").
Bind("/nix/store", "/nix/store").
Bind("/run/current-system", "/run/current-system").
Bind("/sys/block", "/sys/block", true).
Bind("/sys/bus", "/sys/bus", true).
Bind("/sys/class", "/sys/class", true).
Bind("/sys/dev", "/sys/dev", true).
Bind("/sys/devices", "/sys/devices", true).
Bind("/run/opengl-driver", "/run/opengl-driver").
Bind("/dev/dri", "/dev/dri", true, true, true).
Bind("/etc", fst.Tmp+"/etc").
Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
Symlink(fst.Tmp+"/etc/default", "/etc/default").
Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/proc/mounts", "/etc/mtab").
Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
Symlink(fst.Tmp+"/etc/services", "/etc/services").
Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
Symlink(fst.Tmp+"/etc/static", "/etc/static").
Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/1971", 8388608).
Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", false, true).
Bind("/var/lib/persist/module/fortify/0/1", "/var/lib/persist/module/fortify/0/1", false, true).
CopyBind("/etc/passwd", []byte("u0_a1:x:1971:1971:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n")).
CopyBind("/etc/group", []byte("fortify:x:1971:\n")).
Bind("/run/user/1971/wayland-0", "/run/user/1971/wayland-0").
Bind("/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native").
CopyBind(fst.Tmp+"/pulse-cookie", nil).
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus").
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket").
Tmpfs("/var/run/nscd", 8192).
Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
Symlink("fortify", "/.fortify/sbin/init0"),
}, },
} }

View File

@ -6,7 +6,7 @@ import (
"git.gensokyo.uk/security/fortify/acl" "git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap" "git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system" "git.gensokyo.uk/security/fortify/system"
) )
@ -14,7 +14,6 @@ var testCasesPd = []sealTestCase{
{ {
"nixos permissive defaults no enablements", new(stubNixOS), "nixos permissive defaults no enablements", new(stubNixOS),
&fst.Config{ &fst.Config{
Command: make([]string, 0),
Confinement: fst.ConfinementConfig{ Confinement: fst.ConfinementConfig{
AppID: 0, AppID: 0,
Username: "chronos", Username: "chronos",
@ -35,136 +34,132 @@ var testCasesPd = []sealTestCase{
Ephemeral(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", acl.Execute). 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),
(&bwrap.Config{ &sandbox.Params{
Net: true, Flags: sandbox.FAllowNet | sandbox.FAllowUserns | sandbox.FAllowTTY,
UserNS: true, Dir: "/home/chronos",
Clearenv: true, Path: "/run/current-system/sw/bin/zsh",
Syscall: new(bwrap.SyscallPolicy), Args: []string{"/run/current-system/sw/bin/zsh"},
Chdir: "/home/chronos", Env: []string{
SetEnv: map[string]string{ "HOME=/home/chronos",
"HOME": "/home/chronos", "TERM=xterm-256color",
"SHELL": "/run/current-system/sw/bin/zsh", "USER=chronos",
"TERM": "xterm-256color", "XDG_RUNTIME_DIR=/run/user/65534",
"USER": "chronos", "XDG_SESSION_CLASS=user",
"XDG_RUNTIME_DIR": "/run/user/65534", "XDG_SESSION_TYPE=tty",
"XDG_SESSION_CLASS": "user", },
"XDG_SESSION_TYPE": "tty"}, Ops: new(sandbox.Ops).
Chmod: make(bwrap.ChmodConfig), Proc("/proc").
DieWithParent: true, Tmpfs(fst.Tmp, 4096, 0755).
AsInit: true, Dev("/dev").Mqueue("/dev/mqueue").
}).SetUID(65534).SetGID(65534). Bind("/bin", "/bin", sandbox.BindWritable).
Procfs("/proc"). Bind("/boot", "/boot", sandbox.BindWritable).
Tmpfs(fst.Tmp, 4096). Bind("/home", "/home", sandbox.BindWritable).
DevTmpfs("/dev").Mqueue("/dev/mqueue"). Bind("/lib", "/lib", sandbox.BindWritable).
Bind("/bin", "/bin", false, true). Bind("/lib64", "/lib64", sandbox.BindWritable).
Bind("/boot", "/boot", false, true). Bind("/nix", "/nix", sandbox.BindWritable).
Bind("/home", "/home", false, true). Bind("/root", "/root", sandbox.BindWritable).
Bind("/lib", "/lib", false, true). Bind("/run", "/run", sandbox.BindWritable).
Bind("/lib64", "/lib64", false, true). Bind("/srv", "/srv", sandbox.BindWritable).
Bind("/nix", "/nix", false, true). Bind("/sys", "/sys", sandbox.BindWritable).
Bind("/root", "/root", false, true). Bind("/usr", "/usr", sandbox.BindWritable).
Bind("/run", "/run", false, true). Bind("/var", "/var", sandbox.BindWritable).
Bind("/srv", "/srv", false, true). Bind("/dev/kvm", "/dev/kvm", sandbox.BindWritable|sandbox.BindDevice|sandbox.BindOptional).
Bind("/sys", "/sys", false, true). Tmpfs("/run/user/1971", 8192, 0755).
Bind("/usr", "/usr", false, true). Tmpfs("/run/dbus", 8192, 0755).
Bind("/var", "/var", false, true). Bind("/etc", fst.Tmp+"/etc", 0).
Bind("/dev/kvm", "/dev/kvm", true, true, true). Mkdir("/etc", 0700).
Tmpfs("/run/user/1971", 8192). Link(fst.Tmp+"/etc/alsa", "/etc/alsa").
Tmpfs("/run/dbus", 8192). Link(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
Bind("/etc", fst.Tmp+"/etc"). Link(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa"). Link(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc"). Link(fst.Tmp+"/etc/default", "/etc/default").
Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d"). Link(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1"). Link(fst.Tmp+"/etc/fonts", "/etc/fonts").
Symlink(fst.Tmp+"/etc/default", "/etc/default"). Link(fst.Tmp+"/etc/fstab", "/etc/fstab").
Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes"). Link(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts"). Link(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab"). Link(fst.Tmp+"/etc/hostid", "/etc/hostid").
Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf"). Link(fst.Tmp+"/etc/hostname", "/etc/hostname").
Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf"). Link(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid"). Link(fst.Tmp+"/etc/hosts", "/etc/hosts").
Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname"). Link(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM"). Link(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts"). Link(fst.Tmp+"/etc/issue", "/etc/issue").
Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc"). Link(fst.Tmp+"/etc/kbd", "/etc/kbd").
Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d"). Link(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
Symlink(fst.Tmp+"/etc/issue", "/etc/issue"). Link(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd"). Link(fst.Tmp+"/etc/localtime", "/etc/localtime").
Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev"). Link(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf"). Link(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime"). Link(fst.Tmp+"/etc/lvm", "/etc/lvm").
Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs"). Link(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release"). Link(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm"). Link(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id"). Link(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf"). Link("/proc/mounts", "/etc/mtab").
Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d"). Link(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d"). Link(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
Symlink("/proc/mounts", "/etc/mtab"). Link(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc"). Link(fst.Tmp+"/etc/nix", "/etc/nix").
Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup"). Link(fst.Tmp+"/etc/nixos", "/etc/nixos").
Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager"). Link(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
Symlink(fst.Tmp+"/etc/nix", "/etc/nix"). Link(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos"). Link(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS"). Link(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf"). Link(fst.Tmp+"/etc/os-release", "/etc/os-release").
Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf"). Link(fst.Tmp+"/etc/pam", "/etc/pam").
Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd"). Link(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release"). Link(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
Symlink(fst.Tmp+"/etc/pam", "/etc/pam"). Link(fst.Tmp+"/etc/pki", "/etc/pki").
Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d"). Link(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire"). Link(fst.Tmp+"/etc/profile", "/etc/profile").
Symlink(fst.Tmp+"/etc/pki", "/etc/pki"). Link(fst.Tmp+"/etc/protocols", "/etc/protocols").
Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1"). Link(fst.Tmp+"/etc/qemu", "/etc/qemu").
Symlink(fst.Tmp+"/etc/profile", "/etc/profile"). Link(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols"). Link(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu"). Link(fst.Tmp+"/etc/rpc", "/etc/rpc").
Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf"). Link(fst.Tmp+"/etc/samba", "/etc/samba").
Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf"). Link(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc"). Link(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
Symlink(fst.Tmp+"/etc/samba", "/etc/samba"). Link(fst.Tmp+"/etc/services", "/etc/services").
Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf"). Link(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot"). Link(fst.Tmp+"/etc/shadow", "/etc/shadow").
Symlink(fst.Tmp+"/etc/services", "/etc/services"). Link(fst.Tmp+"/etc/shells", "/etc/shells").
Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment"). Link(fst.Tmp+"/etc/ssh", "/etc/ssh").
Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow"). Link(fst.Tmp+"/etc/ssl", "/etc/ssl").
Symlink(fst.Tmp+"/etc/shells", "/etc/shells"). Link(fst.Tmp+"/etc/static", "/etc/static").
Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh"). Link(fst.Tmp+"/etc/subgid", "/etc/subgid").
Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl"). Link(fst.Tmp+"/etc/subuid", "/etc/subuid").
Symlink(fst.Tmp+"/etc/static", "/etc/static"). Link(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid"). Link(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid"). Link(fst.Tmp+"/etc/systemd", "/etc/systemd").
Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers"). Link(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d"). Link(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd"). Link(fst.Tmp+"/etc/udev", "/etc/udev").
Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo"). Link(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d"). Link(fst.Tmp+"/etc/UPower", "/etc/UPower").
Symlink(fst.Tmp+"/etc/udev", "/etc/udev"). Link(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2"). Link(fst.Tmp+"/etc/X11", "/etc/X11").
Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower"). Link(fst.Tmp+"/etc/zfs", "/etc/zfs").
Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf"). Link(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
Symlink(fst.Tmp+"/etc/X11", "/etc/X11"). Link(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs"). Link(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc"). Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo"). Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile"). Tmpfs("/run/user", 4096, 0755).
Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv"). Tmpfs("/run/user/65534", 8388608, 0755).
Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc"). Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", sandbox.BindWritable).
Tmpfs("/run/user", 1048576). Bind("/home/chronos", "/home/chronos", sandbox.BindWritable).
Tmpfs("/run/user/65534", 8388608). Place("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", false, true). Place("/etc/group", []byte("fortify:x:65534:\n")).
Bind("/home/chronos", "/home/chronos", false, true). Tmpfs("/var/run/nscd", 8192, 0755),
CopyBind("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")). },
CopyBind("/etc/group", []byte("fortify:x:65534:\n")).
Tmpfs("/var/run/nscd", 8192).
Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
Symlink("fortify", "/.fortify/sbin/init0"),
}, },
{ {
"nixos permissive defaults chromium", new(stubNixOS), "nixos permissive defaults chromium", new(stubNixOS),
&fst.Config{ &fst.Config{
ID: "org.chromium.Chromium", ID: "org.chromium.Chromium",
Command: []string{"/run/current-system/sw/bin/zsh", "-c", "exec chromium "}, Args: []string{"zsh", "-c", "exec chromium "},
Confinement: fst.ConfinementConfig{ Confinement: fst.ConfinementConfig{
AppID: 9, AppID: 9,
Groups: []string{"video"}, Groups: []string{"video"},
@ -254,141 +249,136 @@ var testCasesPd = []sealTestCase{
}). }).
UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write). UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write).
UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write), UpdatePerm("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write),
(&bwrap.Config{ &sandbox.Params{
Net: true, Flags: sandbox.FAllowNet | sandbox.FAllowUserns | sandbox.FAllowTTY,
UserNS: true, Dir: "/home/chronos",
Chdir: "/home/chronos", Path: "/run/current-system/sw/bin/zsh",
Clearenv: true, Args: []string{"zsh", "-c", "exec chromium "},
Syscall: new(bwrap.SyscallPolicy), Env: []string{
SetEnv: map[string]string{ "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus",
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/65534/bus", "DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
"DBUS_SYSTEM_BUS_ADDRESS": "unix:path=/run/dbus/system_bus_socket", "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", "TERM=xterm-256color",
"SHELL": "/run/current-system/sw/bin/zsh", "USER=chronos",
"TERM": "xterm-256color", "WAYLAND_DISPLAY=wayland-0",
"USER": "chronos", "XDG_RUNTIME_DIR=/run/user/65534",
"WAYLAND_DISPLAY": "wayland-0", "XDG_SESSION_CLASS=user",
"XDG_RUNTIME_DIR": "/run/user/65534", "XDG_SESSION_TYPE=tty",
"XDG_SESSION_CLASS": "user", },
"XDG_SESSION_TYPE": "tty", Ops: new(sandbox.Ops).
Proc("/proc").
Tmpfs(fst.Tmp, 4096, 0755).
Dev("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", sandbox.BindWritable).
Bind("/boot", "/boot", sandbox.BindWritable).
Bind("/home", "/home", sandbox.BindWritable).
Bind("/lib", "/lib", sandbox.BindWritable).
Bind("/lib64", "/lib64", sandbox.BindWritable).
Bind("/nix", "/nix", sandbox.BindWritable).
Bind("/root", "/root", sandbox.BindWritable).
Bind("/run", "/run", sandbox.BindWritable).
Bind("/srv", "/srv", sandbox.BindWritable).
Bind("/sys", "/sys", sandbox.BindWritable).
Bind("/usr", "/usr", sandbox.BindWritable).
Bind("/var", "/var", sandbox.BindWritable).
Bind("/dev/dri", "/dev/dri", sandbox.BindWritable|sandbox.BindDevice|sandbox.BindOptional).
Bind("/dev/kvm", "/dev/kvm", sandbox.BindWritable|sandbox.BindDevice|sandbox.BindOptional).
Tmpfs("/run/user/1971", 8192, 0755).
Tmpfs("/run/dbus", 8192, 0755).
Bind("/etc", fst.Tmp+"/etc", 0).
Mkdir("/etc", 0700).
Link(fst.Tmp+"/etc/alsa", "/etc/alsa").
Link(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
Link(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
Link(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
Link(fst.Tmp+"/etc/default", "/etc/default").
Link(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
Link(fst.Tmp+"/etc/fonts", "/etc/fonts").
Link(fst.Tmp+"/etc/fstab", "/etc/fstab").
Link(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
Link(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
Link(fst.Tmp+"/etc/hostid", "/etc/hostid").
Link(fst.Tmp+"/etc/hostname", "/etc/hostname").
Link(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Link(fst.Tmp+"/etc/hosts", "/etc/hosts").
Link(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
Link(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
Link(fst.Tmp+"/etc/issue", "/etc/issue").
Link(fst.Tmp+"/etc/kbd", "/etc/kbd").
Link(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
Link(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
Link(fst.Tmp+"/etc/localtime", "/etc/localtime").
Link(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
Link(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
Link(fst.Tmp+"/etc/lvm", "/etc/lvm").
Link(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
Link(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
Link(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
Link(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
Link("/proc/mounts", "/etc/mtab").
Link(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
Link(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
Link(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
Link(fst.Tmp+"/etc/nix", "/etc/nix").
Link(fst.Tmp+"/etc/nixos", "/etc/nixos").
Link(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
Link(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
Link(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
Link(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
Link(fst.Tmp+"/etc/os-release", "/etc/os-release").
Link(fst.Tmp+"/etc/pam", "/etc/pam").
Link(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
Link(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
Link(fst.Tmp+"/etc/pki", "/etc/pki").
Link(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
Link(fst.Tmp+"/etc/profile", "/etc/profile").
Link(fst.Tmp+"/etc/protocols", "/etc/protocols").
Link(fst.Tmp+"/etc/qemu", "/etc/qemu").
Link(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
Link(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
Link(fst.Tmp+"/etc/rpc", "/etc/rpc").
Link(fst.Tmp+"/etc/samba", "/etc/samba").
Link(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
Link(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
Link(fst.Tmp+"/etc/services", "/etc/services").
Link(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
Link(fst.Tmp+"/etc/shadow", "/etc/shadow").
Link(fst.Tmp+"/etc/shells", "/etc/shells").
Link(fst.Tmp+"/etc/ssh", "/etc/ssh").
Link(fst.Tmp+"/etc/ssl", "/etc/ssl").
Link(fst.Tmp+"/etc/static", "/etc/static").
Link(fst.Tmp+"/etc/subgid", "/etc/subgid").
Link(fst.Tmp+"/etc/subuid", "/etc/subuid").
Link(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
Link(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
Link(fst.Tmp+"/etc/systemd", "/etc/systemd").
Link(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
Link(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
Link(fst.Tmp+"/etc/udev", "/etc/udev").
Link(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
Link(fst.Tmp+"/etc/UPower", "/etc/UPower").
Link(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
Link(fst.Tmp+"/etc/X11", "/etc/X11").
Link(fst.Tmp+"/etc/zfs", "/etc/zfs").
Link(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
Link(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
Link(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
Link(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Link(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Tmpfs("/run/user", 4096, 0755).
Tmpfs("/run/user/65534", 8388608, 0755).
Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", sandbox.BindWritable).
Bind("/home/chronos", "/home/chronos", sandbox.BindWritable).
Place("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
Place("/etc/group", []byte("fortify:x:65534:\n")).
Bind("/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/65534/wayland-0", 0).
Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native", 0).
Place(fst.Tmp+"/pulse-cookie", nil).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus", 0).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket", 0).
Tmpfs("/var/run/nscd", 8192, 0755),
}, },
Chmod: make(bwrap.ChmodConfig),
DieWithParent: true,
AsInit: true,
}).SetUID(65534).SetGID(65534).
Procfs("/proc").
Tmpfs(fst.Tmp, 4096).
DevTmpfs("/dev").Mqueue("/dev/mqueue").
Bind("/bin", "/bin", false, true).
Bind("/boot", "/boot", false, true).
Bind("/home", "/home", false, true).
Bind("/lib", "/lib", false, true).
Bind("/lib64", "/lib64", false, true).
Bind("/nix", "/nix", false, true).
Bind("/root", "/root", false, true).
Bind("/run", "/run", false, true).
Bind("/srv", "/srv", false, true).
Bind("/sys", "/sys", false, true).
Bind("/usr", "/usr", false, true).
Bind("/var", "/var", false, true).
Bind("/dev/dri", "/dev/dri", true, true, true).
Bind("/dev/kvm", "/dev/kvm", true, true, true).
Tmpfs("/run/user/1971", 8192).
Tmpfs("/run/dbus", 8192).
Bind("/etc", fst.Tmp+"/etc").
Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa").
Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc").
Symlink(fst.Tmp+"/etc/binfmt.d", "/etc/binfmt.d").
Symlink(fst.Tmp+"/etc/dbus-1", "/etc/dbus-1").
Symlink(fst.Tmp+"/etc/default", "/etc/default").
Symlink(fst.Tmp+"/etc/ethertypes", "/etc/ethertypes").
Symlink(fst.Tmp+"/etc/fonts", "/etc/fonts").
Symlink(fst.Tmp+"/etc/fstab", "/etc/fstab").
Symlink(fst.Tmp+"/etc/fuse.conf", "/etc/fuse.conf").
Symlink(fst.Tmp+"/etc/host.conf", "/etc/host.conf").
Symlink(fst.Tmp+"/etc/hostid", "/etc/hostid").
Symlink(fst.Tmp+"/etc/hostname", "/etc/hostname").
Symlink(fst.Tmp+"/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM").
Symlink(fst.Tmp+"/etc/hosts", "/etc/hosts").
Symlink(fst.Tmp+"/etc/inputrc", "/etc/inputrc").
Symlink(fst.Tmp+"/etc/ipsec.d", "/etc/ipsec.d").
Symlink(fst.Tmp+"/etc/issue", "/etc/issue").
Symlink(fst.Tmp+"/etc/kbd", "/etc/kbd").
Symlink(fst.Tmp+"/etc/libblockdev", "/etc/libblockdev").
Symlink(fst.Tmp+"/etc/locale.conf", "/etc/locale.conf").
Symlink(fst.Tmp+"/etc/localtime", "/etc/localtime").
Symlink(fst.Tmp+"/etc/login.defs", "/etc/login.defs").
Symlink(fst.Tmp+"/etc/lsb-release", "/etc/lsb-release").
Symlink(fst.Tmp+"/etc/lvm", "/etc/lvm").
Symlink(fst.Tmp+"/etc/machine-id", "/etc/machine-id").
Symlink(fst.Tmp+"/etc/man_db.conf", "/etc/man_db.conf").
Symlink(fst.Tmp+"/etc/modprobe.d", "/etc/modprobe.d").
Symlink(fst.Tmp+"/etc/modules-load.d", "/etc/modules-load.d").
Symlink("/proc/mounts", "/etc/mtab").
Symlink(fst.Tmp+"/etc/nanorc", "/etc/nanorc").
Symlink(fst.Tmp+"/etc/netgroup", "/etc/netgroup").
Symlink(fst.Tmp+"/etc/NetworkManager", "/etc/NetworkManager").
Symlink(fst.Tmp+"/etc/nix", "/etc/nix").
Symlink(fst.Tmp+"/etc/nixos", "/etc/nixos").
Symlink(fst.Tmp+"/etc/NIXOS", "/etc/NIXOS").
Symlink(fst.Tmp+"/etc/nscd.conf", "/etc/nscd.conf").
Symlink(fst.Tmp+"/etc/nsswitch.conf", "/etc/nsswitch.conf").
Symlink(fst.Tmp+"/etc/opensnitchd", "/etc/opensnitchd").
Symlink(fst.Tmp+"/etc/os-release", "/etc/os-release").
Symlink(fst.Tmp+"/etc/pam", "/etc/pam").
Symlink(fst.Tmp+"/etc/pam.d", "/etc/pam.d").
Symlink(fst.Tmp+"/etc/pipewire", "/etc/pipewire").
Symlink(fst.Tmp+"/etc/pki", "/etc/pki").
Symlink(fst.Tmp+"/etc/polkit-1", "/etc/polkit-1").
Symlink(fst.Tmp+"/etc/profile", "/etc/profile").
Symlink(fst.Tmp+"/etc/protocols", "/etc/protocols").
Symlink(fst.Tmp+"/etc/qemu", "/etc/qemu").
Symlink(fst.Tmp+"/etc/resolv.conf", "/etc/resolv.conf").
Symlink(fst.Tmp+"/etc/resolvconf.conf", "/etc/resolvconf.conf").
Symlink(fst.Tmp+"/etc/rpc", "/etc/rpc").
Symlink(fst.Tmp+"/etc/samba", "/etc/samba").
Symlink(fst.Tmp+"/etc/sddm.conf", "/etc/sddm.conf").
Symlink(fst.Tmp+"/etc/secureboot", "/etc/secureboot").
Symlink(fst.Tmp+"/etc/services", "/etc/services").
Symlink(fst.Tmp+"/etc/set-environment", "/etc/set-environment").
Symlink(fst.Tmp+"/etc/shadow", "/etc/shadow").
Symlink(fst.Tmp+"/etc/shells", "/etc/shells").
Symlink(fst.Tmp+"/etc/ssh", "/etc/ssh").
Symlink(fst.Tmp+"/etc/ssl", "/etc/ssl").
Symlink(fst.Tmp+"/etc/static", "/etc/static").
Symlink(fst.Tmp+"/etc/subgid", "/etc/subgid").
Symlink(fst.Tmp+"/etc/subuid", "/etc/subuid").
Symlink(fst.Tmp+"/etc/sudoers", "/etc/sudoers").
Symlink(fst.Tmp+"/etc/sysctl.d", "/etc/sysctl.d").
Symlink(fst.Tmp+"/etc/systemd", "/etc/systemd").
Symlink(fst.Tmp+"/etc/terminfo", "/etc/terminfo").
Symlink(fst.Tmp+"/etc/tmpfiles.d", "/etc/tmpfiles.d").
Symlink(fst.Tmp+"/etc/udev", "/etc/udev").
Symlink(fst.Tmp+"/etc/udisks2", "/etc/udisks2").
Symlink(fst.Tmp+"/etc/UPower", "/etc/UPower").
Symlink(fst.Tmp+"/etc/vconsole.conf", "/etc/vconsole.conf").
Symlink(fst.Tmp+"/etc/X11", "/etc/X11").
Symlink(fst.Tmp+"/etc/zfs", "/etc/zfs").
Symlink(fst.Tmp+"/etc/zinputrc", "/etc/zinputrc").
Symlink(fst.Tmp+"/etc/zoneinfo", "/etc/zoneinfo").
Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/65534", 8388608).
Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", false, true).
Bind("/home/chronos", "/home/chronos", false, true).
CopyBind("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
CopyBind("/etc/group", []byte("fortify:x:65534:\n")).
Bind("/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/65534/wayland-0").
Bind("/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native").
CopyBind(fst.Tmp+"/pulse-cookie", nil).
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus").
Bind("/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket").
Tmpfs("/var/run/nscd", 8192).
Bind("/run/wrappers/bin/fortify", "/.fortify/sbin/fortify").
Symlink("fortify", "/.fortify/sbin/init0"),
}, },
} }

View File

@ -17,7 +17,8 @@ type stubNixOS struct {
usernameErr map[string]error usernameErr map[string]error
} }
func (s *stubNixOS) Geteuid() int { return 1971 } func (s *stubNixOS) Getuid() int { return 1971 }
func (s *stubNixOS) Getgid() int { return 100 }
func (s *stubNixOS) TempDir() string { return "/tmp" } func (s *stubNixOS) TempDir() string { return "/tmp" }
func (s *stubNixOS) MustExecutable() string { return "/run/wrappers/bin/fortify" } func (s *stubNixOS) MustExecutable() string { return "/run/wrappers/bin/fortify" }
func (s *stubNixOS) Exit(code int) { panic("called exit on stub with code " + strconv.Itoa(code)) } func (s *stubNixOS) Exit(code int) { panic("called exit on stub with code " + strconv.Itoa(code)) }
@ -54,10 +55,8 @@ func (s *stubNixOS) LookPath(file string) (string, error) {
} }
switch file { switch file {
case "sudo": case "zsh":
return "/run/wrappers/bin/sudo", nil return "/run/current-system/sw/bin/zsh", nil
case "machinectl":
return "/home/ophestra/.nix-profile/bin/machinectl", nil
default: default:
panic(fmt.Sprintf("attempted to look up unexpected executable %q", file)) panic(fmt.Sprintf("attempted to look up unexpected executable %q", file))
} }

View File

@ -8,9 +8,9 @@ import (
"time" "time"
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/app" "git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/sys" "git.gensokyo.uk/security/fortify/internal/sys"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system" "git.gensokyo.uk/security/fortify/system"
) )
@ -20,7 +20,7 @@ type sealTestCase struct {
config *fst.Config config *fst.Config
id fst.ID id fst.ID
wantSys *system.I wantSys *system.I
wantBwrap *bwrap.Config wantContainer *sandbox.Params
} }
func TestApp(t *testing.T) { func TestApp(t *testing.T) {
@ -31,14 +31,14 @@ func TestApp(t *testing.T) {
a := app.NewWithID(tc.id, tc.os) a := app.NewWithID(tc.id, tc.os)
var ( var (
gotSys *system.I gotSys *system.I
gotBwrap *bwrap.Config gotContainer *sandbox.Params
) )
if !t.Run("seal", func(t *testing.T) { if !t.Run("seal", func(t *testing.T) {
if sa, err := a.Seal(tc.config); err != nil { if sa, err := a.Seal(tc.config); err != nil {
t.Errorf("Seal: error = %v", err) t.Errorf("Seal: error = %v", err)
return return
} else { } else {
gotSys, gotBwrap = app.AppSystemBwrap(a, sa) gotSys, gotContainer = app.AppIParams(a, sa)
} }
}) { }) {
return return
@ -51,10 +51,10 @@ func TestApp(t *testing.T) {
} }
}) })
t.Run("compare bwrap", func(t *testing.T) { t.Run("compare params", func(t *testing.T) {
if !reflect.DeepEqual(gotBwrap, tc.wantBwrap) { if !reflect.DeepEqual(gotContainer, tc.wantContainer) {
t.Errorf("seal: bwrap =\n%s\n, want\n%s", t.Errorf("seal: params =\n%s\n, want\n%s",
mustMarshal(gotBwrap), mustMarshal(tc.wantBwrap)) mustMarshal(gotContainer), mustMarshal(tc.wantContainer))
} }
}) })
}) })

View File

@ -2,8 +2,8 @@ package app
import ( import (
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/sys" "git.gensokyo.uk/security/fortify/internal/sys"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system" "git.gensokyo.uk/security/fortify/system"
) )
@ -14,7 +14,7 @@ func NewWithID(id fst.ID, os sys.State) fst.App {
return a return a
} }
func AppSystemBwrap(a fst.App, sa fst.SealedApp) (*system.I, *bwrap.Config) { func AppIParams(a fst.App, sa fst.SealedApp) (*system.I, *sandbox.Params) {
v := a.(*app) v := a.(*app)
seal := sa.(*outcome) seal := sa.(*outcome)
if v.outcome != seal || v.id != seal.id { if v.outcome != seal || v.id != seal.id {

View File

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

View File

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

View File

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

View File

@ -3,15 +3,12 @@ package app
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"log" "log"
"os/exec" "os/exec"
"path/filepath"
"strings" "strings"
"time" "time"
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper"
"git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/app/shim" "git.gensokyo.uk/security/fortify/internal/app/shim"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
@ -21,7 +18,7 @@ import (
const shimSetupTimeout = 5 * time.Second const shimSetupTimeout = 5 * time.Second
func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error { func (seal *outcome) Run(rs *fst.RunState) error {
if !seal.f.CompareAndSwap(false, true) { if !seal.f.CompareAndSwap(false, true) {
// run does much more than just starting a process; calling it twice, even if the first call fails, will result // run does much more than just starting a process; calling it twice, even if the first call fails, will result
// in inconsistent state that is impossible to clean up; return here to limit damage and hopefully give the // in inconsistent state that is impossible to clean up; return here to limit damage and hopefully give the
@ -37,33 +34,11 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
fmsg.Verbosef("version %s", internal.Version()) fmsg.Verbosef("version %s", internal.Version())
fmsg.Verbosef("setuid helper at %s", internal.MustFsuPath()) fmsg.Verbosef("setuid helper at %s", internal.MustFsuPath())
/*
resolve exec paths
*/
shimExec := [2]string{helper.BubblewrapName}
if len(seal.command) > 0 {
shimExec[1] = seal.command[0]
}
for i, n := range shimExec {
if len(n) == 0 {
continue
}
if filepath.Base(n) == n {
if s, err := exec.LookPath(n); err == nil {
shimExec[i] = s
} else {
return fmsg.WrapError(err,
fmt.Sprintf("executable file %q not found in $PATH", n))
}
}
}
/* /*
prepare/revert os state prepare/revert os state
*/ */
if err := seal.sys.Commit(ctx); err != nil { if err := seal.sys.Commit(seal.ctx); err != nil {
return err return err
} }
store := state.NewMulti(seal.runDirPath) store := state.NewMulti(seal.runDirPath)
@ -137,7 +112,6 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
if startTime, err := cmd.Start( if startTime, err := cmd.Start(
seal.user.aid.String(), seal.user.aid.String(),
seal.user.supp, seal.user.supp,
seal.bwrapSync,
); err != nil { ); err != nil {
return err return err
} else { } else {
@ -145,7 +119,7 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
rs.Time = startTime rs.Time = startTime
} }
c, cancel := context.WithTimeout(ctx, shimSetupTimeout) ctx, cancel := context.WithTimeout(seal.ctx, shimSetupTimeout)
defer cancel() defer cancel()
go func() { go func() {
@ -154,10 +128,8 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
cancel() cancel()
}() }()
if err := cmd.Serve(c, &shim.Payload{ if err := cmd.Serve(ctx, &shim.Params{
Argv: seal.command, Container: seal.container,
Exec: shimExec,
Bwrap: seal.container,
Home: seal.user.data, Home: seal.user.data,
Verbose: fmsg.Load(), Verbose: fmsg.Load(),
@ -199,18 +171,22 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
// this is reached when a fault makes an already running shim impossible to continue execution // this is reached when a fault makes an already running shim impossible to continue execution
// however a kill signal could not be delivered (should actually always happen like that since fsu) // however a kill signal could not be delivered (should actually always happen like that since fsu)
// the effects of this is similar to the alternative exit path and ensures shim death // the effects of this is similar to the alternative exit path and ensures shim death
case err := <-cmd.WaitFallback(): case err := <-cmd.Fallback():
rs.ExitCode = 255 rs.ExitCode = 255
log.Printf("cannot terminate shim on faulted setup: %v", err) log.Printf("cannot terminate shim on faulted setup: %v", err)
// alternative exit path relying on shim behaviour on monitor process exit // alternative exit path relying on shim behaviour on monitor process exit
case <-ctx.Done(): case <-seal.ctx.Done():
fmsg.Verbose("alternative exit path selected") fmsg.Verbose("alternative exit path selected")
} }
fmsg.Resume() fmsg.Resume()
if seal.sync != nil {
if err := seal.sync.Close(); err != nil {
log.Printf("cannot close wayland security context: %v", err)
}
}
if seal.dbusMsg != nil { if seal.dbusMsg != nil {
// dump dbus message buffer
seal.dbusMsg() seal.dbusMsg()
} }

View File

@ -2,24 +2,28 @@ package app
import ( import (
"bytes" "bytes"
"context"
"encoding/gob" "encoding/gob"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
"maps"
"os" "os"
"path" "path"
"regexp" "regexp"
"slices"
"strings" "strings"
"sync/atomic" "sync/atomic"
"syscall"
"git.gensokyo.uk/security/fortify/acl" "git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/sys" "git.gensokyo.uk/security/fortify/internal/sys"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system" "git.gensokyo.uk/security/fortify/system"
"git.gensokyo.uk/security/fortify/wl" "git.gensokyo.uk/security/fortify/wl"
) )
@ -65,19 +69,19 @@ type outcome struct {
// copied from [sys.State] response // copied from [sys.State] response
runDirPath string runDirPath string
// passed through from [fst.Config]
command []string
// initial [fst.Config] gob stream for state data; // initial [fst.Config] gob stream for state data;
// this is prepared ahead of time as config is mutated during seal creation // this is prepared ahead of time as config is clobbered during seal creation
ct io.WriterTo ct io.WriterTo
// dump dbus proxy message buffer // dump dbus proxy message buffer
dbusMsg func() dbusMsg func()
user fsuUser user fsuUser
sys *system.I sys *system.I
container *bwrap.Config ctx context.Context
bwrapSync *os.File
container *sandbox.Params
env map[string]string
sync *os.File
f atomic.Bool f atomic.Bool
} }
@ -100,7 +104,17 @@ type fsuUser struct {
username string username string
} }
func (seal *outcome) finalise(sys sys.State, config *fst.Config) error { func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Config) error {
if seal.ctx != nil {
panic("finalise called twice")
}
seal.ctx = ctx
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)
@ -111,9 +125,6 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
seal.ct = ct seal.ct = ct
} }
// pass through command slice; this value is never touched in the main process
seal.command = config.Command
// allowed aid range 0 to 9999, this is checked again in fsu // allowed aid range 0 to 9999, this is checked again in fsu
if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 { if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 {
return fmsg.WrapError(ErrUser, return fmsg.WrapError(ErrUser,
@ -167,11 +178,23 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
if config.Confinement.Sandbox == nil { if config.Confinement.Sandbox == nil {
fmsg.Verbose("sandbox configuration not supplied, PROCEED WITH CAUTION") fmsg.Verbose("sandbox configuration not supplied, PROCEED WITH CAUTION")
// fsu clears the environment so resolve paths early
if !path.IsAbs(config.Path) {
if len(config.Args) > 0 {
if p, err := sys.LookPath(config.Args[0]); err != nil {
return fmsg.WrapError(err, err.Error())
} else {
config.Path = p
}
} else {
config.Path = shellPath
}
}
conf := &fst.SandboxConfig{ conf := &fst.SandboxConfig{
UserNS: true, Userns: true,
Net: true, Net: true,
Syscall: new(bwrap.SyscallPolicy), Tty: true,
NoNewSession: true,
AutoEtc: true, AutoEtc: true,
} }
// bind entries in / // bind entries in /
@ -198,7 +221,7 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
// hide nscd from sandbox if present // hide nscd from sandbox if present
nscd := "/var/run/nscd" nscd := "/var/run/nscd"
if _, err := sys.Stat(nscd); !errors.Is(err, fs.ErrNotExist) { if _, err := sys.Stat(nscd); !errors.Is(err, fs.ErrNotExist) {
conf.Override = append(conf.Override, nscd) conf.Cover = append(conf.Cover, nscd)
} }
// bind GPU stuff // bind GPU stuff
if config.Confinement.Enablements.Has(system.EX11) || config.Confinement.Enablements.Has(system.EWayland) { if config.Confinement.Enablements.Has(system.EX11) || config.Confinement.Enablements.Has(system.EWayland) {
@ -210,17 +233,29 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
config.Confinement.Sandbox = conf config.Confinement.Sandbox = conf
} }
var mapuid *stringPair[int] var mapuid, mapgid *stringPair[int]
{ {
var uid int var uid, gid int
var err error var err error
seal.container, err = config.Confinement.Sandbox.Bwrap(sys, &uid) seal.container, seal.env, err = config.Confinement.Sandbox.ToContainer(sys, &uid, &gid)
if err != nil { if err != nil {
return err return fmsg.WrapErrorSuffix(err,
"cannot initialise container configuration:")
} }
if !path.IsAbs(config.Path) {
return fmsg.WrapError(syscall.EINVAL,
"invalid program path")
}
if len(config.Args) == 0 {
config.Args = []string{config.Path}
}
seal.container.Path = config.Path
seal.container.Args = config.Args
mapuid = newInt(uid) mapuid = newInt(uid)
if seal.container.SetEnv == nil { mapgid = newInt(gid)
seal.container.SetEnv = make(map[string]string) if seal.env == nil {
seal.env = make(map[string]string)
} }
} }
@ -255,35 +290,27 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
// inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as post-fsu user // inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as post-fsu user
innerRuntimeDir := path.Join("/run/user", mapuid.String()) innerRuntimeDir := path.Join("/run/user", mapuid.String())
seal.container.Tmpfs("/run/user", 1*1024*1024) seal.container.Tmpfs("/run/user", 1<<12, 0755)
seal.container.Tmpfs(innerRuntimeDir, 8*1024*1024) seal.container.Tmpfs(innerRuntimeDir, 1<<23, 0755)
seal.container.SetEnv[xdgRuntimeDir] = innerRuntimeDir seal.env[xdgRuntimeDir] = innerRuntimeDir
seal.container.SetEnv[xdgSessionClass] = "user" seal.env[xdgSessionClass] = "user"
seal.container.SetEnv[xdgSessionType] = "tty" seal.env[xdgSessionType] = "tty"
// outer path for inner /tmp // outer path for inner /tmp
{ {
tmpdir := path.Join(sc.SharePath, "tmpdir") tmpdir := path.Join(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)
tmpdirProc := path.Join(tmpdir, seal.user.aid.String()) tmpdirInst := path.Join(tmpdir, seal.user.aid.String())
seal.sys.Ensure(tmpdirProc, 01700) seal.sys.Ensure(tmpdirInst, 01700)
seal.sys.UpdatePermType(system.User, tmpdirProc, acl.Read, acl.Write, acl.Execute) seal.sys.UpdatePermType(system.User, tmpdirInst, acl.Read, acl.Write, acl.Execute)
seal.container.Bind(tmpdirProc, "/tmp", false, true) seal.container.Bind(tmpdirInst, "/tmp", sandbox.BindWritable)
} }
/* /*
Passwd database Passwd database
*/ */
// look up shell
sh := "/bin/sh"
if s, ok := sys.LookupEnv(shell); ok {
seal.container.SetEnv[shell] = s
sh = s
}
// bind home directory
homeDir := "/var/empty" homeDir := "/var/empty"
if seal.user.home != "" { if seal.user.home != "" {
homeDir = seal.user.home homeDir = seal.user.home
@ -292,27 +319,25 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
if seal.user.username != "" { if seal.user.username != "" {
username = seal.user.username username = seal.user.username
} }
seal.container.Bind(seal.user.data, homeDir, false, true) seal.container.Bind(seal.user.data, homeDir, sandbox.BindWritable)
seal.container.Chdir = homeDir seal.container.Dir = homeDir
seal.container.SetEnv["HOME"] = homeDir seal.env["HOME"] = homeDir
seal.container.SetEnv["USER"] = username seal.env["USER"] = username
// generate /etc/passwd and /etc/group seal.container.Place("/etc/passwd",
seal.container.CopyBind("/etc/passwd", []byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Fortify:"+homeDir+":"+shellPath+"\n"))
[]byte(username+":x:"+mapuid.String()+":"+mapuid.String()+":Fortify:"+homeDir+":"+sh+"\n")) seal.container.Place("/etc/group",
seal.container.CopyBind("/etc/group", []byte("fortify:x:"+mapgid.String()+":\n"))
[]byte("fortify:x:"+mapuid.String()+":\n"))
/* /*
Display servers Display servers
*/ */
// pass $TERM to launcher // pass $TERM for proper terminal I/O in shell
if t, ok := sys.LookupEnv(term); ok { if t, ok := sys.LookupEnv(term); ok {
seal.container.SetEnv[term] = t seal.env[term] = t
} }
// set up wayland
if config.Confinement.Enablements.Has(system.EWayland) { if config.Confinement.Enablements.Has(system.EWayland) {
// outer wayland socket (usually `/run/user/%d/wayland-%d`) // outer wayland socket (usually `/run/user/%d/wayland-%d`)
var socketPath string var socketPath string
@ -326,7 +351,7 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
} }
innerPath := path.Join(innerRuntimeDir, wl.FallbackName) innerPath := path.Join(innerRuntimeDir, wl.FallbackName)
seal.container.SetEnv[wl.WaylandDisplay] = wl.FallbackName seal.env[wl.WaylandDisplay] = wl.FallbackName
if !config.Confinement.Sandbox.DirectWayland { // set up security-context-v1 if !config.Confinement.Sandbox.DirectWayland { // set up security-context-v1
socketDir := path.Join(sc.SharePath, "wayland") socketDir := path.Join(sc.SharePath, "wayland")
@ -337,25 +362,23 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
// 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()
} }
seal.sys.Wayland(&seal.bwrapSync, outerPath, socketPath, appID, seal.id.String()) seal.sys.Wayland(&seal.sync, outerPath, socketPath, appID, seal.id.String())
seal.container.Bind(outerPath, innerPath) 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")
seal.container.Bind(socketPath, innerPath) 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)
} }
} }
// set up X11
if config.Confinement.Enablements.Has(system.EX11) { if config.Confinement.Enablements.Has(system.EX11) {
// discover X11 and grant user permission via the `ChangeHosts` command
if d, ok := sys.LookupEnv(display); !ok { if d, ok := sys.LookupEnv(display); !ok {
return fmsg.WrapError(ErrXDisplay, return fmsg.WrapError(ErrXDisplay,
"DISPLAY is not set") "DISPLAY is not set")
} else { } else {
seal.sys.ChangeHosts("#" + seal.user.uid.String()) seal.sys.ChangeHosts("#" + seal.user.uid.String())
seal.container.SetEnv[display] = d seal.env[display] = d
seal.container.Bind("/tmp/.X11-unix", "/tmp/.X11-unix") seal.container.Bind("/tmp/.X11-unix", "/tmp/.X11-unix", 0)
} }
} }
@ -396,8 +419,8 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
innerPulseRuntimeDir := path.Join(sharePathLocal, "pulse") innerPulseRuntimeDir := path.Join(sharePathLocal, "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) seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0)
seal.container.SetEnv[pulseServer] = "unix:" + innerPulseSocket seal.env[pulseServer] = "unix:" + innerPulseSocket
// publish current user's pulse cookie for target user // publish current user's pulse cookie for target user
if src, err := discoverPulseCookie(sys); err != nil { if src, err := discoverPulseCookie(sys); err != nil {
@ -405,9 +428,9 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
fmsg.Verbose(strings.TrimSpace(err.(*fmsg.BaseError).Message())) fmsg.Verbose(strings.TrimSpace(err.(*fmsg.BaseError).Message()))
} else { } else {
innerDst := fst.Tmp + "/pulse-cookie" innerDst := fst.Tmp + "/pulse-cookie"
seal.container.SetEnv[pulseCookie] = innerDst seal.env[pulseCookie] = innerDst
payload := new([]byte) var payload *[]byte
seal.container.CopyBindRef(innerDst, &payload) seal.container.PlaceP(innerDst, &payload)
seal.sys.CopyFile(payload, src, 256, 256) seal.sys.CopyFile(payload, src, 256, 256)
} }
} }
@ -437,13 +460,13 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
// share proxy sockets // share proxy sockets
sessionInner := path.Join(innerRuntimeDir, "bus") sessionInner := path.Join(innerRuntimeDir, "bus")
seal.container.SetEnv[dbusSessionBusAddress] = "unix:path=" + sessionInner seal.env[dbusSessionBusAddress] = "unix:path=" + sessionInner
seal.container.Bind(sessionPath, sessionInner) seal.container.Bind(sessionPath, sessionInner, 0)
seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write) seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
if config.Confinement.SystemBus != nil { if config.Confinement.SystemBus != nil {
systemInner := "/run/dbus/system_bus_socket" systemInner := "/run/dbus/system_bus_socket"
seal.container.SetEnv[dbusSystemBusAddress] = "unix:path=" + systemInner seal.env[dbusSystemBusAddress] = "unix:path=" + systemInner
seal.container.Bind(systemPath, systemInner) seal.container.Bind(systemPath, systemInner, 0)
seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write) seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write)
} }
} }
@ -452,9 +475,8 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
Miscellaneous Miscellaneous
*/ */
// queue overriding tmpfs at the end of seal.container.Filesystem for _, dest := range config.Confinement.Sandbox.Cover {
for _, dest := range config.Confinement.Sandbox.Override { seal.container.Tmpfs(dest, 1<<13, 0755)
seal.container.Tmpfs(dest, 8*1024)
} }
// append ExtraPerms last // append ExtraPerms last
@ -480,12 +502,13 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
seal.sys.UpdatePermType(system.User, p.Path, perms...) seal.sys.UpdatePermType(system.User, p.Path, perms...)
} }
// mount fortify in sandbox for init // flatten and sort env for deterministic behaviour
seal.container.Bind(sys.MustExecutable(), path.Join(fst.Tmp, "sbin/fortify")) seal.container.Env = make([]string, 0, len(seal.env))
seal.container.Symlink("fortify", path.Join(fst.Tmp, "sbin/init0")) maps.All(seal.env)(func(k string, v string) bool { seal.container.Env = append(seal.container.Env, k+"="+v); return true })
slices.Sort(seal.container.Env)
fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, command: %s", fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s",
seal.user.uid, seal.user.username, config.Confinement.Groups, config.Command) seal.user.uid, seal.user.username, config.Confinement.Groups, seal.container.Args)
return nil return nil
} }

View File

@ -7,18 +7,26 @@ import (
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"path"
"strconv"
"syscall" "syscall"
"time"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper"
"git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/app/init0"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/sandbox" "git.gensokyo.uk/security/fortify/sandbox"
) )
const Env = "FORTIFY_SHIM"
type Params struct {
// finalised container params
Container *sandbox.Params
// path to outer home directory
Home string
// verbosity pass through
Verbose bool
}
// everything beyond this point runs as unconstrained target user // everything beyond this point runs as unconstrained target user
// proceed with caution! // proceed with caution!
@ -27,17 +35,15 @@ func Main() {
// USE WITH CAUTION // USE WITH CAUTION
fmsg.Prepare("shim") fmsg.Prepare("shim")
// setting this prevents ptrace
if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil { if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err) log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
} }
// receive setup payload
var ( var (
payload Payload params Params
closeSetup func() error closeSetup func() error
) )
if f, err := sandbox.Receive(Env, &payload, nil); err != nil { if f, err := sandbox.Receive(Env, &params, nil); err != nil {
if errors.Is(err, sandbox.ErrInvalid) { if errors.Is(err, sandbox.ErrInvalid) {
log.Fatal("invalid config descriptor") log.Fatal("invalid config descriptor")
} }
@ -45,32 +51,26 @@ func Main() {
log.Fatal("FORTIFY_SHIM not set") log.Fatal("FORTIFY_SHIM not set")
} }
log.Fatalf("cannot decode shim setup payload: %v", err) log.Fatalf("cannot receive shim setup params: %v", err)
} else { } else {
internal.InstallFmsg(payload.Verbose) internal.InstallFmsg(params.Verbose)
closeSetup = f closeSetup = f
} }
if payload.Bwrap == nil { if params.Container == nil || params.Container.Ops == nil {
log.Fatal("bwrap config not supplied") log.Fatal("invalid container params")
}
// restore bwrap sync fd
var syncFd *os.File
if payload.Sync != nil {
syncFd = os.NewFile(*payload.Sync, "sync")
} }
// close setup socket // close setup socket
if err := closeSetup(); err != nil { if err := closeSetup(); err != nil {
log.Println("cannot close setup pipe:", err) log.Printf("cannot close setup pipe: %v", err)
// not fatal // not fatal
} }
// ensure home directory as target user // ensure home directory as target user
if s, err := os.Stat(payload.Home); err != nil { if s, err := os.Stat(params.Home); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
if err = os.Mkdir(payload.Home, 0700); err != nil { if err = os.Mkdir(params.Home, 0700); err != nil {
log.Fatalf("cannot create home directory: %v", err) log.Fatalf("cannot create home directory: %v", err)
} }
} else { } else {
@ -79,72 +79,37 @@ func Main() {
// home directory is created, proceed // home directory is created, proceed
} else if !s.IsDir() { } else if !s.IsDir() {
log.Fatalf("data path %q is not a directory", payload.Home) log.Fatalf("path %q is not a directory", params.Home)
} }
var ic init0.Payload var name string
if len(params.Container.Args) > 0 {
// resolve argv0 name = params.Container.Args[0]
ic.Argv = payload.Argv
if len(ic.Argv) > 0 {
// looked up from $PATH by parent
ic.Argv0 = payload.Exec[1]
} else {
// no argv, look up shell instead
var ok bool
if payload.Bwrap.SetEnv == nil {
log.Fatal("no command was specified and environment is unset")
} }
if ic.Argv0, ok = payload.Bwrap.SetEnv["SHELL"]; !ok {
log.Fatal("no command was specified and $SHELL was unset")
}
ic.Argv = []string{ic.Argv0}
}
conf := payload.Bwrap
var extraFiles []*os.File
// serve setup payload
if fd, encoder, err := sandbox.Setup(&extraFiles); err != nil {
log.Fatalf("cannot pipe: %v", err)
} else {
conf.SetEnv[init0.Env] = strconv.Itoa(fd)
go func() {
fmsg.Verbose("transmitting config to init")
if err = encoder.Encode(&ic); err != nil {
log.Fatalf("cannot transmit init config: %v", err)
}
}()
}
helper.BubblewrapName = payload.Exec[0] // resolved bwrap path by parent
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop() // unreachable defer stop() // unreachable
if b, err := helper.NewBwrap( container := sandbox.New(ctx, name)
ctx, path.Join(fst.Tmp, "sbin/init0"), container.Params = *params.Container
nil, false, container.Stdin, container.Stdout, container.Stderr = os.Stdin, os.Stdout, os.Stderr
func(int, int) []string { return make([]string, 0) }, container.Cancel = func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) }
func(cmd *exec.Cmd) { cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr }, container.WaitDelay = 2 * time.Second
extraFiles,
conf, syncFd, if err := container.Start(); err != nil {
); err != nil { fmsg.PrintBaseError(err, "cannot start container:")
log.Fatalf("malformed sandbox config: %v", err) os.Exit(1)
} else { }
// run and pass through exit code if err := container.Serve(); err != nil {
if err = b.Start(); err != nil { fmsg.PrintBaseError(err, "cannot configure container:")
log.Fatalf("cannot start target process: %v", err) }
} else if err = b.Wait(); err != nil { if err := container.Wait(); err != nil {
var exitError *exec.ExitError var exitError *exec.ExitError
if !errors.As(err, &exitError) { if !errors.As(err, &exitError) {
if errors.Is(err, context.Canceled) {
os.Exit(2)
}
log.Printf("wait: %v", err) log.Printf("wait: %v", err)
internal.Exit(127) os.Exit(127)
panic("unreachable")
}
internal.Exit(exitError.ExitCode())
panic("unreachable")
} }
os.Exit(exitError.ExitCode())
} }
} }

View File

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

View File

@ -8,9 +8,9 @@ import (
"os/exec" "os/exec"
"strconv" "strconv"
"strings" "strings"
"syscall"
"time" "time"
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/sandbox" "git.gensokyo.uk/security/fortify/sandbox"
@ -25,10 +25,11 @@ type Shim struct {
killFallback chan error killFallback chan error
// monitor to shim encoder // monitor to shim encoder
encoder *gob.Encoder encoder *gob.Encoder
// bwrap --sync-fd value
sync *uintptr
} }
func (s *Shim) Unwrap() *exec.Cmd { return s.cmd }
func (s *Shim) Fallback() chan error { return s.killFallback }
func (s *Shim) String() string { func (s *Shim) String() string {
if s.cmd == nil { if s.cmd == nil {
return "(unused shim manager)" return "(unused shim manager)"
@ -36,21 +37,9 @@ func (s *Shim) String() string {
return s.cmd.String() return s.cmd.String()
} }
func (s *Shim) Unwrap() *exec.Cmd {
return s.cmd
}
func (s *Shim) WaitFallback() chan error {
return s.killFallback
}
func (s *Shim) Start( func (s *Shim) Start(
// string representation of application id
aid string, aid string,
// string representation of supplementary group ids
supp []string, supp []string,
// bwrap --sync-fd
syncFd *os.File,
) (*time.Time, error) { ) (*time.Time, error) {
// prepare user switcher invocation // prepare user switcher invocation
fsuPath := internal.MustFsuPath() fsuPath := internal.MustFsuPath()
@ -76,12 +65,6 @@ func (s *Shim) Start(
s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr s.cmd.Stdin, s.cmd.Stdout, s.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
s.cmd.Dir = "/" s.cmd.Dir = "/"
// pass sync fd if set
if syncFd != nil {
fd := proc.ExtraFile(s.cmd, syncFd)
s.sync = &fd
}
fmsg.Verbose("starting shim via fsu:", s.cmd) fmsg.Verbose("starting shim via fsu:", s.cmd)
// withhold messages to stderr // withhold messages to stderr
fmsg.Suspend() fmsg.Suspend()
@ -90,10 +73,11 @@ func (s *Shim) Start(
"cannot start fsu:") "cannot start fsu:")
} }
startTime := time.Now().UTC() startTime := time.Now().UTC()
return &startTime, nil return &startTime, nil
} }
func (s *Shim) Serve(ctx context.Context, payload *Payload) error { func (s *Shim) Serve(ctx context.Context, params *Params) error {
// kill shim if something goes wrong and an error is returned // kill shim if something goes wrong and an error is returned
s.killFallback = make(chan error, 1) s.killFallback = make(chan error, 1)
killShim := func() { killShim := func() {
@ -103,9 +87,8 @@ func (s *Shim) Serve(ctx context.Context, payload *Payload) error {
} }
defer func() { killShim() }() defer func() { killShim() }()
payload.Sync = s.sync
encodeErr := make(chan error) encodeErr := make(chan error)
go func() { encodeErr <- s.encoder.Encode(payload) }() go func() { encodeErr <- s.encoder.Encode(params) }()
select { select {
// encode return indicates setup completion // encode return indicates setup completion
@ -121,11 +104,11 @@ func (s *Shim) Serve(ctx context.Context, payload *Payload) error {
case <-ctx.Done(): case <-ctx.Done():
err := ctx.Err() err := ctx.Err()
if errors.Is(err, context.Canceled) { if errors.Is(err, context.Canceled) {
return fmsg.WrapError(errors.New("shim setup canceled"), return fmsg.WrapError(syscall.ECANCELED,
"shim setup canceled") "shim setup canceled")
} }
if errors.Is(err, context.DeadlineExceeded) { if errors.Is(err, context.DeadlineExceeded) {
return fmsg.WrapError(errors.New("deadline exceeded waiting for shim"), return fmsg.WrapError(syscall.ETIMEDOUT,
"deadline exceeded waiting for shim") "deadline exceeded waiting for shim")
} }
// unreachable // unreachable

View File

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

View File

@ -12,8 +12,10 @@ import (
// State provides safe interaction with operating system state. // State provides safe interaction with operating system state.
type State interface { type State interface {
// Geteuid provides [os.Geteuid]. // Getuid provides [os.Getuid].
Geteuid() int Getuid() int
// Getgid provides [os.Getgid].
Getgid() int
// LookupEnv provides [os.LookupEnv]. // LookupEnv provides [os.LookupEnv].
LookupEnv(key string) (string, bool) LookupEnv(key string) (string, bool)
// TempDir provides [os.TempDir]. // TempDir provides [os.TempDir].
@ -47,7 +49,7 @@ type State interface {
// CopyPaths is a generic implementation of [System.Paths]. // CopyPaths is a generic implementation of [System.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.Geteuid())) v.SharePath = path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Getuid()))
fmsg.Verbosef("process share directory at %q", v.SharePath) fmsg.Verbosef("process share directory at %q", v.SharePath)

View File

@ -31,7 +31,8 @@ type Std struct {
uidMu sync.RWMutex uidMu sync.RWMutex
} }
func (s *Std) Geteuid() int { return os.Geteuid() } func (s *Std) Getuid() int { return os.Getuid() }
func (s *Std) Getgid() int { return os.Getgid() }
func (s *Std) LookupEnv(key string) (string, bool) { return os.LookupEnv(key) } func (s *Std) LookupEnv(key string) (string, bool) { return os.LookupEnv(key) }
func (s *Std) TempDir() string { return os.TempDir() } func (s *Std) TempDir() string { return os.TempDir() }
func (s *Std) LookPath(file string) (string, error) { return exec.LookPath(file) } func (s *Std) LookPath(file string) (string, error) { return exec.LookPath(file) }

18
main.go
View File

@ -20,7 +20,6 @@ import (
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/app" "git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/app/init0"
"git.gensokyo.uk/security/fortify/internal/app/shim" "git.gensokyo.uk/security/fortify/internal/app/shim"
"git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/state" "git.gensokyo.uk/security/fortify/internal/state"
@ -43,7 +42,6 @@ var std sys.State = new(sys.Std)
func main() { func main() {
// early init path, skips root check and duplicate PR_SET_DUMPABLE // early init path, skips root check and duplicate PR_SET_DUMPABLE
sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg) sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg)
init0.TryArgv0()
if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil { if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err) log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
@ -76,9 +74,7 @@ func buildCommand(out io.Writer) command.Command {
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console"). Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output as JSON when applicable") Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output as JSON when applicable")
// internal commands
c.Command("shim", command.UsageInternal, func([]string) error { shim.Main(); return errSuccess }) c.Command("shim", command.UsageInternal, func([]string) error { shim.Main(); return errSuccess })
c.Command("init", command.UsageInternal, func([]string) error { init0.Main(); return errSuccess })
c.Command("app", "Launch app defined by the specified config file", func(args []string) error { c.Command("app", "Launch app defined by the specified config file", func(args []string) error {
if len(args) < 1 { if len(args) < 1 {
@ -87,10 +83,9 @@ func buildCommand(out io.Writer) command.Command {
// config extraArgs... // config extraArgs...
config := tryPath(args[0]) config := tryPath(args[0])
config.Command = append(config.Command, args[1:]...) config.Args = append(config.Args, args[1:]...)
// invoke app runApp(config)
runApp(app.MustNew(std), config)
panic("unreachable") panic("unreachable")
}) })
@ -113,7 +108,7 @@ func buildCommand(out io.Writer) command.Command {
// initialise config from flags // initialise config from flags
config := &fst.Config{ config := &fst.Config{
ID: fid, ID: fid,
Command: args, Args: args,
} }
if aid < 0 || aid > 9999 { if aid < 0 || aid > 9999 {
@ -199,7 +194,7 @@ func buildCommand(out io.Writer) command.Command {
} }
// invoke app // invoke app
runApp(app.MustNew(std), config) runApp(config)
panic("unreachable") panic("unreachable")
}). }).
Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"), Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"),
@ -279,10 +274,11 @@ func buildCommand(out io.Writer) command.Command {
return c return c
} }
func runApp(a fst.App, config *fst.Config) { func runApp(config *fst.Config) {
ctx, stop := signal.NotifyContext(context.Background(), ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM) syscall.SIGINT, syscall.SIGTERM)
defer stop() // unreachable defer stop() // unreachable
a := app.MustNew(ctx, std)
rs := new(fst.RunState) rs := new(fst.RunState)
if sa, err := a.Seal(config); err != nil { if sa, err := a.Seal(config); err != nil {
@ -290,7 +286,7 @@ func runApp(a fst.App, config *fst.Config) {
rs.ExitCode = 1 rs.ExitCode = 1
} else { } else {
// this updates ExitCode // this updates ExitCode
app.PrintRunStateErr(rs, sa.Run(ctx, rs)) app.PrintRunStateErr(rs, sa.Run(rs))
} }
internal.Exit(rs.ExitCode) internal.Exit(rs.ExitCode)
} }

View File

@ -1,3 +1,4 @@
packages:
{ {
lib, lib,
pkgs, pkgs,
@ -26,7 +27,7 @@ let
in in
{ {
imports = [ ./options.nix ]; imports = [ (import ./options.nix packages) ];
config = mkIf cfg.enable { config = mkIf cfg.enable {
security.wrappers.fsu = { security.wrappers.fsu = {
@ -85,12 +86,11 @@ in
enablements = with app.capability; (if wayland then 1 else 0) + (if x11 then 2 else 0) + (if dbus then 4 else 0) + (if pulse then 8 else 0); enablements = with app.capability; (if wayland then 1 else 0) + (if x11 then 2 else 0) + (if dbus then 4 else 0) + (if pulse then 8 else 0);
conf = { conf = {
inherit (app) id; inherit (app) id;
command = [ path = pkgs.writeScript "${app.name}-start" ''
(pkgs.writeScript "${app.name}-start" ''
#!${pkgs.zsh}${pkgs.zsh.shellPath} #!${pkgs.zsh}${pkgs.zsh.shellPath}
${script} ${script}
'') '';
]; args = [ "${app.name}-start" ];
confinement = { confinement = {
app_id = aid; app_id = aid;
inherit (app) groups; inherit (app) groups;
@ -98,17 +98,15 @@ in
home = getsubhome fid aid; home = getsubhome fid aid;
sandbox = { sandbox = {
inherit (app) inherit (app)
devel
userns userns
net net
dev dev
tty
multiarch
env env
; ;
syscall = {
inherit (app) compat multiarch bluetooth;
deny_devel = !app.devel;
};
map_real_uid = app.mapRealUid; map_real_uid = app.mapRealUid;
no_new_session = app.tty;
direct_wayland = app.insecureWayland; direct_wayland = app.insecureWayland;
filesystem = filesystem =
let let
@ -148,7 +146,7 @@ in
] ]
++ app.extraPaths; ++ app.extraPaths;
auto_etc = true; auto_etc = true;
override = [ "/var/run/nscd" ]; cover = [ "/var/run/nscd" ];
}; };
inherit enablements; inherit enablements;
inherit (dbusConfig) session_bus system_bus; inherit (dbusConfig) session_bus system_bus;

View File

@ -1,17 +1,8 @@
packages:
{ lib, pkgs, ... }: { lib, pkgs, ... }:
let let
inherit (lib) types mkOption mkEnableOption; inherit (lib) types mkOption mkEnableOption;
fortify = pkgs.pkgsStatic.callPackage ./package.nix {
inherit (pkgs)
bubblewrap
xdg-dbus-proxy
glibc
zstd
gnutar
coreutils
;
};
in in
{ {
@ -21,13 +12,13 @@ in
package = mkOption { package = mkOption {
type = types.package; type = types.package;
default = fortify; default = packages.${pkgs.system}.fortify;
description = "The fortify package to use."; description = "The fortify package to use.";
}; };
fsuPackage = mkOption { fsuPackage = mkOption {
type = types.package; type = types.package;
default = pkgs.callPackage ./cmd/fsu/package.nix { inherit fortify; }; default = packages.${pkgs.system}.fsu;
description = "The fsu package to use."; description = "The fsu package to use.";
}; };
@ -157,21 +148,19 @@ in
''; '';
}; };
nix = mkEnableOption "nix daemon"; devel = mkEnableOption "debugging-related kernel interfaces";
userns = mkEnableOption "user namespace"; userns = mkEnableOption "user namespace creation";
mapRealUid = mkEnableOption "mapping to priv-user uid";
dev = mkEnableOption "access to all devices";
tty = mkEnableOption "access to the controlling terminal"; tty = mkEnableOption "access to the controlling terminal";
insecureWayland = mkEnableOption "direct access to the Wayland socket"; multiarch = mkEnableOption "multiarch kernel-level support";
net = mkEnableOption "network access" // { net = mkEnableOption "network access" // {
default = true; default = true;
}; };
compat = mkEnableOption "disable syscall filter extensions"; nix = mkEnableOption "nix daemon access";
devel = mkEnableOption "development kernel APIs"; mapRealUid = mkEnableOption "mapping to priv-user uid";
multiarch = mkEnableOption "multiarch kernel support"; dev = mkEnableOption "access to all devices";
bluetooth = mkEnableOption "AF_BLUETOOTH socket operations"; insecureWayland = mkEnableOption "direct access to the Wayland socket";
gpu = mkOption { gpu = mkOption {
type = nullOr bool; type = nullOr bool;

View File

@ -19,6 +19,13 @@
gnutar, gnutar,
coreutils, coreutils,
# for passthru.buildInputs
go,
gcc,
# for check
util-linux,
glibc, # for ldd glibc, # for ldd
withStatic ? stdenv.hostPlatform.isStatic, withStatic ? stdenv.hostPlatform.isStatic,
}: }:
@ -30,7 +37,7 @@ buildGoModule rec {
src = builtins.path { src = builtins.path {
name = "${pname}-src"; name = "${pname}-src";
path = lib.cleanSource ./.; path = lib.cleanSource ./.;
filter = path: type: !(type == "regular" && (lib.hasSuffix ".nix" path || lib.hasSuffix ".py" path)) && !(type == "directory" && lib.hasSuffix "/cmd/fsu" path); filter = path: type: !(type == "regular" && (lib.hasSuffix ".nix" path || lib.hasSuffix ".py" path)) && !(type == "directory" && lib.hasSuffix "/test" path) && !(type == "directory" && lib.hasSuffix "/cmd/fsu" path);
}; };
vendorHash = null; vendorHash = null;
@ -108,4 +115,14 @@ buildGoModule rec {
) )
} }
''; '';
passthru.targetPkgs =
[
go
gcc
xorg.xorgproto
util-linux
]
++ buildInputs
++ nativeBuildInputs;
} }

View File

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

View File

@ -89,10 +89,10 @@ func printShowInstance(
flags = append(flags, name) flags = append(flags, name)
} }
} }
writeFlag("userns", sandbox.UserNS) writeFlag("userns", sandbox.Userns)
writeFlag("net", sandbox.Net) writeFlag("net", sandbox.Net)
writeFlag("dev", sandbox.Dev) writeFlag("dev", sandbox.Dev)
writeFlag("tty", sandbox.NoNewSession) writeFlag("tty", sandbox.Tty)
writeFlag("mapuid", sandbox.MapRealUID) writeFlag("mapuid", sandbox.MapRealUID)
writeFlag("directwl", sandbox.DirectWayland) writeFlag("directwl", sandbox.DirectWayland)
writeFlag("autoetc", sandbox.AutoEtc) writeFlag("autoetc", sandbox.AutoEtc)
@ -107,14 +107,14 @@ func printShowInstance(
} }
t.Printf(" Etc:\t%s\n", etc) t.Printf(" Etc:\t%s\n", etc)
if len(sandbox.Override) > 0 { if len(sandbox.Cover) > 0 {
t.Printf(" Overrides:\t%s\n", strings.Join(sandbox.Override, " ")) t.Printf(" Cover:\t%s\n", strings.Join(sandbox.Cover, " "))
} }
// 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.Command, " ")) t.Printf(" Command:\t%s\n", strings.Join(config.Args, " "))
t.Printf("\n") t.Printf("\n")
if !short { if !short {
@ -256,7 +256,7 @@ func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON boo
) )
if e.Config != nil { if e.Config != nil {
es = e.Config.Confinement.Enablements.String() es = e.Config.Confinement.Enablements.String()
cs = fmt.Sprintf("%q", e.Config.Command) cs = fmt.Sprintf("%q", e.Config.Args)
as = strconv.Itoa(e.Config.Confinement.AppID) as = strconv.Itoa(e.Config.Confinement.AppID)
} }
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\t%s\t%s\n",

View File

@ -43,7 +43,7 @@ func Test_printShowInstance(t *testing.T) {
Hostname: "localhost" Hostname: "localhost"
Flags: userns net dev tty mapuid autoetc Flags: userns net dev tty mapuid autoetc
Etc: /etc Etc: /etc
Overrides: /var/run/nscd Cover: /var/run/nscd
Command: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland Command: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
Filesystem Filesystem
@ -127,7 +127,7 @@ App
Hostname: "localhost" Hostname: "localhost"
Flags: userns net dev tty mapuid autoetc Flags: userns net dev tty mapuid autoetc
Etc: /etc Etc: /etc
Overrides: /var/run/nscd Cover: /var/run/nscd
Command: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland Command: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
Filesystem Filesystem
@ -192,7 +192,8 @@ App
"pid": 3735928559, "pid": 3735928559,
"config": { "config": {
"id": "org.chromium.Chromium", "id": "org.chromium.Chromium",
"command": [ "path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium", "chromium",
"--ignore-gpu-blocklist", "--ignore-gpu-blocklist",
"--disable-smooth-scrolling", "--disable-smooth-scrolling",
@ -209,24 +210,19 @@ App
"home": "/var/lib/persist/home/org.chromium.Chromium", "home": "/var/lib/persist/home/org.chromium.Chromium",
"sandbox": { "sandbox": {
"hostname": "localhost", "hostname": "localhost",
"seccomp": 32,
"devel": true,
"userns": true, "userns": true,
"net": true, "net": true,
"dev": true, "tty": true,
"syscall": {
"compat": false,
"deny_devel": true,
"multiarch": true, "multiarch": true,
"linux32": false,
"can": false,
"bluetooth": false
},
"no_new_session": true,
"map_real_uid": true,
"env": { "env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY", "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com", "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT" "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
}, },
"map_real_uid": true,
"dev": true,
"filesystem": [ "filesystem": [
{ {
"src": "/nix/store" "src": "/nix/store"
@ -259,7 +255,7 @@ App
], ],
"etc": "/etc", "etc": "/etc",
"auto_etc": true, "auto_etc": true,
"override": [ "cover": [
"/var/run/nscd" "/var/run/nscd"
] ]
}, },
@ -320,7 +316,8 @@ App
`}, `},
{"json config", nil, fst.Template(), false, true, `{ {"json config", nil, fst.Template(), false, true, `{
"id": "org.chromium.Chromium", "id": "org.chromium.Chromium",
"command": [ "path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium", "chromium",
"--ignore-gpu-blocklist", "--ignore-gpu-blocklist",
"--disable-smooth-scrolling", "--disable-smooth-scrolling",
@ -337,24 +334,19 @@ App
"home": "/var/lib/persist/home/org.chromium.Chromium", "home": "/var/lib/persist/home/org.chromium.Chromium",
"sandbox": { "sandbox": {
"hostname": "localhost", "hostname": "localhost",
"seccomp": 32,
"devel": true,
"userns": true, "userns": true,
"net": true, "net": true,
"dev": true, "tty": true,
"syscall": {
"compat": false,
"deny_devel": true,
"multiarch": true, "multiarch": true,
"linux32": false,
"can": false,
"bluetooth": false
},
"no_new_session": true,
"map_real_uid": true,
"env": { "env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY", "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com", "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT" "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
}, },
"map_real_uid": true,
"dev": true,
"filesystem": [ "filesystem": [
{ {
"src": "/nix/store" "src": "/nix/store"
@ -387,7 +379,7 @@ App
], ],
"etc": "/etc", "etc": "/etc",
"auto_etc": true, "auto_etc": true,
"override": [ "cover": [
"/var/run/nscd" "/var/run/nscd"
] ]
}, },
@ -506,7 +498,8 @@ func Test_printPs(t *testing.T) {
"pid": 3735928559, "pid": 3735928559,
"config": { "config": {
"id": "org.chromium.Chromium", "id": "org.chromium.Chromium",
"command": [ "path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium", "chromium",
"--ignore-gpu-blocklist", "--ignore-gpu-blocklist",
"--disable-smooth-scrolling", "--disable-smooth-scrolling",
@ -523,24 +516,19 @@ func Test_printPs(t *testing.T) {
"home": "/var/lib/persist/home/org.chromium.Chromium", "home": "/var/lib/persist/home/org.chromium.Chromium",
"sandbox": { "sandbox": {
"hostname": "localhost", "hostname": "localhost",
"seccomp": 32,
"devel": true,
"userns": true, "userns": true,
"net": true, "net": true,
"dev": true, "tty": true,
"syscall": {
"compat": false,
"deny_devel": true,
"multiarch": true, "multiarch": true,
"linux32": false,
"can": false,
"bluetooth": false
},
"no_new_session": true,
"map_real_uid": true,
"env": { "env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY", "GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com", "GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT" "GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
}, },
"map_real_uid": true,
"dev": true,
"filesystem": [ "filesystem": [
{ {
"src": "/nix/store" "src": "/nix/store"
@ -573,7 +561,7 @@ func Test_printPs(t *testing.T) {
], ],
"etc": "/etc", "etc": "/etc",
"auto_etc": true, "auto_etc": true,
"override": [ "cover": [
"/var/run/nscd" "/var/run/nscd"
] ]
}, },

View File

@ -1,6 +0,0 @@
package sandbox
const (
PR_SET_NO_NEW_PRIVS = 0x26
CAP_SYS_ADMIN = 0x15
)

View File

@ -54,7 +54,6 @@ type (
// with behaviour identical to its [exec.Cmd] counterpart. // with behaviour identical to its [exec.Cmd] counterpart.
ExtraFiles []*os.File ExtraFiles []*os.File
InitParams
// Custom [exec.Cmd] initialisation function. // Custom [exec.Cmd] initialisation function.
CommandContext func(ctx context.Context) (cmd *exec.Cmd) CommandContext func(ctx context.Context) (cmd *exec.Cmd)
@ -67,14 +66,16 @@ type (
Stdout io.Writer Stdout io.Writer
Stderr io.Writer Stderr io.Writer
Cancel func() error Cancel func(cmd *exec.Cmd) error
WaitDelay time.Duration WaitDelay time.Duration
cmd *exec.Cmd cmd *exec.Cmd
ctx context.Context ctx context.Context
Params
} }
InitParams struct { // Params holds container configuration and is safe to serialise.
Params struct {
// Working directory in the container. // Working directory in the container.
Dir string Dir string
// Initial process environment. // Initial process environment.
@ -100,7 +101,9 @@ type (
Ops []Op Ops []Op
Op interface { Op interface {
apply(params *InitParams) error early(params *Params) error
apply(params *Params) error
prefix() string
Is(op Op) bool Is(op Op) bool
fmt.Stringer fmt.Stringer
@ -141,7 +144,12 @@ func (p *Container) Start() error {
} }
p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
p.cmd.Cancel, p.cmd.WaitDelay = p.Cancel, p.WaitDelay p.cmd.WaitDelay = p.WaitDelay
if p.Cancel != nil {
p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
} else {
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(syscall.SIGTERM) }
}
p.cmd.Dir = "/" p.cmd.Dir = "/"
p.cmd.SysProcAttr = &syscall.SysProcAttr{ p.cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: p.Flags&FAllowTTY == 0, Setsid: p.Flags&FAllowTTY == 0,
@ -183,7 +191,11 @@ func (p *Container) Serve() error {
panic("invalid serve") panic("invalid serve")
} }
setup := p.setup
p.setup = nil
if p.Path != "" && !path.IsAbs(p.Path) { if p.Path != "" && !path.IsAbs(p.Path) {
p.cancel()
return msg.WrapErr(syscall.EINVAL, return msg.WrapErr(syscall.EINVAL,
fmt.Sprintf("invalid executable path %q", p.Path)) fmt.Sprintf("invalid executable path %q", p.Path))
} }
@ -192,6 +204,7 @@ func (p *Container) Serve() error {
if p.name == "" { if p.name == "" {
p.Path = os.Getenv("SHELL") p.Path = os.Getenv("SHELL")
if !path.IsAbs(p.Path) { if !path.IsAbs(p.Path) {
p.cancel()
return msg.WrapErr(syscall.EBADE, return msg.WrapErr(syscall.EBADE,
"no command specified and $SHELL is invalid") "no command specified and $SHELL is invalid")
} }
@ -199,23 +212,26 @@ func (p *Container) Serve() error {
} else if path.IsAbs(p.name) { } else if path.IsAbs(p.name) {
p.Path = p.name p.Path = p.name
} else if v, err := exec.LookPath(p.name); err != nil { } else if v, err := exec.LookPath(p.name); err != nil {
p.cancel()
return msg.WrapErr(err, err.Error()) return msg.WrapErr(err, err.Error())
} else { } else {
p.Path = v p.Path = v
} }
} }
setup := p.setup err := setup.Encode(
p.setup = nil
return setup.Encode(
&initParams{ &initParams{
p.InitParams, p.Params,
syscall.Getuid(), syscall.Getuid(),
syscall.Getgid(), syscall.Getgid(),
len(p.ExtraFiles), len(p.ExtraFiles),
msg.IsVerbose(), msg.IsVerbose(),
}, },
) )
if err != nil {
p.cancel()
}
return err
} }
func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() } func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() }
@ -227,6 +243,6 @@ func (p *Container) String() string {
func New(ctx context.Context, name string, args ...string) *Container { func New(ctx context.Context, name string, args ...string) *Container {
return &Container{name: name, ctx: ctx, return &Container{name: name, ctx: ctx,
InitParams: InitParams{Args: append([]string{name}, args...), Dir: "/", Ops: new(Ops)}, Params: Params{Args: append([]string{name}, args...), Dir: "/", Ops: new(Ops)},
} }
} }

View File

@ -3,10 +3,11 @@ package sandbox_test
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/gob"
"log" "log"
"os" "os"
"os/exec" "os/exec"
"strings"
"syscall" "syscall"
"testing" "testing"
"time" "time"
@ -17,7 +18,12 @@ import (
"git.gensokyo.uk/security/fortify/ldd" "git.gensokyo.uk/security/fortify/ldd"
"git.gensokyo.uk/security/fortify/sandbox" "git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/sandbox/seccomp" "git.gensokyo.uk/security/fortify/sandbox/seccomp"
check "git.gensokyo.uk/security/fortify/test/sandbox" "git.gensokyo.uk/security/fortify/sandbox/vfs"
)
const (
ignore = "\x00"
ignoreV = -1
) )
func TestContainer(t *testing.T) { func TestContainer(t *testing.T) {
@ -33,7 +39,7 @@ func TestContainer(t *testing.T) {
name string name string
flags sandbox.HardeningFlags flags sandbox.HardeningFlags
ops *sandbox.Ops ops *sandbox.Ops
mnt []*check.Mntent mnt []*vfs.MountInfoEntry
host string host string
}{ }{
{"minimal", 0, new(sandbox.Ops), nil, "test-minimal"}, {"minimal", 0, new(sandbox.Ops), nil, "test-minimal"},
@ -42,21 +48,23 @@ func TestContainer(t *testing.T) {
{"tmpfs", 0, {"tmpfs", 0,
new(sandbox.Ops). new(sandbox.Ops).
Tmpfs(fst.Tmp, 0, 0755), Tmpfs(fst.Tmp, 0, 0755),
[]*check.Mntent{ []*vfs.MountInfoEntry{
{FSName: "tmpfs", Dir: fst.Tmp, Type: "tmpfs", Opts: "\x00"}, e("/", fst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
}, "test-tmpfs"}, }, "test-tmpfs"},
{"dev", sandbox.FAllowTTY, // go test output is not a tty {"dev", sandbox.FAllowTTY, // go test output is not a tty
new(sandbox.Ops). new(sandbox.Ops).
Dev("/dev"), Dev("/dev").
[]*check.Mntent{ Mqueue("/dev/mqueue"),
{FSName: "devtmpfs", Dir: "/dev", Type: "tmpfs", Opts: "\x00"}, []*vfs.MountInfoEntry{
{FSName: "devtmpfs", Dir: "/dev/null", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1}, e("/", "/dev", "rw,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
{FSName: "devtmpfs", Dir: "/dev/zero", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1}, e("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
{FSName: "devtmpfs", Dir: "/dev/full", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1}, e("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
{FSName: "devtmpfs", Dir: "/dev/random", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1}, e("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
{FSName: "devtmpfs", Dir: "/dev/urandom", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1}, e("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
{FSName: "devtmpfs", Dir: "/dev/tty", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1}, e("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
{FSName: "devpts", Dir: "/dev/pts", Type: "devpts", Opts: "rw,nosuid,noexec,relatime,mode=620,ptmxmode=666", Freq: 0, Passno: 0}, e("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
e("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
}, ""}, }, ""},
} }
@ -65,7 +73,7 @@ func TestContainer(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()
container := sandbox.New(ctx, os.Args[0], "-test.v", container := sandbox.New(ctx, "/usr/bin/sandbox.test", "-test.v",
"-test.run=TestHelperCheckContainer", "--", "check", tc.host) "-test.run=TestHelperCheckContainer", "--", "check", tc.host)
container.Uid = 1000 container.Uid = 1000
container.Gid = 100 container.Gid = 100
@ -84,7 +92,10 @@ func TestContainer(t *testing.T) {
container. container.
Tmpfs("/tmp", 0, 0755). Tmpfs("/tmp", 0, 0755).
Bind(os.Args[0], os.Args[0], 0) Bind(os.Args[0], os.Args[0], 0).
Mkdir("/usr/bin", 0755).
Link(os.Args[0], "/usr/bin/sandbox.test").
Place("/etc/hostname", []byte(container.Args[5]))
// in case test has cgo enabled // in case test has cgo enabled
var libPaths []string var libPaths []string
if entries, err := ldd.ExecFilter(ctx, if entries, err := ldd.ExecFilter(ctx,
@ -99,25 +110,26 @@ func TestContainer(t *testing.T) {
for _, name := range libPaths { for _, name := range libPaths {
container.Bind(name, name, 0) container.Bind(name, name, 0)
} }
// needs /proc to check mountinfo
container.Proc("/proc")
mnt := make([]*check.Mntent, 0, 3+len(libPaths)) mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
mnt = append(mnt, &check.Mntent{FSName: "rootfs", Dir: "/", Type: "tmpfs", Opts: "host_passthrough"}) mnt = append(mnt, e("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore))
mnt = append(mnt, tc.mnt...) mnt = append(mnt, tc.mnt...)
mnt = append(mnt, mnt = append(mnt,
&check.Mntent{FSName: "tmpfs", Dir: "/tmp", Type: "tmpfs", Opts: "host_passthrough"}, e("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
&check.Mntent{FSName: "\x00", Dir: os.Args[0], Type: "\x00", Opts: "\x00"}) e(ignore, os.Args[0], "ro,nosuid,nodev,relatime", ignore, ignore, ignore),
e(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
)
for _, name := range libPaths { for _, name := range libPaths {
mnt = append(mnt, &check.Mntent{FSName: "\x00", Dir: name, Type: "\x00", Opts: "\x00", Freq: -1, Passno: -1}) mnt = append(mnt, e(ignore, name, "ro,nosuid,nodev,relatime", ignore, ignore, ignore))
} }
mnt = append(mnt, &check.Mntent{FSName: "proc", Dir: "/proc", Type: "proc", Opts: "rw,nosuid,nodev,noexec,relatime"}) mnt = append(mnt, e("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"))
mntentWant := new(bytes.Buffer) want := new(bytes.Buffer)
if err := json.NewEncoder(mntentWant).Encode(mnt); err != nil { if err := gob.NewEncoder(want).Encode(mnt); err != nil {
t.Fatalf("cannot serialise mntent: %v", err) t.Fatalf("cannot serialise expected mount points: %v", err)
} }
container.Stdin = mntentWant container.Stdin = want
// needs /proc to check mntent
container.Proc("/proc")
if err := container.Start(); err != nil { if err := container.Start(); err != nil {
fmsg.PrintBaseError(err, "start:") fmsg.PrintBaseError(err, "start:")
@ -134,6 +146,21 @@ func TestContainer(t *testing.T) {
} }
} }
func e(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry {
return &vfs.MountInfoEntry{
ID: ignoreV,
Parent: ignoreV,
Devno: vfs.DevT{ignoreV, ignoreV},
Root: root,
Target: target,
VfsOptstr: vfsOptstr,
OptFields: []string{ignore},
FsType: fsType,
Source: source,
FsOptstr: fsOptstr,
}
}
func TestContainerString(t *testing.T) { func TestContainerString(t *testing.T) {
container := sandbox.New(context.TODO(), "ldd", "/usr/bin/env") container := sandbox.New(context.TODO(), "ldd", "/usr/bin/env")
container.Flags |= sandbox.FAllowDevel container.Flags |= sandbox.FAllowDevel
@ -171,9 +198,55 @@ func TestHelperCheckContainer(t *testing.T) {
} else if name != os.Args[5] { } else if name != os.Args[5] {
t.Errorf("Hostname: %q, want %q", name, os.Args[5]) t.Errorf("Hostname: %q, want %q", name, os.Args[5])
} }
if p, err := os.ReadFile("/etc/hostname"); err != nil {
t.Fatalf("%v", err)
} else if string(p) != os.Args[5] {
t.Errorf("/etc/hostname: %q, want %q", string(p), os.Args[5])
}
})
t.Run("mount", func(t *testing.T) {
var mnt []*vfs.MountInfoEntry
if err := gob.NewDecoder(os.Stdin).Decode(&mnt); err != nil {
t.Fatalf("cannot receive expected mount points: %v", err)
}
var d *vfs.MountInfoDecoder
if f, err := os.Open("/proc/self/mountinfo"); err != nil {
t.Fatalf("cannot open mountinfo: %v", err)
} else {
d = vfs.NewMountInfoDecoder(f)
}
i := 0
for cur := range d.Entries() {
if i == len(mnt) {
t.Errorf("got more than %d entries", len(mnt))
break
}
// ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",relatime")
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",noatime")
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",relatime")
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime")
if !cur.EqualWithIgnore(mnt[i], "\x00") {
t.Errorf("[FAIL] %s", cur)
} else {
t.Logf("[ OK ] %s", cur)
}
i++
}
if err := d.Err(); err != nil {
t.Errorf("cannot parse mountinfo: %v", err)
}
if i != len(mnt) {
t.Errorf("got %d entries, want %d", i, len(mnt))
}
}) })
t.Run("seccomp", func(t *testing.T) { check.MustAssertSeccomp() })
t.Run("mntent", func(t *testing.T) { check.MustAssertMounts("", "/proc/mounts", "/proc/self/fd/0") })
} }
func commandContext(ctx context.Context) *exec.Cmd { func commandContext(ctx context.Context) *exec.Cmd {

View File

@ -28,7 +28,7 @@ const (
) )
type initParams struct { type initParams struct {
InitParams Params
HostUid, HostGid int HostUid, HostGid int
// extra files count // extra files count
@ -98,6 +98,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err) log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
} }
oldmask := syscall.Umask(0)
if params.Hostname != "" { if params.Hostname != "" {
if err := syscall.Sethostname([]byte(params.Hostname)); err != nil { if err := syscall.Sethostname([]byte(params.Hostname)); err != nil {
log.Fatalf("cannot set hostname: %v", err) log.Fatalf("cannot set hostname: %v", err)
@ -114,6 +115,19 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
log.Fatalf("cannot make / rslave: %v", err) log.Fatalf("cannot make / rslave: %v", err)
} }
for i, op := range *params.Ops {
if op == nil {
log.Fatalf("invalid op %d", i)
}
if err := op.early(&params.Params); err != nil {
msg.PrintBaseErr(err,
fmt.Sprintf("cannot prepare op %d:", i))
msg.BeforeExit()
os.Exit(1)
}
}
if err := syscall.Mount("rootfs", basePath, "tmpfs", if err := syscall.Mount("rootfs", basePath, "tmpfs",
syscall.MS_NODEV|syscall.MS_NOSUID, syscall.MS_NODEV|syscall.MS_NOSUID,
""); err != nil { ""); err != nil {
@ -143,8 +157,9 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
} }
for i, op := range *params.Ops { for i, op := range *params.Ops {
msg.Verbosef("mounting %s", op) // ops already checked during early setup
if err := op.apply(&params.InitParams); err != nil { msg.Verbosef("%s %s", op.prefix(), op)
if err := op.apply(&params.Params); err != nil {
msg.PrintBaseErr(err, msg.PrintBaseErr(err,
fmt.Sprintf("cannot apply op %d:", i)) fmt.Sprintf("cannot apply op %d:", i))
msg.BeforeExit() msg.BeforeExit()
@ -216,6 +231,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
for i := range extraFiles { for i := range extraFiles {
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)
/* /*
prepare initial process prepare initial process
@ -223,7 +239,6 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
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.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
cmd.Args = params.Args cmd.Args = params.Args
cmd.Env = params.Env cmd.Env = params.Env
cmd.ExtraFiles = extraFiles cmd.ExtraFiles = extraFiles
@ -308,10 +323,13 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
switch { switch {
case w.wstatus.Exited(): case w.wstatus.Exited():
r = w.wstatus.ExitStatus() r = w.wstatus.ExitStatus()
msg.Verbosef("initial process exited with code %d", w.wstatus.ExitStatus())
case w.wstatus.Signaled(): case w.wstatus.Signaled():
r = 128 + int(w.wstatus.Signal()) r = 128 + int(w.wstatus.Signal())
msg.Verbosef("initial process exited with signal %s", w.wstatus.Signal())
default: default:
r = 255 r = 255
msg.Verbosef("initial process exited with status %#x", w.wstatus)
} }
go func() { go func() {

View File

@ -4,86 +4,105 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"strings" "path/filepath"
"syscall" "syscall"
"git.gensokyo.uk/security/fortify/sandbox/vfs"
) )
const ( func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) error {
BindOptional = 1 << iota if eq {
BindSource msg.Verbosef("resolved %q flags %#x", target, flags)
BindRecursive } else {
BindWritable msg.Verbosef("resolved %q on %q flags %#x", source, target, flags)
BindDevices }
)
func bindMount(src, dest string, flags int) error { if err := syscall.Mount(source, target, "",
target := toSysroot(dest) syscall.MS_SILENT|syscall.MS_BIND|flags&syscall.MS_REC, ""); err != nil {
var source string return wrapErrSuffix(err,
fmt.Sprintf("cannot mount %q on %q:", source, target))
}
if flags&BindSource == 0 { var targetFinal string
// this is what bwrap does, so the behaviour is kept for now, if v, err := filepath.EvalSymlinks(target); err != nil {
// however recursively resolving links might improve user experience return msg.WrapErr(err, err.Error())
if rp, err := realpathHost(src); err != nil { } else {
if os.IsNotExist(err) { targetFinal = v
if flags&BindOptional != 0 { if targetFinal != target {
msg.Verbosef("target resolves to %q", targetFinal)
}
}
// final target path according to the kernel through proc
var targetKFinal string
{
var destFd int
if err := IgnoringEINTR(func() (err error) {
destFd, err = syscall.Open(targetFinal, O_PATH|syscall.O_CLOEXEC, 0)
return
}); err != nil {
return wrapErrSuffix(err,
fmt.Sprintf("cannot open %q:", targetFinal))
}
if v, err := os.Readlink(p.fd(destFd)); err != nil {
return msg.WrapErr(err, err.Error())
} else if err = syscall.Close(destFd); err != nil {
return wrapErrSuffix(err,
fmt.Sprintf("cannot close %q:", targetFinal))
} else {
targetKFinal = v
}
}
mf := syscall.MS_NOSUID | flags&syscall.MS_NODEV | flags&syscall.MS_RDONLY
return hostProc.mountinfo(func(d *vfs.MountInfoDecoder) error {
n, err := d.Unfold(targetKFinal)
if err != nil {
if errors.Is(err, syscall.ESTALE) {
return msg.WrapErr(err,
fmt.Sprintf("mount point %q never appeared in mountinfo", targetKFinal))
}
return wrapErrSuffix(err,
"cannot unfold mount hierarchy:")
}
if err = remountWithFlags(n, mf); err != nil {
return err
}
if flags&syscall.MS_REC == 0 {
return nil return nil
} else {
return msg.WrapErr(err,
fmt.Sprintf("path %q does not exist", src))
}
}
return msg.WrapErr(err, err.Error())
} else {
source = toHost(rp)
}
} else if flags&BindOptional != 0 {
return msg.WrapErr(syscall.EINVAL,
"flag source excludes optional")
} else {
source = toHost(src)
} }
if fi, err := os.Stat(source); err != nil { for cur := range n.Collective() {
return msg.WrapErr(err, err.Error()) err = remountWithFlags(cur, mf)
} else if fi.IsDir() { if err != nil && !errors.Is(err, syscall.EACCES) {
if err = os.MkdirAll(target, 0755); err != nil { return err
return wrapErrSuffix(err,
fmt.Sprintf("cannot create directory %q:", dest))
} }
} else if err = ensureFile(target, 0444); err != nil {
if errors.Is(err, syscall.EISDIR) {
return msg.WrapErr(err,
fmt.Sprintf("path %q is a directory", dest))
}
return wrapErrSuffix(err,
fmt.Sprintf("cannot create %q:", dest))
} }
var mf uintptr = syscall.MS_SILENT | syscall.MS_BIND return nil
if flags&BindRecursive != 0 { })
mf |= syscall.MS_REC }
func remountWithFlags(n *vfs.MountInfoNode, mf uintptr) error {
kf, unmatched := n.Flags()
if len(unmatched) != 0 {
msg.Verbosef("unmatched vfs options: %q", unmatched)
} }
if flags&BindWritable == 0 {
mf |= syscall.MS_RDONLY if kf&mf != mf {
return wrapErrSuffix(syscall.Mount("none", n.Clean, "",
syscall.MS_SILENT|syscall.MS_BIND|syscall.MS_REMOUNT|kf|mf,
""),
fmt.Sprintf("cannot remount %q:", n.Clean))
} }
if flags&BindDevices == 0 { return nil
mf |= syscall.MS_NODEV
}
if msg.IsVerbose() {
if strings.TrimPrefix(source, hostPath) == strings.TrimPrefix(target, sysrootPath) {
msg.Verbosef("resolved %q flags %#x", target, mf)
} else {
msg.Verbosef("resolved %q on %q flags %#x", source, target, mf)
}
}
return wrapErrSuffix(syscall.Mount(source, target, "", mf, ""),
fmt.Sprintf("cannot bind %q on %q:", src, dest))
} }
func mountTmpfs(fsname, name string, size int, perm os.FileMode) error { func mountTmpfs(fsname, name string, size int, perm os.FileMode) error {
target := toSysroot(name) target := toSysroot(name)
if err := os.MkdirAll(target, perm); err != nil { if err := os.MkdirAll(target, parentPerm(perm)); err != nil {
return err return msg.WrapErr(err, err.Error())
} }
opt := fmt.Sprintf("mode=%#o", perm) opt := fmt.Sprintf("mode=%#o", perm)
if size > 0 { if size > 0 {
@ -93,3 +112,14 @@ func mountTmpfs(fsname, name string, size int, perm os.FileMode) error {
syscall.MS_NOSUID|syscall.MS_NODEV, opt), syscall.MS_NOSUID|syscall.MS_NODEV, opt),
fmt.Sprintf("cannot mount tmpfs on %q:", name)) fmt.Sprintf("cannot mount tmpfs on %q:", name))
} }
func parentPerm(perm os.FileMode) os.FileMode {
pperm := 0755
if perm&0070 == 0 {
pperm &= ^0050
}
if perm&0007 == 0 {
pperm &= ^0005
}
return os.FileMode(pperm)
}

View File

@ -2,11 +2,15 @@ package sandbox
import ( import (
"errors" "errors"
"fmt"
"io/fs" "io/fs"
"os" "os"
"path" "path"
"strconv"
"strings" "strings"
"syscall" "syscall"
"git.gensokyo.uk/security/fortify/sandbox/vfs"
) )
const ( const (
@ -26,50 +30,65 @@ func toHost(name string) string {
return path.Join(hostPath, name) return path.Join(hostPath, name)
} }
func realpathHost(name string) (string, error) { func createFile(name string, perm, pperm os.FileMode, content []byte) error {
source := toHost(name) if err := os.MkdirAll(path.Dir(name), pperm); err != nil {
rp, err := os.Readlink(source) return msg.WrapErr(err, err.Error())
if err != nil {
if errors.Is(err, syscall.EINVAL) {
// not a symlink
return name, nil
}
return "", err
}
if !path.IsAbs(rp) {
return name, nil
}
msg.Verbosef("path %q resolves to %q", name, rp)
return rp, nil
}
func createFile(name string, perm os.FileMode, content []byte) error {
if err := os.MkdirAll(path.Dir(name), 0755); err != nil {
return err
} }
f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm) f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm)
if err != nil { if err != nil {
return err return msg.WrapErr(err, err.Error())
} }
if content != nil { if content != nil {
_, err = f.Write(content) _, err = f.Write(content)
if err != nil {
err = msg.WrapErr(err, err.Error())
}
} }
return errors.Join(f.Close(), err) return errors.Join(f.Close(), err)
} }
func ensureFile(name string, perm os.FileMode) error { func ensureFile(name string, perm, pperm os.FileMode) error {
fi, err := os.Stat(name) fi, err := os.Stat(name)
if err != nil { if err != nil {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
return err return err
} }
return createFile(name, perm, nil) return createFile(name, perm, pperm, nil)
} }
if mode := fi.Mode(); mode&fs.ModeDir != 0 || mode&fs.ModeSymlink != 0 { if mode := fi.Mode(); mode&fs.ModeDir != 0 || mode&fs.ModeSymlink != 0 {
err = syscall.EISDIR err = msg.WrapErr(syscall.EISDIR,
fmt.Sprintf("path %q is a directory", name))
} }
return err return err
} }
var hostProc = newProcPats(hostPath)
func newProcPats(prefix string) *procPaths {
return &procPaths{prefix + "/proc", prefix + "/proc/self"}
}
type procPaths struct {
prefix string
self string
}
func (p *procPaths) stdout() string { return p.self + "/fd/1" }
func (p *procPaths) fd(fd int) string { return p.self + "/fd/" + strconv.Itoa(fd) }
func (p *procPaths) mountinfo(f func(d *vfs.MountInfoDecoder) error) error {
if r, err := os.Open(p.self + "/mountinfo"); err != nil {
return msg.WrapErr(err, err.Error())
} else {
d := vfs.NewMountInfoDecoder(r)
err0 := f(d)
if err = r.Close(); err != nil {
return wrapErrSuffix(err,
"cannot close mountinfo:")
} else if err = d.Err(); err != nil {
return wrapErrSuffix(err,
"cannot parse mountinfo:")
}
return err0
}
}

View File

@ -93,7 +93,7 @@ func TestExport(t *testing.T) {
t.Errorf("Close: error = %v", err) t.Errorf("Close: error = %v", err)
return return
} }
if got := digest.Sum(nil); slices.Compare(got, tc.want) != 0 { if got := digest.Sum(nil); !slices.Equal(got, tc.want) {
t.Fatalf("Export() hash = %x, want %x", t.Fatalf("Export() hash = %x, want %x",
got, tc.want) got, tc.want)
return return
@ -111,11 +111,14 @@ func TestExport(t *testing.T) {
t.Run("close partial read", func(t *testing.T) { t.Run("close partial read", func(t *testing.T) {
e := seccomp.New(0) e := seccomp.New(0)
if _, err := e.Read(make([]byte, 0)); err != nil { if _, err := e.Read(nil); err != nil {
t.Errorf("Read: error = %v", err) t.Errorf("Read: error = %v", err)
return return
} }
if err := e.Close(); err == nil || !errors.Is(err, syscall.ECANCELED) || !errors.Is(err, syscall.EBADF) { // the underlying implementation uses buffered io, so the outcome of this is nondeterministic;
// that is not harmful however, so both outcomes are checked for here
if err := e.Close(); err != nil &&
(!errors.Is(err, syscall.ECANCELED) || !errors.Is(err, syscall.EBADF)) {
t.Errorf("Close: error = %v", err) t.Errorf("Close: error = %v", err)
return return
} }

View File

@ -6,6 +6,8 @@ import (
"math" "math"
"os" "os"
"path" "path"
"path/filepath"
"slices"
"syscall" "syscall"
"unsafe" "unsafe"
) )
@ -14,20 +16,77 @@ 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.
type BindMount struct { type BindMount struct {
Source, Target string Source, SourceFinal, Target string
Flags int Flags int
} }
func (b *BindMount) apply(*InitParams) error { const (
if !path.IsAbs(b.Source) || !path.IsAbs(b.Target) { BindOptional = 1 << iota
BindWritable
BindDevice
)
func (b *BindMount) early(*Params) error {
if !path.IsAbs(b.Source) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", b.Source))
}
if v, err := filepath.EvalSymlinks(b.Source); err != nil {
if os.IsNotExist(err) && b.Flags&BindOptional != 0 {
b.SourceFinal = "\x00"
return nil
}
return msg.WrapErr(err, err.Error())
} else {
b.SourceFinal = v
return nil
}
}
func (b *BindMount) apply(*Params) error {
if b.SourceFinal == "\x00" {
if b.Flags&BindOptional == 0 {
// unreachable
return syscall.EBADE
}
return nil
}
if !path.IsAbs(b.SourceFinal) || !path.IsAbs(b.Target) {
return msg.WrapErr(syscall.EBADE, return msg.WrapErr(syscall.EBADE,
"path is not absolute") "path is not absolute")
} }
return bindMount(b.Source, b.Target, b.Flags)
source := toHost(b.SourceFinal)
target := toSysroot(b.Target)
// this perm value emulates bwrap behaviour as it clears bits from 0755 based on
// op->perms which is never set for any bind setup op so always results in 0700
if fi, err := os.Stat(source); err != nil {
return msg.WrapErr(err, err.Error())
} else if fi.IsDir() {
if err = os.MkdirAll(target, 0700); err != nil {
return msg.WrapErr(err, err.Error())
}
} else if err = ensureFile(target, 0444, 0700); err != nil {
return err
}
var flags uintptr = syscall.MS_REC
if b.Flags&BindWritable == 0 {
flags |= syscall.MS_RDONLY
}
if b.Flags&BindDevice == 0 {
flags |= syscall.MS_NODEV
}
return hostProc.bindMount(source, target, flags, b.SourceFinal == b.Target)
} }
func (b *BindMount) Is(op Op) bool { vb, ok := op.(*BindMount); return ok && *b == *vb } func (b *BindMount) Is(op Op) bool { vb, ok := op.(*BindMount); return ok && *b == *vb }
func (*BindMount) prefix() string { return "mounting" }
func (b *BindMount) String() string { func (b *BindMount) String() string {
if b.Source == b.Target { if b.Source == b.Target {
return fmt.Sprintf("%q flags %#x", b.Source, b.Flags) return fmt.Sprintf("%q flags %#x", b.Source, b.Flags)
@ -35,54 +94,70 @@ func (b *BindMount) String() string {
return fmt.Sprintf("%q on %q flags %#x", b.Source, b.Target, b.Flags&BindWritable) return fmt.Sprintf("%q on %q flags %#x", b.Source, b.Target, b.Flags&BindWritable)
} }
func (f *Ops) Bind(source, target string, flags int) *Ops { func (f *Ops) Bind(source, target string, flags int) *Ops {
*f = append(*f, &BindMount{source, target, flags | BindRecursive}) *f = append(*f, &BindMount{source, "", target, flags})
return f return f
} }
func init() { gob.Register(new(MountProc)) } func init() { gob.Register(new(MountProc)) }
// MountProc mounts a private proc instance on container Path. // MountProc mounts a private instance of proc.
type MountProc struct { type MountProc string
Path string
}
func (p *MountProc) apply(*InitParams) error { func (p MountProc) early(*Params) error { return nil }
if !path.IsAbs(p.Path) { func (p MountProc) apply(*Params) error {
v := string(p)
if !path.IsAbs(v) {
return msg.WrapErr(syscall.EBADE, return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", p.Path)) fmt.Sprintf("path %q is not absolute", v))
} }
target := toSysroot(p.Path) target := toSysroot(v)
if err := os.MkdirAll(target, 0755); err != nil { if err := os.MkdirAll(target, 0755); err != nil {
return msg.WrapErr(err, err.Error()) return msg.WrapErr(err, err.Error())
} }
return wrapErrSuffix(syscall.Mount("proc", target, "proc", return wrapErrSuffix(syscall.Mount("proc", target, "proc",
syscall.MS_NOSUID|syscall.MS_NOEXEC|syscall.MS_NODEV, ""), syscall.MS_NOSUID|syscall.MS_NOEXEC|syscall.MS_NODEV, ""),
fmt.Sprintf("cannot mount proc on %q:", p.Path)) fmt.Sprintf("cannot mount proc on %q:", v))
}
func (p MountProc) Is(op Op) bool { vp, ok := op.(MountProc); return ok && p == vp }
func (MountProc) prefix() string { return "mounting" }
func (p MountProc) String() string { return fmt.Sprintf("proc on %q", string(p)) }
func (f *Ops) Proc(dest string) *Ops {
*f = append(*f, MountProc(dest))
return f
} }
func init() { gob.Register(new(MountDev)) } func init() { gob.Register(new(MountDev)) }
// MountDev mounts dev on container Path. // MountDev mounts part of host dev.
type MountDev struct { type MountDev string
Path string
}
func (d *MountDev) apply(params *InitParams) error { func (d MountDev) early(*Params) error { return nil }
if !path.IsAbs(d.Path) { func (d MountDev) apply(params *Params) error {
v := string(d)
if !path.IsAbs(v) {
return msg.WrapErr(syscall.EBADE, return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", d.Path)) fmt.Sprintf("path %q is not absolute", v))
} }
target := toSysroot(d.Path) target := toSysroot(v)
if err := mountTmpfs("devtmpfs", d.Path, 0, 0755); err != nil { if err := mountTmpfs("devtmpfs", v, 0, 0755); err != nil {
return err return err
} }
for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} { for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} {
if err := bindMount( targetPath := toSysroot(path.Join(v, name))
"/dev/"+name, path.Join(d.Path, name), if err := ensureFile(targetPath, 0444, 0755); err != nil {
BindSource|BindDevices, return err
}
if err := hostProc.bindMount(
toHost("/dev/"+name),
targetPath,
0,
true,
); err != nil { ); err != nil {
return err return err
} }
@ -125,9 +200,17 @@ func (d *MountDev) apply(params *InitParams) error {
syscall.SYS_IOCTL, 1, syscall.TIOCGWINSZ, syscall.SYS_IOCTL, 1, syscall.TIOCGWINSZ,
uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&buf[0])),
); errno == 0 { ); errno == 0 {
if err := bindMount( consolePath := toSysroot(path.Join(v, "console"))
"/proc/self/fd/1", path.Join(d.Path, "console"), if err := ensureFile(consolePath, 0444, 0755); err != nil {
BindDevices, return err
}
if name, err := os.Readlink(hostProc.stdout()); err != nil {
return msg.WrapErr(err, err.Error())
} else if err = hostProc.bindMount(
toHost(name),
consolePath,
0,
false,
); err != nil { ); err != nil {
return err return err
} }
@ -137,17 +220,42 @@ func (d *MountDev) apply(params *InitParams) error {
return nil return nil
} }
func (d *MountDev) Is(op Op) bool { vd, ok := op.(*MountDev); return ok && *d == *vd } func (d MountDev) Is(op Op) bool { vd, ok := op.(MountDev); return ok && d == vd }
func (d *MountDev) String() string { return fmt.Sprintf("dev on %q", d.Path) } func (MountDev) prefix() string { return "mounting" }
func (d MountDev) String() string { return fmt.Sprintf("dev on %q", string(d)) }
func (f *Ops) Dev(dest string) *Ops { func (f *Ops) Dev(dest string) *Ops {
*f = append(*f, &MountDev{dest}) *f = append(*f, MountDev(dest))
return f return f
} }
func (p *MountProc) Is(op Op) bool { vp, ok := op.(*MountProc); return ok && *p == *vp } func init() { gob.Register(new(MountMqueue)) }
func (p *MountProc) String() string { return fmt.Sprintf("proc on %q", p.Path) }
func (f *Ops) Proc(dest string) *Ops { // MountMqueue mounts a private mqueue instance on container Path.
*f = append(*f, &MountProc{dest}) type MountMqueue string
func (m MountMqueue) early(*Params) error { return nil }
func (m MountMqueue) apply(*Params) error {
v := string(m)
if !path.IsAbs(v) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", v))
}
target := toSysroot(v)
if err := os.MkdirAll(target, 0755); err != nil {
return msg.WrapErr(err, err.Error())
}
return wrapErrSuffix(syscall.Mount("mqueue", target, "mqueue",
syscall.MS_NOSUID|syscall.MS_NOEXEC|syscall.MS_NODEV, ""),
fmt.Sprintf("cannot mount mqueue on %q:", v))
}
func (m MountMqueue) Is(op Op) bool { vm, ok := op.(MountMqueue); return ok && m == vm }
func (MountMqueue) prefix() string { return "mounting" }
func (m MountMqueue) String() string { return fmt.Sprintf("mqueue on %q", string(m)) }
func (f *Ops) Mqueue(dest string) *Ops {
*f = append(*f, MountMqueue(dest))
return f return f
} }
@ -160,7 +268,8 @@ type MountTmpfs struct {
Perm os.FileMode Perm os.FileMode
} }
func (t *MountTmpfs) apply(*InitParams) error { func (t *MountTmpfs) early(*Params) error { return nil }
func (t *MountTmpfs) apply(*Params) error {
if !path.IsAbs(t.Path) { if !path.IsAbs(t.Path) {
return msg.WrapErr(syscall.EBADE, return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", t.Path)) fmt.Sprintf("path %q is not absolute", t.Path))
@ -173,8 +282,133 @@ func (t *MountTmpfs) apply(*InitParams) error {
} }
func (t *MountTmpfs) Is(op Op) bool { vt, ok := op.(*MountTmpfs); return ok && *t == *vt } func (t *MountTmpfs) Is(op Op) bool { vt, ok := op.(*MountTmpfs); return ok && *t == *vt }
func (*MountTmpfs) prefix() string { return "mounting" }
func (t *MountTmpfs) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) } func (t *MountTmpfs) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }
func (f *Ops) Tmpfs(dest string, size int, perm os.FileMode) *Ops { func (f *Ops) Tmpfs(dest string, size int, perm os.FileMode) *Ops {
*f = append(*f, &MountTmpfs{dest, size, perm}) *f = append(*f, &MountTmpfs{dest, size, perm})
return f return f
} }
func init() { gob.Register(new(Symlink)) }
// Symlink creates a symlink in the container filesystem.
type Symlink [2]string
func (l *Symlink) early(*Params) error { return nil }
func (l *Symlink) apply(*Params) error {
// symlink target is an arbitrary path value, so only validate link name here
if !path.IsAbs(l[1]) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", l[1]))
}
target := toSysroot(l[1])
if err := ensureFile(target, 0444, 0755); err != nil {
return err
}
if err := os.Remove(target); err != nil {
return msg.WrapErr(err, err.Error())
}
if err := os.Symlink(l[0], target); err != nil {
return msg.WrapErr(err, err.Error())
}
return nil
}
func (l *Symlink) Is(op Op) bool { vl, ok := op.(*Symlink); return ok && *l == *vl }
func (*Symlink) prefix() string { return "creating" }
func (l *Symlink) String() string { return fmt.Sprintf("symlink on %q target %q", l[1], l[0]) }
func (f *Ops) Link(target, linkName string) *Ops {
*f = append(*f, &Symlink{target, linkName})
return f
}
func init() { gob.Register(new(Mkdir)) }
// Mkdir creates a directory in the container filesystem.
type Mkdir struct {
Path string
Perm os.FileMode
}
func (m *Mkdir) early(*Params) error { return nil }
func (m *Mkdir) apply(*Params) error {
if !path.IsAbs(m.Path) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", m.Path))
}
if err := os.MkdirAll(toSysroot(m.Path), m.Perm); err != nil {
return msg.WrapErr(err, err.Error())
}
return nil
}
func (m *Mkdir) Is(op Op) bool { vm, ok := op.(*Mkdir); return ok && m == vm }
func (*Mkdir) prefix() string { return "creating" }
func (m *Mkdir) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) }
func (f *Ops) Mkdir(dest string, perm os.FileMode) *Ops {
*f = append(*f, &Mkdir{dest, perm})
return f
}
func init() { gob.Register(new(Tmpfile)) }
// Tmpfile places a file in container Path containing Data.
type Tmpfile struct {
Path string
Data []byte
}
func (t *Tmpfile) early(*Params) error { return nil }
func (t *Tmpfile) apply(*Params) error {
if !path.IsAbs(t.Path) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", t.Path))
}
var tmpPath string
if f, err := os.CreateTemp("/", "tmp.*"); err != nil {
return msg.WrapErr(err, err.Error())
} else if _, err = f.Write(t.Data); err != nil {
return wrapErrSuffix(err,
"cannot write to intermediate file:")
} else if err = f.Close(); err != nil {
return wrapErrSuffix(err,
"cannot close intermediate file:")
} else {
tmpPath = f.Name()
}
target := toSysroot(t.Path)
if err := ensureFile(target, 0444, 0755); err != nil {
return err
} else if err = hostProc.bindMount(
tmpPath,
target,
syscall.MS_RDONLY|syscall.MS_NODEV,
false,
); err != nil {
return err
} else if err = os.Remove(tmpPath); err != nil {
return msg.WrapErr(err, err.Error())
}
return nil
}
func (t *Tmpfile) Is(op Op) bool {
vt, ok := op.(*Tmpfile)
return ok && t.Path == vt.Path && slices.Equal(t.Data, vt.Data)
}
func (*Tmpfile) prefix() string { return "placing" }
func (t *Tmpfile) String() string {
return fmt.Sprintf("tmpfile %q (%d bytes)", t.Path, len(t.Data))
}
func (f *Ops) Place(name string, data []byte) *Ops { *f = append(*f, &Tmpfile{name, data}); return f }
func (f *Ops) PlaceP(name string, dataP **[]byte) *Ops {
t := &Tmpfile{Path: name}
*dataP = &t.Data
*f = append(*f, t)
return f
}

View File

@ -2,6 +2,12 @@ package sandbox
import "syscall" import "syscall"
const (
O_PATH = 0x200000
PR_SET_NO_NEW_PRIVS = 0x26
CAP_SYS_ADMIN = 0x15
)
const ( const (
SUID_DUMP_DISABLE = iota SUID_DUMP_DISABLE = iota
SUID_DUMP_USER SUID_DUMP_USER
@ -16,14 +22,6 @@ func SetDumpable(dumpable uintptr) error {
return nil return nil
} }
func SetPdeathsig(sig syscall.Signal) error {
if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_PDEATHSIG, uintptr(sig), 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.

30
sandbox/vfs/mangle.go Normal file
View File

@ -0,0 +1,30 @@
package vfs
import "strings"
func Unmangle(s string) string {
if !strings.ContainsRune(s, '\\') {
return s
}
v := make([]byte, len(s))
var (
j int
c byte
)
for i := 0; i < len(s); i++ {
c = s[i]
if c == '\\' && len(s) > i+3 &&
(s[i+1] == '0' || s[i+1] == '1') &&
(s[i+2] >= '0' && s[i+2] <= '7') &&
(s[i+3] >= '0' && s[i+3] <= '7') {
c = ((s[i+1] - '0') << 6) |
((s[i+2] - '0') << 3) |
(s[i+3] - '0')
i += 3
}
v[j] = c
j++
}
return string(v[:j])
}

View File

@ -0,0 +1,27 @@
package vfs_test
import (
"testing"
"git.gensokyo.uk/security/fortify/sandbox/vfs"
)
func TestUnmangle(t *testing.T) {
testCases := []struct {
want string
sample string
}{
{`\, `, `\134\054\040`},
{`(10) source -- maybe empty string`, `(10)\040source\040--\040maybe empty string`},
}
for _, tc := range testCases {
t.Run(tc.want, func(t *testing.T) {
got := vfs.Unmangle(tc.sample)
if got != tc.want {
t.Errorf("Unmangle: %q, want %q",
got, tc.want)
}
})
}
}

260
sandbox/vfs/mountinfo.go Normal file
View File

@ -0,0 +1,260 @@
// Package vfs provides bindings and iterators over proc_pid_mountinfo(5).
package vfs
import (
"bufio"
"errors"
"fmt"
"io"
"iter"
"slices"
"strconv"
"strings"
"syscall"
)
const (
MS_NOSYMFOLLOW = 0x100
)
var (
ErrMountInfoFields = errors.New("unexpected field count")
ErrMountInfoEmpty = errors.New("unexpected empty field")
ErrMountInfoDevno = errors.New("bad maj:min field")
ErrMountInfoSep = errors.New("bad optional fields separator")
)
type (
// A MountInfoDecoder reads and decodes proc_pid_mountinfo(5) entries from an input stream.
MountInfoDecoder struct {
s *bufio.Scanner
m *MountInfo
current *MountInfo
parseErr error
complete bool
}
// MountInfo represents the contents of a proc_pid_mountinfo(5) document.
MountInfo struct {
Next *MountInfo
MountInfoEntry
}
// MountInfoEntry represents a proc_pid_mountinfo(5) entry.
MountInfoEntry struct {
// mount ID: a unique ID for the mount (may be reused after umount(2)).
ID int `json:"id"`
// parent ID: the ID of the parent mount (or of self for the root of this mount namespace's mount tree).
Parent int `json:"parent"`
// major:minor: the value of st_dev for files on this filesystem (see stat(2)).
Devno DevT `json:"devno"`
// root: the pathname of the directory in the filesystem which forms the root of this mount.
Root string `json:"root"`
// mount point: the pathname of the mount point relative to the process's root directory.
Target string `json:"target"`
// mount options: per-mount options (see mount(2)).
VfsOptstr string `json:"vfs_optstr"`
// optional fields: zero or more fields of the form "tag[:value]"; see below.
// separator: the end of the optional fields is marked by a single hyphen.
OptFields []string `json:"opt_fields"`
// filesystem type: the filesystem type in the form "type[.subtype]".
FsType string `json:"fstype"`
// mount source: filesystem-specific information or "none".
Source string `json:"source"`
// super options: per-superblock options (see mount(2)).
FsOptstr string `json:"fs_optstr"`
}
DevT [2]int
)
// Flags interprets VfsOptstr and returns the resulting flags and unmatched options.
func (e *MountInfoEntry) Flags() (flags uintptr, unmatched []string) {
for _, s := range strings.Split(e.VfsOptstr, ",") {
switch s {
case "rw":
case "ro":
flags |= syscall.MS_RDONLY
case "nosuid":
flags |= syscall.MS_NOSUID
case "nodev":
flags |= syscall.MS_NODEV
case "noexec":
flags |= syscall.MS_NOEXEC
case "nosymfollow":
flags |= MS_NOSYMFOLLOW
case "noatime":
flags |= syscall.MS_NOATIME
case "nodiratime":
flags |= syscall.MS_NODIRATIME
case "relatime":
flags |= syscall.MS_RELATIME
default:
unmatched = append(unmatched, s)
}
}
return
}
// NewMountInfoDecoder returns a new decoder that reads from r.
//
// The decoder introduces its own buffering and may read data from r beyond the mountinfo entries requested.
func NewMountInfoDecoder(r io.Reader) *MountInfoDecoder {
return &MountInfoDecoder{s: bufio.NewScanner(r)}
}
func (d *MountInfoDecoder) Decode(v **MountInfo) (err error) {
for d.scan() {
}
err = d.Err()
if err == nil {
*v = d.m
}
return
}
// Entries returns an iterator over mountinfo entries.
func (d *MountInfoDecoder) Entries() iter.Seq[*MountInfoEntry] {
return func(yield func(*MountInfoEntry) bool) {
for cur := d.m; cur != nil; cur = cur.Next {
if !yield(&cur.MountInfoEntry) {
return
}
}
for d.scan() {
if !yield(&d.current.MountInfoEntry) {
return
}
}
}
}
func (d *MountInfoDecoder) Err() error {
if err := d.s.Err(); err != nil {
return err
}
return d.parseErr
}
func (d *MountInfoDecoder) scan() bool {
if d.complete {
return false
}
if !d.s.Scan() {
d.complete = true
return false
}
m := new(MountInfo)
if err := parseMountInfoLine(d.s.Text(), &m.MountInfoEntry); err != nil {
d.parseErr = err
d.complete = true
return false
}
if d.current == nil {
d.m = m
d.current = d.m
} else {
d.current.Next = m
d.current = d.current.Next
}
return true
}
func parseMountInfoLine(s string, ent *MountInfoEntry) error {
// prevent proceeding with misaligned fields due to optional fields
f := strings.Split(s, " ")
if len(f) < 10 {
return ErrMountInfoFields
}
// 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
// (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11)
// (1) id
if id, err := strconv.Atoi(f[0]); err != nil { // 0
return err
} else {
ent.ID = id
}
// (2) parent
if parent, err := strconv.Atoi(f[1]); err != nil { // 1
return err
} else {
ent.Parent = parent
}
// (3) maj:min
if n, err := fmt.Sscanf(f[2], "%d:%d", &ent.Devno[0], &ent.Devno[1]); err != nil {
return err
} else if n != 2 {
// unreachable
return ErrMountInfoDevno
}
// (4) mountroot
ent.Root = Unmangle(f[3])
if ent.Root == "" {
return ErrMountInfoEmpty
}
// (5) target
ent.Target = Unmangle(f[4])
if ent.Target == "" {
return ErrMountInfoEmpty
}
// (6) vfs options (fs-independent)
ent.VfsOptstr = Unmangle(f[5])
if ent.VfsOptstr == "" {
return ErrMountInfoEmpty
}
// (7) optional fields, terminated by " - "
i := len(f) - 4
ent.OptFields = f[6:i]
// (8) optional fields end marker
if f[i] != "-" {
return ErrMountInfoSep
}
i++
// (9) FS type
ent.FsType = Unmangle(f[i])
if ent.FsType == "" {
return ErrMountInfoEmpty
}
i++
// (10) source -- maybe empty string
ent.Source = Unmangle(f[i])
i++
// (11) fs options (fs specific)
ent.FsOptstr = Unmangle(f[i])
return nil
}
func (e *MountInfoEntry) EqualWithIgnore(want *MountInfoEntry, ignore string) bool {
return (e.ID == want.ID || want.ID == -1) &&
(e.Parent == want.Parent || want.Parent == -1) &&
(e.Devno == want.Devno || (want.Devno[0] == -1 && want.Devno[1] == -1)) &&
(e.Root == want.Root || want.Root == ignore) &&
(e.Target == want.Target || want.Target == ignore) &&
(e.VfsOptstr == want.VfsOptstr || want.VfsOptstr == ignore) &&
(slices.Equal(e.OptFields, want.OptFields) || (len(want.OptFields) == 1 && want.OptFields[0] == ignore)) &&
(e.FsType == want.FsType || want.FsType == ignore) &&
(e.Source == want.Source || want.Source == ignore) &&
(e.FsOptstr == want.FsOptstr || want.FsOptstr == ignore)
}
func (e *MountInfoEntry) String() string {
return fmt.Sprintf("%d %d %d:%d %s %s %s %s %s %s %s",
e.ID, e.Parent, e.Devno[0], e.Devno[1], e.Root, e.Target, e.VfsOptstr,
strings.Join(append(e.OptFields, "-"), " "), e.FsType, e.Source, e.FsOptstr)
}

View File

@ -0,0 +1,404 @@
package vfs_test
import (
"encoding/json"
"errors"
"iter"
"path"
"reflect"
"slices"
"strconv"
"strings"
"syscall"
"testing"
"git.gensokyo.uk/security/fortify/sandbox/vfs"
)
func TestMountInfo(t *testing.T) {
testCases := []mountInfoTest{
{"count", sampleMountinfoBase + `
21 20 0:53/ /mnt/test rw,relatime - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoFields, "", nil, nil, nil},
{"sep", sampleMountinfoBase + `
21 20 0:53 / /mnt/test rw,relatime shared:212 _ tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoSep, "", nil, nil, nil},
{"id", sampleMountinfoBase + `
id 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
strconv.ErrSyntax, "", nil, nil, nil},
{"parent", sampleMountinfoBase + `
21 parent 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
strconv.ErrSyntax, "", nil, nil, nil},
{"devno", sampleMountinfoBase + `
21 20 053 / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
nil, "unexpected EOF", nil, nil, nil},
{"maj", sampleMountinfoBase + `
21 20 maj:53 / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
nil, "expected integer", nil, nil, nil},
{"min", sampleMountinfoBase + `
21 20 0:min / /mnt/test rw,relatime shared:212 - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
nil, "expected integer", nil, nil, nil},
{"mountroot", sampleMountinfoBase + `
21 20 0:53 /mnt/test rw,relatime - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoEmpty, "", nil, nil, nil},
{"target", sampleMountinfoBase + `
21 20 0:53 / rw,relatime - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoEmpty, "", nil, nil, nil},
{"vfs options", sampleMountinfoBase + `
21 20 0:53 / /mnt/test - tmpfs rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoEmpty, "", nil, nil, nil},
{"FS type", sampleMountinfoBase + `
21 20 0:53 / /mnt/test rw,relatime - rw
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755`,
vfs.ErrMountInfoEmpty, "", nil, nil, nil},
{"base", sampleMountinfoBase, nil, "", []*wantMountInfo{
m(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", syscall.MS_RELATIME, nil),
m(16, 20, 0, 15, "/", "/sys", "rw,relatime", o(), "sysfs", "/sys", "rw", syscall.MS_RELATIME, nil),
m(17, 20, 0, 5, "/", "/dev", "rw,relatime", o(), "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755", syscall.MS_RELATIME, nil),
m(18, 17, 0, 10, "/", "/dev/pts", "rw,relatime", o(), "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000", syscall.MS_RELATIME, nil),
m(19, 17, 0, 16, "/", "/dev/shm", "rw,relatime", o(), "tmpfs", "tmpfs", "rw", syscall.MS_RELATIME, nil),
m(20, 1, 8, 4, "/", "/", "ro,noatime,nodiratime,meow", o(), "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered", syscall.MS_RDONLY|syscall.MS_NOATIME|syscall.MS_NODIRATIME, []string{"meow"}),
},
mn(20, 1, 8, 4, "/", "/", "ro,noatime,nodiratime,meow", o(), "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered", false,
mn(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", false, nil,
mn(16, 20, 0, 15, "/", "/sys", "rw,relatime", o(), "sysfs", "/sys", "rw", false, nil,
mn(17, 20, 0, 5, "/", "/dev", "rw,relatime", o(), "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755", false,
mn(18, 17, 0, 10, "/", "/dev/pts", "rw,relatime", o(), "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000", false, nil,
mn(19, 17, 0, 16, "/", "/dev/shm", "rw,relatime", o(), "tmpfs", "tmpfs", "rw", false, nil, nil)),
nil))), nil), func(n *vfs.MountInfoNode) []*vfs.MountInfoNode {
return []*vfs.MountInfoNode{
n,
n.FirstChild,
n.FirstChild.NextSibling,
n.FirstChild.NextSibling.NextSibling,
n.FirstChild.NextSibling.NextSibling.FirstChild,
n.FirstChild.NextSibling.NextSibling.FirstChild.NextSibling,
}
}},
{"sample", sampleMountinfo, nil, "", []*wantMountInfo{
m(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", syscall.MS_RELATIME, nil),
m(16, 20, 0, 15, "/", "/sys", "rw,relatime", o(), "sysfs", "/sys", "rw", syscall.MS_RELATIME, nil),
m(17, 20, 0, 5, "/", "/dev", "rw,relatime", o(), "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755", syscall.MS_RELATIME, nil),
m(18, 17, 0, 10, "/", "/dev/pts", "rw,relatime", o(), "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000", syscall.MS_RELATIME, nil),
m(19, 17, 0, 16, "/", "/dev/shm", "rw,relatime", o(), "tmpfs", "tmpfs", "rw", syscall.MS_RELATIME, nil),
m(20, 1, 8, 4, "/", "/", "rw,noatime", o(), "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered", syscall.MS_NOATIME, nil),
m(21, 16, 0, 17, "/", "/sys/fs/cgroup", "rw,nosuid,nodev,noexec,relatime", o(), "tmpfs", "tmpfs", "rw,mode=755", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
m(22, 21, 0, 18, "/", "/sys/fs/cgroup/systemd", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
m(23, 21, 0, 19, "/", "/sys/fs/cgroup/cpuset", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,cpuset", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
m(24, 21, 0, 20, "/", "/sys/fs/cgroup/ns", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,ns", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
m(25, 21, 0, 21, "/", "/sys/fs/cgroup/cpu", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,cpu", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
m(26, 21, 0, 22, "/", "/sys/fs/cgroup/cpuacct", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,cpuacct", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
m(27, 21, 0, 23, "/", "/sys/fs/cgroup/memory", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,memory", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
m(28, 21, 0, 24, "/", "/sys/fs/cgroup/devices", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,devices", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
m(29, 21, 0, 25, "/", "/sys/fs/cgroup/freezer", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,freezer", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
m(30, 21, 0, 26, "/", "/sys/fs/cgroup/net_cls", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,net_cls", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
m(31, 21, 0, 27, "/", "/sys/fs/cgroup/blkio", "rw,nosuid,nodev,noexec,relatime", o(), "cgroup", "cgroup", "rw,blkio", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_NOEXEC|syscall.MS_RELATIME, nil),
m(32, 16, 0, 28, "/", "/sys/kernel/security", "rw,relatime", o(), "autofs", "systemd-1", "rw,fd=22,pgrp=1,timeout=300,minproto=5,maxproto=5,direct", syscall.MS_RELATIME, nil),
m(33, 17, 0, 29, "/", "/dev/hugepages", "rw,relatime", o(), "autofs", "systemd-1", "rw,fd=23,pgrp=1,timeout=300,minproto=5,maxproto=5,direct", syscall.MS_RELATIME, nil),
m(34, 16, 0, 30, "/", "/sys/kernel/debug", "rw,relatime", o(), "autofs", "systemd-1", "rw,fd=24,pgrp=1,timeout=300,minproto=5,maxproto=5,direct", syscall.MS_RELATIME, nil),
m(35, 15, 0, 31, "/", "/proc/sys/fs/binfmt_misc", "rw,relatime", o(), "autofs", "systemd-1", "rw,fd=25,pgrp=1,timeout=300,minproto=5,maxproto=5,direct", syscall.MS_RELATIME, nil),
m(36, 17, 0, 32, "/", "/dev/mqueue", "rw,relatime", o(), "autofs", "systemd-1", "rw,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct", syscall.MS_RELATIME, nil),
m(37, 15, 0, 14, "/", "/proc/bus/usb", "rw,relatime", o(), "usbfs", "/proc/bus/usb", "rw", syscall.MS_RELATIME, nil),
m(38, 33, 0, 33, "/", "/dev/hugepages", "rw,relatime", o(), "hugetlbfs", "hugetlbfs", "rw", syscall.MS_RELATIME, nil),
m(39, 36, 0, 12, "/", "/dev/mqueue", "rw,relatime", o(), "mqueue", "mqueue", "rw", syscall.MS_RELATIME, nil),
m(40, 20, 8, 6, "/", "/boot", "rw,noatime", o(), "ext3", "/dev/sda6", "rw,errors=continue,barrier=0,data=ordered", syscall.MS_NOATIME, nil),
m(41, 20, 253, 0, "/", "/home/kzak", "rw,noatime", o(), "ext4", "/dev/mapper/kzak-home", "rw,barrier=1,data=ordered", syscall.MS_NOATIME, nil),
m(42, 35, 0, 34, "/", "/proc/sys/fs/binfmt_misc", "rw,relatime", o(), "binfmt_misc", "none", "rw", syscall.MS_RELATIME, nil),
m(43, 16, 0, 35, "/", "/sys/fs/fuse/connections", "rw,relatime", o(), "fusectl", "fusectl", "rw", syscall.MS_RELATIME, nil),
m(44, 41, 0, 36, "/", "/home/kzak/.gvfs", "rw,nosuid,nodev,relatime", o(), "fuse.gvfs-fuse-daemon", "gvfs-fuse-daemon", "rw,user_id=500,group_id=500", syscall.MS_NOSUID|syscall.MS_NODEV|syscall.MS_RELATIME, nil),
m(45, 20, 0, 37, "/", "/var/lib/nfs/rpc_pipefs", "rw,relatime", o(), "rpc_pipefs", "sunrpc", "rw", syscall.MS_RELATIME, nil),
m(47, 20, 0, 38, "/", "/mnt/sounds", "rw,relatime", o(), "cifs", "//foo.home/bar/", "rw,unc=\\\\foo.home\\bar,username=kzak,domain=SRGROUP,uid=0,noforceuid,gid=0,noforcegid,addr=192.168.111.1,posixpaths,serverino,acl,rsize=16384,wsize=57344", syscall.MS_RELATIME, nil),
m(49, 20, 0, 56, "/", "/mnt/test/foobar", "rw,relatime,nosymfollow", o("shared:323"), "tmpfs", "tmpfs", "rw", syscall.MS_RELATIME|vfs.MS_NOSYMFOLLOW, nil),
}, nil, nil},
{"sample nosrc", sampleMountinfoNoSrc, nil, "", []*wantMountInfo{
m(15, 20, 0, 3, "/", "/proc", "rw,relatime", o(), "proc", "/proc", "rw", syscall.MS_RELATIME, nil),
m(16, 20, 0, 15, "/", "/sys", "rw,relatime", o(), "sysfs", "/sys", "rw", syscall.MS_RELATIME, nil),
m(17, 20, 0, 5, "/", "/dev", "rw,relatime", o(), "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755", syscall.MS_RELATIME, nil),
m(18, 17, 0, 10, "/", "/dev/pts", "rw,relatime", o(), "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000", syscall.MS_RELATIME, nil),
m(19, 17, 0, 16, "/", "/dev/shm", "rw,relatime", o(), "tmpfs", "tmpfs", "rw", syscall.MS_RELATIME, nil),
m(20, 1, 8, 4, "/", "/", "rw,noatime", o(), "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered", syscall.MS_NOATIME, nil),
m(21, 20, 0, 53, "/", "/mnt/test", "rw,relatime", o("shared:212"), "tmpfs", "", "rw", syscall.MS_RELATIME, nil),
}, nil, nil},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("decode", func(t *testing.T) {
var got *vfs.MountInfo
d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
err := d.Decode(&got)
tc.check(t, d, "Decode",
func(yield func(*vfs.MountInfoEntry) bool) {
for cur := got; cur != nil; cur = cur.Next {
if !yield(&cur.MountInfoEntry) {
return
}
}
}, func() error { return err })
t.Run("reuse", func(t *testing.T) {
tc.check(t, d, "Entries",
d.Entries(), d.Err)
})
})
t.Run("iter", func(t *testing.T) {
d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
tc.check(t, d, "Entries",
d.Entries(), d.Err)
t.Run("reuse", func(t *testing.T) {
tc.check(t, d, "Entries",
d.Entries(), d.Err)
})
})
t.Run("yield", func(t *testing.T) {
d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
v := false
d.Entries()(func(entry *vfs.MountInfoEntry) bool { v = !v; return v })
d.Entries()(func(entry *vfs.MountInfoEntry) bool { return false })
tc.check(t, d, "Entries",
d.Entries(), d.Err)
t.Run("reuse", func(t *testing.T) {
tc.check(t, d, "Entries",
d.Entries(), d.Err)
})
})
})
}
}
type mountInfoTest struct {
name string
sample string
wantErr error
wantError string
want []*wantMountInfo
wantNode *vfs.MountInfoNode
wantCollectF func(n *vfs.MountInfoNode) []*vfs.MountInfoNode
}
func (tc *mountInfoTest) check(t *testing.T, d *vfs.MountInfoDecoder, funcName string,
got iter.Seq[*vfs.MountInfoEntry], gotErr func() error) {
i := 0
for cur := range got {
if i == len(tc.want) {
if funcName != "Decode" && (tc.wantErr != nil || tc.wantError != "") {
continue
}
t.Errorf("%s: got more than %d entries", funcName, len(tc.want))
break
}
if !reflect.DeepEqual(cur, &tc.want[i].MountInfoEntry) {
t.Errorf("%s: entry %d\ngot: %#v\nwant: %#v",
funcName, i, cur, tc.want[i])
}
flags, unmatched := cur.Flags()
if flags != tc.want[i].flags {
t.Errorf("Flags(%q): %#x, want %#x",
cur.VfsOptstr, flags, tc.want[i].flags)
}
if !slices.Equal(unmatched, tc.want[i].unmatched) {
t.Errorf("Flags(%q): unmatched = %#q, want %#q",
cur.VfsOptstr, unmatched, tc.want[i].unmatched)
}
i++
}
if i != len(tc.want) {
t.Errorf("%s: got %d entries, want %d", funcName, i, len(tc.want))
}
if tc.wantErr == nil && tc.wantError == "" && tc.wantCollectF != nil {
t.Run("unfold", func(t *testing.T) {
n, err := d.Unfold("/")
if err != nil {
t.Errorf("Unfold: error = %v", err)
} else {
t.Run("stop", func(t *testing.T) {
v := false
n.Collective()(func(node *vfs.MountInfoNode) bool { v = !v; return v })
})
if !reflect.DeepEqual(n, tc.wantNode) {
t.Errorf("Unfold: %s, want %s",
mustMarshal(n), mustMarshal(tc.wantNode))
}
t.Run("collective", func(t *testing.T) {
wantCollect := tc.wantCollectF(n)
if gotCollect := slices.Collect(n.Collective()); !reflect.DeepEqual(gotCollect, wantCollect) {
t.Errorf("Collective: \ngot %#v\nwant %#v",
gotCollect, wantCollect)
}
})
}
})
} else if tc.wantNode != nil || tc.wantCollectF != nil {
panic("invalid test case")
} else if _, err := d.Unfold("/"); !errors.Is(err, tc.wantErr) {
if tc.wantError == "" {
t.Errorf("Unfold: error = %v, wantErr %v",
err, tc.wantErr)
} else if err != nil && err.Error() != tc.wantError {
t.Errorf("Unfold: error = %q, wantError %q",
err, tc.wantError)
}
}
if err := gotErr(); !errors.Is(err, tc.wantErr) {
if tc.wantError == "" {
t.Errorf("%s: error = %v, wantErr %v",
funcName, err, tc.wantErr)
} else if err != nil && err.Error() != tc.wantError {
t.Errorf("%s: error = %q, wantError %q",
funcName, err, tc.wantError)
}
}
}
func mustMarshal(v any) string {
p, err := json.Marshal(v)
if err != nil {
panic(err.Error())
}
return string(p)
}
type wantMountInfo struct {
vfs.MountInfoEntry
flags uintptr
unmatched []string
}
func m(
id, parent, maj, min int, root, target, vfsOptstr string, optFields []string, fsType, source, fsOptstr string,
flags uintptr, unmatched []string,
) *wantMountInfo {
return &wantMountInfo{
vfs.MountInfoEntry{
ID: id,
Parent: parent,
Devno: vfs.DevT{maj, min},
Root: root,
Target: target,
VfsOptstr: vfsOptstr,
OptFields: optFields,
FsType: fsType,
Source: source,
FsOptstr: fsOptstr,
}, flags, unmatched,
}
}
func mn(
id, parent, maj, min int, root, target, vfsOptstr string, optFields []string, fsType, source, fsOptstr string,
covered bool, firstChild, nextSibling *vfs.MountInfoNode,
) *vfs.MountInfoNode {
return &vfs.MountInfoNode{
MountInfoEntry: &vfs.MountInfoEntry{
ID: id,
Parent: parent,
Devno: vfs.DevT{maj, min},
Root: root,
Target: target,
VfsOptstr: vfsOptstr,
OptFields: optFields,
FsType: fsType,
Source: source,
FsOptstr: fsOptstr,
},
FirstChild: firstChild,
NextSibling: nextSibling,
Clean: path.Clean(target),
Covered: covered,
}
}
func o(field ...string) []string {
if field == nil {
return []string{}
}
return field
}
const (
sampleMountinfoBase = `15 20 0:3 / /proc rw,relatime - proc /proc rw
16 20 0:15 / /sys rw,relatime - sysfs /sys rw
17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755
18 17 0:10 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000
19 17 0:16 / /dev/shm rw,relatime - tmpfs tmpfs rw
20 1 8:4 / / ro,noatime,nodiratime,meow - ext3 /dev/sda4 rw,errors=continue,user_xattr,acl,barrier=0,data=ordered`
sampleMountinfo = `15 20 0:3 / /proc rw,relatime - proc /proc rw
16 20 0:15 / /sys rw,relatime - sysfs /sys rw
17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755
18 17 0:10 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000
19 17 0:16 / /dev/shm rw,relatime - tmpfs tmpfs rw
20 1 8:4 / / rw,noatime - ext3 /dev/sda4 rw,errors=continue,user_xattr,acl,barrier=0,data=ordered
21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755
22 21 0:18 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd
23 21 0:19 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuset
24 21 0:20 / /sys/fs/cgroup/ns rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,ns
25 21 0:21 / /sys/fs/cgroup/cpu rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpu
26 21 0:22 / /sys/fs/cgroup/cpuacct rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuacct
27 21 0:23 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,memory
28 21 0:24 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,devices
29 21 0:25 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,freezer
30 21 0:26 / /sys/fs/cgroup/net_cls rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,net_cls
31 21 0:27 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,blkio
32 16 0:28 / /sys/kernel/security rw,relatime - autofs systemd-1 rw,fd=22,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
33 17 0:29 / /dev/hugepages rw,relatime - autofs systemd-1 rw,fd=23,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
34 16 0:30 / /sys/kernel/debug rw,relatime - autofs systemd-1 rw,fd=24,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
35 15 0:31 / /proc/sys/fs/binfmt_misc rw,relatime - autofs systemd-1 rw,fd=25,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
36 17 0:32 / /dev/mqueue rw,relatime - autofs systemd-1 rw,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
37 15 0:14 / /proc/bus/usb rw,relatime - usbfs /proc/bus/usb rw
38 33 0:33 / /dev/hugepages rw,relatime - hugetlbfs hugetlbfs rw
39 36 0:12 / /dev/mqueue rw,relatime - mqueue mqueue rw
40 20 8:6 / /boot rw,noatime - ext3 /dev/sda6 rw,errors=continue,barrier=0,data=ordered
41 20 253:0 / /home/kzak rw,noatime - ext4 /dev/mapper/kzak-home rw,barrier=1,data=ordered
42 35 0:34 / /proc/sys/fs/binfmt_misc rw,relatime - binfmt_misc none rw
43 16 0:35 / /sys/fs/fuse/connections rw,relatime - fusectl fusectl rw
44 41 0:36 / /home/kzak/.gvfs rw,nosuid,nodev,relatime - fuse.gvfs-fuse-daemon gvfs-fuse-daemon rw,user_id=500,group_id=500
45 20 0:37 / /var/lib/nfs/rpc_pipefs rw,relatime - rpc_pipefs sunrpc rw
47 20 0:38 / /mnt/sounds rw,relatime - cifs //foo.home/bar/ rw,unc=\\foo.home\bar,username=kzak,domain=SRGROUP,uid=0,noforceuid,gid=0,noforcegid,addr=192.168.111.1,posixpaths,serverino,acl,rsize=16384,wsize=57344
49 20 0:56 / /mnt/test/foobar rw,relatime,nosymfollow shared:323 - tmpfs tmpfs rw`
sampleMountinfoNoSrc = `15 20 0:3 / /proc rw,relatime - proc /proc rw
16 20 0:15 / /sys rw,relatime - sysfs /sys rw
17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755
18 17 0:10 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000
19 17 0:16 / /dev/shm rw,relatime - tmpfs tmpfs rw
20 1 8:4 / / rw,noatime - ext3 /dev/sda4 rw,errors=continue,user_xattr,acl,barrier=0,data=ordered
21 20 0:53 / /mnt/test rw,relatime shared:212 - tmpfs rw`
)

107
sandbox/vfs/unfold.go Normal file
View File

@ -0,0 +1,107 @@
package vfs
import (
"iter"
"path"
"strings"
"syscall"
)
// MountInfoNode positions a [MountInfoEntry] in its mount hierarchy.
type MountInfoNode struct {
*MountInfoEntry
FirstChild *MountInfoNode `json:"first_child"`
NextSibling *MountInfoNode `json:"next_sibling"`
Clean string `json:"clean"`
Covered bool `json:"covered"`
}
// Collective returns an iterator over visible mountinfo nodes.
func (n *MountInfoNode) Collective() iter.Seq[*MountInfoNode] {
return func(yield func(*MountInfoNode) bool) { n.visit(yield) }
}
func (n *MountInfoNode) visit(yield func(*MountInfoNode) bool) bool {
if !n.Covered && !yield(n) {
return false
}
for cur := n.FirstChild; cur != nil; cur = cur.NextSibling {
if !cur.visit(yield) {
return false
}
}
return true
}
// Unfold unfolds the mount hierarchy and resolves covered paths.
func (d *MountInfoDecoder) Unfold(target string) (*MountInfoNode, error) {
targetClean := path.Clean(target)
var mountinfoSize int
for range d.Entries() {
mountinfoSize++
}
if err := d.Err(); err != nil {
return nil, err
}
mountinfo := make([]*MountInfoNode, mountinfoSize)
// mount ID to index lookup
idIndex := make(map[int]int, mountinfoSize)
// final entry to match target
targetIndex := -1
{
i := 0
for ent := range d.Entries() {
mountinfo[i] = &MountInfoNode{Clean: path.Clean(ent.Target), MountInfoEntry: ent}
idIndex[ent.ID] = i
if mountinfo[i].Clean == targetClean {
targetIndex = i
}
i++
}
}
if targetIndex == -1 {
return nil, syscall.ESTALE
}
for _, cur := range mountinfo {
var parent *MountInfoNode
if p, ok := idIndex[cur.Parent]; !ok {
continue
} else {
parent = mountinfo[p]
}
if !strings.HasPrefix(cur.Clean, targetClean) {
continue
}
if parent.Clean == cur.Clean {
parent.Covered = true
}
covered := false
nsp := &parent.FirstChild
for s := parent.FirstChild; s != nil; s = s.NextSibling {
if strings.HasPrefix(cur.Clean, s.Clean) {
covered = true
break
}
if strings.HasPrefix(s.Clean, cur.Clean) {
*nsp = s.NextSibling
} else {
nsp = &s.NextSibling
}
}
if covered {
continue
}
*nsp = cur
}
return mountinfo[targetIndex], nil
}

View File

@ -0,0 +1,93 @@
package vfs_test
import (
"errors"
"reflect"
"slices"
"strings"
"syscall"
"testing"
"git.gensokyo.uk/security/fortify/sandbox/vfs"
)
func TestUnfold(t *testing.T) {
testCases := []struct {
name string
sample string
target string
wantErr error
want *vfs.MountInfoNode
wantCollectF func(n *vfs.MountInfoNode) []*vfs.MountInfoNode
wantCollectN []string
}{
{
"no match",
sampleMountinfoBase,
"/mnt",
syscall.ESTALE, nil, nil, nil,
},
{
"cover",
`33 1 0:33 / / rw,relatime shared:1 - tmpfs impure rw,size=16777216k,mode=755
37 33 0:32 / /proc rw,nosuid,nodev,noexec,relatime shared:41 - proc proc rw
551 33 0:121 / /mnt rw,relatime shared:666 - tmpfs tmpfs rw
595 551 0:123 / /mnt rw,relatime shared:990 - tmpfs tmpfs rw
611 595 0:142 / /mnt/etc rw,relatime shared:1112 - tmpfs tmpfs rw
625 644 0:142 /passwd /mnt/etc/passwd rw,relatime shared:1112 - tmpfs tmpfs rw
641 625 0:33 /etc/passwd /mnt/etc/passwd rw,relatime shared:1 - tmpfs impure rw,size=16777216k,mode=755
644 611 0:33 /etc/passwd /mnt/etc/passwd rw,relatime shared:1 - tmpfs impure rw,size=16777216k,mode=755
`, "/mnt", nil,
mn(595, 551, 0, 123, "/", "/mnt", "rw,relatime", o("shared:990"), "tmpfs", "tmpfs", "rw", false,
mn(611, 595, 0, 142, "/", "/mnt/etc", "rw,relatime", o("shared:1112"), "tmpfs", "tmpfs", "rw", false,
mn(644, 611, 0, 33, "/etc/passwd", "/mnt/etc/passwd", "rw,relatime", o("shared:1"), "tmpfs", "impure", "rw,size=16777216k,mode=755", true,
mn(625, 644, 0, 142, "/passwd", "/mnt/etc/passwd", "rw,relatime", o("shared:1112"), "tmpfs", "tmpfs", "rw", true,
mn(641, 625, 0, 33, "/etc/passwd", "/mnt/etc/passwd", "rw,relatime", o("shared:1"), "tmpfs", "impure", "rw,size=16777216k,mode=755", false,
nil, nil), nil), nil), nil), nil), func(n *vfs.MountInfoNode) []*vfs.MountInfoNode {
return []*vfs.MountInfoNode{n, n.FirstChild, n.FirstChild.FirstChild.FirstChild.FirstChild}
}, []string{"/mnt", "/mnt/etc", "/mnt/etc/passwd"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
d := vfs.NewMountInfoDecoder(strings.NewReader(tc.sample))
got, err := d.Unfold(tc.target)
if !errors.Is(err, tc.wantErr) {
t.Errorf("Unfold: error = %v, wantErr %v",
err, tc.wantErr)
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("Unfold:\ngot %s\nwant %s",
mustMarshal(got), mustMarshal(tc.want))
}
if err == nil && tc.wantCollectF != nil {
t.Run("collective", func(t *testing.T) {
wantCollect := tc.wantCollectF(got)
gotCollect := slices.Collect(got.Collective())
if !reflect.DeepEqual(gotCollect, wantCollect) {
t.Errorf("Collective: \ngot %#v\nwant %#v",
gotCollect, wantCollect)
}
t.Run("target", func(t *testing.T) {
gotCollectN := slices.Collect[string](func(yield func(v string) bool) {
for _, cur := range gotCollect {
if !yield(cur.Clean) {
return
}
}
})
if !reflect.DeepEqual(gotCollectN, tc.wantCollectN) {
t.Errorf("Collective: got %q, want %q",
gotCollectN, tc.wantCollectN)
}
})
})
}
})
}
}

View File

@ -4,6 +4,12 @@
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 = {
@ -102,21 +108,10 @@
home-manager = _: _: { home.stateVersion = "23.05"; }; home-manager = _: _: { home.stateVersion = "23.05"; };
apps = [ apps = [
{ testCases.preset
name = "check-sandbox"; testCases.tty
verbose = true; testCases.mapuid
share = pkgs.foot;
packages = [ ];
command = "${pkgs.callPackage ./sandbox {
inherit (config.environment.fortify.package) version;
}}";
extraPaths = [
{
src = "/proc/mounts";
dst = "/.fortify/host-mounts";
}
];
}
{ {
name = "ne-foot"; name = "ne-foot";
verbose = true; verbose = true;

View File

@ -1,6 +1,7 @@
{ {
lib, lib,
nixosTest, nixosTest,
buildFHSEnv,
writeShellScriptBin, writeShellScriptBin,
system, system,
@ -12,6 +13,21 @@ nixosTest {
name = "fortify" + (if withRace then "-race" else ""); name = "fortify" + (if withRace then "-race" else "");
nodes.machine = nodes.machine =
{ options, pkgs, ... }: { options, pkgs, ... }:
let
fhs =
let
fortify = options.environment.fortify.package.default;
in
buildFHSEnv {
pname = "fortify-fhs";
inherit (fortify) version;
targetPkgs = _: fortify.targetPkgs;
extraOutputsToInstall = [ "dev" ];
profile = ''
export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH"
'';
};
in
{ {
environment.systemPackages = [ environment.systemPackages = [
# For go tests: # For go tests:
@ -21,7 +37,7 @@ nixosTest {
cp -r "${self.packages.${system}.fortify.src}" "$WORK" cp -r "${self.packages.${system}.fortify.src}" "$WORK"
chmod -R +w "$WORK" chmod -R +w "$WORK"
cd "$WORK" cd "$WORK"
${self.packages.${system}.fhs}/bin/fortify-fhs -c \ ${fhs}/bin/fortify-fhs -c \
'go generate ./... && go test ${if withRace then "-race" else "-count 16"} ./... && touch /tmp/go-test-ok' 'go generate ./... && go test ${if withRace then "-race" else "-count 16"} ./... && touch /tmp/go-test-ok'
'') '')
]; ];

View File

@ -1,3 +1,9 @@
/*
Package sandbox provides utilities for checking sandbox outcome.
This package must never be used outside integration tests, there is a much better native implementation of mountinfo
in the public sandbox/vfs package. Files in this package are excluded by the build system to prevent accidental misuse.
*/
package sandbox package sandbox
import ( import (
@ -16,76 +22,89 @@ var (
func printf(format string, v ...any) { printfFunc(format, v...) } 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...) }
func mustDecode(wantFile string, v any) { type TestCase struct {
if f, err := os.Open(wantFile); err != nil { FS *FS `json:"fs"`
fatalf("cannot open %q: %v", wantFile, err) Mount []*MountinfoEntry `json:"mount"`
} else if err = json.NewDecoder(f).Decode(v); err != nil { Seccomp bool `json:"seccomp"`
fatalf("cannot decode %q: %v", wantFile, err)
} else if err = f.Close(); err != nil {
fatalf("cannot close %q: %v", wantFile, err)
}
} }
func MustAssertMounts(name, hostMountsFile, wantFile string) { type T struct {
hostMounts := make([]*Mntent, 0, 128) FS fs.FS
if err := IterMounts(hostMountsFile, func(e *Mntent) {
hostMounts = append(hostMounts, e)
}); err != nil {
fatalf("cannot parse host mounts: %v", err)
}
var want []Mntent MountsPath string
mustDecode(wantFile, &want)
for i := range want {
if want[i].Opts == "host_passthrough" {
for _, ent := range hostMounts {
if want[i].FSName == ent.FSName {
// special case for tmpfs bind mounts
if want[i].FSName == "tmpfs" && want[i].Dir != ent.Dir {
continue
}
want[i].Opts = ent.Opts
goto out
}
}
fatalf("host passthrough missing %q", want[i].FSName)
out:
}
}
i := 0
if err := IterMounts(name, func(e *Mntent) {
if i == len(want) {
fatalf("got more than %d entries", i)
}
if !e.Is(&want[i]) {
fatalf("entry %d\n got: %s\nwant: %s", i,
e, &want[i])
}
printf("%s", e)
i++
}); err != nil {
fatalf("cannot iterate mounts: %v", err)
}
} }
func MustAssertFS(e fs.FS, wantFile string) { func (t *T) MustCheckFile(wantFilePath string) {
var want *FS var want *TestCase
mustDecode(wantFile, &want) mustDecode(wantFilePath, &want)
if want == nil { t.MustCheck(want)
fatalf("invalid payload") }
}
if err := want.Compare(".", e); err != nil { func (t *T) MustCheck(want *TestCase) {
if want.FS != nil && t.FS != nil {
if err := want.FS.Compare(".", t.FS); err != nil {
fatalf("%v", err) fatalf("%v", err)
} }
} } else {
printf("[SKIP] skipping fs check")
}
func MustAssertSeccomp() { if want.Mount != nil {
var fail bool
m := mustParseMountinfo(t.MountsPath)
i := 0
for ent := range m.Entries() {
if i == len(want.Mount) {
fatalf("got more than %d entries", i)
}
if !ent.EqualWithIgnore(want.Mount[i], "//ignore") {
fail = true
printf("[FAIL] %s", ent)
} else {
printf("[ OK ] %s", ent)
}
i++
}
if err := m.Err(); err != nil {
fatalf("%v", err)
}
if i != len(want.Mount) {
fatalf("got %d entries, want %d", i, len(want.Mount))
}
if fail {
fatalf("[FAIL] some mount points did not match")
}
} else {
printf("[SKIP] skipping mounts check")
}
if want.Seccomp {
if TrySyscalls() != nil { if TrySyscalls() != nil {
os.Exit(1) os.Exit(1)
} }
} else {
printf("[SKIP] skipping seccomp check")
}
}
func mustDecode(wantFilePath string, v any) {
if f, err := os.Open(wantFilePath); err != nil {
fatalf("cannot open %q: %v", wantFilePath, err)
} else if err = json.NewDecoder(f).Decode(v); err != nil {
fatalf("cannot decode %q: %v", wantFilePath, err)
} else if err = f.Close(); err != nil {
fatalf("cannot close %q: %v", wantFilePath, err)
}
}
func mustParseMountinfo(name string) *Mountinfo {
m := NewMountinfo(name)
if err := m.Parse(); err != nil {
fatalf("%v", err)
panic("unreachable")
}
return m
} }

30
test/sandbox/assert.nix Normal file
View File

@ -0,0 +1,30 @@
{
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

@ -0,0 +1,58 @@
{
lib,
callPackage,
foot,
version,
}:
let
fs = mode: dir: data: {
mode = lib.fromHexString mode;
inherit
dir
data
;
};
ignore = "//ignore";
ent = root: target: vfs_optstr: fstype: source: fs_optstr: {
id = -1;
parent = -1;
inherit
root
target
vfs_optstr
fstype
source
fs_optstr
;
};
checkSandbox = callPackage ../. { inherit version; };
callTestCase =
path:
let
tc = import path {
inherit
fs
ent
ignore
;
};
in
{
name = "check-sandbox-${tc.name}";
verbose = true;
inherit (tc) tty mapRealUid;
share = foot;
packages = [ ];
command = builtins.toString (checkSandbox tc.name tc.want);
};
in
{
preset = callTestCase ./preset.nix;
tty = callTestCase ./tty.nix;
mapuid = callTestCase ./mapuid.nix;
}

View File

@ -0,0 +1,221 @@
{
fs,
ent,
ignore,
}:
{
name = "mapuid";
tty = false;
mapRealUid = true;
want = {
fs = fs "dead" {
".fortify" = fs "800001ed" {
etc = fs "800001ed" null null;
} null;
bin = fs "800001ed" { sh = fs "80001ff" null null; } null;
dev = fs "800001ed" {
core = fs "80001ff" null null;
dri = fs "800001ed" {
by-path = fs "800001ed" {
"pci-0000:00:09.0-card" = fs "80001ff" null null;
"pci-0000:00:09.0-render" = fs "80001ff" null null;
} null;
card0 = fs "42001b0" null null;
renderD128 = fs "42001b6" null null;
} null;
fd = fs "80001ff" null null;
full = fs "42001b6" null null;
mqueue = fs "801001ff" { } null;
null = fs "42001b6" null "";
ptmx = fs "80001ff" null null;
pts = fs "800001ed" { ptmx = fs "42001b6" null null; } null;
random = fs "42001b6" null null;
shm = fs "800001ed" { } null;
stderr = fs "80001ff" null null;
stdin = fs "80001ff" null null;
stdout = fs "80001ff" null null;
tty = fs "42001b6" null null;
urandom = fs "42001b6" null null;
zero = fs "42001b6" null null;
} null;
etc = fs "800001c0" {
".clean" = fs "80001ff" null null;
".updated" = fs "80001ff" null null;
"NIXOS" = fs "80001ff" null null;
"X11" = fs "80001ff" null null;
"alsa" = fs "80001ff" null null;
"bashrc" = fs "80001ff" null null;
"binfmt.d" = fs "80001ff" null null;
"dbus-1" = fs "80001ff" null null;
"default" = fs "80001ff" null null;
"dhcpcd.exit-hook" = fs "80001ff" null null;
"fonts" = fs "80001ff" null null;
"fstab" = fs "80001ff" null null;
"fsurc" = fs "80001ff" null null;
"fuse.conf" = fs "80001ff" null null;
"group" = fs "180" null "fortify:x:100:\n";
"host.conf" = fs "80001ff" null null;
"hostname" = fs "80001ff" null null;
"hosts" = fs "80001ff" null null;
"inputrc" = fs "80001ff" null null;
"issue" = fs "80001ff" null null;
"kbd" = fs "80001ff" null null;
"locale.conf" = fs "80001ff" null null;
"login.defs" = fs "80001ff" null null;
"lsb-release" = fs "80001ff" null null;
"lvm" = fs "80001ff" null null;
"machine-id" = fs "80001ff" null null;
"man_db.conf" = fs "80001ff" null null;
"modprobe.d" = fs "80001ff" null null;
"modules-load.d" = fs "80001ff" null null;
"mtab" = fs "80001ff" null null;
"nanorc" = fs "80001ff" null null;
"netgroup" = fs "80001ff" null null;
"nix" = fs "80001ff" null null;
"nixos" = fs "80001ff" null null;
"nscd.conf" = fs "80001ff" null null;
"nsswitch.conf" = fs "80001ff" null null;
"os-release" = fs "80001ff" null null;
"pam" = fs "80001ff" null null;
"pam.d" = fs "80001ff" null null;
"passwd" = fs "180" null "u0_a3:x:1000:100:Fortify:/var/lib/fortify/u0/a3:/run/current-system/sw/bin/bash\n";
"pipewire" = fs "80001ff" null null;
"pki" = fs "80001ff" null null;
"polkit-1" = fs "80001ff" null null;
"profile" = fs "80001ff" null null;
"profiles" = fs "80001ff" null null;
"protocols" = fs "80001ff" null null;
"resolv.conf" = fs "80001ff" null null;
"resolvconf.conf" = fs "80001ff" null null;
"rpc" = fs "80001ff" null null;
"services" = fs "80001ff" null null;
"set-environment" = fs "80001ff" null null;
"shadow" = fs "80001ff" null null;
"shells" = fs "80001ff" null null;
"ssh" = fs "80001ff" null null;
"ssl" = fs "80001ff" null null;
"static" = fs "80001ff" null null;
"subgid" = fs "80001ff" null null;
"subuid" = fs "80001ff" null null;
"sudoers" = fs "80001ff" null null;
"sway" = fs "80001ff" null null;
"sysctl.d" = fs "80001ff" null null;
"systemd" = fs "80001ff" null null;
"terminfo" = fs "80001ff" null null;
"tmpfiles.d" = fs "80001ff" null null;
"udev" = fs "80001ff" null null;
"vconsole.conf" = fs "80001ff" null null;
"xdg" = fs "80001ff" null null;
"zoneinfo" = fs "80001ff" null null;
} null;
nix = fs "800001c0" { store = fs "801001fd" null null; } null;
proc = fs "8000016d" null null;
run = fs "800001c0" {
current-system = fs "8000016d" null null;
opengl-driver = fs "8000016d" null null;
user = fs "800001ed" {
"1000" = fs "800001ed" {
bus = fs "10001fd" null null;
pulse = fs "800001c0" { native = fs "10001b6" null null; } null;
wayland-0 = fs "1000038" null null;
} null;
} null;
} null;
sys = fs "800001c0" {
block = fs "800001ed" {
fd0 = fs "80001ff" null null;
loop0 = fs "80001ff" null null;
loop1 = fs "80001ff" null null;
loop2 = fs "80001ff" null null;
loop3 = fs "80001ff" null null;
loop4 = fs "80001ff" null null;
loop5 = fs "80001ff" null null;
loop6 = fs "80001ff" null null;
loop7 = fs "80001ff" null null;
sr0 = fs "80001ff" null null;
vda = fs "80001ff" null null;
} null;
bus = fs "800001ed" null null;
class = fs "800001ed" null null;
dev = fs "800001ed" {
block = fs "800001ed" null null;
char = fs "800001ed" null null;
} null;
devices = fs "800001ed" null null;
} null;
tmp = fs "800001f8" { } null;
usr = fs "800001c0" { bin = fs "800001ed" { env = fs "80001ff" null null; } null; } null;
var = fs "800001c0" {
lib = fs "800001c0" {
fortify = fs "800001c0" {
u0 = fs "800001c0" {
a3 = fs "800001c0" {
".cache" = fs "800001ed" { ".keep" = fs "80001ff" null ""; } null;
".config" = fs "800001ed" { "environment.d" = fs "800001ed" { "10-home-manager.conf" = fs "80001ff" null null; } null; } null;
".local" = fs "800001ed" {
state = fs "800001ed" {
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;
nix = fs "800001ed" {
profiles = fs "800001ed" {
home-manager = fs "80001ff" null null;
home-manager-1-link = fs "80001ff" null null;
profile = fs "80001ff" null null;
profile-1-link = fs "80001ff" null null;
} null;
} null;
} null;
} null;
".nix-defexpr" = fs "800001ed" {
channels = fs "80001ff" null null;
channels_root = fs "80001ff" null null;
} null;
".nix-profile" = fs "80001ff" null null;
} null;
} null;
} null;
} null;
run = fs "800001ed" { nscd = fs "800001ed" { } null; } null;
} null;
} null;
mount = [
(ent "/sysroot" "/" "rw,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000003,gid=1000003")
(ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw")
(ent "/" "/.fortify" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000003,gid=1000003")
(ent "/" "/dev" "rw,nosuid,nodev,relatime" "tmpfs" "devtmpfs" "rw,mode=755,uid=1000003,gid=1000003")
(ent "/null" "/dev/null" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/zero" "/dev/zero" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/full" "/dev/full" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/random" "/dev/random" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/urandom" "/dev/urandom" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/tty" "/dev/tty" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,mode=620,ptmxmode=666")
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
(ent ignore "/run/current-system" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
(ent "/block" "/sys/block" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/bus" "/sys/bus" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/class" "/sys/class" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent ignore "/run/opengl-driver" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(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/1000" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8192k,mode=755,uid=1000003,gid=1000003")
(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 ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000003,gid=1000003")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000003,gid=1000003")
(ent ignore "/run/user/1000/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/1000/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent ignore "/run/user/1000/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/var/run/nscd" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8k,mode=755,uid=1000003,gid=1000003")
];
seccomp = true;
};
}

View File

@ -1,29 +1,17 @@
{ {
lib, fs,
writeText, ent,
buildGoModule, ignore,
version,
}: }:
let {
wantFS = name = "preset";
let tty = false;
fs = mode: dir: data: { mapRealUid = false;
mode = lib.fromHexString mode;
inherit want = {
dir fs = fs "dead" {
data
;
};
in
fs "dead" {
".fortify" = fs "800001ed" { ".fortify" = fs "800001ed" {
etc = fs "800001ed" null null; etc = fs "800001ed" null null;
sbin = fs "800001c0" {
fortify = fs "16d" null null;
init0 = fs "80001ff" null null;
} null;
host-mounts = fs "124" null null;
} null; } null;
bin = fs "800001ed" { sh = fs "80001ff" null null; } null; bin = fs "800001ed" { sh = fs "80001ff" null null; } null;
dev = fs "800001ed" { dev = fs "800001ed" {
@ -191,24 +179,43 @@ let
} null; } null;
} null; } null;
mainFile = writeText "main.go" '' mount = [
package main (ent "/sysroot" "/" "rw,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000001,gid=1000001")
(ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw")
(ent "/" "/.fortify" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000001,gid=1000001")
(ent "/" "/dev" "rw,nosuid,nodev,relatime" "tmpfs" "devtmpfs" "rw,mode=755,uid=1000001,gid=1000001")
(ent "/null" "/dev/null" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/zero" "/dev/zero" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/full" "/dev/full" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/random" "/dev/random" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/urandom" "/dev/urandom" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/tty" "/dev/tty" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,mode=620,ptmxmode=666")
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
(ent ignore "/run/current-system" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
(ent "/block" "/sys/block" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/bus" "/sys/bus" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/class" "/sys/class" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent ignore "/run/opengl-driver" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(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/65534" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8192k,mode=755,uid=1000001,gid=1000001")
(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 ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000001,gid=1000001")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000001,gid=1000001")
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/var/run/nscd" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8k,mode=755,uid=1000001,gid=1000001")
];
import "os" seccomp = true;
import "git.gensokyo.uk/security/fortify/test/sandbox" };
func main() { sandbox.MustAssertFS(os.DirFS("/"), "${writeText "want-fs.json" (builtins.toJSON wantFS)}") }
'';
in
buildGoModule {
pname = "check-fs";
inherit version;
src = ../.;
vendorHash = null;
preBuild = ''
go mod init git.gensokyo.uk/security/fortify/test >& /dev/null
cp ${mainFile} main.go
'';
} }

223
test/sandbox/case/tty.nix Normal file
View File

@ -0,0 +1,223 @@
{
fs,
ent,
ignore,
}:
{
name = "tty";
tty = true;
mapRealUid = false;
want = {
fs = fs "dead" {
".fortify" = fs "800001ed" {
etc = fs "800001ed" null null;
} null;
bin = fs "800001ed" { sh = fs "80001ff" null null; } null;
dev = fs "800001ed" {
console = fs "4200190" null null;
core = fs "80001ff" null null;
dri = fs "800001ed" {
by-path = fs "800001ed" {
"pci-0000:00:09.0-card" = fs "80001ff" null null;
"pci-0000:00:09.0-render" = fs "80001ff" null null;
} null;
card0 = fs "42001b0" null null;
renderD128 = fs "42001b6" null null;
} null;
fd = fs "80001ff" null null;
full = fs "42001b6" null null;
mqueue = fs "801001ff" { } null;
null = fs "42001b6" null "";
ptmx = fs "80001ff" null null;
pts = fs "800001ed" { ptmx = fs "42001b6" null null; } null;
random = fs "42001b6" null null;
shm = fs "800001ed" { } null;
stderr = fs "80001ff" null null;
stdin = fs "80001ff" null null;
stdout = fs "80001ff" null null;
tty = fs "42001b6" null null;
urandom = fs "42001b6" null null;
zero = fs "42001b6" null null;
} null;
etc = fs "800001c0" {
".clean" = fs "80001ff" null null;
".updated" = fs "80001ff" null null;
"NIXOS" = fs "80001ff" null null;
"X11" = fs "80001ff" null null;
"alsa" = fs "80001ff" null null;
"bashrc" = fs "80001ff" null null;
"binfmt.d" = fs "80001ff" null null;
"dbus-1" = fs "80001ff" null null;
"default" = fs "80001ff" null null;
"dhcpcd.exit-hook" = fs "80001ff" null null;
"fonts" = fs "80001ff" null null;
"fstab" = fs "80001ff" null null;
"fsurc" = fs "80001ff" null null;
"fuse.conf" = fs "80001ff" null null;
"group" = fs "180" null "fortify:x:65534:\n";
"host.conf" = fs "80001ff" null null;
"hostname" = fs "80001ff" null null;
"hosts" = fs "80001ff" null null;
"inputrc" = fs "80001ff" null null;
"issue" = fs "80001ff" null null;
"kbd" = fs "80001ff" null null;
"locale.conf" = fs "80001ff" null null;
"login.defs" = fs "80001ff" null null;
"lsb-release" = fs "80001ff" null null;
"lvm" = fs "80001ff" null null;
"machine-id" = fs "80001ff" null null;
"man_db.conf" = fs "80001ff" null null;
"modprobe.d" = fs "80001ff" null null;
"modules-load.d" = fs "80001ff" null null;
"mtab" = fs "80001ff" null null;
"nanorc" = fs "80001ff" null null;
"netgroup" = fs "80001ff" null null;
"nix" = fs "80001ff" null null;
"nixos" = fs "80001ff" null null;
"nscd.conf" = fs "80001ff" null null;
"nsswitch.conf" = fs "80001ff" null null;
"os-release" = fs "80001ff" null null;
"pam" = fs "80001ff" null null;
"pam.d" = fs "80001ff" null null;
"passwd" = fs "180" null "u0_a2:x:65534:65534:Fortify:/var/lib/fortify/u0/a2:/run/current-system/sw/bin/bash\n";
"pipewire" = fs "80001ff" null null;
"pki" = fs "80001ff" null null;
"polkit-1" = fs "80001ff" null null;
"profile" = fs "80001ff" null null;
"profiles" = fs "80001ff" null null;
"protocols" = fs "80001ff" null null;
"resolv.conf" = fs "80001ff" null null;
"resolvconf.conf" = fs "80001ff" null null;
"rpc" = fs "80001ff" null null;
"services" = fs "80001ff" null null;
"set-environment" = fs "80001ff" null null;
"shadow" = fs "80001ff" null null;
"shells" = fs "80001ff" null null;
"ssh" = fs "80001ff" null null;
"ssl" = fs "80001ff" null null;
"static" = fs "80001ff" null null;
"subgid" = fs "80001ff" null null;
"subuid" = fs "80001ff" null null;
"sudoers" = fs "80001ff" null null;
"sway" = fs "80001ff" null null;
"sysctl.d" = fs "80001ff" null null;
"systemd" = fs "80001ff" null null;
"terminfo" = fs "80001ff" null null;
"tmpfiles.d" = fs "80001ff" null null;
"udev" = fs "80001ff" null null;
"vconsole.conf" = fs "80001ff" null null;
"xdg" = fs "80001ff" null null;
"zoneinfo" = fs "80001ff" null null;
} null;
nix = fs "800001c0" { store = fs "801001fd" null null; } null;
proc = fs "8000016d" null null;
run = fs "800001c0" {
current-system = fs "8000016d" null null;
opengl-driver = fs "8000016d" null null;
user = fs "800001ed" {
"65534" = fs "800001ed" {
bus = fs "10001fd" null null;
pulse = fs "800001c0" { native = fs "10001b6" null null; } null;
wayland-0 = fs "1000038" null null;
} null;
} null;
} null;
sys = fs "800001c0" {
block = fs "800001ed" {
fd0 = fs "80001ff" null null;
loop0 = fs "80001ff" null null;
loop1 = fs "80001ff" null null;
loop2 = fs "80001ff" null null;
loop3 = fs "80001ff" null null;
loop4 = fs "80001ff" null null;
loop5 = fs "80001ff" null null;
loop6 = fs "80001ff" null null;
loop7 = fs "80001ff" null null;
sr0 = fs "80001ff" null null;
vda = fs "80001ff" null null;
} null;
bus = fs "800001ed" null null;
class = fs "800001ed" null null;
dev = fs "800001ed" {
block = fs "800001ed" null null;
char = fs "800001ed" null null;
} null;
devices = fs "800001ed" null null;
} null;
tmp = fs "800001f8" { } null;
usr = fs "800001c0" { bin = fs "800001ed" { env = fs "80001ff" null null; } null; } null;
var = fs "800001c0" {
lib = fs "800001c0" {
fortify = fs "800001c0" {
u0 = fs "800001c0" {
a2 = fs "800001c0" {
".cache" = fs "800001ed" { ".keep" = fs "80001ff" null ""; } null;
".config" = fs "800001ed" { "environment.d" = fs "800001ed" { "10-home-manager.conf" = fs "80001ff" null null; } null; } null;
".local" = fs "800001ed" {
state = fs "800001ed" {
home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null;
nix = fs "800001ed" {
profiles = fs "800001ed" {
home-manager = fs "80001ff" null null;
home-manager-1-link = fs "80001ff" null null;
profile = fs "80001ff" null null;
profile-1-link = fs "80001ff" null null;
} null;
} null;
} null;
} null;
".nix-defexpr" = fs "800001ed" {
channels = fs "80001ff" null null;
channels_root = fs "80001ff" null null;
} null;
".nix-profile" = fs "80001ff" null null;
} null;
} null;
} null;
} null;
run = fs "800001ed" { nscd = fs "800001ed" { } null; } null;
} null;
} null;
mount = [
(ent "/sysroot" "/" "rw,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000002,gid=1000002")
(ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw")
(ent "/" "/.fortify" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=4k,mode=755,uid=1000002,gid=1000002")
(ent "/" "/dev" "rw,nosuid,nodev,relatime" "tmpfs" "devtmpfs" "rw,mode=755,uid=1000002,gid=1000002")
(ent "/null" "/dev/null" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/zero" "/dev/zero" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/full" "/dev/full" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/random" "/dev/random" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/urandom" "/dev/urandom" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/tty" "/dev/tty" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,mode=620,ptmxmode=666")
(ent ignore "/dev/console" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,gid=3,mode=620,ptmxmode=666")
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
(ent ignore "/run/current-system" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
(ent "/block" "/sys/block" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/bus" "/sys/bus" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/class" "/sys/class" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw")
(ent ignore "/run/opengl-driver" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
(ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(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/65534" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8192k,mode=755,uid=1000002,gid=1000002")
(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 ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000002,gid=1000002")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000002,gid=1000002")
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/var/run/nscd" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" "rw,size=8k,mode=755,uid=1000002,gid=1000002")
];
seccomp = true;
};
}

View File

@ -1,14 +1,14 @@
{ {
writeShellScript, writeShellScript,
writeText,
callPackage, callPackage,
version, version,
}: }:
writeShellScript "check-sandbox" '' name: want:
writeShellScript "fortify-${name}-check-sandbox-script" ''
set -e set -e
${callPackage ./mount.nix { inherit version; }}/bin/test ${callPackage ./assert.nix { inherit version; }}/bin/test \
${callPackage ./fs.nix { inherit version; }}/bin/test ${writeText "fortify-${name}-want.json" (builtins.toJSON want)}
${callPackage ./seccomp.nix { inherit version; }}/bin/test
touch /tmp/sandbox-ok touch /tmp/sandbox-ok
'' ''

View File

@ -30,7 +30,7 @@ func printDir(prefix string, dir []fs.DirEntry) {
} }
names[i] = fmt.Sprintf("%q", name) names[i] = fmt.Sprintf("%q", name)
} }
printf("[FAIL] d %q: %s", prefix, strings.Join(names, " ")) printf("[FAIL] d %s: %s", prefix, strings.Join(names, " "))
} }
func (s *FS) Compare(prefix string, e fs.FS) error { func (s *FS) Compare(prefix string, e fs.FS) error {
@ -71,7 +71,7 @@ func (s *FS) Compare(prefix string, e fs.FS) error {
if fi, err := got.Info(); err != nil { if fi, err := got.Info(); err != nil {
return err return err
} else if fi.Mode() != want.Mode { } else if fi.Mode() != want.Mode {
printf("[FAIL] m %q: %x, want %x", printf("[FAIL] m %s: %x, want %x",
name, uint32(fi.Mode()), uint32(want.Mode)) name, uint32(fi.Mode()), uint32(want.Mode))
return ErrFSBadMode return ErrFSBadMode
} }
@ -84,6 +84,8 @@ func (s *FS) Compare(prefix string, e fs.FS) error {
return err return err
} else if string(v) != *want.Data { } else if string(v) != *want.Data {
printf("[FAIL] f %s", name) printf("[FAIL] f %s", name)
printf("got: %s", v)
printf("want: %s", *want.Data)
return ErrFSBadData return ErrFSBadData
} }
printf("[ OK ] f %s", name) printf("[ OK ] f %s", name)

View File

@ -31,16 +31,16 @@ func TestCompare(t *testing.T) {
"[ OK ] s .fortify\x00[ OK ] d .\x00", nil}, "[ OK ] s .fortify\x00[ OK ] d .\x00", nil},
{"bad length", fstest.MapFS{".fortify": {Mode: 0x800001ed}}, {"bad length", fstest.MapFS{".fortify": {Mode: 0x800001ed}},
&sandbox.FS{Dir: make(map[string]*sandbox.FS)}, &sandbox.FS{Dir: make(map[string]*sandbox.FS)},
"[FAIL] d \".\": \".fortify/\"\x00", sandbox.ErrFSBadLength}, "[FAIL] d .: \".fortify/\"\x00", sandbox.ErrFSBadLength},
{"top level bad mode", fstest.MapFS{".fortify": {Mode: 0x800001ed}}, {"top level bad mode", fstest.MapFS{".fortify": {Mode: 0x800001ed}},
&sandbox.FS{Dir: map[string]*sandbox.FS{".fortify": {Mode: 0xdeadbeef}}}, &sandbox.FS{Dir: map[string]*sandbox.FS{".fortify": {Mode: 0xdeadbeef}}},
"[FAIL] m \".fortify\": 800001ed, want deadbeef\x00", sandbox.ErrFSBadMode}, "[FAIL] m .fortify: 800001ed, want deadbeef\x00", sandbox.ErrFSBadMode},
{"invalid entry condition", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}}, {"invalid entry condition", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}},
&sandbox.FS{Dir: map[string]*sandbox.FS{"test": {Dir: make(map[string]*sandbox.FS)}}}, &sandbox.FS{Dir: map[string]*sandbox.FS{"test": {Dir: make(map[string]*sandbox.FS)}}},
"[FAIL] d \".\": \"test\"\x00", sandbox.ErrFSInvalidEnt}, "[FAIL] d .: \"test\"\x00", sandbox.ErrFSInvalidEnt},
{"nonexistent", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}}, {"nonexistent", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}},
&sandbox.FS{Dir: map[string]*sandbox.FS{".test": {}}}, &sandbox.FS{Dir: map[string]*sandbox.FS{".test": {}}},
"[FAIL] d \".\": \"test\"\x00", fs.ErrNotExist}, "[FAIL] d .: \"test\"\x00", fs.ErrNotExist},
{"file", fstest.MapFS{"etc": {Mode: 0x800001c0}, {"file", fstest.MapFS{"etc": {Mode: 0x800001c0},
"etc/passwd": {Data: []byte(fsPasswdSample), Mode: 0644}, "etc/passwd": {Data: []byte(fsPasswdSample), Mode: 0644},
"etc/group": {Data: []byte(fsGroupSample), Mode: 0644}, "etc/group": {Data: []byte(fsGroupSample), Mode: 0644},
@ -54,7 +54,7 @@ func TestCompare(t *testing.T) {
}, &sandbox.FS{Dir: map[string]*sandbox.FS{"etc": {Mode: 0x800001c0, Dir: map[string]*sandbox.FS{ }, &sandbox.FS{Dir: map[string]*sandbox.FS{"etc": {Mode: 0x800001c0, Dir: map[string]*sandbox.FS{
"passwd": {Mode: 0x1a4, Data: &fsGroupSample}, "passwd": {Mode: 0x1a4, Data: &fsGroupSample},
"group": {Mode: 0x1a4, Data: &fsGroupSample}, "group": {Mode: 0x1a4, Data: &fsGroupSample},
}}}}, "[ OK ] f etc/group\x00[FAIL] f etc/passwd\x00", sandbox.ErrFSBadData}, }}}}, "[ OK ] f etc/group\x00[FAIL] f etc/passwd\x00got: u0_a20:x:65534:65534:Fortify:/var/lib/persist/module/fortify/u0/a20:/run/current-system/sw/bin/zsh\x00want: fortify:x:65534:\x00", sandbox.ErrFSBadData},
} }
for _, tc := range testCases { for _, tc := range testCases {
@ -75,10 +75,4 @@ func TestCompare(t *testing.T) {
} }
}) })
} }
t.Run("assert", func(t *testing.T) {
oldFatal := sandbox.SwapFatal(t.Fatalf)
t.Cleanup(func() { sandbox.SwapFatal(oldFatal) })
sandbox.MustAssertFS(make(fstest.MapFS), sandbox.MustWantFile(t, &sandbox.FS{Mode: 0xDEADBEEF}))
})
} }

View File

@ -1,146 +1,157 @@
package sandbox package sandbox
/* /*
#cgo linux pkg-config: --static mount
#include <stdlib.h> #include <stdlib.h>
#include <stdio.h> #include <stdio.h>
#include <mntent.h> #include <libmount.h>
const char *F_PROC_MOUNTS = ""; const char *F_MOUNTINFO_PATH = "/proc/self/mountinfo";
const char *F_SET_TYPE = "r";
*/ */
import "C" import "C"
import ( import (
"errors"
"fmt" "fmt"
"iter"
"runtime" "runtime"
"sync" "sync"
"unsafe" "unsafe"
) )
type Mntent struct { var (
/* name of mounted filesystem */ ErrMountinfoParse = errors.New("invalid mountinfo records")
FSName string `json:"fsname"` ErrMountinfoIter = errors.New("cannot allocate iterator")
/* filesystem path prefix */ ErrMountinfoFault = errors.New("cannot iterate on filesystems")
Dir string `json:"dir"` )
/* mount type (see mntent.h) */
Type string `json:"type"`
/* mount options (see mntent.h) */
Opts string `json:"opts"`
/* dump frequency in days */
Freq int `json:"freq"`
/* pass number on parallel fsck */
Passno int `json:"passno"`
}
func (e *Mntent) String() string { type (
return fmt.Sprintf("%s %s %s %s %d %d", Mountinfo struct {
e.FSName, e.Dir, e.Type, e.Opts, e.Freq, e.Passno)
}
func (e *Mntent) Is(want *Mntent) bool {
if want == nil {
return e == nil
}
return (e.FSName == want.FSName || want.FSName == "\x00") &&
(e.Dir == want.Dir || want.Dir == "\x00") &&
(e.Type == want.Type || want.Type == "\x00") &&
(e.Opts == want.Opts || want.Opts == "\x00") &&
(e.Freq == want.Freq || want.Freq == -1) &&
(e.Passno == want.Passno || want.Passno == -1)
}
func IterMounts(name string, f func(e *Mntent)) error {
m := new(mounts)
m.p = name
if err := m.open(); err != nil {
return err
}
for m.scan() {
e := new(Mntent)
m.copy(e)
f(e)
}
m.close()
return m.Err()
}
type mounts struct {
p string
f *C.FILE
mu sync.RWMutex mu sync.RWMutex
p string
ent *C.struct_mntent
err error err error
tb *C.struct_libmnt_table
itr *C.struct_libmnt_iter
fs *C.struct_libmnt_fs
}
// MountinfoEntry represents deterministic mountinfo parts of a libmnt_fs entry.
MountinfoEntry struct {
// mount ID: a unique ID for the mount (may be reused after umount(2)).
ID int `json:"id"`
// parent ID: the ID of the parent mount (or of self for the root of this mount namespace's mount tree).
Parent int `json:"parent"`
// root: the pathname of the directory in the filesystem which forms the root of this mount.
Root string `json:"root"`
// mount point: the pathname of the mount point relative to the process's root directory.
Target string `json:"target"`
// mount options: per-mount options (see mount(2)).
VfsOptstr string `json:"vfs_optstr"`
// filesystem type: the filesystem type in the form "type[.subtype]".
FsType string `json:"fstype"`
// mount source: filesystem-specific information or "none".
Source string `json:"source"`
// super options: per-superblock options (see mount(2)).
FsOptstr string `json:"fs_optstr"`
}
)
func (m *Mountinfo) copy(v *MountinfoEntry) {
if m.fs == nil {
panic("invalid entry")
}
v.ID = int(C.mnt_fs_get_id(m.fs))
v.Parent = int(C.mnt_fs_get_parent_id(m.fs))
v.Root = C.GoString(C.mnt_fs_get_root(m.fs))
v.Target = C.GoString(C.mnt_fs_get_target(m.fs))
v.VfsOptstr = C.GoString(C.mnt_fs_get_vfs_options(m.fs))
v.FsType = C.GoString(C.mnt_fs_get_fstype(m.fs))
v.Source = C.GoString(C.mnt_fs_get_source(m.fs))
v.FsOptstr = C.GoString(C.mnt_fs_get_fs_options(m.fs))
} }
func (m *mounts) open() error { func NewMountinfo(p string) *Mountinfo { m := new(Mountinfo); m.p = p; return m }
func (m *Mountinfo) Err() error { m.mu.RLock(); defer m.mu.RUnlock(); return m.err }
func (m *Mountinfo) Parse() error {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
if m.f != nil { if m.tb != nil {
panic("open called twice") panic("open called twice")
} }
if m.p == "" { if m.p == "" {
m.p = "/proc/mounts" m.tb = C.mnt_new_table_from_file(C.F_MOUNTINFO_PATH)
} } else {
name := C.CString(m.p) name := C.CString(m.p)
f, err := C.setmntent(name, C.F_SET_TYPE) m.tb = C.mnt_new_table_from_file(name)
C.free(unsafe.Pointer(name)) C.free(unsafe.Pointer(name))
if f == nil {
return err
} }
m.f = f if m.tb == nil {
runtime.SetFinalizer(m, (*mounts).close) return ErrMountinfoParse
return err }
m.itr = C.mnt_new_iter(C.MNT_ITER_FORWARD)
if m.itr == nil {
C.mnt_unref_table(m.tb)
return ErrMountinfoIter
}
runtime.SetFinalizer(m, (*Mountinfo).Unref)
return nil
} }
func (m *mounts) close() { func (m *Mountinfo) Unref() {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
if m.f == nil { if m.tb == nil {
panic("close called before open") panic("unref called before parse")
} }
C.endmntent(m.f) C.mnt_unref_table(m.tb)
C.mnt_free_iter(m.itr)
runtime.SetFinalizer(m, nil) runtime.SetFinalizer(m, nil)
} }
func (m *mounts) scan() bool { func (m *Mountinfo) Entries() iter.Seq[*MountinfoEntry] {
return func(yield func(*MountinfoEntry) bool) {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
if m.f == nil { C.mnt_reset_iter(m.itr, -1)
panic("invalid file")
var rc C.int
ent := new(MountinfoEntry)
for rc = C.mnt_table_next_fs(m.tb, m.itr, &m.fs); rc == 0; rc = C.mnt_table_next_fs(m.tb, m.itr, &m.fs) {
m.copy(ent)
if !yield(ent) {
return
}
}
if rc < 0 {
m.err = ErrMountinfoFault
return
} }
m.ent, m.err = C.getmntent(m.f)
return m.ent != nil
}
func (m *mounts) Err() error {
m.mu.RLock()
defer m.mu.RUnlock()
return m.err
}
func (m *mounts) copy(v *Mntent) {
m.mu.RLock()
defer m.mu.RUnlock()
if m.ent == nil {
panic("invalid entry")
} }
v.FSName = C.GoString(m.ent.mnt_fsname) }
v.Dir = C.GoString(m.ent.mnt_dir)
v.Type = C.GoString(m.ent.mnt_type) func (e *MountinfoEntry) EqualWithIgnore(want *MountinfoEntry, ignore string) bool {
v.Opts = C.GoString(m.ent.mnt_opts) return (e.ID == want.ID || want.ID == -1) &&
v.Freq = int(m.ent.mnt_freq) (e.Parent == want.Parent || want.Parent == -1) &&
v.Passno = int(m.ent.mnt_passno) (e.Root == want.Root || want.Root == ignore) &&
(e.Target == want.Target || want.Target == ignore) &&
(e.VfsOptstr == want.VfsOptstr || want.VfsOptstr == ignore) &&
(e.FsType == want.FsType || want.FsType == ignore) &&
(e.Source == want.Source || want.Source == ignore) &&
(e.FsOptstr == want.FsOptstr || want.FsOptstr == ignore)
}
func (e *MountinfoEntry) String() string {
return fmt.Sprintf("%d %d %s %s %s %s %s %s",
e.ID, e.Parent, e.Root, e.Target, e.VfsOptstr, e.FsType, e.Source, e.FsOptstr)
} }

View File

@ -1,79 +0,0 @@
{
writeText,
buildGoModule,
version,
}:
let
wantMounts =
let
ent = fsname: dir: type: opts: freq: passno: {
inherit
fsname
dir
type
opts
freq
passno
;
};
in
[
(ent "tmpfs" "/" "tmpfs" "rw,nosuid,nodev,relatime,uid=1000001,gid=1000001" 0 0)
(ent "proc" "/proc" "proc" "rw,nosuid,nodev,noexec,relatime" 0 0)
(ent "tmpfs" "/.fortify" "tmpfs" "rw,nosuid,nodev,relatime,size=4k,mode=755,uid=1000001,gid=1000001" 0 0)
(ent "tmpfs" "/dev" "tmpfs" "rw,nosuid,nodev,relatime,mode=755,uid=1000001,gid=1000001" 0 0)
(ent "devtmpfs" "/dev/null" "devtmpfs" "host_passthrough" 0 0)
(ent "devtmpfs" "/dev/zero" "devtmpfs" "host_passthrough" 0 0)
(ent "devtmpfs" "/dev/full" "devtmpfs" "host_passthrough" 0 0)
(ent "devtmpfs" "/dev/random" "devtmpfs" "host_passthrough" 0 0)
(ent "devtmpfs" "/dev/urandom" "devtmpfs" "host_passthrough" 0 0)
(ent "devtmpfs" "/dev/tty" "devtmpfs" "host_passthrough" 0 0)
(ent "devpts" "/dev/pts" "devpts" "rw,nosuid,noexec,relatime,mode=620,ptmxmode=666" 0 0)
(ent "mqueue" "/dev/mqueue" "mqueue" "rw,relatime" 0 0)
(ent "/dev/disk/by-label/nixos" "/bin" "ext4" "ro,nosuid,nodev,relatime" 0 0)
(ent "/dev/disk/by-label/nixos" "/usr/bin" "ext4" "ro,nosuid,nodev,relatime" 0 0)
(ent "overlay" "/nix/store" "overlay" "ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on" 0 0)
(ent "overlay" "/run/current-system" "overlay" "ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on" 0 0)
(ent "sysfs" "/sys/block" "sysfs" "ro,nosuid,nodev,noexec,relatime" 0 0)
(ent "sysfs" "/sys/bus" "sysfs" "ro,nosuid,nodev,noexec,relatime" 0 0)
(ent "sysfs" "/sys/class" "sysfs" "ro,nosuid,nodev,noexec,relatime" 0 0)
(ent "sysfs" "/sys/dev" "sysfs" "ro,nosuid,nodev,noexec,relatime" 0 0)
(ent "sysfs" "/sys/devices" "sysfs" "ro,nosuid,nodev,noexec,relatime" 0 0)
(ent "overlay" "/run/opengl-driver" "overlay" "ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on" 0 0)
(ent "devtmpfs" "/dev/dri" "devtmpfs" "host_passthrough" 0 0)
(ent "proc" "/.fortify/host-mounts" "proc" "ro,nosuid,nodev,noexec,relatime" 0 0)
(ent "/dev/disk/by-label/nixos" "/.fortify/etc" "ext4" "ro,nosuid,nodev,relatime" 0 0)
(ent "tmpfs" "/run/user" "tmpfs" "rw,nosuid,nodev,relatime,size=1024k,mode=755,uid=1000001,gid=1000001" 0 0)
(ent "tmpfs" "/run/user/65534" "tmpfs" "rw,nosuid,nodev,relatime,size=8192k,mode=755,uid=1000001,gid=1000001" 0 0)
(ent "/dev/disk/by-label/nixos" "/tmp" "ext4" "rw,nosuid,nodev,relatime" 0 0)
(ent "/dev/disk/by-label/nixos" "/var/lib/fortify/u0/a1" "ext4" "rw,nosuid,nodev,relatime" 0 0)
(ent "tmpfs" "/etc/passwd" "tmpfs" "ro,nosuid,nodev,relatime,uid=1000001,gid=1000001" 0 0)
(ent "tmpfs" "/etc/group" "tmpfs" "ro,nosuid,nodev,relatime,uid=1000001,gid=1000001" 0 0)
(ent "/dev/disk/by-label/nixos" "/run/user/65534/wayland-0" "ext4" "ro,nosuid,nodev,relatime" 0 0)
(ent "tmpfs" "/run/user/65534/pulse/native" "tmpfs" "host_passthrough" 0 0)
(ent "/dev/disk/by-label/nixos" "/run/user/65534/bus" "ext4" "ro,nosuid,nodev,relatime" 0 0)
(ent "tmpfs" "/var/run/nscd" "tmpfs" "rw,nosuid,nodev,relatime,size=8k,mode=755,uid=1000001,gid=1000001" 0 0)
(ent "overlay" "/.fortify/sbin/fortify" "overlay" "ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on" 0 0)
];
mainFile = writeText "main.go" ''
package main
import "git.gensokyo.uk/security/fortify/test/sandbox"
func main() { sandbox.MustAssertMounts("", "/.fortify/host-mounts", "${writeText "want-mounts.json" (builtins.toJSON wantMounts)}") }
'';
in
buildGoModule {
pname = "check-mounts";
inherit version;
src = ../.;
vendorHash = null;
preBuild = ''
go mod init git.gensokyo.uk/security/fortify/test >& /dev/null
cp ${mainFile} main.go
'';
}

View File

@ -8,80 +8,79 @@ import (
"git.gensokyo.uk/security/fortify/test/sandbox" "git.gensokyo.uk/security/fortify/test/sandbox"
) )
func TestMounts(t *testing.T) { func TestMountinfo(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
sample string sample string
want []sandbox.Mntent want []*sandbox.MountinfoEntry
}{ }{
{"fpkg", `tmpfs / tmpfs rw,nosuid,nodev,relatime,uid=1000002,gid=1000002 0 0 {"util-linux", `15 20 0:3 / /proc rw,relatime - proc /proc rw
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0 16 20 0:15 / /sys rw,relatime - sysfs /sys rw
tmpfs /.fortify tmpfs rw,nosuid,nodev,relatime,size=4k,mode=755,uid=1000002,gid=1000002 0 0 17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755
tmpfs /dev tmpfs rw,nosuid,nodev,relatime,mode=755,uid=1000002,gid=1000002 0 0 18 17 0:10 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=000
devtmpfs /dev/null devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0 19 17 0:16 / /dev/shm rw,relatime - tmpfs tmpfs rw
devtmpfs /dev/zero devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0 20 1 8:4 / / rw,noatime - ext3 /dev/sda4 rw,errors=continue,user_xattr,acl,barrier=0,data=ordered
devtmpfs /dev/full devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0 21 16 0:17 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755
devtmpfs /dev/random devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0 22 21 0:18 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd
devtmpfs /dev/urandom devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0 23 21 0:19 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuset
devtmpfs /dev/tty devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0 24 21 0:20 / /sys/fs/cgroup/ns rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,ns
devpts /dev/pts devpts rw,nosuid,noexec,relatime,mode=620,ptmxmode=666 0 0 25 21 0:21 / /sys/fs/cgroup/cpu rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpu
mqueue /dev/mqueue mqueue rw,relatime 0 0 26 21 0:22 / /sys/fs/cgroup/cpuacct rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpuacct
/dev/disk/by-label/nixos /nix/store ext4 ro,nosuid,nodev,relatime 0 0 27 21 0:23 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,memory
/dev/disk/by-label/nixos /.fortify/app ext4 ro,nosuid,nodev,relatime 0 0 28 21 0:24 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,devices
/dev/disk/by-label/nixos /etc/resolv.conf ext4 ro,nosuid,nodev,relatime 0 0 29 21 0:25 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,freezer
sysfs /sys/block sysfs ro,nosuid,nodev,noexec,relatime 0 0 30 21 0:26 / /sys/fs/cgroup/net_cls rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,net_cls
sysfs /sys/bus sysfs ro,nosuid,nodev,noexec,relatime 0 0 31 21 0:27 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,blkio
sysfs /sys/class sysfs ro,nosuid,nodev,noexec,relatime 0 0 32 16 0:28 / /sys/kernel/security rw,relatime - autofs systemd-1 rw,fd=22,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
sysfs /sys/dev sysfs ro,nosuid,nodev,noexec,relatime 0 0 33 17 0:29 / /dev/hugepages rw,relatime - autofs systemd-1 rw,fd=23,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
sysfs /sys/devices sysfs ro,nosuid,nodev,noexec,relatime 0 0 34 16 0:30 / /sys/kernel/debug rw,relatime - autofs systemd-1 rw,fd=24,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
/dev/disk/by-label/nixos /.fortify/nixGL ext4 ro,nosuid,nodev,relatime 0 0 35 15 0:31 / /proc/sys/fs/binfmt_misc rw,relatime - autofs systemd-1 rw,fd=25,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
devtmpfs /dev/dri devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0 36 17 0:32 / /dev/mqueue rw,relatime - autofs systemd-1 rw,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct
/dev/disk/by-label/nixos /.fortify/etc ext4 ro,nosuid,nodev,relatime 0 0 37 15 0:14 / /proc/bus/usb rw,relatime - usbfs /proc/bus/usb rw
tmpfs /run/user tmpfs rw,nosuid,nodev,relatime,size=1024k,mode=755,uid=1000002,gid=1000002 0 0 38 33 0:33 / /dev/hugepages rw,relatime - hugetlbfs hugetlbfs rw
tmpfs /run/user/65534 tmpfs rw,nosuid,nodev,relatime,size=8192k,mode=755,uid=1000002,gid=1000002 0 0 39 36 0:12 / /dev/mqueue rw,relatime - mqueue mqueue rw
/dev/disk/by-label/nixos /tmp ext4 rw,nosuid,nodev,relatime 0 0 40 20 8:6 / /boot rw,noatime - ext3 /dev/sda6 rw,errors=continue,barrier=0,data=ordered
/dev/disk/by-label/nixos /data/data/org.codeberg.dnkl.foot ext4 rw,nosuid,nodev,relatime 0 0 41 20 253:0 / /home/kzak rw,noatime - ext4 /dev/mapper/kzak-home rw,barrier=1,data=ordered
tmpfs /etc/passwd tmpfs ro,nosuid,nodev,relatime,uid=1000002,gid=1000002 0 0 42 35 0:34 / /proc/sys/fs/binfmt_misc rw,relatime - binfmt_misc none rw
tmpfs /etc/group tmpfs ro,nosuid,nodev,relatime,uid=1000002,gid=1000002 0 0 43 16 0:35 / /sys/fs/fuse/connections rw,relatime - fusectl fusectl rw
/dev/disk/by-label/nixos /run/user/65534/wayland-0 ext4 ro,nosuid,nodev,relatime 0 0 44 41 0:36 / /home/kzak/.gvfs rw,nosuid,nodev,relatime - fuse.gvfs-fuse-daemon gvfs-fuse-daemon rw,user_id=500,group_id=500
tmpfs /run/user/65534/pulse/native tmpfs ro,nosuid,nodev,relatime,size=98784k,nr_inodes=24696,mode=700,uid=1000,gid=100 0 0 45 20 0:37 / /var/lib/nfs/rpc_pipefs rw,relatime - rpc_pipefs sunrpc rw
/dev/disk/by-label/nixos /run/user/65534/bus ext4 ro,nosuid,nodev,relatime 0 0 47 20 0:38 / /mnt/sounds rw,relatime - cifs //foo.home/bar/ rw,unc=\\foo.home\bar,username=kzak,domain=SRGROUP,uid=0,noforceuid,gid=0,noforcegid,addr=192.168.111.1,posixpaths,serverino,acl,rsize=16384,wsize=57344
overlay /.fortify/sbin/fortify overlay ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on 0 0 49 20 0:56 / /mnt/test/foobar rw,relatime,nosymfollow shared:323 - tmpfs tmpfs rw`, []*sandbox.MountinfoEntry{
`, []sandbox.Mntent{ e(15, 20, "/", "/proc", "rw,relatime", "proc", "/proc", "rw"),
{"tmpfs", "/", "tmpfs", "rw,nosuid,nodev,relatime,uid=1000002,gid=1000002", 0, 0}, e(16, 20, "/", "/sys", "rw,relatime", "sysfs", "/sys", "rw"),
{"proc", "/proc", "proc", "rw,nosuid,nodev,noexec,relatime", 0, 0}, e(17, 20, "/", "/dev", "rw,relatime", "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755"),
{"tmpfs", "/.fortify", "tmpfs", "rw,nosuid,nodev,relatime,size=4k,mode=755,uid=1000002,gid=1000002", 0, 0}, e(18, 17, "/", "/dev/pts", "rw,relatime", "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000"),
{"tmpfs", "/dev", "tmpfs", "rw,nosuid,nodev,relatime,mode=755,uid=1000002,gid=1000002", 0, 0}, e(19, 17, "/", "/dev/shm", "rw,relatime", "tmpfs", "tmpfs", "rw"),
{"devtmpfs", "/dev/null", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0}, e(20, 1, "/", "/", "rw,noatime", "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered"),
{"devtmpfs", "/dev/zero", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0}, e(21, 16, "/", "/sys/fs/cgroup", "rw,nosuid,nodev,noexec,relatime", "tmpfs", "tmpfs", "rw,mode=755"),
{"devtmpfs", "/dev/full", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0}, e(22, 21, "/", "/sys/fs/cgroup/systemd", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd"),
{"devtmpfs", "/dev/random", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0}, e(23, 21, "/", "/sys/fs/cgroup/cpuset", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,cpuset"),
{"devtmpfs", "/dev/urandom", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0}, e(24, 21, "/", "/sys/fs/cgroup/ns", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,ns"),
{"devtmpfs", "/dev/tty", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0}, e(25, 21, "/", "/sys/fs/cgroup/cpu", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,cpu"),
{"devpts", "/dev/pts", "devpts", "rw,nosuid,noexec,relatime,mode=620,ptmxmode=666", 0, 0}, e(26, 21, "/", "/sys/fs/cgroup/cpuacct", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,cpuacct"),
{"mqueue", "/dev/mqueue", "mqueue", "rw,relatime", 0, 0}, e(27, 21, "/", "/sys/fs/cgroup/memory", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,memory"),
{"/dev/disk/by-label/nixos", "/nix/store", "ext4", "ro,nosuid,nodev,relatime", 0, 0}, e(28, 21, "/", "/sys/fs/cgroup/devices", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,devices"),
{"/dev/disk/by-label/nixos", "/.fortify/app", "ext4", "ro,nosuid,nodev,relatime", 0, 0}, e(29, 21, "/", "/sys/fs/cgroup/freezer", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,freezer"),
{"/dev/disk/by-label/nixos", "/etc/resolv.conf", "ext4", "ro,nosuid,nodev,relatime", 0, 0}, e(30, 21, "/", "/sys/fs/cgroup/net_cls", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,net_cls"),
{"sysfs", "/sys/block", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0}, e(31, 21, "/", "/sys/fs/cgroup/blkio", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,blkio"),
{"sysfs", "/sys/bus", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0}, e(32, 16, "/", "/sys/kernel/security", "rw,relatime", "autofs", "systemd-1", "rw,fd=22,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
{"sysfs", "/sys/class", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0}, e(33, 17, "/", "/dev/hugepages", "rw,relatime", "autofs", "systemd-1", "rw,fd=23,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
{"sysfs", "/sys/dev", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0}, e(34, 16, "/", "/sys/kernel/debug", "rw,relatime", "autofs", "systemd-1", "rw,fd=24,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
{"sysfs", "/sys/devices", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0}, e(35, 15, "/", "/proc/sys/fs/binfmt_misc", "rw,relatime", "autofs", "systemd-1", "rw,fd=25,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
{"/dev/disk/by-label/nixos", "/.fortify/nixGL", "ext4", "ro,nosuid,nodev,relatime", 0, 0}, e(36, 17, "/", "/dev/mqueue", "rw,relatime", "autofs", "systemd-1", "rw,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
{"devtmpfs", "/dev/dri", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0}, e(37, 15, "/", "/proc/bus/usb", "rw,relatime", "usbfs", "/proc/bus/usb", "rw"),
{"/dev/disk/by-label/nixos", "/.fortify/etc", "ext4", "ro,nosuid,nodev,relatime", 0, 0}, e(38, 33, "/", "/dev/hugepages", "rw,relatime", "hugetlbfs", "hugetlbfs", "rw"),
{"tmpfs", "/run/user", "tmpfs", "rw,nosuid,nodev,relatime,size=1024k,mode=755,uid=1000002,gid=1000002", 0, 0}, e(39, 36, "/", "/dev/mqueue", "rw,relatime", "mqueue", "mqueue", "rw"),
{"tmpfs", "/run/user/65534", "tmpfs", "rw,nosuid,nodev,relatime,size=8192k,mode=755,uid=1000002,gid=1000002", 0, 0}, e(40, 20, "/", "/boot", "rw,noatime", "ext3", "/dev/sda6", "rw,errors=continue,barrier=0,data=ordered"),
{"/dev/disk/by-label/nixos", "/tmp", "ext4", "rw,nosuid,nodev,relatime", 0, 0}, e(41, 20, "/", "/home/kzak", "rw,noatime", "ext4", "/dev/mapper/kzak-home", "rw,barrier=1,data=ordered"),
{"/dev/disk/by-label/nixos", "/data/data/org.codeberg.dnkl.foot", "ext4", "rw,nosuid,nodev,relatime", 0, 0}, e(42, 35, "/", "/proc/sys/fs/binfmt_misc", "rw,relatime", "binfmt_misc", "none", "rw"),
{"tmpfs", "/etc/passwd", "tmpfs", "ro,nosuid,nodev,relatime,uid=1000002,gid=1000002", 0, 0}, e(43, 16, "/", "/sys/fs/fuse/connections", "rw,relatime", "fusectl", "fusectl", "rw"),
{"tmpfs", "/etc/group", "tmpfs", "ro,nosuid,nodev,relatime,uid=1000002,gid=1000002", 0, 0}, e(44, 41, "/", "/home/kzak/.gvfs", "rw,nosuid,nodev,relatime", "fuse.gvfs-fuse-daemon", "gvfs-fuse-daemon", "rw,user_id=500,group_id=500"),
{"/dev/disk/by-label/nixos", "/run/user/65534/wayland-0", "ext4", "ro,nosuid,nodev,relatime", 0, 0}, e(45, 20, "/", "/var/lib/nfs/rpc_pipefs", "rw,relatime", "rpc_pipefs", "sunrpc", "rw"),
{"tmpfs", "/run/user/65534/pulse/native", "tmpfs", "ro,nosuid,nodev,relatime,size=98784k,nr_inodes=24696,mode=700,uid=1000,gid=100", 0, 0}, e(47, 20, "/", "/mnt/sounds", "rw,relatime", "cifs", "//foo.home/bar/", "rw,unc=\\\\foo.home\\bar,username=kzak,domain=SRGROUP,uid=0,noforceuid,gid=0,noforcegid,addr=192.168.111.1,posixpaths,serverino,acl,rsize=16384,wsize=57344"),
{"/dev/disk/by-label/nixos", "/run/user/65534/bus", "ext4", "ro,nosuid,nodev,relatime", 0, 0}, e(49, 20, "/", "/mnt/test/foobar", "rw,relatime,nosymfollow", "tmpfs", "tmpfs", "rw"),
{"overlay", "/.fortify/sbin/fortify", "overlay", "ro,nosuid,nodev,relatime,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on", 0, 0},
}}, }},
} }
@ -92,27 +91,33 @@ overlay /.fortify/sbin/fortify overlay ro,nosuid,nodev,relatime,lowerdir=/mnt-ro
} }
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
i := 0 m := sandbox.NewMountinfo(name)
if err := sandbox.IterMounts(name, func(e *sandbox.Mntent) { if err := m.Parse(); err != nil {
if i == len(tc.want) { t.Fatalf("Parse: error = %v", err)
t.Errorf("IterMounts: got more than %d entries", i)
t.FailNow()
} }
if *e != tc.want[i] {
t.Errorf("IterMounts: entry %d\n got: %s\nwant: %s", i,
e, &tc.want[i])
t.FailNow()
}
i++
}); err != nil {
t.Fatalf("IterMounts: error = %v", err)
}
})
t.Run(tc.name+" assert", func(t *testing.T) { i := 0
oldFatal := sandbox.SwapFatal(t.Fatalf) for ent := range m.Entries() {
t.Cleanup(func() { sandbox.SwapFatal(oldFatal) }) if i == len(tc.want) {
sandbox.MustAssertMounts(name, name, sandbox.MustWantFile(t, tc.want)) t.Errorf("Entries: got more than %d entries", i)
t.FailNow()
}
if !ent.EqualWithIgnore(tc.want[i], "\x00") {
t.Errorf("Entries: entry %d\n got: %#v\nwant: %#v", i,
ent, &tc.want[i])
t.FailNow()
} else {
t.Logf("%s", ent)
}
i++
}
if err := m.Err(); err != nil {
t.Fatalf("Mountinfo: error = %v", err)
}
m.Unref()
}) })
if err := os.Remove(name); err != nil { if err := os.Remove(name); err != nil {
@ -120,3 +125,18 @@ overlay /.fortify/sbin/fortify overlay ro,nosuid,nodev,relatime,lowerdir=/mnt-ro
} }
} }
} }
func e(
id, parent int, root, target, vfsOptstr string, fsType, source, fsOptstr string,
) *sandbox.MountinfoEntry {
return &sandbox.MountinfoEntry{
ID: id,
Parent: parent,
Root: root,
Target: target,
VfsOptstr: vfsOptstr,
FsType: fsType,
Source: source,
FsOptstr: fsOptstr,
}
}

View File

@ -1,27 +0,0 @@
{
writeText,
buildGoModule,
version,
}:
let
mainFile = writeText "main.go" ''
package main
import "git.gensokyo.uk/security/fortify/test/sandbox"
func main() { sandbox.MustAssertSeccomp() }
'';
in
buildGoModule {
pname = "check-seccomp";
inherit version;
src = ../.;
vendorHash = null;
preBuild = ''
go mod init git.gensokyo.uk/security/fortify/test >& /dev/null
cp ${mainFile} main.go
'';
}

View File

@ -62,9 +62,12 @@ def check_state(name, enablements):
config = instance['config'] config = instance['config']
if len(config['command']) != 1 or not (config['command'][0].startswith("/nix/store/")) or not ( command = f"{name}-start"
config['command'][0].endswith(f"{name}-start")): if not (config['path'].startswith("/nix/store/")) or not (config['path'].endswith(command)):
raise Exception(f"unexpected command {instance['config']['command']}") raise Exception(f"unexpected path {config['path']}")
if len(config['args']) != 1 or config['args'][0] != command:
raise Exception(f"unexpected args {config['args']}")
if config['confinement']['enablements'] != enablements: if config['confinement']['enablements'] != enablements:
raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}") raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}")
@ -102,9 +105,26 @@ 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 state: # Check sandbox outcome:
swaymsg("exec check-sandbox") check_offset = 0
machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/sandbox-ok", timeout=15) 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):
return 1+check_offset+offset
def tmpdir_path(offset, name):
return f"/tmp/fortify.1000/tmpdir/{aid(offset)}/{name}"
# Start fortify permissive defaults outside Wayland session: # Start fortify permissive defaults outside Wayland session:
print(machine.succeed("sudo -u alice -i fortify -v run -a 0 touch /tmp/success-bare")) print(machine.succeed("sudo -u alice -i fortify -v run -a 0 touch /tmp/success-bare"))
@ -146,23 +166,23 @@ machine.succeed("pkill -9 mako")
# Start app (foot) with Wayland enablement: # Start app (foot) with Wayland enablement:
swaymsg("exec ne-foot") swaymsg("exec ne-foot")
wait_for_window("u0_a2@machine") wait_for_window(f"u0_a{aid(0)}@machine")
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n") machine.send_chars("clear; wayland-info && touch /tmp/client-ok\n")
machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-client", timeout=10) machine.wait_for_file(tmpdir_path(0, "client-ok"), timeout=10)
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 acl on XDG_RUNTIME_DIR:
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002")) print(machine.succeed(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: # Verify acl cleanup on XDG_RUNTIME_DIR:
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002", 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: # 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'") swaymsg("exec foot $SHELL -c '(ne-foot) & sleep 1 && fortify show $(fortify ps --short) && touch /tmp/ps-show-ok && cat'")
wait_for_window("u0_a2@machine") wait_for_window(f"u0_a{aid(0)}@machine")
machine.send_chars("clear; wayland-info && touch /tmp/success-client-term\n") machine.send_chars("clear; wayland-info && touch /tmp/term-ok\n")
machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-client-term", timeout=10) machine.wait_for_file(tmpdir_path(0, "term-ok"), timeout=10)
machine.wait_for_file("/tmp/ps-show-ok", timeout=5) machine.wait_for_file("/tmp/ps-show-ok", timeout=5)
collect_state_ui("foot_wayland_term") collect_state_ui("foot_wayland_term")
check_state("ne-foot", 1) check_state("ne-foot", 1)
@ -173,9 +193,9 @@ 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")
wait_for_window("u0_a3@machine") wait_for_window(f"u0_a{aid(1)}@machine")
machine.send_chars("clear; pactl info && touch /tmp/success-pulse\n") machine.send_chars("clear; pactl info && touch /tmp/pulse-ok\n")
machine.wait_for_file("/tmp/fortify.1000/tmpdir/3/success-pulse", timeout=10) machine.wait_for_file(tmpdir_path(1, "pulse-ok"), timeout=15)
collect_state_ui("pulse_wayland") collect_state_ui("pulse_wayland")
check_state("pa-foot", 9) check_state("pa-foot", 9)
machine.send_chars("exit\n") machine.send_chars("exit\n")
@ -183,9 +203,9 @@ machine.wait_until_fails("pgrep foot", timeout=5)
# Test XWayland (foot does not support X): # Test XWayland (foot does not support X):
swaymsg("exec x11-alacritty") swaymsg("exec x11-alacritty")
wait_for_window("u0_a4@machine") wait_for_window(f"u0_a{aid(2)}@machine")
machine.send_chars("clear; glinfo && touch /tmp/success-client-x11\n") machine.send_chars("clear; glinfo && touch /tmp/x11-ok\n")
machine.wait_for_file("/tmp/fortify.1000/tmpdir/4/success-client-x11", timeout=10) machine.wait_for_file(tmpdir_path(2, "x11-ok"), timeout=10)
collect_state_ui("alacritty_x11") collect_state_ui("alacritty_x11")
check_state("x11-alacritty", 2) check_state("x11-alacritty", 2)
machine.send_chars("exit\n") machine.send_chars("exit\n")
@ -193,17 +213,17 @@ machine.wait_until_fails("pgrep alacritty", timeout=5)
# Start app (foot) with direct Wayland access: # Start app (foot) with direct Wayland access:
swaymsg("exec da-foot") swaymsg("exec da-foot")
wait_for_window("u0_a5@machine") wait_for_window(f"u0_a{aid(3)}@machine")
machine.send_chars("clear; wayland-info && touch /tmp/success-direct\n") machine.send_chars("clear; wayland-info && touch /tmp/direct-ok\n")
machine.wait_for_file("/tmp/fortify.1000/tmpdir/5/success-direct", timeout=10)
collect_state_ui("foot_direct") collect_state_ui("foot_direct")
machine.wait_for_file(tmpdir_path(3, "direct-ok"), timeout=10)
check_state("da-foot", 1) check_state("da-foot", 1)
# Verify acl on XDG_RUNTIME_DIR: # Verify acl on XDG_RUNTIME_DIR:
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000005")) print(machine.succeed(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {aid(3) + 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: # Verify acl cleanup on XDG_RUNTIME_DIR:
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000005", timeout=5) machine.wait_until_fails(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {aid(3) + 1000000}", timeout=5)
# 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"))

View File

@ -94,7 +94,7 @@ func bindRawConn(done chan struct{}, rc syscall.RawConn, p, appID, instanceID st
// keep socket alive until done is requested // keep socket alive until done is requested
<-done <-done
runtime.KeepAlive(syncPipe[1].Fd()) runtime.KeepAlive(syncPipe[1])
}); err != nil { }); err != nil {
setupDone <- err setupDone <- err
} }
@ -107,7 +107,7 @@ func bindRawConn(done chan struct{}, rc syscall.RawConn, p, appID, instanceID st
return syncPipe[1], <-setupDone return syncPipe[1], <-setupDone
} }
func bind(fd uintptr, p, appID, instanceID string, syncFD uintptr) error { func bind(fd uintptr, p, appID, instanceID string, syncFd uintptr) error {
// ensure p is available // ensure p is available
if f, err := os.Create(p); err != nil { if f, err := os.Create(p); err != nil {
return err return err
@ -117,5 +117,5 @@ func bind(fd uintptr, p, appID, instanceID string, syncFD uintptr) error {
return err return err
} }
return bindWaylandFd(p, fd, appID, instanceID, syncFD) return bindWaylandFd(p, fd, appID, instanceID, syncFd)
} }

View File

@ -25,11 +25,11 @@ var resErr = [...]error{
2: errors.New("wp_security_context_v1 not available"), 2: errors.New("wp_security_context_v1 not available"),
} }
func bindWaylandFd(socketPath string, fd uintptr, appID, instanceID string, syncFD uintptr) error { func bindWaylandFd(socketPath string, fd uintptr, appID, instanceID string, syncFd uintptr) error {
if hasNull(appID) || hasNull(instanceID) { if hasNull(appID) || hasNull(instanceID) {
return ErrContainsNull return ErrContainsNull
} }
res := C.f_bind_wayland_fd(C.CString(socketPath), C.int(fd), C.CString(appID), C.CString(instanceID), C.int(syncFD)) res := C.f_bind_wayland_fd(C.CString(socketPath), C.int(fd), C.CString(appID), C.CString(instanceID), C.int(syncFd))
return resErr[int32(res)] return resErr[int32(res)]
} }