This removes the requirement to call fmsg.Exit on every exit path, and enables direct use of the "log" package. However, fmsg.BeforeExit is still encouraged when possible to catch exit on suspended output. Signed-off-by: Ophestra <cat@gensokyo.uk>
340 lines
10 KiB
Go
340 lines
10 KiB
Go
package app
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"path"
|
|
"strings"
|
|
|
|
"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"
|
|
"git.gensokyo.uk/security/fortify/wl"
|
|
)
|
|
|
|
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"
|
|
|
|
pulseServer = "PULSE_SERVER"
|
|
pulseCookie = "PULSE_COOKIE"
|
|
|
|
dbusSessionBusAddress = "DBUS_SESSION_BUS_ADDRESS"
|
|
dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS"
|
|
)
|
|
|
|
var (
|
|
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
|
|
}
|
|
|
|
// bind home directory
|
|
homeDir := "/var/empty"
|
|
if seal.sys.user.home != "" {
|
|
homeDir = seal.sys.user.home
|
|
}
|
|
username := "chronos"
|
|
if seal.sys.user.username != "" {
|
|
username = seal.sys.user.username
|
|
}
|
|
seal.sys.bwrap.Bind(seal.sys.user.data, homeDir, false, true)
|
|
seal.sys.bwrap.Chdir = homeDir
|
|
seal.sys.bwrap.SetEnv["HOME"] = homeDir
|
|
seal.sys.bwrap.SetEnv["USER"] = username
|
|
|
|
// generate /etc/passwd and /etc/group
|
|
seal.sys.bwrap.CopyBind("/etc/passwd",
|
|
[]byte(username+":x:"+seal.sys.mappedIDString+":"+seal.sys.mappedIDString+":Fortify:"+homeDir+":"+sh+"\n"))
|
|
seal.sys.bwrap.CopyBind("/etc/group",
|
|
[]byte("fortify:x:"+seal.sys.mappedIDString+":\n"))
|
|
|
|
/*
|
|
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 socketPath string
|
|
if name, ok := os.LookupEnv(wl.WaylandDisplay); !ok {
|
|
fmsg.Verbose(wl.WaylandDisplay + " is not set, assuming " + wl.FallbackName)
|
|
socketPath = path.Join(seal.RuntimePath, wl.FallbackName)
|
|
} else if !path.IsAbs(name) {
|
|
socketPath = path.Join(seal.RuntimePath, name)
|
|
} else {
|
|
socketPath = name
|
|
}
|
|
|
|
innerPath := path.Join(seal.sys.runtime, wl.FallbackName)
|
|
seal.sys.bwrap.SetEnv[wl.WaylandDisplay] = wl.FallbackName
|
|
|
|
if !seal.directWayland { // set up security-context-v1
|
|
socketDir := path.Join(seal.SharePath, "wayland")
|
|
outerPath := path.Join(socketDir, seal.id)
|
|
seal.sys.Ensure(socketDir, 0711)
|
|
appID := seal.fid
|
|
if appID == "" {
|
|
// use instance ID in case app id is not set
|
|
appID = "uk.gensokyo.fortify." + seal.id
|
|
}
|
|
seal.sys.Wayland(outerPath, socketPath, appID, seal.id)
|
|
seal.sys.bwrap.Bind(outerPath, innerPath)
|
|
} else { // bind mount wayland socket (insecure)
|
|
fmsg.Verbose("direct wayland access, PROCEED WITH CAUTION")
|
|
seal.sys.bwrap.Bind(socketPath, innerPath)
|
|
|
|
// ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`)
|
|
seal.sys.UpdatePermType(system.EWayland, socketPath, 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.Verbose(strings.TrimSpace(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)
|
|
}
|
|
|
|
// mount fortify in sandbox for init
|
|
seal.sys.bwrap.Bind(os.MustExecutable(), path.Join(fst.Tmp, "sbin/fortify"))
|
|
seal.sys.bwrap.Symlink("fortify", path.Join(fst.Tmp, "sbin/init"))
|
|
|
|
// append extra perms
|
|
for _, p := range seal.extraPerms {
|
|
if p == nil {
|
|
continue
|
|
}
|
|
if p.ensure {
|
|
seal.sys.Ensure(p.name, 0700)
|
|
}
|
|
seal.sys.UpdatePermType(system.User, p.name, p.perms...)
|
|
}
|
|
|
|
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))
|
|
}
|