Compare commits

...

5 Commits

Author SHA1 Message Date
ea853e21d9
test/sandbox: check fs outcome
All checks were successful
Test / Create distribution (push) Successful in 19s
Test / Fpkg (push) Successful in 33s
Test / Fortify (push) Successful in 35s
Test / Data race detector (push) Successful in 35s
Test / Flake checks (push) Successful in 52s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-03 01:02:09 +09:00
0bd9b9e8fe
test/sandbox: assert filesystem json
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m27s
Test / Fpkg (push) Successful in 3m30s
Test / Data race detector (push) Successful in 3m30s
Test / Flake checks (push) Successful in 57s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-02 23:23:04 +09:00
39e32799b3
test/sandbox: compare filesystem hierarchy
All checks were successful
Test / Create distribution (push) Successful in 27s
Test / Fortify (push) Successful in 2m34s
Test / Data race detector (push) Successful in 3m37s
Test / Fpkg (push) Successful in 3m41s
Test / Flake checks (push) Successful in 56s
For checking deterministic aspects of fs outcome.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-02 22:59:04 +09:00
9953768de5
test: rename session message identifier
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Fpkg (push) Successful in 35s
Test / Fortify (push) Successful in 2m14s
Test / Data race detector (push) Successful in 2m36s
Test / Flake checks (push) Successful in 56s
Labelling this as sway is misleading.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-02 22:47:33 +09:00
0d3652b793
test/sandbox/assert: wrap printf
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Fortify (push) Successful in 2m34s
Test / Data race detector (push) Successful in 3m30s
Test / Fpkg (push) Successful in 3m38s
Test / Flake checks (push) Successful in 50s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-03-02 18:37:46 +09:00
8 changed files with 458 additions and 27 deletions

View File

@ -70,7 +70,7 @@
echo 'output Virtual-1 res 1680x1050') > ~/.config/sway/config echo 'output Virtual-1 res 1680x1050') > ~/.config/sway/config
sway --validate sway --validate
systemd-cat --identifier=sway sway && touch /tmp/sway-exit-ok systemd-cat --identifier=session sway && touch /tmp/sway-exit-ok
fi fi
''; '';

View File

@ -2,17 +2,30 @@ package sandbox
import ( import (
"encoding/json" "encoding/json"
"io/fs"
"log" "log"
"os" "os"
) )
var ( var (
assert = log.New(os.Stderr, "sandbox: ", 0) assert = log.New(os.Stderr, "sandbox: ", 0)
printfFunc = assert.Printf
fatalfFunc = assert.Fatalf fatalfFunc = assert.Fatalf
) )
func printf(format string, v ...any) { printfFunc(format, v...) }
func fatalf(format string, v ...any) { fatalfFunc(format, v...) } func fatalf(format string, v ...any) { fatalfFunc(format, v...) }
func mustDecode(wantFile string, v any) {
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)
}
}
func MustAssertMounts(name, hostMountsFile, wantFile string) { func MustAssertMounts(name, hostMountsFile, wantFile string) {
hostMounts := make([]*Mntent, 0, 128) hostMounts := make([]*Mntent, 0, 128)
if err := IterMounts(hostMountsFile, func(e *Mntent) { if err := IterMounts(hostMountsFile, func(e *Mntent) {
@ -22,13 +35,7 @@ func MustAssertMounts(name, hostMountsFile, wantFile string) {
} }
var want []Mntent var want []Mntent
if f, err := os.Open(wantFile); err != nil { mustDecode(wantFile, &want)
fatalf("cannot open %q: %v", wantFile, err)
} else if err = json.NewDecoder(f).Decode(&want); err != nil {
fatalf("cannot decode %q: %v", wantFile, err)
} else if err = f.Close(); err != nil {
fatalf("cannot close %q: %v", wantFile, err)
}
for i := range want { for i := range want {
if want[i].Opts == "host_passthrough" { if want[i].Opts == "host_passthrough" {
@ -53,9 +60,21 @@ func MustAssertMounts(name, hostMountsFile, wantFile string) {
e, &want[i]) e, &want[i])
} }
assert.Printf("%s", e) printf("%s", e)
i++ i++
}); err != nil { }); err != nil {
fatalf("cannot iterate mounts: %v", err) fatalf("cannot iterate mounts: %v", err)
} }
} }
func MustAssertFS(e fs.FS, wantFile string) {
var want *FS
mustDecode(wantFile, &want)
if want == nil {
fatalf("invalid payload")
}
if err := want.Compare(".", e); err != nil {
fatalf("%v", err)
}
}

