diff options
| author | Ophestra <cat@gensokyo.uk> | 2025-04-12 13:56:41 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2025-04-12 13:56:41 +0900 |
| commit | 6309469e933a31a300fbf16d8e77f48dcee402d3 (patch) | |
| tree | 8f8a72ee02b3ca15b104a6e55c9379e20f8d7e8a /fst | |
| parent | 0d7c1a9a4356614f035225aeb24e66421879a99b (diff) | |
app/instance: wrap internal implementation
This reduces the scope of the fst package, which was growing questionably large.
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'fst')
| -rw-r--r-- | fst/app.go | 57 | ||||
| -rw-r--r-- | fst/config.go | 1 | ||||
| -rw-r--r-- | fst/id.go | 48 | ||||
| -rw-r--r-- | fst/id_test.go | 63 | ||||
| -rw-r--r-- | fst/path.go | 11 | ||||
| -rw-r--r-- | fst/path_test.go | 85 | ||||
| -rw-r--r-- | fst/sandbox.go | 192 |
7 files changed, 1 insertions, 456 deletions
diff --git a/fst/app.go b/fst/app.go deleted file mode 100644 index 51ff0d7a..00000000 --- a/fst/app.go +++ /dev/null @@ -1,57 +0,0 @@ -// Package fst exports shared fortify types. -package fst - -import ( - "syscall" - "time" -) - -type App interface { - // ID returns a copy of [fst.ID] held by App. - ID() ID - - // Seal determines the outcome of config as a [SealedApp]. - // The value of config might be overwritten and must not be used again. - Seal(config *Config) (SealedApp, error) - - String() string -} - -type SealedApp interface { - // Run commits sealed system setup and starts the app process. - Run(rs *RunState) error -} - -// RunState stores the outcome of a call to [SealedApp.Run]. -type RunState struct { - // Time is the exact point in time where the process was created. - // Location must be set to UTC. - // - // Time is nil if no process was ever created. - Time *time.Time - // RevertErr is stored by the deferred revert call. - RevertErr error - // WaitErr is the generic error value created by the standard library. - WaitErr error - - syscall.WaitStatus -} - -// SetStart stores the current time in [RunState] once. -func (rs *RunState) SetStart() { - if rs.Time != nil { - panic("attempted to store time twice") - } - now := time.Now().UTC() - rs.Time = &now -} - -// Paths contains environment-dependent paths used by fortify. -type Paths struct { - // path to shared directory (usually `/tmp/fortify.%d`) - SharePath string `json:"share_path"` - // XDG_RUNTIME_DIR value (usually `/run/user/%d`) - RuntimePath string `json:"runtime_path"` - // application runtime directory (usually `/run/user/%d/fortify`) - RunDirPath string `json:"run_dir_path"` -} diff --git a/fst/config.go b/fst/config.go index f227b029..5c259790 100644 --- a/fst/config.go +++ b/fst/config.go @@ -1,3 +1,4 @@ +// Package fst exports shared fortify types. package fst import ( diff --git a/fst/id.go b/fst/id.go deleted file mode 100644 index a8363c25..00000000 --- a/fst/id.go +++ /dev/null @@ -1,48 +0,0 @@ -package fst - -import ( - "crypto/rand" - "encoding/hex" - "errors" - "fmt" -) - -type ID [16]byte - -var ( - ErrInvalidLength = errors.New("string representation must have a length of 32") -) - -func (a *ID) String() string { - return hex.EncodeToString(a[:]) -} - -func NewAppID(id *ID) error { - _, err := rand.Read(id[:]) - return err -} - -func ParseAppID(id *ID, s string) error { - if len(s) != 32 { - return ErrInvalidLength - } - - for i, b := range s { - if b < '0' || b > 'f' { - return fmt.Errorf("invalid char %q at byte %d", b, i) - } - - v := uint8(b) - if v > '9' { - v = 10 + v - 'a' - } else { - v -= '0' - } - if i%2 == 0 { - v <<= 4 - } - id[i/2] += v - } - - return nil -} diff --git a/fst/id_test.go b/fst/id_test.go deleted file mode 100644 index f40026a8..00000000 --- a/fst/id_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package fst_test - -import ( - "errors" - "testing" - - "git.gensokyo.uk/security/fortify/fst" -) - -func TestParseAppID(t *testing.T) { - t.Run("bad length", func(t *testing.T) { - if err := fst.ParseAppID(new(fst.ID), "meow"); !errors.Is(err, fst.ErrInvalidLength) { - t.Errorf("ParseAppID: error = %v, wantErr = %v", err, fst.ErrInvalidLength) - } - }) - - t.Run("bad byte", func(t *testing.T) { - wantErr := "invalid char '\\n' at byte 15" - if err := fst.ParseAppID(new(fst.ID), "02bc7f8936b2af6\n\ne2535cd71ef0bb7"); err == nil || err.Error() != wantErr { - t.Errorf("ParseAppID: error = %v, wantErr = %v", err, wantErr) - } - }) - - t.Run("fuzz 16 iterations", func(t *testing.T) { - for i := 0; i < 16; i++ { - testParseAppIDWithRandom(t) - } - }) -} - -func FuzzParseAppID(f *testing.F) { - for i := 0; i < 16; i++ { - id := new(fst.ID) - if err := fst.NewAppID(id); err != nil { - panic(err.Error()) - } - f.Add(id[0], id[1], id[2], id[3], id[4], id[5], id[6], id[7], id[8], id[9], id[10], id[11], id[12], id[13], id[14], id[15]) - } - - f.Fuzz(func(t *testing.T, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15 byte) { - testParseAppID(t, &fst.ID{b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15}) - }) -} - -func testParseAppIDWithRandom(t *testing.T) { - id := new(fst.ID) - if err := fst.NewAppID(id); err != nil { - t.Fatalf("cannot generate app ID: %v", err) - } - testParseAppID(t, id) -} - -func testParseAppID(t *testing.T, id *fst.ID) { - s := id.String() - got := new(fst.ID) - if err := fst.ParseAppID(got, s); err != nil { - t.Fatalf("cannot parse app ID: %v", err) - } - - if *got != *id { - t.Fatalf("ParseAppID(%#v) = \n%#v, want \n%#v", s, got, id) - } -} diff --git a/fst/path.go b/fst/path.go deleted file mode 100644 index 2bdab87c..00000000 --- a/fst/path.go +++ /dev/null @@ -1,11 +0,0 @@ -package fst - -import ( - "path/filepath" - "strings" -) - -func deepContainsH(basepath, targpath string) (bool, error) { - rel, err := filepath.Rel(basepath, targpath) - return err == nil && rel != ".." && !strings.HasPrefix(rel, string([]byte{'.', '.', filepath.Separator})), err -} diff --git a/fst/path_test.go b/fst/path_test.go deleted file mode 100644 index 35895901..00000000 --- a/fst/path_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package fst - -import ( - "testing" -) - -func TestDeepContainsH(t *testing.T) { - testCases := []struct { - name string - basepath string - targpath string - want bool - wantErr bool - }{ - { - name: "empty", - want: true, - }, - { - name: "equal abs", - basepath: "/run", - targpath: "/run", - want: true, - }, - { - name: "equal rel", - basepath: "./run", - targpath: "run", - want: true, - }, - { - name: "contains abs", - basepath: "/run", - targpath: "/run/dbus", - want: true, - }, - { - name: "inverse contains abs", - basepath: "/run/dbus", - targpath: "/run", - want: false, - }, - { - name: "contains rel", - basepath: "../run", - targpath: "../run/dbus", - want: true, - }, - { - name: "inverse contains rel", - basepath: "../run/dbus", - targpath: "../run", - want: false, - }, - { - name: "weird abs", - basepath: "/run/dbus", - targpath: "/run/dbus/../current-system", - want: false, - }, - { - name: "weird rel", - basepath: "../run/dbus", - targpath: "../run/dbus/../current-system", - want: false, - }, - - { - name: "invalid mix", - basepath: "/run", - targpath: "./run", - wantErr: true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - if got, err := deepContainsH(tc.basepath, tc.targpath); (err != nil) != tc.wantErr { - t.Errorf("deepContainsH() error = %v, wantErr %v", err, tc.wantErr) - } else if got != tc.want { - t.Errorf("deepContainsH() = %v, want %v", got, tc.want) - } - }) - } -} diff --git a/fst/sandbox.go b/fst/sandbox.go index 194fcb76..c4936a2e 100644 --- a/fst/sandbox.go +++ b/fst/sandbox.go @@ -1,16 +1,6 @@ package fst import ( - "errors" - "fmt" - "io/fs" - "maps" - "path" - "slices" - "syscall" - - "git.gensokyo.uk/security/fortify/dbus" - "git.gensokyo.uk/security/fortify/sandbox" "git.gensokyo.uk/security/fortify/sandbox/seccomp" ) @@ -57,18 +47,6 @@ type ( 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 @@ -83,173 +61,3 @@ type ( Must bool `json:"require,omitempty"` } ) - -// ToContainer initialises [sandbox.Params] via [SandboxConfig]. -// Note that remaining container setup must be queued by the [App] implementation. -func (s *SandboxConfig) ToContainer(sys SandboxSys, uid, gid *int) (*sandbox.Params, map[string]string, error) { - if s == nil { - return nil, nil, syscall.EBADE - } - - container := &sandbox.Params{ - Hostname: s.Hostname, - Ops: new(sandbox.Ops), - Seccomp: s.Seccomp, - } - - if s.Multiarch { - container.Seccomp |= seccomp.FilterMultiarch - } - - /* this is only 4 KiB of memory on a 64-bit system, - permissive defaults on NixOS results in around 100 entries - so this capacity should eliminate copies for most setups */ - *container.Ops = slices.Grow(*container.Ops, 1<<8) - - if s.Devel { - container.Flags |= sandbox.FAllowDevel - } - if s.Userns { - container.Flags |= sandbox.FAllowUserns - } - if s.Net { - container.Flags |= sandbox.FAllowNet - } - if s.Tty { - container.Flags |= sandbox.FAllowTTY - } - - if s.MapRealUID { - /* some programs fail to connect to dbus session running as a different uid - so this workaround is introduced to map priv-side caller uid in container */ - container.Uid = sys.Getuid() - *uid = container.Uid - container.Gid = sys.Getgid() - *gid = container.Gid - } else { - *uid = sandbox.OverflowUid() - *gid = sandbox.OverflowGid() - } - - container. - Proc("/proc"). - Tmpfs(Tmp, 1<<12, 0755) - - if !s.Device { - container.Dev("/dev").Mqueue("/dev/mqueue") - } else { - container.Bind("/dev", "/dev", sandbox.BindWritable|sandbox.BindDevice) - } - - /* retrieve paths and hide them if they're made available in the sandbox; - this feature tries to improve user experience of permissive defaults, and - to warn about issues in custom configuration; it is NOT a security feature - and should not be treated as such, ALWAYS be careful with what you bind */ - var hidePaths []string - sc := sys.Paths() - hidePaths = append(hidePaths, sc.RuntimePath, sc.SharePath) - _, systemBusAddr := dbus.Address() - if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil { - return nil, nil, err - } else { - // there is usually only one, do not preallocate - for _, entry := range entries { - if entry.Method != "unix" { - continue - } - for _, pair := range entry.Values { - if pair[0] == "path" { - if path.IsAbs(pair[1]) { - // get parent dir of socket - dir := path.Dir(pair[1]) - if dir == "." || dir == "/" { - sys.Printf("dbus socket %q is in an unusual location", pair[1]) - } - hidePaths = append(hidePaths, dir) - } else { - sys.Printf("dbus socket %q is not absolute", pair[1]) - } - } - } - } - } - hidePathMatch := make([]bool, len(hidePaths)) - for i := range hidePaths { - if err := evalSymlinks(sys, &hidePaths[i]); err != nil { - return nil, nil, err - } - } - - for _, c := range s.Filesystem { - if c == nil { - continue - } - - if !path.IsAbs(c.Src) { - return nil, nil, fmt.Errorf("src path %q is not absolute", c.Src) - } - - dest := c.Dst - if c.Dst == "" { - dest = c.Src - } else if !path.IsAbs(dest) { - return nil, nil, fmt.Errorf("dst path %q is not absolute", dest) - } - - srcH := c.Src - if err := evalSymlinks(sys, &srcH); err != nil { - return nil, nil, err - } - - for i := range hidePaths { - // skip matched entries - if hidePathMatch[i] { - continue - } - - if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil { - return nil, nil, err - } else if ok { - hidePathMatch[i] = true - sys.Printf("hiding paths from %q", c.Src) - } - } - - var flags int - if c.Write { - flags |= sandbox.BindWritable - } - if c.Device { - flags |= sandbox.BindDevice | sandbox.BindWritable - } - if !c.Must { - flags |= sandbox.BindOptional - } - container.Bind(c.Src, dest, flags) - } - - // cover matched paths - for i, ok := range hidePathMatch { - if ok { - container.Tmpfs(hidePaths[i], 1<<13, 0755) - } - } - - for _, l := range s.Link { - container.Link(l[0], l[1]) - } - - return container, maps.Clone(s.Env), nil -} - -func evalSymlinks(sys SandboxSys, v *string) error { - if p, err := sys.EvalSymlinks(*v); err != nil { - if !errors.Is(err, fs.ErrNotExist) { - return err - } - sys.Printf("path %q does not yet exist", *v) - } else { - *v = p - } - return nil -} |
