diff options
| author | Ophestra <cat@gensokyo.uk> | 2024-12-28 11:12:35 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2024-12-28 11:12:35 +0900 |
| commit | 0107620d8c174dcf9237c16d4b4b7b2f6955a214 (patch) | |
| tree | 3b082eafad73eaa971a59baf762260c89917831b /internal/app/share.go | |
| parent | fc26659ea11fea8f41fc2551d6d39d30fe3a49e7 (diff) | |
app: merge share methods
This significantly increases readability and makes order of ops more obvious.
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/app/share.go')
| -rw-r--r-- | internal/app/share.go | 335 |
1 files changed, 335 insertions, 0 deletions
diff --git a/internal/app/share.go b/internal/app/share.go new file mode 100644 index 00000000..dea55a87 --- /dev/null +++ b/internal/app/share.go @@ -0,0 +1,335 @@ +package app + +import ( + "errors" + "fmt" + "io/fs" + "path" + + "git.gensokyo.uk/security/fortify/acl" + "git.gensokyo.uk/security/fortify/dbus" + "git.gensokyo.uk/security/fortify/fst" + "git.gensokyo.uk/security/fortify/internal/fmsg" + "git.gensokyo.uk/security/fortify/internal/linux" + "git.gensokyo.uk/security/fortify/internal/system" +) + +const ( + home = "HOME" + shell = "SHELL" + + xdgConfigHome = "XDG_CONFIG_HOME" + xdgRuntimeDir = "XDG_RUNTIME_DIR" + xdgSessionClass = "XDG_SESSION_CLASS" + xdgSessionType = "XDG_SESSION_TYPE" + + term = "TERM" + display = "DISPLAY" + + // https://manpages.debian.org/experimental/libwayland-doc/wl_display_connect.3.en.html + waylandDisplay = "WAYLAND_DISPLAY" + + pulseServer = "PULSE_SERVER" + pulseCookie = "PULSE_COOKIE" + + dbusSessionBusAddress = "DBUS_SESSION_BUS_ADDRESS" + dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS" +) + +var ( + ErrWayland = errors.New(waylandDisplay + " unset") + ErrXDisplay = errors.New(display + " unset") + + ErrPulseCookie = errors.New("pulse cookie not present") + ErrPulseSocket = errors.New("pulse socket not present") + ErrPulseMode = errors.New("unexpected pulse socket mode") +) + +func (seal *appSeal) setupShares(bus [2]*dbus.Config, os linux.System) error { + if seal.shared { + panic("seal shared twice") + } + seal.shared = true + + /* + Tmpdir-based share directory + */ + + // ensure Share (e.g. `/tmp/fortify.%d`) + // acl is unnecessary as this directory is world executable + seal.sys.Ensure(seal.SharePath, 0711) + + // ensure process-specific share (e.g. `/tmp/fortify.%d/%s`) + // acl is unnecessary as this directory is world executable + seal.share = path.Join(seal.SharePath, seal.id) + seal.sys.Ephemeral(system.Process, seal.share, 0711) + + // ensure child tmpdir parent directory (e.g. `/tmp/fortify.%d/tmpdir`) + targetTmpdirParent := path.Join(seal.SharePath, "tmpdir") + seal.sys.Ensure(targetTmpdirParent, 0700) + seal.sys.UpdatePermType(system.User, targetTmpdirParent, acl.Execute) + + // ensure child tmpdir (e.g. `/tmp/fortify.%d/tmpdir/%d`) + targetTmpdir := path.Join(targetTmpdirParent, seal.sys.user.as) + seal.sys.Ensure(targetTmpdir, 01700) + seal.sys.UpdatePermType(system.User, targetTmpdir, acl.Read, acl.Write, acl.Execute) + seal.sys.bwrap.Bind(targetTmpdir, "/tmp", false, true) + + /* + XDG runtime directory + */ + + // mount tmpfs on inner runtime (e.g. `/run/user/%d`) + seal.sys.bwrap.Tmpfs("/run/user", 1*1024*1024) + seal.sys.bwrap.Tmpfs(seal.sys.runtime, 8*1024*1024) + + // point to inner runtime path `/run/user/%d` + seal.sys.bwrap.SetEnv[xdgRuntimeDir] = seal.sys.runtime + seal.sys.bwrap.SetEnv[xdgSessionClass] = "user" + seal.sys.bwrap.SetEnv[xdgSessionType] = "tty" + + // ensure RunDir (e.g. `/run/user/%d/fortify`) + seal.sys.Ensure(seal.RunDirPath, 0700) + seal.sys.UpdatePermType(system.User, seal.RunDirPath, acl.Execute) + + // ensure runtime directory ACL (e.g. `/run/user/%d`) + seal.sys.Ensure(seal.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset + seal.sys.UpdatePermType(system.User, seal.RuntimePath, acl.Execute) + + // ensure process-specific share local to XDG_RUNTIME_DIR (e.g. `/run/user/%d/fortify/%s`) + seal.shareLocal = path.Join(seal.RunDirPath, seal.id) + seal.sys.Ephemeral(system.Process, seal.shareLocal, 0700) + seal.sys.UpdatePerm(seal.shareLocal, acl.Execute) + + /* + Inner passwd database + */ + + // look up shell + sh := "/bin/sh" + if s, ok := os.LookupEnv(shell); ok { + seal.sys.bwrap.SetEnv[shell] = s + sh = s + } + + // generate /etc/passwd + passwdPath := path.Join(seal.share, "passwd") + username := "chronos" + if seal.sys.user.username != "" { + username = seal.sys.user.username + } + homeDir := "/var/empty" + if seal.sys.user.home != "" { + homeDir = seal.sys.user.home + } + + // bind home directory + seal.sys.bwrap.Bind(seal.sys.user.data, homeDir, false, true) + seal.sys.bwrap.Chdir = homeDir + + seal.sys.bwrap.SetEnv["USER"] = username + seal.sys.bwrap.SetEnv["HOME"] = homeDir + + passwd := username + ":x:" + seal.sys.mappedIDString + ":" + seal.sys.mappedIDString + ":Fortify:" + homeDir + ":" + sh + "\n" + seal.sys.Write(passwdPath, passwd) + + // write /etc/group + groupPath := path.Join(seal.share, "group") + seal.sys.Write(groupPath, "fortify:x:"+seal.sys.mappedIDString+":\n") + + // bind /etc/passwd and /etc/group + seal.sys.bwrap.Bind(passwdPath, "/etc/passwd") + seal.sys.bwrap.Bind(groupPath, "/etc/group") + + /* + Display servers + */ + + // pass $TERM to launcher + if t, ok := os.LookupEnv(term); ok { + seal.sys.bwrap.SetEnv[term] = t + } + + // set up wayland + if seal.et.Has(system.EWayland) { + var wp string + if wd, ok := os.LookupEnv(waylandDisplay); !ok { + return fmsg.WrapError(ErrWayland, + "WAYLAND_DISPLAY is not set") + } else { + wp = path.Join(seal.RuntimePath, wd) + } + + w := path.Join(seal.sys.runtime, "wayland-0") + seal.sys.bwrap.SetEnv[waylandDisplay] = w + + if !seal.directWayland { // set up security-context-v1 + wc := path.Join(seal.SharePath, "wayland") + wt := path.Join(wc, seal.id) + seal.sys.Ensure(wc, 0711) + appID := seal.fid + if appID == "" { + // use instance ID in case app id is not set + appID = "moe.ophivana.fortify." + seal.id + } + seal.sys.Wayland(wt, wp, appID, seal.id) + seal.sys.bwrap.Bind(wt, w) + } else { // bind mount wayland socket (insecure) + // hardlink wayland socket + wpi := path.Join(seal.shareLocal, "wayland") + seal.sys.Link(wp, wpi) + seal.sys.bwrap.Bind(wpi, w) + + // ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`) + seal.sys.UpdatePermType(system.EWayland, wp, acl.Read, acl.Write, acl.Execute) + } + } + + // set up X11 + if seal.et.Has(system.EX11) { + // discover X11 and grant user permission via the `ChangeHosts` command + if d, ok := os.LookupEnv(display); !ok { + return fmsg.WrapError(ErrXDisplay, + "DISPLAY is not set") + } else { + seal.sys.ChangeHosts("#" + seal.sys.user.us) + seal.sys.bwrap.SetEnv[display] = d + seal.sys.bwrap.Bind("/tmp/.X11-unix", "/tmp/.X11-unix") + } + } + + /* + PulseAudio server and authentication + */ + + if seal.et.Has(system.EPulse) { + // check PulseAudio directory presence (e.g. `/run/user/%d/pulse`) + pd := path.Join(seal.RuntimePath, "pulse") + ps := path.Join(pd, "native") + if _, err := os.Stat(pd); err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return fmsg.WrapErrorSuffix(err, + fmt.Sprintf("cannot access PulseAudio directory %q:", pd)) + } + return fmsg.WrapError(ErrPulseSocket, + fmt.Sprintf("PulseAudio directory %q not found", pd)) + } + + // check PulseAudio socket permission (e.g. `/run/user/%d/pulse/native`) + if s, err := os.Stat(ps); err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return fmsg.WrapErrorSuffix(err, + fmt.Sprintf("cannot access PulseAudio socket %q:", ps)) + } + return fmsg.WrapError(ErrPulseSocket, + fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pd)) + } else { + if m := s.Mode(); m&0o006 != 0o006 { + return fmsg.WrapError(ErrPulseMode, + fmt.Sprintf("unexpected permissions on %q:", ps), m) + } + } + + // 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.sys.bwrap.Bind(psi, p) + seal.sys.bwrap.SetEnv[pulseServer] = "unix:" + p + + // publish current user's pulse cookie for target user + if src, err := discoverPulseCookie(os); err != nil { + // not fatal + fmsg.VPrintln(err.(*fmsg.BaseError).Message()) + } else { + dst := path.Join(seal.share, "pulse-cookie") + innerDst := fst.Tmp + "/pulse-cookie" + seal.sys.bwrap.SetEnv[pulseCookie] = innerDst + seal.sys.CopyFile(dst, src) + seal.sys.bwrap.Bind(dst, innerDst) + } + } + + /* + D-Bus proxy + */ + + if seal.et.Has(system.EDBus) { + // ensure dbus session bus defaults + if bus[0] == nil { + bus[0] = dbus.NewConfig(seal.fid, true, true) + } + + // downstream socket paths + sessionPath, systemPath := path.Join(seal.share, "bus"), path.Join(seal.share, "system_bus_socket") + + // configure dbus proxy + if f, err := seal.sys.ProxyDBus(bus[0], bus[1], sessionPath, systemPath); err != nil { + return err + } else { + seal.dbusMsg = f + } + + // share proxy sockets + sessionInner := path.Join(seal.sys.runtime, "bus") + seal.sys.bwrap.SetEnv[dbusSessionBusAddress] = "unix:path=" + sessionInner + seal.sys.bwrap.Bind(sessionPath, sessionInner) + seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write) + if bus[1] != nil { + systemInner := "/run/dbus/system_bus_socket" + seal.sys.bwrap.SetEnv[dbusSystemBusAddress] = "unix:path=" + systemInner + seal.sys.bwrap.Bind(systemPath, systemInner) + seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write) + } + } + + /* + Miscellaneous + */ + + // queue overriding tmpfs at the end of seal.sys.bwrap.Filesystem + for _, dest := range seal.sys.override { + seal.sys.bwrap.Tmpfs(dest, 8*1024) + } + + return nil +} + +// discoverPulseCookie attempts various standard methods to discover the current user's PulseAudio authentication cookie +func discoverPulseCookie(os linux.System) (string, error) { + if p, ok := os.LookupEnv(pulseCookie); ok { + return p, nil + } + + // dotfile $HOME/.pulse-cookie + if p, ok := os.LookupEnv(home); ok { + p = path.Join(p, ".pulse-cookie") + if s, err := os.Stat(p); err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return p, fmsg.WrapErrorSuffix(err, + fmt.Sprintf("cannot access PulseAudio cookie %q:", p)) + } + // not found, try next method + } else if !s.IsDir() { + return p, nil + } + } + + // $XDG_CONFIG_HOME/pulse/cookie + if p, ok := os.LookupEnv(xdgConfigHome); ok { + p = path.Join(p, "pulse", "cookie") + if s, err := os.Stat(p); err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return p, fmsg.WrapErrorSuffix(err, + fmt.Sprintf("cannot access PulseAudio cookie %q:", p)) + } + // not found, try next method + } else if !s.IsDir() { + return p, nil + } + } + + return "", fmsg.WrapError(ErrPulseCookie, + fmt.Sprintf("cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)", + pulseCookie, xdgConfigHome, home)) +} |