View File

@ -1,3 +1,32 @@
package sandbox package sandbox
func ReplaceFatal(f func(format string, v ...any)) { fatalfFunc = f } import (
"encoding/json"
"os"
"path"
"testing"
)
type F func(format string, v ...any)
func SwapPrint(f F) (old F) { old = printfFunc; printfFunc = f; return }
func SwapFatal(f F) (old F) { old = fatalfFunc; fatalfFunc = f; return }
func MustWantFile(t *testing.T, v any) (wantFile string) {
wantFile = path.Join(t.TempDir(), "want.json")
if f, err := os.OpenFile(wantFile, os.O_CREATE|os.O_WRONLY, 0400); err != nil {
t.Fatalf("cannot create %q: %v", wantFile, err)
} else if err = json.NewEncoder(f).Encode(v); err != nil {
t.Fatalf("cannot encode to %q: %v", wantFile, err)
} else if err = f.Close(); err != nil {
t.Fatalf("cannot close %q: %v", wantFile, err)
}
t.Cleanup(func() {
if err := os.Remove(wantFile); err != nil {
t.Fatalf("cannot remove %q: %v", wantFile, err)
}
})
return
}

View File

@ -7,6 +7,7 @@
writeShellScript "check-sandbox" '' writeShellScript "check-sandbox" ''
set -e set -e
${callPackage ./mount.nix { inherit version; }}/bin/test ${callPackage ./mount.nix { inherit version; }}/bin/test
${callPackage ./fs.nix { inherit version; }}/bin/test
touch /tmp/sandbox-ok touch /tmp/sandbox-ok
'' ''

98
test/sandbox/fs.go Normal file
View File

@ -0,0 +1,98 @@
package sandbox
import (
"errors"
"fmt"
"io/fs"
"path"
"strings"
)
var (
ErrFSBadLength = errors.New("bad dir length")
ErrFSBadData = errors.New("data differs")
ErrFSBadMode = errors.New("mode differs")
ErrFSInvalidEnt = errors.New("invalid entry condition")
)
type FS struct {
Mode fs.FileMode `json:"mode"`
Dir map[string]*FS `json:"dir"`
Data *string `json:"data"`
}
func printDir(prefix string, dir []fs.DirEntry) {
names := make([]string, len(dir))
for i, ent := range dir {
name := ent.Name()
if ent.IsDir() {
name += "/"
}
names[i] = fmt.Sprintf("%q", name)
}
printf("[FAIL] d %q: %s", prefix, strings.Join(names, " "))
}
func (s *FS) Compare(prefix string, e fs.FS) error {
if s.Data != nil {
if s.Dir != nil {
panic("invalid state")
}
panic("invalid compare call")
}
if s.Dir == nil {
printf("[ OK ] s %s", prefix)
return nil
}
var dir []fs.DirEntry
if d, err := fs.ReadDir(e, prefix); err != nil {
return err
} else if len(d) != len(s.Dir) {
printDir(prefix, d)
return ErrFSBadLength
} else {
dir = d
}
for _, got := range dir {
name := got.Name()
if want, ok := s.Dir[name]; !ok {
printDir(prefix, dir)
return fs.ErrNotExist
} else if want.Dir != nil && !got.IsDir() {
printDir(prefix, dir)
return ErrFSInvalidEnt
} else {
name = path.Join(prefix, name)
if fi, err := got.Info(); err != nil {
return err
} else if fi.Mode() != want.Mode {
printf("[FAIL] m %q: %x, want %x",
name, uint32(fi.Mode()), uint32(want.Mode))
return ErrFSBadMode
}
if want.Data != nil {
if want.Dir != nil {
panic("invalid state")
}
if v, err := fs.ReadFile(e, name); err != nil {
return err
} else if string(v) != *want.Data {
printf("[FAIL] f %s", name)
return ErrFSBadData
}
printf("[ OK ] f %s", name)
} else if err := want.Compare(name, e); err != nil {
return err
}
}
}
printf("[ OK ] d %s", prefix)
return nil
}

214
test/sandbox/fs.nix Normal file
View File

