app: tag ACL operations for revert

ACL operations are now tagged with the enablement causing them. At the end of child process's life, enablements of all remaining launchers are resolved and inverted. This allows Wait to only revert operations targeting resources no longer required by other launchers.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
This commit is contained in:
Ophestra Umiker 2024-10-10 14:33:58 +09:00
parent 86cb5ac1db
commit a3aadd4146
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
4 changed files with 72 additions and 28 deletions

View File

@ -42,7 +42,7 @@ func (seal *appSeal) shareDisplay() error {
seal.appendEnv(waylandDisplay, wpi)
// ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`)
seal.sys.updatePerm(wp, acl.Read, acl.Write, acl.Execute)
seal.sys.updatePermTag(state.EnableWayland, wp, acl.Read, acl.Write, acl.Execute)
}
}

View File

@ -4,6 +4,7 @@ import (
"path"
"git.ophivana.moe/cat/fortify/acl"
"git.ophivana.moe/cat/fortify/internal/state"
)
const (
@ -16,10 +17,10 @@ const (
func (seal *appSeal) shareRuntime() {
// ensure RunDir (e.g. `/run/user/%d/fortify`)
seal.sys.ensure(seal.RunDirPath, 0700)
seal.sys.updatePerm(seal.RunDirPath, acl.Execute)
seal.sys.updatePermTag(state.EnableLength, seal.RunDirPath, acl.Execute)
// ensure runtime directory ACL (e.g. `/run/user/%d`)
seal.sys.updatePerm(seal.RuntimePath, acl.Execute)
seal.sys.updatePermTag(state.EnableLength, seal.RuntimePath, acl.Execute)
// ensure Share (e.g. `/tmp/fortify.%d`)
// acl is unnecessary as this directory is world executable
@ -40,12 +41,12 @@ 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)
seal.sys.updatePermTag(state.EnableLength, 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)
seal.sys.updatePermTag(state.EnableLength, targetRuntime, acl.Read, acl.Write, acl.Execute)
// point to ensured runtime path
seal.appendEnv(xdgRuntimeDir, targetRuntime)

View File

@ -155,24 +155,42 @@ func (a *app) Wait() (int, error) {
return err
}
var global bool
// measure remaining state entries
if l, err := b.Len(); err != nil {
// enablements of remaining launchers
rt, tags := new(state.Enablements), new(state.Enablements)
tags.Set(state.EnableLength + 1)
if states, err := b.Load(); 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 {
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)
} else {
verbose.Printf("found %d active launchers, cleaning up without globals\n", l)
}
// accumulate capabilities of other launchers
for _, s := range states {
*rt |= s.Capability
}
}
// invert accumulated enablements for cleanup
for i := state.Enablement(0); i < state.EnableLength; i++ {
if !rt.Has(i) {
tags.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)
}
}
verbose.Println("will revert operations tagged", ct, "as no remaining launchers hold these enablements")
}
// FIXME: depending on exit sequence, some parts of the transaction never gets reverted
if err := a.seal.sys.revert(global); err != nil {
if err := a.seal.sys.revert(tags); err != nil {
return err.(RevertCompoundError)
}

View File

@ -112,10 +112,22 @@ func (tx *appSealTx) ensureEphemeral(path string, perm os.FileMode) {
// 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 {
@ -131,9 +143,16 @@ func (e *appACLEntry) String() string {
return string(s)
}
// updatePerm appends an acl update action
// updatePerm appends an untagged acl update action
func (tx *appSealTx) updatePerm(path string, perms ...acl.Perm) {
tx.acl = append(tx.acl, &appACLEntry{path, perms})
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
@ -175,7 +194,11 @@ func (tx *appSealTx) commit() error {
// 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 {
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)
}
}
@ -250,13 +273,13 @@ func (tx *appSealTx) commit() error {
// apply ACLs
for _, e := range tx.acl {
verbose.Println("applying ACL", e, "uid:", tx.Uid, "path:", e.path)
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.updatePerm(e.path, e.perms...)
txp.updatePermTag(e.tag, e.path, e.perms...)
}
}
@ -268,7 +291,7 @@ func (tx *appSealTx) commit() error {
// revert rolls back recorded actions
// order: acl, dbus, hardlinks, tmpfiles, mkdir, xhost
// errors are printed but not treated as fatal
func (tx *appSealTx) revert(global bool) error {
func (tx *appSealTx) revert(tags *state.Enablements) error {
if tx.closed {
panic("seal transaction reverted twice")
}
@ -284,12 +307,14 @@ func (tx *appSealTx) revert(global bool) error {
errs = append(errs, e)
}
if global {
// revert ACLs
for _, e := range tx.acl {
verbose.Println("stripping ACL", e, "uid:", tx.Uid, "path:", e.path)
// 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)
}
}
@ -326,7 +351,7 @@ func (tx *appSealTx) revert(global bool) error {
joinError(err, fmt.Sprintf("cannot remove ephemeral directory '%s': %s", dir.path, err))
}
if global {
if tags.Has(state.EnableX) {
// rollback xhost insertions
for _, username := range tx.xhost {
verbose.Printf("deleting XHost entry SI:localuser:%s\n", username)