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

View File

@ -2,6 +2,7 @@ package app
import (
"git.ophivana.moe/cat/fortify/dbus"
"git.ophivana.moe/cat/fortify/helper/bwrap"
"git.ophivana.moe/cat/fortify/internal/state"
)
@ -22,6 +23,11 @@ type Config struct {
// ConfinementConfig defines fortified child's confinement
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,
// nil value disables system bus proxy
SystemBus *dbus.Config `json:"system_bus,omitempty"`

View File

@ -8,7 +8,7 @@ import (
"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))
// shell --uid=$USER
@ -25,7 +25,7 @@ func (a *app) commandBuilderMachineCtl() (args []string) {
envQ[i] = "-E" + e
}
// 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...)
// -- .host

View File

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

View File

@ -63,6 +63,12 @@ func (a *app) Seal(config *Config) error {
// pass through config values
seal.fid = config.ID
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
switch config.Method {

View File

@ -34,7 +34,7 @@ func (seal *appSeal) shareDisplay() error {
if seal.et.Has(state.EnableWayland) {
if wd, ok := os.LookupEnv(waylandDisplay); !ok {
return (*ErrDisplayEnv)(wrapError(ErrWayland, "WAYLAND_DISPLAY is not set"))
} else {
} else if seal.wlDone == nil {
// hardlink wayland socket
wp := path.Join(seal.RuntimePath, wd)
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`)
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
import (
"os"
"path"
"git.ophivana.moe/cat/fortify/acl"
@ -11,10 +12,17 @@ const (
xdgRuntimeDir = "XDG_RUNTIME_DIR"
xdgSessionClass = "XDG_SESSION_CLASS"
xdgSessionType = "XDG_SESSION_TYPE"
shell = "SHELL"
)
// shareRuntime queues actions for sharing/ensuring the runtime and share directories
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`)
seal.sys.ensure(seal.RunDirPath, 0700)
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 (
"errors"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"strconv"
"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/verbose"
)
@ -14,6 +19,8 @@ import (
type (
// ProcessError encapsulates errors returned by starting *exec.Cmd
ProcessError BaseError
// ShimError encapsulates errors returned by shim.ServeConfig.
ShimError BaseError
)
// Start starts the fortified child
@ -21,12 +28,30 @@ func (a *app) Start() error {
a.lock.Lock()
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 {
return err
}
// select command builder
var commandBuilder func() (args []string)
var commandBuilder func(shimEnv string) (args []string)
switch a.seal.launchOption {
case LaunchMethodSudo:
commandBuilder = a.commandBuilderSudo
@ -37,15 +62,30 @@ func (a *app) Start() error {
}
// 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.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 wls, err := shim.ServeConfig(confSockPath, &shim.Payload{
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 {
return (*ProcessError)(wrapError(err, "cannot start process:", err))
}
@ -62,11 +102,11 @@ func (a *app) Start() error {
}
// register process state
var e = new(StateStoreError)
e.Inner, e.DoErr = a.seal.store.Do(func(b state.Backend) {
e.InnerErr = b.Save(&sd)
var err = new(StateStoreError)
err.Inner, err.DoErr = a.seal.store.Do(func(b state.Backend) {
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
@ -146,6 +186,14 @@ func (a *app) Wait() (int, error) {
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
e := new(StateStoreError)
e.Inner, e.DoErr = a.seal.store.Do(func(b state.Backend) {
@ -187,8 +235,10 @@ func (a *app) Wait() (int, error) {
ct = append(ct, i)
}
}
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 {
return err.(RevertCompoundError)

View File

@ -9,6 +9,7 @@ import (
"git.ophivana.moe/cat/fortify/acl"
"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/state"
"git.ophivana.moe/cat/fortify/internal/verbose"
@ -19,6 +20,12 @@ import (
type appSeal struct {
// application unique identifier
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
fid string
@ -187,7 +194,7 @@ func (tx *appSealTx) commit() error {
}
tx.complete = true
txp := &appSealTx{}
txp := &appSealTx{User: tx.User}
defer func() {
// rollback partial commit
if txp != nil {
@ -371,6 +378,8 @@ func (seal *appSeal) shareAll(bus [2]*dbus.Config) error {
seal.shared = true
seal.shareRuntime()
targetRuntime := seal.shareRuntimeChild()
verbose.Printf("child runtime data dir '%s' configured\n", targetRuntime)
if err := seal.shareDisplay(); err != nil {
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)
}
// workaround for launch method sudo
if seal.launchOption == LaunchMethodSudo {
targetRuntime := seal.shareRuntimeChild()
verbose.Printf("child runtime data dir '%s' configured\n", targetRuntime)
}
return nil
}

View File

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