app: migrate to new shim implementation

Both machinectl and sudo launch methods launch shim as shim is now responsible for setting up the sandbox. Various app structures are adapted to accommodate bwrap configuration and mediated wayland access.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
This commit is contained in:
Ophestra Umiker 2024-10-11 02:01:03 +09:00
parent b86fa6b4c9
commit 6220f7e197
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
11 changed files with 105 additions and 109 deletions

View File

@ -1,6 +1,7 @@
package app package app
import ( import (
"net"
"os/exec" "os/exec"
"sync" "sync"
) )
@ -18,6 +19,8 @@ type app struct {
seal *appSeal seal *appSeal
// underlying fortified child process // underlying fortified child process
cmd *exec.Cmd cmd *exec.Cmd
// wayland connection if wayland mediation is enabled
wayland *net.UnixConn
// error returned waiting for process // error returned waiting for process
wait error wait error

View File

@ -2,6 +2,7 @@ package app
import ( import (
"git.ophivana.moe/cat/fortify/dbus" "git.ophivana.moe/cat/fortify/dbus"
"git.ophivana.moe/cat/fortify/helper/bwrap"
"git.ophivana.moe/cat/fortify/internal/state" "git.ophivana.moe/cat/fortify/internal/state"
) )
@ -22,6 +23,11 @@ type Config struct {
// ConfinementConfig defines fortified child's confinement // ConfinementConfig defines fortified child's confinement
type ConfinementConfig struct { type ConfinementConfig struct {
// bwrap sandbox confinement configuration
Sandbox *bwrap.Config `json:"sandbox"`
// mediated access to wayland socket
Wayland bool `json:"wayland"`
// reference to a system D-Bus proxy configuration, // reference to a system D-Bus proxy configuration,
// nil value disables system bus proxy // nil value disables system bus proxy
SystemBus *dbus.Config `json:"system_bus,omitempty"` SystemBus *dbus.Config `json:"system_bus,omitempty"`

View File

@ -8,7 +8,7 @@ import (
"git.ophivana.moe/cat/fortify/internal/verbose" "git.ophivana.moe/cat/fortify/internal/verbose"
) )
func (a *app) commandBuilderMachineCtl() (args []string) { func (a *app) commandBuilderMachineCtl(shimEnv string) (args []string) {
args = make([]string, 0, 9+len(a.seal.env)) args = make([]string, 0, 9+len(a.seal.env))
// shell --uid=$USER // shell --uid=$USER
@ -25,7 +25,7 @@ func (a *app) commandBuilderMachineCtl() (args []string) {
envQ[i] = "-E" + e envQ[i] = "-E" + e
} }
// add shim payload to environment for shim path // add shim payload to environment for shim path
envQ[len(a.seal.env)] = "-E" + a.shimPayloadEnv() envQ[len(a.seal.env)] = "-E" + shimEnv
args = append(args, envQ...) args = append(args, envQ...)
// -- .host // -- .host

View File

@ -10,8 +10,8 @@ const (
sudoAskPass = "SUDO_ASKPASS" sudoAskPass = "SUDO_ASKPASS"
) )
func (a *app) commandBuilderSudo() (args []string) { func (a *app) commandBuilderSudo(shimEnv string) (args []string) {
args = make([]string, 0, 4+len(a.seal.env)+len(a.seal.command)) args = make([]string, 0, 8)
// -Hiu $USER // -Hiu $USER
args = append(args, "-Hiu", a.seal.sys.Username) args = append(args, "-Hiu", a.seal.sys.Username)
@ -22,12 +22,11 @@ func (a *app) commandBuilderSudo() (args []string) {
args = append(args, "-A") args = append(args, "-A")
} }
// environ // shim payload
args = append(args, a.seal.env...) args = append(args, shimEnv)
// -- $@ // -- $@
args = append(args, "--") args = append(args, "--", a.seal.sys.executable, "-V", "--license") // magic for shim.Try()
args = append(args, a.seal.command...)
return return
} }

View File

@ -63,6 +63,12 @@ func (a *app) Seal(config *Config) error {
// pass through config values // pass through config values
seal.fid = config.ID seal.fid = config.ID
seal.command = config.Command seal.command = config.Command
seal.bwrap = config.Confinement.Sandbox
// create wayland client wait channel
if config.Confinement.Wayland {
seal.wlDone = make(chan struct{})
}
// parses launch method text and looks up tool path // parses launch method text and looks up tool path
switch config.Method { switch config.Method {

View File

@ -34,7 +34,7 @@ func (seal *appSeal) shareDisplay() error {
if seal.et.Has(state.EnableWayland) { if seal.et.Has(state.EnableWayland) {
if wd, ok := os.LookupEnv(waylandDisplay); !ok { if wd, ok := os.LookupEnv(waylandDisplay); !ok {
return (*ErrDisplayEnv)(wrapError(ErrWayland, "WAYLAND_DISPLAY is not set")) return (*ErrDisplayEnv)(wrapError(ErrWayland, "WAYLAND_DISPLAY is not set"))
} else { } else if seal.wlDone == nil {
// hardlink wayland socket // hardlink wayland socket
wp := path.Join(seal.RuntimePath, wd) wp := path.Join(seal.RuntimePath, wd)
wpi := path.Join(seal.shareLocal, "wayland") wpi := path.Join(seal.shareLocal, "wayland")
@ -43,6 +43,9 @@ func (seal *appSeal) shareDisplay() error {
// ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`) // ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`)
seal.sys.updatePermTag(state.EnableWayland, wp, acl.Read, acl.Write, acl.Execute) seal.sys.updatePermTag(state.EnableWayland, wp, acl.Read, acl.Write, acl.Execute)
} else {
// set wayland socket path (e.g. `/run/user/%d/wayland-%d`)
seal.wl = path.Join(seal.RuntimePath, wd)
} }
} }

View File

@ -1,6 +1,7 @@
package app package app
import ( import (
"os"
"path" "path"
"git.ophivana.moe/cat/fortify/acl" "git.ophivana.moe/cat/fortify/acl"
@ -11,10 +12,17 @@ const (
xdgRuntimeDir = "XDG_RUNTIME_DIR" xdgRuntimeDir = "XDG_RUNTIME_DIR"
xdgSessionClass = "XDG_SESSION_CLASS" xdgSessionClass = "XDG_SESSION_CLASS"
xdgSessionType = "XDG_SESSION_TYPE" xdgSessionType = "XDG_SESSION_TYPE"
shell = "SHELL"
) )
// shareRuntime queues actions for sharing/ensuring the runtime and share directories // shareRuntime queues actions for sharing/ensuring the runtime and share directories
func (seal *appSeal) shareRuntime() { func (seal *appSeal) shareRuntime() {
// look up shell
if s, ok := os.LookupEnv(shell); ok {
seal.appendEnv(shell, s)
}
// ensure RunDir (e.g. `/run/user/%d/fortify`) // ensure RunDir (e.g. `/run/user/%d/fortify`)
seal.sys.ensure(seal.RunDirPath, 0700) seal.sys.ensure(seal.RunDirPath, 0700)
seal.sys.updatePermTag(state.EnableLength, seal.RunDirPath, acl.Execute) seal.sys.updatePermTag(state.EnableLength, seal.RunDirPath, acl.Execute)

View File

@ -1,83 +0,0 @@
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
}
}

View File

@ -2,11 +2,16 @@ package app
import ( import (
"errors" "errors"
"fmt"
"os" "os"
"os/exec" "os/exec"
"path"
"path/filepath"
"strconv" "strconv"
"time" "time"
"git.ophivana.moe/cat/fortify/helper"
"git.ophivana.moe/cat/fortify/internal/shim"
"git.ophivana.moe/cat/fortify/internal/state" "git.ophivana.moe/cat/fortify/internal/state"
"git.ophivana.moe/cat/fortify/internal/verbose" "git.ophivana.moe/cat/fortify/internal/verbose"
) )
@ -14,6 +19,8 @@ import (
type ( type (
// ProcessError encapsulates errors returned by starting *exec.Cmd // ProcessError encapsulates errors returned by starting *exec.Cmd
ProcessError BaseError ProcessError BaseError
// ShimError encapsulates errors returned by shim.ServeConfig.
ShimError BaseError
) )
// Start starts the fortified child // Start starts the fortified child
@ -21,12 +28,30 @@ func (a *app) Start() error {
a.lock.Lock() a.lock.Lock()
defer a.lock.Unlock() defer a.lock.Unlock()
// resolve exec paths
e := [2]string{helper.BubblewrapName}
if len(a.seal.command) > 0 {
e[1] = a.seal.command[0]
}
for i, n := range e {
if len(n) == 0 {
continue
}
if filepath.Base(n) == n {
if s, err := exec.LookPath(n); err == nil {
e[i] = s
} else {
return (*ProcessError)(wrapError(err, fmt.Sprintf("cannot find %q in PATH: %v", n, err)))
}
}
}
if err := a.seal.sys.commit(); err != nil { if err := a.seal.sys.commit(); err != nil {
return err return err
} }
// select command builder // select command builder
var commandBuilder func() (args []string) var commandBuilder func(shimEnv string) (args []string)
switch a.seal.launchOption { switch a.seal.launchOption {
case LaunchMethodSudo: case LaunchMethodSudo:
commandBuilder = a.commandBuilderSudo commandBuilder = a.commandBuilderSudo
@ -37,15 +62,30 @@ func (a *app) Start() error {
} }
// configure child process // configure child process
a.cmd = exec.Command(a.seal.toolPath, commandBuilder()...) confSockPath := path.Join(a.seal.share, "shim")
a.cmd = exec.Command(a.seal.toolPath, commandBuilder(shim.EnvShim+"="+confSockPath)...)
a.cmd.Env = []string{} a.cmd.Env = []string{}
a.cmd.Stdin = os.Stdin a.cmd.Stdin = os.Stdin
a.cmd.Stdout = os.Stdout a.cmd.Stdout = os.Stdout
a.cmd.Stderr = os.Stderr a.cmd.Stderr = os.Stderr
a.cmd.Dir = a.seal.RunDirPath a.cmd.Dir = a.seal.RunDirPath
// start child process if wls, err := shim.ServeConfig(confSockPath, &shim.Payload{
verbose.Println("starting main process:", a.cmd) Argv: a.seal.command,
Env: a.seal.env,
Exec: e,
Bwrap: a.seal.bwrap,
WL: a.seal.wlDone != nil,
Verbose: verbose.Get(),
}, a.seal.wl, a.seal.wlDone); err != nil {
return (*ShimError)(wrapError(err, "cannot listen on shim socket:", err))
} else {
a.wayland = wls
}
// start shim
verbose.Println("starting shim as target user:", a.cmd)
if err := a.cmd.Start(); err != nil { if err := a.cmd.Start(); err != nil {
return (*ProcessError)(wrapError(err, "cannot start process:", err)) return (*ProcessError)(wrapError(err, "cannot start process:", err))
} }
@ -62,11 +102,11 @@ func (a *app) Start() error {
} }
// register process state // register process state
var e = new(StateStoreError) var err = new(StateStoreError)
e.Inner, e.DoErr = a.seal.store.Do(func(b state.Backend) { err.Inner, err.DoErr = a.seal.store.Do(func(b state.Backend) {
e.InnerErr = b.Save(&sd) err.InnerErr = b.Save(&sd)
}) })
return e.equiv("cannot save process state:", e) return err.equiv("cannot save process state:", e)
} }
// StateStoreError is returned for a failed state save // StateStoreError is returned for a failed state save
@ -146,6 +186,14 @@ func (a *app) Wait() (int, error) {
verbose.Println("process", strconv.Itoa(a.cmd.Process.Pid), "exited with exit code", r) verbose.Println("process", strconv.Itoa(a.cmd.Process.Pid), "exited with exit code", r)
// close wayland connection
if a.wayland != nil {
close(a.seal.wlDone)
if err := a.wayland.Close(); err != nil {
fmt.Println("fortify: cannot close wayland connection:", err)
}
}
// update store and revert app setup transaction // update store and revert app setup transaction
e := new(StateStoreError) e := new(StateStoreError)
e.Inner, e.DoErr = a.seal.store.Do(func(b state.Backend) { e.Inner, e.DoErr = a.seal.store.Do(func(b state.Backend) {
@ -187,7 +235,9 @@ func (a *app) Wait() (int, error) {
ct = append(ct, i) ct = append(ct, i)
} }
} }
verbose.Println("will revert operations tagged", ct, "as no remaining launchers hold these enablements") if len(ct) > 0 {
verbose.Println("will revert operations tagged", ct, "as no remaining launchers hold these enablements")
}
} }
if err := a.seal.sys.revert(tags); err != nil { if err := a.seal.sys.revert(tags); err != nil {

View File

@ -9,6 +9,7 @@ import (
"git.ophivana.moe/cat/fortify/acl" "git.ophivana.moe/cat/fortify/acl"
"git.ophivana.moe/cat/fortify/dbus" "git.ophivana.moe/cat/fortify/dbus"
"git.ophivana.moe/cat/fortify/helper/bwrap"
"git.ophivana.moe/cat/fortify/internal" "git.ophivana.moe/cat/fortify/internal"
"git.ophivana.moe/cat/fortify/internal/state" "git.ophivana.moe/cat/fortify/internal/state"
"git.ophivana.moe/cat/fortify/internal/verbose" "git.ophivana.moe/cat/fortify/internal/verbose"
@ -19,6 +20,12 @@ import (
type appSeal struct { type appSeal struct {
// application unique identifier // application unique identifier
id *appID id *appID
// bwrap config
bwrap *bwrap.Config
// wayland socket path if mediated wayland is enabled
wl string
// wait for wayland client to exit if mediated wayland is enabled
wlDone chan struct{}
// freedesktop application ID // freedesktop application ID
fid string fid string
@ -187,7 +194,7 @@ func (tx *appSealTx) commit() error {
} }
tx.complete = true tx.complete = true
txp := &appSealTx{} txp := &appSealTx{User: tx.User}
defer func() { defer func() {
// rollback partial commit // rollback partial commit
if txp != nil { if txp != nil {
@ -371,6 +378,8 @@ func (seal *appSeal) shareAll(bus [2]*dbus.Config) error {
seal.shared = true seal.shared = true
seal.shareRuntime() seal.shareRuntime()
targetRuntime := seal.shareRuntimeChild()
verbose.Printf("child runtime data dir '%s' configured\n", targetRuntime)
if err := seal.shareDisplay(); err != nil { if err := seal.shareDisplay(); err != nil {
return err return err
} }
@ -393,11 +402,5 @@ func (seal *appSeal) shareAll(bus [2]*dbus.Config) error {
verbose.Println("message bus proxy final args:", seal.sys.dbus) verbose.Println("message bus proxy final args:", seal.sys.dbus)
} }
// workaround for launch method sudo
if seal.launchOption == LaunchMethodSudo {
targetRuntime := seal.shareRuntimeChild()
verbose.Printf("child runtime data dir '%s' configured\n", targetRuntime)
}
return nil return nil
} }

View File

@ -10,6 +10,7 @@ import (
"git.ophivana.moe/cat/fortify/dbus" "git.ophivana.moe/cat/fortify/dbus"
"git.ophivana.moe/cat/fortify/internal" "git.ophivana.moe/cat/fortify/internal"
"git.ophivana.moe/cat/fortify/internal/app" "git.ophivana.moe/cat/fortify/internal/app"
"git.ophivana.moe/cat/fortify/internal/shim"
"git.ophivana.moe/cat/fortify/internal/state" "git.ophivana.moe/cat/fortify/internal/state"
"git.ophivana.moe/cat/fortify/internal/verbose" "git.ophivana.moe/cat/fortify/internal/verbose"
) )
@ -35,7 +36,7 @@ func main() {
// launcher payload early exit // launcher payload early exit
if printVersion && printLicense { if printVersion && printLicense {
app.TryShim() shim.Try()
} }
// version/license command early exit // version/license command early exit