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)) }