aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/app
diff options
context:
space:
mode:
Diffstat (limited to 'internal/app')
-rw-r--r--internal/app/app.go52
-rw-r--r--internal/app/builder.go13
-rw-r--r--internal/app/config.go34
-rw-r--r--internal/app/copy.go33
-rw-r--r--internal/app/dbus.go123
-rw-r--r--internal/app/ensure.go63
-rw-r--r--internal/app/error.go51
-rw-r--r--internal/app/id.go18
-rw-r--r--internal/app/launch.bwrap.go8
-rw-r--r--internal/app/launch.go73
-rw-r--r--internal/app/launch.machinectl.go67
-rw-r--r--internal/app/launch.sudo.go33
-rw-r--r--internal/app/pulse.go109
-rw-r--r--internal/app/run.go163
-rw-r--r--internal/app/seal.go154
-rw-r--r--internal/app/setup.go150
-rw-r--r--internal/app/share.dbus.go152
-rw-r--r--internal/app/share.display.go59
-rw-r--r--internal/app/share.pulse.go116
-rw-r--r--internal/app/share.runtime.go50
-rw-r--r--internal/app/shim.go83
-rw-r--r--internal/app/start.go187
-rw-r--r--internal/app/system.go351
-rw-r--r--internal/app/wayland.go35
-rw-r--r--internal/app/x.go31
25 files changed, 1448 insertions, 760 deletions
diff --git a/internal/app/app.go b/internal/app/app.go
new file mode 100644
index 00000000..9d1165f4
--- /dev/null
+++ b/internal/app/app.go
@@ -0,0 +1,52 @@
+package app
+
+import (
+ "os/exec"
+ "sync"
+)
+
+type App interface {
+ Seal(config *Config) error
+ Start() error
+ Wait() (int, error)
+ WaitErr() error
+ String() string
+}
+
+type app struct {
+ // child process related information
+ seal *appSeal
+ // underlying fortified child process
+ cmd *exec.Cmd
+ // error returned waiting for process
+ wait error
+
+ lock sync.RWMutex
+}
+
+func (a *app) String() string {
+ if a == nil {
+ return "(invalid fortified app)"
+ }
+
+ a.lock.RLock()
+ defer a.lock.RUnlock()
+
+ if a.cmd != nil {
+ return a.cmd.String()
+ }
+
+ if a.seal != nil {
+ return "(sealed fortified app as uid " + a.seal.sys.Uid + ")"
+ }
+
+ return "(unsealed fortified app)"
+}
+
+func (a *app) WaitErr() error {
+ return a.wait
+}
+
+func New() App {
+ return new(app)
+}
diff --git a/internal/app/builder.go b/internal/app/builder.go
deleted file mode 100644
index 2bce43b4..00000000
--- a/internal/app/builder.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package app
-
-func (a *App) Command() []string {
- return a.command
-}
-
-func (a *App) UID() int {
- return a.uid
-}
-
-func (a *App) AppendEnv(k, v string) {
- a.env = append(a.env, k+"="+v)
-}
diff --git a/internal/app/config.go b/internal/app/config.go
new file mode 100644
index 00000000..d9648740
--- /dev/null
+++ b/internal/app/config.go
@@ -0,0 +1,34 @@
+package app
+
+import (
+ "git.ophivana.moe/cat/fortify/dbus"
+ "git.ophivana.moe/cat/fortify/internal/state"
+)
+
+// Config is used to seal an *App
+type Config struct {
+ // D-Bus application ID
+ ID string `json:"id"`
+ // username of the target user to switch to
+ User string `json:"user"`
+ // value passed through to the child process as its argv
+ Command []string `json:"command"`
+ // string representation of the child's launch method
+ Method string `json:"method"`
+
+ // child confinement configuration
+ Confinement ConfinementConfig `json:"confinement"`
+}
+
+// ConfinementConfig defines fortified child's confinement
+type ConfinementConfig struct {
+ // reference to a system D-Bus proxy configuration,
+ // nil value disables system bus proxy
+ SystemBus *dbus.Config `json:"system_bus,omitempty"`
+ // reference to a session D-Bus proxy configuration,
+ // nil value makes session bus proxy assume built-in defaults
+ SessionBus *dbus.Config `json:"session_bus,omitempty"`
+
+ // child capability enablements
+ Enablements state.Enablements `json:"enablements"`
+}
diff --git a/internal/app/copy.go b/internal/app/copy.go
new file mode 100644
index 00000000..bf90bfea
--- /dev/null
+++ b/internal/app/copy.go
@@ -0,0 +1,33 @@
+package app
+
+import (
+ "io"
+ "os"
+)
+
+func copyFile(dst, src string) error {
+ srcD, err := os.Open(src)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if srcD.Close() != nil {
+ // unreachable
+ panic("src file closed prematurely")
+ }
+ }()
+
+ dstD, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if dstD.Close() != nil {
+ // unreachable
+ panic("dst file closed prematurely")
+ }
+ }()
+
+ _, err = io.Copy(dstD, srcD)
+ return err
+}
diff --git a/internal/app/dbus.go b/internal/app/dbus.go
deleted file mode 100644
index fe8039f6..00000000
--- a/internal/app/dbus.go
+++ /dev/null
@@ -1,123 +0,0 @@
-package app
-
-import (
- "errors"
- "fmt"
- "os"
- "path"
- "strconv"
-
- "git.ophivana.moe/cat/fortify/acl"
- "git.ophivana.moe/cat/fortify/dbus"
- "git.ophivana.moe/cat/fortify/internal"
- "git.ophivana.moe/cat/fortify/internal/util"
- "git.ophivana.moe/cat/fortify/internal/verbose"
-)
-
-const (
- dbusSessionBusAddress = "DBUS_SESSION_BUS_ADDRESS"
- dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS"
-)
-
-var (
- dbusAddress [2]string
- dbusSystem bool
-)
-
-func (a *App) ShareDBus(dse, dsg *dbus.Config, log bool) {
- a.setEnablement(internal.EnableDBus)
-
- dbusSystem = dsg != nil
- var binPath string
- var sessionBus, systemBus [2]string
-
- target := path.Join(a.sharePath, strconv.Itoa(os.Getpid()))
- sessionBus[1] = target + ".bus"
- systemBus[1] = target + ".system-bus"
- dbusAddress = [2]string{
- "unix:path=" + sessionBus[1],
- "unix:path=" + systemBus[1],
- }
-
- if b, ok := util.Which("xdg-dbus-proxy"); !ok {
- internal.Fatal("D-Bus: Did not find 'xdg-dbus-proxy' in PATH")
- } else {
- binPath = b
- }
-
- if addr, ok := os.LookupEnv(dbusSessionBusAddress); !ok {
- verbose.Println("D-Bus: DBUS_SESSION_BUS_ADDRESS not set, assuming default format")
- sessionBus[0] = fmt.Sprintf("unix:path=/run/user/%d/bus", os.Getuid())
- } else {
- sessionBus[0] = addr
- }
-
- if addr, ok := os.LookupEnv(dbusSystemBusAddress); !ok {
- verbose.Println("D-Bus: DBUS_SYSTEM_BUS_ADDRESS not set, assuming default format")
- systemBus[0] = "unix:path=/run/dbus/system_bus_socket"
- } else {
- systemBus[0] = addr
- }
-
- p := dbus.New(binPath, sessionBus, systemBus)
-
- dse.Log = log
- verbose.Println("D-Bus: sealing session proxy", dse.Args(sessionBus))
- if dsg != nil {
- dsg.Log = log
- verbose.Println("D-Bus: sealing system proxy", dsg.Args(systemBus))
- }
- if err := p.Seal(dse, dsg); err != nil {
- internal.Fatal("D-Bus: invalid config when sealing proxy,", err)
- }
-
- ready := make(chan bool, 1)
- done := make(chan struct{})
-
- verbose.Printf("Starting session bus proxy '%s' for address '%s'\n", dbusAddress[0], sessionBus[0])
- if dsg != nil {
- verbose.Printf("Starting system bus proxy '%s' for address '%s'\n", dbusAddress[1], systemBus[0])
- }
- if err := p.Start(&ready); err != nil {
- internal.Fatal("D-Bus: error starting proxy,", err)
- }
- verbose.Println("D-Bus proxy launch:", p)
-
- go func() {
- if err := p.Wait(); err != nil {
- fmt.Println("warn: D-Bus proxy returned error,", err)
- } else {
- verbose.Println("D-Bus proxy uneventful wait")
- }
- if err := os.Remove(target); err != nil && !errors.Is(err, os.ErrNotExist) {
- fmt.Println("Error removing dangling D-Bus socket:", err)
- }
- done <- struct{}{}
- }()
-
- // register early to enable Fatal cleanup
- a.exit.SealDBus(p, &done)
-
- if !<-ready {
- internal.Fatal("D-Bus: proxy did not start correctly")
- }
-
- a.AppendEnv(dbusSessionBusAddress, dbusAddress[0])
- if err := acl.UpdatePerm(sessionBus[1], a.UID(), acl.Read, acl.Write); err != nil {
- internal.Fatal(fmt.Sprintf("Error preparing D-Bus session proxy '%s':", dbusAddress[0]), err)
- } else {
- a.exit.RegisterRevertPath(sessionBus[1])
- }
- if dsg != nil {
- a.AppendEnv(dbusSystemBusAddress, dbusAddress[1])
- if err := acl.UpdatePerm(systemBus[1], a.UID(), acl.Read, acl.Write); err != nil {
- internal.Fatal(fmt.Sprintf("Error preparing D-Bus system proxy '%s':", dbusAddress[1]), err)
- } else {
- a.exit.RegisterRevertPath(systemBus[1])
- }
- }
- verbose.Printf("Session bus proxy '%s' for address '%s' configured\n", dbusAddress[0], sessionBus[0])
- if dsg != nil {
- verbose.Printf("System bus proxy '%s' for address '%s' configured\n", dbusAddress[1], systemBus[0])
- }
-}
diff --git a/internal/app/ensure.go b/internal/app/ensure.go
deleted file mode 100644
index ed283a94..00000000
--- a/internal/app/ensure.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package app
-
-import (
- "errors"
- "fmt"
- "io/fs"
- "os"
- "path"
-
- "git.ophivana.moe/cat/fortify/acl"
- "git.ophivana.moe/cat/fortify/internal"
- "git.ophivana.moe/cat/fortify/internal/verbose"
-)
-
-func (a *App) EnsureRunDir() {
- if err := os.Mkdir(a.runDirPath, 0700); err != nil && !errors.Is(err, fs.ErrExist) {
- internal.Fatal("Error creating runtime directory:", err)
- }
-}
-
-func (a *App) EnsureRuntime() {
- if s, err := os.Stat(a.runtimePath); err != nil {
- if errors.Is(err, fs.ErrNotExist) {
- internal.Fatal("Runtime directory does not exist")
- }
- internal.Fatal("Error accessing runtime directory:", err)
- } else if !s.IsDir() {
- internal.Fatal(fmt.Sprintf("Path '%s' is not a directory", a.runtimePath))
- } else {
- if err = acl.UpdatePerm(a.runtimePath, a.UID(), acl.Execute); err != nil {
- internal.Fatal("Error preparing runtime directory:", err)
- } else {
- a.exit.RegisterRevertPath(a.runtimePath)
- }
- verbose.Printf("Runtime data dir '%s' configured\n", a.runtimePath)
- }
-}
-
-func (a *App) EnsureShare() {
- // acl is unnecessary as this directory is world executable
- if err := os.Mkdir(a.sharePath, 0701); err != nil && !errors.Is(err, fs.ErrExist) {
- internal.Fatal("Error creating shared directory:", err)
- }
-
- // workaround for launch method sudo
- if a.LaunchOption() == LaunchMethodSudo {
- // ensure child runtime directory (e.g. `/tmp/fortify.%d/%d.share`)
- cr := path.Join(a.sharePath, a.Uid+".share")
- if err := os.Mkdir(cr, 0700); err != nil && !errors.Is(err, fs.ErrExist) {
- internal.Fatal("Error creating child runtime directory:", err)
- } else {
- if err = acl.UpdatePerm(cr, a.UID(), acl.Read, acl.Write, acl.Execute); err != nil {
- internal.Fatal("Error preparing child runtime directory:", err)
- } else {
- a.exit.RegisterRevertPath(cr)
- }
- a.AppendEnv("XDG_RUNTIME_DIR", cr)
- a.AppendEnv("XDG_SESSION_CLASS", "user")
- a.AppendEnv("XDG_SESSION_TYPE", "tty")
- verbose.Printf("Child runtime data dir '%s' configured\n", cr)
- }
- }
-}
diff --git a/internal/app/error.go b/internal/app/error.go
new file mode 100644
index 00000000..7c18a89e
--- /dev/null
+++ b/internal/app/error.go
@@ -0,0 +1,51 @@
+package app
+
+import (
+ "fmt"
+ "reflect"
+)
+
+// baseError implements a basic error container
+type baseError struct {
+ Err error
+}
+
+func (e *baseError) Error() string {
+ return e.Err.Error()
+}
+
+func (e *baseError) Unwrap() error {
+ return e.Err
+}
+
+// BaseError implements an error container with a user-facing message
+type BaseError struct {
+ message string
+ baseError
+}
+
+// Message returns a user-facing error message
+func (e *BaseError) Message() string {
+ return e.message
+}
+
+func wrapError(err error, a ...any) *BaseError {
+ return &BaseError{
+ message: fmt.Sprintln(a...),
+ baseError: baseError{err},
+ }
+}
+
+var (
+ baseErrorType = reflect.TypeFor[*BaseError]()
+)
+
+func AsBaseError(err error, target **BaseError) bool {
+ v := reflect.ValueOf(err)
+ if !v.CanConvert(baseErrorType) {
+ return false
+ }
+
+ *target = v.Convert(baseErrorType).Interface().(*BaseError)
+ return true
+}
diff --git a/internal/app/id.go b/internal/app/id.go
new file mode 100644
index 00000000..e996a3a6
--- /dev/null
+++ b/internal/app/id.go
@@ -0,0 +1,18 @@
+package app
+
+import (
+ "crypto/rand"
+ "encoding/hex"
+)
+
+type appID [16]byte
+
+func (a *appID) String() string {
+ return hex.EncodeToString(a[:])
+}
+
+func newAppID() (*appID, error) {
+ a := &appID{}
+ _, err := rand.Read(a[:])
+ return a, err
+}
diff --git a/internal/app/launch.bwrap.go b/internal/app/launch.bwrap.go
new file mode 100644
index 00000000..f172843c
--- /dev/null
+++ b/internal/app/launch.bwrap.go
@@ -0,0 +1,8 @@
+package app
+
+// TODO: launch dbus proxy via bwrap
+
+func (a *app) commandBuilderBwrap() (args []string) {
+ // TODO: build bwrap command
+ panic("bwrap")
+}
diff --git a/internal/app/launch.go b/internal/app/launch.go
deleted file mode 100644
index b44c7785..00000000
--- a/internal/app/launch.go
+++ /dev/null
@@ -1,73 +0,0 @@
-package app
-
-import (
- "bytes"
- "encoding/base64"
- "encoding/gob"
- "fmt"
- "os"
- "strings"
- "syscall"
-
- "git.ophivana.moe/cat/fortify/internal"
- "git.ophivana.moe/cat/fortify/internal/util"
-)
-
-const launcherPayload = "FORTIFY_LAUNCHER_PAYLOAD"
-
-func (a *App) launcherPayloadEnv() string {
- r := &bytes.Buffer{}
- enc := base64.NewEncoder(base64.StdEncoding, r)
-
- if err := gob.NewEncoder(enc).Encode(a.command); err != nil {
- internal.Fatal("Error encoding launcher payload:", err)
- }
-
- _ = enc.Close()
- return launcherPayload + "=" + r.String()
-}
-
-// Early hidden launcher path
-func Early(printVersion bool) {
- if printVersion {
- if r, ok := os.LookupEnv(launcherPayload); ok {
- dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(r))
-
- var argv []string
- if err := gob.NewDecoder(dec).Decode(&argv); err != nil {
- fmt.Println("Error decoding launcher payload:", err)
- os.Exit(1)
- }
-
- if err := os.Unsetenv(launcherPayload); err != nil {
- fmt.Println("Error unsetting launcher payload:", err)
- // not fatal, do not fail
- }
-
- var p string
-
- if len(argv) > 0 {
- if p, ok = util.Which(argv[0]); !ok {
- fmt.Printf("Did not find '%s' in PATH\n", argv[0])
- os.Exit(1)
- }
- } else {
- if p, ok = os.LookupEnv("SHELL"); !ok {
- fmt.Println("No command was specified and $SHELL was unset")
- os.Exit(1)
- }
-
- argv = []string{p}
- }
-
- if err := syscall.Exec(p, argv, os.Environ()); err != nil {
- fmt.Println("Error executing launcher payload:", err)
- os.Exit(1)
- }
-
- // unreachable
- os.Exit(1)
- return
- }
- }
-}
diff --git a/internal/app/launch.machinectl.go b/internal/app/launch.machinectl.go
new file mode 100644
index 00000000..21b9d40b
--- /dev/null
+++ b/internal/app/launch.machinectl.go
@@ -0,0 +1,67 @@
+package app
+
+import (
+ "os/exec"
+ "strings"
+
+ "git.ophivana.moe/cat/fortify/internal/state"
+ "git.ophivana.moe/cat/fortify/internal/verbose"
+)
+
+func (a *app) commandBuilderMachineCtl() (args []string) {
+ args = make([]string, 0, 9+len(a.seal.env))
+
+ // shell --uid=$USER
+ args = append(args, "shell", "--uid="+a.seal.sys.Username)
+
+ // --quiet
+ if !verbose.Get() {
+ args = append(args, "--quiet")
+ }
+
+ // environ
+ envQ := make([]string, len(a.seal.env)+1)
+ for i, e := range a.seal.env {
+ envQ[i] = "-E" + e
+ }
+ // add shim payload to environment for shim path
+ envQ[len(a.seal.env)] = "-E" + a.shimPayloadEnv()
+ args = append(args, envQ...)
+
+ // -- .host
+ args = append(args, "--", ".host")
+
+ // /bin/sh -c
+ if sh, err := exec.LookPath("sh"); err != nil {
+ // hardcode /bin/sh path since it exists more often than not
+ args = append(args, "/bin/sh", "-c")
+ } else {
+ args = append(args, sh, "-c")
+ }
+
+ // build inner command expression ran as target user
+ innerCommand := strings.Builder{}
+
+ // apply custom environment variables to activation environment
+ innerCommand.WriteString("dbus-update-activation-environment --systemd")
+ for _, e := range a.seal.env {
+ innerCommand.WriteString(" " + strings.SplitN(e, "=", 2)[0])
+ }
+ 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] + "' ")
+ }
+ }
+
+ // both license and version flags need to be set to activate shim path
+ innerCommand.WriteString("exec " + a.seal.sys.executable + " -V -license")
+
+ // append inner command
+ args = append(args, innerCommand.String())
+
+ return
+}
diff --git a/internal/app/launch.sudo.go b/internal/app/launch.sudo.go
new file mode 100644
index 00000000..666d0b3a
--- /dev/null
+++ b/internal/app/launch.sudo.go
@@ -0,0 +1,33 @@
+package app
+
+import (
+ "os"
+
+ "git.ophivana.moe/cat/fortify/internal/verbose"
+)
+
+const (
+ sudoAskPass = "SUDO_ASKPASS"
+)
+
+func (a *app) commandBuilderSudo() (args []string) {
+ args = make([]string, 0, 4+len(a.seal.env)+len(a.seal.command))
+
+ // -Hiu $USER
+ args = append(args, "-Hiu", a.seal.sys.Username)
+
+ // -A?
+ if _, ok := os.LookupEnv(sudoAskPass); ok {
+ verbose.Printf("%s set, adding askpass flag\n", sudoAskPass)
+ args = append(args, "-A")
+ }
+
+ // environ
+ args = append(args, a.seal.env...)
+
+ // -- $@
+ args = append(args, "--")
+ args = append(args, a.seal.command...)
+
+ return
+}
diff --git a/internal/app/pulse.go b/internal/app/pulse.go
deleted file mode 100644
index 8b4921ed..00000000
--- a/internal/app/pulse.go
+++ /dev/null
@@ -1,109 +0,0 @@
-package app
-
-import (
- "errors"
- "fmt"
- "io/fs"
- "os"
- "path"
-
- "git.ophivana.moe/cat/fortify/acl"
- "git.ophivana.moe/cat/fortify/internal"
- "git.ophivana.moe/cat/fortify/internal/util"
- "git.ophivana.moe/cat/fortify/internal/verbose"
-)
-
-const (
- pulseServer = "PULSE_SERVER"
- pulseCookie = "PULSE_COOKIE"
-
- home = "HOME"
- xdgConfigHome = "XDG_CONFIG_HOME"
-)
-
-func (a *App) SharePulse() {
- a.setEnablement(internal.EnablePulse)
-
- // ensure PulseAudio directory ACL (e.g. `/run/user/%d/pulse`)
- pulse := path.Join(a.runtimePath, "pulse")
- pulseS := path.Join(pulse, "native")
- if s, err := os.Stat(pulse); err != nil {
- if !errors.Is(err, fs.ErrNotExist) {
- internal.Fatal("Error accessing PulseAudio directory:", err)
- }
- internal.Fatal(fmt.Sprintf("PulseAudio dir '%s' not found", pulse))
- } else {
- // add environment variable for new process
- a.AppendEnv(pulseServer, "unix:"+pulseS)
- if err = acl.UpdatePerm(pulse, a.UID(), acl.Execute); err != nil {
- internal.Fatal("Error preparing PulseAudio:", err)
- } else {
- a.exit.RegisterRevertPath(pulse)
- }
-
- // ensure PulseAudio socket permission (e.g. `/run/user/%d/pulse/native`)
- if s, err = os.Stat(pulseS); err != nil {
- if errors.Is(err, fs.ErrNotExist) {
- internal.Fatal("PulseAudio directory found but socket does not exist")
- }
- internal.Fatal("Error accessing PulseAudio socket:", err)
- } else {
- if m := s.Mode(); m&0o006 != 0o006 {
- internal.Fatal(fmt.Sprintf("Unexpected permissions on '%s':", pulseS), m)
- }
- }
-
- // Publish current user's pulse-cookie for target user
- pulseCookieSource := discoverPulseCookie()
- pulseCookieFinal := path.Join(a.sharePath, "pulse-cookie")
- a.AppendEnv(pulseCookie, pulseCookieFinal)
- verbose.Printf("Publishing PulseAudio cookie '%s' to '%s'\n", pulseCookieSource, pulseCookieFinal)
- if err = util.CopyFile(pulseCookieFinal, pulseCookieSource); err != nil {
- internal.Fatal("Error copying PulseAudio cookie:", err)
- }
- if err = acl.UpdatePerm(pulseCookieFinal, a.UID(), acl.Read); err != nil {
- internal.Fatal("Error publishing PulseAudio cookie:", err)
- } else {
- a.exit.RegisterRevertPath(pulseCookieFinal)
- }
-
- verbose.Printf("PulseAudio dir '%s' configured\n", pulse)
- }
-}
-
-// discoverPulseCookie try various standard methods to discover the current user's PulseAudio authentication cookie
-func discoverPulseCookie() string {
- if p, ok := os.LookupEnv(pulseCookie); ok {
- return p
- }
-
- if p, ok := os.LookupEnv(home); ok {
- p = path.Join(p, ".pulse-cookie")
- if s, err := os.Stat(p); err != nil {
- if !errors.Is(err, fs.ErrNotExist) {
- internal.Fatal("Error accessing PulseAudio cookie:", err)
- // unreachable
- return p
- }
- } else if !s.IsDir() {
- return p
- }
- }
-
- if p, ok := os.LookupEnv(xdgConfigHome); ok {
- p = path.Join(p, "pulse", "cookie")
- if s, err := os.Stat(p); err != nil {
- if !errors.Is(err, fs.ErrNotExist) {
- internal.Fatal("Error accessing PulseAudio cookie:", err)
- // unreachable
- return p
- }
- } else if !s.IsDir() {
- return p
- }
- }
-
- internal.Fatal(fmt.Sprintf("Cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)",
- pulseCookie, xdgConfigHome, home))
- return ""
-}
diff --git a/internal/app/run.go b/internal/app/run.go
deleted file mode 100644
index cc01f487..00000000
--- a/internal/app/run.go
+++ /dev/null
@@ -1,163 +0,0 @@
-package app
-
-import (
- "errors"
- "fmt"
- "os"
- "os/exec"
- "strings"
-
- "git.ophivana.moe/cat/fortify/internal"
- "git.ophivana.moe/cat/fortify/internal/state"
- "git.ophivana.moe/cat/fortify/internal/util"
- "git.ophivana.moe/cat/fortify/internal/verbose"
-)
-
-const (
- term = "TERM"
- sudoAskPass = "SUDO_ASKPASS"
-)
-const (
- LaunchMethodSudo uint8 = iota
- LaunchMethodBwrap
- LaunchMethodMachineCtl
-)
-
-func (a *App) Run() {
- // pass $TERM to launcher
- if t, ok := os.LookupEnv(term); ok {
- a.AppendEnv(term, t)
- }
-
- var commandBuilder func() (args []string)
-
- switch a.launchOption {
- case LaunchMethodSudo:
- commandBuilder = a.commandBuilderSudo
- case LaunchMethodBwrap:
- commandBuilder = a.commandBuilderBwrap
- case LaunchMethodMachineCtl:
- commandBuilder = a.commandBuilderMachineCtl
- default:
- panic("unreachable")
- }
-
- cmd := exec.Command(a.toolPath, commandBuilder()...)
- cmd.Env = []string{}
- cmd.Stdin = os.Stdin
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- cmd.Dir = a.runDirPath
-
- verbose.Println("Executing:", cmd)
-
- if err := cmd.Start(); err != nil {
- internal.Fatal("Error starting process:", err)
- }
-
- a.exit.SealEnablements(a.enablements)
-
- if statePath, err := state.SaveProcess(a.Uid, cmd, a.runDirPath, a.command, a.enablements); err != nil {
- // process already started, shouldn't be fatal
- fmt.Println("Error registering process:", err)
- } else {
- a.exit.SealStatePath(statePath)
- }
-
- var r int
- if err := cmd.Wait(); err != nil {
- var exitError *exec.ExitError
- if !errors.As(err, &exitError) {
- internal.Fatal("Error running process:", err)
- }
- }
-
- verbose.Println("Process exited with exit code", r)
- internal.BeforeExit()
- os.Exit(r)
-}
-
-func (a *App) commandBuilderSudo() (args []string) {
- args = make([]string, 0, 4+len(a.env)+len(a.command))
-
- // -Hiu $USER
- args = append(args, "-Hiu", a.Username)
-
- // -A?
- if _, ok := os.LookupEnv(sudoAskPass); ok {
- verbose.Printf("%s set, adding askpass flag\n", sudoAskPass)
- args = append(args, "-A")
- }
-
- // environ
- args = append(args, a.env...)
-
- // -- $@
- args = append(args, "--")
- args = append(args, a.command...)
-
- return
-}
-
-func (a *App) commandBuilderBwrap() (args []string) {
- // TODO: build bwrap command
- internal.Fatal("bwrap")
- panic("unreachable")
-}
-
-func (a *App) commandBuilderMachineCtl() (args []string) {
- args = make([]string, 0, 9+len(a.env))
-
- // shell --uid=$USER
- args = append(args, "shell", "--uid="+a.Username)
-
- // --quiet
- if !verbose.Get() {
- args = append(args, "--quiet")
- }
-
- // environ
- envQ := make([]string, len(a.env)+1)
- for i, e := range a.env {
- envQ[i] = "-E" + e
- }
- envQ[len(a.env)] = "-E" + a.launcherPayloadEnv()
- args = append(args, envQ...)
-
- // -- .host
- args = append(args, "--", ".host")
-
- // /bin/sh -c
- if sh, ok := util.Which("sh"); !ok {
- internal.Fatal("Did not find 'sh' in PATH")
- } else {
- args = append(args, sh, "-c")
- }
-
- if len(a.command) == 0 { // execute shell if command is not provided
- a.command = []string{"$SHELL"}
- }
-
- innerCommand := strings.Builder{}
-
- innerCommand.WriteString("dbus-update-activation-environment --systemd")
- for _, e := range a.env {
- innerCommand.WriteString(" " + strings.SplitN(e, "=", 2)[0])
- }
- innerCommand.WriteString("; ")
-
- if executable, err := os.Executable(); err != nil {
- internal.Fatal("Error reading executable path:", err)
- } else {
- if a.enablements.Has(internal.EnableDBus) {
- innerCommand.WriteString(dbusSessionBusAddress + "=" + "'" + dbusAddress[0] + "' ")
- if dbusSystem {
- innerCommand.WriteString(dbusSystemBusAddress + "=" + "'" + dbusAddress[1] + "' ")
- }
- }
- innerCommand.WriteString("exec " + executable + " -V")
- }
- args = append(args, innerCommand.String())
-
- return
-}
diff --git a/internal/app/seal.go b/internal/app/seal.go
new file mode 100644
index 00000000..162faf40
--- /dev/null
+++ b/internal/app/seal.go
@@ -0,0 +1,154 @@
+package app
+
+import (
+ "errors"
+ "os"
+ "os/exec"
+ "os/user"
+ "strconv"
+
+ "git.ophivana.moe/cat/fortify/dbus"
+ "git.ophivana.moe/cat/fortify/internal"
+ "git.ophivana.moe/cat/fortify/internal/state"
+ "git.ophivana.moe/cat/fortify/internal/verbose"
+)
+
+const (
+ LaunchMethodSudo uint8 = iota
+ LaunchMethodBwrap
+ LaunchMethodMachineCtl
+)
+
+var (
+ ErrConfig = errors.New("no configuration to seal")
+ ErrUser = errors.New("unknown user")
+ ErrLaunch = errors.New("invalid launch method")
+
+ ErrSudo = errors.New("sudo not available")
+ ErrBwrap = errors.New("bwrap not available")
+ ErrSystemd = errors.New("systemd not available")
+ 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()
+ defer a.lock.Unlock()
+
+ if a.seal != nil {
+ panic("app sealed twice")
+ }
+
+ if config == nil {
+ return (*SealConfigError)(wrapError(ErrConfig, "attempted to seal app with nil config"))
+ }
+
+ // create seal
+ seal := new(appSeal)
+
+ // generate application ID
+ if id, err := newAppID(); err != nil {
+ return (*SecurityError)(wrapError(err, "cannot generate application ID:", err))
+ } else {
+ seal.id = id
+ }
+
+ // fetch system constants
+ seal.SystemConstants = internal.GetSC()
+
+ // pass through config values
+ seal.fid = config.ID
+ seal.command = config.Command
+
+ // parses launch method text and looks up tool path
+ switch config.Method {
+ case "sudo":
+ seal.launchOption = LaunchMethodSudo
+ if sudoPath, err := exec.LookPath("sudo"); err != nil {
+ return (*LauncherLookupError)(wrapError(ErrSudo, "sudo not found"))
+ } else {
+ seal.toolPath = sudoPath
+ }
+ case "bubblewrap":
+ seal.launchOption = LaunchMethodBwrap
+ if bwrapPath, err := exec.LookPath("bwrap"); err != nil {
+ return (*LauncherLookupError)(wrapError(ErrBwrap, "bwrap not found"))
+ } else {
+ seal.toolPath = bwrapPath
+ }
+ case "systemd":
+ seal.launchOption = LaunchMethodMachineCtl
+ if !internal.SdBootedV {
+ return (*LauncherLookupError)(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"))
+ } else {
+ seal.toolPath = machineCtlPath
+ }
+ default:
+ return (*SealConfigError)(wrapError(ErrLaunch, "invalid launch method"))
+ }
+
+ // create seal system component
+ seal.sys = new(appSealTx)
+
+ // look up fortify executable path
+ if p, err := os.Executable(); err != nil {
+ return (*LauncherLookupError)(wrapError(err, "cannot look up fortify executable path:", err))
+ } else {
+ seal.sys.executable = p
+ }
+
+ // 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))
+ } else {
+ // unreachable
+ panic(err)
+ }
+ } else {
+ seal.sys.User = u
+ }
+
+ // 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)
+
+ // parse string UID
+ if u, err := strconv.Atoi(seal.sys.Uid); err != nil {
+ // unreachable unless kernel bug
+ panic("uid parse")
+ } else {
+ seal.sys.uid = u
+ }
+
+ // pass through enablements
+ seal.et = config.Confinement.Enablements
+
+ // this method calls all share methods in sequence
+ if err := seal.shareAll([2]*dbus.Config{config.Confinement.SessionBus, config.Confinement.SystemBus}); err != nil {
+ return err
+ }
+
+ // verbose log seal information
+ verbose.Println("created application seal as user",
+ seal.sys.Username, "("+seal.sys.Uid+"),",
+ "method:", config.Method+",",
+ "launcher:", seal.toolPath+",",
+ "command:", config.Command)
+
+ // seal app and release lock
+ a.seal = seal
+ return nil
+}
diff --git a/internal/app/setup.go b/internal/app/setup.go
deleted file mode 100644
index c13468a0..00000000
--- a/internal/app/setup.go
+++ /dev/null
@@ -1,150 +0,0 @@
-package app
-
-import (
- "errors"
- "fmt"
- "os"
- "os/user"
- "path"
- "strconv"
-
- "git.ophivana.moe/cat/fortify/internal"
- "git.ophivana.moe/cat/fortify/internal/util"
- "git.ophivana.moe/cat/fortify/internal/verbose"
-)
-
-const (
- xdgRuntimeDir = "XDG_RUNTIME_DIR"
-)
-
-type App struct {
- uid int // assigned
- env []string // modified via AppendEnv
- command []string // set on initialisation
-
- exit *internal.ExitState // assigned
-
- launchOptionText string // set on initialisation
- launchOption uint8 // assigned
-
- sharePath string // set on initialisation
- runtimePath string // assigned
- runDirPath string // assigned
- toolPath string // assigned
-
- enablements internal.Enablements // set via setEnablement
- *user.User // assigned
-
- // absolutely *no* method of this type is thread-safe
- // so don't treat it as if it is
-}
-
-func (a *App) LaunchOption() uint8 {
- return a.launchOption
-}
-
-func (a *App) RunDir() string {
- return a.runDirPath
-}
-
-func (a *App) setEnablement(e internal.Enablement) {
- if a.enablements.Has(e) {
- panic("enablement " + e.String() + " set twice")
- }
-
- a.enablements |= e.Mask()
-}
-
-func (a *App) SealExit(exit *internal.ExitState) {
- if a.exit != nil {
- panic("application exit state sealed twice")
- }
- a.exit = exit
-}
-
-func New(userName string, args []string, launchOptionText string) *App {
- a := &App{
- command: args,
- launchOptionText: launchOptionText,
- sharePath: path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Geteuid())),
- }
-
- // runtimePath, runDirPath
- if r, ok := os.LookupEnv(xdgRuntimeDir); !ok {
- fmt.Println("Env variable", xdgRuntimeDir, "unset")
-
- // too early for fatal
- os.Exit(1)
- } else {
- a.runtimePath = r
- a.runDirPath = path.Join(a.runtimePath, "fortify")
- verbose.Println("Runtime directory at", a.runDirPath)
- }
-
- // *user.User
- if u, err := user.Lookup(userName); err != nil {
- if errors.As(err, new(user.UnknownUserError)) {
- fmt.Println("unknown user", userName)
- } else {
- // unreachable
- panic(err)
- }
-
- // too early for fatal
- os.Exit(1)
- } else {
- a.User = u
- }
-
- // uid
- if u, err := strconv.Atoi(a.Uid); err != nil {
- // usually unreachable
- panic("uid parse")
- } else {
- a.uid = u
- }
-
- verbose.Println("Running as user", a.Username, "("+a.Uid+"),", "command:", a.command)
- if internal.SdBootedV {
- verbose.Println("System booted with systemd as init system (PID 1).")
- }
-
- // launchOption, toolPath
- switch a.launchOptionText {
- case "sudo":
- a.launchOption = LaunchMethodSudo
- if sudoPath, ok := util.Which("sudo"); !ok {
- fmt.Println("Did not find 'sudo' in PATH")
- os.Exit(1)
- } else {
- a.toolPath = sudoPath
- }
- case "bubblewrap":
- a.launchOption = LaunchMethodBwrap
- if bwrapPath, ok := util.Which("bwrap"); !ok {
- fmt.Println("Did not find 'bwrap' in PATH")
- os.Exit(1)
- } else {
- a.toolPath = bwrapPath
- }
- case "systemd":
- a.launchOption = LaunchMethodMachineCtl
- if !internal.SdBootedV {
- fmt.Println("System has not been booted with systemd as init system (PID 1).")
- os.Exit(1)
- }
-
- if machineCtlPath, ok := util.Which("machinectl"); !ok {
- fmt.Println("Did not find 'machinectl' in PATH")
- } else {
- a.toolPath = machineCtlPath
- }
- default:
- fmt.Println("invalid launch method")
- os.Exit(1)
- }
-
- verbose.Println("Determined launch method to be", a.launchOptionText, "with tool at", a.toolPath)
-
- return a
-}
diff --git a/internal/app/share.dbus.go b/internal/app/share.dbus.go
new file mode 100644
index 00000000..c350c21c
--- /dev/null
+++ b/internal/app/share.dbus.go
@@ -0,0 +1,152 @@
+package app
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "os/exec"
+ "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 (
+ dbusSessionBusAddress = "DBUS_SESSION_BUS_ADDRESS"
+ dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS"
+
+ xdgDBusProxy = "xdg-dbus-proxy"
+)
+
+var (
+ ErrDBusConfig = errors.New("dbus config not supplied")
+ ErrDBusProxy = errors.New(xdgDBusProxy + " not found")
+ ErrDBusFault = errors.New(xdgDBusProxy + " did not start correctly")
+)
+
+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")
+
+ // resolve upstream session bus address
+ if addr, ok := os.LookupEnv(dbusSessionBusAddress); !ok {
+ // fall back to default format
+ sessionBus[0] = fmt.Sprintf("unix:path=/run/user/%d/bus", os.Getuid())
+ } else {
+ sessionBus[0] = addr
+ }
+
+ // resolve upstream system bus address
+ if addr, ok := os.LookupEnv(dbusSystemBusAddress); !ok {
+ // fall back to default hardcoded value
+ systemBus[0] = "unix:path=/run/dbus/system_bus_socket"
+ } else {
+ systemBus[0] = addr
+ }
+
+ // look up proxy program path for dbus.New
+ if b, err := exec.LookPath(xdgDBusProxy); err != nil {
+ return (*LookupDBusError)(wrapError(ErrDBusProxy, xdgDBusProxy, "not found"))
+ } else {
+ // create proxy instance
+ seal.sys.dbus = dbus.New(b, 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
+ seal.appendEnv(dbusSessionBusAddress, "unix:path="+sessionBus[1])
+ seal.sys.updatePerm(sessionBus[1], acl.Read, acl.Write)
+ if seal.sys.dbusSystem {
+ seal.appendEnv(dbusSystemBusAddress, "unix:path="+systemBus[1])
+ seal.sys.updatePerm(systemBus[1], acl.Read, acl.Write)
+ }
+
+ return nil
+}
+
+func (tx *appSealTx) startDBus() error {
+ // ready channel passed to dbus package
+ ready := make(chan bool, 1)
+ // used by waiting goroutine to notify process return
+ tx.dbusWait = make(chan struct{})
+
+ // background dbus proxy start
+ if err := tx.dbus.Start(&ready); err != nil {
+ return (*StartDBusError)(wrapError(err, "cannot start message bus proxy:", err))
+ }
+
+ // 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)
+ } else {
+ verbose.Println("message bus proxy uneventful wait")
+ }
+
+ // 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 false if the proxy process faulted
+ if !<-ready {
+ return (*StartDBusError)(wrapError(ErrDBusFault, "message bus proxy failed"))
+ }
+
+ return nil
+}
+
+func (tx *appSealTx) stopDBus() error {
+ if err := tx.dbus.Close(); err != nil {
+ if errors.Is(err, os.ErrClosed) {
+ return (*CloseDBusError)(wrapError(err, "message bus proxy already closed"))
+ } else {
+ return (*CloseDBusError)(wrapError(err, "cannot close message bus proxy:", err))
+ }
+ }
+
+ // block until proxy wait returns
+ <-tx.dbusWait
+ return nil
+}
diff --git a/internal/app/share.display.go b/internal/app/share.display.go
new file mode 100644
index 00000000..c1891876
--- /dev/null
+++ b/internal/app/share.display.go
@@ -0,0 +1,59 @@
+package app
+
+import (
+ "errors"
+ "os"
+ "path"
+
+ "git.ophivana.moe/cat/fortify/acl"
+ "git.ophivana.moe/cat/fortify/internal/state"
+)
+
+const (
+ term = "TERM"
+ display = "DISPLAY"
+
+ // https://manpages.debian.org/experimental/libwayland-doc/wl_display_connect.3.en.html
+ waylandDisplay = "WAYLAND_DISPLAY"
+)
+
+var (
+ ErrWayland = errors.New(waylandDisplay + " unset")
+ 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.appendEnv(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"))
+ } else {
+ // wayland socket path
+ wp := path.Join(seal.RuntimePath, wd)
+ seal.appendEnv(waylandDisplay, wp)
+
+ // ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`)
+ seal.sys.updatePerm(wp, acl.Read, acl.Write, acl.Execute)
+ }
+ }
+
+ // set up X11
+ 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"))
+ } else {
+ seal.sys.changeHosts(seal.sys.Username)
+ seal.appendEnv(display, d)
+ }
+ }
+
+ return nil
+}
diff --git a/internal/app/share.pulse.go b/internal/app/share.pulse.go
new file mode 100644
index 00000000..633b859d
--- /dev/null
+++ b/internal/app/share.pulse.go
@@ -0,0 +1,116 @@
+package app
+
+import (
+ "errors"
+ "fmt"
+ "io/fs"
+ "os"
+ "path"
+
+ "git.ophivana.moe/cat/fortify/acl"
+ "git.ophivana.moe/cat/fortify/internal/state"
+)
+
+const (
+ pulseServer = "PULSE_SERVER"
+ pulseCookie = "PULSE_COOKIE"
+
+ home = "HOME"
+ xdgConfigHome = "XDG_CONFIG_HOME"
+)
+
+var (
+ ErrPulseCookie = errors.New("pulse cookie not present")
+ ErrPulseSocket = errors.New("pulse socket not present")
+ 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
+ }
+
+ // ensure PulseAudio directory ACL (e.g. `/run/user/%d/pulse`)
+ pd := path.Join(seal.RuntimePath, "pulse")
+ 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 (*PulseSocketAccessError)(wrapError(ErrPulseSocket,
+ fmt.Sprintf("PulseAudio directory '%s' not found", pd)))
+ }
+
+ seal.appendEnv(pulseServer, "unix:"+ps)
+ seal.sys.updatePerm(pd, acl.Execute)
+
+ // ensure 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 (*PulseSocketAccessError)(wrapError(ErrPulseSocket,
+ fmt.Sprintf("PulseAudio directory '%s' 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))
+ }
+ }
+
+ // 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.appendEnv(pulseCookie, dst)
+ seal.sys.copyFile(dst, src)
+ }
+
+ return nil
+}
+
+// discoverPulseCookie attempts various standard methods to discover the current user's PulseAudio authentication cookie
+func discoverPulseCookie() (string, error) {
+ if p, ok := os.LookupEnv(pulseCookie); ok {
+ return p, nil
+ }
+
+ // dotfile $HOME/.pulse-cookie
+ if p, ok := os.LookupEnv(home); ok {
+ 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))
+ }
+ // not found, try next method
+ } else if !s.IsDir() {
+ return p, nil
+ }
+ }
+
+ // $XDG_CONFIG_HOME/pulse/cookie
+ if p, ok := os.LookupEnv(xdgConfigHome); ok {
+ 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))
+ }
+ // not found, try next method
+ } else if !s.IsDir() {
+ return p, nil
+ }
+ }
+
+ return "", (*PulseCookieAccessError)(wrapError(ErrPulseCookie,
+ fmt.Sprintf("cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)",
+ pulseCookie, xdgConfigHome, home)))
+}
diff --git a/internal/app/share.runtime.go b/internal/app/share.runtime.go
new file mode 100644
index 00000000..e43effe2
--- /dev/null
+++ b/internal/app/share.runtime.go
@@ -0,0 +1,50 @@
+package app
+
+import (
+ "path"
+
+ "git.ophivana.moe/cat/fortify/acl"
+)
+
+const (
+ xdgRuntimeDir = "XDG_RUNTIME_DIR"
+ xdgSessionClass = "XDG_SESSION_CLASS"
+ xdgSessionType = "XDG_SESSION_TYPE"
+)
+
+// shareRuntime queues actions for sharing/ensuring the runtime and share directories
+func (seal *appSeal) shareRuntime() {
+ // ensure RunDir (e.g. `/run/user/%d/fortify`)
+ seal.sys.ensure(seal.RunDirPath, 0700)
+
+ // ensure runtime directory ACL (e.g. `/run/user/%d`)
+ seal.sys.updatePerm(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)
+
+ // 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)
+}
+
+func (seal *appSeal) shareRuntimeChild() string {
+ // ensure child runtime parent directory (e.g. `/tmp/fortify.%d/runtime`)
+ targetRuntimeParent := path.Join(seal.SharePath, "runtime")
+ seal.sys.ensure(targetRuntimeParent, 0700)
+ seal.sys.updatePerm(targetRuntimeParent, acl.Execute)
+
+ // ensure child runtime directory (e.g. `/tmp/fortify.%d/runtime/%d`)
+ targetRuntime := path.Join(targetRuntimeParent, seal.sys.Uid)
+ seal.sys.ensure(targetRuntime, 0700)
+ seal.sys.updatePerm(targetRuntime, acl.Read, acl.Write, acl.Execute)
+
+ // point to ensured runtime path
+ seal.appendEnv(xdgRuntimeDir, targetRuntime)
+ seal.appendEnv(xdgSessionClass, "user")
+ seal.appendEnv(xdgSessionType, "tty")
+
+ return targetRuntime
+}
diff --git a/internal/app/shim.go b/internal/app/shim.go
new file mode 100644
index 00000000..00f711a1
--- /dev/null
+++ b/internal/app/shim.go
@@ -0,0 +1,83 @@
+package app
+
+import (
+ "bytes"
+ "encoding/base64"
+ "encoding/gob"
+ "fmt"
+ "os"
+ "os/exec"
+ "strings"
+ "syscall"
+)
+
+const shimPayload = "FORTIFY_SHIM_PAYLOAD"
+
+func (a *app) shimPayloadEnv() string {
+ r := &bytes.Buffer{}
+ enc := base64.NewEncoder(base64.StdEncoding, r)
+
+ if err := gob.NewEncoder(enc).Encode(a.seal.command); err != nil {
+ // should be unreachable
+ panic(err)
+ }
+
+ _ = enc.Close()
+ return shimPayload + "=" + r.String()
+}
+
+// TryShim attempts the early hidden launcher shim path
+func TryShim() {
+ // environment variable contains encoded argv
+ if r, ok := os.LookupEnv(shimPayload); ok {
+ // everything beyond this point runs as target user
+ // proceed with caution!
+
+ // parse base64 revealing underlying gob stream
+ dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(r))
+
+ // decode argv gob stream
+ var argv []string
+ if err := gob.NewDecoder(dec).Decode(&argv); err != nil {
+ fmt.Println("fortify-shim: cannot decode shim payload:", err)
+ os.Exit(1)
+ }
+
+ // remove payload variable since the child does not need to see it
+ if err := os.Unsetenv(shimPayload); err != nil {
+ fmt.Println("fortify-shim: cannot unset shim payload:", err)
+ // not fatal, do not fail
+ }
+
+ // look up argv0
+ var argv0 string
+
+ if len(argv) > 0 {
+ // look up program from $PATH
+ if p, err := exec.LookPath(argv[0]); err != nil {
+ fmt.Printf("%s not found: %s\n", argv[0], err)
+ os.Exit(1)
+ } else {
+ argv0 = p
+ }
+ } else {
+ // no argv, look up shell instead
+ if argv0, ok = os.LookupEnv("SHELL"); !ok {
+ fmt.Println("fortify-shim: no command was specified and $SHELL was unset")
+ os.Exit(1)
+ }
+
+ argv = []string{argv0}
+ }
+
+ // exec target process
+ if err := syscall.Exec(argv0, argv, os.Environ()); err != nil {
+ fmt.Println("fortify-shim: cannot execute shim payload:", err)
+ os.Exit(1)
+ }
+
+ // unreachable
+ os.Exit(1)
+ return
+ }
+}
diff --git a/internal/app/start.go b/internal/app/start.go
new file mode 100644
index 00000000..b335b68a
--- /dev/null
+++ b/internal/app/start.go
@@ -0,0 +1,187 @@
+package app
+
+import (
+ "errors"
+ "os"
+ "os/exec"
+ "strconv"
+ "time"
+
+ "git.ophivana.moe/cat/fortify/internal/state"
+ "git.ophivana.moe/cat/fortify/internal/verbose"
+)
+
+type (
+ // ProcessError encapsulates errors returned by starting *exec.Cmd
+ ProcessError BaseError
+)
+
+// Start starts the fortified child
+func (a *app) Start() error {
+ a.lock.Lock()
+ defer a.lock.Unlock()
+
+ if err := a.seal.sys.commit(); err != nil {
+ return err
+ }
+
+ // select command builder
+ var commandBuilder func() (args []string)
+ switch a.seal.launchOption {
+ case LaunchMethodSudo:
+ commandBuilder = a.commandBuilderSudo
+ case LaunchMethodBwrap:
+ commandBuilder = a.commandBuilderBwrap
+ case LaunchMethodMachineCtl:
+ commandBuilder = a.commandBuilderMachineCtl
+ default:
+ panic("unreachable")
+ }
+
+ // configure child process
+ a.cmd = exec.Command(a.seal.toolPath, commandBuilder()...)
+ a.cmd.Env = []string{}
+ a.cmd.Stdin = os.Stdin
+ a.cmd.Stdout = os.Stdout
+ a.cmd.Stderr = os.Stderr
+ a.cmd.Dir = a.seal.RunDirPath
+
+ // start child process
+ verbose.Println("starting main process:", a.cmd)
+ if err := a.cmd.Start(); err != nil {
+ return (*ProcessError)(wrapError(err, "cannot start process:", err))
+ }
+ startTime := time.Now().UTC()
+
+ // create process state
+ sd := state.State{
+ PID: a.cmd.Process.Pid,
+ Command: a.seal.command,
+ Capability: a.seal.et,
+ Launcher: a.seal.toolPath,
+ Argv: a.cmd.Args,
+ Time: startTime,
+ }
+
+ // register process state
+ var e = new(StateStoreError)
+ e.Inner, e.DoErr = a.seal.store.Do(func(b state.Backend) {
+ e.InnerErr = b.Save(&sd)
+ })
+ return e.equiv("cannot save process state:", e)
+}
+
+// StateStoreError is returned for a failed state save
+type StateStoreError struct {
+ // whether inner function was called
+ Inner bool
+ // error returned by state.Store Do method
+ DoErr error
+ // error returned by state.Backend Save method
+ InnerErr error
+ // any other errors needing to be tracked
+ Err error
+}
+
+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...)
+ }
+}
+
+func (e *StateStoreError) Error() string {
+ if e.Inner && e.InnerErr != nil {
+ return e.InnerErr.Error()
+ }
+
+ if e.DoErr != nil {
+ return e.DoErr.Error()
+ }
+
+ if e.Err != nil {
+ return e.Err.Error()
+ }
+
+ return "(nil)"
+}
+
+func (e *StateStoreError) Unwrap() (errs []error) {
+ errs = make([]error, 0, 3)
+ if e.DoErr != nil {
+ errs = append(errs, e.DoErr)
+ }
+ if e.InnerErr != nil {
+ errs = append(errs, e.InnerErr)
+ }
+ if e.Err != nil {
+ errs = append(errs, e.Err)
+ }
+ return
+}
+
+type RevertCompoundError interface {
+ Error() string
+ Unwrap() []error
+}
+
+func (a *app) Wait() (int, error) {
+ a.lock.Lock()
+ defer a.lock.Unlock()
+
+ var r int
+
+ // wait for process and resolve exit code
+ if err := a.cmd.Wait(); err != nil {
+ var exitError *exec.ExitError
+ if !errors.As(err, &exitError) {
+ // should be unreachable
+ a.wait = err
+ }
+
+ // store non-zero return code
+ r = exitError.ExitCode()
+ } else {
+ r = a.cmd.ProcessState.ExitCode()
+ }
+
+ verbose.Println("process", strconv.Itoa(a.cmd.Process.Pid), "exited with exit code", r)
+
+ // update store and revert app setup transaction
+ e := new(StateStoreError)
+ e.Inner, e.DoErr = a.seal.store.Do(func(b state.Backend) {
+ e.InnerErr = func() error {
+ // destroy defunct state entry
+ if err := b.Destroy(a.cmd.Process.Pid); err != nil {
+ return err
+ }
+
+ var global bool
+
+ // measure remaining state entries
+ if l, err := b.Len(); err != nil {
+ return err
+ } else {
+ // clean up global modifications if we're the last launcher alive
+ global = l == 0
+
+ if !global {
+ verbose.Printf("found %d active launchers, cleaning up without globals\n", l)
+ } else {
+ verbose.Println("no other launchers active, will clean up globals")
+ }
+ }
+
+ // FIXME: depending on exit sequence, some parts of the transaction never gets reverted
+ if err := a.seal.sys.revert(global); err != nil {
+ return err.(RevertCompoundError)
+ }
+
+ return nil
+ }()
+ })
+
+ e.Err = a.seal.store.Close()
+ return r, e.equiv("error returned during cleanup:", e)
+}
diff --git a/internal/app/system.go b/internal/app/system.go
new file mode 100644
index 00000000..36802ddb
--- /dev/null
+++ b/internal/app/system.go
@@ -0,0 +1,351 @@
+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/internal"
+ "git.ophivana.moe/cat/fortify/internal/state"
+ "git.ophivana.moe/cat/fortify/internal/verbose"
+ "git.ophivana.moe/cat/fortify/xcb"
+)
+
+// appSeal seals the application with child-related information
+type appSeal struct {
+ // application unique identifier
+ id *appID
+
+ // freedesktop application ID
+ fid string
+ // argv to start process with in the final confined environment
+ command []string
+ // environment variables of fortified process
+ env []string
+ // persistent process state store
+ store state.Store
+
+ // uint8 representation of launch method sealed from config
+ launchOption uint8
+ // process-specific share directory path
+ share string
+
+ // path to launcher program
+ toolPath string
+ // pass-through enablement tracking from config
+ et state.Enablements
+
+ // prevents sharing from happening twice
+ shared bool
+ // seal system-level component
+ sys *appSealTx
+
+ // used in various sealing operations
+ internal.SystemConstants
+
+ // protected by upstream mutex
+}
+
+// appendEnv appends an environment variable for the child process
+func (seal *appSeal) appendEnv(k, v string) {
+ seal.env = append(seal.env, k+"="+v)
+}
+
+// appSealTx contains the system-level component of the app seal
+type appSealTx struct {
+ // 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, src pairs of temporarily shared files
+ tmpfiles [][2]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
+
+ // prevents commit from happening twice
+ complete bool
+ // prevents cleanup from happening twice
+ closed bool
+
+ // protected by upstream mutex
+}
+
+type appEnsureEntry struct {
+ path string
+ perm os.FileMode
+ remove bool
+}
+
+// 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 {
+ path string
+ perms []acl.Perm
+}
+
+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 acl update action
+func (tx *appSealTx) updatePerm(path string, perms ...acl.Perm) {
+ tx.acl = append(tx.acl, &appACLEntry{path, perms})
+}
+
+// changeHosts appends target username of an X11 ChangeHosts action
+func (tx *appSealTx) changeHosts(username string) {
+ tx.xhost = append(tx.xhost, username)
+}
+
+// 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)
+}
+
+type (
+ ChangeHostsError BaseError
+ EnsureDirError BaseError
+ TmpfileError BaseError
+ DBusStartError BaseError
+ ACLUpdateError BaseError
+)
+
+// commit applies recorded actions
+// order: xhost, mkdir, tmpfiles, dbus, acl
+func (tx *appSealTx) commit() error {
+ if tx.complete {
+ panic("seal transaction committed twice")
+ }
+ tx.complete = true
+
+ txp := &appSealTx{}
+ 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
+ if err := txp.revert(false); 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)
+ }
+ }
+ }
+
+ // 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])
+ }
+ }
+
+ if tx.dbus != nil {
+ // start dbus proxy
+ verbose.Printf("starting session bus proxy on '%s' for upstream '%s'\n", tx.dbusAddr[0][1], tx.dbusAddr[0][0])
+ if tx.dbusSystem {
+ verbose.Printf("starting 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
+
+ verbose.Println(xdgDBusProxy, "launch:", tx.dbus)
+ }
+ }
+
+ // apply ACLs
+ for _, e := range tx.acl {
+ verbose.Println("applying ACL", e, "uid:", tx.Uid, "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.updatePerm(e.path, e.perms...)
+ }
+ }
+
+ // disarm partial commit rollback
+ txp = nil
+ return nil
+}
+
+// revert rolls back recorded actions
+// order: acl, dbus, tmpfiles, mkdir, xhost
+// errors are printed but not treated as fatal
+func (tx *appSealTx) revert(global bool) 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)
+ }
+
+ if global {
+ // revert ACLs
+ for _, e := range tx.acl {
+ verbose.Println("stripping ACL", e, "uid:", tx.Uid, "path:", e.path)
+ err := acl.UpdatePerm(e.path, tx.uid)
+ joinError(err, fmt.Sprintf("cannot strip ACL entry from '%s': %s", e.path, err))
+ }
+ }
+
+ 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 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 (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 global {
+ // 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 {
+ panic("seal shared twice")
+ }
+ seal.shared = true
+
+ seal.shareRuntime()
+ if err := seal.shareDisplay(); err != nil {
+ return err
+ }
+ if err := seal.sharePulse(); err != nil {
+ return err
+ }
+
+ // ensure dbus session bus defaults
+ if bus[0] == nil {
+ bus[0] = dbus.NewConfig(seal.fid, true, true)
+ }
+
+ 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]))
+ }
+ }
+
+ // workaround for launch method sudo
+ if seal.launchOption == LaunchMethodSudo {
+ targetRuntime := seal.shareRuntimeChild()
+ verbose.Printf("child runtime data dir '%s' configured\n", targetRuntime)
+ }
+
+ return nil
+}
diff --git a/internal/app/wayland.go b/internal/app/wayland.go
deleted file mode 100644
index 33ed9914..00000000
--- a/internal/app/wayland.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package app
-
-import (
- "fmt"
- "os"
- "path"
-
- "git.ophivana.moe/cat/fortify/acl"
- "git.ophivana.moe/cat/fortify/internal"
- "git.ophivana.moe/cat/fortify/internal/verbose"
-)
-
-const (
- // https://manpages.debian.org/experimental/libwayland-doc/wl_display_connect.3.en.html
- waylandDisplay = "WAYLAND_DISPLAY"
-)
-
-func (a *App) ShareWayland() {
- a.setEnablement(internal.EnableWayland)
-
- // ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`)
- if w, ok := os.LookupEnv(waylandDisplay); !ok {
- internal.Fatal("Wayland: WAYLAND_DISPLAY not set")
- } else {
- // add environment variable for new process
- wp := path.Join(a.runtimePath, w)
- a.AppendEnv(waylandDisplay, wp)
- if err := acl.UpdatePerm(wp, a.UID(), acl.Read, acl.Write, acl.Execute); err != nil {
- internal.Fatal(fmt.Sprintf("Error preparing Wayland '%s':", w), err)
- } else {
- a.exit.RegisterRevertPath(wp)
- }
- verbose.Printf("Wayland socket '%s' configured\n", w)
- }
-}
diff --git a/internal/app/x.go b/internal/app/x.go
deleted file mode 100644
index 16b3d95c..00000000
--- a/internal/app/x.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package app
-
-import (
- "fmt"
- "os"
-
- "git.ophivana.moe/cat/fortify/internal"
- "git.ophivana.moe/cat/fortify/internal/verbose"
- "git.ophivana.moe/cat/fortify/xcb"
-)
-
-const display = "DISPLAY"
-
-func (a *App) ShareX() {
- a.setEnablement(internal.EnableX)
-
- // discovery X11 and grant user permission via the `ChangeHosts` command
- if d, ok := os.LookupEnv(display); !ok {
- internal.Fatal("X11: DISPLAY not set")
- } else {
- // add environment variable for new process
- a.AppendEnv(display, d)
-
- verbose.Printf("X11: Adding XHost entry SI:localuser:%s to display '%s'\n", a.Username, d)
- if err := xcb.ChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+a.Username); err != nil {
- internal.Fatal(fmt.Sprintf("Error adding XHost entry to '%s':", d), err)
- } else {
- a.exit.XcbActionComplete()
- }
- }
-}