diff --git a/internal/app/config.go b/internal/app/config.go index 17203a4..550bc7c 100644 --- a/internal/app/config.go +++ b/internal/app/config.go @@ -24,9 +24,7 @@ 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"` + Sandbox *SandboxConfig `json:"sandbox"` // reference to a system D-Bus proxy configuration, // nil value disables system bus proxy @@ -38,3 +36,56 @@ type ConfinementConfig struct { // child capability enablements Enablements state.Enablements `json:"enablements"` } + +// SandboxConfig describes resources made available to the sandbox. +type SandboxConfig struct { + // unix hostname within sandbox + Hostname string `json:"hostname,omitempty"` + // userns availability within sandbox + UserNS bool `json:"userns,omitempty"` + // share net namespace + Net bool `json:"net,omitempty"` + // do not run in new session + NoNewSession bool `json:"no_new_session,omitempty"` + // mediated access to wayland socket + Wayland bool `json:"wayland,omitempty"` + + UID int `json:"uid,omitempty"` + GID int `json:"gid,omitempty"` + // final environment variables + Env map[string]string `json:"env"` + + // paths made available within the sandbox + Bind [][2]string `json:"bind"` + // paths made available read-only within the sandbox + ROBind [][2]string `json:"ro-bind"` +} + +func (s *SandboxConfig) Bwrap() *bwrap.Config { + if s == nil { + return nil + } + + conf := &bwrap.Config{ + Net: s.Net, + UserNS: s.UserNS, + Hostname: s.Hostname, + Clearenv: true, + SetEnv: s.Env, + Bind: s.Bind, + ROBind: s.ROBind, + Procfs: []string{"/proc"}, + DevTmpfs: []string{"/dev"}, + Mqueue: []string{"/dev/mqueue"}, + NewSession: !s.NoNewSession, + DieWithParent: true, + } + if s.UID > 0 { + conf.UID = &s.UID + } + if s.GID > 0 { + conf.GID = &s.GID + } + + return conf +} diff --git a/internal/app/launch.machinectl.go b/internal/app/launch.machinectl.go index 19a81aa..59efd55 100644 --- a/internal/app/launch.machinectl.go +++ b/internal/app/launch.machinectl.go @@ -9,7 +9,7 @@ import ( ) 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.sys.bwrap.SetEnv)) // shell --uid=$USER args = append(args, "shell", "--uid="+a.seal.sys.Username) @@ -20,12 +20,12 @@ func (a *app) commandBuilderMachineCtl(shimEnv string) (args []string) { } // environ - envQ := make([]string, len(a.seal.env)+1) - for i, e := range a.seal.env { - envQ[i] = "-E" + e + envQ := make([]string, 0, len(a.seal.sys.bwrap.SetEnv)+1) + for k, v := range a.seal.sys.bwrap.SetEnv { + envQ = append(envQ, "-E"+k+"="+v) } // add shim payload to environment for shim path - envQ[len(a.seal.env)] = "-E" + shimEnv + envQ = append(envQ, "-E"+shimEnv) args = append(args, envQ...) // -- .host @@ -44,8 +44,8 @@ func (a *app) commandBuilderMachineCtl(shimEnv string) (args []string) { // apply custom environment variables to activation environment innerCommand.WriteString("dbus-update-activation-environment --systemd") - for _, e := range a.seal.env { - innerCommand.WriteString(" " + strings.SplitN(e, "=", 2)[0]) + for k := range a.seal.sys.bwrap.SetEnv { + innerCommand.WriteString(" " + k) } innerCommand.WriteString("; ") diff --git a/internal/app/seal.go b/internal/app/seal.go index d325e04..0d5ee7a 100644 --- a/internal/app/seal.go +++ b/internal/app/seal.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" "os/user" + "path" "strconv" "git.ophivana.moe/cat/fortify/dbus" @@ -63,12 +64,6 @@ 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 { @@ -115,6 +110,65 @@ func (a *app) Seal(config *Config) error { } } else { seal.sys.User = u + seal.sys.runtime = path.Join("/run/user", u.Uid) + } + + // map sandbox config to bwrap + if config.Confinement.Sandbox == nil { + verbose.Println("sandbox configuration not supplied, PROCEED WITH CAUTION") + + // permissive defaults + conf := &SandboxConfig{ + UserNS: true, + Net: true, + NoNewSession: true, + } + // bind entries in / + if d, err := os.ReadDir("/"); err != nil { + return err + } else { + b := make([][2]string, 0, len(d)) + for _, ent := range d { + name := ent.Name() + switch name { + case "proc": + case "dev": + case "run": + default: + p := "/" + name + b = append(b, [2]string{p, p}) + } + } + conf.Bind = append(conf.Bind, b...) + } + // bind entries in /run + if d, err := os.ReadDir("/run"); err != nil { + return err + } else { + b := make([][2]string, 0, len(d)) + for _, ent := range d { + name := ent.Name() + switch name { + case "user": + case "dbus": + default: + p := "/run/" + name + b = append(b, [2]string{p, p}) + } + } + conf.Bind = append(conf.Bind, b...) + } + config.Confinement.Sandbox = conf + } + seal.sys.bwrap = config.Confinement.Sandbox.Bwrap() + if seal.sys.bwrap.SetEnv == nil { + seal.sys.bwrap.SetEnv = make(map[string]string) + } + + // create wayland client wait channel if mediated wayland is enabled + // this channel being set enables mediated wayland setup later on + if config.Confinement.Sandbox.Wayland { + seal.wlDone = make(chan struct{}) } // open process state store diff --git a/internal/app/share.dbus.go b/internal/app/share.dbus.go index 468a8af..804dbd3 100644 --- a/internal/app/share.dbus.go +++ b/internal/app/share.dbus.go @@ -63,10 +63,14 @@ func (seal *appSeal) shareDBus(config [2]*dbus.Config) error { seal.sys.dbusAddr = &[2][2]string{sessionBus, systemBus} // share proxy sockets - seal.appendEnv(dbusSessionBusAddress, "unix:path="+sessionBus[1]) + sessionInner := path.Join(seal.sys.runtime, "bus") + seal.sys.setEnv(dbusSessionBusAddress, "unix:path="+sessionInner) + seal.sys.bind(sessionBus[1], sessionInner, true) seal.sys.updatePerm(sessionBus[1], acl.Read, acl.Write) if seal.sys.dbusSystem { - seal.appendEnv(dbusSystemBusAddress, "unix:path="+systemBus[1]) + systemInner := "/run/dbus/system_bus_socket" + seal.sys.setEnv(dbusSystemBusAddress, "unix:path="+systemInner) + seal.sys.bind(systemBus[1], systemInner, true) seal.sys.updatePerm(systemBus[1], acl.Read, acl.Write) } diff --git a/internal/app/share.display.go b/internal/app/share.display.go index 01ae00a..da9f6ef 100644 --- a/internal/app/share.display.go +++ b/internal/app/share.display.go @@ -27,7 +27,7 @@ type ErrDisplayEnv BaseError func (seal *appSeal) shareDisplay() error { // pass $TERM to launcher if t, ok := os.LookupEnv(term); ok { - seal.appendEnv(term, t) + seal.sys.setEnv(term, t) } // set up wayland @@ -38,8 +38,10 @@ func (seal *appSeal) shareDisplay() error { // hardlink wayland socket wp := path.Join(seal.RuntimePath, wd) wpi := path.Join(seal.shareLocal, "wayland") + w := path.Join(seal.sys.runtime, "wayland-0") seal.sys.link(wp, wpi) - seal.appendEnv(waylandDisplay, wpi) + seal.sys.setEnv(waylandDisplay, w) + seal.sys.bind(wpi, w, true) // ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`) seal.sys.updatePermTag(state.EnableWayland, wp, acl.Read, acl.Write, acl.Execute) @@ -56,7 +58,8 @@ func (seal *appSeal) shareDisplay() error { return (*ErrDisplayEnv)(wrapError(ErrXDisplay, "DISPLAY is not set")) } else { seal.sys.changeHosts(seal.sys.Username) - seal.appendEnv(display, d) + seal.sys.setEnv(display, d) + seal.sys.bind("/tmp/.X11-unix", "/tmp/.X11-unix", true) } } diff --git a/internal/app/share.pulse.go b/internal/app/share.pulse.go index b432345..39e6fc8 100644 --- a/internal/app/share.pulse.go +++ b/internal/app/share.pulse.go @@ -63,15 +63,17 @@ func (seal *appSeal) sharePulse() error { // hard link pulse socket into target-executable share psi := path.Join(seal.shareLocal, "pulse") + p := path.Join(seal.sys.runtime, "pulse", "native") seal.sys.link(ps, psi) - seal.appendEnv(pulseServer, "unix:"+psi) + seal.sys.bind(psi, p, true) + seal.sys.setEnv(pulseServer, "unix:"+p) // publish current user's pulse cookie for target user if src, err := discoverPulseCookie(); err != nil { return err } else { dst := path.Join(seal.share, "pulse-cookie") - seal.appendEnv(pulseCookie, dst) + seal.sys.setEnv(pulseCookie, dst) seal.sys.copyFile(dst, src) } diff --git a/internal/app/share.runtime.go b/internal/app/share.runtime.go index c25a89b..86add79 100644 --- a/internal/app/share.runtime.go +++ b/internal/app/share.runtime.go @@ -5,6 +5,7 @@ import ( "path" "git.ophivana.moe/cat/fortify/acl" + "git.ophivana.moe/cat/fortify/helper/bwrap" "git.ophivana.moe/cat/fortify/internal/state" ) @@ -20,9 +21,25 @@ const ( func (seal *appSeal) shareRuntime() { // look up shell if s, ok := os.LookupEnv(shell); ok { - seal.appendEnv(shell, s) + seal.sys.setEnv(shell, s) } + // mount tmpfs on inner runtime (e.g. `/run/user/%d`) + seal.sys.bwrap.Tmpfs = append(seal.sys.bwrap.Tmpfs, + bwrap.PermConfig[bwrap.TmpfsConfig]{ + Path: bwrap.TmpfsConfig{ + Size: 1 * 1024 * 1024, + Dir: "/run/user", + }, + }, + bwrap.PermConfig[bwrap.TmpfsConfig]{ + Path: bwrap.TmpfsConfig{ + Size: 8 * 1024 * 1024, + Dir: seal.sys.runtime, + }, + }, + ) + // ensure RunDir (e.g. `/run/user/%d/fortify`) seal.sys.ensure(seal.RunDirPath, 0700) seal.sys.updatePermTag(state.EnableLength, seal.RunDirPath, acl.Execute) @@ -57,9 +74,9 @@ func (seal *appSeal) shareRuntimeChild() string { seal.sys.updatePermTag(state.EnableLength, targetRuntime, acl.Read, acl.Write, acl.Execute) // point to ensured runtime path - seal.appendEnv(xdgRuntimeDir, targetRuntime) - seal.appendEnv(xdgSessionClass, "user") - seal.appendEnv(xdgSessionType, "tty") + seal.sys.setEnv(xdgRuntimeDir, targetRuntime) + seal.sys.setEnv(xdgSessionClass, "user") + seal.sys.setEnv(xdgSessionType, "tty") return targetRuntime } diff --git a/internal/app/start.go b/internal/app/start.go index ed34b7c..7325ee8 100644 --- a/internal/app/start.go +++ b/internal/app/start.go @@ -72,9 +72,8 @@ func (a *app) Start() error { if wls, err := shim.ServeConfig(confSockPath, &shim.Payload{ Argv: a.seal.command, - Env: a.seal.env, Exec: e, - Bwrap: a.seal.bwrap, + Bwrap: a.seal.sys.bwrap, WL: a.seal.wlDone != nil, Verbose: verbose.Get(), diff --git a/internal/app/system.go b/internal/app/system.go index b318b3a..7fc7312 100644 --- a/internal/app/system.go +++ b/internal/app/system.go @@ -20,19 +20,16 @@ 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 + // wait for wayland client to exit if mediated wayland is enabled, + // (wlDone == nil) determines whether mediated wayland setup is performed wlDone chan struct{} // freedesktop application ID fid string // argv to start process with in the final confined environment command []string - // environment variables of fortified process - env []string // persistent process state store store state.Store @@ -59,13 +56,10 @@ type appSeal struct { // protected by upstream mutex } -// appendEnv appends an environment variable for the child process -func (seal *appSeal) appendEnv(k, v string) { - seal.env = append(seal.env, k+"="+v) -} - // appSealTx contains the system-level component of the app seal type appSealTx struct { + bwrap *bwrap.Config + // reference to D-Bus proxy instance, nil if disabled dbus *dbus.Proxy // notification from goroutine waiting for dbus.Proxy @@ -86,6 +80,8 @@ type appSealTx struct { // dst, src pairs of temporarily hard linked files hardlinks [][2]string + // default formatted XDG_RUNTIME_DIR of User + runtime string // sealed path to fortify executable, used by shim executable string // target user UID as an integer @@ -107,6 +103,20 @@ type appEnsureEntry struct { remove bool } +// setEnv sets an environment variable for the child process +func (tx *appSealTx) setEnv(k, v string) { + tx.bwrap.SetEnv[k] = v +} + +// bind mounts a directory within the sandbox +func (tx *appSealTx) bind(src, dest string, ro bool) { + if !ro { + tx.bwrap.Bind = append(tx.bwrap.Bind, [2]string{src, dest}) + } else { + tx.bwrap.ROBind = append(tx.bwrap.ROBind, [2]string{src, dest}) + } +} + // ensure appends a directory ensure action func (tx *appSealTx) ensure(path string, perm os.FileMode) { tx.mkdir = append(tx.mkdir, appEnsureEntry{path, perm, false}) @@ -171,6 +181,7 @@ func (tx *appSealTx) changeHosts(username string) { func (tx *appSealTx) copyFile(dst, src string) { tx.tmpfiles = append(tx.tmpfiles, [2]string{dst, src}) tx.updatePerm(dst, acl.Read) + tx.bind(dst, dst, true) } // link appends a hardlink action @@ -194,7 +205,7 @@ func (tx *appSealTx) commit() error { } tx.complete = true - txp := &appSealTx{User: tx.User} + txp := &appSealTx{User: tx.User, bwrap: &bwrap.Config{SetEnv: make(map[string]string)}} defer func() { // rollback partial commit if txp != nil {