diff options
| author | Ophestra <cat@gensokyo.uk> | 2025-02-17 19:00:43 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2025-02-17 19:00:43 +0900 |
| commit | 90cb01b2748550228d0bc179691a19ebb996fc36 (patch) | |
| tree | 06a2e17b578312c3f0734bf4c476e3c2eb5f6908 /internal/system | |
| parent | b1e1d5627e6532ec719c761846d7f38fe93fcadd (diff) | |
system: move out of internal
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/system')
| -rw-r--r-- | internal/system/acl.go | 65 | ||||
| -rw-r--r-- | internal/system/acl_test.go | 90 | ||||
| -rw-r--r-- | internal/system/dbus.go | 148 | ||||
| -rw-r--r-- | internal/system/enablement.go | 68 | ||||
| -rw-r--r-- | internal/system/link.go | 47 | ||||
| -rw-r--r-- | internal/system/mkdir.go | 86 | ||||
| -rw-r--r-- | internal/system/mkdir_test.go | 73 | ||||
| -rw-r--r-- | internal/system/op.go | 162 | ||||
| -rw-r--r-- | internal/system/op_internal_test.go | 79 | ||||
| -rw-r--r-- | internal/system/op_test.go | 129 | ||||
| -rw-r--r-- | internal/system/tmpfiles.go | 76 | ||||
| -rw-r--r-- | internal/system/tmpfiles_test.go | 81 | ||||
| -rw-r--r-- | internal/system/wayland.go | 89 | ||||
| -rw-r--r-- | internal/system/xhost.go | 53 | ||||
| -rw-r--r-- | internal/system/xhost_test.go | 34 |
15 files changed, 0 insertions, 1280 deletions
diff --git a/internal/system/acl.go b/internal/system/acl.go deleted file mode 100644 index 840c3971..00000000 --- a/internal/system/acl.go +++ /dev/null @@ -1,65 +0,0 @@ -package system - -import ( - "fmt" - "slices" - - "git.gensokyo.uk/security/fortify/acl" -) - -// UpdatePerm appends an ephemeral acl update Op. -func (sys *I) UpdatePerm(path string, perms ...acl.Perm) *I { - sys.UpdatePermType(Process, path, perms...) - - return sys -} - -// UpdatePermType appends an acl update Op. -func (sys *I) UpdatePermType(et Enablement, path string, perms ...acl.Perm) *I { - sys.lock.Lock() - defer sys.lock.Unlock() - - sys.ops = append(sys.ops, &ACL{et, path, perms}) - - return sys -} - -type ACL struct { - et Enablement - path string - perms acl.Perms -} - -func (a *ACL) Type() Enablement { return a.et } - -func (a *ACL) apply(sys *I) error { - sys.println("applying ACL", a) - return sys.wrapErrSuffix(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) { - sys.println("stripping ACL", a) - return sys.wrapErrSuffix(acl.UpdatePerm(a.path, sys.uid), - fmt.Sprintf("cannot strip ACL entry from %q:", a.path)) - } else { - sys.println("skipping ACL", a) - 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 { - return fmt.Sprintf("%s type: %s path: %q", - a.perms, TypeString(a.et), a.path) -} diff --git a/internal/system/acl_test.go b/internal/system/acl_test.go deleted file mode 100644 index ed154ad1..00000000 --- a/internal/system/acl_test.go +++ /dev/null @@ -1,90 +0,0 @@ -package system - -import ( - "testing" - - "git.gensokyo.uk/security/fortify/acl" -) - -func TestUpdatePerm(t *testing.T) { - testCases := []struct { - path string - perms []acl.Perm - }{ - {"/run/user/1971/fortify", []acl.Perm{acl.Execute}}, - {"/tmp/fortify.1971/tmpdir/150", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, - } - - for _, tc := range testCases { - t.Run(tc.path+permSubTestSuffix(tc.perms), func(t *testing.T) { - sys := New(150) - sys.UpdatePerm(tc.path, tc.perms...) - (&tcOp{Process, tc.path}).test(t, sys.ops, []Op{&ACL{Process, tc.path, tc.perms}}, "UpdatePerm") - }) - } -} - -func TestUpdatePermType(t *testing.T) { - testCases := []struct { - perms []acl.Perm - tcOp - }{ - {[]acl.Perm{acl.Execute}, tcOp{User, "/tmp/fortify.1971/tmpdir"}}, - {[]acl.Perm{acl.Read, acl.Write, acl.Execute}, tcOp{User, "/tmp/fortify.1971/tmpdir/150"}}, - {[]acl.Perm{acl.Execute}, tcOp{Process, "/run/user/1971/fortify/fcb8a12f7c482d183ade8288c3de78b5"}}, - {[]acl.Perm{acl.Read}, tcOp{Process, "/tmp/fortify.1971/fcb8a12f7c482d183ade8288c3de78b5/passwd"}}, - {[]acl.Perm{acl.Read}, tcOp{Process, "/tmp/fortify.1971/fcb8a12f7c482d183ade8288c3de78b5/group"}}, - {[]acl.Perm{acl.Read, acl.Write, acl.Execute}, tcOp{EWayland, "/run/user/1971/wayland-0"}}, - } - - for _, tc := range testCases { - t.Run(tc.path+"_"+TypeString(tc.et)+permSubTestSuffix(tc.perms), func(t *testing.T) { - sys := New(150) - sys.UpdatePermType(tc.et, tc.path, tc.perms...) - tc.test(t, sys.ops, []Op{&ACL{tc.et, tc.path, tc.perms}}, "UpdatePermType") - }) - } -} - -func TestACL_String(t *testing.T) { - testCases := []struct { - want string - et Enablement - perms []acl.Perm - }{ - {`--- type: Process path: "/nonexistent"`, Process, []acl.Perm{}}, - {`r-- type: User path: "/nonexistent"`, User, []acl.Perm{acl.Read}}, - {`-w- type: Wayland path: "/nonexistent"`, EWayland, []acl.Perm{acl.Write}}, - {`--x type: X11 path: "/nonexistent"`, EX11, []acl.Perm{acl.Execute}}, - {`rw- type: D-Bus path: "/nonexistent"`, EDBus, []acl.Perm{acl.Read, acl.Write}}, - {`r-x type: PulseAudio path: "/nonexistent"`, EPulse, []acl.Perm{acl.Read, acl.Execute}}, - {`rwx type: User path: "/nonexistent"`, User, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, - {`rwx type: Process path: "/nonexistent"`, Process, []acl.Perm{acl.Read, acl.Write, acl.Write, acl.Execute}}, - } - - for _, tc := range testCases { - t.Run(tc.want, func(t *testing.T) { - a := &ACL{et: tc.et, perms: tc.perms, path: "/nonexistent"} - if got := a.String(); got != tc.want { - t.Errorf("String() = %v, want %v", - got, tc.want) - } - }) - } -} - -func permSubTestSuffix(perms []acl.Perm) (suffix string) { - for _, perm := range perms { - switch perm { - case acl.Read: - suffix += "_read" - case acl.Write: - suffix += "_write" - case acl.Execute: - suffix += "_execute" - default: - panic("unreachable") - } - } - return -} diff --git a/internal/system/dbus.go b/internal/system/dbus.go deleted file mode 100644 index cbb127ed..00000000 --- a/internal/system/dbus.go +++ /dev/null @@ -1,148 +0,0 @@ -package system - -import ( - "bytes" - "errors" - "log" - "strings" - "sync" - - "git.gensokyo.uk/security/fortify/dbus" -) - -var ( - ErrDBusConfig = errors.New("dbus config not supplied") -) - -func (sys *I) MustProxyDBus(sessionPath string, session *dbus.Config, systemPath string, system *dbus.Config) *I { - if _, err := sys.ProxyDBus(session, system, sessionPath, systemPath); err != nil { - panic(err.Error()) - } else { - return sys - } -} - -func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath string) (func(), error) { - d := new(DBus) - - // session bus is mandatory - if session == nil { - return nil, sys.wrapErr(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 sys.IsVerbose() && d.proxy.Sealed() { - sys.println("sealed session proxy", session.Args(sessionBus)) - if system != nil { - sys.println("sealed system proxy", system.Args(systemBus)) - } - sys.println("message bus proxy final args:", d.proxy) - } - }() - - // queue operation - sys.ops = append(sys.ops, d) - - // seal dbus proxy - d.out = &scanToFmsg{msg: new(strings.Builder)} - return d.out.Dump, sys.wrapErrSuffix(d.proxy.Seal(session, system), - "cannot seal message bus proxy:") -} - -type DBus struct { - proxy *dbus.Proxy - - out *scanToFmsg - // whether system bus proxy is enabled - system bool -} - -func (d *DBus) Type() Enablement { return Process } - -func (d *DBus) apply(sys *I) error { - sys.printf("session bus proxy on %q for upstream %q", d.proxy.Session()[1], d.proxy.Session()[0]) - if d.system { - sys.printf("system bus proxy on %q for upstream %q", d.proxy.System()[1], d.proxy.System()[0]) - } - - // this starts the process and blocks until ready - if err := d.proxy.Start(sys.ctx, d.out, true); err != nil { - d.out.Dump() - return sys.wrapErrSuffix(err, - "cannot start message bus proxy:") - } - sys.println("starting message bus proxy:", d.proxy) - return nil -} - -func (d *DBus) revert(sys *I, _ *Criteria) error { - // criteria ignored here since dbus is always process-scoped - sys.println("terminating message bus proxy") - d.proxy.Close() - defer sys.println("message bus proxy exit") - return sys.wrapErrSuffix(d.proxy.Wait(), "message bus proxy error:") -} - -func (d *DBus) Is(o Op) bool { - d0, ok := o.(*DBus) - return ok && d0 != nil && - ((d.proxy == nil && d0.proxy == nil) || - (d.proxy != nil && d0.proxy != nil && d.proxy.String() == d0.proxy.String())) -} - -func (d *DBus) Path() string { - return "(dbus proxy)" -} - -func (d *DBus) String() string { - return d.proxy.String() -} - -type scanToFmsg struct { - msg *strings.Builder - msgbuf []string - - mu sync.RWMutex -} - -func (s *scanToFmsg) Write(p []byte) (n int, err error) { - s.mu.Lock() - defer s.mu.Unlock() - return s.write(p, 0) -} - -func (s *scanToFmsg) write(p []byte, a int) (int, error) { - if i := bytes.IndexByte(p, '\n'); i == -1 { - n, _ := s.msg.Write(p) - return a + n, nil - } else { - n, _ := s.msg.Write(p[:i]) - s.msgbuf = append(s.msgbuf, s.msg.String()) - s.msg.Reset() - return s.write(p[i+1:], a+n+1) - } -} - -func (s *scanToFmsg) Dump() { - s.mu.RLock() - for _, msg := range s.msgbuf { - log.Println(msg) - } - s.mu.RUnlock() -} diff --git a/internal/system/enablement.go b/internal/system/enablement.go deleted file mode 100644 index f0911d54..00000000 --- a/internal/system/enablement.go +++ /dev/null @@ -1,68 +0,0 @@ -package system - -import ( - "strings" -) - -type ( - // Enablement represents an optional system resource - Enablement uint8 - // Enablements represents optional system resources to share - Enablements uint64 -) - -const ( - EWayland Enablement = iota - EX11 - EDBus - EPulse -) - -var enablementString = [...]string{ - EWayland: "Wayland", - EX11: "X11", - EDBus: "D-Bus", - EPulse: "PulseAudio", -} - -const ELen = len(enablementString) - -func (e Enablement) String() string { - if int(e) >= ELen { - return "<invalid enablement>" - } - return enablementString[e] -} - -func (e Enablement) Mask() Enablements { - return 1 << e -} - -// Has returns whether a feature is enabled -func (es *Enablements) Has(e Enablement) bool { - return *es&e.Mask() != 0 -} - -// Set enables a feature -func (es *Enablements) Set(e Enablement) { - if es.Has(e) { - panic("enablement " + e.String() + " set twice") - } - - *es |= e.Mask() -} - -func (es *Enablements) String() string { - buf := new(strings.Builder) - for i := Enablement(0); i < Enablement(ELen); i++ { - if es.Has(i) { - buf.WriteString(", " + i.String()) - } - } - - if buf.Len() == 0 { - buf.WriteString("(No enablements)") - } - - return strings.TrimPrefix(buf.String(), ", ") -} diff --git a/internal/system/link.go b/internal/system/link.go deleted file mode 100644 index 4182d560..00000000 --- a/internal/system/link.go +++ /dev/null @@ -1,47 +0,0 @@ -package system - -import ( - "fmt" - "os" -) - -// Link registers an Op that links dst to src. -func (sys *I) Link(oldname, newname string) *I { return sys.LinkFileType(Process, oldname, newname) } - -// LinkFileType registers a file linking Op labelled with type et. -func (sys *I) LinkFileType(et Enablement, oldname, newname string) *I { - sys.lock.Lock() - defer sys.lock.Unlock() - - sys.ops = append(sys.ops, &Hardlink{et, newname, oldname}) - - return sys -} - -type Hardlink struct { - et Enablement - dst, src string -} - -func (l *Hardlink) Type() Enablement { return l.et } - -func (l *Hardlink) apply(sys *I) error { - sys.println("linking", l) - return sys.wrapErrSuffix(os.Link(l.src, l.dst), - fmt.Sprintf("cannot link %q:", l.dst)) -} - -func (l *Hardlink) revert(sys *I, ec *Criteria) error { - if ec.hasType(l) { - sys.printf("removing hard link %q", l.dst) - return sys.wrapErrSuffix(os.Remove(l.dst), - fmt.Sprintf("cannot remove hard link %q:", l.dst)) - } else { - sys.printf("skipping hard link %q", l.dst) - return nil - } -} - -func (l *Hardlink) Is(o Op) bool { l0, ok := o.(*Hardlink); return ok && l0 != nil && *l == *l0 } -func (l *Hardlink) Path() string { return l.src } -func (l *Hardlink) String() string { return fmt.Sprintf("%q from %q", l.dst, l.src) } diff --git a/internal/system/mkdir.go b/internal/system/mkdir.go deleted file mode 100644 index 524289e5..00000000 --- a/internal/system/mkdir.go +++ /dev/null @@ -1,86 +0,0 @@ -package system - -import ( - "errors" - "fmt" - "os" -) - -// Ensure the existence and mode of a directory. -func (sys *I) Ensure(name string, perm os.FileMode) *I { - sys.lock.Lock() - defer sys.lock.Unlock() - - sys.ops = append(sys.ops, &Mkdir{User, name, perm, false}) - - return sys -} - -// Ephemeral ensures the temporary existence and mode of a directory through the life of et. -func (sys *I) Ephemeral(et Enablement, name string, perm os.FileMode) *I { - sys.lock.Lock() - defer sys.lock.Unlock() - - sys.ops = append(sys.ops, &Mkdir{et, name, perm, true}) - - return sys -} - -type Mkdir struct { - et Enablement - path string - perm os.FileMode - ephemeral bool -} - -func (m *Mkdir) Type() Enablement { - return m.et -} - -func (m *Mkdir) apply(sys *I) error { - sys.println("ensuring directory", m) - - // create directory - err := os.Mkdir(m.path, m.perm) - if !errors.Is(err, os.ErrExist) { - return sys.wrapErrSuffix(err, - fmt.Sprintf("cannot create directory %q:", m.path)) - } - - // directory exists, ensure mode - return sys.wrapErrSuffix(os.Chmod(m.path, m.perm), - fmt.Sprintf("cannot change mode of %q to %s:", m.path, m.perm)) -} - -func (m *Mkdir) revert(sys *I, ec *Criteria) error { - if !m.ephemeral { - // skip non-ephemeral dir and do not log anything - return nil - } - - if ec.hasType(m) { - sys.println("destroying ephemeral directory", m) - return sys.wrapErrSuffix(os.Remove(m.path), - fmt.Sprintf("cannot remove ephemeral directory %q:", m.path)) - } else { - sys.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 { - t := "Ensure" - if m.ephemeral { - t = TypeString(m.Type()) - } - return fmt.Sprintf("mode: %s type: %s path: %q", m.perm.String(), t, m.path) -} diff --git a/internal/system/mkdir_test.go b/internal/system/mkdir_test.go deleted file mode 100644 index 20826940..00000000 --- a/internal/system/mkdir_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package system - -import ( - "os" - "testing" -) - -func TestEnsure(t *testing.T) { - testCases := []struct { - name string - perm os.FileMode - }{ - {"/tmp/fortify.1971", 0701}, - {"/tmp/fortify.1971/tmpdir", 0700}, - {"/tmp/fortify.1971/tmpdir/150", 0700}, - {"/run/user/1971/fortify", 0700}, - } - for _, tc := range testCases { - t.Run(tc.name+"_"+tc.perm.String(), func(t *testing.T) { - sys := New(150) - sys.Ensure(tc.name, tc.perm) - (&tcOp{User, tc.name}).test(t, sys.ops, []Op{&Mkdir{User, tc.name, tc.perm, false}}, "Ensure") - }) - } -} - -func TestEphemeral(t *testing.T) { - testCases := []struct { - perm os.FileMode - tcOp - }{ - {0700, tcOp{Process, "/run/user/1971/fortify/ec07546a772a07cde87389afc84ffd13"}}, - {0701, tcOp{Process, "/tmp/fortify.1971/ec07546a772a07cde87389afc84ffd13"}}, - } - for _, tc := range testCases { - t.Run(tc.path+"_"+tc.perm.String()+"_"+TypeString(tc.et), func(t *testing.T) { - sys := New(150) - sys.Ephemeral(tc.et, tc.path, tc.perm) - tc.test(t, sys.ops, []Op{&Mkdir{tc.et, tc.path, tc.perm, true}}, "Ephemeral") - }) - } -} - -func TestMkdir_String(t *testing.T) { - testCases := []struct { - want string - ephemeral bool - et Enablement - }{ - {"Ensure", false, User}, - {"Ensure", false, Process}, - {"Ensure", false, EWayland}, - - {"Wayland", true, EWayland}, - {"X11", true, EX11}, - {"D-Bus", true, EDBus}, - {"PulseAudio", true, EPulse}, - } - for _, tc := range testCases { - t.Run(tc.want, func(t *testing.T) { - m := &Mkdir{ - et: tc.et, - path: "/nonexistent", - perm: 0701, - ephemeral: tc.ephemeral, - } - want := "mode: " + os.FileMode(0701).String() + " type: " + tc.want + " path: \"/nonexistent\"" - if got := m.String(); got != want { - t.Errorf("String() = %v, want %v", got, want) - } - }) - } -} diff --git a/internal/system/op.go b/internal/system/op.go deleted file mode 100644 index 0e421aec..00000000 --- a/internal/system/op.go +++ /dev/null @@ -1,162 +0,0 @@ -package system - -import ( - "context" - "errors" - "log" - "sync" -) - -const ( - // User type is reverted at final launcher exit. - User = Enablement(ELen) - // Process type is unconditionally reverted on exit. - Process = Enablement(ELen + 1) -) - -type Criteria struct { - *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() 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 Enablement) string { - switch e { - case User: - return "User" - case Process: - return "Process" - default: - return e.String() - } -} - -// New initialises sys with no-op verbose functions. -func New(uid int) (sys *I) { - sys = new(I) - sys.uid = uid - sys.IsVerbose = func() bool { return false } - sys.Verbose = func(...any) {} - sys.Verbosef = func(string, ...any) {} - sys.WrapErr = func(err error, _ ...any) error { return err } - return -} - -type I struct { - uid int - ops []Op - ctx context.Context - - IsVerbose func() bool - Verbose func(v ...any) - Verbosef func(format string, v ...any) - WrapErr func(err error, a ...any) error - - // whether sys has been reverted - state bool - - lock sync.Mutex -} - -func (sys *I) UID() int { return sys.uid } -func (sys *I) println(v ...any) { sys.Verbose(v...) } -func (sys *I) printf(format string, v ...any) { sys.Verbosef(format, v...) } -func (sys *I) wrapErr(err error, a ...any) error { return sys.WrapErr(err, a...) } -func (sys *I) wrapErrSuffix(err error, a ...any) error { - if err == nil { - return nil - } - return sys.wrapErr(err, append(a, err)...) -} - -func (sys *I) Equal(v *I) bool { - if v == nil || sys.uid != v.uid || len(sys.ops) != len(v.ops) { - return false - } - - for i, o := range sys.ops { - if !o.Is(v.ops[i]) { - return false - } - } - - return true -} - -func (sys *I) Commit(ctx context.Context) error { - sys.lock.Lock() - defer sys.lock.Unlock() - - if sys.ctx != nil { - panic("sys instance committed twice") - } - sys.ctx = ctx - - 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 - sys.printf("commit faulted after %d ops, rolling back partial commit", len(sp.ops)) - if err := sp.Revert(&Criteria{nil}); err != nil { - log.Println("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 { - panic("sys instance reverted twice") - } - sys.state = 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...) -} diff --git a/internal/system/op_internal_test.go b/internal/system/op_internal_test.go deleted file mode 100644 index 7ec4f174..00000000 --- a/internal/system/op_internal_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package system - -import "testing" - -type tcOp struct { - et Enablement - path string -} - -// test an instance of the Op interface -func (ptc tcOp) test(t *testing.T, gotOps []Op, wantOps []Op, fn string) { - if len(gotOps) != len(wantOps) { - t.Errorf("%s: inserted %v Ops, want %v", fn, - len(gotOps), len(wantOps)) - return - } - - t.Run("path", func(t *testing.T) { - if len(gotOps) > 0 { - if got := gotOps[0].Path(); got != ptc.path { - t.Errorf("Path() = %q, want %q", - got, ptc.path) - return - } - } - }) - - for i := range gotOps { - o := gotOps[i] - - t.Run("is", func(t *testing.T) { - if !o.Is(o) { - t.Errorf("Is returned false on self") - return - } - if !o.Is(wantOps[i]) { - t.Errorf("%s: inserted %#v, want %#v", - fn, - o, wantOps[i]) - return - } - }) - - t.Run("criteria", func(t *testing.T) { - testCases := []struct { - name string - ec *Criteria - want bool - }{ - {"nil", newCriteria(), ptc.et != User}, - {"self", newCriteria(ptc.et), true}, - {"all", newCriteria(EWayland, EX11, EDBus, EPulse, User, Process), true}, - {"enablements", newCriteria(EWayland, EX11, EDBus, EPulse), ptc.et != User && ptc.et != Process}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - if got := tc.ec.hasType(o); got != tc.want { - t.Errorf("hasType: got %v, want %v", - got, tc.want) - } - }) - } - }) - } -} - -func newCriteria(labels ...Enablement) *Criteria { - ec := new(Criteria) - if len(labels) == 0 { - return ec - } - - ec.Enablements = new(Enablements) - for _, e := range labels { - ec.Set(e) - } - return ec -} diff --git a/internal/system/op_test.go b/internal/system/op_test.go deleted file mode 100644 index 3e8c4d2d..00000000 --- a/internal/system/op_test.go +++ /dev/null @@ -1,129 +0,0 @@ -package system_test - -import ( - "strconv" - "testing" - - "git.gensokyo.uk/security/fortify/internal/system" -) - -func TestNew(t *testing.T) { - testCases := []struct { - uid int - }{ - {150}, - {149}, - {148}, - {147}, - } - - for _, tc := range testCases { - t.Run("sys initialised with uid "+strconv.Itoa(tc.uid), func(t *testing.T) { - if got := system.New(tc.uid); got.UID() != tc.uid { - t.Errorf("New(%d) uid = %d, want %d", - tc.uid, - got.UID(), tc.uid) - } - }) - } -} - -func TestTypeString(t *testing.T) { - testCases := []struct { - e system.Enablement - want string - }{ - {system.EWayland, system.EWayland.String()}, - {system.EX11, system.EX11.String()}, - {system.EDBus, system.EDBus.String()}, - {system.EPulse, system.EPulse.String()}, - {system.User, "User"}, - {system.Process, "Process"}, - } - - for _, tc := range testCases { - t.Run("label type string "+tc.want, func(t *testing.T) { - if got := system.TypeString(tc.e); got != tc.want { - t.Errorf("TypeString(%d) = %v, want %v", - tc.e, - got, tc.want) - } - }) - } -} - -func TestI_Equal(t *testing.T) { - testCases := []struct { - name string - sys *system.I - v *system.I - want bool - }{ - { - "simple UID", - system.New(150), - system.New(150), - true, - }, - { - "simple UID differ", - system.New(150), - system.New(151), - false, - }, - { - "simple UID nil", - system.New(150), - nil, - false, - }, - { - "op length mismatch", - system.New(150). - ChangeHosts("chronos"), - system.New(150). - ChangeHosts("chronos"). - Ensure("/run", 0755), - false, - }, - { - "op value mismatch", - system.New(150). - ChangeHosts("chronos"). - Ensure("/run", 0644), - system.New(150). - ChangeHosts("chronos"). - Ensure("/run", 0755), - false, - }, - { - "op type mismatch", - system.New(150). - ChangeHosts("chronos"). - CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 0, 256), - system.New(150). - ChangeHosts("chronos"). - Ensure("/run", 0755), - false, - }, - { - "op equals", - system.New(150). - ChangeHosts("chronos"). - Ensure("/run", 0755), - system.New(150). - ChangeHosts("chronos"). - Ensure("/run", 0755), - true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - if tc.sys.Equal(tc.v) != tc.want { - t.Errorf("Equal: got %v; want %v", - !tc.want, tc.want) - } - }) - } -} diff --git a/internal/system/tmpfiles.go b/internal/system/tmpfiles.go deleted file mode 100644 index 636943bc..00000000 --- a/internal/system/tmpfiles.go +++ /dev/null @@ -1,76 +0,0 @@ -package system - -import ( - "bytes" - "errors" - "fmt" - "io" - "os" - "syscall" -) - -// CopyFile registers an Op that copies from src. -// A buffer is initialised with size cap and the Op faults if bytes read exceed n. -func (sys *I) CopyFile(payload *[]byte, src string, cap int, n int64) *I { - buf := new(bytes.Buffer) - buf.Grow(cap) - - sys.lock.Lock() - sys.ops = append(sys.ops, &Tmpfile{payload, src, n, buf}) - sys.lock.Unlock() - - return sys -} - -type Tmpfile struct { - payload *[]byte - src string - - n int64 - buf *bytes.Buffer -} - -func (t *Tmpfile) Type() Enablement { return Process } -func (t *Tmpfile) apply(sys *I) error { - sys.println("copying", t) - - if t.payload == nil { - // this is a misuse of the API; do not return an error message - return errors.New("invalid payload") - } - - if b, err := os.Stat(t.src); err != nil { - return sys.wrapErrSuffix(err, - fmt.Sprintf("cannot stat %q:", t.src)) - } else { - if b.IsDir() { - return sys.wrapErrSuffix(syscall.EISDIR, - fmt.Sprintf("%q is a directory", t.src)) - } - if s := b.Size(); s > t.n { - return sys.wrapErrSuffix(syscall.ENOMEM, - fmt.Sprintf("file %q is too long: %d > %d", - t.src, s, t.n)) - } - } - - if f, err := os.Open(t.src); err != nil { - return sys.wrapErrSuffix(err, - fmt.Sprintf("cannot open %q:", t.src)) - } else if _, err = io.CopyN(t.buf, f, t.n); err != nil { - return sys.wrapErrSuffix(err, - fmt.Sprintf("cannot read from %q:", t.src)) - } - - *t.payload = t.buf.Bytes() - return nil -} -func (t *Tmpfile) revert(*I, *Criteria) error { t.buf.Reset(); return nil } - -func (t *Tmpfile) Is(o Op) bool { - t0, ok := o.(*Tmpfile) - return ok && t0 != nil && - t.src == t0.src && t.n == t0.n -} -func (t *Tmpfile) Path() string { return t.src } -func (t *Tmpfile) String() string { return fmt.Sprintf("up to %d bytes from %q", t.n, t.src) } diff --git a/internal/system/tmpfiles_test.go b/internal/system/tmpfiles_test.go deleted file mode 100644 index c7402f59..00000000 --- a/internal/system/tmpfiles_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package system - -import ( - "strconv" - "testing" -) - -func TestCopyFile(t *testing.T) { - testCases := []struct { - tcOp - cap int - n int64 - }{ - {tcOp{Process, "/home/ophestra/xdg/config/pulse/cookie"}, 256, 256}, - } - for _, tc := range testCases { - t.Run("copy file "+tc.path+" with cap = "+strconv.Itoa(tc.cap)+" n = "+strconv.Itoa(int(tc.n)), func(t *testing.T) { - sys := New(150) - sys.CopyFile(new([]byte), tc.path, tc.cap, tc.n) - tc.test(t, sys.ops, []Op{ - &Tmpfile{nil, tc.path, tc.n, nil}, - }, "CopyFile") - }) - } -} - -func TestLink(t *testing.T) { - testCases := []struct { - dst, src string - }{ - {"/tmp/fortify.1971/f587afe9fce3c8e1ad5b64deb6c41ad5/pulse-cookie", "/home/ophestra/xdg/config/pulse/cookie"}, - {"/tmp/fortify.1971/62154f708b5184ab01f9dcc2bbe7a33b/pulse-cookie", "/home/ophestra/xdg/config/pulse/cookie"}, - } - for _, tc := range testCases { - t.Run("link file "+tc.dst+" from "+tc.src, func(t *testing.T) { - sys := New(150) - sys.Link(tc.src, tc.dst) - (&tcOp{Process, tc.src}).test(t, sys.ops, []Op{ - &Hardlink{Process, tc.dst, tc.src}, - }, "Link") - }) - } -} - -func TestLinkFileType(t *testing.T) { - testCases := []struct { - tcOp - dst string - }{ - {tcOp{User, "/tmp/fortify.1971/f587afe9fce3c8e1ad5b64deb6c41ad5/pulse-cookie"}, "/home/ophestra/xdg/config/pulse/cookie"}, - {tcOp{Process, "/tmp/fortify.1971/62154f708b5184ab01f9dcc2bbe7a33b/pulse-cookie"}, "/home/ophestra/xdg/config/pulse/cookie"}, - } - for _, tc := range testCases { - t.Run("link file "+tc.dst+" from "+tc.path+" with type "+TypeString(tc.et), func(t *testing.T) { - sys := New(150) - sys.LinkFileType(tc.et, tc.path, tc.dst) - tc.test(t, sys.ops, []Op{ - &Hardlink{tc.et, tc.dst, tc.path}, - }, "LinkFileType") - }) - } -} - -func TestTmpfile_String(t *testing.T) { - testCases := []struct { - src string - n int64 - want string - }{ - {"/home/ophestra/xdg/config/pulse/cookie", 256, - `up to 256 bytes from "/home/ophestra/xdg/config/pulse/cookie"`}, - } - - for _, tc := range testCases { - t.Run(tc.want, func(t *testing.T) { - if got := (&Tmpfile{src: tc.src, n: tc.n}).String(); got != tc.want { - t.Errorf("String() = %v, want %v", got, tc.want) - } - }) - } -} diff --git a/internal/system/wayland.go b/internal/system/wayland.go deleted file mode 100644 index 11f54d80..00000000 --- a/internal/system/wayland.go +++ /dev/null @@ -1,89 +0,0 @@ -package system - -import ( - "errors" - "fmt" - "os" - - "git.gensokyo.uk/security/fortify/acl" - "git.gensokyo.uk/security/fortify/wl" -) - -// Wayland sets up a wayland socket with a security context attached. -func (sys *I) Wayland(syncFd **os.File, dst, src, appID, instanceID string) *I { - sys.lock.Lock() - defer sys.lock.Unlock() - - sys.ops = append(sys.ops, &Wayland{syncFd, dst, src, appID, instanceID, wl.Conn{}}) - - return sys -} - -type Wayland struct { - sync **os.File - dst, src string - appID, instanceID string - - conn wl.Conn -} - -func (w *Wayland) Type() Enablement { return Process } - -func (w *Wayland) apply(sys *I) error { - if w.sync == nil { - // this is a misuse of the API; do not return an error message - return errors.New("invalid sync") - } - - // the Wayland op is not repeatable - if *w.sync != nil { - // this is a misuse of the API; do not return an error message - return errors.New("attempted to attach multiple wayland sockets") - } - - if err := w.conn.Attach(w.src); err != nil { - // make console output less nasty - if errors.Is(err, os.ErrNotExist) { - err = os.ErrNotExist - } - return sys.wrapErrSuffix(err, - fmt.Sprintf("cannot attach to wayland on %q:", w.src)) - } else { - sys.printf("wayland attached on %q", w.src) - } - - if sp, err := w.conn.Bind(w.dst, w.appID, w.instanceID); err != nil { - return sys.wrapErrSuffix(err, - fmt.Sprintf("cannot bind to socket on %q:", w.dst)) - } else { - *w.sync = sp - sys.printf("wayland listening on %q", w.dst) - return sys.wrapErrSuffix(errors.Join(os.Chmod(w.dst, 0), acl.UpdatePerm(w.dst, sys.uid, acl.Read, acl.Write, acl.Execute)), - fmt.Sprintf("cannot chmod socket on %q:", w.dst)) - } -} - -func (w *Wayland) revert(sys *I, ec *Criteria) error { - if ec.hasType(w) { - sys.printf("removing wayland socket on %q", w.dst) - if err := os.Remove(w.dst); err != nil && !errors.Is(err, os.ErrNotExist) { - return err - } - - sys.printf("detaching from wayland on %q", w.src) - return sys.wrapErrSuffix(w.conn.Close(), - fmt.Sprintf("cannot detach from wayland on %q:", w.src)) - } else { - sys.printf("skipping wayland cleanup on %q", w.dst) - return nil - } -} - -func (w *Wayland) Is(o Op) bool { - w0, ok := o.(*Wayland) - return ok && w.dst == w0.dst && w.src == w0.src && - w.appID == w0.appID && w.instanceID == w0.instanceID -} - -func (w *Wayland) Path() string { return w.dst } -func (w *Wayland) String() string { return fmt.Sprintf("wayland socket at %q", w.dst) } diff --git a/internal/system/xhost.go b/internal/system/xhost.go deleted file mode 100644 index bbc0a914..00000000 --- a/internal/system/xhost.go +++ /dev/null @@ -1,53 +0,0 @@ -package system - -import ( - "fmt" - - "git.gensokyo.uk/security/fortify/xcb" -) - -// ChangeHosts appends an X11 ChangeHosts command Op. -func (sys *I) ChangeHosts(username string) *I { - sys.lock.Lock() - defer sys.lock.Unlock() - - sys.ops = append(sys.ops, XHost(username)) - - return sys -} - -type XHost string - -func (x XHost) Type() Enablement { - return EX11 -} - -func (x XHost) apply(sys *I) error { - sys.printf("inserting entry %s to X11", x) - return sys.wrapErrSuffix(xcb.ChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)), - fmt.Sprintf("cannot insert entry %s to X11:", x)) -} - -func (x XHost) revert(sys *I, ec *Criteria) error { - if ec.hasType(x) { - sys.printf("deleting entry %s from X11", x) - return sys.wrapErrSuffix(xcb.ChangeHosts(xcb.HostModeDelete, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)), - fmt.Sprintf("cannot delete entry %s from X11:", x)) - } else { - sys.printf("skipping entry %s in X11", 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) -} diff --git a/internal/system/xhost_test.go b/internal/system/xhost_test.go deleted file mode 100644 index 4b364432..00000000 --- a/internal/system/xhost_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package system - -import ( - "testing" -) - -func TestChangeHosts(t *testing.T) { - testCases := []string{"chronos", "keyring", "cat", "kbd", "yonah"} - for _, tc := range testCases { - t.Run("append ChangeHosts operation for "+tc, func(t *testing.T) { - sys := New(150) - sys.ChangeHosts(tc) - (&tcOp{EX11, tc}).test(t, sys.ops, []Op{ - XHost(tc), - }, "ChangeHosts") - }) - } -} - -func TestXHost_String(t *testing.T) { - testCases := []struct { - username string - want string - }{ - {"chronos", "SI:localuser:chronos"}, - } - for _, tc := range testCases { - t.Run(tc.want, func(t *testing.T) { - if got := XHost(tc.username).String(); got != tc.want { - t.Errorf("String() = %v, want %v", got, tc.want) - } - }) - } -} |