@ -0,0 +1,214 @@
{
lib,
writeText,
buildGoModule,
version,
}:
let
wantFS =
let
fs = mode: dir: data: {
mode = lib.fromHexString mode;
inherit
dir
data
;
};
in
fs "dead" {
".fortify" = fs "800001ed" {
etc = fs "800001ed" null null;
sbin = fs "800001c0" {
fortify = fs "16d" null null;
init = fs "80001ff" null null;
} null;
host-mounts = fs "124" 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: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_a1:x:65534:65534:Fortify:/var/lib/fortify/u0/a1:/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" {
a1 = 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;
mainFile = writeText "main.go" ''
package main
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
'';
}

84
test/sandbox/fs_test.go Normal file
View File

@ -0,0 +1,84 @@
package sandbox_test
import (
"errors"
"fmt"
"io/fs"
"strings"
"testing"
"testing/fstest"
"git.gensokyo.uk/security/fortify/test/sandbox"
)
var (
fsPasswdSample = "u0_a20:x:65534:65534:Fortify:/var/lib/persist/module/fortify/u0/a20:/run/current-system/sw/bin/zsh"
fsGroupSample = "fortify:x:65534:"
)
func TestCompare(t *testing.T) {
testCases := []struct {
name string
sample fstest.MapFS
want *sandbox.FS
wantOut string
wantErr error
}{
{"skip", fstest.MapFS{}, &sandbox.FS{}, "[ OK ] s .\x00", nil},
{"simple pass", fstest.MapFS{".fortify": {Mode: 0x800001ed}},
&sandbox.FS{Dir: map[string]*sandbox.FS{".fortify": {Mode: 0x800001ed}}},
"[ 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},
{"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},
{"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},
{"nonexistent", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}},
&sandbox.FS{Dir: map[string]*sandbox.FS{".test": {}}},
"[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},
}, &sandbox.FS{Dir: map[string]*sandbox.FS{"etc": {Mode: 0x800001c0, Dir: map[string]*sandbox.FS{
"passwd": {Mode: 0x1a4, Data: &fsPasswdSample},
"group": {Mode: 0x1a4, Data: &fsGroupSample},
}}}}, "[ OK ] f etc/group\x00[ OK ] f etc/passwd\x00[ OK ] d etc\x00[ OK ] d .\x00", nil},
{"file differ", fstest.MapFS{"etc": {Mode: 0x800001c0},
"etc/passwd": {Data: []byte(fsPasswdSample), Mode: 0644},
"etc/group": {Data: []byte(fsGroupSample), Mode: 0644},
}, &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},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
gotOut := new(strings.Builder)
oldPrint := sandbox.SwapPrint(func(format string, v ...any) { _, _ = fmt.Fprintf(gotOut, format+"\x00", v...) })
t.Cleanup(func() { sandbox.SwapPrint(oldPrint) })
err := tc.want.Compare(".", tc.sample)
if !errors.Is(err, tc.wantErr) {
t.Errorf("Compare: error = %v; wantErr %v",
err, tc.wantErr)
}
if gotOut.String() != tc.wantOut {
t.Errorf("Compare: output %q; want %q",
gotOut, tc.wantOut)
}
})
}
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,7 +1,6 @@
package sandbox_test package sandbox_test
import ( import (
"encoding/json"
"os" "os"
"path" "path"
"testing" "testing"
@ -111,22 +110,9 @@ overlay /.fortify/sbin/fortify overlay ro,nosuid,nodev,relatime,lowerdir=/mnt-ro
}) })
t.Run(tc.name+" assert", func(t *testing.T) { t.Run(tc.name+" assert", func(t *testing.T) {
sandbox.ReplaceFatal(t.Fatalf) oldFatal := sandbox.SwapFatal(t.Fatalf)
t.Cleanup(func() { sandbox.SwapFatal(oldFatal) })
wantFile := path.Join(t.TempDir(), "want.json") sandbox.MustAssertMounts(name, name, sandbox.MustWantFile(t, tc.want))
if f, err := os.OpenFile(wantFile, os.O_CREATE|os.O_WRONLY, 0400); err != nil {
t.Fatalf("cannot create %q: %v", wantFile, err)
} else if err = json.NewEncoder(f).Encode(tc.want); err != nil {
t.Fatalf("cannot encode to %q: %v", wantFile, err)
} else if err = f.Close(); err != nil {
t.Fatalf("cannot close %q: %v", wantFile, err)
}
sandbox.MustAssertMounts(name, name, wantFile)
if err := os.Remove(wantFile); err != nil {
t.Fatalf("cannot remove %q: %v", wantFile, err)
}
}) })
if err := os.Remove(name); err != nil { if err := os.Remove(name); err != nil {