diff --git a/error.go b/error.go index ee48684..a941ac5 100644 --- a/error.go +++ b/error.go @@ -6,11 +6,12 @@ import ( "os" "git.ophivana.moe/cat/fortify/internal/app" + "git.ophivana.moe/cat/fortify/internal/fmsg" ) func logWaitError(err error) { - var e *app.BaseError - if !app.AsBaseError(err, &e) { + var e *fmsg.BaseError + if !fmsg.AsBaseError(err, &e) { fmt.Println("fortify: wait failed:", err) } else { // Wait only returns either *app.ProcessError or *app.StateStoreError wrapped in a *app.BaseError @@ -31,7 +32,7 @@ func logWaitError(err error) { // every error here is wrapped in *app.BaseError for _, ei := range errs { - var eb *app.BaseError + var eb *fmsg.BaseError if !errors.As(ei, &eb) { // unreachable fmt.Println("fortify: invalid error type returned by revert:", ei) @@ -46,9 +47,9 @@ func logWaitError(err error) { } func logBaseError(err error, message string) { - var e *app.BaseError + var e *fmsg.BaseError - if app.AsBaseError(err, &e) { + if fmsg.AsBaseError(err, &e) { fmt.Print("fortify: " + e.Message()) } else { fmt.Println(message, err) diff --git a/internal/app/app.go b/internal/app/app.go index 50d59da..a04ae74 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -40,7 +40,7 @@ func (a *app) String() string { } if a.seal != nil { - return "(sealed fortified app as uid " + a.seal.sys.Uid + ")" + return "(sealed fortified app as uid " + a.seal.sys.user.Uid + ")" } return "(unsealed fortified app)" diff --git a/internal/app/config.go b/internal/app/config.go index 3f6d1c0..72648f0 100644 --- a/internal/app/config.go +++ b/internal/app/config.go @@ -61,8 +61,8 @@ type SandboxConfig struct { Env map[string]string `json:"env"` // sandbox host filesystem access Filesystem []*FilesystemConfig `json:"filesystem"` - // tmpfs mount points to mount last - Tmpfs []string `json:"tmpfs"` + // paths to override by mounting tmpfs over them + Override []string `json:"override"` } type FilesystemConfig struct { @@ -149,7 +149,7 @@ func Template() *Config { {Src: "/data/user/0", Dst: "/data/data", Write: true, Must: true}, {Src: "/var/tmp", Write: true}, }, - Tmpfs: []string{"/var/run/nscd"}, + Override: []string{"/var/run/nscd"}, }, SystemBus: &dbus.Config{ See: nil, diff --git a/internal/app/copy.go b/internal/app/copy.go deleted file mode 100644 index bf90bfe..0000000 --- a/internal/app/copy.go +++ /dev/null @@ -1,33 +0,0 @@ -package app - -import ( - "io" - "os" -) - -func copyFile(dst, src string) error { - srcD, err := os.Open(src) - if err != nil { - return err - } - defer func() { - if srcD.Close() != nil { - // unreachable - panic("src file closed prematurely") - } - }() - - dstD, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) - if err != nil { - return err - } - defer func() { - if dstD.Close() != nil { - // unreachable - panic("dst file closed prematurely") - } - }() - - _, err = io.Copy(dstD, srcD) - return err -} diff --git a/internal/app/error.go b/internal/app/error.go deleted file mode 100644 index 7c18a89..0000000 --- a/internal/app/error.go +++ /dev/null @@ -1,51 +0,0 @@ -package app - -import ( - "fmt" - "reflect" -) - -// baseError implements a basic error container -type baseError struct { - Err error -} - -func (e *baseError) Error() string { - return e.Err.Error() -} - -func (e *baseError) Unwrap() error { - return e.Err -} - -// BaseError implements an error container with a user-facing message -type BaseError struct { - message string - baseError -} - -// Message returns a user-facing error message -func (e *BaseError) Message() string { - return e.message -} - -func wrapError(err error, a ...any) *BaseError { - return &BaseError{ - message: fmt.Sprintln(a...), - baseError: baseError{err}, - } -} - -var ( - baseErrorType = reflect.TypeFor[*BaseError]() -) - -func AsBaseError(err error, target **BaseError) bool { - v := reflect.ValueOf(err) - if !v.CanConvert(baseErrorType) { - return false - } - - *target = v.Convert(baseErrorType).Interface().(*BaseError) - return true -} diff --git a/internal/app/launch.machinectl.go b/internal/app/launch.machinectl.go index 9fb9ea0..606fd9d 100644 --- a/internal/app/launch.machinectl.go +++ b/internal/app/launch.machinectl.go @@ -4,7 +4,6 @@ import ( "os/exec" "strings" - "git.ophivana.moe/cat/fortify/internal/state" "git.ophivana.moe/cat/fortify/internal/verbose" ) @@ -12,7 +11,7 @@ func (a *app) commandBuilderMachineCtl(shimEnv string) (args []string) { args = make([]string, 0, 9+len(a.seal.sys.bwrap.SetEnv)) // shell --uid=$USER - args = append(args, "shell", "--uid="+a.seal.sys.Username) + args = append(args, "shell", "--uid="+a.seal.sys.user.Username) // --quiet if !verbose.Get() { @@ -49,14 +48,6 @@ func (a *app) commandBuilderMachineCtl(shimEnv string) (args []string) { } innerCommand.WriteString("; ") - // override message bus address if enabled - if a.seal.et.Has(state.EnableDBus) { - innerCommand.WriteString(dbusSessionBusAddress + "=" + "'" + "unix:path=" + a.seal.sys.dbusAddr[0][1] + "' ") - if a.seal.sys.dbusSystem { - innerCommand.WriteString(dbusSystemBusAddress + "=" + "'" + "unix:path=" + a.seal.sys.dbusAddr[1][1] + "' ") - } - } - // launch fortify as shim innerCommand.WriteString("exec " + a.seal.sys.executable + " shim") diff --git a/internal/app/launch.sudo.go b/internal/app/launch.sudo.go index 3e364ac..965af96 100644 --- a/internal/app/launch.sudo.go +++ b/internal/app/launch.sudo.go @@ -14,7 +14,7 @@ func (a *app) commandBuilderSudo(shimEnv string) (args []string) { args = make([]string, 0, 8) // -Hiu $USER - args = append(args, "-Hiu", a.seal.sys.Username) + args = append(args, "-Hiu", a.seal.sys.user.Username) // -A? if _, ok := os.LookupEnv(sudoAskPass); ok { diff --git a/internal/app/seal.go b/internal/app/seal.go index bf634c4..76ce8d3 100644 --- a/internal/app/seal.go +++ b/internal/app/seal.go @@ -10,7 +10,9 @@ import ( "git.ophivana.moe/cat/fortify/dbus" "git.ophivana.moe/cat/fortify/internal" + "git.ophivana.moe/cat/fortify/internal/fmsg" "git.ophivana.moe/cat/fortify/internal/state" + "git.ophivana.moe/cat/fortify/internal/system" "git.ophivana.moe/cat/fortify/internal/verbose" ) @@ -29,12 +31,6 @@ var ( ErrMachineCtl = errors.New("machinectl not available") ) -type ( - SealConfigError BaseError - LauncherLookupError BaseError - SecurityError BaseError -) - // Seal seals the app launch context func (a *app) Seal(config *Config) error { a.lock.Lock() @@ -45,7 +41,8 @@ func (a *app) Seal(config *Config) error { } if config == nil { - return (*SealConfigError)(wrapError(ErrConfig, "attempted to seal app with nil config")) + return fmsg.WrapError(ErrConfig, + "attempted to seal app with nil config") } // create seal @@ -53,7 +50,8 @@ func (a *app) Seal(config *Config) error { // generate application ID if id, err := newAppID(); err != nil { - return (*SecurityError)(wrapError(err, "cannot generate application ID:", err)) + return fmsg.WrapErrorSuffix(err, + "cannot generate application ID:") } else { seal.id = id } @@ -70,32 +68,35 @@ func (a *app) Seal(config *Config) error { case "sudo": seal.launchOption = LaunchMethodSudo if sudoPath, err := exec.LookPath("sudo"); err != nil { - return (*LauncherLookupError)(wrapError(ErrSudo, "sudo not found")) + return fmsg.WrapError(ErrSudo, + "sudo not found") } else { seal.toolPath = sudoPath } case "systemd": seal.launchOption = LaunchMethodMachineCtl if !internal.SdBootedV { - return (*LauncherLookupError)(wrapError(ErrSystemd, - "system has not been booted with systemd as init system")) + return fmsg.WrapError(ErrSystemd, + "system has not been booted with systemd as init system") } if machineCtlPath, err := exec.LookPath("machinectl"); err != nil { - return (*LauncherLookupError)(wrapError(ErrMachineCtl, "machinectl not found")) + return fmsg.WrapError(ErrMachineCtl, + "machinectl not found") } else { seal.toolPath = machineCtlPath } default: - return (*SealConfigError)(wrapError(ErrLaunch, "invalid launch method")) + return fmsg.WrapError(ErrLaunch, + "invalid launch method") } // create seal system component - seal.sys = new(appSealTx) + seal.sys = new(appSealSys) // look up fortify executable path if p, err := os.Executable(); err != nil { - return (*LauncherLookupError)(wrapError(err, "cannot look up fortify executable path:", err)) + return fmsg.WrapErrorSuffix(err, "cannot look up fortify executable path:") } else { seal.sys.executable = p } @@ -103,13 +104,13 @@ func (a *app) Seal(config *Config) error { // look up user from system if u, err := user.Lookup(config.User); err != nil { if errors.As(err, new(user.UnknownUserError)) { - return (*SealConfigError)(wrapError(ErrUser, "unknown user", config.User)) + return fmsg.WrapError(ErrUser, "unknown user", config.User) } else { // unreachable panic(err) } } else { - seal.sys.User = u + seal.sys.user = u seal.sys.runtime = path.Join("/run/user", u.Uid) } @@ -163,7 +164,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) { - conf.Tmpfs = append(conf.Tmpfs, nscd) + conf.Override = append(conf.Override, nscd) } // bind GPU stuff if config.Confinement.Enablements.Has(state.EnableX) || config.Confinement.Enablements.Has(state.EnableWayland) { @@ -172,7 +173,7 @@ func (a *app) Seal(config *Config) error { config.Confinement.Sandbox = conf } seal.sys.bwrap = config.Confinement.Sandbox.Bwrap() - seal.sys.tmpfs = config.Confinement.Sandbox.Tmpfs + seal.sys.override = config.Confinement.Sandbox.Override if seal.sys.bwrap.SetEnv == nil { seal.sys.bwrap.SetEnv = make(map[string]string) } @@ -186,14 +187,14 @@ 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.Uid) + seal.store = state.NewSimple(seal.SystemConstants.RunDirPath, seal.sys.user.Uid) // parse string UID - if u, err := strconv.Atoi(seal.sys.Uid); err != nil { + if u, err := strconv.Atoi(seal.sys.user.Uid); err != nil { // unreachable unless kernel bug panic("uid parse") } else { - seal.sys.uid = u + seal.sys.I = system.New(u) } // pass through enablements @@ -206,7 +207,7 @@ func (a *app) Seal(config *Config) error { // verbose log seal information verbose.Println("created application seal as user", - seal.sys.Username, "("+seal.sys.Uid+"),", + seal.sys.user.Username, "("+seal.sys.user.Uid+"),", "method:", config.Method+",", "launcher:", seal.toolPath+",", "command:", config.Command) diff --git a/internal/app/share.dbus.go b/internal/app/share.dbus.go index 912cd97..6b0ff43 100644 --- a/internal/app/share.dbus.go +++ b/internal/app/share.dbus.go @@ -1,15 +1,11 @@ package app import ( - "errors" - "fmt" - "os" "path" "git.ophivana.moe/cat/fortify/acl" "git.ophivana.moe/cat/fortify/dbus" "git.ophivana.moe/cat/fortify/internal/state" - "git.ophivana.moe/cat/fortify/internal/verbose" ) const ( @@ -17,122 +13,30 @@ const ( dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS" ) -var ( - ErrDBusConfig = errors.New("dbus config not supplied") -) - -type ( - SealDBusError BaseError - LookupDBusError BaseError - StartDBusError BaseError - CloseDBusError BaseError -) - func (seal *appSeal) shareDBus(config [2]*dbus.Config) error { if !seal.et.Has(state.EnableDBus) { return nil } - // session bus is mandatory - if config[0] == nil { - return (*SealDBusError)(wrapError(ErrDBusConfig, "attempted to seal session bus proxy with nil config")) - } - - // system bus is optional - seal.sys.dbusSystem = config[1] != nil - - // upstream address, downstream socket path - var sessionBus, systemBus [2]string - // downstream socket paths - sessionBus[1] = path.Join(seal.share, "bus") - systemBus[1] = path.Join(seal.share, "system_bus_socket") + sessionPath, systemPath := path.Join(seal.share, "bus"), path.Join(seal.share, "system_bus_socket") - // resolve upstream bus addresses - sessionBus[0], systemBus[0] = dbus.Address() - - // create proxy instance - seal.sys.dbus = dbus.New(sessionBus, systemBus) - - // seal dbus proxy - if err := seal.sys.dbus.Seal(config[0], config[1]); err != nil { - return (*SealDBusError)(wrapError(err, "cannot seal message bus proxy:", err)) + // configure dbus proxy + if err := seal.sys.ProxyDBus(config[0], config[1], sessionPath, systemPath); err != nil { + return err } - // store addresses for cleanup and logging - seal.sys.dbusAddr = &[2][2]string{sessionBus, systemBus} - // share proxy sockets sessionInner := path.Join(seal.sys.runtime, "bus") - seal.sys.setEnv(dbusSessionBusAddress, "unix:path="+sessionInner) - seal.sys.bwrap.Bind(sessionBus[1], sessionInner) - seal.sys.updatePerm(sessionBus[1], acl.Read, acl.Write) - if seal.sys.dbusSystem { + seal.sys.bwrap.SetEnv[dbusSessionBusAddress] = "unix:path=" + sessionInner + seal.sys.bwrap.Bind(sessionPath, sessionInner) + seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write) + if config[1] != nil { systemInner := "/run/dbus/system_bus_socket" - seal.sys.setEnv(dbusSystemBusAddress, "unix:path="+systemInner) - seal.sys.bwrap.Bind(systemBus[1], systemInner) - seal.sys.updatePerm(systemBus[1], acl.Read, acl.Write) + seal.sys.bwrap.SetEnv[dbusSystemBusAddress] = "unix:path=" + systemInner + seal.sys.bwrap.Bind(systemPath, systemInner) + seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write) } return nil } - -func (tx *appSealTx) startDBus() error { - // ready channel passed to dbus package - ready := make(chan error, 1) - // used by waiting goroutine to notify process return - tx.dbusWait = make(chan struct{}) - - // background dbus proxy start - if err := tx.dbus.Start(ready, os.Stderr, true); err != nil { - return (*StartDBusError)(wrapError(err, "cannot start message bus proxy:", err)) - } - verbose.Println("starting message bus proxy:", tx.dbus) - verbose.Println("message bus proxy bwrap args:", tx.dbus.Bwrap()) - - // background wait for proxy instance and notify completion - go func() { - if err := tx.dbus.Wait(); err != nil { - fmt.Println("fortify: warn: message bus proxy returned error:", err) - go func() { ready <- err }() - } else { - verbose.Println("message bus proxy exit") - } - - // ensure socket removal so ephemeral directory is empty at revert - if err := os.Remove(tx.dbusAddr[0][1]); err != nil && !errors.Is(err, os.ErrNotExist) { - fmt.Println("fortify: cannot remove dangling session bus socket:", err) - } - if tx.dbusSystem { - if err := os.Remove(tx.dbusAddr[1][1]); err != nil && !errors.Is(err, os.ErrNotExist) { - fmt.Println("fortify: cannot remove dangling system bus socket:", err) - } - } - - // notify proxy completion - tx.dbusWait <- struct{}{} - }() - - // ready is not nil if the proxy process faulted - if err := <-ready; err != nil { - // note that err here is either an I/O related error or a predetermined unexpected behaviour error - return (*StartDBusError)(wrapError(err, "message bus proxy fault after start:", err)) - } - verbose.Println("message bus proxy ready") - - return nil -} - -func (tx *appSealTx) stopDBus() error { - if err := tx.dbus.Close(); err != nil { - if errors.Is(err, os.ErrClosed) { - return (*CloseDBusError)(wrapError(err, "message bus proxy already closed")) - } else { - return (*CloseDBusError)(wrapError(err, "cannot close message bus proxy:", err)) - } - } - - // block until proxy wait returns - <-tx.dbusWait - return nil -} diff --git a/internal/app/share.display.go b/internal/app/share.display.go index 86343d8..2675c01 100644 --- a/internal/app/share.display.go +++ b/internal/app/share.display.go @@ -6,6 +6,7 @@ import ( "path" "git.ophivana.moe/cat/fortify/acl" + "git.ophivana.moe/cat/fortify/internal/fmsg" "git.ophivana.moe/cat/fortify/internal/state" ) @@ -22,29 +23,28 @@ var ( ErrXDisplay = errors.New(display + " unset") ) -type ErrDisplayEnv BaseError - func (seal *appSeal) shareDisplay() error { // pass $TERM to launcher if t, ok := os.LookupEnv(term); ok { - seal.sys.setEnv(term, t) + seal.sys.bwrap.SetEnv[term] = t } // set up wayland if seal.et.Has(state.EnableWayland) { if wd, ok := os.LookupEnv(waylandDisplay); !ok { - return (*ErrDisplayEnv)(wrapError(ErrWayland, "WAYLAND_DISPLAY is not set")) + return fmsg.WrapError(ErrWayland, + "WAYLAND_DISPLAY is not set") } else if seal.wlDone == nil { // hardlink wayland socket wp := path.Join(seal.RuntimePath, wd) wpi := path.Join(seal.shareLocal, "wayland") w := path.Join(seal.sys.runtime, "wayland-0") - seal.sys.link(wp, wpi) - seal.sys.setEnv(waylandDisplay, w) + seal.sys.Link(wp, wpi) + seal.sys.bwrap.SetEnv[waylandDisplay] = w seal.sys.bwrap.Bind(wpi, w) // ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`) - seal.sys.updatePermTag(state.EnableWayland, wp, acl.Read, acl.Write, acl.Execute) + seal.sys.UpdatePermType(state.EnableWayland, wp, acl.Read, acl.Write, acl.Execute) } else { // set wayland socket path (e.g. `/run/user/%d/wayland-%d`) seal.wl = path.Join(seal.RuntimePath, wd) @@ -55,10 +55,11 @@ func (seal *appSeal) shareDisplay() error { if seal.et.Has(state.EnableX) { // discover X11 and grant user permission via the `ChangeHosts` command if d, ok := os.LookupEnv(display); !ok { - return (*ErrDisplayEnv)(wrapError(ErrXDisplay, "DISPLAY is not set")) + return fmsg.WrapError(ErrXDisplay, + "DISPLAY is not set") } else { - seal.sys.changeHosts(seal.sys.Username) - seal.sys.setEnv(display, d) + seal.sys.ChangeHosts(seal.sys.user.Username) + seal.sys.bwrap.SetEnv[display] = d seal.sys.bwrap.Bind("/tmp/.X11-unix", "/tmp/.X11-unix") } } diff --git a/internal/app/share.pulse.go b/internal/app/share.pulse.go index df26b16..0103b89 100644 --- a/internal/app/share.pulse.go +++ b/internal/app/share.pulse.go @@ -7,6 +7,7 @@ import ( "os" "path" + "git.ophivana.moe/cat/fortify/internal/fmsg" "git.ophivana.moe/cat/fortify/internal/state" ) @@ -24,11 +25,6 @@ var ( ErrPulseMode = errors.New("unexpected pulse socket mode") ) -type ( - PulseCookieAccessError BaseError - PulseSocketAccessError BaseError -) - func (seal *appSeal) sharePulse() error { if !seal.et.Has(state.EnablePulse) { return nil @@ -39,42 +35,43 @@ func (seal *appSeal) sharePulse() error { ps := path.Join(pd, "native") if _, err := os.Stat(pd); err != nil { if !errors.Is(err, fs.ErrNotExist) { - return (*PulseSocketAccessError)(wrapError(err, - fmt.Sprintf("cannot access PulseAudio directory '%s':", pd), err)) + return fmsg.WrapErrorSuffix(err, + fmt.Sprintf("cannot access PulseAudio directory %q:", pd)) } - return (*PulseSocketAccessError)(wrapError(ErrPulseSocket, - fmt.Sprintf("PulseAudio directory '%s' not found", pd))) + return fmsg.WrapError(ErrPulseSocket, + fmt.Sprintf("PulseAudio directory %q not found", pd)) } // check PulseAudio socket permission (e.g. `/run/user/%d/pulse/native`) if s, err := os.Stat(ps); err != nil { if !errors.Is(err, fs.ErrNotExist) { - return (*PulseSocketAccessError)(wrapError(err, - fmt.Sprintf("cannot access PulseAudio socket '%s':", ps), err)) + return fmsg.WrapErrorSuffix(err, + fmt.Sprintf("cannot access PulseAudio socket %q:", ps)) } - return (*PulseSocketAccessError)(wrapError(ErrPulseSocket, - fmt.Sprintf("PulseAudio directory '%s' found but socket does not exist", pd))) + return fmsg.WrapError(ErrPulseSocket, + fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pd)) } else { if m := s.Mode(); m&0o006 != 0o006 { - return (*PulseSocketAccessError)(wrapError(ErrPulseMode, - fmt.Sprintf("unexpected permissions on '%s':", ps), m)) + return fmsg.WrapError(ErrPulseMode, + fmt.Sprintf("unexpected permissions on %q:", ps), m) } } // hard link pulse socket into target-executable share psi := path.Join(seal.shareLocal, "pulse") p := path.Join(seal.sys.runtime, "pulse", "native") - seal.sys.link(ps, psi) + seal.sys.Link(ps, psi) seal.sys.bwrap.Bind(psi, p) - seal.sys.setEnv(pulseServer, "unix:"+p) + seal.sys.bwrap.SetEnv[pulseServer] = "unix:" + p // publish current user's pulse cookie for target user if src, err := discoverPulseCookie(); err != nil { return err } else { dst := path.Join(seal.share, "pulse-cookie") - seal.sys.setEnv(pulseCookie, dst) - seal.sys.copyFile(dst, src) + seal.sys.bwrap.SetEnv[pulseCookie] = dst + seal.sys.CopyFile(dst, src) + seal.sys.bwrap.Bind(dst, dst) } return nil @@ -91,8 +88,8 @@ func discoverPulseCookie() (string, error) { p = path.Join(p, ".pulse-cookie") if s, err := os.Stat(p); err != nil { if !errors.Is(err, fs.ErrNotExist) { - return p, (*PulseCookieAccessError)(wrapError(err, - fmt.Sprintf("cannot access PulseAudio cookie '%s':", p), err)) + return p, fmsg.WrapErrorSuffix(err, + fmt.Sprintf("cannot access PulseAudio cookie %q:", p)) } // not found, try next method } else if !s.IsDir() { @@ -105,7 +102,8 @@ func discoverPulseCookie() (string, error) { p = path.Join(p, "pulse", "cookie") if s, err := os.Stat(p); err != nil { if !errors.Is(err, fs.ErrNotExist) { - return p, (*PulseCookieAccessError)(wrapError(err, "cannot access PulseAudio cookie", p+":", err)) + return p, fmsg.WrapErrorSuffix(err, + fmt.Sprintf("cannot access PulseAudio cookie %q:", p)) } // not found, try next method } else if !s.IsDir() { @@ -113,7 +111,7 @@ func discoverPulseCookie() (string, error) { } } - return "", (*PulseCookieAccessError)(wrapError(ErrPulseCookie, + return "", fmsg.WrapError(ErrPulseCookie, fmt.Sprintf("cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)", - pulseCookie, xdgConfigHome, home))) + pulseCookie, xdgConfigHome, home)) } diff --git a/internal/app/share.runtime.go b/internal/app/share.runtime.go index ebb13aa..0c7149e 100644 --- a/internal/app/share.runtime.go +++ b/internal/app/share.runtime.go @@ -4,7 +4,7 @@ import ( "path" "git.ophivana.moe/cat/fortify/acl" - "git.ophivana.moe/cat/fortify/internal/state" + "git.ophivana.moe/cat/fortify/internal/system" ) const ( @@ -20,28 +20,28 @@ func (seal *appSeal) shareRuntime() { seal.sys.bwrap.Tmpfs(seal.sys.runtime, 8*1024*1024) // point to inner runtime path `/run/user/%d` - seal.sys.setEnv(xdgRuntimeDir, seal.sys.runtime) - seal.sys.setEnv(xdgSessionClass, "user") - seal.sys.setEnv(xdgSessionType, "tty") + seal.sys.bwrap.SetEnv[xdgRuntimeDir] = seal.sys.runtime + seal.sys.bwrap.SetEnv[xdgSessionClass] = "user" + seal.sys.bwrap.SetEnv[xdgSessionType] = "tty" // ensure RunDir (e.g. `/run/user/%d/fortify`) - seal.sys.ensure(seal.RunDirPath, 0700) - seal.sys.updatePermTag(state.EnableLength, seal.RunDirPath, acl.Execute) + seal.sys.Ensure(seal.RunDirPath, 0700) + seal.sys.UpdatePermType(system.User, seal.RunDirPath, acl.Execute) // ensure runtime directory ACL (e.g. `/run/user/%d`) - seal.sys.updatePermTag(state.EnableLength, seal.RuntimePath, acl.Execute) + seal.sys.UpdatePermType(system.User, seal.RuntimePath, acl.Execute) // ensure Share (e.g. `/tmp/fortify.%d`) // acl is unnecessary as this directory is world executable - seal.sys.ensure(seal.SharePath, 0701) + seal.sys.Ensure(seal.SharePath, 0701) // ensure process-specific share (e.g. `/tmp/fortify.%d/%s`) // acl is unnecessary as this directory is world executable seal.share = path.Join(seal.SharePath, seal.id.String()) - seal.sys.ensureEphemeral(seal.share, 0701) + seal.sys.Ephemeral(system.Process, seal.share, 0701) // ensure process-specific share local to XDG_RUNTIME_DIR (e.g. `/run/user/%d/fortify/%s`) seal.shareLocal = path.Join(seal.RunDirPath, seal.id.String()) - seal.sys.ensureEphemeral(seal.shareLocal, 0700) - seal.sys.updatePerm(seal.shareLocal, acl.Execute) + seal.sys.Ephemeral(system.Process, seal.shareLocal, 0700) + seal.sys.UpdatePerm(seal.shareLocal, acl.Execute) } diff --git a/internal/app/share.system.go b/internal/app/share.system.go index e1a3e1d..e4c848b 100644 --- a/internal/app/share.system.go +++ b/internal/app/share.system.go @@ -5,7 +5,7 @@ import ( "path" "git.ophivana.moe/cat/fortify/acl" - "git.ophivana.moe/cat/fortify/internal/state" + "git.ophivana.moe/cat/fortify/internal/system" ) const ( @@ -17,28 +17,28 @@ func (seal *appSeal) shareSystem() { // look up shell sh := "/bin/sh" if s, ok := os.LookupEnv(shell); ok { - seal.sys.setEnv(shell, s) + seal.sys.bwrap.SetEnv[shell] = s sh = s } // generate /etc/passwd passwdPath := path.Join(seal.share, "passwd") username := "chronos" - if seal.sys.Username != "" { - username = seal.sys.Username - seal.sys.setEnv("USER", seal.sys.Username) + if seal.sys.user.Username != "" { + username = seal.sys.user.Username + seal.sys.bwrap.SetEnv["USER"] = seal.sys.user.Username } homeDir := "/var/empty" - if seal.sys.HomeDir != "" { - homeDir = seal.sys.HomeDir - seal.sys.setEnv("HOME", seal.sys.HomeDir) + if seal.sys.user.HomeDir != "" { + homeDir = seal.sys.user.HomeDir + seal.sys.bwrap.SetEnv["HOME"] = seal.sys.user.HomeDir } passwd := username + ":x:65534:65534:Fortify:" + homeDir + ":" + sh + "\n" - seal.sys.writeFile(passwdPath, []byte(passwd)) + seal.sys.Write(passwdPath, passwd) // write /etc/group groupPath := path.Join(seal.share, "group") - seal.sys.writeFile(groupPath, []byte("fortify:x:65534:\n")) + seal.sys.Write(groupPath, "fortify:x:65534:\n") // bind /etc/passwd and /etc/group seal.sys.bwrap.Bind(passwdPath, "/etc/passwd") @@ -48,13 +48,13 @@ func (seal *appSeal) shareSystem() { func (seal *appSeal) shareTmpdirChild() string { // ensure child tmpdir parent directory (e.g. `/tmp/fortify.%d/tmpdir`) targetTmpdirParent := path.Join(seal.SharePath, "tmpdir") - seal.sys.ensure(targetTmpdirParent, 0700) - seal.sys.updatePermTag(state.EnableLength, targetTmpdirParent, acl.Execute) + seal.sys.Ensure(targetTmpdirParent, 0700) + seal.sys.UpdatePermType(system.User, targetTmpdirParent, acl.Execute) // ensure child tmpdir (e.g. `/tmp/fortify.%d/tmpdir/%d`) - targetTmpdir := path.Join(targetTmpdirParent, seal.sys.Uid) - seal.sys.ensure(targetTmpdir, 01700) - seal.sys.updatePermTag(state.EnableLength, targetTmpdir, acl.Read, acl.Write, acl.Execute) + targetTmpdir := path.Join(targetTmpdirParent, seal.sys.user.Uid) + seal.sys.Ensure(targetTmpdir, 01700) + seal.sys.UpdatePermType(system.User, targetTmpdir, acl.Read, acl.Write, acl.Execute) seal.sys.bwrap.Bind(targetTmpdir, "/tmp", false, true) // mount tmpfs on inner shared directory (e.g. `/tmp/fortify.%d`) diff --git a/internal/app/start.go b/internal/app/start.go index c496561..9e54063 100644 --- a/internal/app/start.go +++ b/internal/app/start.go @@ -8,21 +8,17 @@ import ( "path" "path/filepath" "strconv" + "strings" "time" "git.ophivana.moe/cat/fortify/helper" + "git.ophivana.moe/cat/fortify/internal/fmsg" "git.ophivana.moe/cat/fortify/internal/shim" "git.ophivana.moe/cat/fortify/internal/state" + "git.ophivana.moe/cat/fortify/internal/system" "git.ophivana.moe/cat/fortify/internal/verbose" ) -type ( - // ProcessError encapsulates errors returned by starting *exec.Cmd - ProcessError BaseError - // ShimError encapsulates errors returned by shim.ServeConfig. - ShimError BaseError -) - // Start starts the fortified child func (a *app) Start() error { a.lock.Lock() @@ -41,12 +37,13 @@ func (a *app) Start() error { if s, err := exec.LookPath(n); err == nil { shimExec[i] = s } else { - return (*ProcessError)(wrapError(err, fmt.Sprintf("cannot find %q: %v", n, err))) + return fmsg.WrapErrorSuffix(err, + fmt.Sprintf("cannot find %q:", n)) } } } - if err := a.seal.sys.commit(); err != nil { + if err := a.seal.sys.Commit(); err != nil { return err } @@ -70,7 +67,7 @@ func (a *app) Start() error { a.cmd.Stderr = os.Stderr a.cmd.Dir = a.seal.RunDirPath - if wls, err := shim.ServeConfig(confSockPath, a.seal.sys.uid, &shim.Payload{ + if wls, err := shim.ServeConfig(confSockPath, a.seal.sys.UID(), &shim.Payload{ Argv: a.seal.command, Exec: shimExec, Bwrap: a.seal.sys.bwrap, @@ -78,7 +75,8 @@ func (a *app) Start() error { Verbose: verbose.Get(), }, a.seal.wl, a.seal.wlDone); err != nil { - return (*ShimError)(wrapError(err, "cannot listen on shim socket:", err)) + return fmsg.WrapErrorSuffix(err, + "cannot listen on shim socket:") } else { a.wayland = wls } @@ -86,7 +84,8 @@ func (a *app) Start() error { // start shim verbose.Println("starting shim as target user:", a.cmd) if err := a.cmd.Start(); err != nil { - return (*ProcessError)(wrapError(err, "cannot start process:", err)) + return fmsg.WrapErrorSuffix(err, + "cannot start process:") } startTime := time.Now().UTC() @@ -105,7 +104,7 @@ func (a *app) Start() error { err.Inner, err.DoErr = a.seal.store.Do(func(b state.Backend) { err.InnerErr = b.Save(&sd) }) - return err.equiv("cannot save process state:", err) + return err.equiv("cannot save process state:") } // StateStoreError is returned for a failed state save @@ -124,7 +123,7 @@ func (e *StateStoreError) equiv(a ...any) error { if e.Inner == true && e.DoErr == nil && e.InnerErr == nil && e.Err == nil { return nil } else { - return wrapError(e, a...) + return fmsg.WrapErrorSuffix(e, a...) } } @@ -203,15 +202,16 @@ func (a *app) Wait() (int, error) { } // enablements of remaining launchers - rt, tags := new(state.Enablements), new(state.Enablements) - tags.Set(state.EnableLength + 1) + rt, ec := new(state.Enablements), new(system.Criteria) + ec.Enablements = new(state.Enablements) + ec.Set(system.Process) if states, err := b.Load(); err != nil { return err } else { if l := len(states); l == 0 { // cleanup globals as the final launcher verbose.Println("no other launchers active, will clean up globals") - tags.Set(state.EnableLength) + ec.Set(system.User) } else { verbose.Printf("found %d active launchers, cleaning up without globals\n", l) } @@ -224,22 +224,22 @@ func (a *app) Wait() (int, error) { // invert accumulated enablements for cleanup for i := state.Enablement(0); i < state.EnableLength; i++ { if !rt.Has(i) { - tags.Set(i) + ec.Set(i) } } if verbose.Get() { - ct := make([]state.Enablement, 0, state.EnableLength) - for i := state.Enablement(0); i < state.EnableLength; i++ { - if tags.Has(i) { - ct = append(ct, i) + labels := make([]string, 0, state.EnableLength+1) + for i := state.Enablement(0); i < state.EnableLength+2; i++ { + if ec.Has(i) { + labels = append(labels, system.TypeString(i)) } } - if len(ct) > 0 { - verbose.Println("will revert operations tagged", ct, "as no remaining launchers hold these enablements") + if len(labels) > 0 { + verbose.Println("reverting operations labelled", strings.Join(labels, ", ")) } } - if err := a.seal.sys.revert(tags); err != nil { + if err := a.seal.sys.Revert(ec); err != nil { return err.(RevertCompoundError) } diff --git a/internal/app/system.go b/internal/app/system.go index a016293..5696647 100644 --- a/internal/app/system.go +++ b/internal/app/system.go @@ -1,19 +1,14 @@ package app import ( - "errors" - "fmt" - "io/fs" - "os" "os/user" - "git.ophivana.moe/cat/fortify/acl" "git.ophivana.moe/cat/fortify/dbus" "git.ophivana.moe/cat/fortify/helper/bwrap" "git.ophivana.moe/cat/fortify/internal" "git.ophivana.moe/cat/fortify/internal/state" + "git.ophivana.moe/cat/fortify/internal/system" "git.ophivana.moe/cat/fortify/internal/verbose" - "git.ophivana.moe/cat/fortify/xcb" ) // appSeal seals the application with child-related information @@ -48,7 +43,7 @@ type appSeal struct { // prevents sharing from happening twice shared bool // seal system-level component - sys *appSealTx + sys *appSealSys // used in various sealing operations internal.SystemConstants @@ -56,357 +51,24 @@ type appSeal struct { // protected by upstream mutex } -// appSealTx contains the system-level component of the app seal -type appSealTx struct { +// appSealSys encapsulates app seal behaviour with OS interactions +type appSealSys struct { bwrap *bwrap.Config - tmpfs []string - - // reference to D-Bus proxy instance, nil if disabled - dbus *dbus.Proxy - // notification from goroutine waiting for dbus.Proxy - dbusWait chan struct{} - // upstream address/downstream path used to initialise dbus.Proxy - dbusAddr *[2][2]string - // whether system bus proxy is enabled - dbusSystem bool - - // paths to append/strip ACLs (of target user) from - acl []*appACLEntry - // X11 ChangeHosts commands to perform - xhost []string - // paths of directories to ensure - mkdir []appEnsureEntry - // dst, data pairs of temporarily available files - files [][2]string - // dst, src pairs of temporarily shared files - tmpfiles [][2]string - // dst, src pairs of temporarily hard linked files - hardlinks [][2]string + // paths to override by mounting tmpfs over them + override []string // default formatted XDG_RUNTIME_DIR of User runtime string // sealed path to fortify executable, used by shim executable string - // target user UID as an integer - uid int // target user sealed from config - *user.User + user *user.User - // prevents commit from happening twice - complete bool - // prevents cleanup from happening twice - closed bool + *system.I // protected by upstream mutex } -type appEnsureEntry struct { - path string - perm os.FileMode - remove bool -} - -// setEnv sets an environment variable for the child process -func (tx *appSealTx) setEnv(k, v string) { - tx.bwrap.SetEnv[k] = v -} - -// ensure appends a directory ensure action -func (tx *appSealTx) ensure(path string, perm os.FileMode) { - tx.mkdir = append(tx.mkdir, appEnsureEntry{path, perm, false}) -} - -// ensureEphemeral appends a directory ensure action with removal in rollback -func (tx *appSealTx) ensureEphemeral(path string, perm os.FileMode) { - tx.mkdir = append(tx.mkdir, appEnsureEntry{path, perm, true}) -} - -// appACLEntry contains information for applying/reverting an ACL entry -type appACLEntry struct { - tag state.Enablement - path string - perms []acl.Perm -} - -func (e *appACLEntry) ts() string { - switch e.tag { - case state.EnableLength: - return "Global" - case state.EnableLength + 1: - return "Process" - default: - return e.tag.String() - } -} - -func (e *appACLEntry) String() string { - var s = []byte("---") - for _, p := range e.perms { - switch p { - case acl.Read: - s[0] = 'r' - case acl.Write: - s[1] = 'w' - case acl.Execute: - s[2] = 'x' - } - } - return string(s) -} - -// updatePerm appends an untagged acl update action -func (tx *appSealTx) updatePerm(path string, perms ...acl.Perm) { - tx.updatePermTag(state.EnableLength+1, path, perms...) -} - -// updatePermTag appends an acl update action -// Tagging with state.EnableLength sets cleanup to happen at final active launcher exit, -// while tagging with state.EnableLength+1 will unconditionally clean up on exit. -func (tx *appSealTx) updatePermTag(tag state.Enablement, path string, perms ...acl.Perm) { - tx.acl = append(tx.acl, &appACLEntry{tag, path, perms}) -} - -// changeHosts appends target username of an X11 ChangeHosts action -func (tx *appSealTx) changeHosts(username string) { - tx.xhost = append(tx.xhost, username) -} - -// writeFile appends a files action -func (tx *appSealTx) writeFile(dst string, data []byte) { - tx.files = append(tx.files, [2]string{dst, string(data)}) - tx.updatePerm(dst, acl.Read) - tx.bwrap.Bind(dst, dst) -} - -// copyFile appends a tmpfiles action -func (tx *appSealTx) copyFile(dst, src string) { - tx.tmpfiles = append(tx.tmpfiles, [2]string{dst, src}) - tx.updatePerm(dst, acl.Read) - tx.bwrap.Bind(dst, dst) -} - -// link appends a hardlink action -func (tx *appSealTx) link(oldname, newname string) { - tx.hardlinks = append(tx.hardlinks, [2]string{oldname, newname}) -} - -type ( - ChangeHostsError BaseError - EnsureDirError BaseError - TmpfileError BaseError - DBusStartError BaseError - ACLUpdateError BaseError -) - -// commit applies recorded actions -// order: xhost, mkdir, files, tmpfiles, hardlinks, dbus, acl -func (tx *appSealTx) commit() error { - if tx.complete { - panic("seal transaction committed twice") - } - tx.complete = true - - txp := &appSealTx{User: tx.User, bwrap: &bwrap.Config{SetEnv: make(map[string]string)}} - defer func() { - // rollback partial commit - if txp != nil { - // global changes (x11, ACLs) are always repeated and check for other launchers cannot happen here - // attempting cleanup here will cause other fortified processes to lose access to them - // a better (and more secure) fix is to proxy access to these resources and eliminate the ACLs altogether - tags := new(state.Enablements) - for e := state.Enablement(0); e < state.EnableLength+2; e++ { - tags.Set(e) - } - if err := txp.revert(tags); err != nil { - fmt.Println("fortify: errors returned reverting partial commit:", err) - } - } - }() - - // insert xhost entries - for _, username := range tx.xhost { - verbose.Printf("inserting XHost entry SI:localuser:%s\n", username) - if err := xcb.ChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+username); err != nil { - return (*ChangeHostsError)(wrapError(err, - fmt.Sprintf("cannot insert XHost entry SI:localuser:%s, %s", username, err))) - } else { - // register partial commit - txp.changeHosts(username) - } - } - - // ensure directories - for _, dir := range tx.mkdir { - verbose.Println("ensuring directory mode:", dir.perm.String(), "path:", dir.path) - if err := os.Mkdir(dir.path, dir.perm); err != nil && !errors.Is(err, fs.ErrExist) { - return (*EnsureDirError)(wrapError(err, - fmt.Sprintf("cannot create directory '%s': %s", dir.path, err))) - } else { - // only ephemeral dirs require rollback - if dir.remove { - // register partial commit - txp.ensureEphemeral(dir.path, dir.perm) - } - } - } - - // write files - for _, file := range tx.files { - verbose.Println("writing", len(file[1]), "bytes of data to", file[0]) - if err := os.WriteFile(file[0], []byte(file[1]), 0600); err != nil { - return (*TmpfileError)(wrapError(err, - fmt.Sprintf("cannot write file '%s': %s", file[0], err))) - } else { - // register partial commit - txp.writeFile(file[0], make([]byte, 0)) // data not necessary for revert - } - } - - // publish tmpfiles - for _, tmpfile := range tx.tmpfiles { - verbose.Println("publishing tmpfile", tmpfile[0], "from", tmpfile[1]) - if err := copyFile(tmpfile[0], tmpfile[1]); err != nil { - return (*TmpfileError)(wrapError(err, - fmt.Sprintf("cannot publish tmpfile '%s' from '%s': %s", tmpfile[0], tmpfile[1], err))) - } else { - // register partial commit - txp.copyFile(tmpfile[0], tmpfile[1]) - } - } - - // create hardlinks - for _, link := range tx.hardlinks { - verbose.Println("creating hardlink", link[1], "from", link[0]) - if err := os.Link(link[0], link[1]); err != nil { - return (*TmpfileError)(wrapError(err, - fmt.Sprintf("cannot create hardlink '%s' from '%s': %s", link[1], link[0], err))) - } else { - // register partial commit - txp.link(link[0], link[1]) - } - } - - if tx.dbus != nil { - // start dbus proxy - verbose.Printf("session bus proxy on '%s' for upstream '%s'\n", tx.dbusAddr[0][1], tx.dbusAddr[0][0]) - if tx.dbusSystem { - verbose.Printf("system bus proxy on '%s' for upstream '%s'\n", tx.dbusAddr[1][1], tx.dbusAddr[1][0]) - } - if err := tx.startDBus(); err != nil { - return (*DBusStartError)(wrapError(err, "cannot start message bus proxy:", err)) - } else { - txp.dbus = tx.dbus - txp.dbusAddr = tx.dbusAddr - txp.dbusSystem = tx.dbusSystem - txp.dbusWait = tx.dbusWait - } - } - - // apply ACLs - for _, e := range tx.acl { - verbose.Println("applying ACL", e, "uid:", tx.Uid, "tag:", e.ts(), "path:", e.path) - if err := acl.UpdatePerm(e.path, tx.uid, e.perms...); err != nil { - return (*ACLUpdateError)(wrapError(err, - fmt.Sprintf("cannot apply ACL to '%s': %s", e.path, err))) - } else { - // register partial commit - txp.updatePermTag(e.tag, e.path, e.perms...) - } - } - - // disarm partial commit rollback - txp = nil - - // queue tmpfs at the end of tx.bwrap.Filesystem - for _, dest := range tx.tmpfs { - tx.bwrap.Tmpfs(dest, 8*1024) - } - - return nil -} - -// revert rolls back recorded actions -// order: acl, dbus, hardlinks, tmpfiles, files, mkdir, xhost -// errors are printed but not treated as fatal -func (tx *appSealTx) revert(tags *state.Enablements) error { - if tx.closed { - panic("seal transaction reverted twice") - } - tx.closed = true - - // will be slightly over-sized with ephemeral dirs - errs := make([]error, 0, len(tx.acl)+1+len(tx.tmpfiles)+len(tx.mkdir)+len(tx.xhost)) - joinError := func(err error, a ...any) { - var e error - if err != nil { - e = wrapError(err, a...) - } - errs = append(errs, e) - } - - // revert ACLs - for _, e := range tx.acl { - if tags.Has(e.tag) { - verbose.Println("stripping ACL", e, "uid:", tx.Uid, "tag:", e.ts(), "path:", e.path) - err := acl.UpdatePerm(e.path, tx.uid) - joinError(err, fmt.Sprintf("cannot strip ACL entry from '%s': %s", e.path, err)) - } else { - verbose.Println("skipping ACL", e, "uid:", tx.Uid, "tag:", e.ts(), "path:", e.path) - } - } - - if tx.dbus != nil { - // stop dbus proxy - verbose.Println("terminating message bus proxy") - err := tx.stopDBus() - joinError(err, "cannot stop message bus proxy:", err) - } - - // remove hardlinks - for _, link := range tx.hardlinks { - verbose.Println("removing hardlink", link[1]) - err := os.Remove(link[1]) - joinError(err, fmt.Sprintf("cannot remove hardlink '%s': %s", link[1], err)) - } - - // remove tmpfiles - for _, tmpfile := range tx.tmpfiles { - verbose.Println("removing tmpfile", tmpfile[0]) - err := os.Remove(tmpfile[0]) - joinError(err, fmt.Sprintf("cannot remove tmpfile '%s': %s", tmpfile[0], err)) - } - - // remove files - for _, file := range tx.files { - verbose.Println("removing file", file[0]) - err := os.Remove(file[0]) - joinError(err, fmt.Sprintf("cannot remove file '%s': %s", file[0], err)) - } - - // remove (empty) ephemeral directories - for i := len(tx.mkdir); i > 0; i-- { - dir := tx.mkdir[i-1] - if !dir.remove { - continue - } - - verbose.Println("destroying ephemeral directory mode:", dir.perm.String(), "path:", dir.path) - err := os.Remove(dir.path) - joinError(err, fmt.Sprintf("cannot remove ephemeral directory '%s': %s", dir.path, err)) - } - - if tags.Has(state.EnableX) { - // rollback xhost insertions - for _, username := range tx.xhost { - verbose.Printf("deleting XHost entry SI:localuser:%s\n", username) - err := xcb.ChangeHosts(xcb.HostModeDelete, xcb.FamilyServerInterpreted, "localuser\x00"+username) - joinError(err, "cannot remove XHost entry:", err) - } - } - - return errors.Join(errs...) -} - // shareAll calls all share methods in sequence func (seal *appSeal) shareAll(bus [2]*dbus.Config) error { if seal.shared { @@ -432,12 +94,11 @@ func (seal *appSeal) shareAll(bus [2]*dbus.Config) error { if err := seal.shareDBus(bus); err != nil { return err - } else if seal.sys.dbusAddr != nil { // set if D-Bus enabled and share successful - verbose.Println("sealed session proxy", bus[0].Args(seal.sys.dbusAddr[0])) - if bus[1] != nil { - verbose.Println("sealed system proxy", bus[1].Args(seal.sys.dbusAddr[1])) - } - verbose.Println("message bus proxy final args:", seal.sys.dbus) + } + + // queue overriding tmpfs at the end of seal.sys.bwrap.Filesystem + for _, dest := range seal.sys.override { + seal.sys.bwrap.Tmpfs(dest, 8*1024) } return nil