app: port app to use the system package

This commit does away with almost all baggage left over from the Ego port. Error wrapping also got simplified. All API changes happens to be internal which means no changes to main except renaming of the BaseError type.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
This commit is contained in:
Ophestra Umiker 2024-10-16 01:38:59 +09:00
parent 430f1a5b4e
commit 084cd84f36
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
15 changed files with 144 additions and 671 deletions

View File

@ -6,11 +6,12 @@ import (
"os" "os"
"git.ophivana.moe/cat/fortify/internal/app" "git.ophivana.moe/cat/fortify/internal/app"
"git.ophivana.moe/cat/fortify/internal/fmsg"
) )
func logWaitError(err error) { func logWaitError(err error) {
var e *app.BaseError var e *fmsg.BaseError
if !app.AsBaseError(err, &e) { if !fmsg.AsBaseError(err, &e) {
fmt.Println("fortify: wait failed:", err) fmt.Println("fortify: wait failed:", err)
} else { } else {
// Wait only returns either *app.ProcessError or *app.StateStoreError wrapped in a *app.BaseError // 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 // every error here is wrapped in *app.BaseError
for _, ei := range errs { for _, ei := range errs {
var eb *app.BaseError var eb *fmsg.BaseError
if !errors.As(ei, &eb) { if !errors.As(ei, &eb) {
// unreachable // unreachable
fmt.Println("fortify: invalid error type returned by revert:", ei) fmt.Println("fortify: invalid error type returned by revert:", ei)
@ -46,9 +47,9 @@ func logWaitError(err error) {
} }
func logBaseError(err error, message string) { 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()) fmt.Print("fortify: " + e.Message())
} else { } else {
fmt.Println(message, err) fmt.Println(message, err)

View File

@ -40,7 +40,7 @@ func (a *app) String() string {
} }
if a.seal != nil { 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)" return "(unsealed fortified app)"

View File

@ -61,8 +61,8 @@ type SandboxConfig struct {
Env map[string]string `json:"env"` Env map[string]string `json:"env"`
// sandbox host filesystem access // sandbox host filesystem access
Filesystem []*FilesystemConfig `json:"filesystem"` Filesystem []*FilesystemConfig `json:"filesystem"`
// tmpfs mount points to mount last // paths to override by mounting tmpfs over them
Tmpfs []string `json:"tmpfs"` Override []string `json:"override"`
} }
type FilesystemConfig struct { type FilesystemConfig struct {
@ -149,7 +149,7 @@ func Template() *Config {
{Src: "/data/user/0", Dst: "/data/data", Write: true, Must: true}, {Src: "/data/user/0", Dst: "/data/data", Write: true, Must: true},
{Src: "/var/tmp", Write: true}, {Src: "/var/tmp", Write: true},
}, },
Tmpfs: []string{"/var/run/nscd"}, Override: []string{"/var/run/nscd"},
}, },
SystemBus: &dbus.Config{ SystemBus: &dbus.Config{
See: nil, See: nil,

View File

@ -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
}

View File

@ -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
}

View File

@ -4,7 +4,6 @@ import (
"os/exec" "os/exec"
"strings" "strings"
"git.ophivana.moe/cat/fortify/internal/state"
"git.ophivana.moe/cat/fortify/internal/verbose" "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)) args = make([]string, 0, 9+len(a.seal.sys.bwrap.SetEnv))
// shell --uid=$USER // shell --uid=$USER
args = append(args, "shell", "--uid="+a.seal.sys.Username) args = append(args, "shell", "--uid="+a.seal.sys.user.Username)
// --quiet // --quiet
if !verbose.Get() { if !verbose.Get() {
@ -49,14 +48,6 @@ func (a *app) commandBuilderMachineCtl(shimEnv string) (args []string) {
} }
innerCommand.WriteString("; ") 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 // launch fortify as shim
innerCommand.WriteString("exec " + a.seal.sys.executable + " shim") innerCommand.WriteString("exec " + a.seal.sys.executable + " shim")

View File

@ -14,7 +14,7 @@ func (a *app) commandBuilderSudo(shimEnv string) (args []string) {
args = make([]string, 0, 8) args = make([]string, 0, 8)
// -Hiu $USER // -Hiu $USER
args = append(args, "-Hiu", a.seal.sys.Username) args = append(args, "-Hiu", a.seal.sys.user.Username)
// -A? // -A?
if _, ok := os.LookupEnv(sudoAskPass); ok { if _, ok := os.LookupEnv(sudoAskPass); ok {

View File

@ -10,7 +10,9 @@ import (
"git.ophivana.moe/cat/fortify/dbus" "git.ophivana.moe/cat/fortify/dbus"
"git.ophivana.moe/cat/fortify/internal" "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/state"
"git.ophivana.moe/cat/fortify/internal/system"
"git.ophivana.moe/cat/fortify/internal/verbose" "git.ophivana.moe/cat/fortify/internal/verbose"
) )
@ -29,12 +31,6 @@ var (
ErrMachineCtl = errors.New("machinectl not available") ErrMachineCtl = errors.New("machinectl not available")
) )
type (
SealConfigError BaseError
LauncherLookupError BaseError
SecurityError BaseError
)
// Seal seals the app launch context // Seal seals the app launch context
func (a *app) Seal(config *Config) error { func (a *app) Seal(config *Config) error {
a.lock.Lock() a.lock.Lock()
@ -45,7 +41,8 @@ func (a *app) Seal(config *Config) error {
} }
if config == nil { 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 // create seal
@ -53,7 +50,8 @@ func (a *app) Seal(config *Config) error {
// generate application ID // generate application ID
if id, err := newAppID(); err != nil { if id, err := newAppID(); err != nil {
return (*SecurityError)(wrapError(err, "cannot generate application ID:", err)) return fmsg.WrapErrorSuffix(err,
"cannot generate application ID:")
} else { } else {
seal.id = id seal.id = id
} }
@ -70,32 +68,35 @@ func (a *app) Seal(config *Config) error {
case "sudo": case "sudo":
seal.launchOption = LaunchMethodSudo seal.launchOption = LaunchMethodSudo
if sudoPath, err := exec.LookPath("sudo"); err != nil { if sudoPath, err := exec.LookPath("sudo"); err != nil {
return (*LauncherLookupError)(wrapError(ErrSudo, "sudo not found")) return fmsg.WrapError(ErrSudo,
"sudo not found")
} else { } else {
seal.toolPath = sudoPath seal.toolPath = sudoPath
} }
case "systemd": case "systemd":
seal.launchOption = LaunchMethodMachineCtl seal.launchOption = LaunchMethodMachineCtl
if !internal.SdBootedV { if !internal.SdBootedV {
return (*LauncherLookupError)(wrapError(ErrSystemd, return fmsg.WrapError(ErrSystemd,
"system has not been booted with systemd as init system")) "system has not been booted with systemd as init system")
} }
if machineCtlPath, err := exec.LookPath("machinectl"); err != nil { if machineCtlPath, err := exec.LookPath("machinectl"); err != nil {
return (*LauncherLookupError)(wrapError(ErrMachineCtl, "machinectl not found")) return fmsg.WrapError(ErrMachineCtl,
"machinectl not found")
} else { } else {
seal.toolPath = machineCtlPath seal.toolPath = machineCtlPath
} }
default: default:
return (*SealConfigError)(wrapError(ErrLaunch, "invalid launch method")) return fmsg.WrapError(ErrLaunch,
"invalid launch method")
} }
// create seal system component // create seal system component
seal.sys = new(appSealTx) seal.sys = new(appSealSys)
// look up fortify executable path // look up fortify executable path
if p, err := os.Executable(); err != nil { 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 { } else {
seal.sys.executable = p seal.sys.executable = p
} }
@ -103,13 +104,13 @@ func (a *app) Seal(config *Config) error {
// look up user from system // look up user from system
if u, err := user.Lookup(config.User); err != nil { if u, err := user.Lookup(config.User); err != nil {
if errors.As(err, new(user.UnknownUserError)) { if errors.As(err, new(user.UnknownUserError)) {
return (*SealConfigError)(wrapError(ErrUser, "unknown user", config.User)) return fmsg.WrapError(ErrUser, "unknown user", config.User)
} else { } else {
// unreachable // unreachable
panic(err) panic(err)
} }
} else { } else {
seal.sys.User = u seal.sys.user = u
seal.sys.runtime = path.Join("/run/user", u.Uid) 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 // hide nscd from sandbox if present
nscd := "/var/run/nscd" nscd := "/var/run/nscd"
if _, err := os.Stat(nscd); !errors.Is(err, os.ErrNotExist) { 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 // bind GPU stuff
if config.Confinement.Enablements.Has(state.EnableX) || config.Confinement.Enablements.Has(state.EnableWayland) { 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 config.Confinement.Sandbox = conf
} }
seal.sys.bwrap = config.Confinement.Sandbox.Bwrap() 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 { if seal.sys.bwrap.SetEnv == nil {
seal.sys.bwrap.SetEnv = make(map[string]string) seal.sys.bwrap.SetEnv = make(map[string]string)
} }
@ -186,14 +187,14 @@ func (a *app) Seal(config *Config) error {
// open process state store // open process state store
// the simple store only starts holding an open file after first action // the simple store only starts holding an open file after first action
// store activity begins after Start is called and must end before Wait // 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 // 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 // unreachable unless kernel bug
panic("uid parse") panic("uid parse")
} else { } else {
seal.sys.uid = u seal.sys.I = system.New(u)
} }
// pass through enablements // pass through enablements
@ -206,7 +207,7 @@ func (a *app) Seal(config *Config) error {
// verbose log seal information // verbose log seal information
verbose.Println("created application seal as user", verbose.Println("created application seal as user",
seal.sys.Username, "("+seal.sys.Uid+"),", seal.sys.user.Username, "("+seal.sys.user.Uid+"),",
"method:", config.Method+",", "method:", config.Method+",",
"launcher:", seal.toolPath+",", "launcher:", seal.toolPath+",",
"command:", config.Command) "command:", config.Command)

View File

@ -1,15 +1,11 @@
package app package app
import ( import (
"errors"
"fmt"
"os"
"path" "path"
"git.ophivana.moe/cat/fortify/acl" "git.ophivana.moe/cat/fortify/acl"
"git.ophivana.moe/cat/fortify/dbus" "git.ophivana.moe/cat/fortify/dbus"
"git.ophivana.moe/cat/fortify/internal/state" "git.ophivana.moe/cat/fortify/internal/state"
"git.ophivana.moe/cat/fortify/internal/verbose"
) )
const ( const (
@ -17,122 +13,30 @@ const (
dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS" 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 { func (seal *appSeal) shareDBus(config [2]*dbus.Config) error {
if !seal.et.Has(state.EnableDBus) { if !seal.et.Has(state.EnableDBus) {
return nil 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 // downstream socket paths
sessionBus[1] = path.Join(seal.share, "bus") sessionPath, systemPath := path.Join(seal.share, "bus"), path.Join(seal.share, "system_bus_socket")
systemBus[1] = path.Join(seal.share, "system_bus_socket")
// resolve upstream bus addresses // configure dbus proxy
sessionBus[0], systemBus[0] = dbus.Address() if err := seal.sys.ProxyDBus(config[0], config[1], sessionPath, systemPath); err != nil {
return err
// 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))
} }
// store addresses for cleanup and logging
seal.sys.dbusAddr = &[2][2]string{sessionBus, systemBus}
// share proxy sockets // share proxy sockets
sessionInner := path.Join(seal.sys.runtime, "bus") sessionInner := path.Join(seal.sys.runtime, "bus")
seal.sys.setEnv(dbusSessionBusAddress, "unix:path="+sessionInner) seal.sys.bwrap.SetEnv[dbusSessionBusAddress] = "unix:path=" + sessionInner
seal.sys.bwrap.Bind(sessionBus[1], sessionInner) seal.sys.bwrap.Bind(sessionPath, sessionInner)
seal.sys.updatePerm(sessionBus[1], acl.Read, acl.Write) seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
if seal.sys.dbusSystem { if config[1] != nil {
systemInner := "/run/dbus/system_bus_socket" systemInner := "/run/dbus/system_bus_socket"
seal.sys.setEnv(dbusSystemBusAddress, "unix:path="+systemInner) seal.sys.bwrap.SetEnv[dbusSystemBusAddress] = "unix:path=" + systemInner
seal.sys.bwrap.Bind(systemBus[1], systemInner) seal.sys.bwrap.Bind(systemPath, systemInner)
seal.sys.updatePerm(systemBus[1], acl.Read, acl.Write) seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write)
} }
return nil 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
}

View File

@ -6,6 +6,7 @@ import (
"path" "path"
"git.ophivana.moe/cat/fortify/acl" "git.ophivana.moe/cat/fortify/acl"
"git.ophivana.moe/cat/fortify/internal/fmsg"
"git.ophivana.moe/cat/fortify/internal/state" "git.ophivana.moe/cat/fortify/internal/state"
) )
@ -22,29 +23,28 @@ var (
ErrXDisplay = errors.New(display + " unset") ErrXDisplay = errors.New(display + " unset")
) )
type ErrDisplayEnv BaseError
func (seal *appSeal) shareDisplay() error { func (seal *appSeal) shareDisplay() error {
// pass $TERM to launcher // pass $TERM to launcher
if t, ok := os.LookupEnv(term); ok { if t, ok := os.LookupEnv(term); ok {
seal.sys.setEnv(term, t) seal.sys.bwrap.SetEnv[term] = t
} }
// set up wayland // set up wayland
if seal.et.Has(state.EnableWayland) { if seal.et.Has(state.EnableWayland) {
if wd, ok := os.LookupEnv(waylandDisplay); !ok { 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 { } else if seal.wlDone == nil {
// hardlink wayland socket // hardlink wayland socket
wp := path.Join(seal.RuntimePath, wd) wp := path.Join(seal.RuntimePath, wd)
wpi := path.Join(seal.shareLocal, "wayland") wpi := path.Join(seal.shareLocal, "wayland")
w := path.Join(seal.sys.runtime, "wayland-0") w := path.Join(seal.sys.runtime, "wayland-0")
seal.sys.link(wp, wpi) seal.sys.Link(wp, wpi)
seal.sys.setEnv(waylandDisplay, w) seal.sys.bwrap.SetEnv[waylandDisplay] = w
seal.sys.bwrap.Bind(wpi, w) seal.sys.bwrap.Bind(wpi, w)
// ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`) // 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 { } else {
// set wayland socket path (e.g. `/run/user/%d/wayland-%d`) // set wayland socket path (e.g. `/run/user/%d/wayland-%d`)
seal.wl = path.Join(seal.RuntimePath, wd) seal.wl = path.Join(seal.RuntimePath, wd)
@ -55,10 +55,11 @@ func (seal *appSeal) shareDisplay() error {
if seal.et.Has(state.EnableX) { if seal.et.Has(state.EnableX) {
// discover X11 and grant user permission via the `ChangeHosts` command // discover X11 and grant user permission via the `ChangeHosts` command
if d, ok := os.LookupEnv(display); !ok { if d, ok := os.LookupEnv(display); !ok {
return (*ErrDisplayEnv)(wrapError(ErrXDisplay, "DISPLAY is not set")) return fmsg.WrapError(ErrXDisplay,
"DISPLAY is not set")
} else { } else {
seal.sys.changeHosts(seal.sys.Username) seal.sys.ChangeHosts(seal.sys.user.Username)
seal.sys.setEnv(display, d) seal.sys.bwrap.SetEnv[display] = d
seal.sys.bwrap.Bind("/tmp/.X11-unix", "/tmp/.X11-unix") seal.sys.bwrap.Bind("/tmp/.X11-unix", "/tmp/.X11-unix")
} }
} }

View File

@ -7,6 +7,7 @@ import (
"os" "os"
"path" "path"
"git.ophivana.moe/cat/fortify/internal/fmsg"
"git.ophivana.moe/cat/fortify/internal/state" "git.ophivana.moe/cat/fortify/internal/state"
) )
@ -24,11 +25,6 @@ var (
ErrPulseMode = errors.New("unexpected pulse socket mode") ErrPulseMode = errors.New("unexpected pulse socket mode")
) )
type (
PulseCookieAccessError BaseError
PulseSocketAccessError BaseError
)
func (seal *appSeal) sharePulse() error { func (seal *appSeal) sharePulse() error {
if !seal.et.Has(state.EnablePulse) { if !seal.et.Has(state.EnablePulse) {
return nil return nil
@ -39,42 +35,43 @@ func (seal *appSeal) sharePulse() error {
ps := path.Join(pd, "native") ps := path.Join(pd, "native")
if _, err := os.Stat(pd); err != nil { if _, err := os.Stat(pd); err != nil {
if !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
return (*PulseSocketAccessError)(wrapError(err, return fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot access PulseAudio directory '%s':", pd), err)) fmt.Sprintf("cannot access PulseAudio directory %q:", pd))
} }
return (*PulseSocketAccessError)(wrapError(ErrPulseSocket, return fmsg.WrapError(ErrPulseSocket,
fmt.Sprintf("PulseAudio directory '%s' not found", pd))) fmt.Sprintf("PulseAudio directory %q not found", pd))
} }
// check PulseAudio socket permission (e.g. `/run/user/%d/pulse/native`) // check PulseAudio socket permission (e.g. `/run/user/%d/pulse/native`)
if s, err := os.Stat(ps); err != nil { if s, err := os.Stat(ps); err != nil {
if !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
return (*PulseSocketAccessError)(wrapError(err, return fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot access PulseAudio socket '%s':", ps), err)) fmt.Sprintf("cannot access PulseAudio socket %q:", ps))
} }
return (*PulseSocketAccessError)(wrapError(ErrPulseSocket, return fmsg.WrapError(ErrPulseSocket,
fmt.Sprintf("PulseAudio directory '%s' found but socket does not exist", pd))) fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pd))
} else { } else {
if m := s.Mode(); m&0o006 != 0o006 { if m := s.Mode(); m&0o006 != 0o006 {
return (*PulseSocketAccessError)(wrapError(ErrPulseMode, return fmsg.WrapError(ErrPulseMode,
fmt.Sprintf("unexpected permissions on '%s':", ps), m)) fmt.Sprintf("unexpected permissions on %q:", ps), m)
} }
} }
// hard link pulse socket into target-executable share // hard link pulse socket into target-executable share
psi := path.Join(seal.shareLocal, "pulse") psi := path.Join(seal.shareLocal, "pulse")
p := path.Join(seal.sys.runtime, "pulse", "native") 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.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 // publish current user's pulse cookie for target user
if src, err := discoverPulseCookie(); err != nil { if src, err := discoverPulseCookie(); err != nil {
return err return err
} else { } else {
dst := path.Join(seal.share, "pulse-cookie") dst := path.Join(seal.share, "pulse-cookie")
seal.sys.setEnv(pulseCookie, dst) seal.sys.bwrap.SetEnv[pulseCookie] = dst
seal.sys.copyFile(dst, src) seal.sys.CopyFile(dst, src)
seal.sys.bwrap.Bind(dst, dst)
} }
return nil return nil
@ -91,8 +88,8 @@ func discoverPulseCookie() (string, error) {
p = path.Join(p, ".pulse-cookie") p = path.Join(p, ".pulse-cookie")
if s, err := os.Stat(p); err != nil { if s, err := os.Stat(p); err != nil {
if !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
return p, (*PulseCookieAccessError)(wrapError(err, return p, fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot access PulseAudio cookie '%s':", p), err)) fmt.Sprintf("cannot access PulseAudio cookie %q:", p))
} }
// not found, try next method // not found, try next method
} else if !s.IsDir() { } else if !s.IsDir() {
@ -105,7 +102,8 @@ func discoverPulseCookie() (string, error) {
p = path.Join(p, "pulse", "cookie") p = path.Join(p, "pulse", "cookie")
if s, err := os.Stat(p); err != nil { if s, err := os.Stat(p); err != nil {
if !errors.Is(err, fs.ErrNotExist) { 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 // not found, try next method
} else if !s.IsDir() { } 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)", fmt.Sprintf("cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)",
pulseCookie, xdgConfigHome, home))) pulseCookie, xdgConfigHome, home))
} }

View File

@ -4,7 +4,7 @@ import (
"path" "path"
"git.ophivana.moe/cat/fortify/acl" "git.ophivana.moe/cat/fortify/acl"
"git.ophivana.moe/cat/fortify/internal/state" "git.ophivana.moe/cat/fortify/internal/system"
) )
const ( const (
@ -20,28 +20,28 @@ func (seal *appSeal) shareRuntime() {
seal.sys.bwrap.Tmpfs(seal.sys.runtime, 8*1024*1024) seal.sys.bwrap.Tmpfs(seal.sys.runtime, 8*1024*1024)
// point to inner runtime path `/run/user/%d` // point to inner runtime path `/run/user/%d`
seal.sys.setEnv(xdgRuntimeDir, seal.sys.runtime) seal.sys.bwrap.SetEnv[xdgRuntimeDir] = seal.sys.runtime
seal.sys.setEnv(xdgSessionClass, "user") seal.sys.bwrap.SetEnv[xdgSessionClass] = "user"
seal.sys.setEnv(xdgSessionType, "tty") seal.sys.bwrap.SetEnv[xdgSessionType] = "tty"
// ensure RunDir (e.g. `/run/user/%d/fortify`) // ensure RunDir (e.g. `/run/user/%d/fortify`)
seal.sys.ensure(seal.RunDirPath, 0700) seal.sys.Ensure(seal.RunDirPath, 0700)
seal.sys.updatePermTag(state.EnableLength, seal.RunDirPath, acl.Execute) seal.sys.UpdatePermType(system.User, seal.RunDirPath, acl.Execute)
// ensure runtime directory ACL (e.g. `/run/user/%d`) // 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`) // ensure Share (e.g. `/tmp/fortify.%d`)
// acl is unnecessary as this directory is world executable // 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`) // ensure process-specific share (e.g. `/tmp/fortify.%d/%s`)
// acl is unnecessary as this directory is world executable // acl is unnecessary as this directory is world executable
seal.share = path.Join(seal.SharePath, seal.id.String()) 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`) // 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.shareLocal = path.Join(seal.RunDirPath, seal.id.String())
seal.sys.ensureEphemeral(seal.shareLocal, 0700) seal.sys.Ephemeral(system.Process, seal.shareLocal, 0700)
seal.sys.updatePerm(seal.shareLocal, acl.Execute) seal.sys.UpdatePerm(seal.shareLocal, acl.Execute)
} }

View File

@ -5,7 +5,7 @@ import (
"path" "path"
"git.ophivana.moe/cat/fortify/acl" "git.ophivana.moe/cat/fortify/acl"
"git.ophivana.moe/cat/fortify/internal/state" "git.ophivana.moe/cat/fortify/internal/system"
) )
const ( const (
@ -17,28 +17,28 @@ func (seal *appSeal) shareSystem() {
// look up shell // look up shell
sh := "/bin/sh" sh := "/bin/sh"
if s, ok := os.LookupEnv(shell); ok { if s, ok := os.LookupEnv(shell); ok {
seal.sys.setEnv(shell, s) seal.sys.bwrap.SetEnv[shell] = s
sh = s sh = s
} }
// generate /etc/passwd // generate /etc/passwd
passwdPath := path.Join(seal.share, "passwd") passwdPath := path.Join(seal.share, "passwd")
username := "chronos" username := "chronos"
if seal.sys.Username != "" { if seal.sys.user.Username != "" {
username = seal.sys.Username username = seal.sys.user.Username
seal.sys.setEnv("USER", seal.sys.Username) seal.sys.bwrap.SetEnv["USER"] = seal.sys.user.Username
} }
homeDir := "/var/empty" homeDir := "/var/empty"
if seal.sys.HomeDir != "" { if seal.sys.user.HomeDir != "" {
homeDir = seal.sys.HomeDir homeDir = seal.sys.user.HomeDir
seal.sys.setEnv("HOME", seal.sys.HomeDir) seal.sys.bwrap.SetEnv["HOME"] = seal.sys.user.HomeDir
} }
passwd := username + ":x:65534:65534:Fortify:" + homeDir + ":" + sh + "\n" passwd := username + ":x:65534:65534:Fortify:" + homeDir + ":" + sh + "\n"
seal.sys.writeFile(passwdPath, []byte(passwd)) seal.sys.Write(passwdPath, passwd)
// write /etc/group // write /etc/group
groupPath := path.Join(seal.share, "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 // bind /etc/passwd and /etc/group
seal.sys.bwrap.Bind(passwdPath, "/etc/passwd") seal.sys.bwrap.Bind(passwdPath, "/etc/passwd")
@ -48,13 +48,13 @@ func (seal *appSeal) shareSystem() {
func (seal *appSeal) shareTmpdirChild() string { func (seal *appSeal) shareTmpdirChild() string {
// ensure child tmpdir parent directory (e.g. `/tmp/fortify.%d/tmpdir`) // ensure child tmpdir parent directory (e.g. `/tmp/fortify.%d/tmpdir`)
targetTmpdirParent := path.Join(seal.SharePath, "tmpdir") targetTmpdirParent := path.Join(seal.SharePath, "tmpdir")
seal.sys.ensure(targetTmpdirParent, 0700) seal.sys.Ensure(targetTmpdirParent, 0700)
seal.sys.updatePermTag(state.EnableLength, targetTmpdirParent, acl.Execute) seal.sys.UpdatePermType(system.User, targetTmpdirParent, acl.Execute)
// ensure child tmpdir (e.g. `/tmp/fortify.%d/tmpdir/%d`) // ensure child tmpdir (e.g. `/tmp/fortify.%d/tmpdir/%d`)
targetTmpdir := path.Join(targetTmpdirParent, seal.sys.Uid) targetTmpdir := path.Join(targetTmpdirParent, seal.sys.user.Uid)
seal.sys.ensure(targetTmpdir, 01700) seal.sys.Ensure(targetTmpdir, 01700)
seal.sys.updatePermTag(state.EnableLength, targetTmpdir, acl.Read, acl.Write, acl.Execute) seal.sys.UpdatePermType(system.User, targetTmpdir, acl.Read, acl.Write, acl.Execute)
seal.sys.bwrap.Bind(targetTmpdir, "/tmp", false, true) seal.sys.bwrap.Bind(targetTmpdir, "/tmp", false, true)
// mount tmpfs on inner shared directory (e.g. `/tmp/fortify.%d`) // mount tmpfs on inner shared directory (e.g. `/tmp/fortify.%d`)

View File

@ -8,21 +8,17 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings"
"time" "time"
"git.ophivana.moe/cat/fortify/helper" "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/shim"
"git.ophivana.moe/cat/fortify/internal/state" "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/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 // Start starts the fortified child
func (a *app) Start() error { func (a *app) Start() error {
a.lock.Lock() a.lock.Lock()
@ -41,12 +37,13 @@ func (a *app) Start() error {
if s, err := exec.LookPath(n); err == nil { if s, err := exec.LookPath(n); err == nil {
shimExec[i] = s shimExec[i] = s
} else { } 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 return err
} }
@ -70,7 +67,7 @@ func (a *app) Start() error {
a.cmd.Stderr = os.Stderr a.cmd.Stderr = os.Stderr
a.cmd.Dir = a.seal.RunDirPath 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, Argv: a.seal.command,
Exec: shimExec, Exec: shimExec,
Bwrap: a.seal.sys.bwrap, Bwrap: a.seal.sys.bwrap,
@ -78,7 +75,8 @@ func (a *app) Start() error {
Verbose: verbose.Get(), Verbose: verbose.Get(),
}, a.seal.wl, a.seal.wlDone); err != nil { }, 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 { } else {
a.wayland = wls a.wayland = wls
} }
@ -86,7 +84,8 @@ func (a *app) Start() error {
// start shim // start shim
verbose.Println("starting shim as target user:", a.cmd) verbose.Println("starting shim as target user:", a.cmd)
if err := a.cmd.Start(); err != nil { 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() 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.Inner, err.DoErr = a.seal.store.Do(func(b state.Backend) {
err.InnerErr = b.Save(&sd) 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 // 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 { if e.Inner == true && e.DoErr == nil && e.InnerErr == nil && e.Err == nil {
return nil return nil
} else { } else {
return wrapError(e, a...) return fmsg.WrapErrorSuffix(e, a...)
} }
} }
@ -203,15 +202,16 @@ func (a *app) Wait() (int, error) {
} }
// enablements of remaining launchers // enablements of remaining launchers
rt, tags := new(state.Enablements), new(state.Enablements) rt, ec := new(state.Enablements), new(system.Criteria)
tags.Set(state.EnableLength + 1) ec.Enablements = new(state.Enablements)
ec.Set(system.Process)
if states, err := b.Load(); err != nil { if states, err := b.Load(); err != nil {
return err return err
} else { } else {
if l := len(states); l == 0 { if l := len(states); l == 0 {
// cleanup globals as the final launcher // cleanup globals as the final launcher
verbose.Println("no other launchers active, will clean up globals") verbose.Println("no other launchers active, will clean up globals")
tags.Set(state.EnableLength) ec.Set(system.User)
} else { } else {
verbose.Printf("found %d active launchers, cleaning up without globals\n", l) 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 // invert accumulated enablements for cleanup
for i := state.Enablement(0); i < state.EnableLength; i++ { for i := state.Enablement(0); i < state.EnableLength; i++ {
if !rt.Has(i) { if !rt.Has(i) {
tags.Set(i) ec.Set(i)
} }
} }
if verbose.Get() { if verbose.Get() {
ct := make([]state.Enablement, 0, state.EnableLength) labels := make([]string, 0, state.EnableLength+1)
for i := state.Enablement(0); i < state.EnableLength; i++ { for i := state.Enablement(0); i < state.EnableLength+2; i++ {
if tags.Has(i) { if ec.Has(i) {
ct = append(ct, i) labels = append(labels, system.TypeString(i))
} }
} }
if len(ct) > 0 { if len(labels) > 0 {
verbose.Println("will revert operations tagged", ct, "as no remaining launchers hold these enablements") 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) return err.(RevertCompoundError)
} }

View File

@ -1,19 +1,14 @@
package app package app
import ( import (
"errors"
"fmt"
"io/fs"
"os"
"os/user" "os/user"
"git.ophivana.moe/cat/fortify/acl"
"git.ophivana.moe/cat/fortify/dbus" "git.ophivana.moe/cat/fortify/dbus"
"git.ophivana.moe/cat/fortify/helper/bwrap" "git.ophivana.moe/cat/fortify/helper/bwrap"
"git.ophivana.moe/cat/fortify/internal" "git.ophivana.moe/cat/fortify/internal"
"git.ophivana.moe/cat/fortify/internal/state" "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/internal/verbose"
"git.ophivana.moe/cat/fortify/xcb"
) )
// appSeal seals the application with child-related information // appSeal seals the application with child-related information
@ -48,7 +43,7 @@ type appSeal struct {
// prevents sharing from happening twice // prevents sharing from happening twice
shared bool shared bool
// seal system-level component // seal system-level component
sys *appSealTx sys *appSealSys
// used in various sealing operations // used in various sealing operations
internal.SystemConstants internal.SystemConstants
@ -56,357 +51,24 @@ type appSeal struct {
// protected by upstream mutex // protected by upstream mutex
} }
// appSealTx contains the system-level component of the app seal // appSealSys encapsulates app seal behaviour with OS interactions
type appSealTx struct { type appSealSys struct {
bwrap *bwrap.Config bwrap *bwrap.Config
tmpfs []string // paths to override by mounting tmpfs over them
override []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
// default formatted XDG_RUNTIME_DIR of User // default formatted XDG_RUNTIME_DIR of User
runtime string runtime string
// sealed path to fortify executable, used by shim // sealed path to fortify executable, used by shim
executable string executable string
// target user UID as an integer
uid int
// target user sealed from config // target user sealed from config
*user.User user *user.User
// prevents commit from happening twice *system.I
complete bool
// prevents cleanup from happening twice
closed bool
// protected by upstream mutex // 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 // shareAll calls all share methods in sequence
func (seal *appSeal) shareAll(bus [2]*dbus.Config) error { func (seal *appSeal) shareAll(bus [2]*dbus.Config) error {
if seal.shared { if seal.shared {
@ -432,12 +94,11 @@ func (seal *appSeal) shareAll(bus [2]*dbus.Config) error {
if err := seal.shareDBus(bus); err != nil { if err := seal.shareDBus(bus); err != nil {
return err 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 return nil