app: separate instance from process state
All checks were successful
Test / Create distribution (push) Successful in 19s
Test / Run NixOS test (push) Successful in 1m59s

This works better for the implementation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
Ophestra 2025-02-21 16:00:31 +09:00
parent 9d9a165379
commit c64b8163e7
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
7 changed files with 102 additions and 69 deletions

View File

@ -6,16 +6,22 @@ import (
) )
type App interface { type App interface {
// ID returns a copy of App's unique ID. // ID returns a copy of [fst.ID] held by App.
ID() ID ID() ID
// Run sets up the system and runs the App.
Run(ctx context.Context, rs *RunState) error
Seal(config *Config) error // Seal determines the outcome of config as a [SealedApp].
// The value of config might be overwritten and must not be used again.
Seal(config *Config) (SealedApp, error)
String() string String() string
} }
// RunState stores the outcome of a call to [App.Run]. type SealedApp interface {
// Run commits sealed system setup and starts the app process.
Run(ctx context.Context, rs *RunState) error
}
// RunState stores the outcome of a call to [SealedApp.Run].
type RunState struct { type RunState struct {
// Time is the exact point in time where the process was created. // Time is the exact point in time where the process was created.
// Location must be set to UTC. // Location must be set to UTC.

View File

@ -2,6 +2,7 @@ package app
import ( import (
"fmt" "fmt"
"log"
"sync" "sync"
"git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/fst"
@ -20,15 +21,23 @@ func New(os sys.State) (fst.App, error) {
return a, err return a, err
} }
func MustNew(os sys.State) fst.App {
a, err := New(os)
if err != nil {
log.Fatalf("cannot create app: %v", err)
}
return a
}
type app struct { type app struct {
id *stringPair[fst.ID] id *stringPair[fst.ID]
sys sys.State sys sys.State
*appSeal *outcome
mu sync.RWMutex mu sync.RWMutex
} }
func (a *app) ID() fst.ID { return a.id.unwrap() } func (a *app) ID() fst.ID { a.mu.RLock(); defer a.mu.RUnlock(); return a.id.unwrap() }
func (a *app) String() string { func (a *app) String() string {
if a == nil { if a == nil {
@ -38,32 +47,33 @@ func (a *app) String() string {
a.mu.RLock() a.mu.RLock()
defer a.mu.RUnlock() defer a.mu.RUnlock()
if a.appSeal != nil { if a.outcome != nil {
if a.appSeal.user.uid == nil { if a.outcome.user.uid == nil {
return fmt.Sprintf("(sealed app %s with invalid uid)", a.id) return fmt.Sprintf("(sealed app %s with invalid uid)", a.id)
} }
return fmt.Sprintf("(sealed app %s as uid %s)", a.id, a.appSeal.user.uid) return fmt.Sprintf("(sealed app %s as uid %s)", a.id, a.outcome.user.uid)
} }
return fmt.Sprintf("(unsealed app %s)", a.id) return fmt.Sprintf("(unsealed app %s)", a.id)
} }
func (a *app) Seal(config *fst.Config) (err error) { func (a *app) Seal(config *fst.Config) (fst.SealedApp, error) {
a.mu.Lock() a.mu.Lock()
defer a.mu.Unlock() defer a.mu.Unlock()
if a.appSeal != nil { if a.outcome != nil {
panic("app sealed twice") panic("app sealed twice")
} }
if config == nil { if config == nil {
return fmsg.WrapError(ErrConfig, return nil, fmsg.WrapError(ErrConfig,
"attempted to seal app with nil config") "attempted to seal app with nil config")
} }
seal := new(appSeal) seal := new(outcome)
err = seal.finalise(a.sys, config, a.id.String()) seal.id = a.id
err := seal.finalise(a.sys, config)
if err == nil { if err == nil {
a.appSeal = seal a.outcome = seal
} }
return return seal, err
} }

View File

@ -29,17 +29,21 @@ func TestApp(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
a := app.NewWithID(tc.id, tc.os) a := app.NewWithID(tc.id, tc.os)
var (
gotSys *system.I
gotBwrap *bwrap.Config
)
if !t.Run("seal", func(t *testing.T) { if !t.Run("seal", func(t *testing.T) {
if err := a.Seal(tc.config); err != nil { if sa, err := a.Seal(tc.config); err != nil {
t.Errorf("Seal: error = %v", err) t.Errorf("Seal: error = %v", err)
return
} else {
gotSys, gotBwrap = app.AppSystemBwrap(a, sa)
} }
}) { }) {
return return
} }
gotSys, gotBwrap := app.AppSystemBwrap(a)
t.Run("compare sys", func(t *testing.T) { t.Run("compare sys", func(t *testing.T) {
if !gotSys.Equal(tc.wantSys) { if !gotSys.Equal(tc.wantSys) {
t.Errorf("Seal: sys = %#v, want %#v", t.Errorf("Seal: sys = %#v, want %#v",

View File

@ -14,7 +14,11 @@ func NewWithID(id fst.ID, os sys.State) fst.App {
return a return a
} }
func AppSystemBwrap(a fst.App) (*system.I, *bwrap.Config) { func AppSystemBwrap(a fst.App, sa fst.SealedApp) (*system.I, *bwrap.Config) {
v := a.(*app) v := a.(*app)
return v.appSeal.sys, v.appSeal.container seal := sa.(*outcome)
if v.outcome != seal || v.id != seal.id {
panic("broken app/outcome link")
}
return seal.sys, seal.container
} }

View File

@ -20,12 +20,16 @@ import (
const shimSetupTimeout = 5 * time.Second const shimSetupTimeout = 5 * time.Second
func (a *app) Run(ctx context.Context, rs *fst.RunState) error { func (seal *outcome) Run(ctx context.Context, rs *fst.RunState) error {
a.mu.Lock() if !seal.f.CompareAndSwap(false, true) {
defer a.mu.Unlock() // run does much more than just starting a process; calling it twice, even if the first call fails, will result
// in inconsistent state that is impossible to clean up; return here to limit damage and hopefully give the
// other Run a chance to return
panic("attempted to run twice")
}
if rs == nil { if rs == nil {
panic("attempted to pass nil state to run") panic("invalid state")
} }
/* /*
@ -33,8 +37,8 @@ func (a *app) Run(ctx context.Context, rs *fst.RunState) error {
*/ */
shimExec := [2]string{helper.BubblewrapName} shimExec := [2]string{helper.BubblewrapName}
if len(a.appSeal.command) > 0 { if len(seal.command) > 0 {
shimExec[1] = a.appSeal.command[0] shimExec[1] = seal.command[0]
} }
for i, n := range shimExec { for i, n := range shimExec {
if len(n) == 0 { if len(n) == 0 {
@ -54,15 +58,15 @@ func (a *app) Run(ctx context.Context, rs *fst.RunState) error {
prepare/revert os state prepare/revert os state
*/ */
if err := a.appSeal.sys.Commit(ctx); err != nil { if err := seal.sys.Commit(ctx); err != nil {
return err return err
} }
store := state.NewMulti(a.sys.Paths().RunDirPath) store := state.NewMulti(seal.runDirPath)
deferredStoreFunc := func(c state.Cursor) error { return nil } deferredStoreFunc := func(c state.Cursor) error { return nil }
defer func() { defer func() {
var revertErr error var revertErr error
storeErr := new(StateStoreError) storeErr := new(StateStoreError)
storeErr.Inner, storeErr.DoErr = store.Do(a.appSeal.user.aid.unwrap(), func(c state.Cursor) { storeErr.Inner, storeErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) {
revertErr = func() error { revertErr = func() error {
storeErr.InnerErr = deferredStoreFunc(c) storeErr.InnerErr = deferredStoreFunc(c)
@ -75,7 +79,7 @@ func (a *app) Run(ctx context.Context, rs *fst.RunState) error {
ec.Set(system.Process) ec.Set(system.Process)
if states, err := c.Load(); err != nil { if states, err := c.Load(); err != nil {
// revert per-process state here to limit damage // revert per-process state here to limit damage
return errors.Join(err, a.appSeal.sys.Revert(ec)) return errors.Join(err, seal.sys.Revert(ec))
} else { } else {
if l := len(states); l == 0 { if l := len(states); l == 0 {
fmsg.Verbose("no other launchers active, will clean up globals") fmsg.Verbose("no other launchers active, will clean up globals")
@ -111,7 +115,7 @@ func (a *app) Run(ctx context.Context, rs *fst.RunState) error {
} }
} }
err := a.appSeal.sys.Revert(ec) err := seal.sys.Revert(ec)
if err != nil { if err != nil {
err = err.(RevertCompoundError) err = err.(RevertCompoundError)
} }
@ -129,9 +133,9 @@ func (a *app) Run(ctx context.Context, rs *fst.RunState) error {
waitErr := make(chan error, 1) waitErr := make(chan error, 1)
cmd := new(shim.Shim) cmd := new(shim.Shim)
if startTime, err := cmd.Start( if startTime, err := cmd.Start(
a.appSeal.user.aid.String(), seal.user.aid.String(),
a.appSeal.user.supp, seal.user.supp,
a.appSeal.bwrapSync, seal.bwrapSync,
); err != nil { ); err != nil {
return err return err
} else { } else {
@ -139,20 +143,20 @@ func (a *app) Run(ctx context.Context, rs *fst.RunState) error {
rs.Time = startTime rs.Time = startTime
} }
shimSetupCtx, shimSetupCancel := context.WithDeadline(ctx, time.Now().Add(shimSetupTimeout)) c, cancel := context.WithTimeout(ctx, shimSetupTimeout)
defer shimSetupCancel() defer cancel()
go func() { go func() {
waitErr <- cmd.Unwrap().Wait() waitErr <- cmd.Unwrap().Wait()
// cancel shim setup in case shim died before receiving payload // cancel shim setup in case shim died before receiving payload
shimSetupCancel() cancel()
}() }()
if err := cmd.Serve(shimSetupCtx, &shim.Payload{ if err := cmd.Serve(c, &shim.Payload{
Argv: a.appSeal.command, Argv: seal.command,
Exec: shimExec, Exec: shimExec,
Bwrap: a.appSeal.container, Bwrap: seal.container,
Home: a.appSeal.user.data, Home: seal.user.data,
Verbose: fmsg.Load(), Verbose: fmsg.Load(),
}); err != nil { }); err != nil {
@ -161,14 +165,14 @@ func (a *app) Run(ctx context.Context, rs *fst.RunState) error {
// shim accepted setup payload, create process state // shim accepted setup payload, create process state
sd := state.State{ sd := state.State{
ID: a.id.unwrap(), ID: seal.id.unwrap(),
PID: cmd.Unwrap().Process.Pid, PID: cmd.Unwrap().Process.Pid,
Time: *rs.Time, Time: *rs.Time,
} }
var earlyStoreErr = new(StateStoreError) // returned after blocking on waitErr var earlyStoreErr = new(StateStoreError) // returned after blocking on waitErr
earlyStoreErr.Inner, earlyStoreErr.DoErr = store.Do(a.appSeal.user.aid.unwrap(), func(c state.Cursor) { earlyStoreErr.InnerErr = c.Save(&sd, a.appSeal.ct) }) earlyStoreErr.Inner, earlyStoreErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) { earlyStoreErr.InnerErr = c.Save(&sd, seal.ct) })
// destroy defunct state entry // destroy defunct state entry
deferredStoreFunc = func(c state.Cursor) error { return c.Destroy(a.id.unwrap()) } deferredStoreFunc = func(c state.Cursor) error { return c.Destroy(seal.id.unwrap()) }
select { select {
case err := <-waitErr: // block until fsu/shim returns case err := <-waitErr: // block until fsu/shim returns
@ -201,9 +205,9 @@ func (a *app) Run(ctx context.Context, rs *fst.RunState) error {
} }
fmsg.Resume() fmsg.Resume()
if a.appSeal.dbusMsg != nil { if seal.dbusMsg != nil {
// dump dbus message buffer // dump dbus message buffer
a.appSeal.dbusMsg() seal.dbusMsg()
} }
return earlyStoreErr.equiv("cannot save process state:") return earlyStoreErr.equiv("cannot save process state:")

View File

@ -11,6 +11,7 @@ import (
"path" "path"
"regexp" "regexp"
"strings" "strings"
"sync/atomic"
"git.gensokyo.uk/security/fortify/acl" "git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
@ -57,8 +58,13 @@ var (
var posixUsername = regexp.MustCompilePOSIX("^[a-z_]([A-Za-z0-9_-]{0,31}|[A-Za-z0-9_-]{0,30}\\$)$") var posixUsername = regexp.MustCompilePOSIX("^[a-z_]([A-Za-z0-9_-]{0,31}|[A-Za-z0-9_-]{0,30}\\$)$")
// appSeal stores copies of various parts of [fst.Config] // outcome stores copies of various parts of [fst.Config]
type appSeal struct { type outcome struct {
// copied from initialising [app]
id *stringPair[fst.ID]
// copied from [sys.State] response
runDirPath string
// passed through from [fst.Config] // passed through from [fst.Config]
command []string command []string
@ -68,16 +74,16 @@ type appSeal struct {
// dump dbus proxy message buffer // dump dbus proxy message buffer
dbusMsg func() dbusMsg func()
user appUser user fsuUser
sys *system.I sys *system.I
container *bwrap.Config container *bwrap.Config
bwrapSync *os.File bwrapSync *os.File
// protected by upstream mutex f atomic.Bool
} }
// appUser stores post-fsu credentials and metadata // fsuUser stores post-fsu credentials and metadata
type appUser struct { type fsuUser struct {
// application id // application id
aid *stringPair[int] aid *stringPair[int]
// target uid resolved by fid:aid // target uid resolved by fid:aid
@ -94,7 +100,7 @@ type appUser struct {
username string username string
} }
func (seal *appSeal) finalise(sys sys.State, config *fst.Config, id string) error { func (seal *outcome) finalise(sys sys.State, config *fst.Config) error {
{ {
// encode initial configuration for state tracking // encode initial configuration for state tracking
ct := new(bytes.Buffer) ct := new(bytes.Buffer)
@ -118,7 +124,7 @@ func (seal *appSeal) finalise(sys sys.State, config *fst.Config, id string) erro
Resolve post-fsu user state Resolve post-fsu user state
*/ */
seal.user = appUser{ seal.user = fsuUser{
aid: newInt(config.Confinement.AppID), aid: newInt(config.Confinement.AppID),
data: config.Confinement.Outer, data: config.Confinement.Outer,
home: config.Confinement.Inner, home: config.Confinement.Inner,
@ -223,6 +229,7 @@ func (seal *appSeal) finalise(sys sys.State, config *fst.Config, id string) erro
*/ */
sc := sys.Paths() sc := sys.Paths()
seal.runDirPath = sc.RunDirPath
seal.sys = system.New(seal.user.uid.unwrap()) seal.sys = system.New(seal.user.uid.unwrap())
seal.sys.IsVerbose = fmsg.Load seal.sys.IsVerbose = fmsg.Load
seal.sys.Verbose = fmsg.Verbose seal.sys.Verbose = fmsg.Verbose
@ -243,10 +250,10 @@ func (seal *appSeal) finalise(sys sys.State, config *fst.Config, id string) erro
seal.sys.UpdatePermType(system.User, sc.RuntimePath, acl.Execute) seal.sys.UpdatePermType(system.User, sc.RuntimePath, acl.Execute)
// outer process-specific share directory // outer process-specific share directory
sharePath := path.Join(sc.SharePath, id) sharePath := path.Join(sc.SharePath, seal.id.String())
seal.sys.Ephemeral(system.Process, sharePath, 0711) seal.sys.Ephemeral(system.Process, sharePath, 0711)
// similar to share but within XDG_RUNTIME_DIR // similar to share but within XDG_RUNTIME_DIR
sharePathLocal := path.Join(sc.RunDirPath, id) sharePathLocal := path.Join(sc.RunDirPath, seal.id.String())
seal.sys.Ephemeral(system.Process, sharePathLocal, 0700) seal.sys.Ephemeral(system.Process, sharePathLocal, 0700)
seal.sys.UpdatePerm(sharePathLocal, acl.Execute) seal.sys.UpdatePerm(sharePathLocal, acl.Execute)
@ -327,14 +334,14 @@ func (seal *appSeal) finalise(sys sys.State, config *fst.Config, id string) erro
if !config.Confinement.Sandbox.DirectWayland { // set up security-context-v1 if !config.Confinement.Sandbox.DirectWayland { // set up security-context-v1
socketDir := path.Join(sc.SharePath, "wayland") socketDir := path.Join(sc.SharePath, "wayland")
outerPath := path.Join(socketDir, id) outerPath := path.Join(socketDir, seal.id.String())
seal.sys.Ensure(socketDir, 0711) seal.sys.Ensure(socketDir, 0711)
appID := config.ID appID := config.ID
if appID == "" { if appID == "" {
// use instance ID in case app id is not set // use instance ID in case app id is not set
appID = "uk.gensokyo.fortify." + id appID = "uk.gensokyo.fortify." + seal.id.String()
} }
seal.sys.Wayland(&seal.bwrapSync, outerPath, socketPath, appID, id) seal.sys.Wayland(&seal.bwrapSync, outerPath, socketPath, appID, seal.id.String())
seal.container.Bind(outerPath, innerPath) seal.container.Bind(outerPath, innerPath)
} else { // bind mount wayland socket (insecure) } else { // bind mount wayland socket (insecure)
fmsg.Verbose("direct wayland access, PROCEED WITH CAUTION") fmsg.Verbose("direct wayland access, PROCEED WITH CAUTION")

12
main.go
View File

@ -173,7 +173,7 @@ func main() {
config.Command = append(config.Command, args[2:]...) config.Command = append(config.Command, args[2:]...)
// invoke app // invoke app
runApp(config) runApp(app.MustNew(std), config)
panic("unreachable") panic("unreachable")
case "run": // run app in permissive defaults usage pattern case "run": // run app in permissive defaults usage pattern
@ -300,7 +300,7 @@ func main() {
} }
// invoke app // invoke app
runApp(config) runApp(app.MustNew(std), config)
panic("unreachable") panic("unreachable")
// internal commands // internal commands
@ -318,7 +318,7 @@ func main() {
panic("unreachable") panic("unreachable")
} }
func runApp(config *fst.Config) { func runApp(a fst.App, config *fst.Config) {
rs := new(fst.RunState) rs := new(fst.RunState)
ctx, stop := signal.NotifyContext(context.Background(), ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM) syscall.SIGINT, syscall.SIGTERM)
@ -328,12 +328,10 @@ func runApp(config *fst.Config) {
seccomp.CPrintln = log.Println seccomp.CPrintln = log.Println
} }
if a, err := app.New(std); err != nil { if sa, err := a.Seal(config); err != nil {
log.Fatalf("cannot create app: %s", err)
} else if err = a.Seal(config); err != nil {
fmsg.PrintBaseError(err, "cannot seal app:") fmsg.PrintBaseError(err, "cannot seal app:")
internal.Exit(1) internal.Exit(1)
} else if err = a.Run(ctx, rs); err != nil { } else if err = sa.Run(ctx, rs); err != nil {
if rs.Time == nil { if rs.Time == nil {
fmsg.PrintBaseError(err, "cannot start app:") fmsg.PrintBaseError(err, "cannot start app:")
} else { } else {