aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/state
diff options
context:
space:
mode:
Diffstat (limited to 'internal/state')
-rw-r--r--internal/state/data.go59
-rw-r--r--internal/state/enablement.go46
-rw-r--r--internal/state/print.go125
-rw-r--r--internal/state/simple.go219
-rw-r--r--internal/state/state.go40
-rw-r--r--internal/state/track.go41
6 files changed, 394 insertions, 136 deletions
diff --git a/internal/state/data.go b/internal/state/data.go
deleted file mode 100644
index 45493885..00000000
--- a/internal/state/data.go
+++ /dev/null
@@ -1,59 +0,0 @@
-package state
-
-import (
- "encoding/gob"
- "os"
- "path"
-
- "git.ophivana.moe/cat/fortify/internal"
-)
-
-// we unfortunately have to assume there are never races between processes
-// this and launcher should eventually be replaced by a server process
-
-type launcherState struct {
- PID int
- Launcher string
- Argv []string
- Command []string
- Capability internal.Enablements
-}
-
-// ReadLaunchers reads all launcher state file entries for the requested user
-// and if decode is true decodes these launchers as well.
-func ReadLaunchers(runDirPath, uid string, decode bool) ([]*launcherState, error) {
- var f *os.File
- var r []*launcherState
- launcherPrefix := path.Join(runDirPath, uid)
-
- if pl, err := os.ReadDir(launcherPrefix); err != nil {
- return nil, err
- } else {
- for _, e := range pl {
- if err = func() error {
- if f, err = os.Open(path.Join(launcherPrefix, e.Name())); err != nil {
- return err
- } else {
- defer func() {
- if f.Close() != nil {
- // unreachable
- panic("foreign state file closed prematurely")
- }
- }()
-
- var s launcherState
- r = append(r, &s)
- if decode {
- return gob.NewDecoder(f).Decode(&s)
- } else {
- return nil
- }
- }
- }(); err != nil {
- return nil, err
- }
- }
- }
-
- return r, nil
-}
diff --git a/internal/state/enablement.go b/internal/state/enablement.go
new file mode 100644
index 00000000..9cc8f60d
--- /dev/null
+++ b/internal/state/enablement.go
@@ -0,0 +1,46 @@
+package state
+
+type (
+ // Enablement represents an optional system resource
+ Enablement uint8
+ // Enablements represents optional system resources to share
+ Enablements uint64
+)
+
+const (
+ EnableWayland Enablement = iota
+ EnableX
+ EnableDBus
+ EnablePulse
+
+ EnableLength
+)
+
+var enablementString = [EnableLength]string{
+ "Wayland",
+ "X11",
+ "D-Bus",
+ "PulseAudio",
+}
+
+func (e Enablement) String() string {
+ 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()
+}
diff --git a/internal/state/print.go b/internal/state/print.go
index 838cacb0..b4c74a90 100644
--- a/internal/state/print.go
+++ b/internal/state/print.go
@@ -3,69 +3,122 @@ package state
import (
"fmt"
"os"
+ "path"
"strconv"
"strings"
"text/tabwriter"
+ "time"
"git.ophivana.moe/cat/fortify/internal"
"git.ophivana.moe/cat/fortify/internal/verbose"
)
-func MustPrintLauncherStateGlobal(w **tabwriter.Writer, runDirPath string) {
- if dirs, err := os.ReadDir(runDirPath); err != nil {
- fmt.Println("Error reading runtime directory:", err)
+// MustPrintLauncherStateSimpleGlobal prints active launcher states of all simple stores
+// in an implementation-specific way.
+func MustPrintLauncherStateSimpleGlobal(w **tabwriter.Writer) {
+ sc := internal.GetSC()
+ now := time.Now().UTC()
+
+ // read runtime directory to get all UIDs
+ if dirs, err := os.ReadDir(sc.RunDirPath); err != nil {
+ fmt.Println("cannot read runtime directory:", err)
+ os.Exit(1)
} else {
for _, e := range dirs {
+ // skip non-directories
if !e.IsDir() {
- verbose.Println("Skipped non-directory entry", e.Name())
+ verbose.Println("skipped non-directory entry", e.Name())
continue
}
+ // skip non-numerical names
if _, err = strconv.Atoi(e.Name()); err != nil {
- verbose.Println("Skipped non-uid entry", e.Name())
+ verbose.Println("skipped non-uid entry", e.Name())
continue
}
- MustPrintLauncherState(w, runDirPath, e.Name())
+ // obtain temporary store
+ s := NewSimple(sc.RunDirPath, e.Name()).(*simpleStore)
+
+ // print states belonging to this store
+ s.mustPrintLauncherState(w, now)
+
+ // mustPrintLauncherState causes store activity so store needs to be closed
+ if err = s.Close(); err != nil {
+ fmt.Printf("warn: error closing store for user %s: %s\n", e.Name(), err)
+ }
}
}
}
-func MustPrintLauncherState(w **tabwriter.Writer, runDirPath, uid string) {
- launchers, err := ReadLaunchers(runDirPath, uid, true)
- if err != nil {
- fmt.Println("Error reading launchers:", err)
- os.Exit(1)
- }
+func (s *simpleStore) mustPrintLauncherState(w **tabwriter.Writer, now time.Time) {
+ var innerErr error
- if *w == nil {
- *w = tabwriter.NewWriter(os.Stdout, 0, 1, 4, ' ', 0)
+ if ok, err := s.Do(func(b Backend) {
+ innerErr = func() error {
+ // read launcher states
+ states, err := b.Load()
+ if err != nil {
+ return err
+ }
- if !verbose.Get() {
- _, _ = fmt.Fprintln(*w, "\tUID\tPID\tEnablements\tLauncher\tCommand")
- } else {
- _, _ = fmt.Fprintln(*w, "\tUID\tPID\tArgv")
- }
- }
+ // initialise tabwriter if nil
+ if *w == nil {
+ *w = tabwriter.NewWriter(os.Stdout, 0, 1, 4, ' ', 0)
- for _, state := range launchers {
- enablementsDescription := strings.Builder{}
- for i := internal.Enablement(0); i < internal.EnableLength; i++ {
- if state.Capability.Has(i) {
- enablementsDescription.WriteString(", " + i.String())
+ // write header when initialising
+ if !verbose.Get() {
+ _, _ = fmt.Fprintln(*w, "\tUID\tPID\tUptime\tEnablements\tLauncher\tCommand")
+ } else {
+ // argv is emitted in body when verbose
+ _, _ = fmt.Fprintln(*w, "\tUID\tPID\tArgv")
+ }
}
- }
- if enablementsDescription.Len() == 0 {
- enablementsDescription.WriteString("none")
- }
- if !verbose.Get() {
- _, _ = fmt.Fprintf(*w, "\t%s\t%d\t%s\t%s\t%s\n",
- uid, state.PID, strings.TrimPrefix(enablementsDescription.String(), ", "), state.Launcher,
- state.Command)
- } else {
- _, _ = fmt.Fprintf(*w, "\t%s\t%d\t%s\n",
- uid, state.PID, state.Argv)
+ // print each state
+ for _, state := range states {
+ // skip nil states
+ if state == nil {
+ _, _ = fmt.Fprintln(*w, "\tnil state entry")
+ continue
+ }
+
+ // build enablements string
+ ets := strings.Builder{}
+ // append enablement strings in order
+ for i := Enablement(0); i < EnableLength; i++ {
+ if state.Capability.Has(i) {
+ ets.WriteString(", " + i.String())
+ }
+ }
+ // prevent an empty string when
+ if ets.Len() == 0 {
+ ets.WriteString("(No enablements)")
+ }
+
+ if !verbose.Get() {
+ _, _ = fmt.Fprintf(*w, "\t%s\t%d\t%s\t%s\t%s\t%s\n",
+ s.path[len(s.path)-1], state.PID, now.Sub(state.Time).String(), strings.TrimPrefix(ets.String(), ", "), state.Launcher,
+ state.Command)
+ } else {
+ // emit argv instead when verbose
+ _, _ = fmt.Fprintf(*w, "\t%s\t%d\t%s\n",
+ s.path[len(s.path)-1], state.PID, state.Argv)
+ }
+ }
+
+ return nil
+ }()
+ }); err != nil {
+ fmt.Printf("cannot perform action on store '%s': %s\n", path.Join(s.path...), err)
+ if !ok {
+ fmt.Println("warn: store faulted before printing")
+ os.Exit(1)
}
}
+
+ if innerErr != nil {
+ fmt.Printf("cannot print launcher state for store '%s': %s\n", path.Join(s.path...), innerErr)
+ os.Exit(1)
+ }
}
diff --git a/internal/state/simple.go b/internal/state/simple.go
new file mode 100644
index 00000000..a18d1523
--- /dev/null
+++ b/internal/state/simple.go
@@ -0,0 +1,219 @@
+package state
+
+import (
+ "encoding/gob"
+ "errors"
+ "io/fs"
+ "os"
+ "path"
+ "strconv"
+ "sync"
+ "syscall"
+)
+
+// file-based locking
+type simpleStore struct {
+ path []string
+
+ // created/opened by prepare
+ lockfile *os.File
+ // enforce prepare method
+ init sync.Once
+ // error returned by prepare
+ initErr error
+
+ lock sync.Mutex
+}
+
+func (s *simpleStore) Do(f func(b Backend)) (bool, error) {
+ s.init.Do(s.prepare)
+ if s.initErr != nil {
+ return false, s.initErr
+ }
+
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ // lock store
+ if err := s.lockFile(); err != nil {
+ return false, err
+ }
+
+ // initialise new backend for caller
+ b := new(simpleBackend)
+ b.path = path.Join(s.path...)
+ f(b)
+ // disable backend
+ b.lock.Lock()
+
+ // unlock store
+ return true, s.unlockFile()
+}
+
+func (s *simpleStore) lockFileAct(lt int) (err error) {
+ op := "LockAct"
+ switch lt {
+ case syscall.LOCK_EX:
+ op = "Lock"
+ case syscall.LOCK_UN:
+ op = "Unlock"
+ }
+
+ for {
+ err = syscall.Flock(int(s.lockfile.Fd()), lt)
+ if !errors.Is(err, syscall.EINTR) {
+ break
+ }
+ }
+ if err != nil {
+ return &fs.PathError{
+ Op: op,
+ Path: s.lockfile.Name(),
+ Err: err,
+ }
+ }
+ return nil
+}
+
+func (s *simpleStore) lockFile() error {
+ return s.lockFileAct(syscall.LOCK_EX)
+}
+
+func (s *simpleStore) unlockFile() error {
+ return s.lockFileAct(syscall.LOCK_UN)
+}
+
+func (s *simpleStore) prepare() {
+ s.initErr = func() error {
+ prefix := path.Join(s.path...)
+ // ensure directory
+ if err := os.MkdirAll(prefix, 0700); err != nil && !errors.Is(err, fs.ErrExist) {
+ return err
+ }
+
+ // open locker file
+ if f, err := os.OpenFile(prefix+".lock", os.O_RDWR|os.O_CREATE, 0600); err != nil {
+ return err
+ } else {
+ s.lockfile = f
+ }
+
+ return nil
+ }()
+}
+
+func (s *simpleStore) Close() error {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ err := s.lockfile.Close()
+ if err == nil || errors.Is(err, os.ErrInvalid) || errors.Is(err, os.ErrClosed) {
+ return nil
+ }
+ return err
+}
+
+type simpleBackend struct {
+ path string
+
+ lock sync.RWMutex
+}
+
+func (b *simpleBackend) filename(pid int) string {
+ return path.Join(b.path, strconv.Itoa(pid))
+}
+
+// reads all launchers in simpleBackend
+// file contents are ignored if decode is false
+func (b *simpleBackend) load(decode bool) ([]*State, error) {
+ b.lock.RLock()
+ defer b.lock.RUnlock()
+
+ var (
+ r []*State
+ f *os.File
+ )
+
+ // read directory contents, should only contain files named after PIDs
+ if pl, err := os.ReadDir(b.path); err != nil {
+ return nil, err
+ } else {
+ for _, e := range pl {
+ // run in a function to better handle file closing
+ if err = func() error {
+ // open state file for reading
+ if f, err = os.Open(path.Join(b.path, e.Name())); err != nil {
+ return err
+ } else {
+ defer func() {
+ if f.Close() != nil {
+ // unreachable
+ panic("foreign state file closed prematurely")
+ }
+ }()
+
+ var s State
+ r = append(r, &s)
+
+ // append regardless, but only parse if required, used to implement Len
+ if decode {
+ return gob.NewDecoder(f).Decode(&s)
+ } else {
+ return nil
+ }
+ }
+ }(); err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ return r, nil
+}
+
+// Save writes process state to filesystem
+func (b *simpleBackend) Save(state *State) error {
+ b.lock.Lock()
+ defer b.lock.Unlock()
+
+ statePath := b.filename(state.PID)
+
+ // create and open state data file
+ if f, err := os.OpenFile(statePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600); err != nil {
+ return err
+ } else {
+ defer func() {
+ if f.Close() != nil {
+ // unreachable
+ panic("state file closed prematurely")
+ }
+ }()
+ // encode into state file
+ return gob.NewEncoder(f).Encode(state)
+ }
+}
+
+func (b *simpleBackend) Destroy(pid int) error {
+ b.lock.Lock()
+ defer b.lock.Unlock()
+
+ return os.Remove(b.filename(pid))
+}
+
+func (b *simpleBackend) Load() ([]*State, error) {
+ return b.load(true)
+}
+
+func (b *simpleBackend) Len() (int, error) {
+ // rn consists of only nil entries but has the correct length
+ rn, err := b.load(false)
+ return len(rn), err
+}
+
+// NewSimple returns an instance of a file-based store.
+// Store prefix is typically (runDir, uid).
+func NewSimple(prefix ...string) Store {
+ b := new(simpleStore)
+ b.path = prefix
+ return b
+}
diff --git a/internal/state/state.go b/internal/state/state.go
new file mode 100644
index 00000000..61aae2cf
--- /dev/null
+++ b/internal/state/state.go
@@ -0,0 +1,40 @@
+package state
+
+import (
+ "time"
+)
+
+type Store interface {
+ // Do calls f exactly once and ensures store exclusivity until f returns.
+ // Returns whether f is called and any errors during the locking process.
+ // Backend provided to f becomes invalid as soon as f returns.
+ Do(f func(b Backend)) (bool, error)
+
+ // Close releases any resources held by Store.
+ Close() error
+}
+
+// Backend provides access to the store
+type Backend interface {
+ Save(state *State) error
+ Destroy(pid int) error
+ Load() ([]*State, error)
+ Len() (int, error)
+}
+
+// State is the on-disk format for a fortified process's state information
+type State struct {
+ // child process PID value
+ PID int
+ // command used to seal the app
+ Command []string
+ // capability enablements applied to child
+ Capability Enablements
+
+ // resolved launcher path
+ Launcher string
+ // full argv whe launching
+ Argv []string
+ // process start time
+ Time time.Time
+}
diff --git a/internal/state/track.go b/internal/state/track.go
deleted file mode 100644
index 8ecad134..00000000
--- a/internal/state/track.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package state
-
-import (
- "encoding/gob"
- "errors"
- "io/fs"
- "os"
- "os/exec"
- "path"
- "strconv"
-
- "git.ophivana.moe/cat/fortify/internal"
-)
-
-// SaveProcess called after process start, before wait
-func SaveProcess(uid string, cmd *exec.Cmd, runDirPath string, command []string, enablements internal.Enablements) (string, error) {
- statePath := path.Join(runDirPath, uid, strconv.Itoa(cmd.Process.Pid))
- state := launcherState{
- PID: cmd.Process.Pid,
- Launcher: cmd.Path,
- Argv: cmd.Args,
- Command: command,
- Capability: enablements,
- }
-
- if err := os.Mkdir(path.Join(runDirPath, uid), 0700); err != nil && !errors.Is(err, fs.ErrExist) {
- return statePath, err
- }
-
- if f, err := os.OpenFile(statePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600); err != nil {
- return statePath, err
- } else {
- defer func() {
- if f.Close() != nil {
- // unreachable
- panic("state file closed prematurely")
- }
- }()
- return statePath, gob.NewEncoder(f).Encode(state)
- }
-}