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"
"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)

View File

@ -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)"

View File

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

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"
"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")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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`)

View File

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

View File

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