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