app: clean up interactions and handle all application state and setup/teardown
There was an earlier attempt of cleaning up the app package however it ended up creating even more of a mess and the code structure largely still looked like Ego with state setup scattered everywhere and a bunch of ugly hacks had to be implemented to keep track of all of them. In this commit the entire app package is rewritten to track everything that has to do with an app in one thread safe value. In anticipation of the client/server split also made changes: - Console messages are cleaned up to be consistent - State tracking is fully rewritten to be cleaner and usable for multiple process and client/server - Encapsulate errors to easier identify type of action causing the error as well as additional info - System-level setup operations is grouped in a way that can be collectively committed/reverted and gracefully handles errors returned by each operation - Resource sharing is made more fine-grained with PID-scoped resources whenever possible, a few remnants (X11, Wayland, PulseAudio) will be addressed when a generic proxy is available - Application setup takes a JSON-friendly config struct and deterministically generates system setup operations Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
This commit is contained in:
parent
11832a9379
commit
62cb8a91b6
6
flag.go
6
flag.go
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
var (
|
||||
userName string
|
||||
confPath string
|
||||
|
||||
dbusConfigSession string
|
||||
dbusConfigSystem string
|
||||
|
@ -22,10 +23,13 @@ var (
|
|||
|
||||
flagVerbose bool
|
||||
printVersion bool
|
||||
|
||||
launchMethodText string
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&userName, "u", "chronos", "Passwd name of user to run as")
|
||||
flag.StringVar(&confPath, "c", "nil", "Path to full app configuration, or \"nil\" to configure from flags")
|
||||
|
||||
flag.StringVar(&dbusConfigSession, "dbus-config", "builtin", "Path to D-Bus proxy config file, or \"builtin\" for defaults")
|
||||
flag.StringVar(&dbusConfigSystem, "dbus-system", "nil", "Path to system D-Bus proxy config file, or \"nil\" to disable")
|
||||
|
@ -48,5 +52,5 @@ func init() {
|
|||
methodHelpString += ", \"systemd\""
|
||||
}
|
||||
|
||||
flag.StringVar(&launchOptionText, "method", "sudo", methodHelpString)
|
||||
flag.StringVar(&launchMethodText, "method", "sudo", methodHelpString)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type App interface {
|
||||
Seal(config *Config) error
|
||||
Start() error
|
||||
Wait() (int, error)
|
||||
WaitErr() error
|
||||
String() string
|
||||
}
|
||||
|
||||
type app struct {
|
||||
// child process related information
|
||||
seal *appSeal
|
||||
// underlying fortified child process
|
||||
cmd *exec.Cmd
|
||||
// error returned waiting for process
|
||||
wait error
|
||||
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func (a *app) String() string {
|
||||
if a == nil {
|
||||
return "(invalid fortified app)"
|
||||
}
|
||||
|
||||
a.lock.RLock()
|
||||
defer a.lock.RUnlock()
|
||||
|
||||
if a.cmd != nil {
|
||||
return a.cmd.String()
|
||||
}
|
||||
|
||||
if a.seal != nil {
|
||||
return "(sealed fortified app as uid " + a.seal.sys.Uid + ")"
|
||||
}
|
||||
|
||||
return "(unsealed fortified app)"
|
||||
}
|
||||
|
||||
func (a *app) WaitErr() error {
|
||||
return a.wait
|
||||
}
|
||||
|
||||
func New() App {
|
||||
return new(app)
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package app
|
||||
|
||||
func (a *App) Command() []string {
|
||||
return a.command
|
||||
}
|
||||
|
||||
func (a *App) UID() int {
|
||||
return a.uid
|
||||
}
|
||||
|
||||
func (a *App) AppendEnv(k, v string) {
|
||||
a.env = append(a.env, k+"="+v)
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"git.ophivana.moe/cat/fortify/dbus"
|
||||
"git.ophivana.moe/cat/fortify/internal/state"
|
||||
)
|
||||
|
||||
// Config is used to seal an *App
|
||||
type Config struct {
|
||||
// D-Bus application ID
|
||||
ID string `json:"id"`
|
||||
// username of the target user to switch to
|
||||
User string `json:"user"`
|
||||
// value passed through to the child process as its argv
|
||||
Command []string `json:"command"`
|
||||
// string representation of the child's launch method
|
||||
Method string `json:"method"`
|
||||
|
||||
// child confinement configuration
|
||||
Confinement ConfinementConfig `json:"confinement"`
|
||||
}
|
||||
|
||||
// ConfinementConfig defines fortified child's confinement
|
||||
type ConfinementConfig struct {
|
||||
// reference to a system D-Bus proxy configuration,
|
||||
// nil value disables system bus proxy
|
||||
SystemBus *dbus.Config `json:"system_bus,omitempty"`
|
||||
// reference to a session D-Bus proxy configuration,
|
||||
// nil value makes session bus proxy assume built-in defaults
|
||||
SessionBus *dbus.Config `json:"session_bus,omitempty"`
|
||||
|
||||
// child capability enablements
|
||||
Enablements state.Enablements `json:"enablements"`
|
||||
}
|
|
@ -1,17 +1,11 @@
|
|||
package util
|
||||
package app
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func Which(file string) (string, bool) {
|
||||
p, err := exec.LookPath(file)
|
||||
return p, err == nil
|
||||
}
|
||||
|
||||
func CopyFile(dst, src string) error {
|
||||
func copyFile(dst, src string) error {
|
||||
srcD, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
|
@ -1,123 +0,0 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/acl"
|
||||
"git.ophivana.moe/cat/fortify/dbus"
|
||||
"git.ophivana.moe/cat/fortify/internal"
|
||||
"git.ophivana.moe/cat/fortify/internal/util"
|
||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
||||
)
|
||||
|
||||
const (
|
||||
dbusSessionBusAddress = "DBUS_SESSION_BUS_ADDRESS"
|
||||
dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS"
|
||||
)
|
||||
|
||||
var (
|
||||
dbusAddress [2]string
|
||||
dbusSystem bool
|
||||
)
|
||||
|
||||
func (a *App) ShareDBus(dse, dsg *dbus.Config, log bool) {
|
||||
a.setEnablement(internal.EnableDBus)
|
||||
|
||||
dbusSystem = dsg != nil
|
||||
var binPath string
|
||||
var sessionBus, systemBus [2]string
|
||||
|
||||
target := path.Join(a.sharePath, strconv.Itoa(os.Getpid()))
|
||||
sessionBus[1] = target + ".bus"
|
||||
systemBus[1] = target + ".system-bus"
|
||||
dbusAddress = [2]string{
|
||||
"unix:path=" + sessionBus[1],
|
||||
"unix:path=" + systemBus[1],
|
||||
}
|
||||
|
||||
if b, ok := util.Which("xdg-dbus-proxy"); !ok {
|
||||
internal.Fatal("D-Bus: Did not find 'xdg-dbus-proxy' in PATH")
|
||||
} else {
|
||||
binPath = b
|
||||
}
|
||||
|
||||
if addr, ok := os.LookupEnv(dbusSessionBusAddress); !ok {
|
||||
verbose.Println("D-Bus: DBUS_SESSION_BUS_ADDRESS not set, assuming default format")
|
||||
sessionBus[0] = fmt.Sprintf("unix:path=/run/user/%d/bus", os.Getuid())
|
||||
} else {
|
||||
sessionBus[0] = addr
|
||||
}
|
||||
|
||||
if addr, ok := os.LookupEnv(dbusSystemBusAddress); !ok {
|
||||
verbose.Println("D-Bus: DBUS_SYSTEM_BUS_ADDRESS not set, assuming default format")
|
||||
systemBus[0] = "unix:path=/run/dbus/system_bus_socket"
|
||||
} else {
|
||||
systemBus[0] = addr
|
||||
}
|
||||
|
||||
p := dbus.New(binPath, sessionBus, systemBus)
|
||||
|
||||
dse.Log = log
|
||||
verbose.Println("D-Bus: sealing session proxy", dse.Args(sessionBus))
|
||||
if dsg != nil {
|
||||
dsg.Log = log
|
||||
verbose.Println("D-Bus: sealing system proxy", dsg.Args(systemBus))
|
||||
}
|
||||
if err := p.Seal(dse, dsg); err != nil {
|
||||
internal.Fatal("D-Bus: invalid config when sealing proxy,", err)
|
||||
}
|
||||
|
||||
ready := make(chan bool, 1)
|
||||
done := make(chan struct{})
|
||||
|
||||
verbose.Printf("Starting session bus proxy '%s' for address '%s'\n", dbusAddress[0], sessionBus[0])
|
||||
if dsg != nil {
|
||||
verbose.Printf("Starting system bus proxy '%s' for address '%s'\n", dbusAddress[1], systemBus[0])
|
||||
}
|
||||
if err := p.Start(&ready); err != nil {
|
||||
internal.Fatal("D-Bus: error starting proxy,", err)
|
||||
}
|
||||
verbose.Println("D-Bus proxy launch:", p)
|
||||
|
||||
go func() {
|
||||
if err := p.Wait(); err != nil {
|
||||
fmt.Println("warn: D-Bus proxy returned error,", err)
|
||||
} else {
|
||||
verbose.Println("D-Bus proxy uneventful wait")
|
||||
}
|
||||
if err := os.Remove(target); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
fmt.Println("Error removing dangling D-Bus socket:", err)
|
||||
}
|
||||
done <- struct{}{}
|
||||
}()
|
||||
|
||||
// register early to enable Fatal cleanup
|
||||
a.exit.SealDBus(p, &done)
|
||||
|
||||
if !<-ready {
|
||||
internal.Fatal("D-Bus: proxy did not start correctly")
|
||||
}
|
||||
|
||||
a.AppendEnv(dbusSessionBusAddress, dbusAddress[0])
|
||||
if err := acl.UpdatePerm(sessionBus[1], a.UID(), acl.Read, acl.Write); err != nil {
|
||||
internal.Fatal(fmt.Sprintf("Error preparing D-Bus session proxy '%s':", dbusAddress[0]), err)
|
||||
} else {
|
||||
a.exit.RegisterRevertPath(sessionBus[1])
|
||||
}
|
||||
if dsg != nil {
|
||||
a.AppendEnv(dbusSystemBusAddress, dbusAddress[1])
|
||||
if err := acl.UpdatePerm(systemBus[1], a.UID(), acl.Read, acl.Write); err != nil {
|
||||
internal.Fatal(fmt.Sprintf("Error preparing D-Bus system proxy '%s':", dbusAddress[1]), err)
|
||||
} else {
|
||||
a.exit.RegisterRevertPath(systemBus[1])
|
||||
}
|
||||
}
|
||||
verbose.Printf("Session bus proxy '%s' for address '%s' configured\n", dbusAddress[0], sessionBus[0])
|
||||
if dsg != nil {
|
||||
verbose.Printf("System bus proxy '%s' for address '%s' configured\n", dbusAddress[1], systemBus[0])
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/acl"
|
||||
"git.ophivana.moe/cat/fortify/internal"
|
||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
||||
)
|
||||
|
||||
func (a *App) EnsureRunDir() {
|
||||
if err := os.Mkdir(a.runDirPath, 0700); err != nil && !errors.Is(err, fs.ErrExist) {
|
||||
internal.Fatal("Error creating runtime directory:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) EnsureRuntime() {
|
||||
if s, err := os.Stat(a.runtimePath); err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
internal.Fatal("Runtime directory does not exist")
|
||||
}
|
||||
internal.Fatal("Error accessing runtime directory:", err)
|
||||
} else if !s.IsDir() {
|
||||
internal.Fatal(fmt.Sprintf("Path '%s' is not a directory", a.runtimePath))
|
||||
} else {
|
||||
if err = acl.UpdatePerm(a.runtimePath, a.UID(), acl.Execute); err != nil {
|
||||
internal.Fatal("Error preparing runtime directory:", err)
|
||||
} else {
|
||||
a.exit.RegisterRevertPath(a.runtimePath)
|
||||
}
|
||||
verbose.Printf("Runtime data dir '%s' configured\n", a.runtimePath)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) EnsureShare() {
|
||||
// acl is unnecessary as this directory is world executable
|
||||
if err := os.Mkdir(a.sharePath, 0701); err != nil && !errors.Is(err, fs.ErrExist) {
|
||||
internal.Fatal("Error creating shared directory:", err)
|
||||
}
|
||||
|
||||
// workaround for launch method sudo
|
||||
if a.LaunchOption() == LaunchMethodSudo {
|
||||
// ensure child runtime directory (e.g. `/tmp/fortify.%d/%d.share`)
|
||||
cr := path.Join(a.sharePath, a.Uid+".share")
|
||||
if err := os.Mkdir(cr, 0700); err != nil && !errors.Is(err, fs.ErrExist) {
|
||||
internal.Fatal("Error creating child runtime directory:", err)
|
||||
} else {
|
||||
if err = acl.UpdatePerm(cr, a.UID(), acl.Read, acl.Write, acl.Execute); err != nil {
|
||||
internal.Fatal("Error preparing child runtime directory:", err)
|
||||
} else {
|
||||
a.exit.RegisterRevertPath(cr)
|
||||
}
|
||||
a.AppendEnv("XDG_RUNTIME_DIR", cr)
|
||||
a.AppendEnv("XDG_SESSION_CLASS", "user")
|
||||
a.AppendEnv("XDG_SESSION_TYPE", "tty")
|
||||
verbose.Printf("Child runtime data dir '%s' configured\n", cr)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// baseError implements a basic error container
|
||||
type baseError struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *baseError) Error() string {
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
func (e *baseError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// BaseError implements an error container with a user-facing message
|
||||
type BaseError struct {
|
||||
message string
|
||||
baseError
|
||||
}
|
||||
|
||||
// Message returns a user-facing error message
|
||||
func (e *BaseError) Message() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func wrapError(err error, a ...any) *BaseError {
|
||||
return &BaseError{
|
||||
message: fmt.Sprintln(a...),
|
||||
baseError: baseError{err},
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
baseErrorType = reflect.TypeFor[*BaseError]()
|
||||
)
|
||||
|
||||
func AsBaseError(err error, target **BaseError) bool {
|
||||
v := reflect.ValueOf(err)
|
||||
if !v.CanConvert(baseErrorType) {
|
||||
return false
|
||||
}
|
||||
|
||||
*target = v.Convert(baseErrorType).Interface().(*BaseError)
|
||||
return true
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
type appID [16]byte
|
||||
|
||||
func (a *appID) String() string {
|
||||
return hex.EncodeToString(a[:])
|
||||
}
|
||||
|
||||
func newAppID() (*appID, error) {
|
||||
a := &appID{}
|
||||
_, err := rand.Read(a[:])
|
||||
return a, err
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package app
|
||||
|
||||
// TODO: launch dbus proxy via bwrap
|
||||
|
||||
func (a *app) commandBuilderBwrap() (args []string) {
|
||||
// TODO: build bwrap command
|
||||
panic("bwrap")
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/internal"
|
||||
"git.ophivana.moe/cat/fortify/internal/util"
|
||||
)
|
||||
|
||||
const launcherPayload = "FORTIFY_LAUNCHER_PAYLOAD"
|
||||
|
||||
func (a *App) launcherPayloadEnv() string {
|
||||
r := &bytes.Buffer{}
|
||||
enc := base64.NewEncoder(base64.StdEncoding, r)
|
||||
|
||||
if err := gob.NewEncoder(enc).Encode(a.command); err != nil {
|
||||
internal.Fatal("Error encoding launcher payload:", err)
|
||||
}
|
||||
|
||||
_ = enc.Close()
|
||||
return launcherPayload + "=" + r.String()
|
||||
}
|
||||
|
||||
// Early hidden launcher path
|
||||
func Early(printVersion bool) {
|
||||
if printVersion {
|
||||
if r, ok := os.LookupEnv(launcherPayload); ok {
|
||||
dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(r))
|
||||
|
||||
var argv []string
|
||||
if err := gob.NewDecoder(dec).Decode(&argv); err != nil {
|
||||
fmt.Println("Error decoding launcher payload:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := os.Unsetenv(launcherPayload); err != nil {
|
||||
fmt.Println("Error unsetting launcher payload:", err)
|
||||
// not fatal, do not fail
|
||||
}
|
||||
|
||||
var p string
|
||||
|
||||
if len(argv) > 0 {
|
||||
if p, ok = util.Which(argv[0]); !ok {
|
||||
fmt.Printf("Did not find '%s' in PATH\n", argv[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
if p, ok = os.LookupEnv("SHELL"); !ok {
|
||||
fmt.Println("No command was specified and $SHELL was unset")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
argv = []string{p}
|
||||
}
|
||||
|
||||
if err := syscall.Exec(p, argv, os.Environ()); err != nil {
|
||||
fmt.Println("Error executing launcher payload:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// unreachable
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/internal/state"
|
||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
||||
)
|
||||
|
||||
func (a *app) commandBuilderMachineCtl() (args []string) {
|
||||
args = make([]string, 0, 9+len(a.seal.env))
|
||||
|
||||
// shell --uid=$USER
|
||||
args = append(args, "shell", "--uid="+a.seal.sys.Username)
|
||||
|
||||
// --quiet
|
||||
if !verbose.Get() {
|
||||
args = append(args, "--quiet")
|
||||
}
|
||||
|
||||
// environ
|
||||
envQ := make([]string, len(a.seal.env)+1)
|
||||
for i, e := range a.seal.env {
|
||||
envQ[i] = "-E" + e
|
||||
}
|
||||
// add shim payload to environment for shim path
|
||||
envQ[len(a.seal.env)] = "-E" + a.shimPayloadEnv()
|
||||
args = append(args, envQ...)
|
||||
|
||||
// -- .host
|
||||
args = append(args, "--", ".host")
|
||||
|
||||
// /bin/sh -c
|
||||
if sh, err := exec.LookPath("sh"); err != nil {
|
||||
// hardcode /bin/sh path since it exists more often than not
|
||||
args = append(args, "/bin/sh", "-c")
|
||||
} else {
|
||||
args = append(args, sh, "-c")
|
||||
}
|
||||
|
||||
// build inner command expression ran as target user
|
||||
innerCommand := strings.Builder{}
|
||||
|
||||
// apply custom environment variables to activation environment
|
||||
innerCommand.WriteString("dbus-update-activation-environment --systemd")
|
||||
for _, e := range a.seal.env {
|
||||
innerCommand.WriteString(" " + strings.SplitN(e, "=", 2)[0])
|
||||
}
|
||||
innerCommand.WriteString("; ")
|
||||
|
||||
// override message bus address if enabled
|
||||
if a.seal.et.Has(state.EnableDBus) {
|
||||
innerCommand.WriteString(dbusSessionBusAddress + "=" + "'" + "unix:path=" + a.seal.sys.dbusAddr[0][1] + "' ")
|
||||
if a.seal.sys.dbusSystem {
|
||||
innerCommand.WriteString(dbusSystemBusAddress + "=" + "'" + "unix:path=" + a.seal.sys.dbusAddr[1][1] + "' ")
|
||||
}
|
||||
}
|
||||
|
||||
// both license and version flags need to be set to activate shim path
|
||||
innerCommand.WriteString("exec " + a.seal.sys.executable + " -V -license")
|
||||
|
||||
// append inner command
|
||||
args = append(args, innerCommand.String())
|
||||
|
||||
return
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
||||
)
|
||||
|
||||
const (
|
||||
sudoAskPass = "SUDO_ASKPASS"
|
||||
)
|
||||
|
||||
func (a *app) commandBuilderSudo() (args []string) {
|
||||
args = make([]string, 0, 4+len(a.seal.env)+len(a.seal.command))
|
||||
|
||||
// -Hiu $USER
|
||||
args = append(args, "-Hiu", a.seal.sys.Username)
|
||||
|
||||
// -A?
|
||||
if _, ok := os.LookupEnv(sudoAskPass); ok {
|
||||
verbose.Printf("%s set, adding askpass flag\n", sudoAskPass)
|
||||
args = append(args, "-A")
|
||||
}
|
||||
|
||||
// environ
|
||||
args = append(args, a.seal.env...)
|
||||
|
||||
// -- $@
|
||||
args = append(args, "--")
|
||||
args = append(args, a.seal.command...)
|
||||
|
||||
return
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/acl"
|
||||
"git.ophivana.moe/cat/fortify/internal"
|
||||
"git.ophivana.moe/cat/fortify/internal/util"
|
||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
||||
)
|
||||
|
||||
const (
|
||||
pulseServer = "PULSE_SERVER"
|
||||
pulseCookie = "PULSE_COOKIE"
|
||||
|
||||
home = "HOME"
|
||||
xdgConfigHome = "XDG_CONFIG_HOME"
|
||||
)
|
||||
|
||||
func (a *App) SharePulse() {
|
||||
a.setEnablement(internal.EnablePulse)
|
||||
|
||||
// ensure PulseAudio directory ACL (e.g. `/run/user/%d/pulse`)
|
||||
pulse := path.Join(a.runtimePath, "pulse")
|
||||
pulseS := path.Join(pulse, "native")
|
||||
if s, err := os.Stat(pulse); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
internal.Fatal("Error accessing PulseAudio directory:", err)
|
||||
}
|
||||
internal.Fatal(fmt.Sprintf("PulseAudio dir '%s' not found", pulse))
|
||||
} else {
|
||||
// add environment variable for new process
|
||||
a.AppendEnv(pulseServer, "unix:"+pulseS)
|
||||
if err = acl.UpdatePerm(pulse, a.UID(), acl.Execute); err != nil {
|
||||
internal.Fatal("Error preparing PulseAudio:", err)
|
||||
} else {
|
||||
a.exit.RegisterRevertPath(pulse)
|
||||
}
|
||||
|
||||
// ensure PulseAudio socket permission (e.g. `/run/user/%d/pulse/native`)
|
||||
if s, err = os.Stat(pulseS); err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
internal.Fatal("PulseAudio directory found but socket does not exist")
|
||||
}
|
||||
internal.Fatal("Error accessing PulseAudio socket:", err)
|
||||
} else {
|
||||
if m := s.Mode(); m&0o006 != 0o006 {
|
||||
internal.Fatal(fmt.Sprintf("Unexpected permissions on '%s':", pulseS), m)
|
||||
}
|
||||
}
|
||||
|
||||
// Publish current user's pulse-cookie for target user
|
||||
pulseCookieSource := discoverPulseCookie()
|
||||
pulseCookieFinal := path.Join(a.sharePath, "pulse-cookie")
|
||||
a.AppendEnv(pulseCookie, pulseCookieFinal)
|
||||
verbose.Printf("Publishing PulseAudio cookie '%s' to '%s'\n", pulseCookieSource, pulseCookieFinal)
|
||||
if err = util.CopyFile(pulseCookieFinal, pulseCookieSource); err != nil {
|
||||
internal.Fatal("Error copying PulseAudio cookie:", err)
|
||||
}
|
||||
if err = acl.UpdatePerm(pulseCookieFinal, a.UID(), acl.Read); err != nil {
|
||||
internal.Fatal("Error publishing PulseAudio cookie:", err)
|
||||
} else {
|
||||
a.exit.RegisterRevertPath(pulseCookieFinal)
|
||||
}
|
||||
|
||||
verbose.Printf("PulseAudio dir '%s' configured\n", pulse)
|
||||
}
|
||||
}
|
||||
|
||||
// discoverPulseCookie try various standard methods to discover the current user's PulseAudio authentication cookie
|
||||
func discoverPulseCookie() string {
|
||||
if p, ok := os.LookupEnv(pulseCookie); ok {
|
||||
return p
|
||||
}
|
||||
|
||||
if p, ok := os.LookupEnv(home); ok {
|
||||
p = path.Join(p, ".pulse-cookie")
|
||||
if s, err := os.Stat(p); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
internal.Fatal("Error accessing PulseAudio cookie:", err)
|
||||
// unreachable
|
||||
return p
|
||||
}
|
||||
} else if !s.IsDir() {
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
if p, ok := os.LookupEnv(xdgConfigHome); ok {
|
||||
p = path.Join(p, "pulse", "cookie")
|
||||
if s, err := os.Stat(p); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
internal.Fatal("Error accessing PulseAudio cookie:", err)
|
||||
// unreachable
|
||||
return p
|
||||
}
|
||||
} else if !s.IsDir() {
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
internal.Fatal(fmt.Sprintf("Cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)",
|
||||
pulseCookie, xdgConfigHome, home))
|
||||
return ""
|
||||
}
|
|
@ -1,163 +0,0 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/internal"
|
||||
"git.ophivana.moe/cat/fortify/internal/state"
|
||||
"git.ophivana.moe/cat/fortify/internal/util"
|
||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
||||
)
|
||||
|
||||
const (
|
||||
term = "TERM"
|
||||
sudoAskPass = "SUDO_ASKPASS"
|
||||
)
|
||||
const (
|
||||
LaunchMethodSudo uint8 = iota
|
||||
LaunchMethodBwrap
|
||||
LaunchMethodMachineCtl
|
||||
)
|
||||
|
||||
func (a *App) Run() {
|
||||
// pass $TERM to launcher
|
||||
if t, ok := os.LookupEnv(term); ok {
|
||||
a.AppendEnv(term, t)
|
||||
}
|
||||
|
||||
var commandBuilder func() (args []string)
|
||||
|
||||
switch a.launchOption {
|
||||
case LaunchMethodSudo:
|
||||
commandBuilder = a.commandBuilderSudo
|
||||
case LaunchMethodBwrap:
|
||||
commandBuilder = a.commandBuilderBwrap
|
||||
case LaunchMethodMachineCtl:
|
||||
commandBuilder = a.commandBuilderMachineCtl
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
cmd := exec.Command(a.toolPath, commandBuilder()...)
|
||||
cmd.Env = []string{}
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Dir = a.runDirPath
|
||||
|
||||
verbose.Println("Executing:", cmd)
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
internal.Fatal("Error starting process:", err)
|
||||
}
|
||||
|
||||
a.exit.SealEnablements(a.enablements)
|
||||
|
||||
if statePath, err := state.SaveProcess(a.Uid, cmd, a.runDirPath, a.command, a.enablements); err != nil {
|
||||
// process already started, shouldn't be fatal
|
||||
fmt.Println("Error registering process:", err)
|
||||
} else {
|
||||
a.exit.SealStatePath(statePath)
|
||||
}
|
||||
|
||||
var r int
|
||||
if err := cmd.Wait(); err != nil {
|
||||
var exitError *exec.ExitError
|
||||
if !errors.As(err, &exitError) {
|
||||
internal.Fatal("Error running process:", err)
|
||||
}
|
||||
}
|
||||
|
||||
verbose.Println("Process exited with exit code", r)
|
||||
internal.BeforeExit()
|
||||
os.Exit(r)
|
||||
}
|
||||
|
||||
func (a *App) commandBuilderSudo() (args []string) {
|
||||
args = make([]string, 0, 4+len(a.env)+len(a.command))
|
||||
|
||||
// -Hiu $USER
|
||||
args = append(args, "-Hiu", a.Username)
|
||||
|
||||
// -A?
|
||||
if _, ok := os.LookupEnv(sudoAskPass); ok {
|
||||
verbose.Printf("%s set, adding askpass flag\n", sudoAskPass)
|
||||
args = append(args, "-A")
|
||||
}
|
||||
|
||||
// environ
|
||||
args = append(args, a.env...)
|
||||
|
||||
// -- $@
|
||||
args = append(args, "--")
|
||||
args = append(args, a.command...)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (a *App) commandBuilderBwrap() (args []string) {
|
||||
// TODO: build bwrap command
|
||||
internal.Fatal("bwrap")
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func (a *App) commandBuilderMachineCtl() (args []string) {
|
||||
args = make([]string, 0, 9+len(a.env))
|
||||
|
||||
// shell --uid=$USER
|
||||
args = append(args, "shell", "--uid="+a.Username)
|
||||
|
||||
// --quiet
|
||||
if !verbose.Get() {
|
||||
args = append(args, "--quiet")
|
||||
}
|
||||
|
||||
// environ
|
||||
envQ := make([]string, len(a.env)+1)
|
||||
for i, e := range a.env {
|
||||
envQ[i] = "-E" + e
|
||||
}
|
||||
envQ[len(a.env)] = "-E" + a.launcherPayloadEnv()
|
||||
args = append(args, envQ...)
|
||||
|
||||
// -- .host
|
||||
args = append(args, "--", ".host")
|
||||
|
||||
// /bin/sh -c
|
||||
if sh, ok := util.Which("sh"); !ok {
|
||||
internal.Fatal("Did not find 'sh' in PATH")
|
||||
} else {
|
||||
args = append(args, sh, "-c")
|
||||
}
|
||||
|
||||
if len(a.command) == 0 { // execute shell if command is not provided
|
||||
a.command = []string{"$SHELL"}
|
||||
}
|
||||
|
||||
innerCommand := strings.Builder{}
|
||||
|
||||
innerCommand.WriteString("dbus-update-activation-environment --systemd")
|
||||
for _, e := range a.env {
|
||||
innerCommand.WriteString(" " + strings.SplitN(e, "=", 2)[0])
|
||||
}
|
||||
innerCommand.WriteString("; ")
|
||||
|
||||
if executable, err := os.Executable(); err != nil {
|
||||
internal.Fatal("Error reading executable path:", err)
|
||||
} else {
|
||||
if a.enablements.Has(internal.EnableDBus) {
|
||||
innerCommand.WriteString(dbusSessionBusAddress + "=" + "'" + dbusAddress[0] + "' ")
|
||||
if dbusSystem {
|
||||
innerCommand.WriteString(dbusSystemBusAddress + "=" + "'" + dbusAddress[1] + "' ")
|
||||
}
|
||||
}
|
||||
innerCommand.WriteString("exec " + executable + " -V")
|
||||
}
|
||||
args = append(args, innerCommand.String())
|
||||
|
||||
return
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"strconv"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/dbus"
|
||||
"git.ophivana.moe/cat/fortify/internal"
|
||||
"git.ophivana.moe/cat/fortify/internal/state"
|
||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
||||
)
|
||||
|
||||
const (
|
||||
LaunchMethodSudo uint8 = iota
|
||||
LaunchMethodBwrap
|
||||
LaunchMethodMachineCtl
|
||||
)
|
||||
|
||||
var (
|
||||
ErrConfig = errors.New("no configuration to seal")
|
||||
ErrUser = errors.New("unknown user")
|
||||
ErrLaunch = errors.New("invalid launch method")
|
||||
|
||||
ErrSudo = errors.New("sudo not available")
|
||||
ErrBwrap = errors.New("bwrap not available")
|
||||
ErrSystemd = errors.New("systemd not available")
|
||||
ErrMachineCtl = errors.New("machinectl not available")
|
||||
)
|
||||
|
||||
type (
|
||||
SealConfigError BaseError
|
||||
LauncherLookupError BaseError
|
||||
SecurityError BaseError
|
||||
)
|
||||
|
||||
// Seal seals the app launch context
|
||||
func (a *app) Seal(config *Config) error {
|
||||
a.lock.Lock()
|
||||
defer a.lock.Unlock()
|
||||
|
||||
if a.seal != nil {
|
||||
panic("app sealed twice")
|
||||
}
|
||||
|
||||
if config == nil {
|
||||
return (*SealConfigError)(wrapError(ErrConfig, "attempted to seal app with nil config"))
|
||||
}
|
||||
|
||||
// create seal
|
||||
seal := new(appSeal)
|
||||
|
||||
// generate application ID
|
||||
if id, err := newAppID(); err != nil {
|
||||
return (*SecurityError)(wrapError(err, "cannot generate application ID:", err))
|
||||
} else {
|
||||
seal.id = id
|
||||
}
|
||||
|
||||
// fetch system constants
|
||||
seal.SystemConstants = internal.GetSC()
|
||||
|
||||
// pass through config values
|
||||
seal.fid = config.ID
|
||||
seal.command = config.Command
|
||||
|
||||
// parses launch method text and looks up tool path
|
||||
switch config.Method {
|
||||
case "sudo":
|
||||
seal.launchOption = LaunchMethodSudo
|
||||
if sudoPath, err := exec.LookPath("sudo"); err != nil {
|
||||
return (*LauncherLookupError)(wrapError(ErrSudo, "sudo not found"))
|
||||
} else {
|
||||
seal.toolPath = sudoPath
|
||||
}
|
||||
case "bubblewrap":
|
||||
seal.launchOption = LaunchMethodBwrap
|
||||
if bwrapPath, err := exec.LookPath("bwrap"); err != nil {
|
||||
return (*LauncherLookupError)(wrapError(ErrBwrap, "bwrap not found"))
|
||||
} else {
|
||||
seal.toolPath = bwrapPath
|
||||
}
|
||||
case "systemd":
|
||||
seal.launchOption = LaunchMethodMachineCtl
|
||||
if !internal.SdBootedV {
|
||||
return (*LauncherLookupError)(wrapError(ErrSystemd,
|
||||
"system has not been booted with systemd as init system"))
|
||||
}
|
||||
|
||||
if machineCtlPath, err := exec.LookPath("machinectl"); err != nil {
|
||||
return (*LauncherLookupError)(wrapError(ErrMachineCtl, "machinectl not found"))
|
||||
} else {
|
||||
seal.toolPath = machineCtlPath
|
||||
}
|
||||
default:
|
||||
return (*SealConfigError)(wrapError(ErrLaunch, "invalid launch method"))
|
||||
}
|
||||
|
||||
// create seal system component
|
||||
seal.sys = new(appSealTx)
|
||||
|
||||
// look up fortify executable path
|
||||
if p, err := os.Executable(); err != nil {
|
||||
return (*LauncherLookupError)(wrapError(err, "cannot look up fortify executable path:", err))
|
||||
} else {
|
||||
seal.sys.executable = p
|
||||
}
|
||||
|
||||
// look up user from system
|
||||
if u, err := user.Lookup(config.User); err != nil {
|
||||
if errors.As(err, new(user.UnknownUserError)) {
|
||||
return (*SealConfigError)(wrapError(ErrUser, "unknown user", config.User))
|
||||
} else {
|
||||
// unreachable
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
seal.sys.User = u
|
||||
}
|
||||
|
||||
// open process state store
|
||||
// the simple store only starts holding an open file after first action
|
||||
// store activity begins after Start is called and must end before Wait
|
||||
seal.store = state.NewSimple(seal.SystemConstants.RunDirPath, seal.sys.Uid)
|
||||
|
||||
// parse string UID
|
||||
if u, err := strconv.Atoi(seal.sys.Uid); err != nil {
|
||||
// unreachable unless kernel bug
|
||||
panic("uid parse")
|
||||
} else {
|
||||
seal.sys.uid = u
|
||||
}
|
||||
|
||||
// pass through enablements
|
||||
seal.et = config.Confinement.Enablements
|
||||
|
||||
// this method calls all share methods in sequence
|
||||
if err := seal.shareAll([2]*dbus.Config{config.Confinement.SessionBus, config.Confinement.SystemBus}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// verbose log seal information
|
||||
verbose.Println("created application seal as user",
|
||||
seal.sys.Username, "("+seal.sys.Uid+"),",
|
||||
"method:", config.Method+",",
|
||||
"launcher:", seal.toolPath+",",
|
||||
"command:", config.Command)
|
||||
|
||||
// seal app and release lock
|
||||
a.seal = seal
|
||||
return nil
|
||||
}
|
|
@ -1,150 +0,0 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/internal"
|
||||
"git.ophivana.moe/cat/fortify/internal/util"
|
||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
||||
)
|
||||
|
||||
const (
|
||||
xdgRuntimeDir = "XDG_RUNTIME_DIR"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
uid int // assigned
|
||||
env []string // modified via AppendEnv
|
||||
command []string // set on initialisation
|
||||
|
||||
exit *internal.ExitState // assigned
|
||||
|
||||
launchOptionText string // set on initialisation
|
||||
launchOption uint8 // assigned
|
||||
|
||||
sharePath string // set on initialisation
|
||||
runtimePath string // assigned
|
||||
runDirPath string // assigned
|
||||
toolPath string // assigned
|
||||
|
||||
enablements internal.Enablements // set via setEnablement
|
||||
*user.User // assigned
|
||||
|
||||
// absolutely *no* method of this type is thread-safe
|
||||
// so don't treat it as if it is
|
||||
}
|
||||
|
||||
func (a *App) LaunchOption() uint8 {
|
||||
return a.launchOption
|
||||
}
|
||||
|
||||
func (a *App) RunDir() string {
|
||||
return a.runDirPath
|
||||
}
|
||||
|
||||
func (a *App) setEnablement(e internal.Enablement) {
|
||||
if a.enablements.Has(e) {
|
||||
panic("enablement " + e.String() + " set twice")
|
||||
}
|
||||
|
||||
a.enablements |= e.Mask()
|
||||
}
|
||||
|
||||
func (a *App) SealExit(exit *internal.ExitState) {
|
||||
if a.exit != nil {
|
||||
panic("application exit state sealed twice")
|
||||
}
|
||||
a.exit = exit
|
||||
}
|
||||
|
||||
func New(userName string, args []string, launchOptionText string) *App {
|
||||
a := &App{
|
||||
command: args,
|
||||
launchOptionText: launchOptionText,
|
||||
sharePath: path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Geteuid())),
|
||||
}
|
||||
|
||||
// runtimePath, runDirPath
|
||||
if r, ok := os.LookupEnv(xdgRuntimeDir); !ok {
|
||||
fmt.Println("Env variable", xdgRuntimeDir, "unset")
|
||||
|
||||
// too early for fatal
|
||||
os.Exit(1)
|
||||
} else {
|
||||
a.runtimePath = r
|
||||
a.runDirPath = path.Join(a.runtimePath, "fortify")
|
||||
verbose.Println("Runtime directory at", a.runDirPath)
|
||||
}
|
||||
|
||||
// *user.User
|
||||
if u, err := user.Lookup(userName); err != nil {
|
||||
if errors.As(err, new(user.UnknownUserError)) {
|
||||
fmt.Println("unknown user", userName)
|
||||
} else {
|
||||
// unreachable
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// too early for fatal
|
||||
os.Exit(1)
|
||||
} else {
|
||||
a.User = u
|
||||
}
|
||||
|
||||
// uid
|
||||
if u, err := strconv.Atoi(a.Uid); err != nil {
|
||||
// usually unreachable
|
||||
panic("uid parse")
|
||||
} else {
|
||||
a.uid = u
|
||||
}
|
||||
|
||||
verbose.Println("Running as user", a.Username, "("+a.Uid+"),", "command:", a.command)
|
||||
if internal.SdBootedV {
|
||||
verbose.Println("System booted with systemd as init system (PID 1).")
|
||||
}
|
||||
|
||||
// launchOption, toolPath
|
||||
switch a.launchOptionText {
|
||||
case "sudo":
|
||||
a.launchOption = LaunchMethodSudo
|
||||
if sudoPath, ok := util.Which("sudo"); !ok {
|
||||
fmt.Println("Did not find 'sudo' in PATH")
|
||||
os.Exit(1)
|
||||
} else {
|
||||
a.toolPath = sudoPath
|
||||
}
|
||||
case "bubblewrap":
|
||||
a.launchOption = LaunchMethodBwrap
|
||||
if bwrapPath, ok := util.Which("bwrap"); !ok {
|
||||
fmt.Println("Did not find 'bwrap' in PATH")
|
||||
os.Exit(1)
|
||||
} else {
|
||||
a.toolPath = bwrapPath
|
||||
}
|
||||
case "systemd":
|
||||
a.launchOption = LaunchMethodMachineCtl
|
||||
if !internal.SdBootedV {
|
||||
fmt.Println("System has not been booted with systemd as init system (PID 1).")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if machineCtlPath, ok := util.Which("machinectl"); !ok {
|
||||
fmt.Println("Did not find 'machinectl' in PATH")
|
||||
} else {
|
||||
a.toolPath = machineCtlPath
|
||||
}
|
||||
default:
|
||||
fmt.Println("invalid launch method")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
verbose.Println("Determined launch method to be", a.launchOptionText, "with tool at", a.toolPath)
|
||||
|
||||
return a
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/acl"
|
||||
"git.ophivana.moe/cat/fortify/dbus"
|
||||
"git.ophivana.moe/cat/fortify/internal/state"
|
||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
||||
)
|
||||
|
||||
const (
|
||||
dbusSessionBusAddress = "DBUS_SESSION_BUS_ADDRESS"
|
||||
dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS"
|
||||
|
||||
xdgDBusProxy = "xdg-dbus-proxy"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDBusConfig = errors.New("dbus config not supplied")
|
||||
ErrDBusProxy = errors.New(xdgDBusProxy + " not found")
|
||||
ErrDBusFault = errors.New(xdgDBusProxy + " did not start correctly")
|
||||
)
|
||||
|
||||
type (
|
||||
SealDBusError BaseError
|
||||
LookupDBusError BaseError
|
||||
StartDBusError BaseError
|
||||
CloseDBusError BaseError
|
||||
)
|
||||
|
||||
func (seal *appSeal) shareDBus(config [2]*dbus.Config) error {
|
||||
if !seal.et.Has(state.EnableDBus) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// session bus is mandatory
|
||||
if config[0] == nil {
|
||||
return (*SealDBusError)(wrapError(ErrDBusConfig, "attempted to seal session bus proxy with nil config"))
|
||||
}
|
||||
|
||||
// system bus is optional
|
||||
seal.sys.dbusSystem = config[1] != nil
|
||||
|
||||
// upstream address, downstream socket path
|
||||
var sessionBus, systemBus [2]string
|
||||
|
||||
// downstream socket paths
|
||||
sessionBus[1] = path.Join(seal.share, "bus")
|
||||
systemBus[1] = path.Join(seal.share, "system_bus_socket")
|
||||
|
||||
// resolve upstream session bus address
|
||||
if addr, ok := os.LookupEnv(dbusSessionBusAddress); !ok {
|
||||
// fall back to default format
|
||||
sessionBus[0] = fmt.Sprintf("unix:path=/run/user/%d/bus", os.Getuid())
|
||||
} else {
|
||||
sessionBus[0] = addr
|
||||
}
|
||||
|
||||
// resolve upstream system bus address
|
||||
if addr, ok := os.LookupEnv(dbusSystemBusAddress); !ok {
|
||||
// fall back to default hardcoded value
|
||||
systemBus[0] = "unix:path=/run/dbus/system_bus_socket"
|
||||
} else {
|
||||
systemBus[0] = addr
|
||||
}
|
||||
|
||||
// look up proxy program path for dbus.New
|
||||
if b, err := exec.LookPath(xdgDBusProxy); err != nil {
|
||||
return (*LookupDBusError)(wrapError(ErrDBusProxy, xdgDBusProxy, "not found"))
|
||||
} else {
|
||||
// create proxy instance
|
||||
seal.sys.dbus = dbus.New(b, sessionBus, systemBus)
|
||||
}
|
||||
|
||||
// seal dbus proxy
|
||||
if err := seal.sys.dbus.Seal(config[0], config[1]); err != nil {
|
||||
return (*SealDBusError)(wrapError(err, "cannot seal message bus proxy:", err))
|
||||
}
|
||||
|
||||
// store addresses for cleanup and logging
|
||||
seal.sys.dbusAddr = &[2][2]string{sessionBus, systemBus}
|
||||
|
||||
// share proxy sockets
|
||||
seal.appendEnv(dbusSessionBusAddress, "unix:path="+sessionBus[1])
|
||||
seal.sys.updatePerm(sessionBus[1], acl.Read, acl.Write)
|
||||
if seal.sys.dbusSystem {
|
||||
seal.appendEnv(dbusSystemBusAddress, "unix:path="+systemBus[1])
|
||||
seal.sys.updatePerm(systemBus[1], acl.Read, acl.Write)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tx *appSealTx) startDBus() error {
|
||||
// ready channel passed to dbus package
|
||||
ready := make(chan bool, 1)
|
||||
// used by waiting goroutine to notify process return
|
||||
tx.dbusWait = make(chan struct{})
|
||||
|
||||
// background dbus proxy start
|
||||
if err := tx.dbus.Start(&ready); err != nil {
|
||||
return (*StartDBusError)(wrapError(err, "cannot start message bus proxy:", err))
|
||||
}
|
||||
|
||||
// background wait for proxy instance and notify completion
|
||||
go func() {
|
||||
if err := tx.dbus.Wait(); err != nil {
|
||||
fmt.Println("fortify: warn: message bus proxy returned error:", err)
|
||||
} else {
|
||||
verbose.Println("message bus proxy uneventful wait")
|
||||
}
|
||||
|
||||
// ensure socket removal so ephemeral directory is empty at revert
|
||||
if err := os.Remove(tx.dbusAddr[0][1]); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
fmt.Println("fortify: cannot remove dangling session bus socket:", err)
|
||||
}
|
||||
if tx.dbusSystem {
|
||||
if err := os.Remove(tx.dbusAddr[1][1]); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
fmt.Println("fortify: cannot remove dangling system bus socket:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// notify proxy completion
|
||||
tx.dbusWait <- struct{}{}
|
||||
}()
|
||||
|
||||
// ready is false if the proxy process faulted
|
||||
if !<-ready {
|
||||
return (*StartDBusError)(wrapError(ErrDBusFault, "message bus proxy failed"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tx *appSealTx) stopDBus() error {
|
||||
if err := tx.dbus.Close(); err != nil {
|
||||
if errors.Is(err, os.ErrClosed) {
|
||||
return (*CloseDBusError)(wrapError(err, "message bus proxy already closed"))
|
||||
} else {
|
||||
return (*CloseDBusError)(wrapError(err, "cannot close message bus proxy:", err))
|
||||
}
|
||||
}
|
||||
|
||||
// block until proxy wait returns
|
||||
<-tx.dbusWait
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/acl"
|
||||
"git.ophivana.moe/cat/fortify/internal/state"
|
||||
)
|
||||
|
||||
const (
|
||||
term = "TERM"
|
||||
display = "DISPLAY"
|
||||
|
||||
// https://manpages.debian.org/experimental/libwayland-doc/wl_display_connect.3.en.html
|
||||
waylandDisplay = "WAYLAND_DISPLAY"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrWayland = errors.New(waylandDisplay + " unset")
|
||||
ErrXDisplay = errors.New(display + " unset")
|
||||
)
|
||||
|
||||
type ErrDisplayEnv BaseError
|
||||
|
||||
func (seal *appSeal) shareDisplay() error {
|
||||
// pass $TERM to launcher
|
||||
if t, ok := os.LookupEnv(term); ok {
|
||||
seal.appendEnv(term, t)
|
||||
}
|
||||
|
||||
// set up wayland
|
||||
if seal.et.Has(state.EnableWayland) {
|
||||
if wd, ok := os.LookupEnv(waylandDisplay); !ok {
|
||||
return (*ErrDisplayEnv)(wrapError(ErrWayland, "WAYLAND_DISPLAY is not set"))
|
||||
} else {
|
||||
// wayland socket path
|
||||
wp := path.Join(seal.RuntimePath, wd)
|
||||
seal.appendEnv(waylandDisplay, wp)
|
||||
|
||||
// ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`)
|
||||
seal.sys.updatePerm(wp, acl.Read, acl.Write, acl.Execute)
|
||||
}
|
||||
}
|
||||
|
||||
// set up X11
|
||||
if seal.et.Has(state.EnableX) {
|
||||
// discover X11 and grant user permission via the `ChangeHosts` command
|
||||
if d, ok := os.LookupEnv(display); !ok {
|
||||
return (*ErrDisplayEnv)(wrapError(ErrXDisplay, "DISPLAY is not set"))
|
||||
} else {
|
||||
seal.sys.changeHosts(seal.sys.Username)
|
||||
seal.appendEnv(display, d)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/acl"
|
||||
"git.ophivana.moe/cat/fortify/internal/state"
|
||||
)
|
||||
|
||||
const (
|
||||
pulseServer = "PULSE_SERVER"
|
||||
pulseCookie = "PULSE_COOKIE"
|
||||
|
||||
home = "HOME"
|
||||
xdgConfigHome = "XDG_CONFIG_HOME"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrPulseCookie = errors.New("pulse cookie not present")
|
||||
ErrPulseSocket = errors.New("pulse socket not present")
|
||||
ErrPulseMode = errors.New("unexpected pulse socket mode")
|
||||
)
|
||||
|
||||
type (
|
||||
PulseCookieAccessError BaseError
|
||||
PulseSocketAccessError BaseError
|
||||
)
|
||||
|
||||
func (seal *appSeal) sharePulse() error {
|
||||
if !seal.et.Has(state.EnablePulse) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensure PulseAudio directory ACL (e.g. `/run/user/%d/pulse`)
|
||||
pd := path.Join(seal.RuntimePath, "pulse")
|
||||
ps := path.Join(pd, "native")
|
||||
if _, err := os.Stat(pd); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return (*PulseSocketAccessError)(wrapError(err,
|
||||
fmt.Sprintf("cannot access PulseAudio directory '%s':", pd), err))
|
||||
}
|
||||
return (*PulseSocketAccessError)(wrapError(ErrPulseSocket,
|
||||
fmt.Sprintf("PulseAudio directory '%s' not found", pd)))
|
||||
}
|
||||
|
||||
seal.appendEnv(pulseServer, "unix:"+ps)
|
||||
seal.sys.updatePerm(pd, acl.Execute)
|
||||
|
||||
// ensure PulseAudio socket permission (e.g. `/run/user/%d/pulse/native`)
|
||||
if s, err := os.Stat(ps); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return (*PulseSocketAccessError)(wrapError(err,
|
||||
fmt.Sprintf("cannot access PulseAudio socket '%s':", ps), err))
|
||||
}
|
||||
return (*PulseSocketAccessError)(wrapError(ErrPulseSocket,
|
||||
fmt.Sprintf("PulseAudio directory '%s' found but socket does not exist", pd)))
|
||||
} else {
|
||||
if m := s.Mode(); m&0o006 != 0o006 {
|
||||
return (*PulseSocketAccessError)(wrapError(ErrPulseMode,
|
||||
fmt.Sprintf("unexpected permissions on '%s':", ps), m))
|
||||
}
|
||||
}
|
||||
|
||||
// publish current user's pulse cookie for target user
|
||||
if src, err := discoverPulseCookie(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
dst := path.Join(seal.share, "pulse-cookie")
|
||||
seal.appendEnv(pulseCookie, dst)
|
||||
seal.sys.copyFile(dst, src)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// discoverPulseCookie attempts various standard methods to discover the current user's PulseAudio authentication cookie
|
||||
func discoverPulseCookie() (string, error) {
|
||||
if p, ok := os.LookupEnv(pulseCookie); ok {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// dotfile $HOME/.pulse-cookie
|
||||
if p, ok := os.LookupEnv(home); ok {
|
||||
p = path.Join(p, ".pulse-cookie")
|
||||
if s, err := os.Stat(p); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return p, (*PulseCookieAccessError)(wrapError(err,
|
||||
fmt.Sprintf("cannot access PulseAudio cookie '%s':", p), err))
|
||||
}
|
||||
// not found, try next method
|
||||
} else if !s.IsDir() {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
// $XDG_CONFIG_HOME/pulse/cookie
|
||||
if p, ok := os.LookupEnv(xdgConfigHome); ok {
|
||||
p = path.Join(p, "pulse", "cookie")
|
||||
if s, err := os.Stat(p); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return p, (*PulseCookieAccessError)(wrapError(err, "cannot access PulseAudio cookie", p+":", err))
|
||||
}
|
||||
// not found, try next method
|
||||
} else if !s.IsDir() {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", (*PulseCookieAccessError)(wrapError(ErrPulseCookie,
|
||||
fmt.Sprintf("cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)",
|
||||
pulseCookie, xdgConfigHome, home)))
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/acl"
|
||||
)
|
||||
|
||||
const (
|
||||
xdgRuntimeDir = "XDG_RUNTIME_DIR"
|
||||
xdgSessionClass = "XDG_SESSION_CLASS"
|
||||
xdgSessionType = "XDG_SESSION_TYPE"
|
||||
)
|
||||
|
||||
// shareRuntime queues actions for sharing/ensuring the runtime and share directories
|
||||
func (seal *appSeal) shareRuntime() {
|
||||
// ensure RunDir (e.g. `/run/user/%d/fortify`)
|
||||
seal.sys.ensure(seal.RunDirPath, 0700)
|
||||
|
||||
// ensure runtime directory ACL (e.g. `/run/user/%d`)
|
||||
seal.sys.updatePerm(seal.RuntimePath, acl.Execute)
|
||||
|
||||
// ensure Share (e.g. `/tmp/fortify.%d`)
|
||||
// acl is unnecessary as this directory is world executable
|
||||
seal.sys.ensure(seal.SharePath, 0701)
|
||||
|
||||
// ensure process-specific share (e.g. `/tmp/fortify.%d/%s`)
|
||||
// acl is unnecessary as this directory is world executable
|
||||
seal.share = path.Join(seal.SharePath, seal.id.String())
|
||||
seal.sys.ensureEphemeral(seal.share, 0701)
|
||||
}
|
||||
|
||||
func (seal *appSeal) shareRuntimeChild() string {
|
||||
// ensure child runtime parent directory (e.g. `/tmp/fortify.%d/runtime`)
|
||||
targetRuntimeParent := path.Join(seal.SharePath, "runtime")
|
||||
seal.sys.ensure(targetRuntimeParent, 0700)
|
||||
seal.sys.updatePerm(targetRuntimeParent, acl.Execute)
|
||||
|
||||
// ensure child runtime directory (e.g. `/tmp/fortify.%d/runtime/%d`)
|
||||
targetRuntime := path.Join(targetRuntimeParent, seal.sys.Uid)
|
||||
seal.sys.ensure(targetRuntime, 0700)
|
||||
seal.sys.updatePerm(targetRuntime, acl.Read, acl.Write, acl.Execute)
|
||||
|
||||
// point to ensured runtime path
|
||||
seal.appendEnv(xdgRuntimeDir, targetRuntime)
|
||||
seal.appendEnv(xdgSessionClass, "user")
|
||||
seal.appendEnv(xdgSessionType, "tty")
|
||||
|
||||
return targetRuntime
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const shimPayload = "FORTIFY_SHIM_PAYLOAD"
|
||||
|
||||
func (a *app) shimPayloadEnv() string {
|
||||
r := &bytes.Buffer{}
|
||||
enc := base64.NewEncoder(base64.StdEncoding, r)
|
||||
|
||||
if err := gob.NewEncoder(enc).Encode(a.seal.command); err != nil {
|
||||
// should be unreachable
|
||||
panic(err)
|
||||
}
|
||||
|
||||
_ = enc.Close()
|
||||
return shimPayload + "=" + r.String()
|
||||
}
|
||||
|
||||
// TryShim attempts the early hidden launcher shim path
|
||||
func TryShim() {
|
||||
// environment variable contains encoded argv
|
||||
if r, ok := os.LookupEnv(shimPayload); ok {
|
||||
// everything beyond this point runs as target user
|
||||
// proceed with caution!
|
||||
|
||||
// parse base64 revealing underlying gob stream
|
||||
dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(r))
|
||||
|
||||
// decode argv gob stream
|
||||
var argv []string
|
||||
if err := gob.NewDecoder(dec).Decode(&argv); err != nil {
|
||||
fmt.Println("fortify-shim: cannot decode shim payload:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// remove payload variable since the child does not need to see it
|
||||
if err := os.Unsetenv(shimPayload); err != nil {
|
||||
fmt.Println("fortify-shim: cannot unset shim payload:", err)
|
||||
// not fatal, do not fail
|
||||
}
|
||||
|
||||
// look up argv0
|
||||
var argv0 string
|
||||
|
||||
if len(argv) > 0 {
|
||||
// look up program from $PATH
|
||||
if p, err := exec.LookPath(argv[0]); err != nil {
|
||||
fmt.Printf("%s not found: %s\n", argv[0], err)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
argv0 = p
|
||||
}
|
||||
} else {
|
||||
// no argv, look up shell instead
|
||||
if argv0, ok = os.LookupEnv("SHELL"); !ok {
|
||||
fmt.Println("fortify-shim: no command was specified and $SHELL was unset")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
argv = []string{argv0}
|
||||
}
|
||||
|
||||
// exec target process
|
||||
if err := syscall.Exec(argv0, argv, os.Environ()); err != nil {
|
||||
fmt.Println("fortify-shim: cannot execute shim payload:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// unreachable
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/internal/state"
|
||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
||||
)
|
||||
|
||||
type (
|
||||
// ProcessError encapsulates errors returned by starting *exec.Cmd
|
||||
ProcessError BaseError
|
||||
)
|
||||
|
||||
// Start starts the fortified child
|
||||
func (a *app) Start() error {
|
||||
a.lock.Lock()
|
||||
defer a.lock.Unlock()
|
||||
|
||||
if err := a.seal.sys.commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// select command builder
|
||||
var commandBuilder func() (args []string)
|
||||
switch a.seal.launchOption {
|
||||
case LaunchMethodSudo:
|
||||
commandBuilder = a.commandBuilderSudo
|
||||
case LaunchMethodBwrap:
|
||||
commandBuilder = a.commandBuilderBwrap
|
||||
case LaunchMethodMachineCtl:
|
||||
commandBuilder = a.commandBuilderMachineCtl
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
// configure child process
|
||||
a.cmd = exec.Command(a.seal.toolPath, commandBuilder()...)
|
||||
a.cmd.Env = []string{}
|
||||
a.cmd.Stdin = os.Stdin
|
||||
a.cmd.Stdout = os.Stdout
|
||||
a.cmd.Stderr = os.Stderr
|
||||
a.cmd.Dir = a.seal.RunDirPath
|
||||
|
||||
// start child process
|
||||
verbose.Println("starting main process:", a.cmd)
|
||||
if err := a.cmd.Start(); err != nil {
|
||||
return (*ProcessError)(wrapError(err, "cannot start process:", err))
|
||||
}
|
||||
startTime := time.Now().UTC()
|
||||
|
||||
// create process state
|
||||
sd := state.State{
|
||||
PID: a.cmd.Process.Pid,
|
||||
Command: a.seal.command,
|
||||
Capability: a.seal.et,
|
||||
Launcher: a.seal.toolPath,
|
||||
Argv: a.cmd.Args,
|
||||
Time: startTime,
|
||||
}
|
||||
|
||||
// register process state
|
||||
var e = new(StateStoreError)
|
||||
e.Inner, e.DoErr = a.seal.store.Do(func(b state.Backend) {
|
||||
e.InnerErr = b.Save(&sd)
|
||||
})
|
||||
return e.equiv("cannot save process state:", e)
|
||||
}
|
||||
|
||||
// StateStoreError is returned for a failed state save
|
||||
type StateStoreError struct {
|
||||
// whether inner function was called
|
||||
Inner bool
|
||||
// error returned by state.Store Do method
|
||||
DoErr error
|
||||
// error returned by state.Backend Save method
|
||||
InnerErr error
|
||||
// any other errors needing to be tracked
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *StateStoreError) equiv(a ...any) error {
|
||||
if e.Inner == true && e.DoErr == nil && e.InnerErr == nil && e.Err == nil {
|
||||
return nil
|
||||
} else {
|
||||
return wrapError(e, a...)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *StateStoreError) Error() string {
|
||||
if e.Inner && e.InnerErr != nil {
|
||||
return e.InnerErr.Error()
|
||||
}
|
||||
|
||||
if e.DoErr != nil {
|
||||
return e.DoErr.Error()
|
||||
}
|
||||
|
||||
if e.Err != nil {
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
return "(nil)"
|
||||
}
|
||||
|
||||
func (e *StateStoreError) Unwrap() (errs []error) {
|
||||
errs = make([]error, 0, 3)
|
||||
if e.DoErr != nil {
|
||||
errs = append(errs, e.DoErr)
|
||||
}
|
||||
if e.InnerErr != nil {
|
||||
errs = append(errs, e.InnerErr)
|
||||
}
|
||||
if e.Err != nil {
|
||||
errs = append(errs, e.Err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type RevertCompoundError interface {
|
||||
Error() string
|
||||
Unwrap() []error
|
||||
}
|
||||
|
||||
func (a *app) Wait() (int, error) {
|
||||
a.lock.Lock()
|
||||
defer a.lock.Unlock()
|
||||
|
||||
var r int
|
||||
|
||||
// wait for process and resolve exit code
|
||||
if err := a.cmd.Wait(); err != nil {
|
||||
var exitError *exec.ExitError
|
||||
if !errors.As(err, &exitError) {
|
||||
// should be unreachable
|
||||
a.wait = err
|
||||
}
|
||||
|
||||
// store non-zero return code
|
||||
r = exitError.ExitCode()
|
||||
} else {
|
||||
r = a.cmd.ProcessState.ExitCode()
|
||||
}
|
||||
|
||||
verbose.Println("process", strconv.Itoa(a.cmd.Process.Pid), "exited with exit code", r)
|
||||
|
||||
// update store and revert app setup transaction
|
||||
e := new(StateStoreError)
|
||||
e.Inner, e.DoErr = a.seal.store.Do(func(b state.Backend) {
|
||||
e.InnerErr = func() error {
|
||||
// destroy defunct state entry
|
||||
if err := b.Destroy(a.cmd.Process.Pid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var global bool
|
||||
|
||||
// measure remaining state entries
|
||||
if l, err := b.Len(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
// clean up global modifications if we're the last launcher alive
|
||||
global = l == 0
|
||||
|
||||
if !global {
|
||||
verbose.Printf("found %d active launchers, cleaning up without globals\n", l)
|
||||
} else {
|
||||
verbose.Println("no other launchers active, will clean up globals")
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: depending on exit sequence, some parts of the transaction never gets reverted
|
||||
if err := a.seal.sys.revert(global); err != nil {
|
||||
return err.(RevertCompoundError)
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
})
|
||||
|
||||
e.Err = a.seal.store.Close()
|
||||
return r, e.equiv("error returned during cleanup:", e)
|
||||
}
|
|
@ -0,0 +1,351 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/user"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/acl"
|
||||
"git.ophivana.moe/cat/fortify/dbus"
|
||||
"git.ophivana.moe/cat/fortify/internal"
|
||||
"git.ophivana.moe/cat/fortify/internal/state"
|
||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
||||
"git.ophivana.moe/cat/fortify/xcb"
|
||||
)
|
||||
|
||||
// appSeal seals the application with child-related information
|
||||
type appSeal struct {
|
||||
// application unique identifier
|
||||
id *appID
|
||||
|
||||
// freedesktop application ID
|
||||
fid string
|
||||
// argv to start process with in the final confined environment
|
||||
command []string
|
||||
// environment variables of fortified process
|
||||
env []string
|
||||
// persistent process state store
|
||||
store state.Store
|
||||
|
||||
// uint8 representation of launch method sealed from config
|
||||
launchOption uint8
|
||||
// process-specific share directory path
|
||||
share string
|
||||
|
||||
// path to launcher program
|
||||
toolPath string
|
||||
// pass-through enablement tracking from config
|
||||
et state.Enablements
|
||||
|
||||
// prevents sharing from happening twice
|
||||
shared bool
|
||||
// seal system-level component
|
||||
sys *appSealTx
|
||||
|
||||
// used in various sealing operations
|
||||
internal.SystemConstants
|
||||
|
||||
// protected by upstream mutex
|
||||
}
|
||||
|
||||
// appendEnv appends an environment variable for the child process
|
||||
func (seal *appSeal) appendEnv(k, v string) {
|
||||
seal.env = append(seal.env, k+"="+v)
|
||||
}
|
||||
|
||||
// appSealTx contains the system-level component of the app seal
|
||||
type appSealTx struct {
|
||||
// reference to D-Bus proxy instance, nil if disabled
|
||||
dbus *dbus.Proxy
|
||||
// notification from goroutine waiting for dbus.Proxy
|
||||
dbusWait chan struct{}
|
||||
// upstream address/downstream path used to initialise dbus.Proxy
|
||||
dbusAddr *[2][2]string
|
||||
// whether system bus proxy is enabled
|
||||
dbusSystem bool
|
||||
|
||||
// paths to append/strip ACLs (of target user) from
|
||||
acl []*appACLEntry
|
||||
// X11 ChangeHosts commands to perform
|
||||
xhost []string
|
||||
// paths of directories to ensure
|
||||
mkdir []appEnsureEntry
|
||||
// dst, src pairs of temporarily shared files
|
||||
tmpfiles [][2]string
|
||||
|
||||
// sealed path to fortify executable, used by shim
|
||||
executable string
|
||||
// target user UID as an integer
|
||||
uid int
|
||||
// target user sealed from config
|
||||
*user.User
|
||||
|
||||
// prevents commit from happening twice
|
||||
complete bool
|
||||
// prevents cleanup from happening twice
|
||||
closed bool
|
||||
|
||||
// protected by upstream mutex
|
||||
}
|
||||
|
||||
type appEnsureEntry struct {
|
||||
path string
|
||||
perm os.FileMode
|
||||
remove bool
|
||||
}
|
||||
|
||||
// ensure appends a directory ensure action
|
||||
func (tx *appSealTx) ensure(path string, perm os.FileMode) {
|
||||
tx.mkdir = append(tx.mkdir, appEnsureEntry{path, perm, false})
|
||||
}
|
||||
|
||||
// ensureEphemeral appends a directory ensure action with removal in rollback
|
||||
func (tx *appSealTx) ensureEphemeral(path string, perm os.FileMode) {
|
||||
tx.mkdir = append(tx.mkdir, appEnsureEntry{path, perm, true})
|
||||
}
|
||||
|
||||
// appACLEntry contains information for applying/reverting an ACL entry
|
||||
type appACLEntry struct {
|
||||
path string
|
||||
perms []acl.Perm
|
||||
}
|
||||
|
||||
func (e *appACLEntry) String() string {
|
||||
var s = []byte("---")
|
||||
for _, p := range e.perms {
|
||||
switch p {
|
||||
case acl.Read:
|
||||
s[0] = 'r'
|
||||
case acl.Write:
|
||||
s[1] = 'w'
|
||||
case acl.Execute:
|
||||
s[2] = 'x'
|
||||
}
|
||||
}
|
||||
return string(s)
|
||||
}
|
||||
|
||||
// updatePerm appends an acl update action
|
||||
func (tx *appSealTx) updatePerm(path string, perms ...acl.Perm) {
|
||||
tx.acl = append(tx.acl, &appACLEntry{path, perms})
|
||||
}
|
||||
|
||||
// changeHosts appends target username of an X11 ChangeHosts action
|
||||
func (tx *appSealTx) changeHosts(username string) {
|
||||
tx.xhost = append(tx.xhost, username)
|
||||
}
|
||||
|
||||
// copyFile appends a tmpfiles action
|
||||
func (tx *appSealTx) copyFile(dst, src string) {
|
||||
tx.tmpfiles = append(tx.tmpfiles, [2]string{dst, src})
|
||||
tx.updatePerm(dst, acl.Read)
|
||||
}
|
||||
|
||||
type (
|
||||
ChangeHostsError BaseError
|
||||
EnsureDirError BaseError
|
||||
TmpfileError BaseError
|
||||
DBusStartError BaseError
|
||||
ACLUpdateError BaseError
|
||||
)
|
||||
|
||||
// commit applies recorded actions
|
||||
// order: xhost, mkdir, tmpfiles, dbus, acl
|
||||
func (tx *appSealTx) commit() error {
|
||||
if tx.complete {
|
||||
panic("seal transaction committed twice")
|
||||
}
|
||||
tx.complete = true
|
||||
|
||||
txp := &appSealTx{}
|
||||
defer func() {
|
||||
// rollback partial commit
|
||||
if txp != nil {
|
||||
// global changes (x11, ACLs) are always repeated and check for other launchers cannot happen here
|
||||
// attempting cleanup here will cause other fortified processes to lose access to them
|
||||
// a better (and more secure) fix is to proxy access to these resources and eliminate the ACLs altogether
|
||||
if err := txp.revert(false); err != nil {
|
||||
fmt.Println("fortify: errors returned reverting partial commit:", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// insert xhost entries
|
||||
for _, username := range tx.xhost {
|
||||
verbose.Printf("inserting XHost entry SI:localuser:%s\n", username)
|
||||
if err := xcb.ChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+username); err != nil {
|
||||
return (*ChangeHostsError)(wrapError(err,
|
||||
fmt.Sprintf("cannot insert XHost entry SI:localuser:%s, %s", username, err)))
|
||||
} else {
|
||||
// register partial commit
|
||||
txp.changeHosts(username)
|
||||
}
|
||||
}
|
||||
|
||||
// ensure directories
|
||||
for _, dir := range tx.mkdir {
|
||||
verbose.Println("ensuring directory mode:", dir.perm.String(), "path:", dir.path)
|
||||
if err := os.Mkdir(dir.path, dir.perm); err != nil && !errors.Is(err, fs.ErrExist) {
|
||||
return (*EnsureDirError)(wrapError(err,
|
||||
fmt.Sprintf("cannot create directory '%s': %s", dir.path, err)))
|
||||
} else {
|
||||
// only ephemeral dirs require rollback
|
||||
if dir.remove {
|
||||
// register partial commit
|
||||
txp.ensureEphemeral(dir.path, dir.perm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// publish tmpfiles
|
||||
for _, tmpfile := range tx.tmpfiles {
|
||||
verbose.Println("publishing tmpfile", tmpfile[0], "from", tmpfile[1])
|
||||
if err := copyFile(tmpfile[0], tmpfile[1]); err != nil {
|
||||
return (*TmpfileError)(wrapError(err,
|
||||
fmt.Sprintf("cannot publish tmpfile '%s' from '%s': %s", tmpfile[0], tmpfile[1], err)))
|
||||
} else {
|
||||
// register partial commit
|
||||
txp.copyFile(tmpfile[0], tmpfile[1])
|
||||
}
|
||||
}
|
||||
|
||||
if tx.dbus != nil {
|
||||
// start dbus proxy
|
||||
verbose.Printf("starting session bus proxy on '%s' for upstream '%s'\n", tx.dbusAddr[0][1], tx.dbusAddr[0][0])
|
||||
if tx.dbusSystem {
|
||||
verbose.Printf("starting system bus proxy on '%s' for upstream '%s'\n", tx.dbusAddr[1][1], tx.dbusAddr[1][0])
|
||||
}
|
||||
if err := tx.startDBus(); err != nil {
|
||||
return (*DBusStartError)(wrapError(err, "cannot start message bus proxy:", err))
|
||||
} else {
|
||||
txp.dbus = tx.dbus
|
||||
txp.dbusAddr = tx.dbusAddr
|
||||
txp.dbusSystem = tx.dbusSystem
|
||||
txp.dbusWait = tx.dbusWait
|
||||
|
||||
verbose.Println(xdgDBusProxy, "launch:", tx.dbus)
|
||||
}
|
||||
}
|
||||
|
||||
// apply ACLs
|
||||
for _, e := range tx.acl {
|
||||
verbose.Println("applying ACL", e, "uid:", tx.Uid, "path:", e.path)
|
||||
if err := acl.UpdatePerm(e.path, tx.uid, e.perms...); err != nil {
|
||||
return (*ACLUpdateError)(wrapError(err,
|
||||
fmt.Sprintf("cannot apply ACL to '%s': %s", e.path, err)))
|
||||
} else {
|
||||
// register partial commit
|
||||
txp.updatePerm(e.path, e.perms...)
|
||||
}
|
||||
}
|
||||
|
||||
// disarm partial commit rollback
|
||||
txp = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// revert rolls back recorded actions
|
||||
// order: acl, dbus, tmpfiles, mkdir, xhost
|
||||
// errors are printed but not treated as fatal
|
||||
func (tx *appSealTx) revert(global bool) error {
|
||||
if tx.closed {
|
||||
panic("seal transaction reverted twice")
|
||||
}
|
||||
tx.closed = true
|
||||
|
||||
// will be slightly over-sized with ephemeral dirs
|
||||
errs := make([]error, 0, len(tx.acl)+1+len(tx.tmpfiles)+len(tx.mkdir)+len(tx.xhost))
|
||||
joinError := func(err error, a ...any) {
|
||||
var e error
|
||||
if err != nil {
|
||||
e = wrapError(err, a...)
|
||||
}
|
||||
errs = append(errs, e)
|
||||
}
|
||||
|
||||
if global {
|
||||
// revert ACLs
|
||||
for _, e := range tx.acl {
|
||||
verbose.Println("stripping ACL", e, "uid:", tx.Uid, "path:", e.path)
|
||||
err := acl.UpdatePerm(e.path, tx.uid)
|
||||
joinError(err, fmt.Sprintf("cannot strip ACL entry from '%s': %s", e.path, err))
|
||||
}
|
||||
}
|
||||
|
||||
if tx.dbus != nil {
|
||||
// stop dbus proxy
|
||||
verbose.Println("terminating message bus proxy")
|
||||
err := tx.stopDBus()
|
||||
joinError(err, "cannot stop message bus proxy:", err)
|
||||
}
|
||||
|
||||
// remove tmpfiles
|
||||
for _, tmpfile := range tx.tmpfiles {
|
||||
verbose.Println("removing tmpfile", tmpfile[0])
|
||||
err := os.Remove(tmpfile[0])
|
||||
joinError(err, fmt.Sprintf("cannot remove tmpfile '%s': %s", tmpfile[0], err))
|
||||
}
|
||||
|
||||
// remove (empty) ephemeral directories
|
||||
for i := len(tx.mkdir); i > 0; i-- {
|
||||
dir := tx.mkdir[i-1]
|
||||
if !dir.remove {
|
||||
continue
|
||||
}
|
||||
|
||||
verbose.Println("destroying ephemeral directory mode:", dir.perm.String(), "path:", dir.path)
|
||||
err := os.Remove(dir.path)
|
||||
joinError(err, fmt.Sprintf("cannot remove ephemeral directory '%s': %s", dir.path, err))
|
||||
}
|
||||
|
||||
if global {
|
||||
// rollback xhost insertions
|
||||
for _, username := range tx.xhost {
|
||||
verbose.Printf("deleting XHost entry SI:localuser:%s\n", username)
|
||||
err := xcb.ChangeHosts(xcb.HostModeDelete, xcb.FamilyServerInterpreted, "localuser\x00"+username)
|
||||
joinError(err, "cannot remove XHost entry:", err)
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// shareAll calls all share methods in sequence
|
||||
func (seal *appSeal) shareAll(bus [2]*dbus.Config) error {
|
||||
if seal.shared {
|
||||
panic("seal shared twice")
|
||||
}
|
||||
seal.shared = true
|
||||
|
||||
seal.shareRuntime()
|
||||
if err := seal.shareDisplay(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := seal.sharePulse(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ensure dbus session bus defaults
|
||||
if bus[0] == nil {
|
||||
bus[0] = dbus.NewConfig(seal.fid, true, true)
|
||||
}
|
||||
|
||||
if err := seal.shareDBus(bus); err != nil {
|
||||
return err
|
||||
} else if seal.sys.dbusAddr != nil { // set if D-Bus enabled and share successful
|
||||
verbose.Println("sealed session proxy", bus[0].Args(seal.sys.dbusAddr[0]))
|
||||
if bus[1] != nil {
|
||||
verbose.Println("sealed system proxy", bus[1].Args(seal.sys.dbusAddr[1]))
|
||||
}
|
||||
}
|
||||
|
||||
// workaround for launch method sudo
|
||||
if seal.launchOption == LaunchMethodSudo {
|
||||
targetRuntime := seal.shareRuntimeChild()
|
||||
verbose.Printf("child runtime data dir '%s' configured\n", targetRuntime)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/acl"
|
||||
"git.ophivana.moe/cat/fortify/internal"
|
||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
||||
)
|
||||
|
||||
const (
|
||||
// https://manpages.debian.org/experimental/libwayland-doc/wl_display_connect.3.en.html
|
||||
waylandDisplay = "WAYLAND_DISPLAY"
|
||||
)
|
||||
|
||||
func (a *App) ShareWayland() {
|
||||
a.setEnablement(internal.EnableWayland)
|
||||
|
||||
// ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`)
|
||||
if w, ok := os.LookupEnv(waylandDisplay); !ok {
|
||||
internal.Fatal("Wayland: WAYLAND_DISPLAY not set")
|
||||
} else {
|
||||
// add environment variable for new process
|
||||
wp := path.Join(a.runtimePath, w)
|
||||
a.AppendEnv(waylandDisplay, wp)
|
||||
if err := acl.UpdatePerm(wp, a.UID(), acl.Read, acl.Write, acl.Execute); err != nil {
|
||||
internal.Fatal(fmt.Sprintf("Error preparing Wayland '%s':", w), err)
|
||||
} else {
|
||||
a.exit.RegisterRevertPath(wp)
|
||||
}
|
||||
verbose.Printf("Wayland socket '%s' configured\n", w)
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/internal"
|
||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
||||
"git.ophivana.moe/cat/fortify/xcb"
|
||||
)
|
||||
|
||||
const display = "DISPLAY"
|
||||
|
||||
func (a *App) ShareX() {
|
||||
a.setEnablement(internal.EnableX)
|
||||
|
||||
// discovery X11 and grant user permission via the `ChangeHosts` command
|
||||
if d, ok := os.LookupEnv(display); !ok {
|
||||
internal.Fatal("X11: DISPLAY not set")
|
||||
} else {
|
||||
// add environment variable for new process
|
||||
a.AppendEnv(display, d)
|
||||
|
||||
verbose.Printf("X11: Adding XHost entry SI:localuser:%s to display '%s'\n", a.Username, d)
|
||||
if err := xcb.ChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+a.Username); err != nil {
|
||||
internal.Fatal(fmt.Sprintf("Error adding XHost entry to '%s':", d), err)
|
||||
} else {
|
||||
a.exit.XcbActionComplete()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,16 +1,34 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
)
|
||||
|
||||
"git.ophivana.moe/cat/fortify/internal/util"
|
||||
const (
|
||||
systemdCheckPath = "/run/systemd/system"
|
||||
)
|
||||
|
||||
var SdBootedV = func() bool {
|
||||
if v, err := util.SdBooted(); err != nil {
|
||||
if v, err := SdBooted(); err != nil {
|
||||
fmt.Println("warn: read systemd marker:", err)
|
||||
return false
|
||||
} else {
|
||||
return v
|
||||
}
|
||||
}()
|
||||
|
||||
// SdBooted implements https://www.freedesktop.org/software/systemd/man/sd_booted.html
|
||||
func SdBooted() (bool, error) {
|
||||
_, err := os.Stat(systemdCheckPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
err = nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
package internal
|
||||
|
||||
type (
|
||||
Enablement uint8
|
||||
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
|
||||
}
|
||||
|
||||
func (es Enablements) Has(e Enablement) bool {
|
||||
return es&e.Mask() != 0
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
||||
)
|
||||
|
||||
// state that remain constant for the lifetime of the process
|
||||
// fetched and cached here
|
||||
|
||||
const (
|
||||
xdgRuntimeDir = "XDG_RUNTIME_DIR"
|
||||
)
|
||||
|
||||
// SystemConstants contains state from the operating system
|
||||
type SystemConstants struct {
|
||||
// path to shared directory e.g. /tmp/fortify.%d
|
||||
SharePath string `json:"share_path"`
|
||||
// XDG_RUNTIME_DIR value e.g. /run/user/%d
|
||||
RuntimePath string `json:"runtime_path"`
|
||||
// application runtime directory e.g. /run/user/%d/fortify
|
||||
RunDirPath string `json:"run_dir_path"`
|
||||
}
|
||||
|
||||
var (
|
||||
scVal SystemConstants
|
||||
scOnce sync.Once
|
||||
)
|
||||
|
||||
func copySC() {
|
||||
sc := SystemConstants{
|
||||
SharePath: path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Geteuid())),
|
||||
}
|
||||
|
||||
verbose.Println("process share directory at", sc.SharePath)
|
||||
|
||||
// runtimePath, runDirPath
|
||||
if r, ok := os.LookupEnv(xdgRuntimeDir); !ok {
|
||||
fmt.Println("Env variable", xdgRuntimeDir, "unset")
|
||||
os.Exit(1)
|
||||
} else {
|
||||
sc.RuntimePath = r
|
||||
sc.RunDirPath = path.Join(sc.RuntimePath, "fortify")
|
||||
verbose.Println("XDG runtime directory at", sc.RunDirPath)
|
||||
}
|
||||
|
||||
scVal = sc
|
||||
}
|
||||
|
||||
// GetSC returns a populated SystemConstants value
|
||||
func GetSC() SystemConstants {
|
||||
scOnce.Do(copySC)
|
||||
return scVal
|
||||
}
|
176
internal/exit.go
176
internal/exit.go
|
@ -1,176 +0,0 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/user"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/acl"
|
||||
"git.ophivana.moe/cat/fortify/dbus"
|
||||
"git.ophivana.moe/cat/fortify/internal/verbose"
|
||||
"git.ophivana.moe/cat/fortify/xcb"
|
||||
)
|
||||
|
||||
// ExitState keeps track of various changes fortify made to the system
|
||||
// as well as other resources that need to be manually released.
|
||||
// NOT thread safe.
|
||||
type ExitState struct {
|
||||
// target fortified user inherited from app.App
|
||||
user *user.User
|
||||
// integer UID of targeted user
|
||||
uid int
|
||||
// returns amount of launcher states read
|
||||
launcherStateCount func() (int, error)
|
||||
|
||||
// paths to strip ACLs (of target user) from
|
||||
aclCleanupCandidate []string
|
||||
// target process capability enablements
|
||||
enablements *Enablements
|
||||
// whether the xcb.ChangeHosts action was complete
|
||||
xcbActionComplete bool
|
||||
|
||||
// reference to D-Bus proxy instance, nil if disabled
|
||||
dbusProxy *dbus.Proxy
|
||||
// D-Bus wait complete notification
|
||||
dbusDone *chan struct{}
|
||||
|
||||
// path to fortify process state information
|
||||
statePath string
|
||||
|
||||
// prevents cleanup from happening twice
|
||||
complete bool
|
||||
}
|
||||
|
||||
// RegisterRevertPath registers a path with ACLs added by fortify
|
||||
func (s *ExitState) RegisterRevertPath(p string) {
|
||||
s.aclCleanupCandidate = append(s.aclCleanupCandidate, p)
|
||||
}
|
||||
|
||||
// SealEnablements submits the child process enablements
|
||||
func (s *ExitState) SealEnablements(e Enablements) {
|
||||
if s.enablements != nil {
|
||||
panic("enablement exit state set twice")
|
||||
}
|
||||
s.enablements = &e
|
||||
}
|
||||
|
||||
// XcbActionComplete submits xcb.ChangeHosts action completion
|
||||
func (s *ExitState) XcbActionComplete() {
|
||||
if s.xcbActionComplete {
|
||||
Fatal("xcb inserted twice")
|
||||
}
|
||||
s.xcbActionComplete = true
|
||||
}
|
||||
|
||||
// SealDBus submits the child's D-Bus proxy instance
|
||||
func (s *ExitState) SealDBus(p *dbus.Proxy, done *chan struct{}) {
|
||||
if p == nil {
|
||||
Fatal("unexpected nil dbus proxy exit state submitted")
|
||||
}
|
||||
if s.dbusProxy != nil {
|
||||
Fatal("dbus proxy exit state set twice")
|
||||
}
|
||||
s.dbusProxy = p
|
||||
s.dbusDone = done
|
||||
}
|
||||
|
||||
// SealStatePath submits filesystem path to the fortify process's state file
|
||||
func (s *ExitState) SealStatePath(v string) {
|
||||
if s.statePath != "" {
|
||||
panic("statePath set twice")
|
||||
}
|
||||
|
||||
s.statePath = v
|
||||
}
|
||||
|
||||
// NewExit initialises a new ExitState containing basic, unchanging information
|
||||
// about the fortify process required during cleanup
|
||||
func NewExit(u *user.User, uid int, f func() (int, error)) *ExitState {
|
||||
return &ExitState{
|
||||
uid: uid,
|
||||
user: u,
|
||||
|
||||
launcherStateCount: f,
|
||||
}
|
||||
}
|
||||
|
||||
func Fatal(msg ...any) {
|
||||
fmt.Println(msg...)
|
||||
BeforeExit()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var exitState *ExitState
|
||||
|
||||
func SealExit(s *ExitState) {
|
||||
if exitState != nil {
|
||||
panic("exit state submitted twice")
|
||||
}
|
||||
|
||||
exitState = s
|
||||
}
|
||||
|
||||
func BeforeExit() {
|
||||
if exitState == nil {
|
||||
fmt.Println("warn: cleanup attempted before exit state submission")
|
||||
return
|
||||
}
|
||||
|
||||
exitState.beforeExit()
|
||||
}
|
||||
|
||||
func (s *ExitState) beforeExit() {
|
||||
if s.complete {
|
||||
panic("beforeExit called twice")
|
||||
}
|
||||
|
||||
if s.statePath == "" {
|
||||
verbose.Println("State path is unset")
|
||||
} else {
|
||||
if err := os.Remove(s.statePath); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
fmt.Println("Error removing state file:", err)
|
||||
}
|
||||
}
|
||||
|
||||
if count, err := s.launcherStateCount(); err != nil {
|
||||
fmt.Println("Error reading active launchers:", err)
|
||||
os.Exit(1)
|
||||
} else if count > 0 {
|
||||
// other launchers are still active
|
||||
verbose.Printf("Found %d active launchers, exiting without cleaning up\n", count)
|
||||
return
|
||||
}
|
||||
|
||||
verbose.Println("No other launchers active, will clean up")
|
||||
|
||||
if s.xcbActionComplete {
|
||||
verbose.Printf("X11: Removing XHost entry SI:localuser:%s\n", s.user.Username)
|
||||
if err := xcb.ChangeHosts(xcb.HostModeDelete, xcb.FamilyServerInterpreted, "localuser\x00"+s.user.Username); err != nil {
|
||||
fmt.Println("Error removing XHost entry:", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, candidate := range s.aclCleanupCandidate {
|
||||
if err := acl.UpdatePerm(candidate, s.uid); err != nil {
|
||||
fmt.Printf("Error stripping ACL entry from '%s': %s\n", candidate, err)
|
||||
}
|
||||
verbose.Printf("Stripped ACL entry for user '%s' from '%s'\n", s.user.Username, candidate)
|
||||
}
|
||||
|
||||
if s.dbusProxy != nil {
|
||||
verbose.Println("D-Bus proxy registered, cleaning up")
|
||||
|
||||
if err := s.dbusProxy.Close(); err != nil {
|
||||
if errors.Is(err, os.ErrClosed) {
|
||||
verbose.Println("D-Bus proxy already closed")
|
||||
} else {
|
||||
fmt.Println("Error closing D-Bus proxy:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// wait for Proxy.Wait to return
|
||||
<-*s.dbusDone
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
func (s *simpleStore) mustPrintLauncherState(w **tabwriter.Writer, now time.Time) {
|
||||
var innerErr error
|
||||
|
||||
if ok, err := s.Do(func(b Backend) {
|
||||
innerErr = func() error {
|
||||
// read launcher states
|
||||
states, err := b.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// initialise tabwriter if nil
|
||||
if *w == nil {
|
||||
*w = tabwriter.NewWriter(os.Stdout, 0, 1, 4, ' ', 0)
|
||||
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
if *w == nil {
|
||||
*w = tabwriter.NewWriter(os.Stdout, 0, 1, 4, ' ', 0)
|
||||
|
||||
if !verbose.Get() {
|
||||
_, _ = fmt.Fprintln(*w, "\tUID\tPID\tEnablements\tLauncher\tCommand")
|
||||
} else {
|
||||
_, _ = fmt.Fprintln(*w, "\tUID\tPID\tArgv")
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
systemdCheckPath = "/run/systemd/system"
|
||||
)
|
||||
|
||||
// SdBooted implements https://www.freedesktop.org/software/systemd/man/sd_booted.html
|
||||
func SdBooted() (bool, error) {
|
||||
_, err := os.Stat(systemdCheckPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
err = nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
|
@ -2,14 +2,16 @@ package verbose
|
|||
|
||||
import "fmt"
|
||||
|
||||
const prefix = "fortify:"
|
||||
|
||||
func Println(a ...any) {
|
||||
if verbose.Load() {
|
||||
fmt.Println(a...)
|
||||
fmt.Println(append([]any{prefix}, a...)...)
|
||||
}
|
||||
}
|
||||
|
||||
func Printf(format string, a ...any) {
|
||||
if verbose.Load() {
|
||||
fmt.Printf(format, a...)
|
||||
fmt.Printf(prefix+" "+format, a...)
|
||||
}
|
||||
}
|
||||
|
|
201
main.go
201
main.go
|
@ -5,10 +5,7 @@ import (
|
|||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/dbus"
|
||||
"git.ophivana.moe/cat/fortify/internal"
|
||||
|
@ -19,14 +16,6 @@ import (
|
|||
|
||||
var (
|
||||
Version = "impure"
|
||||
|
||||
a *app.App
|
||||
s *internal.ExitState
|
||||
|
||||
dbusSession *dbus.Config
|
||||
dbusSystem *dbus.Config
|
||||
|
||||
launchOptionText string
|
||||
)
|
||||
|
||||
func tryVersion() {
|
||||
|
@ -40,31 +29,133 @@ func main() {
|
|||
flag.Parse()
|
||||
verbose.Set(flagVerbose)
|
||||
|
||||
if internal.SdBootedV {
|
||||
verbose.Println("system booted with systemd as init system")
|
||||
}
|
||||
|
||||
// launcher payload early exit
|
||||
app.Early(printVersion)
|
||||
if printVersion && printLicense {
|
||||
app.TryShim()
|
||||
}
|
||||
|
||||
// version/license command early exit
|
||||
tryVersion()
|
||||
tryLicense()
|
||||
|
||||
a = app.New(userName, flag.Args(), launchOptionText)
|
||||
s = internal.NewExit(a.User, a.UID(), func() (int, error) {
|
||||
d, err := state.ReadLaunchers(a.RunDir(), a.Uid, false)
|
||||
return len(d), err
|
||||
})
|
||||
a.SealExit(s)
|
||||
internal.SealExit(s)
|
||||
// state query command early exit
|
||||
tryState()
|
||||
|
||||
// parse D-Bus config file if applicable
|
||||
// prepare config
|
||||
var config *app.Config
|
||||
|
||||
if confPath == "nil" {
|
||||
// config from flags
|
||||
config = configFromFlags()
|
||||
} else {
|
||||
// config from file
|
||||
if f, err := os.Open(confPath); err != nil {
|
||||
fatalf("cannot access config file '%s': %s\n", confPath, err)
|
||||
} else {
|
||||
if err = json.NewDecoder(f).Decode(&config); err != nil {
|
||||
fatalf("cannot parse config file '%s': %s\n", confPath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// invoke app
|
||||
r := 1
|
||||
a := app.New()
|
||||
if err := a.Seal(config); err != nil {
|
||||
logBaseError(err, "fortify: cannot seal app:")
|
||||
} else if err = a.Start(); err != nil {
|
||||
logBaseError(err, "fortify: cannot start app:")
|
||||
} else if r, err = a.Wait(); err != nil {
|
||||
r = 1
|
||||
|
||||
var e *app.BaseError
|
||||
if !app.AsBaseError(err, &e) {
|
||||
fmt.Println("fortify: wait failed:", err)
|
||||
} else {
|
||||
// Wait only returns either *app.ProcessError or *app.StateStoreError wrapped in a *app.BaseError
|
||||
var se *app.StateStoreError
|
||||
if !errors.As(err, &se) {
|
||||
// does not need special handling
|
||||
fmt.Print("fortify: " + e.Message())
|
||||
} else {
|
||||
// inner error are either unwrapped store errors
|
||||
// or joined errors returned by *appSealTx revert
|
||||
// wrapped in *app.BaseError
|
||||
var ej app.RevertCompoundError
|
||||
if !errors.As(se.InnerErr, &ej) {
|
||||
// does not require special handling
|
||||
fmt.Print("fortify: " + e.Message())
|
||||
} else {
|
||||
errs := ej.Unwrap()
|
||||
|
||||
// every error here is wrapped in *app.BaseError
|
||||
for _, ei := range errs {
|
||||
var eb *app.BaseError
|
||||
if !errors.As(ei, &eb) {
|
||||
// unreachable
|
||||
fmt.Println("fortify: invalid error type returned by revert:", ei)
|
||||
} else {
|
||||
// print inner *app.BaseError message
|
||||
fmt.Print("fortify: " + eb.Message())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := a.WaitErr(); err != nil {
|
||||
fmt.Println("fortify: inner wait failed:", err)
|
||||
}
|
||||
os.Exit(r)
|
||||
}
|
||||
|
||||
func logBaseError(err error, message string) {
|
||||
var e *app.BaseError
|
||||
|
||||
if app.AsBaseError(err, &e) {
|
||||
fmt.Print("fortify: " + e.Message())
|
||||
} else {
|
||||
fmt.Println(message, err)
|
||||
}
|
||||
}
|
||||
|
||||
func configFromFlags() (config *app.Config) {
|
||||
// initialise config from flags
|
||||
config = &app.Config{
|
||||
ID: dbusID,
|
||||
User: userName,
|
||||
Command: flag.Args(),
|
||||
Method: launchMethodText,
|
||||
}
|
||||
|
||||
// enablements from flags
|
||||
if mustWayland {
|
||||
config.Confinement.Enablements.Set(state.EnableWayland)
|
||||
}
|
||||
if mustX {
|
||||
config.Confinement.Enablements.Set(state.EnableX)
|
||||
}
|
||||
if mustDBus {
|
||||
config.Confinement.Enablements.Set(state.EnableDBus)
|
||||
}
|
||||
if mustPulse {
|
||||
config.Confinement.Enablements.Set(state.EnablePulse)
|
||||
}
|
||||
|
||||
// parse D-Bus config file from flags if applicable
|
||||
if mustDBus {
|
||||
if dbusConfigSession == "builtin" {
|
||||
dbusSession = dbus.NewConfig(dbusID, true, mpris)
|
||||
config.Confinement.SessionBus = dbus.NewConfig(dbusID, true, mpris)
|
||||
} else {
|
||||
if f, err := os.Open(dbusConfigSession); err != nil {
|
||||
internal.Fatal("Error opening D-Bus proxy config file:", err)
|
||||
fatalf("cannot access session bus proxy config file '%s': %s\n", dbusConfigSession, err)
|
||||
} else {
|
||||
if err = json.NewDecoder(f).Decode(&dbusSession); err != nil {
|
||||
internal.Fatal("Error parsing D-Bus proxy config file:", err)
|
||||
if err = json.NewDecoder(f).Decode(&config.Confinement.SessionBus); err != nil {
|
||||
fatalf("cannot parse session bus proxy config file '%s': %s\n", dbusConfigSession, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -72,62 +163,24 @@ func main() {
|
|||
// system bus proxy is optional
|
||||
if dbusConfigSystem != "nil" {
|
||||
if f, err := os.Open(dbusConfigSystem); err != nil {
|
||||
internal.Fatal("Error opening D-Bus proxy config file:", err)
|
||||
fatalf("cannot access system bus proxy config file '%s': %s\n", dbusConfigSystem, err)
|
||||
} else {
|
||||
if err = json.NewDecoder(f).Decode(&dbusSystem); err != nil {
|
||||
internal.Fatal("Error parsing D-Bus proxy config file:", err)
|
||||
if err = json.NewDecoder(f).Decode(&config.Confinement.SystemBus); err != nil {
|
||||
fatalf("cannot parse system bus proxy config file '%s': %s\n", dbusConfigSystem, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ensure RunDir (e.g. `/run/user/%d/fortify`)
|
||||
a.EnsureRunDir()
|
||||
|
||||
// state query command early exit
|
||||
tryState()
|
||||
|
||||
// ensure Share (e.g. `/tmp/fortify.%d`)
|
||||
a.EnsureShare()
|
||||
|
||||
// warn about target user home directory ownership
|
||||
if stat, err := os.Stat(a.HomeDir); err != nil {
|
||||
if verbose.Get() {
|
||||
switch {
|
||||
case errors.Is(err, fs.ErrPermission):
|
||||
fmt.Printf("User %s home directory %s is not accessible\n", a.Username, a.HomeDir)
|
||||
case errors.Is(err, fs.ErrNotExist):
|
||||
fmt.Printf("User %s home directory %s does not exis\n", a.Username, a.HomeDir)
|
||||
default:
|
||||
fmt.Printf("Error stat user %s home directory %s: %s\n", a.Username, a.HomeDir, err)
|
||||
}
|
||||
}
|
||||
return
|
||||
} else {
|
||||
// FreeBSD: not cross-platform
|
||||
if u := strconv.Itoa(int(stat.Sys().(*syscall.Stat_t).Uid)); u != a.Uid {
|
||||
fmt.Printf("User %s home directory %s has incorrect ownership (expected UID %s, found %s)", a.Username, a.HomeDir, a.Uid, u)
|
||||
if dbusVerbose {
|
||||
config.Confinement.SessionBus.Log = true
|
||||
config.Confinement.SystemBus.Log = true
|
||||
}
|
||||
}
|
||||
|
||||
// ensure runtime directory ACL (e.g. `/run/user/%d`)
|
||||
a.EnsureRuntime()
|
||||
|
||||
if mustWayland {
|
||||
a.ShareWayland()
|
||||
}
|
||||
|
||||
if mustX {
|
||||
a.ShareX()
|
||||
}
|
||||
|
||||
if mustDBus {
|
||||
a.ShareDBus(dbusSession, dbusSystem, dbusVerbose)
|
||||
}
|
||||
|
||||
if mustPulse {
|
||||
a.SharePulse()
|
||||
}
|
||||
|
||||
a.Run()
|
||||
return
|
||||
}
|
||||
|
||||
func fatalf(format string, a ...any) {
|
||||
fmt.Printf("fortify: "+format, a...)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
|
37
state.go
37
state.go
|
@ -3,40 +3,33 @@ package main
|
|||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"git.ophivana.moe/cat/fortify/internal/state"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
"git.ophivana.moe/cat/fortify/internal/state"
|
||||
)
|
||||
|
||||
var (
|
||||
stateActionEarly [2]bool
|
||||
stateActionEarly bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&stateActionEarly[0], "state", false, "print state information of active launchers")
|
||||
flag.BoolVar(&stateActionEarly[1], "state-current", false, "print state information of active launchers for the specified user")
|
||||
flag.BoolVar(&stateActionEarly, "state", false, "print state information of active launchers")
|
||||
}
|
||||
|
||||
// tryState is called after app initialisation
|
||||
func tryState() {
|
||||
var w *tabwriter.Writer
|
||||
|
||||
switch {
|
||||
case stateActionEarly[0]:
|
||||
state.MustPrintLauncherStateGlobal(&w, a.RunDir())
|
||||
case stateActionEarly[1]:
|
||||
state.MustPrintLauncherState(&w, a.RunDir(), a.Uid)
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
if w != nil {
|
||||
if err := w.Flush(); err != nil {
|
||||
fmt.Println("warn: error formatting output:", err)
|
||||
if stateActionEarly {
|
||||
var w *tabwriter.Writer
|
||||
state.MustPrintLauncherStateSimpleGlobal(&w)
|
||||
if w != nil {
|
||||
if err := w.Flush(); err != nil {
|
||||
fmt.Println("warn: error formatting output:", err)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("No information available")
|
||||
}
|
||||
} else {
|
||||
fmt.Println("No information available")
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,13 +20,29 @@ const (
|
|||
FamilyInternet6 = C.XCB_FAMILY_INTERNET_6
|
||||
)
|
||||
|
||||
type ConnectionError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *ConnectionError) Error() string {
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
func (e *ConnectionError) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
var (
|
||||
ErrChangeHosts = errors.New("xcb_change_hosts() failed")
|
||||
)
|
||||
|
||||
func ChangeHosts(mode, family C.uint8_t, address string) error {
|
||||
var c *C.xcb_connection_t
|
||||
c = C.xcb_connect(nil, nil)
|
||||
defer C.xcb_disconnect(c)
|
||||
|
||||
if err := xcbHandleConnectionError(c); err != nil {
|
||||
return err
|
||||
return &ConnectionError{err}
|
||||
}
|
||||
|
||||
addr := C.CString(address)
|
||||
|
@ -34,13 +50,13 @@ func ChangeHosts(mode, family C.uint8_t, address string) error {
|
|||
C.free(unsafe.Pointer(addr))
|
||||
|
||||
if err := xcbHandleConnectionError(c); err != nil {
|
||||
return err
|
||||
return &ConnectionError{err}
|
||||
}
|
||||
|
||||
e := C.xcb_request_check(c, cookie)
|
||||
if e != nil {
|
||||
defer C.free(unsafe.Pointer(e))
|
||||
return errors.New("xcb_change_hosts() failed")
|
||||
return ErrChangeHosts
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
Loading…
Reference in New Issue