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

View File

@ -12,9 +12,7 @@ import (
"git.gensokyo.uk/security/fortify/command"
"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/app/init0"
"git.gensokyo.uk/security/fortify/internal/app/shim"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/sys"
@ -39,7 +37,6 @@ func init() {
func main() {
// early init path, skips root check and duplicate PR_SET_DUMPABLE
sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg)
init0.TryArgv0()
if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
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(&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("init", command.UsageInternal, func([]string) error { init0.Main(); return errSuccess })
{
var (
@ -124,7 +119,7 @@ func main() {
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)
app := bundle
@ -140,7 +135,7 @@ func main() {
log.Printf("metadata path %q is not a file", pathSet.metaPath)
return syscall.EBADMSG
} else {
app = loadBundleInfo(pathSet.metaPath, cleanup)
app = loadAppInfo(pathSet.metaPath, cleanup)
if app.ID != bundle.ID {
cleanup()
log.Printf("app %q claims to have identifier %q",
@ -273,7 +268,7 @@ func main() {
id := args[0]
pathSet := pathSetByApp(id)
app := loadBundleInfo(pathSet.metaPath, func() {})
app := loadAppInfo(pathSet.metaPath, func() {})
if app.ID != id {
log.Printf("app %q claims to have identifier %q", id, app.ID)
return syscall.EBADE
@ -322,51 +317,7 @@ func main() {
}
argv = append(argv, args[1:]...)
config := &fst.Config{
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,
},
}
config := app.toFst(pathSet, argv, flagDropShell)
/*
Expose GPU devices.

View File

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

View File

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

View File

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

12
flake.lock generated
View File

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

100
flake.nix
View File

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

View File

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

View File

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

View File

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

2
go.mod
View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import (
"git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/sandbox"
"git.gensokyo.uk/security/fortify/system"
)
@ -12,20 +12,20 @@ var testCasesNixos = []sealTestCase{
{
"nixos chromium direct wayland", new(stubNixOS),
&fst.Config{
ID: "org.chromium.Chromium",
Command: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
ID: "org.chromium.Chromium",
Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
Confinement: fst.ConfinementConfig{
AppID: 1, Groups: []string{}, Username: "u0_a1",
Outer: "/var/lib/persist/module/fortify/0/1",
Sandbox: &fst.SandboxConfig{
UserNS: true, Net: true, MapRealUID: true, DirectWayland: true, Env: nil, AutoEtc: true,
Userns: true, Net: true, MapRealUID: true, DirectWayland: true, Env: nil, AutoEtc: true,
Filesystem: []*fst.FilesystemConfig{
{Src: "/bin", Must: true}, {Src: "/usr/bin", Must: true},
{Src: "/nix/store", Must: true}, {Src: "/run/current-system", Must: true},
{Src: "/sys/block"}, {Src: "/sys/bus"}, {Src: "/sys/class"}, {Src: "/sys/dev"}, {Src: "/sys/devices"},
{Src: "/run/opengl-driver", Must: true}, {Src: "/dev/dri", Device: true},
},
Override: []string{"/var/run/nscd"},
Cover: []string{"/var/run/nscd"},
},
SystemBus: &dbus.Config{
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
@ -88,136 +88,133 @@ var testCasesNixos = []sealTestCase{
}).
UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write).
UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write),
(&bwrap.Config{
Net: true,
UserNS: true,
Chdir: "/var/lib/persist/module/fortify/0/1",
Clearenv: true,
SetEnv: map[string]string{
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/1971/bus",
"DBUS_SYSTEM_BUS_ADDRESS": "unix:path=/run/dbus/system_bus_socket",
"HOME": "/var/lib/persist/module/fortify/0/1",
"PULSE_COOKIE": fst.Tmp + "/pulse-cookie",
"PULSE_SERVER": "unix:/run/user/1971/pulse/native",
"SHELL": "/run/current-system/sw/bin/zsh",
"TERM": "xterm-256color",
"USER": "u0_a1",
"WAYLAND_DISPLAY": "wayland-0",
"XDG_RUNTIME_DIR": "/run/user/1971",
"XDG_SESSION_CLASS": "user",
"XDG_SESSION_TYPE": "tty",
&sandbox.Params{
Uid: 1971,
Gid: 100,
Flags: sandbox.FAllowNet | sandbox.FAllowUserns,
Dir: "/var/lib/persist/module/fortify/0/1",
Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
Args: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
Env: []string{
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus",
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
"HOME=/var/lib/persist/module/fortify/0/1",
"PULSE_COOKIE=" + fst.Tmp + "/pulse-cookie",
"PULSE_SERVER=unix:/run/user/1971/pulse/native",
"TERM=xterm-256color",
"USER=u0_a1",
"WAYLAND_DISPLAY=wayland-0",
"XDG_RUNTIME_DIR=/run/user/1971",
"XDG_SESSION_CLASS=user",
"XDG_SESSION_TYPE=tty",
},
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"),
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),
},
},
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,18 +0,0 @@
package init0
import (
"os"
"path"
"git.gensokyo.uk/security/fortify/internal"
)
// used by the parent process
// TryArgv0 calls [Main] if 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 (
"context"
"errors"
"fmt"
"log"
"os/exec"
"path/filepath"
"strings"
"time"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/app/shim"
"git.gensokyo.uk/security/fortify/internal/fmsg"
@ -21,7 +18,7 @@ import (
const shimSetupTimeout = 5 * time.Second
func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
func (seal *outcome) Run(rs *fst.RunState) error {
if !seal.f.CompareAndSwap(false, true) {
// run does much more than just starting a process; calling it twice, even if the first call fails, will result
// in inconsistent state that is impossible to clean up; return here to limit damage and hopefully give the
@ -37,33 +34,11 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
fmsg.Verbosef("version %s", internal.Version())
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
*/
if err := seal.sys.Commit(ctx); err != nil {
if err := seal.sys.Commit(seal.ctx); err != nil {
return err
}
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(
seal.user.aid.String(),
seal.user.supp,
seal.bwrapSync,
); err != nil {
return err
} else {
@ -145,7 +119,7 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
rs.Time = startTime
}
c, cancel := context.WithTimeout(ctx, shimSetupTimeout)
ctx, cancel := context.WithTimeout(seal.ctx, shimSetupTimeout)
defer cancel()
go func() {
@ -154,11 +128,9 @@ func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
cancel()
}()
if err := cmd.Serve(c, &shim.Payload{
Argv: seal.command,
Exec: shimExec,
Bwrap: seal.container,
Home: seal.user.data,
if err := cmd.Serve(ctx, &shim.Params{
Container: seal.container,
Home: seal.user.data,
Verbose: fmsg.Load(),
}); err != nil {
@ -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
// however a kill signal could not be delivered (should actually always happen like that since fsu)
// the effects of this is similar to the alternative exit path and ensures shim death
case err := <-cmd.WaitFallback():
case err := <-cmd.Fallback():
rs.ExitCode = 255
log.Printf("cannot terminate shim on faulted setup: %v", err)
// alternative exit path relying on shim behaviour on monitor process exit
case <-ctx.Done():
case <-seal.ctx.Done():
fmsg.Verbose("alternative exit path selected")
}
fmsg.Resume()
if seal.sync != nil {
if err := seal.sync.Close(); err != nil {
log.Printf("cannot close wayland security context: %v", err)
}
}
if seal.dbusMsg != nil {
// dump dbus message buffer
seal.dbusMsg()
}

View File

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

View File

@ -7,18 +7,26 @@ import (
"os"
"os/exec"
"os/signal"
"path"
"strconv"
"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/app/init0"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"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
// proceed with caution!
@ -27,17 +35,15 @@ func Main() {
// USE WITH CAUTION
fmsg.Prepare("shim")
// setting this prevents ptrace
if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
}
// receive setup payload
var (
payload Payload
params Params
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) {
log.Fatal("invalid config descriptor")
}
@ -45,32 +51,26 @@ func Main() {
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 {
internal.InstallFmsg(payload.Verbose)
internal.InstallFmsg(params.Verbose)
closeSetup = f
}
if payload.Bwrap == nil {
log.Fatal("bwrap config not supplied")
}
// restore bwrap sync fd
var syncFd *os.File
if payload.Sync != nil {
syncFd = os.NewFile(*payload.Sync, "sync")
if params.Container == nil || params.Container.Ops == nil {
log.Fatal("invalid container params")
}
// close setup socket
if err := closeSetup(); err != nil {
log.Println("cannot close setup pipe:", err)
log.Printf("cannot close setup pipe: %v", err)
// not fatal
}
// 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 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)
}
} else {
@ -79,72 +79,37 @@ func Main() {
// home directory is created, proceed
} 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
// resolve argv0
ic.Argv = payload.Argv
if len(ic.Argv) > 0 {
// looked up from $PATH by parent
ic.Argv0 = payload.Exec[1]
} else {
// no argv, look up shell instead
var ok bool
if payload.Bwrap.SetEnv == nil {
log.Fatal("no command was specified and environment is unset")
}
if ic.Argv0, ok = payload.Bwrap.SetEnv["SHELL"]; !ok {
log.Fatal("no command was specified and $SHELL was unset")
}
ic.Argv = []string{ic.Argv0}
var name string
if len(params.Container.Args) > 0 {
name = params.Container.Args[0]
}
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)
defer stop() // unreachable
if b, err := helper.NewBwrap(
ctx, path.Join(fst.Tmp, "sbin/init0"),
nil, false,
func(int, int) []string { return make([]string, 0) },
func(cmd *exec.Cmd) { cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr },
extraFiles,
conf, syncFd,
); err != nil {
log.Fatalf("malformed sandbox config: %v", err)
} else {
// run and pass through exit code
if err = b.Start(); err != nil {
log.Fatalf("cannot start target process: %v", err)
} else if err = b.Wait(); err != nil {
var exitError *exec.ExitError
if !errors.As(err, &exitError) {
log.Printf("wait: %v", err)
internal.Exit(127)
panic("unreachable")
container := sandbox.New(ctx, name)
container.Params = *params.Container
container.Stdin, container.Stdout, container.Stderr = os.Stdin, os.Stdout, os.Stderr
container.Cancel = func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) }
container.WaitDelay = 2 * time.Second
if err := container.Start(); err != nil {
fmsg.PrintBaseError(err, "cannot start container:")
os.Exit(1)
}
if err := container.Serve(); err != nil {
fmsg.PrintBaseError(err, "cannot configure container:")
}
if err := container.Wait(); err != nil {
var exitError *exec.ExitError
if !errors.As(err, &exitError) {
if errors.Is(err, context.Canceled) {
os.Exit(2)
}
internal.Exit(exitError.ExitCode())
panic("unreachable")
log.Printf("wait: %v", err)
os.Exit(127)
}
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"
"strconv"
"strings"
"syscall"
"time"
"git.gensokyo.uk/security/fortify/helper/proc"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/sandbox"
@ -25,10 +25,11 @@ type Shim struct {
killFallback chan error
// monitor to shim 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 {
if s.cmd == nil {
return "(unused shim manager)"
@ -36,21 +37,9 @@ func (s *Shim) String() 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(
// string representation of application id
aid string,
// string representation of supplementary group ids
supp []string,
// bwrap --sync-fd
syncFd *os.File,
) (*time.Time, error) {
// prepare user switcher invocation
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.Dir = "/"
// pass sync fd if set
if syncFd != nil {
fd := proc.ExtraFile(s.cmd, syncFd)
s.sync = &fd
}
fmsg.Verbose("starting shim via fsu:", s.cmd)
// withhold messages to stderr
fmsg.Suspend()
@ -90,10 +73,11 @@ func (s *Shim) Start(
"cannot start fsu:")
}
startTime := time.Now().UTC()
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
s.killFallback = make(chan error, 1)
killShim := func() {
@ -103,9 +87,8 @@ func (s *Shim) Serve(ctx context.Context, payload *Payload) error {
}
defer func() { killShim() }()
payload.Sync = s.sync
encodeErr := make(chan error)
go func() { encodeErr <- s.encoder.Encode(payload) }()
go func() { encodeErr <- s.encoder.Encode(params) }()
select {
// encode return indicates setup completion
@ -121,11 +104,11 @@ func (s *Shim) Serve(ctx context.Context, payload *Payload) error {
case <-ctx.Done():
err := ctx.Err()
if errors.Is(err, context.Canceled) {
return fmsg.WrapError(errors.New("shim setup canceled"),
return fmsg.WrapError(syscall.ECANCELED,
"shim setup canceled")
}
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")
}
// unreachable

View File

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

View File

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

View File

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

20
main.go
View File

@ -20,7 +20,6 @@ import (
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/app/init0"
"git.gensokyo.uk/security/fortify/internal/app/shim"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/state"
@ -43,7 +42,6 @@ var std sys.State = new(sys.Std)
func main() {
// early init path, skips root check and duplicate PR_SET_DUMPABLE
sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg)
init0.TryArgv0()
if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
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(&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("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 {
if len(args) < 1 {
@ -87,10 +83,9 @@ func buildCommand(out io.Writer) command.Command {
// config extraArgs...
config := tryPath(args[0])
config.Command = append(config.Command, args[1:]...)
config.Args = append(config.Args, args[1:]...)
// invoke app
runApp(app.MustNew(std), config)
runApp(config)
panic("unreachable")
})
@ -112,8 +107,8 @@ func buildCommand(out io.Writer) command.Command {
c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error {
// initialise config from flags
config := &fst.Config{
ID: fid,
Command: args,
ID: fid,
Args: args,
}
if aid < 0 || aid > 9999 {
@ -199,7 +194,7 @@ func buildCommand(out io.Writer) command.Command {
}
// invoke app
runApp(app.MustNew(std), config)
runApp(config)
panic("unreachable")
}).
Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"),
@ -279,10 +274,11 @@ func buildCommand(out io.Writer) command.Command {
return c
}
func runApp(a fst.App, config *fst.Config) {
func runApp(config *fst.Config) {
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer stop() // unreachable
a := app.MustNew(ctx, std)
rs := new(fst.RunState)
if sa, err := a.Seal(config); err != nil {
@ -290,7 +286,7 @@ func runApp(a fst.App, config *fst.Config) {
rs.ExitCode = 1
} else {
// this updates ExitCode
app.PrintRunStateErr(rs, sa.Run(ctx, rs))
app.PrintRunStateErr(rs, sa.Run(rs))
}
internal.Exit(rs.ExitCode)
}

View File

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

View File

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

View File

@ -19,6 +19,13 @@
gnutar,
coreutils,
# for passthru.buildInputs
go,
gcc,
# for check
util-linux,
glibc, # for ldd
withStatic ? stdenv.hostPlatform.isStatic,
}:
@ -30,7 +37,7 @@ buildGoModule rec {
src = builtins.path {
name = "${pname}-src";
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;
@ -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 {
if v, err := strconv.Atoi(name); err != nil {
fmsg.Verbosef("name cannot be interpreted as int64: %v", err)
if !errors.Is(err, strconv.ErrSyntax) {
fmsg.Verbosef("name cannot be interpreted as int64: %v", err)
}
return nil
} else {
fmsg.Verbosef("trying config stream from %d", v)
fd := uintptr(v)
if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 {
if errors.Is(errno, syscall.EBADF) {

View File

@ -89,10 +89,10 @@ func printShowInstance(
flags = append(flags, name)
}
}
writeFlag("userns", sandbox.UserNS)
writeFlag("userns", sandbox.Userns)
writeFlag("net", sandbox.Net)
writeFlag("dev", sandbox.Dev)
writeFlag("tty", sandbox.NoNewSession)
writeFlag("tty", sandbox.Tty)
writeFlag("mapuid", sandbox.MapRealUID)
writeFlag("directwl", sandbox.DirectWayland)
writeFlag("autoetc", sandbox.AutoEtc)
@ -107,14 +107,14 @@ func printShowInstance(
}
t.Printf(" Etc:\t%s\n", etc)
if len(sandbox.Override) > 0 {
t.Printf(" Overrides:\t%s\n", strings.Join(sandbox.Override, " "))
if len(sandbox.Cover) > 0 {
t.Printf(" Cover:\t%s\n", strings.Join(sandbox.Cover, " "))
}
// Env map[string]string `json:"env"`
// Link [][2]string `json:"symlink"`
}
t.Printf(" Command:\t%s\n", strings.Join(config.Command, " "))
t.Printf(" Command:\t%s\n", strings.Join(config.Args, " "))
t.Printf("\n")
if !short {
@ -256,7 +256,7 @@ func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON boo
)
if e.Config != nil {
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)
}
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"
Flags: userns net dev tty mapuid autoetc
Etc: /etc
Overrides: /var/run/nscd
Cover: /var/run/nscd
Command: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
Filesystem
@ -127,7 +127,7 @@ App
Hostname: "localhost"
Flags: userns net dev tty mapuid autoetc
Etc: /etc
Overrides: /var/run/nscd
Cover: /var/run/nscd
Command: chromium --ignore-gpu-blocklist --disable-smooth-scrolling --enable-features=UseOzonePlatform --ozone-platform=wayland
Filesystem
@ -192,7 +192,8 @@ App
"pid": 3735928559,
"config": {
"id": "org.chromium.Chromium",
"command": [
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
@ -209,24 +210,19 @@ App
"home": "/var/lib/persist/home/org.chromium.Chromium",
"sandbox": {
"hostname": "localhost",
"seccomp": 32,
"devel": true,
"userns": true,
"net": true,
"dev": true,
"syscall": {
"compat": false,
"deny_devel": true,
"multiarch": true,
"linux32": false,
"can": false,
"bluetooth": false
},
"no_new_session": true,
"map_real_uid": true,
"tty": true,
"multiarch": true,
"env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
},
"map_real_uid": true,
"dev": true,
"filesystem": [
{
"src": "/nix/store"
@ -259,7 +255,7 @@ App
],
"etc": "/etc",
"auto_etc": true,
"override": [
"cover": [
"/var/run/nscd"
]
},
@ -320,7 +316,8 @@ App
`},
{"json config", nil, fst.Template(), false, true, `{
"id": "org.chromium.Chromium",
"command": [
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
@ -337,24 +334,19 @@ App
"home": "/var/lib/persist/home/org.chromium.Chromium",
"sandbox": {
"hostname": "localhost",
"seccomp": 32,
"devel": true,
"userns": true,
"net": true,
"dev": true,
"syscall": {
"compat": false,
"deny_devel": true,
"multiarch": true,
"linux32": false,
"can": false,
"bluetooth": false
},
"no_new_session": true,
"map_real_uid": true,
"tty": true,
"multiarch": true,
"env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
},
"map_real_uid": true,
"dev": true,
"filesystem": [
{
"src": "/nix/store"
@ -387,7 +379,7 @@ App
],
"etc": "/etc",
"auto_etc": true,
"override": [
"cover": [
"/var/run/nscd"
]
},
@ -506,7 +498,8 @@ func Test_printPs(t *testing.T) {
"pid": 3735928559,
"config": {
"id": "org.chromium.Chromium",
"command": [
"path": "/run/current-system/sw/bin/chromium",
"args": [
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
@ -523,24 +516,19 @@ func Test_printPs(t *testing.T) {
"home": "/var/lib/persist/home/org.chromium.Chromium",
"sandbox": {
"hostname": "localhost",
"seccomp": 32,
"devel": true,
"userns": true,
"net": true,
"dev": true,
"syscall": {
"compat": false,
"deny_devel": true,
"multiarch": true,
"linux32": false,
"can": false,
"bluetooth": false
},
"no_new_session": true,
"map_real_uid": true,
"tty": true,
"multiarch": true,
"env": {
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
},
"map_real_uid": true,
"dev": true,
"filesystem": [
{
"src": "/nix/store"
@ -573,7 +561,7 @@ func Test_printPs(t *testing.T) {
],
"etc": "/etc",
"auto_etc": true,
"override": [
"cover": [
"/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.
ExtraFiles []*os.File
InitParams
// Custom [exec.Cmd] initialisation function.
CommandContext func(ctx context.Context) (cmd *exec.Cmd)
@ -67,14 +66,16 @@ type (
Stdout io.Writer
Stderr io.Writer
Cancel func() error
Cancel func(cmd *exec.Cmd) error
WaitDelay time.Duration
cmd *exec.Cmd
ctx context.Context
Params
}
InitParams struct {
// Params holds container configuration and is safe to serialise.
Params struct {
// Working directory in the container.
Dir string
// Initial process environment.
@ -100,7 +101,9 @@ type (
Ops []Op
Op interface {
apply(params *InitParams) error
early(params *Params) error
apply(params *Params) error
prefix() string
Is(op Op) bool
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.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.SysProcAttr = &syscall.SysProcAttr{
Setsid: p.Flags&FAllowTTY == 0,
@ -183,7 +191,11 @@ func (p *Container) Serve() error {
panic("invalid serve")
}
setup := p.setup
p.setup = nil
if p.Path != "" && !path.IsAbs(p.Path) {
p.cancel()
return msg.WrapErr(syscall.EINVAL,
fmt.Sprintf("invalid executable path %q", p.Path))
}
@ -192,6 +204,7 @@ func (p *Container) Serve() error {
if p.name == "" {
p.Path = os.Getenv("SHELL")
if !path.IsAbs(p.Path) {
p.cancel()
return msg.WrapErr(syscall.EBADE,
"no command specified and $SHELL is invalid")
}
@ -199,23 +212,26 @@ func (p *Container) Serve() error {
} else if path.IsAbs(p.name) {
p.Path = p.name
} else if v, err := exec.LookPath(p.name); err != nil {
p.cancel()
return msg.WrapErr(err, err.Error())
} else {
p.Path = v
}
}
setup := p.setup
p.setup = nil
return setup.Encode(
err := setup.Encode(
&initParams{
p.InitParams,
p.Params,
syscall.Getuid(),
syscall.Getgid(),
len(p.ExtraFiles),
msg.IsVerbose(),
},
)
if err != nil {
p.cancel()
}
return err
}
func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() }
@ -227,6 +243,6 @@ func (p *Container) String() string {
func New(ctx context.Context, name string, args ...string) *Container {
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 (
"bytes"
"context"
"encoding/json"
"encoding/gob"
"log"
"os"
"os/exec"
"strings"
"syscall"
"testing"
"time"
@ -17,7 +18,12 @@ import (
"git.gensokyo.uk/security/fortify/ldd"
"git.gensokyo.uk/security/fortify/sandbox"
"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) {
@ -33,7 +39,7 @@ func TestContainer(t *testing.T) {
name string
flags sandbox.HardeningFlags
ops *sandbox.Ops
mnt []*check.Mntent
mnt []*vfs.MountInfoEntry
host string
}{
{"minimal", 0, new(sandbox.Ops), nil, "test-minimal"},
@ -42,21 +48,23 @@ func TestContainer(t *testing.T) {
{"tmpfs", 0,
new(sandbox.Ops).
Tmpfs(fst.Tmp, 0, 0755),
[]*check.Mntent{
{FSName: "tmpfs", Dir: fst.Tmp, Type: "tmpfs", Opts: "\x00"},
[]*vfs.MountInfoEntry{
e("/", fst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
}, "test-tmpfs"},
{"dev", sandbox.FAllowTTY, // go test output is not a tty
new(sandbox.Ops).
Dev("/dev"),
[]*check.Mntent{
{FSName: "devtmpfs", Dir: "/dev", Type: "tmpfs", Opts: "\x00"},
{FSName: "devtmpfs", Dir: "/dev/null", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
{FSName: "devtmpfs", Dir: "/dev/zero", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
{FSName: "devtmpfs", Dir: "/dev/full", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
{FSName: "devtmpfs", Dir: "/dev/random", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
{FSName: "devtmpfs", Dir: "/dev/urandom", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
{FSName: "devtmpfs", Dir: "/dev/tty", Type: "devtmpfs", Opts: "\x00", Freq: -1, Passno: -1},
{FSName: "devpts", Dir: "/dev/pts", Type: "devpts", Opts: "rw,nosuid,noexec,relatime,mode=620,ptmxmode=666", Freq: 0, Passno: 0},
Dev("/dev").
Mqueue("/dev/mqueue"),
[]*vfs.MountInfoEntry{
e("/", "/dev", "rw,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
e("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
e("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
e("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
}, ""},
}
@ -65,7 +73,7 @@ func TestContainer(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
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)
container.Uid = 1000
container.Gid = 100
@ -84,7 +92,10 @@ func TestContainer(t *testing.T) {
container.
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
var libPaths []string
if entries, err := ldd.ExecFilter(ctx,
@ -99,25 +110,26 @@ func TestContainer(t *testing.T) {
for _, name := range libPaths {
container.Bind(name, name, 0)
}
// needs /proc to check mountinfo
container.Proc("/proc")
mnt := make([]*check.Mntent, 0, 3+len(libPaths))
mnt = append(mnt, &check.Mntent{FSName: "rootfs", Dir: "/", Type: "tmpfs", Opts: "host_passthrough"})
mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
mnt = append(mnt, e("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore))
mnt = append(mnt, tc.mnt...)
mnt = append(mnt,
&check.Mntent{FSName: "tmpfs", Dir: "/tmp", Type: "tmpfs", Opts: "host_passthrough"},
&check.Mntent{FSName: "\x00", Dir: os.Args[0], Type: "\x00", Opts: "\x00"})
e("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
e(ignore, os.Args[0], "ro,nosuid,nodev,relatime", ignore, ignore, ignore),
e(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore),
)
for _, name := range libPaths {
mnt = append(mnt, &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"})
mntentWant := new(bytes.Buffer)
if err := json.NewEncoder(mntentWant).Encode(mnt); err != nil {
t.Fatalf("cannot serialise mntent: %v", err)
mnt = append(mnt, e("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"))
want := new(bytes.Buffer)
if err := gob.NewEncoder(want).Encode(mnt); err != nil {
t.Fatalf("cannot serialise expected mount points: %v", err)
}
container.Stdin = mntentWant
// needs /proc to check mntent
container.Proc("/proc")
container.Stdin = want
if err := container.Start(); err != nil {
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) {
container := sandbox.New(context.TODO(), "ldd", "/usr/bin/env")
container.Flags |= sandbox.FAllowDevel
@ -171,9 +198,55 @@ func TestHelperCheckContainer(t *testing.T) {
} else if name != os.Args[5] {
t.Errorf("Hostname: %q, want %q", name, os.Args[5])
}
if p, err := os.ReadFile("/etc/hostname"); err != nil {
t.Fatalf("%v", err)
} else if string(p) != os.Args[5] {
t.Errorf("/etc/hostname: %q, want %q", string(p), os.Args[5])
}
})
t.Run("mount", func(t *testing.T) {
var mnt []*vfs.MountInfoEntry
if err := gob.NewDecoder(os.Stdin).Decode(&mnt); err != nil {
t.Fatalf("cannot receive expected mount points: %v", err)
}
var d *vfs.MountInfoDecoder
if f, err := os.Open("/proc/self/mountinfo"); err != nil {
t.Fatalf("cannot open mountinfo: %v", err)
} else {
d = vfs.NewMountInfoDecoder(f)
}
i := 0
for cur := range d.Entries() {
if i == len(mnt) {
t.Errorf("got more than %d entries", len(mnt))
break
}
// ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",relatime")
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",noatime")
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",relatime")
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime")
if !cur.EqualWithIgnore(mnt[i], "\x00") {
t.Errorf("[FAIL] %s", cur)
} else {
t.Logf("[ OK ] %s", cur)
}
i++
}
if err := d.Err(); err != nil {
t.Errorf("cannot parse mountinfo: %v", err)
}
if i != len(mnt) {
t.Errorf("got %d entries, want %d", i, len(mnt))
}
})
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 {

View File

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

View File

@ -4,86 +4,105 @@ import (
"errors"
"fmt"
"os"
"strings"
"path/filepath"
"syscall"
"git.gensokyo.uk/security/fortify/sandbox/vfs"
)
const (
BindOptional = 1 << iota
BindSource
BindRecursive
BindWritable
BindDevices
)
func bindMount(src, dest string, flags int) error {
target := toSysroot(dest)
var source string
if flags&BindSource == 0 {
// this is what bwrap does, so the behaviour is kept for now,
// however recursively resolving links might improve user experience
if rp, err := realpathHost(src); err != nil {
if os.IsNotExist(err) {
if flags&BindOptional != 0 {
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")
func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) error {
if eq {
msg.Verbosef("resolved %q flags %#x", target, flags)
} else {
source = toHost(src)
msg.Verbosef("resolved %q on %q flags %#x", source, target, flags)
}
if fi, err := os.Stat(source); err != nil {
return msg.WrapErr(err, err.Error())
} else if fi.IsDir() {
if err = os.MkdirAll(target, 0755); err != nil {
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))
}
if err := syscall.Mount(source, target, "",
syscall.MS_SILENT|syscall.MS_BIND|flags&syscall.MS_REC, ""); err != nil {
return wrapErrSuffix(err,
fmt.Sprintf("cannot create %q:", dest))
fmt.Sprintf("cannot mount %q on %q:", source, target))
}
var mf uintptr = syscall.MS_SILENT | syscall.MS_BIND
if flags&BindRecursive != 0 {
mf |= syscall.MS_REC
}
if flags&BindWritable == 0 {
mf |= syscall.MS_RDONLY
}
if flags&BindDevices == 0 {
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)
var targetFinal string
if v, err := filepath.EvalSymlinks(target); err != nil {
return msg.WrapErr(err, err.Error())
} else {
targetFinal = v
if targetFinal != target {
msg.Verbosef("target resolves to %q", targetFinal)
}
}
return wrapErrSuffix(syscall.Mount(source, target, "", mf, ""),
fmt.Sprintf("cannot bind %q on %q:", src, dest))
// 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
}
for cur := range n.Collective() {
err = remountWithFlags(cur, mf)
if err != nil && !errors.Is(err, syscall.EACCES) {
return err
}
}
return nil
})
}
func remountWithFlags(n *vfs.MountInfoNode, mf uintptr) error {
kf, unmatched := n.Flags()
if len(unmatched) != 0 {
msg.Verbosef("unmatched vfs options: %q", unmatched)
}
if kf&mf != mf {
return wrapErrSuffix(syscall.Mount("none", n.Clean, "",
syscall.MS_SILENT|syscall.MS_BIND|syscall.MS_REMOUNT|kf|mf,
""),
fmt.Sprintf("cannot remount %q:", n.Clean))
}
return nil
}
func mountTmpfs(fsname, name string, size int, perm os.FileMode) error {
target := toSysroot(name)
if err := os.MkdirAll(target, perm); err != nil {
return err
if err := os.MkdirAll(target, parentPerm(perm)); err != nil {
return msg.WrapErr(err, err.Error())
}
opt := fmt.Sprintf("mode=%#o", perm)
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),
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 (
"errors"
"fmt"
"io/fs"
"os"
"path"
"strconv"
"strings"
"syscall"
"git.gensokyo.uk/security/fortify/sandbox/vfs"
)
const (
@ -26,50 +30,65 @@ func toHost(name string) string {
return path.Join(hostPath, name)
}
func realpathHost(name string) (string, error) {
source := toHost(name)
rp, err := os.Readlink(source)
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
func createFile(name string, perm, pperm os.FileMode, content []byte) error {
if err := os.MkdirAll(path.Dir(name), pperm); err != nil {
return msg.WrapErr(err, err.Error())
}
f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm)
if err != nil {
return err
return msg.WrapErr(err, err.Error())
}
if content != nil {
_, err = f.Write(content)
if err != nil {
err = msg.WrapErr(err, err.Error())
}
}
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)
if err != nil {
if !os.IsNotExist(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 {
err = syscall.EISDIR
err = msg.WrapErr(syscall.EISDIR,
fmt.Sprintf("path %q is a directory", name))
}
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)
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",
got, tc.want)
return
@ -111,11 +111,14 @@ func TestExport(t *testing.T) {
t.Run("close partial read", func(t *testing.T) {
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)
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)
return
}

View File

@ -6,6 +6,8 @@ import (
"math"
"os"
"path"
"path/filepath"
"slices"
"syscall"
"unsafe"
)
@ -14,20 +16,77 @@ func init() { gob.Register(new(BindMount)) }
// BindMount bind mounts host path Source on container path Target.
type BindMount struct {
Source, Target string
Source, SourceFinal, Target string
Flags int
}
func (b *BindMount) apply(*InitParams) error {
if !path.IsAbs(b.Source) || !path.IsAbs(b.Target) {
const (
BindOptional = 1 << iota
BindWritable
BindDevice
)
func (b *BindMount) early(*Params) error {
if !path.IsAbs(b.Source) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", b.Source))
}
if v, err := filepath.EvalSymlinks(b.Source); err != nil {
if os.IsNotExist(err) && b.Flags&BindOptional != 0 {
b.SourceFinal = "\x00"
return nil
}
return 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,
"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 (*BindMount) prefix() string { return "mounting" }
func (b *BindMount) String() string {
if b.Source == b.Target {
return fmt.Sprintf("%q flags %#x", b.Source, b.Flags)
@ -35,54 +94,70 @@ func (b *BindMount) String() string {
return fmt.Sprintf("%q on %q flags %#x", b.Source, b.Target, b.Flags&BindWritable)
}
func (f *Ops) Bind(source, target string, flags int) *Ops {
*f = append(*f, &BindMount{source, target, flags | BindRecursive})
*f = append(*f, &BindMount{source, "", target, flags})
return f
}
func init() { gob.Register(new(MountProc)) }
// MountProc mounts a private proc instance on container Path.
type MountProc struct {
Path string
}
// MountProc mounts a private instance of proc.
type MountProc string
func (p *MountProc) apply(*InitParams) error {
if !path.IsAbs(p.Path) {
func (p MountProc) early(*Params) error { return nil }
func (p MountProc) apply(*Params) error {
v := string(p)
if !path.IsAbs(v) {
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 {
return msg.WrapErr(err, err.Error())
}
return wrapErrSuffix(syscall.Mount("proc", target, "proc",
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)) }
// MountDev mounts dev on container Path.
type MountDev struct {
Path string
}
// MountDev mounts part of host dev.
type MountDev string
func (d *MountDev) apply(params *InitParams) error {
if !path.IsAbs(d.Path) {
func (d MountDev) early(*Params) error { return nil }
func (d MountDev) apply(params *Params) error {
v := string(d)
if !path.IsAbs(v) {
return msg.WrapErr(syscall.EBADE,
fmt.Sprintf("path %q is not absolute", 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
}
for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} {
if err := bindMount(
"/dev/"+name, path.Join(d.Path, name),
BindSource|BindDevices,
targetPath := toSysroot(path.Join(v, name))
if err := ensureFile(targetPath, 0444, 0755); err != nil {
return err
}
if err := hostProc.bindMount(
toHost("/dev/"+name),
targetPath,
0,
true,
); err != nil {
return err
}
@ -125,9 +200,17 @@ func (d *MountDev) apply(params *InitParams) error {
syscall.SYS_IOCTL, 1, syscall.TIOCGWINSZ,
uintptr(unsafe.Pointer(&buf[0])),
); errno == 0 {
if err := bindMount(
"/proc/self/fd/1", path.Join(d.Path, "console"),
BindDevices,
consolePath := toSysroot(path.Join(v, "console"))
if err := ensureFile(consolePath, 0444, 0755); err != nil {
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 {
return err
}
@ -137,17 +220,42 @@ func (d *MountDev) apply(params *InitParams) error {
return nil
}
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 (d MountDev) Is(op Op) bool { vd, ok := op.(MountDev); return ok && d == vd }
func (MountDev) prefix() string { return "mounting" }
func (d MountDev) String() string { return fmt.Sprintf("dev on %q", string(d)) }
func (f *Ops) Dev(dest string) *Ops {
*f = append(*f, &MountDev{dest})
*f = append(*f, MountDev(dest))
return f
}
func (p *MountProc) Is(op Op) bool { vp, ok := op.(*MountProc); return ok && *p == *vp }
func (p *MountProc) String() string { return fmt.Sprintf("proc on %q", p.Path) }
func (f *Ops) Proc(dest string) *Ops {
*f = append(*f, &MountProc{dest})
func init() { gob.Register(new(MountMqueue)) }
// MountMqueue mounts a private mqueue instance on container Path.
type MountMqueue string
func (m MountMqueue) early(*Params) error { return nil }
func (m MountMqueue) apply(*Params) 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
}
@ -160,7 +268,8 @@ type MountTmpfs struct {
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) {
return msg.WrapErr(syscall.EBADE,
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 (*MountTmpfs) prefix() string { return "mounting" }
func (t *MountTmpfs) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }
func (f *Ops) Tmpfs(dest string, size int, perm os.FileMode) *Ops {
*f = append(*f, &MountTmpfs{dest, size, perm})
return f
}
func init() { gob.Register(new(Symlink)) }
// Symlink creates a symlink in the container filesystem.
type Symlink [2]string
func (l *Symlink) early(*Params) error { 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"
const (
O_PATH = 0x200000
PR_SET_NO_NEW_PRIVS = 0x26
CAP_SYS_ADMIN = 0x15
)
const (
SUID_DUMP_DISABLE = iota
SUID_DUMP_USER
@ -16,14 +22,6 @@ func SetDumpable(dumpable uintptr) error {
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
// EINTR error. This appears to be required even though we install all
// 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,
...
}:
let
testCases = import ./sandbox/case {
inherit (pkgs) lib callPackage foot;
inherit (config.environment.fortify.package) version;
};
in
{
users.users = {
alice = {
@ -102,21 +108,10 @@
home-manager = _: _: { home.stateVersion = "23.05"; };
apps = [
{
name = "check-sandbox";
verbose = true;
share = pkgs.foot;
packages = [ ];
command = "${pkgs.callPackage ./sandbox {
inherit (config.environment.fortify.package) version;
}}";
extraPaths = [
{
src = "/proc/mounts";
dst = "/.fortify/host-mounts";
}
];
}
testCases.preset
testCases.tty
testCases.mapuid
{
name = "ne-foot";
verbose = true;

View File

@ -1,6 +1,7 @@
{
lib,
nixosTest,
buildFHSEnv,
writeShellScriptBin,
system,
@ -12,6 +13,21 @@ nixosTest {
name = "fortify" + (if withRace then "-race" else "");
nodes.machine =
{ 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 = [
# For go tests:
@ -21,7 +37,7 @@ nixosTest {
cp -r "${self.packages.${system}.fortify.src}" "$WORK"
chmod -R +w "$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'
'')
];

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
import (
@ -16,76 +22,89 @@ var (
func printf(format string, v ...any) { printfFunc(format, v...) }
func fatalf(format string, v ...any) { fatalfFunc(format, v...) }
func mustDecode(wantFile string, v any) {
if f, err := os.Open(wantFile); err != nil {
fatalf("cannot open %q: %v", wantFile, err)
} else if err = json.NewDecoder(f).Decode(v); err != nil {
fatalf("cannot decode %q: %v", wantFile, err)
} else if err = f.Close(); err != nil {
fatalf("cannot close %q: %v", wantFile, err)
}
type TestCase struct {
FS *FS `json:"fs"`
Mount []*MountinfoEntry `json:"mount"`
Seccomp bool `json:"seccomp"`
}
func MustAssertMounts(name, hostMountsFile, wantFile string) {
hostMounts := make([]*Mntent, 0, 128)
if err := IterMounts(hostMountsFile, func(e *Mntent) {
hostMounts = append(hostMounts, e)
}); err != nil {
fatalf("cannot parse host mounts: %v", err)
type T struct {
FS fs.FS
MountsPath string
}
func (t *T) MustCheckFile(wantFilePath string) {
var want *TestCase
mustDecode(wantFilePath, &want)
t.MustCheck(want)
}
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)
}
} else {
printf("[SKIP] skipping fs check")
}
var want []Mntent
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
}
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)
}
fatalf("host passthrough missing %q", want[i].FSName)
out:
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")
}
i := 0
if err := IterMounts(name, func(e *Mntent) {
if i == len(want) {
fatalf("got more than %d entries", i)
if want.Seccomp {
if TrySyscalls() != nil {
os.Exit(1)
}
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)
} else {
printf("[SKIP] skipping seccomp check")
}
}
func MustAssertFS(e fs.FS, wantFile string) {
var want *FS
mustDecode(wantFile, &want)
if want == nil {
fatalf("invalid payload")
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)
}
}
if err := want.Compare(".", e); err != nil {
func mustParseMountinfo(name string) *Mountinfo {
m := NewMountinfo(name)
if err := m.Parse(); err != nil {
fatalf("%v", err)
panic("unreachable")
}
}
func MustAssertSeccomp() {
if TrySyscalls() != nil {
os.Exit(1)
}
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,
writeText,
buildGoModule,
version,
fs,
ent,
ignore,
}:
let
wantFS =
let
fs = mode: dir: data: {
mode = lib.fromHexString mode;
inherit
dir
data
;
};
in
fs "dead" {
{
name = "preset";
tty = false;
mapRealUid = false;
want = {
fs = fs "dead" {
".fortify" = fs "800001ed" {
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;
bin = fs "800001ed" { sh = fs "80001ff" null null; } null;
dev = fs "800001ed" {
@ -191,24 +179,43 @@ let
} null;
} null;
mainFile = writeText "main.go" ''
package main
mount = [
(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"
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
'';
seccomp = true;
};
}

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,
writeText,
callPackage,
version,
}:
writeShellScript "check-sandbox" ''
name: want:
writeShellScript "fortify-${name}-check-sandbox-script" ''
set -e
${callPackage ./mount.nix { inherit version; }}/bin/test
${callPackage ./fs.nix { inherit version; }}/bin/test
${callPackage ./seccomp.nix { inherit version; }}/bin/test
${callPackage ./assert.nix { inherit version; }}/bin/test \
${writeText "fortify-${name}-want.json" (builtins.toJSON want)}
touch /tmp/sandbox-ok
''

View File

@ -30,7 +30,7 @@ func printDir(prefix string, dir []fs.DirEntry) {
}
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 {
@ -71,7 +71,7 @@ func (s *FS) Compare(prefix string, e fs.FS) error {
if fi, err := got.Info(); err != nil {
return err
} 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))
return ErrFSBadMode
}
@ -84,6 +84,8 @@ func (s *FS) Compare(prefix string, e fs.FS) error {
return err
} else if string(v) != *want.Data {
printf("[FAIL] f %s", name)
printf("got: %s", v)
printf("want: %s", *want.Data)
return ErrFSBadData
}
printf("[ OK ] f %s", name)

View File

@ -31,16 +31,16 @@ func TestCompare(t *testing.T) {
"[ OK ] s .fortify\x00[ OK ] d .\x00", nil},
{"bad length", fstest.MapFS{".fortify": {Mode: 0x800001ed}},
&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}},
&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}},
&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}},
&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},
"etc/passwd": {Data: []byte(fsPasswdSample), 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{
"passwd": {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 {
@ -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
/*
#cgo linux pkg-config: --static mount
#include <stdlib.h>
#include <stdio.h>
#include <mntent.h>
#include <libmount.h>
const char *F_PROC_MOUNTS = "";
const char *F_SET_TYPE = "r";
const char *F_MOUNTINFO_PATH = "/proc/self/mountinfo";
*/
import "C"
import (
"errors"
"fmt"
"iter"
"runtime"
"sync"
"unsafe"
)
type Mntent struct {
/* name of mounted filesystem */
FSName string `json:"fsname"`
/* filesystem path prefix */
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"`
}
var (
ErrMountinfoParse = errors.New("invalid mountinfo records")
ErrMountinfoIter = errors.New("cannot allocate iterator")
ErrMountinfoFault = errors.New("cannot iterate on filesystems")
)
func (e *Mntent) String() string {
return fmt.Sprintf("%s %s %s %s %d %d",
e.FSName, e.Dir, e.Type, e.Opts, e.Freq, e.Passno)
}
type (
Mountinfo struct {
mu sync.RWMutex
p string
err error
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)
}
tb *C.struct_libmnt_table
itr *C.struct_libmnt_iter
func IterMounts(name string, f func(e *Mntent)) error {
m := new(mounts)
m.p = name
if err := m.open(); err != nil {
return err
fs *C.struct_libmnt_fs
}
for m.scan() {
e := new(Mntent)
m.copy(e)
f(e)
// 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"`
}
)
m.close()
return m.Err()
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))
}
type mounts struct {
p string
f *C.FILE
mu sync.RWMutex
func NewMountinfo(p string) *Mountinfo { m := new(Mountinfo); m.p = p; return m }
ent *C.struct_mntent
err error
}
func (m *Mountinfo) Err() error { m.mu.RLock(); defer m.mu.RUnlock(); return m.err }
func (m *mounts) open() error {
func (m *Mountinfo) Parse() error {
m.mu.Lock()
defer m.mu.Unlock()
if m.f != nil {
if m.tb != nil {
panic("open called twice")
}
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)
m.tb = C.mnt_new_table_from_file(name)
C.free(unsafe.Pointer(name))
}
if m.tb == nil {
return ErrMountinfoParse
}
m.itr = C.mnt_new_iter(C.MNT_ITER_FORWARD)
if m.itr == nil {
C.mnt_unref_table(m.tb)
return ErrMountinfoIter
}
name := C.CString(m.p)
f, err := C.setmntent(name, C.F_SET_TYPE)
C.free(unsafe.Pointer(name))
if f == nil {
return err
}
m.f = f
runtime.SetFinalizer(m, (*mounts).close)
return err
runtime.SetFinalizer(m, (*Mountinfo).Unref)
return nil
}
func (m *mounts) close() {
func (m *Mountinfo) Unref() {
m.mu.Lock()
defer m.mu.Unlock()
if m.f == nil {
panic("close called before open")
if m.tb == nil {
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)
}
func (m *mounts) scan() bool {
m.mu.Lock()
defer m.mu.Unlock()
func (m *Mountinfo) Entries() iter.Seq[*MountinfoEntry] {
return func(yield func(*MountinfoEntry) bool) {
m.mu.Lock()
defer m.mu.Unlock()
if m.f == nil {
panic("invalid file")
C.mnt_reset_iter(m.itr, -1)
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 (e *MountinfoEntry) EqualWithIgnore(want *MountinfoEntry, ignore string) bool {
return (e.ID == want.ID || want.ID == -1) &&
(e.Parent == want.Parent || want.Parent == -1) &&
(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 (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)
v.Opts = C.GoString(m.ent.mnt_opts)
v.Freq = int(m.ent.mnt_freq)
v.Passno = int(m.ent.mnt_passno)
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"
)
func TestMounts(t *testing.T) {
func TestMountinfo(t *testing.T) {
testCases := []struct {
name string
sample string
want []sandbox.Mntent
want []*sandbox.MountinfoEntry
}{
{"fpkg", `tmpfs / tmpfs rw,nosuid,nodev,relatime,uid=1000002,gid=1000002 0 0
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
tmpfs /.fortify tmpfs rw,nosuid,nodev,relatime,size=4k,mode=755,uid=1000002,gid=1000002 0 0
tmpfs /dev tmpfs rw,nosuid,nodev,relatime,mode=755,uid=1000002,gid=1000002 0 0
devtmpfs /dev/null devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
devtmpfs /dev/zero devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
devtmpfs /dev/full devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
devtmpfs /dev/random devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
devtmpfs /dev/urandom devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
devtmpfs /dev/tty devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
devpts /dev/pts devpts rw,nosuid,noexec,relatime,mode=620,ptmxmode=666 0 0
mqueue /dev/mqueue mqueue rw,relatime 0 0
/dev/disk/by-label/nixos /nix/store ext4 ro,nosuid,nodev,relatime 0 0
/dev/disk/by-label/nixos /.fortify/app ext4 ro,nosuid,nodev,relatime 0 0
/dev/disk/by-label/nixos /etc/resolv.conf ext4 ro,nosuid,nodev,relatime 0 0
sysfs /sys/block sysfs ro,nosuid,nodev,noexec,relatime 0 0
sysfs /sys/bus sysfs ro,nosuid,nodev,noexec,relatime 0 0
sysfs /sys/class sysfs ro,nosuid,nodev,noexec,relatime 0 0
sysfs /sys/dev sysfs ro,nosuid,nodev,noexec,relatime 0 0
sysfs /sys/devices sysfs ro,nosuid,nodev,noexec,relatime 0 0
/dev/disk/by-label/nixos /.fortify/nixGL ext4 ro,nosuid,nodev,relatime 0 0
devtmpfs /dev/dri devtmpfs rw,nosuid,size=49396k,nr_inodes=121247,mode=755 0 0
/dev/disk/by-label/nixos /.fortify/etc ext4 ro,nosuid,nodev,relatime 0 0
tmpfs /run/user tmpfs rw,nosuid,nodev,relatime,size=1024k,mode=755,uid=1000002,gid=1000002 0 0
tmpfs /run/user/65534 tmpfs rw,nosuid,nodev,relatime,size=8192k,mode=755,uid=1000002,gid=1000002 0 0
/dev/disk/by-label/nixos /tmp ext4 rw,nosuid,nodev,relatime 0 0
/dev/disk/by-label/nixos /data/data/org.codeberg.dnkl.foot ext4 rw,nosuid,nodev,relatime 0 0
tmpfs /etc/passwd tmpfs ro,nosuid,nodev,relatime,uid=1000002,gid=1000002 0 0
tmpfs /etc/group tmpfs ro,nosuid,nodev,relatime,uid=1000002,gid=1000002 0 0
/dev/disk/by-label/nixos /run/user/65534/wayland-0 ext4 ro,nosuid,nodev,relatime 0 0
tmpfs /run/user/65534/pulse/native tmpfs ro,nosuid,nodev,relatime,size=98784k,nr_inodes=24696,mode=700,uid=1000,gid=100 0 0
/dev/disk/by-label/nixos /run/user/65534/bus ext4 ro,nosuid,nodev,relatime 0 0
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
`, []sandbox.Mntent{
{"tmpfs", "/", "tmpfs", "rw,nosuid,nodev,relatime,uid=1000002,gid=1000002", 0, 0},
{"proc", "/proc", "proc", "rw,nosuid,nodev,noexec,relatime", 0, 0},
{"tmpfs", "/.fortify", "tmpfs", "rw,nosuid,nodev,relatime,size=4k,mode=755,uid=1000002,gid=1000002", 0, 0},
{"tmpfs", "/dev", "tmpfs", "rw,nosuid,nodev,relatime,mode=755,uid=1000002,gid=1000002", 0, 0},
{"devtmpfs", "/dev/null", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
{"devtmpfs", "/dev/zero", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
{"devtmpfs", "/dev/full", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
{"devtmpfs", "/dev/random", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
{"devtmpfs", "/dev/urandom", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
{"devtmpfs", "/dev/tty", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
{"devpts", "/dev/pts", "devpts", "rw,nosuid,noexec,relatime,mode=620,ptmxmode=666", 0, 0},
{"mqueue", "/dev/mqueue", "mqueue", "rw,relatime", 0, 0},
{"/dev/disk/by-label/nixos", "/nix/store", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
{"/dev/disk/by-label/nixos", "/.fortify/app", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
{"/dev/disk/by-label/nixos", "/etc/resolv.conf", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
{"sysfs", "/sys/block", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
{"sysfs", "/sys/bus", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
{"sysfs", "/sys/class", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
{"sysfs", "/sys/dev", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
{"sysfs", "/sys/devices", "sysfs", "ro,nosuid,nodev,noexec,relatime", 0, 0},
{"/dev/disk/by-label/nixos", "/.fortify/nixGL", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
{"devtmpfs", "/dev/dri", "devtmpfs", "rw,nosuid,size=49396k,nr_inodes=121247,mode=755", 0, 0},
{"/dev/disk/by-label/nixos", "/.fortify/etc", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
{"tmpfs", "/run/user", "tmpfs", "rw,nosuid,nodev,relatime,size=1024k,mode=755,uid=1000002,gid=1000002", 0, 0},
{"tmpfs", "/run/user/65534", "tmpfs", "rw,nosuid,nodev,relatime,size=8192k,mode=755,uid=1000002,gid=1000002", 0, 0},
{"/dev/disk/by-label/nixos", "/tmp", "ext4", "rw,nosuid,nodev,relatime", 0, 0},
{"/dev/disk/by-label/nixos", "/data/data/org.codeberg.dnkl.foot", "ext4", "rw,nosuid,nodev,relatime", 0, 0},
{"tmpfs", "/etc/passwd", "tmpfs", "ro,nosuid,nodev,relatime,uid=1000002,gid=1000002", 0, 0},
{"tmpfs", "/etc/group", "tmpfs", "ro,nosuid,nodev,relatime,uid=1000002,gid=1000002", 0, 0},
{"/dev/disk/by-label/nixos", "/run/user/65534/wayland-0", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
{"tmpfs", "/run/user/65534/pulse/native", "tmpfs", "ro,nosuid,nodev,relatime,size=98784k,nr_inodes=24696,mode=700,uid=1000,gid=100", 0, 0},
{"/dev/disk/by-label/nixos", "/run/user/65534/bus", "ext4", "ro,nosuid,nodev,relatime", 0, 0},
{"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},
{"util-linux", `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`, []*sandbox.MountinfoEntry{
e(15, 20, "/", "/proc", "rw,relatime", "proc", "/proc", "rw"),
e(16, 20, "/", "/sys", "rw,relatime", "sysfs", "/sys", "rw"),
e(17, 20, "/", "/dev", "rw,relatime", "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755"),
e(18, 17, "/", "/dev/pts", "rw,relatime", "devpts", "devpts", "rw,gid=5,mode=620,ptmxmode=000"),
e(19, 17, "/", "/dev/shm", "rw,relatime", "tmpfs", "tmpfs", "rw"),
e(20, 1, "/", "/", "rw,noatime", "ext3", "/dev/sda4", "rw,errors=continue,user_xattr,acl,barrier=0,data=ordered"),
e(21, 16, "/", "/sys/fs/cgroup", "rw,nosuid,nodev,noexec,relatime", "tmpfs", "tmpfs", "rw,mode=755"),
e(22, 21, "/", "/sys/fs/cgroup/systemd", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd"),
e(23, 21, "/", "/sys/fs/cgroup/cpuset", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,cpuset"),
e(24, 21, "/", "/sys/fs/cgroup/ns", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,ns"),
e(25, 21, "/", "/sys/fs/cgroup/cpu", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,cpu"),
e(26, 21, "/", "/sys/fs/cgroup/cpuacct", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,cpuacct"),
e(27, 21, "/", "/sys/fs/cgroup/memory", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,memory"),
e(28, 21, "/", "/sys/fs/cgroup/devices", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,devices"),
e(29, 21, "/", "/sys/fs/cgroup/freezer", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,freezer"),
e(30, 21, "/", "/sys/fs/cgroup/net_cls", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,net_cls"),
e(31, 21, "/", "/sys/fs/cgroup/blkio", "rw,nosuid,nodev,noexec,relatime", "cgroup", "cgroup", "rw,blkio"),
e(32, 16, "/", "/sys/kernel/security", "rw,relatime", "autofs", "systemd-1", "rw,fd=22,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
e(33, 17, "/", "/dev/hugepages", "rw,relatime", "autofs", "systemd-1", "rw,fd=23,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
e(34, 16, "/", "/sys/kernel/debug", "rw,relatime", "autofs", "systemd-1", "rw,fd=24,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
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"),
e(36, 17, "/", "/dev/mqueue", "rw,relatime", "autofs", "systemd-1", "rw,fd=26,pgrp=1,timeout=300,minproto=5,maxproto=5,direct"),
e(37, 15, "/", "/proc/bus/usb", "rw,relatime", "usbfs", "/proc/bus/usb", "rw"),
e(38, 33, "/", "/dev/hugepages", "rw,relatime", "hugetlbfs", "hugetlbfs", "rw"),
e(39, 36, "/", "/dev/mqueue", "rw,relatime", "mqueue", "mqueue", "rw"),
e(40, 20, "/", "/boot", "rw,noatime", "ext3", "/dev/sda6", "rw,errors=continue,barrier=0,data=ordered"),
e(41, 20, "/", "/home/kzak", "rw,noatime", "ext4", "/dev/mapper/kzak-home", "rw,barrier=1,data=ordered"),
e(42, 35, "/", "/proc/sys/fs/binfmt_misc", "rw,relatime", "binfmt_misc", "none", "rw"),
e(43, 16, "/", "/sys/fs/fuse/connections", "rw,relatime", "fusectl", "fusectl", "rw"),
e(44, 41, "/", "/home/kzak/.gvfs", "rw,nosuid,nodev,relatime", "fuse.gvfs-fuse-daemon", "gvfs-fuse-daemon", "rw,user_id=500,group_id=500"),
e(45, 20, "/", "/var/lib/nfs/rpc_pipefs", "rw,relatime", "rpc_pipefs", "sunrpc", "rw"),
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"),
e(49, 20, "/", "/mnt/test/foobar", "rw,relatime,nosymfollow", "tmpfs", "tmpfs", "rw"),
}},
}
@ -92,27 +91,33 @@ overlay /.fortify/sbin/fortify overlay ro,nosuid,nodev,relatime,lowerdir=/mnt-ro
}
t.Run(tc.name, func(t *testing.T) {
i := 0
if err := sandbox.IterMounts(name, func(e *sandbox.Mntent) {
if i == len(tc.want) {
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)
m := sandbox.NewMountinfo(name)
if err := m.Parse(); err != nil {
t.Fatalf("Parse: error = %v", err)
}
})
t.Run(tc.name+" assert", func(t *testing.T) {
oldFatal := sandbox.SwapFatal(t.Fatalf)
t.Cleanup(func() { sandbox.SwapFatal(oldFatal) })
sandbox.MustAssertMounts(name, name, sandbox.MustWantFile(t, tc.want))
i := 0
for ent := range m.Entries() {
if i == len(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 {
@ -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']
if len(config['command']) != 1 or not (config['command'][0].startswith("/nix/store/")) or not (
config['command'][0].endswith(f"{name}-start")):
raise Exception(f"unexpected command {instance['config']['command']}")
command = f"{name}-start"
if not (config['path'].startswith("/nix/store/")) or not (config['path'].endswith(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:
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":
raise Exception(f"unexpected deny verbose output:\n{denyOutputVerbose}")
# Check sandbox state:
swaymsg("exec check-sandbox")
machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/sandbox-ok", timeout=15)
# Check sandbox outcome:
check_offset = 0
def check_sandbox(name):
global check_offset
check_offset += 1
swaymsg(f"exec script /dev/null -E always -qec check-sandbox-{name}")
machine.wait_for_file(f"/tmp/fortify.1000/tmpdir/{check_offset}/sandbox-ok", timeout=15)
check_sandbox("preset")
check_sandbox("tty")
check_sandbox("mapuid")
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:
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:
swaymsg("exec ne-foot")
wait_for_window("u0_a2@machine")
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-client", timeout=10)
wait_for_window(f"u0_a{aid(0)}@machine")
machine.send_chars("clear; wayland-info && touch /tmp/client-ok\n")
machine.wait_for_file(tmpdir_path(0, "client-ok"), timeout=10)
collect_state_ui("foot_wayland")
check_state("ne-foot", 1)
# 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.wait_until_fails("pgrep foot", timeout=5)
# 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:
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")
machine.send_chars("clear; wayland-info && touch /tmp/success-client-term\n")
machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-client-term", timeout=10)
wait_for_window(f"u0_a{aid(0)}@machine")
machine.send_chars("clear; wayland-info && touch /tmp/term-ok\n")
machine.wait_for_file(tmpdir_path(0, "term-ok"), timeout=10)
machine.wait_for_file("/tmp/ps-show-ok", timeout=5)
collect_state_ui("foot_wayland_term")
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):
swaymsg("exec pa-foot")
wait_for_window("u0_a3@machine")
machine.send_chars("clear; pactl info && touch /tmp/success-pulse\n")
machine.wait_for_file("/tmp/fortify.1000/tmpdir/3/success-pulse", timeout=10)
wait_for_window(f"u0_a{aid(1)}@machine")
machine.send_chars("clear; pactl info && touch /tmp/pulse-ok\n")
machine.wait_for_file(tmpdir_path(1, "pulse-ok"), timeout=15)
collect_state_ui("pulse_wayland")
check_state("pa-foot", 9)
machine.send_chars("exit\n")
@ -183,9 +203,9 @@ machine.wait_until_fails("pgrep foot", timeout=5)
# Test XWayland (foot does not support X):
swaymsg("exec x11-alacritty")
wait_for_window("u0_a4@machine")
machine.send_chars("clear; glinfo && touch /tmp/success-client-x11\n")
machine.wait_for_file("/tmp/fortify.1000/tmpdir/4/success-client-x11", timeout=10)
wait_for_window(f"u0_a{aid(2)}@machine")
machine.send_chars("clear; glinfo && touch /tmp/x11-ok\n")
machine.wait_for_file(tmpdir_path(2, "x11-ok"), timeout=10)
collect_state_ui("alacritty_x11")
check_state("x11-alacritty", 2)
machine.send_chars("exit\n")
@ -193,17 +213,17 @@ machine.wait_until_fails("pgrep alacritty", timeout=5)
# Start app (foot) with direct Wayland access:
swaymsg("exec da-foot")
wait_for_window("u0_a5@machine")
machine.send_chars("clear; wayland-info && touch /tmp/success-direct\n")
machine.wait_for_file("/tmp/fortify.1000/tmpdir/5/success-direct", timeout=10)
wait_for_window(f"u0_a{aid(3)}@machine")
machine.send_chars("clear; wayland-info && touch /tmp/direct-ok\n")
collect_state_ui("foot_direct")
machine.wait_for_file(tmpdir_path(3, "direct-ok"), timeout=10)
check_state("da-foot", 1)
# 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.wait_until_fails("pgrep foot", timeout=5)
# 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:
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
<-done
runtime.KeepAlive(syncPipe[1].Fd())
runtime.KeepAlive(syncPipe[1])
}); err != nil {
setupDone <- err
}
@ -107,7 +107,7 @@ func bindRawConn(done chan struct{}, rc syscall.RawConn, p, appID, instanceID st
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
if f, err := os.Create(p); err != nil {
return err
@ -117,5 +117,5 @@ func bind(fd uintptr, p, appID, instanceID string, syncFD uintptr) error {
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"),
}
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) {
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)]
}