diff options
Diffstat (limited to 'internal/state')
| -rw-r--r-- | internal/state/data.go | 59 | ||||
| -rw-r--r-- | internal/state/enablement.go | 46 | ||||
| -rw-r--r-- | internal/state/print.go | 125 | ||||
| -rw-r--r-- | internal/state/simple.go | 219 | ||||
| -rw-r--r-- | internal/state/state.go | 40 | ||||
| -rw-r--r-- | internal/state/track.go | 41 |
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) - } -} |
