system: move out of internal
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m17s

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
2025-02-17 19:00:43 +09:00
parent b1e1d5627e
commit 90cb01b274
26 changed files with 12 additions and 12 deletions

View File

@@ -5,7 +5,7 @@ import (
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/system"
"git.gensokyo.uk/security/fortify/system"
)
var testCasesNixos = []sealTestCase{

View File

@@ -7,7 +7,7 @@ import (
"git.gensokyo.uk/security/fortify/dbus"
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/system"
"git.gensokyo.uk/security/fortify/system"
)
var testCasesPd = []sealTestCase{

View File

@@ -11,7 +11,7 @@ import (
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/app"
"git.gensokyo.uk/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/system"
"git.gensokyo.uk/security/fortify/system"
)
type sealTestCase struct {

View File

@@ -4,7 +4,7 @@ import (
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/system"
"git.gensokyo.uk/security/fortify/system"
)
func NewWithID(id fst.ID, os linux.System) App {

View File

@@ -19,7 +19,7 @@ import (
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/state"
"git.gensokyo.uk/security/fortify/internal/system"
"git.gensokyo.uk/security/fortify/system"
)
var (

View File

@@ -12,7 +12,7 @@ import (
"git.gensokyo.uk/security/fortify/fst"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/linux"
"git.gensokyo.uk/security/fortify/internal/system"
"git.gensokyo.uk/security/fortify/system"
"git.gensokyo.uk/security/fortify/wl"
)

View File

@@ -14,7 +14,7 @@ import (
"git.gensokyo.uk/security/fortify/internal/app/shim"
"git.gensokyo.uk/security/fortify/internal/fmsg"
"git.gensokyo.uk/security/fortify/internal/state"
"git.gensokyo.uk/security/fortify/internal/system"
"git.gensokyo.uk/security/fortify/system"
)
const shimSetupTimeout = 5 * time.Second

View File

@@ -4,7 +4,7 @@ import (
"os"
"git.gensokyo.uk/security/fortify/helper/bwrap"
"git.gensokyo.uk/security/fortify/internal/system"
"git.gensokyo.uk/security/fortify/system"
)
// appSealSys encapsulates app seal behaviour with OS interactions

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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(), ", ")
}

View File

@@ -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) }

View File

@@ -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)
}

View File

@@ -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)
}
})
}
}

View File

@@ -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...)
}

View File

@@ -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
}

View File

@@ -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)
}
})
}
}

View File

@@ -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) }

View File

@@ -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)
}
})
}
}

View File

@@ -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) }

View File

@@ -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)
}

View File

@@ -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)
}
})
}
}