From 6220f7e197f01846f5e4e3090acf9251f3ae6ade Mon Sep 17 00:00:00 2001 From: Ophestra Umiker Date: Fri, 11 Oct 2024 02:01:03 +0900 Subject: [PATCH] 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 --- internal/app/app.go | 3 ++ internal/app/config.go | 6 +++ internal/app/launch.machinectl.go | 4 +- internal/app/launch.sudo.go | 11 ++-- internal/app/seal.go | 6 +++ internal/app/share.display.go | 5 +- internal/app/share.runtime.go | 8 +++ internal/app/shim.go | 83 ------------------------------- internal/app/start.go | 68 +++++++++++++++++++++---- internal/app/system.go | 17 ++++--- main.go | 3 +- 11 files changed, 105 insertions(+), 109 deletions(-) delete mode 100644 internal/app/shim.go diff --git a/internal/app/app.go b/internal/app/app.go index 9d1165f..50d59da 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -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 diff --git a/internal/app/config.go b/internal/app/config.go index d964874..17203a4 100644 --- a/internal/app/config.go +++ b/internal/app/config.go @@ -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"` diff --git a/internal/app/launch.machinectl.go b/internal/app/launch.machinectl.go index 21b9d40..19a81aa 100644 --- a/internal/app/launch.machinectl.go +++ b/internal/app/launch.machinectl.go @@ -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 diff --git a/internal/app/launch.sudo.go b/internal/app/launch.sudo.go index 666d0b3..777cb2a 100644 --- a/internal/app/launch.sudo.go +++ b/internal/app/launch.sudo.go @@ -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 } diff --git a/internal/app/seal.go b/internal/app/seal.go index ffbd748..d325e04 100644 --- a/internal/app/seal.go +++ b/internal/app/seal.go @@ -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 { diff --git a/internal/app/share.display.go b/internal/app/share.display.go index 51ac54d..01ae00a 100644 --- a/internal/app/share.display.go +++ b/internal/app/share.display.go @@ -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) } } diff --git a/internal/app/share.runtime.go b/internal/app/share.runtime.go index b015d45..c25a89b 100644 --- a/internal/app/share.runtime.go +++ b/internal/app/share.runtime.go @@ -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) diff --git a/internal/app/shim.go b/internal/app/shim.go deleted file mode 100644 index 00f711a..0000000 --- a/internal/app/shim.go +++ /dev/null @@ -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 - } -} diff --git a/internal/app/start.go b/internal/app/start.go index 02fad97..ed34b7c 100644 --- a/internal/app/start.go +++ b/internal/app/start.go @@ -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,7 +235,9 @@ func (a *app) Wait() (int, error) { 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 { diff --git a/internal/app/system.go b/internal/app/system.go index aa62cfb..b318b3a 100644 --- a/internal/app/system.go +++ b/internal/app/system.go @@ -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 } diff --git a/main.go b/main.go index cc1809f..c30bfca 100644 --- a/main.go +++ b/main.go @@ -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