diff --git a/cmd/fpkg/main.go b/cmd/fpkg/main.go index 3102d40..e48cfb5 100644 --- a/cmd/fpkg/main.go +++ b/cmd/fpkg/main.go @@ -13,7 +13,7 @@ import ( "git.gensokyo.uk/security/fortify/command" "git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/internal" - "git.gensokyo.uk/security/fortify/internal/app/setuid" + "git.gensokyo.uk/security/fortify/internal/app/instance" "git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/sys" "git.gensokyo.uk/security/fortify/sandbox" @@ -62,7 +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") - c.Command("shim", command.UsageInternal, func([]string) error { setuid.ShimMain(); return errSuccess }) + c.Command("shim", command.UsageInternal, func([]string) error { instance.ShimMain(); return errSuccess }) { var ( diff --git a/cmd/fpkg/proc.go b/cmd/fpkg/proc.go index 6cda734..45fe352 100644 --- a/cmd/fpkg/proc.go +++ b/cmd/fpkg/proc.go @@ -5,20 +5,21 @@ import ( "os" "git.gensokyo.uk/security/fortify/fst" - "git.gensokyo.uk/security/fortify/internal/app/setuid" + "git.gensokyo.uk/security/fortify/internal/app" + "git.gensokyo.uk/security/fortify/internal/app/instance" "git.gensokyo.uk/security/fortify/internal/fmsg" ) func mustRunApp(ctx context.Context, config *fst.Config, beforeFail func()) { - rs := new(fst.RunState) - a := setuid.MustNew(ctx, std) + rs := new(app.RunState) + a := instance.MustNew(instance.ISetuid, ctx, std) var code int if sa, err := a.Seal(config); err != nil { fmsg.PrintBaseError(err, "cannot seal app:") code = 1 } else { - code = setuid.PrintRunStateErr(rs, sa.Run(rs)) + code = instance.PrintRunStateErr(instance.ISetuid, rs, sa.Run(rs)) } if code != 0 { diff --git a/fst/config.go b/fst/config.go index f227b02..5c25979 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/sandbox.go b/fst/sandbox.go index 194fcb7..c4936a2 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 -} diff --git a/fst/app.go b/internal/app/app.go similarity index 87% rename from fst/app.go rename to internal/app/app.go index 51ff0d7..325d9f8 100644 --- a/fst/app.go +++ b/internal/app/app.go @@ -1,18 +1,20 @@ -// Package fst exports shared fortify types. -package fst +// Package app defines the generic [App] interface. +package app import ( "syscall" "time" + + "git.gensokyo.uk/security/fortify/fst" ) type App interface { - // ID returns a copy of [fst.ID] held by App. + // ID returns a copy of [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) + Seal(config *fst.Config) (SealedApp, error) String() string } diff --git a/fst/id.go b/internal/app/id.go similarity index 98% rename from fst/id.go rename to internal/app/id.go index a8363c2..e674c7d 100644 --- a/fst/id.go +++ b/internal/app/id.go @@ -1,4 +1,4 @@ -package fst +package app import ( "crypto/rand" diff --git a/fst/id_test.go b/internal/app/id_test.go similarity index 59% rename from fst/id_test.go rename to internal/app/id_test.go index f40026a..f928a48 100644 --- a/fst/id_test.go +++ b/internal/app/id_test.go @@ -1,22 +1,22 @@ -package fst_test +package app_test import ( "errors" "testing" - "git.gensokyo.uk/security/fortify/fst" + . "git.gensokyo.uk/security/fortify/internal/app" ) 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) + if err := ParseAppID(new(ID), "meow"); !errors.Is(err, ErrInvalidLength) { + t.Errorf("ParseAppID: error = %v, wantErr = %v", err, 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 { + if err := ParseAppID(new(ID), "02bc7f8936b2af6\n\ne2535cd71ef0bb7"); err == nil || err.Error() != wantErr { t.Errorf("ParseAppID: error = %v, wantErr = %v", err, wantErr) } }) @@ -30,30 +30,30 @@ func TestParseAppID(t *testing.T) { func FuzzParseAppID(f *testing.F) { for i := 0; i < 16; i++ { - id := new(fst.ID) - if err := fst.NewAppID(id); err != nil { + id := new(ID) + if err := 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}) + testParseAppID(t, &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 { + id := new(ID) + if err := NewAppID(id); err != nil { t.Fatalf("cannot generate app ID: %v", err) } testParseAppID(t, id) } -func testParseAppID(t *testing.T, id *fst.ID) { +func testParseAppID(t *testing.T, id *ID) { s := id.String() - got := new(fst.ID) - if err := fst.ParseAppID(got, s); err != nil { + got := new(ID) + if err := ParseAppID(got, s); err != nil { t.Fatalf("cannot parse app ID: %v", err) } diff --git a/internal/app/instance/common/container.go b/internal/app/instance/common/container.go new file mode 100644 index 0000000..cc26c80 --- /dev/null +++ b/internal/app/instance/common/container.go @@ -0,0 +1,187 @@ +package common + +import ( + "errors" + "fmt" + "io/fs" + "maps" + "path" + "slices" + "syscall" + + "git.gensokyo.uk/security/fortify/dbus" + "git.gensokyo.uk/security/fortify/fst" + "git.gensokyo.uk/security/fortify/internal/sys" + "git.gensokyo.uk/security/fortify/sandbox" + "git.gensokyo.uk/security/fortify/sandbox/seccomp" +) + +// NewContainer initialises [sandbox.Params] via [fst.SandboxConfig]. +// Note that remaining container setup must be queued by the caller. +func NewContainer(s *fst.SandboxConfig, os sys.State, 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 = os.Getuid() + *uid = container.Uid + container.Gid = os.Getgid() + *gid = container.Gid + } else { + *uid = sandbox.OverflowUid() + *gid = sandbox.OverflowGid() + } + + container. + Proc("/proc"). + Tmpfs(fst.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 := os.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 == "/" { + os.Printf("dbus socket %q is in an unusual location", pair[1]) + } + hidePaths = append(hidePaths, dir) + } else { + os.Printf("dbus socket %q is not absolute", pair[1]) + } + } + } + } + } + hidePathMatch := make([]bool, len(hidePaths)) + for i := range hidePaths { + if err := evalSymlinks(os, &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(os, &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 + os.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(os sys.State, v *string) error { + if p, err := os.EvalSymlinks(*v); err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return err + } + os.Printf("path %q does not yet exist", *v) + } else { + *v = p + } + return nil +} diff --git a/fst/path.go b/internal/app/instance/common/path.go similarity index 94% rename from fst/path.go rename to internal/app/instance/common/path.go index 2bdab87..ce40f4f 100644 --- a/fst/path.go +++ b/internal/app/instance/common/path.go @@ -1,4 +1,4 @@ -package fst +package common import ( "path/filepath" diff --git a/fst/path_test.go b/internal/app/instance/common/path_test.go similarity index 99% rename from fst/path_test.go rename to internal/app/instance/common/path_test.go index 3589590..b14f24d 100644 --- a/fst/path_test.go +++ b/internal/app/instance/common/path_test.go @@ -1,4 +1,4 @@ -package fst +package common import ( "testing" diff --git a/internal/app/instance/errors.go b/internal/app/instance/errors.go new file mode 100644 index 0000000..b3331e3 --- /dev/null +++ b/internal/app/instance/errors.go @@ -0,0 +1,17 @@ +package instance + +import ( + "syscall" + + "git.gensokyo.uk/security/fortify/internal/app" + "git.gensokyo.uk/security/fortify/internal/app/internal/setuid" +) + +func PrintRunStateErr(whence int, rs *app.RunState, runErr error) (code int) { + switch whence { + case ISetuid: + return setuid.PrintRunStateErr(rs, runErr) + default: + panic(syscall.EINVAL) + } +} diff --git a/internal/app/instance/new.go b/internal/app/instance/new.go new file mode 100644 index 0000000..cb5e8b2 --- /dev/null +++ b/internal/app/instance/new.go @@ -0,0 +1,33 @@ +// Package instance exposes cross-package implementation details and provides constructors for builtin implementations. +package instance + +import ( + "context" + "log" + "syscall" + + "git.gensokyo.uk/security/fortify/internal/app" + "git.gensokyo.uk/security/fortify/internal/app/internal/setuid" + "git.gensokyo.uk/security/fortify/internal/sys" +) + +const ( + ISetuid = iota +) + +func New(whence int, ctx context.Context, os sys.State) (app.App, error) { + switch whence { + case ISetuid: + return setuid.New(ctx, os) + default: + return nil, syscall.EINVAL + } +} + +func MustNew(whence int, ctx context.Context, os sys.State) app.App { + a, err := New(whence, ctx, os) + if err != nil { + log.Fatalf("cannot create app: %v", err) + } + return a +} diff --git a/internal/app/instance/shim.go b/internal/app/instance/shim.go new file mode 100644 index 0000000..bc497ad --- /dev/null +++ b/internal/app/instance/shim.go @@ -0,0 +1,6 @@ +package instance + +import "git.gensokyo.uk/security/fortify/internal/app/internal/setuid" + +// ShimMain is the main function of the shim process and runs as the unconstrained target user. +func ShimMain() { setuid.ShimMain() } diff --git a/internal/app/setuid/app.go b/internal/app/internal/setuid/app.go similarity index 70% rename from internal/app/setuid/app.go rename to internal/app/internal/setuid/app.go index 472d06e..6af224c 100644 --- a/internal/app/setuid/app.go +++ b/internal/app/internal/setuid/app.go @@ -3,36 +3,28 @@ package setuid import ( "context" "fmt" - "log" "sync" "git.gensokyo.uk/security/fortify/fst" + . "git.gensokyo.uk/security/fortify/internal/app" "git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/sys" ) -func New(ctx context.Context, os sys.State) (fst.App, error) { +func New(ctx context.Context, os sys.State) (App, error) { a := new(app) a.sys = os a.ctx = ctx - id := new(fst.ID) - err := fst.NewAppID(id) + id := new(ID) + err := NewAppID(id) a.id = newID(id) return a, err } -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) - } - return a -} - type app struct { - id *stringPair[fst.ID] + id *stringPair[ID] sys sys.State ctx context.Context @@ -40,7 +32,7 @@ type app struct { mu sync.RWMutex } -func (a *app) ID() fst.ID { a.mu.RLock(); defer a.mu.RUnlock(); return a.id.unwrap() } +func (a *app) ID() ID { a.mu.RLock(); defer a.mu.RUnlock(); return a.id.unwrap() } func (a *app) String() string { if a == nil { @@ -60,7 +52,7 @@ func (a *app) String() string { return fmt.Sprintf("(unsealed app %s)", a.id) } -func (a *app) Seal(config *fst.Config) (fst.SealedApp, error) { +func (a *app) Seal(config *fst.Config) (SealedApp, error) { a.mu.Lock() defer a.mu.Unlock() diff --git a/internal/app/setuid/app_nixos_test.go b/internal/app/internal/setuid/app_nixos_test.go similarity index 99% rename from internal/app/setuid/app_nixos_test.go rename to internal/app/internal/setuid/app_nixos_test.go index 6469f23..0daaf69 100644 --- a/internal/app/setuid/app_nixos_test.go +++ b/internal/app/internal/setuid/app_nixos_test.go @@ -4,6 +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/internal/app" "git.gensokyo.uk/security/fortify/sandbox" "git.gensokyo.uk/security/fortify/system" ) @@ -48,7 +49,7 @@ var testCasesNixos = []sealTestCase{ Enablements: system.EWayland | system.EDBus | system.EPulse, }, }, - fst.ID{ + app.ID{ 0x8e, 0x2c, 0x76, 0xb0, 0x66, 0xda, 0xbe, 0x57, 0x4c, 0xf0, 0x73, 0xbd, diff --git a/internal/app/setuid/app_pd_test.go b/internal/app/internal/setuid/app_pd_test.go similarity index 99% rename from internal/app/setuid/app_pd_test.go rename to internal/app/internal/setuid/app_pd_test.go index c4ab579..2dc0625 100644 --- a/internal/app/setuid/app_pd_test.go +++ b/internal/app/internal/setuid/app_pd_test.go @@ -6,6 +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/internal/app" "git.gensokyo.uk/security/fortify/sandbox" "git.gensokyo.uk/security/fortify/system" ) @@ -20,7 +21,7 @@ var testCasesPd = []sealTestCase{ Outer: "/home/chronos", }, }, - fst.ID{ + app.ID{ 0x4a, 0x45, 0x0b, 0x65, 0x96, 0xd7, 0xbc, 0x15, 0xbd, 0x01, 0x78, 0x0e, @@ -117,7 +118,7 @@ var testCasesPd = []sealTestCase{ Enablements: system.EWayland | system.EDBus | system.EPulse, }, }, - fst.ID{ + app.ID{ 0xeb, 0xf0, 0x83, 0xd1, 0xb1, 0x75, 0x91, 0x17, 0x82, 0xd4, 0x13, 0x36, diff --git a/internal/app/setuid/app_stub_test.go b/internal/app/internal/setuid/app_stub_test.go similarity index 97% rename from internal/app/setuid/app_stub_test.go rename to internal/app/internal/setuid/app_stub_test.go index 0b414f2..c3d0a67 100644 --- a/internal/app/setuid/app_stub_test.go +++ b/internal/app/internal/setuid/app_stub_test.go @@ -7,7 +7,7 @@ import ( "os/user" "strconv" - "git.gensokyo.uk/security/fortify/fst" + "git.gensokyo.uk/security/fortify/internal/app" ) // fs methods are not implemented using a real FS @@ -125,8 +125,8 @@ func (s *stubNixOS) Open(name string) (fs.File, error) { } } -func (s *stubNixOS) Paths() fst.Paths { - return fst.Paths{ +func (s *stubNixOS) Paths() app.Paths { + return app.Paths{ SharePath: "/tmp/fortify.1971", RuntimePath: "/run/user/1971", RunDirPath: "/run/user/1971/fortify", diff --git a/internal/app/setuid/app_test.go b/internal/app/internal/setuid/app_test.go similarity index 95% rename from internal/app/setuid/app_test.go rename to internal/app/internal/setuid/app_test.go index 4454e6f..9e08c07 100644 --- a/internal/app/setuid/app_test.go +++ b/internal/app/internal/setuid/app_test.go @@ -8,7 +8,8 @@ import ( "time" "git.gensokyo.uk/security/fortify/fst" - "git.gensokyo.uk/security/fortify/internal/app/setuid" + "git.gensokyo.uk/security/fortify/internal/app" + "git.gensokyo.uk/security/fortify/internal/app/internal/setuid" "git.gensokyo.uk/security/fortify/internal/sys" "git.gensokyo.uk/security/fortify/sandbox" "git.gensokyo.uk/security/fortify/system" @@ -18,7 +19,7 @@ type sealTestCase struct { name string os sys.State config *fst.Config - id fst.ID + id app.ID wantSys *system.I wantContainer *sandbox.Params } diff --git a/internal/app/setuid/errors.go b/internal/app/internal/setuid/errors.go similarity index 97% rename from internal/app/setuid/errors.go rename to internal/app/internal/setuid/errors.go index e6c9685..fd5acc5 100644 --- a/internal/app/setuid/errors.go +++ b/internal/app/internal/setuid/errors.go @@ -4,11 +4,11 @@ import ( "errors" "log" - "git.gensokyo.uk/security/fortify/fst" + . "git.gensokyo.uk/security/fortify/internal/app" "git.gensokyo.uk/security/fortify/internal/fmsg" ) -func PrintRunStateErr(rs *fst.RunState, runErr error) (code int) { +func PrintRunStateErr(rs *RunState, runErr error) (code int) { code = rs.ExitStatus() if runErr != nil { diff --git a/internal/app/setuid/export_test.go b/internal/app/internal/setuid/export_test.go similarity index 69% rename from internal/app/setuid/export_test.go rename to internal/app/internal/setuid/export_test.go index 7718286..d7e2f16 100644 --- a/internal/app/setuid/export_test.go +++ b/internal/app/internal/setuid/export_test.go @@ -1,20 +1,20 @@ package setuid import ( - "git.gensokyo.uk/security/fortify/fst" + . "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" ) -func NewWithID(id fst.ID, os sys.State) fst.App { +func NewWithID(id ID, os sys.State) App { a := new(app) a.id = newID(&id) a.sys = os return a } -func AppIParams(a fst.App, sa fst.SealedApp) (*system.I, *sandbox.Params) { +func AppIParams(a App, sa SealedApp) (*system.I, *sandbox.Params) { v := a.(*app) seal := sa.(*outcome) if v.outcome != seal || v.id != seal.id { diff --git a/internal/app/setuid/process.go b/internal/app/internal/setuid/process.go similarity index 98% rename from internal/app/setuid/process.go rename to internal/app/internal/setuid/process.go index e730225..271e64e 100644 --- a/internal/app/setuid/process.go +++ b/internal/app/internal/setuid/process.go @@ -12,8 +12,8 @@ import ( "syscall" "time" - "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/fmsg" "git.gensokyo.uk/security/fortify/internal/state" "git.gensokyo.uk/security/fortify/sandbox" @@ -22,7 +22,7 @@ import ( const shimWaitTimeout = 5 * time.Second -func (seal *outcome) Run(rs *fst.RunState) error { +func (seal *outcome) Run(rs *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 diff --git a/internal/app/setuid/seal.go b/internal/app/internal/setuid/seal.go similarity index 98% rename from internal/app/setuid/seal.go rename to internal/app/internal/setuid/seal.go index 92fbc2f..44a73b1 100644 --- a/internal/app/setuid/seal.go +++ b/internal/app/internal/setuid/seal.go @@ -20,6 +20,8 @@ import ( "git.gensokyo.uk/security/fortify/dbus" "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/instance/common" "git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/sys" "git.gensokyo.uk/security/fortify/sandbox" @@ -64,7 +66,7 @@ var posixUsername = regexp.MustCompilePOSIX("^[a-z_]([A-Za-z0-9_-]{0,31}|[A-Za-z // outcome stores copies of various parts of [fst.Config] type outcome struct { // copied from initialising [app] - id *stringPair[fst.ID] + id *stringPair[ID] // copied from [sys.State] response runDirPath string @@ -95,7 +97,7 @@ type shareHost struct { runtimeSharePath string seal *outcome - sc fst.Paths + sc Paths } // ensureRuntimeDir must be called if direct access to paths within XDG_RUNTIME_DIR is required @@ -279,7 +281,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co { var uid, gid int var err error - seal.container, seal.env, err = config.Confinement.Sandbox.ToContainer(sys, &uid, &gid) + seal.container, seal.env, err = common.NewContainer(config.Confinement.Sandbox, sys, &uid, &gid) if err != nil { return fmsg.WrapErrorSuffix(err, "cannot initialise container configuration:") diff --git a/internal/app/setuid/shim.go b/internal/app/internal/setuid/shim.go similarity index 100% rename from internal/app/setuid/shim.go rename to internal/app/internal/setuid/shim.go diff --git a/internal/app/setuid/strings.go b/internal/app/internal/setuid/strings.go similarity index 54% rename from internal/app/setuid/strings.go rename to internal/app/internal/setuid/strings.go index f5b5134..6489d6c 100644 --- a/internal/app/setuid/strings.go +++ b/internal/app/internal/setuid/strings.go @@ -3,11 +3,11 @@ package setuid import ( "strconv" - "git.gensokyo.uk/security/fortify/fst" + . "git.gensokyo.uk/security/fortify/internal/app" ) -func newInt(v int) *stringPair[int] { return &stringPair[int]{v, strconv.Itoa(v)} } -func newID(id *fst.ID) *stringPair[fst.ID] { return &stringPair[fst.ID]{*id, id.String()} } +func newInt(v int) *stringPair[int] { return &stringPair[int]{v, strconv.Itoa(v)} } +func newID(id *ID) *stringPair[ID] { return &stringPair[ID]{*id, id.String()} } // stringPair stores a value and its string representation. type stringPair[T comparable] struct { diff --git a/internal/state/multi.go b/internal/state/multi.go index 58b223c..f443611 100644 --- a/internal/state/multi.go +++ b/internal/state/multi.go @@ -14,6 +14,7 @@ import ( "syscall" "git.gensokyo.uk/security/fortify/fst" + "git.gensokyo.uk/security/fortify/internal/app" "git.gensokyo.uk/security/fortify/internal/fmsg" ) @@ -129,7 +130,7 @@ type multiBackend struct { lock sync.RWMutex } -func (b *multiBackend) filename(id *fst.ID) string { +func (b *multiBackend) filename(id *app.ID) string { return path.Join(b.path, id.String()) } @@ -189,8 +190,8 @@ func (b *multiBackend) load(decode bool) (Entries, error) { return nil, fmt.Errorf("unexpected directory %q in store", e.Name()) } - id := new(fst.ID) - if err := fst.ParseAppID(id, e.Name()); err != nil { + id := new(app.ID) + if err := app.ParseAppID(id, e.Name()); err != nil { return nil, err } @@ -335,7 +336,7 @@ func (b *multiBackend) encodeState(w io.WriteSeeker, state *State, configWriter return err } -func (b *multiBackend) Destroy(id fst.ID) error { +func (b *multiBackend) Destroy(id app.ID) error { b.lock.Lock() defer b.lock.Unlock() diff --git a/internal/state/state.go b/internal/state/state.go index 6e76d51..609b4e9 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -6,11 +6,12 @@ import ( "time" "git.gensokyo.uk/security/fortify/fst" + "git.gensokyo.uk/security/fortify/internal/app" ) var ErrNoConfig = errors.New("state does not contain config") -type Entries map[fst.ID]*State +type Entries map[app.ID]*State type Store interface { // Do calls f exactly once and ensures store exclusivity until f returns. @@ -29,7 +30,7 @@ type Store interface { // Cursor provides access to the store type Cursor interface { Save(state *State, configWriter io.WriterTo) error - Destroy(id fst.ID) error + Destroy(id app.ID) error Load() (Entries, error) Len() (int, error) } @@ -37,7 +38,7 @@ type Cursor interface { // State is a fortify process's state type State struct { // fortify instance id - ID fst.ID `json:"instance"` + ID app.ID `json:"instance"` // child process PID value PID int `json:"pid"` // sealed app configuration diff --git a/internal/state/state_test.go b/internal/state/state_test.go index 22fd3cd..fe7a6cc 100644 --- a/internal/state/state_test.go +++ b/internal/state/state_test.go @@ -11,6 +11,7 @@ import ( "time" "git.gensokyo.uk/security/fortify/fst" + "git.gensokyo.uk/security/fortify/internal/app" "git.gensokyo.uk/security/fortify/internal/state" ) @@ -133,7 +134,7 @@ func testStore(t *testing.T, s state.Store) { } func makeState(t *testing.T, s *state.State, ct io.Writer) { - if err := fst.NewAppID(&s.ID); err != nil { + if err := app.NewAppID(&s.ID); err != nil { t.Fatalf("cannot create dummy state: %v", err) } if err := gob.NewEncoder(ct).Encode(fst.Template()); err != nil { diff --git a/internal/sys/interface.go b/internal/sys/interface.go index 88afd67..ba8e0bf 100644 --- a/internal/sys/interface.go +++ b/internal/sys/interface.go @@ -6,7 +6,7 @@ import ( "path" "strconv" - "git.gensokyo.uk/security/fortify/fst" + "git.gensokyo.uk/security/fortify/internal/app" "git.gensokyo.uk/security/fortify/internal/fmsg" ) @@ -41,14 +41,14 @@ type State interface { Printf(format string, v ...any) // Paths returns a populated [Paths] struct. - Paths() fst.Paths + Paths() app.Paths // Uid invokes fsu and returns target uid. // Any errors returned by Uid is already wrapped [fmsg.BaseError]. Uid(aid int) (int, error) } // CopyPaths is a generic implementation of [fst.Paths]. -func CopyPaths(os State, v *fst.Paths) { +func CopyPaths(os State, v *app.Paths) { v.SharePath = path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Getuid())) fmsg.Verbosef("process share directory at %q", v.SharePath) diff --git a/internal/sys/std.go b/internal/sys/std.go index 5e63396..1b23579 100644 --- a/internal/sys/std.go +++ b/internal/sys/std.go @@ -12,15 +12,15 @@ import ( "sync" "syscall" - "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/fmsg" "git.gensokyo.uk/security/fortify/sandbox" ) // Std implements System using the standard library. type Std struct { - paths fst.Paths + paths app.Paths pathsOnce sync.Once uidOnce sync.Once @@ -48,7 +48,7 @@ func (s *Std) Printf(format string, v ...any) { fmsg.Verbosef(form const xdgRuntimeDir = "XDG_RUNTIME_DIR" -func (s *Std) Paths() fst.Paths { +func (s *Std) Paths() app.Paths { s.pathsOnce.Do(func() { CopyPaths(s, &s.paths) }) return s.paths } diff --git a/main.go b/main.go index aa7de3b..1fe2f7a 100644 --- a/main.go +++ b/main.go @@ -19,7 +19,8 @@ import ( "git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/internal" - "git.gensokyo.uk/security/fortify/internal/app/setuid" + "git.gensokyo.uk/security/fortify/internal/app" + "git.gensokyo.uk/security/fortify/internal/app/instance" "git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/state" "git.gensokyo.uk/security/fortify/internal/sys" @@ -73,7 +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") - c.Command("shim", command.UsageInternal, func([]string) error { setuid.ShimMain(); return errSuccess }) + c.Command("shim", command.UsageInternal, func([]string) error { instance.ShimMain(); return errSuccess }) c.Command("app", "Launch app defined by the specified config file", func(args []string) error { if len(args) < 1 { @@ -239,11 +240,11 @@ func buildCommand(out io.Writer) command.Command { case 1: // instance name := args[0] - config, instance := tryShort(name) + config, entry := tryShort(name) if config == nil { config = tryPath(name) } - printShowInstance(os.Stdout, time.Now().UTC(), instance, config, showFlagShort, flagJSON) + printShowInstance(os.Stdout, time.Now().UTC(), entry, config, showFlagShort, flagJSON) default: log.Fatal("show requires 1 argument") @@ -284,14 +285,14 @@ func runApp(config *fst.Config) { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() // unreachable - a := setuid.MustNew(ctx, std) + a := instance.MustNew(instance.ISetuid, ctx, std) - rs := new(fst.RunState) + rs := new(app.RunState) if sa, err := a.Seal(config); err != nil { fmsg.PrintBaseError(err, "cannot seal app:") internal.Exit(1) } else { - internal.Exit(setuid.PrintRunStateErr(rs, sa.Run(rs))) + internal.Exit(instance.PrintRunStateErr(instance.ISetuid, rs, sa.Run(rs))) } *(*int)(nil) = 0 // not reached diff --git a/parse.go b/parse.go index 4281e32..3efb044 100644 --- a/parse.go +++ b/parse.go @@ -67,7 +67,7 @@ func tryFd(name string) io.ReadCloser { } } -func tryShort(name string) (config *fst.Config, instance *state.State) { +func tryShort(name string) (config *fst.Config, entry *state.State) { likePrefix := false if len(name) <= 32 { likePrefix = true @@ -96,8 +96,8 @@ func tryShort(name string) (config *fst.Config, instance *state.State) { v := id.String() if strings.HasPrefix(v, name) { // match, use config from this state entry - instance = entries[id] - config = instance.Config + entry = entries[id] + config = entry.Config break } diff --git a/print_test.go b/print_test.go index fe5e694..1073a07 100644 --- a/print_test.go +++ b/print_test.go @@ -7,11 +7,12 @@ import ( "git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/fst" + "git.gensokyo.uk/security/fortify/internal/app" "git.gensokyo.uk/security/fortify/internal/state" ) var ( - testID = fst.ID{ + testID = app.ID{ 0x8e, 0x2c, 0x76, 0xb0, 0x66, 0xda, 0xbe, 0x57, 0x4c, 0xf0, 0x73, 0xbd, @@ -457,7 +458,7 @@ func Test_printPs(t *testing.T) { {"no entries", make(state.Entries), false, false, " Instance PID Application Uptime\n"}, {"no entries short", make(state.Entries), true, false, ""}, {"nil instance", state.Entries{testID: nil}, false, false, " Instance PID Application Uptime\n"}, - {"state corruption", state.Entries{fst.ID{}: testState}, false, false, " Instance PID Application Uptime\n"}, + {"state corruption", state.Entries{app.ID{}: testState}, false, false, " Instance PID Application Uptime\n"}, {"valid pd", state.Entries{testID: &state.State{ID: testID, PID: 1 << 8, Config: new(fst.Config), Time: testAppTime}}, false, false, ` Instance PID Application Uptime 8e2c76b0 256 0 (uk.gensokyo.fortify.8e2c76b0) 1h2m32s