aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/system
diff options
context:
space:
mode:
Diffstat (limited to 'internal/system')
-rw-r--r--internal/system/acl.go78
-rw-r--r--internal/system/dbus.go159
-rw-r--r--internal/system/mkdir.go82
-rw-r--r--internal/system/op.go126
-rw-r--r--internal/system/tmpfiles.go141
-rw-r--r--internal/system/xhost.go54
6 files changed, 640 insertions, 0 deletions
diff --git a/internal/system/acl.go b/internal/system/acl.go
new file mode 100644
index 00000000..9c89752b
--- /dev/null
+++ b/internal/system/acl.go
@@ -0,0 +1,78 @@
+package system
+
+import (
+ "fmt"
+ "slices"
+
+ "git.ophivana.moe/cat/fortify/acl"
+ "git.ophivana.moe/cat/fortify/internal/fmsg"
+ "git.ophivana.moe/cat/fortify/internal/state"
+ "git.ophivana.moe/cat/fortify/internal/verbose"
+)
+
+// UpdatePerm appends an ephemeral acl update Op.
+func (sys *I) UpdatePerm(path string, perms ...acl.Perm) {
+ sys.UpdatePermType(Process, path, perms...)
+}
+
+// UpdatePermType appends an acl update Op.
+func (sys *I) UpdatePermType(et state.Enablement, path string, perms ...acl.Perm) {
+ sys.lock.Lock()
+ defer sys.lock.Unlock()
+
+ sys.ops = append(sys.ops, &ACL{et, path, perms})
+}
+
+type ACL struct {
+ et state.Enablement
+ path string
+ perms []acl.Perm
+}
+
+func (a *ACL) Type() state.Enablement {
+ return a.et
+}
+
+func (a *ACL) apply(sys *I) error {
+ verbose.Println("applying ACL", a, "uid:", sys.uid, "type:", TypeString(a.et), "path:", a.path)
+ return fmsg.WrapErrorSuffix(acl.UpdatePerm(a.path, sys.uid, a.perms...),
+ fmt.Sprintf("cannot apply ACL entry to %q:", a.path))
+}
+
+func (a *ACL) revert(sys *I, ec *Criteria) error {
+ if ec.hasType(a) {
+ verbose.Println("stripping ACL", a, "uid:", sys.uid, "type:", TypeString(a.et), "path:", a.path)
+ return fmsg.WrapErrorSuffix(acl.UpdatePerm(a.path, sys.uid),
+ fmt.Sprintf("cannot strip ACL entry from %q:", a.path))
+ } else {
+ verbose.Println("skipping ACL", a, "uid:", sys.uid, "tag:", TypeString(a.et), "path:", a.path)
+ return nil
+ }
+}
+
+func (a *ACL) Is(o Op) bool {
+ a0, ok := o.(*ACL)
+ return ok && a0 != nil &&
+ a.et == a0.et &&
+ a.path == a0.path &&
+ slices.Equal(a.perms, a0.perms)
+}
+
+func (a *ACL) Path() string {
+ return a.path
+}
+
+func (a *ACL) String() string {
+ var s = []byte("---")
+ for _, p := range a.perms {
+ switch p {
+ case acl.Read:
+ s[0] = 'r'
+ case acl.Write:
+ s[1] = 'w'
+ case acl.Execute:
+ s[2] = 'x'
+ }
+ }
+ return string(s)
+}
diff --git a/internal/system/dbus.go b/internal/system/dbus.go
new file mode 100644
index 00000000..3e4a6e65
--- /dev/null
+++ b/internal/system/dbus.go
@@ -0,0 +1,159 @@
+package system
+
+import (
+ "errors"
+ "fmt"
+ "os"
+
+ "git.ophivana.moe/cat/fortify/dbus"
+ "git.ophivana.moe/cat/fortify/internal/fmsg"
+ "git.ophivana.moe/cat/fortify/internal/state"
+ "git.ophivana.moe/cat/fortify/internal/verbose"
+)
+
+var (
+ ErrDBusConfig = errors.New("dbus config not supplied")
+)
+
+func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath string) error {
+ d := new(DBus)
+
+ // used by waiting goroutine to notify process exit
+ d.done = make(chan struct{})
+
+ // session bus is mandatory
+ if session == nil {
+ return fmsg.WrapError(ErrDBusConfig,
+ "attempted to seal message bus proxy without session bus config")
+ }
+
+ // system bus is optional
+ d.system = system == nil
+
+ // upstream address, downstream socket path
+ var sessionBus, systemBus [2]string
+
+ // resolve upstream bus addresses
+ sessionBus[0], systemBus[0] = dbus.Address()
+
+ // set paths from caller
+ sessionBus[1], systemBus[1] = sessionPath, systemPath
+
+ // create proxy instance
+ d.proxy = dbus.New(sessionBus, systemBus)
+
+ defer func() {
+ if verbose.Get() && d.proxy.Sealed() {
+ verbose.Println("sealed session proxy", session.Args(sessionBus))
+ if system != nil {
+ verbose.Println("sealed system proxy", system.Args(systemBus))
+ }
+ verbose.Println("message bus proxy final args:", d.proxy)
+ }
+ }()
+
+ // queue operation
+ sys.ops = append(sys.ops, d)
+
+ // seal dbus proxy
+ return fmsg.WrapErrorSuffix(d.proxy.Seal(session, system),
+ "cannot seal message bus proxy:")
+}
+
+type DBus struct {
+ proxy *dbus.Proxy
+
+ // whether system bus proxy is enabled
+ system bool
+ // notification from goroutine waiting for dbus.Proxy
+ done chan struct{}
+}
+
+func (d *DBus) Type() state.Enablement {
+ return Process
+}
+
+func (d *DBus) apply(_ *I) error {
+ verbose.Printf("session bus proxy on %q for upstream %q\n", d.proxy.Session()[1], d.proxy.Session()[0])
+ if d.system {
+ verbose.Printf("system bus proxy on %q for upstream %q\n", d.proxy.System()[1], d.proxy.System()[0])
+ }
+
+ // ready channel passed to dbus package
+ ready := make(chan error, 1)
+
+ // background dbus proxy start
+ if err := d.proxy.Start(ready, os.Stderr, true); err != nil {
+ return fmsg.WrapErrorSuffix(err,
+ "cannot start message bus proxy:")
+ }
+ verbose.Println("starting message bus proxy:", d.proxy)
+ if verbose.Get() { // save the extra bwrap arg build when verbose logging is off
+ verbose.Println("message bus proxy bwrap args:", d.proxy.Bwrap())
+ }
+
+ // background wait for proxy instance and notify completion
+ go func() {
+ if err := d.proxy.Wait(); err != nil {
+ fmt.Println("fortify: message bus proxy exited with error:", err)
+ go func() { ready <- err }()
+ } else {
+ verbose.Println("message bus proxy exit")
+ }
+
+ // ensure socket removal so ephemeral directory is empty at revert
+ if err := os.Remove(d.proxy.Session()[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
+ fmt.Println("fortify: cannot remove dangling session bus socket:", err)
+ }
+ if d.system {
+ if err := os.Remove(d.proxy.System()[1]); err != nil && !errors.Is(err, os.ErrNotExist) {
+ fmt.Println("fortify: cannot remove dangling system bus socket:", err)
+ }
+ }
+
+ // notify proxy completion
+ close(d.done)
+ }()
+
+ // ready is not nil if the proxy process faulted
+ if err := <-ready; err != nil {
+ // note that err here is either an I/O error or a predetermined unexpected behaviour error
+ return fmsg.WrapErrorSuffix(err,
+ "message bus proxy fault after start:")
+ }
+ verbose.Println("message bus proxy ready")
+
+ return nil
+}
+
+func (d *DBus) revert(_ *I, _ *Criteria) error {
+ // criteria ignored here since dbus is always process-scoped
+ verbose.Println("terminating message bus proxy")
+
+ if err := d.proxy.Close(); err != nil {
+ if errors.Is(err, os.ErrClosed) {
+ return fmsg.WrapError(err,
+ "message bus proxy already closed")
+ } else {
+ return fmsg.WrapErrorSuffix(err,
+ "cannot stop message bus proxy:")
+ }
+ }
+
+ // block until proxy wait returns
+ <-d.done
+ return nil
+}
+
+func (d *DBus) Is(o Op) bool {
+ d0, ok := o.(*DBus)
+ return ok && d0 != nil && *d == *d0
+}
+
+func (d *DBus) Path() string {
+ return "(dbus proxy)"
+}
+
+func (d *DBus) String() string {
+ return d.proxy.String()
+}
diff --git a/internal/system/mkdir.go b/internal/system/mkdir.go
new file mode 100644
index 00000000..2c303630
--- /dev/null
+++ b/internal/system/mkdir.go
@@ -0,0 +1,82 @@
+package system
+
+import (
+ "errors"
+ "fmt"
+ "os"
+
+ "git.ophivana.moe/cat/fortify/internal/fmsg"
+ "git.ophivana.moe/cat/fortify/internal/state"
+ "git.ophivana.moe/cat/fortify/internal/verbose"
+)
+
+// Ensure the existence and mode of a directory.
+func (sys *I) Ensure(name string, perm os.FileMode) {
+ sys.lock.Lock()
+ defer sys.lock.Unlock()
+
+ sys.ops = append(sys.ops, &Mkdir{User, name, perm, false})
+}
+
+// Ephemeral ensures the temporary existence and mode of a directory through the life of et.
+func (sys *I) Ephemeral(et state.Enablement, name string, perm os.FileMode) {
+ sys.lock.Lock()
+ defer sys.lock.Unlock()
+
+ sys.ops = append(sys.ops, &Mkdir{et, name, perm, true})
+}
+
+type Mkdir struct {
+ et state.Enablement
+ path string
+ perm os.FileMode
+ ephemeral bool
+}
+
+func (m *Mkdir) Type() state.Enablement {
+ return m.et
+}
+
+func (m *Mkdir) apply(_ *I) error {
+ verbose.Println("ensuring directory", m)
+
+ // create directory
+ err := os.Mkdir(m.path, m.perm)
+ if !errors.Is(err, os.ErrExist) {
+ return fmsg.WrapErrorSuffix(err,
+ fmt.Sprintf("cannot create directory %q:", m.path))
+ }
+
+ // directory exists, ensure mode
+ return fmsg.WrapErrorSuffix(os.Chmod(m.path, m.perm),
+ fmt.Sprintf("cannot change mode of %q to %s:", m.path, m.perm))
+}
+
+func (m *Mkdir) revert(_ *I, ec *Criteria) error {
+ if !m.ephemeral {
+ // skip non-ephemeral dir and do not log anything
+ return nil
+ }
+
+ if ec.hasType(m) {
+ verbose.Println("destroying ephemeral directory", m)
+ return fmsg.WrapErrorSuffix(os.Remove(m.path),
+ fmt.Sprintf("cannot remove ephemeral directory %q:", m.path))
+ } else {
+ verbose.Println("skipping ephemeral directory", m)
+ return nil
+ }
+}
+
+func (m *Mkdir) Is(o Op) bool {
+ m0, ok := o.(*Mkdir)
+ return ok && m0 != nil && *m == *m0
+}
+
+func (m *Mkdir) Path() string {
+ return m.path
+}
+
+func (m *Mkdir) String() string {
+ return fmt.Sprintf("mode: %s path: %q", m.perm.String(), m.path)
+}
diff --git a/internal/system/op.go b/internal/system/op.go
new file mode 100644
index 00000000..c3f93cdb
--- /dev/null
+++ b/internal/system/op.go
@@ -0,0 +1,126 @@
+package system
+
+import (
+ "errors"
+ "fmt"
+ "sync"
+
+ "git.ophivana.moe/cat/fortify/internal/state"
+)
+
+const (
+ // Process type is unconditionally reverted on exit.
+ Process = state.EnableLength + 1
+ // User type is reverted at final launcher exit.
+ User = state.EnableLength
+)
+
+type Criteria struct {
+ *state.Enablements
+}
+
+func (ec *Criteria) hasType(o Op) bool {
+ // nil criteria: revert everything except User
+ if ec.Enablements == nil {
+ return o.Type() != User
+ }
+
+ return ec.Has(o.Type())
+}
+
+// Op is a reversible system operation.
+type Op interface {
+ // Type returns Op's enablement type.
+ Type() state.Enablement
+
+ // apply the Op
+ apply(sys *I) error
+ // revert reverses the Op if criteria is met
+ revert(sys *I, ec *Criteria) error
+
+ Is(o Op) bool
+ Path() string
+ String() string
+}
+
+func TypeString(e state.Enablement) string {
+ switch e {
+ case User:
+ return "User"
+ case Process:
+ return "Process"
+ default:
+ return e.String()
+ }
+}
+
+type I struct {
+ uid int
+ ops []Op
+
+ state [2]bool
+ lock sync.Mutex
+}
+
+func (sys *I) UID() int {
+ return sys.uid
+}
+
+func (sys *I) Commit() error {
+ sys.lock.Lock()
+ defer sys.lock.Unlock()
+
+ if sys.state[0] {
+ panic("sys instance committed twice")
+ }
+ sys.state[0] = true
+
+ sp := New(sys.uid)
+ sp.ops = make([]Op, 0, len(sys.ops)) // prevent copies during commits
+ defer func() {
+ // sp is set to nil when all ops are applied
+ if sp != nil {
+ // rollback partial commit
+ if err := sp.Revert(&Criteria{nil}); err != nil {
+ fmt.Println("fortify: errors returned reverting partial commit:", err)
+ }
+ }
+ }()
+
+ for _, o := range sys.ops {
+ if err := o.apply(sys); err != nil {
+ return err
+ } else {
+ // register partial commit
+ sp.ops = append(sp.ops, o)
+ }
+ }
+
+ // disarm partial commit rollback
+ sp = nil
+ return nil
+}
+
+func (sys *I) Revert(ec *Criteria) error {
+ sys.lock.Lock()
+ defer sys.lock.Unlock()
+
+ if sys.state[1] {
+ panic("sys instance reverted twice")
+ }
+ sys.state[1] = true
+
+ // collect errors
+ errs := make([]error, len(sys.ops))
+
+ for i := range sys.ops {
+ errs[i] = sys.ops[len(sys.ops)-i-1].revert(sys, ec)
+ }
+
+ // errors.Join filters nils
+ return errors.Join(errs...)
+}
+
+func New(uid int) *I {
+ return &I{uid: uid}
+}
diff --git a/internal/system/tmpfiles.go b/internal/system/tmpfiles.go
new file mode 100644
index 00000000..2a9f724a
--- /dev/null
+++ b/internal/system/tmpfiles.go
@@ -0,0 +1,141 @@
+package system
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "strconv"
+
+ "git.ophivana.moe/cat/fortify/acl"
+ "git.ophivana.moe/cat/fortify/internal/fmsg"
+ "git.ophivana.moe/cat/fortify/internal/state"
+ "git.ophivana.moe/cat/fortify/internal/verbose"
+)
+
+// CopyFile registers an Op that copies path dst from src.
+func (sys *I) CopyFile(dst, src string) {
+ sys.CopyFileType(Process, dst, src)
+}
+
+// CopyFileType registers a file copying Op labelled with type et.
+func (sys *I) CopyFileType(et state.Enablement, dst, src string) {
+ sys.lock.Lock()
+ sys.ops = append(sys.ops, &Tmpfile{et, tmpfileCopy, dst, src})
+ sys.lock.Unlock()
+
+ sys.UpdatePermType(et, dst, acl.Read)
+}
+
+// Link registers an Op that links dst to src.
+func (sys *I) Link(oldname, newname string) {
+ sys.LinkFileType(Process, oldname, newname)
+}
+
+// LinkFileType registers a file linking Op labelled with type et.
+func (sys *I) LinkFileType(et state.Enablement, oldname, newname string) {
+ sys.lock.Lock()
+ defer sys.lock.Unlock()
+
+ sys.ops = append(sys.ops, &Tmpfile{et, tmpfileLink, newname, oldname})
+}
+
+// Write registers an Op that writes dst with the contents of src.
+func (sys *I) Write(dst, src string) {
+ sys.WriteType(Process, dst, src)
+}
+
+// WriteType registers a file writing Op labelled with type et.
+func (sys *I) WriteType(et state.Enablement, dst, src string) {
+ sys.lock.Lock()
+ sys.ops = append(sys.ops, &Tmpfile{et, tmpfileWrite, dst, src})
+ sys.lock.Unlock()
+
+ sys.UpdatePermType(et, dst, acl.Read)
+}
+
+const (
+ tmpfileCopy uint8 = iota
+ tmpfileLink
+ tmpfileWrite
+)
+
+type Tmpfile struct {
+ et state.Enablement
+ method uint8
+ dst, src string
+}
+
+func (t *Tmpfile) Type() state.Enablement {
+ return t.et
+}
+
+func (t *Tmpfile) apply(_ *I) error {
+ switch t.method {
+ case tmpfileCopy:
+ verbose.Printf("publishing tmpfile %s\n", t)
+ return fmsg.WrapErrorSuffix(copyFile(t.dst, t.src),
+ fmt.Sprintf("cannot copy tmpfile %q:", t.dst))
+ case tmpfileLink:
+ verbose.Printf("linking tmpfile %s\n", t)
+ return fmsg.WrapErrorSuffix(os.Link(t.src, t.dst),
+ fmt.Sprintf("cannot link tmpfile %q:", t.dst))
+ case tmpfileWrite:
+ verbose.Printf("writing %s\n", t)
+ return fmsg.WrapErrorSuffix(os.WriteFile(t.dst, []byte(t.src), 0600),
+ fmt.Sprintf("cannot write tmpfile %q:", t.dst))
+ default:
+ panic("invalid tmpfile method " + strconv.Itoa(int(t.method)))
+ }
+}
+
+func (t *Tmpfile) revert(_ *I, ec *Criteria) error {
+ if ec.hasType(t) {
+ verbose.Printf("removing tmpfile %q\n", t.dst)
+ return fmsg.WrapErrorSuffix(os.Remove(t.dst),
+ fmt.Sprintf("cannot remove tmpfile %q:", t.dst))
+ } else {
+ verbose.Printf("skipping tmpfile %q\n", t.dst)
+ return nil
+ }
+}
+
+func (t *Tmpfile) Is(o Op) bool {
+ t0, ok := o.(*Tmpfile)
+ return ok && t0 != nil && *t == *t0
+}
+
+func (t *Tmpfile) Path() string {
+ if t.method == tmpfileWrite {
+ return fmt.Sprintf("(%d bytes of data)", len(t.src))
+ }
+ return t.src
+}
+
+func (t *Tmpfile) String() string {
+ switch t.method {
+ case tmpfileCopy:
+ return fmt.Sprintf("%q from %q", t.dst, t.src)
+ case tmpfileLink:
+ return fmt.Sprintf("%q from %q", t.dst, t.src)
+ case tmpfileWrite:
+ return fmt.Sprintf("%d bytes of data to %q", len(t.src), t.dst)
+ default:
+ panic("invalid tmpfile method " + strconv.Itoa(int(t.method)))
+ }
+}
+
+func copyFile(dst, src string) error {
+ dstD, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
+ if err != nil {
+ return err
+ }
+
+ srcD, err := os.Open(src)
+ if err != nil {
+ return errors.Join(err, dstD.Close())
+ }
+
+ _, err = io.Copy(dstD, srcD)
+ return errors.Join(err, dstD.Close(), srcD.Close())
+}
diff --git a/internal/system/xhost.go b/internal/system/xhost.go
new file mode 100644
index 00000000..2cdddd56
--- /dev/null
+++ b/internal/system/xhost.go
@@ -0,0 +1,54 @@
+package system
+
+import (
+ "fmt"
+
+ "git.ophivana.moe/cat/fortify/internal/fmsg"
+ "git.ophivana.moe/cat/fortify/internal/state"
+ "git.ophivana.moe/cat/fortify/internal/verbose"
+ "git.ophivana.moe/cat/fortify/xcb"
+)
+
+// ChangeHosts appends an X11 ChangeHosts command Op.
+func (sys *I) ChangeHosts(username string) {
+ sys.lock.Lock()
+ defer sys.lock.Unlock()
+
+ sys.ops = append(sys.ops, XHost(username))
+}
+
+type XHost string
+
+func (x XHost) Type() state.Enablement {
+ return state.EnableX
+}
+
+func (x XHost) apply(_ *I) error {
+ verbose.Printf("inserting entry %s to X11\n", x)
+ return fmsg.WrapErrorSuffix(xcb.ChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)),
+ fmt.Sprintf("cannot insert entry %s to X11:", x))
+}
+
+func (x XHost) revert(_ *I, ec *Criteria) error {
+ if ec.hasType(x) {
+ verbose.Printf("deleting entry %s from X11\n", x)
+ return fmsg.WrapErrorSuffix(xcb.ChangeHosts(xcb.HostModeDelete, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)),
+ fmt.Sprintf("cannot delete entry %s from X11:", x))
+ } else {
+ verbose.Printf("skipping entry %s in X11\n", x)
+ return nil
+ }
+}
+
+func (x XHost) Is(o Op) bool {
+ x0, ok := o.(XHost)
+ return ok && x == x0
+}
+
+func (x XHost) Path() string {
+ return string(x)
+}
+
+func (x XHost) String() string {
+ return string("SI:localuser:" + x)
+}