From 6bc5be7e5a2334274adf2945ad6d29d063a7086b Mon Sep 17 00:00:00 2001 From: Ophestra Umiker Date: Wed, 23 Oct 2024 21:46:21 +0900 Subject: [PATCH] internal: wrap calls to os standard library functions This change helps tests stub out and simulate OS behaviour during the sealing process. This also removes dependency on XDG_RUNTIME_DIR as the internal.System implementation provided to App provides a compat directory inside the tmpdir-based share when XDG_RUNTIME_DIR is unavailable. Signed-off-by: Ophestra Umiker --- config.go | 1 - internal/app/app.go | 7 +- internal/app/launch.machinectl.go | 3 +- internal/app/launch.sudo.go | 4 +- internal/app/seal.go | 26 +++---- internal/app/share.display.go | 4 +- internal/app/share.pulse.go | 8 +- internal/app/share.runtime.go | 1 + internal/app/share.system.go | 4 +- internal/app/system.go | 9 ++- internal/environ.go | 58 --------------- internal/system.go | 120 ++++++++++++++++++++++++++++++ license.go | 1 - main.go | 11 +-- state.go | 4 +- version.go | 1 - 16 files changed, 161 insertions(+), 101 deletions(-) delete mode 100644 internal/environ.go create mode 100644 internal/system.go diff --git a/config.go b/config.go index 26510e4..05f692c 100644 --- a/config.go +++ b/config.go @@ -4,7 +4,6 @@ import ( "encoding/json" "flag" "fmt" - "os" "git.ophivana.moe/security/fortify/dbus" "git.ophivana.moe/security/fortify/internal" diff --git a/internal/app/app.go b/internal/app/app.go index 0055dd7..de2c461 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -3,6 +3,8 @@ package app import ( "os/exec" "sync" + + "git.ophivana.moe/security/fortify/internal" ) type App interface { @@ -22,6 +24,8 @@ type App interface { type app struct { // application unique identifier id *ID + // operating system interface + os internal.System // underlying user switcher process cmd *exec.Cmd // shim setup abort reason and completion @@ -61,8 +65,9 @@ func (a *app) WaitErr() error { return a.waitErr } -func New() (App, error) { +func New(os internal.System) (App, error) { a := new(app) a.id = new(ID) + a.os = os return a, newAppID(a.id) } diff --git a/internal/app/launch.machinectl.go b/internal/app/launch.machinectl.go index 32f0674..c1d2610 100644 --- a/internal/app/launch.machinectl.go +++ b/internal/app/launch.machinectl.go @@ -1,7 +1,6 @@ package app import ( - "os/exec" "strings" "git.ophivana.moe/security/fortify/internal/fmsg" @@ -31,7 +30,7 @@ func (a *app) commandBuilderMachineCtl(shimEnv string) (args []string) { args = append(args, "--", ".host") // /bin/sh -c - if sh, err := exec.LookPath("sh"); err != nil { + if sh, err := a.os.LookPath("sh"); err != nil { // hardcode /bin/sh path since it exists more often than not args = append(args, "/bin/sh", "-c") } else { diff --git a/internal/app/launch.sudo.go b/internal/app/launch.sudo.go index 2df7be0..e7a2067 100644 --- a/internal/app/launch.sudo.go +++ b/internal/app/launch.sudo.go @@ -1,8 +1,6 @@ package app import ( - "os" - "git.ophivana.moe/security/fortify/internal/fmsg" ) @@ -17,7 +15,7 @@ func (a *app) commandBuilderSudo(shimEnv string) (args []string) { args = append(args, "-Hiu", a.seal.sys.user.Username) // -A? - if _, ok := os.LookupEnv(sudoAskPass); ok { + if _, ok := a.os.LookupEnv(sudoAskPass); ok { fmsg.VPrintln(sudoAskPass, "set, adding askpass flag") args = append(args, "-A") } diff --git a/internal/app/seal.go b/internal/app/seal.go index f0fe558..4501060 100644 --- a/internal/app/seal.go +++ b/internal/app/seal.go @@ -2,8 +2,7 @@ package app import ( "errors" - "os" - "os/exec" + "io/fs" "os/user" "path" "strconv" @@ -67,8 +66,7 @@ type appSeal struct { // seal system-level component sys *appSealSys - // used in various sealing operations - internal.SystemConstants + internal.Paths // protected by upstream mutex } @@ -91,7 +89,7 @@ func (a *app) Seal(config *Config) error { seal := new(appSeal) // fetch system constants - seal.SystemConstants = internal.GetSC() + seal.Paths = a.os.Paths() // pass through config values seal.id = a.id.String() @@ -102,7 +100,7 @@ func (a *app) Seal(config *Config) error { switch config.Method { case method[LaunchMethodSudo]: seal.launchOption = LaunchMethodSudo - if sudoPath, err := exec.LookPath("sudo"); err != nil { + if sudoPath, err := a.os.LookPath("sudo"); err != nil { return fmsg.WrapError(ErrSudo, "sudo not found") } else { @@ -115,7 +113,7 @@ func (a *app) Seal(config *Config) error { "system has not been booted with systemd as init system") } - if machineCtlPath, err := exec.LookPath("machinectl"); err != nil { + if machineCtlPath, err := a.os.LookPath("machinectl"); err != nil { return fmsg.WrapError(ErrMachineCtl, "machinectl not found") } else { @@ -130,14 +128,14 @@ func (a *app) Seal(config *Config) error { seal.sys = new(appSealSys) // look up fortify executable path - if p, err := os.Executable(); err != nil { + if p, err := a.os.Executable(); err != nil { return fmsg.WrapErrorSuffix(err, "cannot look up fortify executable path:") } else { seal.sys.executable = p } // look up user from system - if u, err := user.Lookup(config.User); err != nil { + if u, err := a.os.Lookup(config.User); err != nil { if errors.As(err, new(user.UnknownUserError)) { return fmsg.WrapError(ErrUser, "unknown user", config.User) } else { @@ -160,7 +158,7 @@ func (a *app) Seal(config *Config) error { NoNewSession: true, } // bind entries in / - if d, err := os.ReadDir("/"); err != nil { + if d, err := a.os.ReadDir("/"); err != nil { return err } else { b := make([]*FilesystemConfig, 0, len(d)) @@ -180,7 +178,7 @@ func (a *app) Seal(config *Config) error { conf.Filesystem = append(conf.Filesystem, b...) } // bind entries in /run - if d, err := os.ReadDir("/run"); err != nil { + if d, err := a.os.ReadDir("/run"); err != nil { return err } else { b := make([]*FilesystemConfig, 0, len(d)) @@ -198,7 +196,7 @@ func (a *app) Seal(config *Config) error { } // hide nscd from sandbox if present nscd := "/var/run/nscd" - if _, err := os.Stat(nscd); !errors.Is(err, os.ErrNotExist) { + if _, err := a.os.Stat(nscd); !errors.Is(err, fs.ErrNotExist) { conf.Override = append(conf.Override, nscd) } // bind GPU stuff @@ -222,7 +220,7 @@ func (a *app) Seal(config *Config) error { // open process state store // the simple store only starts holding an open file after first action // store activity begins after Start is called and must end before Wait - seal.store = state.NewSimple(seal.SystemConstants.RunDirPath, seal.sys.user.Uid) + seal.store = state.NewSimple(seal.RunDirPath, seal.sys.user.Uid) // parse string UID if u, err := strconv.Atoi(seal.sys.user.Uid); err != nil { @@ -236,7 +234,7 @@ func (a *app) Seal(config *Config) error { seal.et = config.Confinement.Enablements // this method calls all share methods in sequence - if err := seal.shareAll([2]*dbus.Config{config.Confinement.SessionBus, config.Confinement.SystemBus}); err != nil { + if err := seal.shareAll([2]*dbus.Config{config.Confinement.SessionBus, config.Confinement.SystemBus}, a.os); err != nil { return err } diff --git a/internal/app/share.display.go b/internal/app/share.display.go index 18fad7a..9513211 100644 --- a/internal/app/share.display.go +++ b/internal/app/share.display.go @@ -2,10 +2,10 @@ package app import ( "errors" - "os" "path" "git.ophivana.moe/security/fortify/acl" + "git.ophivana.moe/security/fortify/internal" "git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/system" ) @@ -23,7 +23,7 @@ var ( ErrXDisplay = errors.New(display + " unset") ) -func (seal *appSeal) shareDisplay() error { +func (seal *appSeal) shareDisplay(os internal.System) error { // pass $TERM to launcher if t, ok := os.LookupEnv(term); ok { seal.sys.bwrap.SetEnv[term] = t diff --git a/internal/app/share.pulse.go b/internal/app/share.pulse.go index 83c16fd..fa5c0a7 100644 --- a/internal/app/share.pulse.go +++ b/internal/app/share.pulse.go @@ -4,9 +4,9 @@ import ( "errors" "fmt" "io/fs" - "os" "path" + "git.ophivana.moe/security/fortify/internal" "git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/system" ) @@ -25,7 +25,7 @@ var ( ErrPulseMode = errors.New("unexpected pulse socket mode") ) -func (seal *appSeal) sharePulse() error { +func (seal *appSeal) sharePulse(os internal.System) error { if !seal.et.Has(system.EPulse) { return nil } @@ -65,7 +65,7 @@ func (seal *appSeal) sharePulse() error { seal.sys.bwrap.SetEnv[pulseServer] = "unix:" + p // publish current user's pulse cookie for target user - if src, err := discoverPulseCookie(); err != nil { + if src, err := discoverPulseCookie(os); err != nil { return err } else { dst := path.Join(seal.share, "pulse-cookie") @@ -78,7 +78,7 @@ func (seal *appSeal) sharePulse() error { } // discoverPulseCookie attempts various standard methods to discover the current user's PulseAudio authentication cookie -func discoverPulseCookie() (string, error) { +func discoverPulseCookie(os internal.System) (string, error) { if p, ok := os.LookupEnv(pulseCookie); ok { return p, nil } diff --git a/internal/app/share.runtime.go b/internal/app/share.runtime.go index 1c65aa7..dd3a6b1 100644 --- a/internal/app/share.runtime.go +++ b/internal/app/share.runtime.go @@ -29,6 +29,7 @@ func (seal *appSeal) shareRuntime() { seal.sys.UpdatePermType(system.User, seal.RunDirPath, acl.Execute) // ensure runtime directory ACL (e.g. `/run/user/%d`) + seal.sys.Ensure(seal.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset seal.sys.UpdatePermType(system.User, seal.RuntimePath, acl.Execute) // ensure process-specific share local to XDG_RUNTIME_DIR (e.g. `/run/user/%d/fortify/%s`) diff --git a/internal/app/share.system.go b/internal/app/share.system.go index b8c5f06..600119e 100644 --- a/internal/app/share.system.go +++ b/internal/app/share.system.go @@ -1,10 +1,10 @@ package app import ( - "os" "path" "git.ophivana.moe/security/fortify/acl" + "git.ophivana.moe/security/fortify/internal" "git.ophivana.moe/security/fortify/internal/system" ) @@ -38,7 +38,7 @@ func (seal *appSeal) shareSystem() { seal.sys.bwrap.Tmpfs(seal.SharePath, 1*1024*1024) } -func (seal *appSeal) sharePasswd() { +func (seal *appSeal) sharePasswd(os internal.System) { // look up shell sh := "/bin/sh" if s, ok := os.LookupEnv(shell); ok { diff --git a/internal/app/system.go b/internal/app/system.go index 2fc413e..7ccc2ee 100644 --- a/internal/app/system.go +++ b/internal/app/system.go @@ -5,6 +5,7 @@ import ( "git.ophivana.moe/security/fortify/dbus" "git.ophivana.moe/security/fortify/helper/bwrap" + "git.ophivana.moe/security/fortify/internal" "git.ophivana.moe/security/fortify/internal/system" ) @@ -27,7 +28,7 @@ type appSealSys struct { } // shareAll calls all share methods in sequence -func (seal *appSeal) shareAll(bus [2]*dbus.Config) error { +func (seal *appSeal) shareAll(bus [2]*dbus.Config, os internal.System) error { if seal.shared { panic("seal shared twice") } @@ -35,11 +36,11 @@ func (seal *appSeal) shareAll(bus [2]*dbus.Config) error { seal.shareSystem() seal.shareRuntime() - seal.sharePasswd() - if err := seal.shareDisplay(); err != nil { + seal.sharePasswd(os) + if err := seal.shareDisplay(os); err != nil { return err } - if err := seal.sharePulse(); err != nil { + if err := seal.sharePulse(os); err != nil { return err } diff --git a/internal/environ.go b/internal/environ.go deleted file mode 100644 index 8f03cb3..0000000 --- a/internal/environ.go +++ /dev/null @@ -1,58 +0,0 @@ -package internal - -import ( - "os" - "path" - "strconv" - "sync" - - "git.ophivana.moe/security/fortify/internal/fmsg" -) - -// state that remain constant for the lifetime of the process -// fetched and cached here - -const ( - xdgRuntimeDir = "XDG_RUNTIME_DIR" -) - -// SystemConstants contains state from the operating system -type SystemConstants struct { - // path to shared directory e.g. /tmp/fortify.%d - SharePath string `json:"share_path"` - // XDG_RUNTIME_DIR value e.g. /run/user/%d - RuntimePath string `json:"runtime_path"` - // application runtime directory e.g. /run/user/%d/fortify - RunDirPath string `json:"run_dir_path"` -} - -var ( - scVal SystemConstants - scOnce sync.Once -) - -func copySC() { - sc := SystemConstants{ - SharePath: path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Geteuid())), - } - - fmsg.VPrintf("process share directory at %q", sc.SharePath) - - // runtimePath, runDirPath - if r, ok := os.LookupEnv(xdgRuntimeDir); !ok { - fmsg.Println("variable", xdgRuntimeDir, "unset") - os.Exit(1) - } else { - sc.RuntimePath = r - sc.RunDirPath = path.Join(sc.RuntimePath, "fortify") - fmsg.VPrintf("XDG runtime directory at %q", sc.RunDirPath) - } - - scVal = sc -} - -// GetSC returns a populated SystemConstants value -func GetSC() SystemConstants { - scOnce.Do(copySC) - return scVal -} diff --git a/internal/system.go b/internal/system.go new file mode 100644 index 0000000..6467ea0 --- /dev/null +++ b/internal/system.go @@ -0,0 +1,120 @@ +package internal + +import ( + "io/fs" + "os" + "os/exec" + "os/user" + "path" + "strconv" + "sync" + + "git.ophivana.moe/security/fortify/internal/fmsg" +) + +// System provides safe access to operating system resources. +type System interface { + // Geteuid provides [os.Geteuid]. + Geteuid() int + // LookupEnv provides [os.LookupEnv]. + LookupEnv(key string) (string, bool) + // TempDir provides [os.TempDir]. + TempDir() string + // LookPath provides [exec.LookPath]. + LookPath(file string) (string, error) + // Executable provides [os.Executable]. + Executable() (string, error) + // Lookup provides [user.Lookup]. + Lookup(username string) (*user.User, error) + // ReadDir provides [os.ReadDir]. + ReadDir(name string) ([]os.DirEntry, error) + // Stat provides [os.Stat]. + Stat(name string) (fs.FileInfo, error) + // Open provides [os.Open] + Open(name string) (fs.File, error) + // Exit provides [os.Exit]. + Exit(code int) + + // Paths returns a populated [Paths] struct. + Paths() Paths +} + +// Paths contains environment dependent paths used by fortify. +type Paths struct { + // path to shared directory e.g. /tmp/fortify.%d + SharePath string `json:"share_path"` + // XDG_RUNTIME_DIR value e.g. /run/user/%d + RuntimePath string `json:"runtime_path"` + // application runtime directory e.g. /run/user/%d/fortify + RunDirPath string `json:"run_dir_path"` +} + +// CopyPaths is a generic implementation of [System.Paths]. +func CopyPaths(os System, v *Paths) { + v.SharePath = path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Geteuid())) + + fmsg.VPrintf("process share directory at %q", v.SharePath) + + if r, ok := os.LookupEnv(xdgRuntimeDir); !ok { + // fall back to path in share since fortify has no hard XDG dependency + v.RunDirPath = path.Join(v.SharePath, "run") + v.RuntimePath = path.Join(v.RunDirPath, "compat") + } else { + v.RuntimePath = r + v.RunDirPath = path.Join(v.RuntimePath, "fortify") + } + + fmsg.VPrintf("runtime directory at %q", v.RunDirPath) +} + +// Std implements System using the standard library. +type Std struct { + paths Paths + pathsOnce sync.Once +} + +func (s *Std) Geteuid() int { + return os.Geteuid() +} + +func (s *Std) LookupEnv(key string) (string, bool) { + return os.LookupEnv(key) +} + +func (s *Std) TempDir() string { + return os.TempDir() +} + +func (s *Std) LookPath(file string) (string, error) { + return exec.LookPath(file) +} + +func (s *Std) Executable() (string, error) { + return os.Executable() +} + +func (s *Std) Lookup(username string) (*user.User, error) { + return user.Lookup(username) +} + +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) Exit(code int) { + os.Exit(code) +} + +const xdgRuntimeDir = "XDG_RUNTIME_DIR" + +func (s *Std) Paths() Paths { + s.pathsOnce.Do(func() { CopyPaths(s, &s.paths) }) + return s.paths +} diff --git a/license.go b/license.go index 4cbe012..dbeede0 100644 --- a/license.go +++ b/license.go @@ -4,7 +4,6 @@ import ( _ "embed" "flag" "fmt" - "os" ) var ( diff --git a/main.go b/main.go index 9a390af..3883eff 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,6 @@ package main import ( "flag" - "os" "syscall" "git.ophivana.moe/security/fortify/internal" @@ -20,6 +19,8 @@ func init() { flag.BoolVar(&flagVerbose, "v", false, "Verbose output") } +var os = new(internal.Std) + func main() { // linux/sched/coredump.h if _, _, errno := syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, 0, 0); errno != 0 { @@ -38,9 +39,9 @@ func main() { shim.Try() // root check - if os.Getuid() == 0 { - fmsg.Println("this program must not run as root") - os.Exit(1) + if os.Geteuid() == 0 { + fmsg.Fatal("this program must not run as root") + panic("unreachable") } // version/license/template command early exit @@ -53,7 +54,7 @@ func main() { // invoke app r := 1 - a, err := app.New() + a, err := app.New(os) if err != nil { fmsg.Fatalf("cannot create app: %s\n", err) } else if err = a.Seal(loadConfig()); err != nil { diff --git a/state.go b/state.go index c4551ac..40b550c 100644 --- a/state.go +++ b/state.go @@ -3,10 +3,8 @@ package main import ( "flag" "fmt" - "os" "text/tabwriter" - "git.ophivana.moe/security/fortify/internal" "git.ophivana.moe/security/fortify/internal/fmsg" "git.ophivana.moe/security/fortify/internal/state" ) @@ -23,7 +21,7 @@ func init() { func tryState() { if stateActionEarly { var w *tabwriter.Writer - state.MustPrintLauncherStateSimpleGlobal(&w, internal.GetSC().RunDirPath) + state.MustPrintLauncherStateSimpleGlobal(&w, os.Paths().RunDirPath) if w != nil { if err := w.Flush(); err != nil { fmsg.Println("cannot format output:", err) diff --git a/version.go b/version.go index 808a57b..d3136cd 100644 --- a/version.go +++ b/version.go @@ -3,7 +3,6 @@ package main import ( "flag" "fmt" - "os" ) var (