aboutsummaryrefslogtreecommitdiffhomepage
path: root/fst
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2025-04-12 13:56:41 +0900
committerOphestra <cat@gensokyo.uk>2025-04-12 13:56:41 +0900
commit6309469e933a31a300fbf16d8e77f48dcee402d3 (patch)
tree8f8a72ee02b3ca15b104a6e55c9379e20f8d7e8a /fst
parent0d7c1a9a4356614f035225aeb24e66421879a99b (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.go57
-rw-r--r--fst/config.go1
-rw-r--r--fst/id.go48
-rw-r--r--fst/id_test.go63
-rw-r--r--fst/path.go11
-rw-r--r--fst/path_test.go85
-rw-r--r--fst/sandbox.go192
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
-}