aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/app/system.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/app/system.go')
-rw-r--r--internal/app/system.go365
1 files changed, 13 insertions, 352 deletions
diff --git a/internal/app/system.go b/internal/app/system.go
index a0162939..56966479 100644
--- a/internal/app/system.go
+++ b/internal/app/system.go
@@ -1,19 +1,14 @@
package app
import (
- "errors"
- "fmt"
- "io/fs"
- "os"
"os/user"
- "git.ophivana.moe/cat/fortify/acl"
"git.ophivana.moe/cat/fortify/dbus"
"git.ophivana.moe/cat/fortify/helper/bwrap"
"git.ophivana.moe/cat/fortify/internal"
"git.ophivana.moe/cat/fortify/internal/state"
+ "git.ophivana.moe/cat/fortify/internal/system"
"git.ophivana.moe/cat/fortify/internal/verbose"
- "git.ophivana.moe/cat/fortify/xcb"
)
// appSeal seals the application with child-related information
@@ -48,7 +43,7 @@ type appSeal struct {
// prevents sharing from happening twice
shared bool
// seal system-level component
- sys *appSealTx
+ sys *appSealSys
// used in various sealing operations
internal.SystemConstants
@@ -56,357 +51,24 @@ type appSeal struct {
// protected by upstream mutex
}
-// appSealTx contains the system-level component of the app seal
-type appSealTx struct {
+// appSealSys encapsulates app seal behaviour with OS interactions
+type appSealSys struct {
bwrap *bwrap.Config
- tmpfs []string
-
- // reference to D-Bus proxy instance, nil if disabled
- dbus *dbus.Proxy
- // notification from goroutine waiting for dbus.Proxy
- dbusWait chan struct{}
- // upstream address/downstream path used to initialise dbus.Proxy
- dbusAddr *[2][2]string
- // whether system bus proxy is enabled
- dbusSystem bool
-
- // paths to append/strip ACLs (of target user) from
- acl []*appACLEntry
- // X11 ChangeHosts commands to perform
- xhost []string
- // paths of directories to ensure
- mkdir []appEnsureEntry
- // dst, data pairs of temporarily available files
- files [][2]string
- // dst, src pairs of temporarily shared files
- tmpfiles [][2]string
- // dst, src pairs of temporarily hard linked files
- hardlinks [][2]string
+ // paths to override by mounting tmpfs over them
+ override []string
// default formatted XDG_RUNTIME_DIR of User
runtime string
// sealed path to fortify executable, used by shim
executable string
- // target user UID as an integer
- uid int
// target user sealed from config
- *user.User
+ user *user.User
- // prevents commit from happening twice
- complete bool
- // prevents cleanup from happening twice
- closed bool
+ *system.I
// protected by upstream mutex
}
-type appEnsureEntry struct {
- path string
- perm os.FileMode
- remove bool
-}
-
-// setEnv sets an environment variable for the child process
-func (tx *appSealTx) setEnv(k, v string) {
- tx.bwrap.SetEnv[k] = v
-}
-
-// ensure appends a directory ensure action
-func (tx *appSealTx) ensure(path string, perm os.FileMode) {
- tx.mkdir = append(tx.mkdir, appEnsureEntry{path, perm, false})
-}
-
-// ensureEphemeral appends a directory ensure action with removal in rollback
-func (tx *appSealTx) ensureEphemeral(path string, perm os.FileMode) {
- tx.mkdir = append(tx.mkdir, appEnsureEntry{path, perm, true})
-}
-
-// appACLEntry contains information for applying/reverting an ACL entry
-type appACLEntry struct {
- tag state.Enablement
- path string
- perms []acl.Perm
-}
-
-func (e *appACLEntry) ts() string {
- switch e.tag {
- case state.EnableLength:
- return "Global"
- case state.EnableLength + 1:
- return "Process"
- default:
- return e.tag.String()
- }
-}
-
-func (e *appACLEntry) String() string {
- var s = []byte("---")
- for _, p := range e.perms {
- switch p {
- case acl.Read:
- s[0] = 'r'
- case acl.Write:
- s[1] = 'w'
- case acl.Execute:
- s[2] = 'x'
- }
- }
- return string(s)
-}
-
-// updatePerm appends an untagged acl update action
-func (tx *appSealTx) updatePerm(path string, perms ...acl.Perm) {
- tx.updatePermTag(state.EnableLength+1, path, perms...)
-}
-
-// updatePermTag appends an acl update action
-// Tagging with state.EnableLength sets cleanup to happen at final active launcher exit,
-// while tagging with state.EnableLength+1 will unconditionally clean up on exit.
-func (tx *appSealTx) updatePermTag(tag state.Enablement, path string, perms ...acl.Perm) {
- tx.acl = append(tx.acl, &appACLEntry{tag, path, perms})
-}
-
-// changeHosts appends target username of an X11 ChangeHosts action
-func (tx *appSealTx) changeHosts(username string) {
- tx.xhost = append(tx.xhost, username)
-}
-
-// writeFile appends a files action
-func (tx *appSealTx) writeFile(dst string, data []byte) {
- tx.files = append(tx.files, [2]string{dst, string(data)})
- tx.updatePerm(dst, acl.Read)
- tx.bwrap.Bind(dst, dst)
-}
-
-// copyFile appends a tmpfiles action
-func (tx *appSealTx) copyFile(dst, src string) {
- tx.tmpfiles = append(tx.tmpfiles, [2]string{dst, src})
- tx.updatePerm(dst, acl.Read)
- tx.bwrap.Bind(dst, dst)
-}
-
-// link appends a hardlink action
-func (tx *appSealTx) link(oldname, newname string) {
- tx.hardlinks = append(tx.hardlinks, [2]string{oldname, newname})
-}
-
-type (
- ChangeHostsError BaseError
- EnsureDirError BaseError
- TmpfileError BaseError
- DBusStartError BaseError
- ACLUpdateError BaseError
-)
-
-// commit applies recorded actions
-// order: xhost, mkdir, files, tmpfiles, hardlinks, dbus, acl
-func (tx *appSealTx) commit() error {
- if tx.complete {
- panic("seal transaction committed twice")
- }
- tx.complete = true
-
- txp := &appSealTx{User: tx.User, bwrap: &bwrap.Config{SetEnv: make(map[string]string)}}
- defer func() {
- // rollback partial commit
- if txp != nil {
- // global changes (x11, ACLs) are always repeated and check for other launchers cannot happen here
- // attempting cleanup here will cause other fortified processes to lose access to them
- // a better (and more secure) fix is to proxy access to these resources and eliminate the ACLs altogether
- tags := new(state.Enablements)
- for e := state.Enablement(0); e < state.EnableLength+2; e++ {
- tags.Set(e)
- }
- if err := txp.revert(tags); err != nil {
- fmt.Println("fortify: errors returned reverting partial commit:", err)
- }
- }
- }()
-
- // insert xhost entries
- for _, username := range tx.xhost {
- verbose.Printf("inserting XHost entry SI:localuser:%s\n", username)
- if err := xcb.ChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+username); err != nil {
- return (*ChangeHostsError)(wrapError(err,
- fmt.Sprintf("cannot insert XHost entry SI:localuser:%s, %s", username, err)))
- } else {
- // register partial commit
- txp.changeHosts(username)
- }
- }
-
- // ensure directories
- for _, dir := range tx.mkdir {
- verbose.Println("ensuring directory mode:", dir.perm.String(), "path:", dir.path)
- if err := os.Mkdir(dir.path, dir.perm); err != nil && !errors.Is(err, fs.ErrExist) {
- return (*EnsureDirError)(wrapError(err,
- fmt.Sprintf("cannot create directory '%s': %s", dir.path, err)))
- } else {
- // only ephemeral dirs require rollback
- if dir.remove {
- // register partial commit
- txp.ensureEphemeral(dir.path, dir.perm)
- }
- }
- }
-
- // write files
- for _, file := range tx.files {
- verbose.Println("writing", len(file[1]), "bytes of data to", file[0])
- if err := os.WriteFile(file[0], []byte(file[1]), 0600); err != nil {
- return (*TmpfileError)(wrapError(err,
- fmt.Sprintf("cannot write file '%s': %s", file[0], err)))
- } else {
- // register partial commit
- txp.writeFile(file[0], make([]byte, 0)) // data not necessary for revert
- }
- }
-
- // publish tmpfiles
- for _, tmpfile := range tx.tmpfiles {
- verbose.Println("publishing tmpfile", tmpfile[0], "from", tmpfile[1])
- if err := copyFile(tmpfile[0], tmpfile[1]); err != nil {
- return (*TmpfileError)(wrapError(err,
- fmt.Sprintf("cannot publish tmpfile '%s' from '%s': %s", tmpfile[0], tmpfile[1], err)))
- } else {
- // register partial commit
- txp.copyFile(tmpfile[0], tmpfile[1])
- }
- }
-
- // create hardlinks
- for _, link := range tx.hardlinks {
- verbose.Println("creating hardlink", link[1], "from", link[0])
- if err := os.Link(link[0], link[1]); err != nil {
- return (*TmpfileError)(wrapError(err,
- fmt.Sprintf("cannot create hardlink '%s' from '%s': %s", link[1], link[0], err)))
- } else {
- // register partial commit
- txp.link(link[0], link[1])
- }
- }
-
- if tx.dbus != nil {
- // start dbus proxy
- verbose.Printf("session bus proxy on '%s' for upstream '%s'\n", tx.dbusAddr[0][1], tx.dbusAddr[0][0])
- if tx.dbusSystem {
- verbose.Printf("system bus proxy on '%s' for upstream '%s'\n", tx.dbusAddr[1][1], tx.dbusAddr[1][0])
- }
- if err := tx.startDBus(); err != nil {
- return (*DBusStartError)(wrapError(err, "cannot start message bus proxy:", err))
- } else {
- txp.dbus = tx.dbus
- txp.dbusAddr = tx.dbusAddr
- txp.dbusSystem = tx.dbusSystem
- txp.dbusWait = tx.dbusWait
- }
- }
-
- // apply ACLs
- for _, e := range tx.acl {
- verbose.Println("applying ACL", e, "uid:", tx.Uid, "tag:", e.ts(), "path:", e.path)
- if err := acl.UpdatePerm(e.path, tx.uid, e.perms...); err != nil {
- return (*ACLUpdateError)(wrapError(err,
- fmt.Sprintf("cannot apply ACL to '%s': %s", e.path, err)))
- } else {
- // register partial commit
- txp.updatePermTag(e.tag, e.path, e.perms...)
- }
- }
-
- // disarm partial commit rollback
- txp = nil
-
- // queue tmpfs at the end of tx.bwrap.Filesystem
- for _, dest := range tx.tmpfs {
- tx.bwrap.Tmpfs(dest, 8*1024)
- }
-
- return nil
-}
-
-// revert rolls back recorded actions
-// order: acl, dbus, hardlinks, tmpfiles, files, mkdir, xhost
-// errors are printed but not treated as fatal
-func (tx *appSealTx) revert(tags *state.Enablements) error {
- if tx.closed {
- panic("seal transaction reverted twice")
- }
- tx.closed = true
-
- // will be slightly over-sized with ephemeral dirs
- errs := make([]error, 0, len(tx.acl)+1+len(tx.tmpfiles)+len(tx.mkdir)+len(tx.xhost))
- joinError := func(err error, a ...any) {
- var e error
- if err != nil {
- e = wrapError(err, a...)
- }
- errs = append(errs, e)
- }
-
- // revert ACLs
- for _, e := range tx.acl {
- if tags.Has(e.tag) {
- verbose.Println("stripping ACL", e, "uid:", tx.Uid, "tag:", e.ts(), "path:", e.path)
- err := acl.UpdatePerm(e.path, tx.uid)
- joinError(err, fmt.Sprintf("cannot strip ACL entry from '%s': %s", e.path, err))
- } else {
- verbose.Println("skipping ACL", e, "uid:", tx.Uid, "tag:", e.ts(), "path:", e.path)
- }
- }
-
- if tx.dbus != nil {
- // stop dbus proxy
- verbose.Println("terminating message bus proxy")
- err := tx.stopDBus()
- joinError(err, "cannot stop message bus proxy:", err)
- }
-
- // remove hardlinks
- for _, link := range tx.hardlinks {
- verbose.Println("removing hardlink", link[1])
- err := os.Remove(link[1])
- joinError(err, fmt.Sprintf("cannot remove hardlink '%s': %s", link[1], err))
- }
-
- // remove tmpfiles
- for _, tmpfile := range tx.tmpfiles {
- verbose.Println("removing tmpfile", tmpfile[0])
- err := os.Remove(tmpfile[0])
- joinError(err, fmt.Sprintf("cannot remove tmpfile '%s': %s", tmpfile[0], err))
- }
-
- // remove files
- for _, file := range tx.files {
- verbose.Println("removing file", file[0])
- err := os.Remove(file[0])
- joinError(err, fmt.Sprintf("cannot remove file '%s': %s", file[0], err))
- }
-
- // remove (empty) ephemeral directories
- for i := len(tx.mkdir); i > 0; i-- {
- dir := tx.mkdir[i-1]
- if !dir.remove {
- continue
- }
-
- verbose.Println("destroying ephemeral directory mode:", dir.perm.String(), "path:", dir.path)
- err := os.Remove(dir.path)
- joinError(err, fmt.Sprintf("cannot remove ephemeral directory '%s': %s", dir.path, err))
- }
-
- if tags.Has(state.EnableX) {
- // rollback xhost insertions
- for _, username := range tx.xhost {
- verbose.Printf("deleting XHost entry SI:localuser:%s\n", username)
- err := xcb.ChangeHosts(xcb.HostModeDelete, xcb.FamilyServerInterpreted, "localuser\x00"+username)
- joinError(err, "cannot remove XHost entry:", err)
- }
- }
-
- return errors.Join(errs...)
-}
-
// shareAll calls all share methods in sequence
func (seal *appSeal) shareAll(bus [2]*dbus.Config) error {
if seal.shared {
@@ -432,12 +94,11 @@ func (seal *appSeal) shareAll(bus [2]*dbus.Config) error {
if err := seal.shareDBus(bus); err != nil {
return err
- } else if seal.sys.dbusAddr != nil { // set if D-Bus enabled and share successful
- verbose.Println("sealed session proxy", bus[0].Args(seal.sys.dbusAddr[0]))
- if bus[1] != nil {
- verbose.Println("sealed system proxy", bus[1].Args(seal.sys.dbusAddr[1]))
- }
- verbose.Println("message bus proxy final args:", seal.sys.dbus)
+ }
+
+ // queue overriding tmpfs at the end of seal.sys.bwrap.Filesystem
+ for _, dest := range seal.sys.override {
+ seal.sys.bwrap.Tmpfs(dest, 8*1024)
}
return nil