aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/app/instance
diff options
context:
space:
mode:
Diffstat (limited to 'internal/app/instance')
-rw-r--r--internal/app/instance/common/container.go187
-rw-r--r--internal/app/instance/common/path.go11
-rw-r--r--internal/app/instance/common/path_test.go85
-rw-r--r--internal/app/instance/errors.go17
-rw-r--r--internal/app/instance/new.go33
-rw-r--r--internal/app/instance/shim.go6
6 files changed, 339 insertions, 0 deletions
diff --git a/internal/app/instance/common/container.go b/internal/app/instance/common/container.go
new file mode 100644
index 00000000..cc26c803
--- /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/internal/app/instance/common/path.go b/internal/app/instance/common/path.go
new file mode 100644
index 00000000..ce40f4f1
--- /dev/null
+++ b/internal/app/instance/common/path.go
@@ -0,0 +1,11 @@
+package common
+
+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/internal/app/instance/common/path_test.go b/internal/app/instance/common/path_test.go
new file mode 100644
index 00000000..b14f24df
--- /dev/null
+++ b/internal/app/instance/common/path_test.go
@@ -0,0 +1,85 @@
+package common
+
+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/internal/app/instance/errors.go b/internal/app/instance/errors.go
new file mode 100644
index 00000000..b3331e3d
--- /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 00000000..cb5e8b27
--- /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 00000000..bc497ad3
--- /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() }