diff --git a/fst/path.go b/fst/path.go new file mode 100644 index 0000000..2bdab87 --- /dev/null +++ b/fst/path.go @@ -0,0 +1,11 @@ +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 new file mode 100644 index 0000000..3589590 --- /dev/null +++ b/fst/path_test.go @@ -0,0 +1,85 @@ +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 32508fd..8def908 100644 --- a/fst/sandbox.go +++ b/fst/sandbox.go @@ -2,8 +2,13 @@ package fst import ( "errors" + "fmt" + "io/fs" + "path" + "git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/helper/bwrap" + "git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/linux" ) @@ -68,7 +73,8 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) { DieWithParent: true, AsInit: true, - // initialise map + // initialise unconditionally as Once cannot be justified + // for saving such a miniscule amount of memory Chmod: make(bwrap.ChmodConfig), }). SetUID(uid).SetGID(uid). @@ -89,16 +95,85 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) { } } + // retrieve paths and hide them if they're made available in the sandbox + 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, 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 == "/" { + fmsg.VPrintf("dbus socket %q is in an unusual location", pair[1]) + } + hidePaths = append(hidePaths, dir) + } else { + fmsg.VPrintf("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, err + } + } + for _, c := range s.Filesystem { if c == nil { continue } - src := c.Src + + if !path.IsAbs(c.Src) { + return 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, fmt.Errorf("dst path %q is not absolute", dest) + } + + srcH := c.Src + if err := evalSymlinks(os, &srcH); err != nil { + return nil, err + } + + for i := range hidePaths { + // skip matched entries + if hidePathMatch[i] { + continue + } + + if ok, err := deepContainsH(srcH, hidePaths[i]); err != nil { + return nil, err + } else if ok { + hidePathMatch[i] = true + fmsg.VPrintf("hiding paths from %q", c.Src) + } + } + + conf.Bind(c.Src, dest, !c.Must, c.Write, c.Device) + } + + // hide marked paths before setting up shares + for i, ok := range hidePathMatch { + if ok { + conf.Tmpfs(hidePaths[i], 8192) } - conf.Bind(src, dest, !c.Must, c.Write, c.Device) } for _, l := range s.Link { @@ -133,3 +208,15 @@ func (s *SandboxConfig) Bwrap(os linux.System) (*bwrap.Config, error) { return conf, nil } + +func evalSymlinks(os linux.System, v *string) error { + if p, err := os.EvalSymlinks(*v); err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return err + } + fmsg.VPrintf("path %q does not yet exist", *v) + } else { + *v = p + } + return nil +} diff --git a/internal/app/app_pd_test.go b/internal/app/app_pd_test.go index f025b44..968a11a 100644 --- a/internal/app/app_pd_test.go +++ b/internal/app/app_pd_test.go @@ -62,45 +62,14 @@ var testCasesPd = []sealTestCase{ Bind("/lib64", "/lib64", false, true). Bind("/nix", "/nix", false, true). Bind("/root", "/root", false, true). + Bind("/run", "/run", false, true). Bind("/srv", "/srv", false, true). Bind("/sys", "/sys", false, true). Bind("/usr", "/usr", false, true). Bind("/var", "/var", false, true). - Bind("/run/agetty.reload", "/run/agetty.reload", false, true). - Bind("/run/binfmt", "/run/binfmt", false, true). - Bind("/run/booted-system", "/run/booted-system", false, true). - Bind("/run/credentials", "/run/credentials", false, true). - Bind("/run/cryptsetup", "/run/cryptsetup", false, true). - Bind("/run/current-system", "/run/current-system", false, true). - Bind("/run/host", "/run/host", false, true). - Bind("/run/keys", "/run/keys", false, true). - Bind("/run/libvirt", "/run/libvirt", false, true). - Bind("/run/libvirtd.pid", "/run/libvirtd.pid", false, true). - Bind("/run/lock", "/run/lock", false, true). - Bind("/run/log", "/run/log", false, true). - Bind("/run/lvm", "/run/lvm", false, true). - Bind("/run/mount", "/run/mount", false, true). - Bind("/run/NetworkManager", "/run/NetworkManager", false, true). - Bind("/run/nginx", "/run/nginx", false, true). - Bind("/run/nixos", "/run/nixos", false, true). - Bind("/run/nscd", "/run/nscd", false, true). - Bind("/run/opengl-driver", "/run/opengl-driver", false, true). - Bind("/run/pppd", "/run/pppd", false, true). - Bind("/run/resolvconf", "/run/resolvconf", false, true). - Bind("/run/sddm", "/run/sddm", false, true). - Bind("/run/store", "/run/store", false, true). - Bind("/run/syncoid", "/run/syncoid", false, true). - Bind("/run/system", "/run/system", false, true). - Bind("/run/systemd", "/run/systemd", false, true). - Bind("/run/tmpfiles.d", "/run/tmpfiles.d", false, true). - Bind("/run/udev", "/run/udev", false, true). - Bind("/run/udisks2", "/run/udisks2", false, true). - Bind("/run/utmp", "/run/utmp", false, true). - Bind("/run/virtlogd.pid", "/run/virtlogd.pid", false, true). - Bind("/run/wrappers", "/run/wrappers", false, true). - Bind("/run/zed.pid", "/run/zed.pid", false, true). - Bind("/run/zed.state", "/run/zed.state", false, true). Bind("/dev/kvm", "/dev/kvm", true, true, true). + Tmpfs("/run/user/1971", 8192). + Tmpfs("/run/dbus", 8192). Bind("/etc", fst.Tmp+"/etc"). Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa"). Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc"). @@ -317,46 +286,15 @@ var testCasesPd = []sealTestCase{ Bind("/lib64", "/lib64", false, true). Bind("/nix", "/nix", false, true). Bind("/root", "/root", false, true). + Bind("/run", "/run", false, true). Bind("/srv", "/srv", false, true). Bind("/sys", "/sys", false, true). Bind("/usr", "/usr", false, true). Bind("/var", "/var", false, true). - Bind("/run/agetty.reload", "/run/agetty.reload", false, true). - Bind("/run/binfmt", "/run/binfmt", false, true). - Bind("/run/booted-system", "/run/booted-system", false, true). - Bind("/run/credentials", "/run/credentials", false, true). - Bind("/run/cryptsetup", "/run/cryptsetup", false, true). - Bind("/run/current-system", "/run/current-system", false, true). - Bind("/run/host", "/run/host", false, true). - Bind("/run/keys", "/run/keys", false, true). - Bind("/run/libvirt", "/run/libvirt", false, true). - Bind("/run/libvirtd.pid", "/run/libvirtd.pid", false, true). - Bind("/run/lock", "/run/lock", false, true). - Bind("/run/log", "/run/log", false, true). - Bind("/run/lvm", "/run/lvm", false, true). - Bind("/run/mount", "/run/mount", false, true). - Bind("/run/NetworkManager", "/run/NetworkManager", false, true). - Bind("/run/nginx", "/run/nginx", false, true). - Bind("/run/nixos", "/run/nixos", false, true). - Bind("/run/nscd", "/run/nscd", false, true). - Bind("/run/opengl-driver", "/run/opengl-driver", false, true). - Bind("/run/pppd", "/run/pppd", false, true). - Bind("/run/resolvconf", "/run/resolvconf", false, true). - Bind("/run/sddm", "/run/sddm", false, true). - Bind("/run/store", "/run/store", false, true). - Bind("/run/syncoid", "/run/syncoid", false, true). - Bind("/run/system", "/run/system", false, true). - Bind("/run/systemd", "/run/systemd", false, true). - Bind("/run/tmpfiles.d", "/run/tmpfiles.d", false, true). - Bind("/run/udev", "/run/udev", false, true). - Bind("/run/udisks2", "/run/udisks2", false, true). - Bind("/run/utmp", "/run/utmp", false, true). - Bind("/run/virtlogd.pid", "/run/virtlogd.pid", false, true). - Bind("/run/wrappers", "/run/wrappers", false, true). - Bind("/run/zed.pid", "/run/zed.pid", false, true). - Bind("/run/zed.state", "/run/zed.state", false, true). Bind("/dev/dri", "/dev/dri", true, true, true). Bind("/dev/kvm", "/dev/kvm", true, true, true). + Tmpfs("/run/user/1971", 8192). + Tmpfs("/run/dbus", 8192). Bind("/etc", fst.Tmp+"/etc"). Symlink(fst.Tmp+"/etc/alsa", "/etc/alsa"). Symlink(fst.Tmp+"/etc/bashrc", "/etc/bashrc"). diff --git a/internal/app/app_stub_test.go b/internal/app/app_stub_test.go index 9ab0919..4d2e83e 100644 --- a/internal/app/app_stub_test.go +++ b/internal/app/app_stub_test.go @@ -2,7 +2,6 @@ package app_test import ( "fmt" - "io" "io/fs" "os/user" "strconv" @@ -128,12 +127,12 @@ func (s *stubNixOS) Open(name string) (fs.File, error) { } } -func (s *stubNixOS) Exit(code int) { - panic("called exit on stub with code " + strconv.Itoa(code)) +func (s *stubNixOS) EvalSymlinks(path string) (string, error) { + return path, nil } -func (s *stubNixOS) Stdout() io.Writer { - panic("requested stdout") +func (s *stubNixOS) Exit(code int) { + panic("called exit on stub with code " + strconv.Itoa(code)) } func (s *stubNixOS) Paths() linux.Paths { diff --git a/internal/app/app_test.go b/internal/app/app_test.go index a8fd761..25691ff 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -1,6 +1,7 @@ package app_test import ( + "encoding/json" "io/fs" "reflect" "testing" @@ -48,14 +49,22 @@ func TestApp(t *testing.T) { t.Run("compare bwrap", func(t *testing.T) { if !reflect.DeepEqual(gotBwrap, tc.wantBwrap) { - t.Errorf("seal: bwrap = %#v, want %#v", - gotBwrap, tc.wantBwrap) + t.Errorf("seal: bwrap =\n%s\n, want\n%s", + mustMarshal(gotBwrap), mustMarshal(tc.wantBwrap)) } }) }) } } +func mustMarshal(v any) string { + if b, err := json.Marshal(v); err != nil { + panic(err.Error()) + } else { + return string(b) + } +} + func stubDirEntries(names ...string) (e []fs.DirEntry, err error) { e = make([]fs.DirEntry, len(names)) for i, name := range names { diff --git a/internal/app/seal.go b/internal/app/seal.go index 2766377..a67feb4 100644 --- a/internal/app/seal.go +++ b/internal/app/seal.go @@ -194,7 +194,6 @@ func (a *app) Seal(config *fst.Config) error { switch p { case "/proc": case "/dev": - case "/run": case "/tmp": case "/mnt": case "/etc": @@ -205,23 +204,7 @@ func (a *app) Seal(config *fst.Config) error { } conf.Filesystem = append(conf.Filesystem, b...) } - // bind entries in /run - if d, err := a.os.ReadDir("/run"); err != nil { - return err - } else { - b := make([]*fst.FilesystemConfig, 0, len(d)) - for _, ent := range d { - name := ent.Name() - switch name { - case "user": - case "dbus": - default: - p := "/run/" + name - b = append(b, &fst.FilesystemConfig{Src: p, Write: true, Must: true}) - } - } - conf.Filesystem = append(conf.Filesystem, b...) - } + // hide nscd from sandbox if present nscd := "/var/run/nscd" if _, err := a.os.Stat(nscd); !errors.Is(err, fs.ErrNotExist) { diff --git a/internal/linux/interface.go b/internal/linux/interface.go index dd0782d..1bb87db 100644 --- a/internal/linux/interface.go +++ b/internal/linux/interface.go @@ -1,7 +1,6 @@ package linux import ( - "io" "io/fs" "os/user" "path" @@ -30,10 +29,10 @@ type System interface { Stat(name string) (fs.FileInfo, error) // Open provides [os.Open] Open(name string) (fs.File, error) + // EvalSymlinks provides [filepath.EvalSymlinks] + EvalSymlinks(path string) (string, error) // Exit provides [os.Exit]. Exit(code int) - // Stdout provides [os.Stdout]. - Stdout() io.Writer // Paths returns a populated [Paths] struct. Paths() Paths diff --git a/internal/linux/std.go b/internal/linux/std.go index a82fff2..7c6b927 100644 --- a/internal/linux/std.go +++ b/internal/linux/std.go @@ -2,11 +2,11 @@ package linux import ( "errors" - "io" "io/fs" "os" "os/exec" "os/user" + "path/filepath" "strconv" "sync" "syscall" @@ -37,8 +37,8 @@ func (s *Std) LookupGroup(name string) (*user.Group, error) { return user.Lookup func (s *Std) ReadDir(name string) ([]os.DirEntry, error) { return os.ReadDir(name) } func (s *Std) Stat(name string) (fs.FileInfo, error) { return os.Stat(name) } func (s *Std) Open(name string) (fs.File, error) { return os.Open(name) } +func (s *Std) EvalSymlinks(path string) (string, error) { return filepath.EvalSymlinks(path) } func (s *Std) Exit(code int) { fmsg.Exit(code) } -func (s *Std) Stdout() io.Writer { return os.Stdout } const xdgRuntimeDir = "XDG_RUNTIME_DIR"