diff options
Diffstat (limited to 'internal/app/system.go')
| -rw-r--r-- | internal/app/system.go | 365 |
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 |
