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:
parent
430f1a5b4e
commit
084cd84f36
11
error.go
11
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)
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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`)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue