app: remove share method
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Run NixOS test (push) Successful in 2m3s

This is yet another implementation detail from before system.I, getting rid of this vastly cuts down on redundant seal state.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
Ophestra 2025-02-19 13:41:06 +09:00
parent 2978a6f046
commit ef81828e0c
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
7 changed files with 394 additions and 484 deletions

View File

@ -24,10 +24,10 @@ type RunState struct {
// Paths contains environment-dependent paths used by fortify. // Paths contains environment-dependent paths used by fortify.
type Paths struct { type Paths struct {
// path to shared directory e.g. /tmp/fortify.%d // path to shared directory (usually `/tmp/fortify.%d`)
SharePath string `json:"share_path"` SharePath string `json:"share_path"`
// XDG_RUNTIME_DIR value e.g. /run/user/%d // XDG_RUNTIME_DIR value (usually `/run/user/%d`)
RuntimePath string `json:"runtime_path"` RuntimePath string `json:"runtime_path"`
// application runtime directory e.g. /run/user/%d/fortify // application runtime directory (usually `/run/user/%d/fortify`)
RunDirPath string `json:"run_dir_path"` RunDirPath string `json:"run_dir_path"`
} }

View File

@ -10,9 +10,11 @@ const Tmp = "/.fortify"
// Config is used to seal an app // Config is used to seal an app
type Config struct { type Config struct {
// application ID // reverse-DNS style arbitrary identifier string from config;
// passed to wayland security-context-v1 as application ID
// and used as part of defaults in dbus session proxy
ID string `json:"id"` ID string `json:"id"`
// value passed through to the child process as its argv // final argv, passed to init
Command []string `json:"command"` Command []string `json:"command"`
Confinement ConfinementConfig `json:"confinement"` Confinement ConfinementConfig `json:"confinement"`
@ -32,7 +34,7 @@ type ConfinementConfig struct {
Outer string `json:"home"` Outer string `json:"home"`
// bwrap sandbox confinement configuration // bwrap sandbox confinement configuration
Sandbox *SandboxConfig `json:"sandbox"` Sandbox *SandboxConfig `json:"sandbox"`
// extra acl entries to append // extra acl ops, runs after everything else
ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"` ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"`
// reference to a system D-Bus proxy configuration, // reference to a system D-Bus proxy configuration,

View File

@ -26,7 +26,8 @@ type SandboxConfig struct {
NoNewSession bool `json:"no_new_session,omitempty"` NoNewSession bool `json:"no_new_session,omitempty"`
// map target user uid to privileged user uid in the user namespace // map target user uid to privileged user uid in the user namespace
MapRealUID bool `json:"map_real_uid"` MapRealUID bool `json:"map_real_uid"`
// direct access to wayland socket // direct access to wayland socket; when this gets set no attempt is made to attach security-context-v1
// and the bare socket is mounted to the sandbox
DirectWayland bool `json:"direct_wayland,omitempty"` DirectWayland bool `json:"direct_wayland,omitempty"`
// final environment variables // final environment variables
@ -39,7 +40,8 @@ type SandboxConfig struct {
Etc string `json:"etc,omitempty"` Etc string `json:"etc,omitempty"`
// automatically set up /etc symlinks // automatically set up /etc symlinks
AutoEtc bool `json:"auto_etc"` AutoEtc bool `json:"auto_etc"`
// paths to override by mounting tmpfs over them // mount tmpfs over these paths,
// runs right before [ConfinementConfig.ExtraPerms]
Override []string `json:"override"` Override []string `json:"override"`
} }
@ -56,7 +58,7 @@ type SandboxSys interface {
// Bwrap returns the address of the corresponding bwrap.Config to s. // Bwrap returns the address of the corresponding bwrap.Config to s.
// Note that remaining tmpfs entries must be queued by the caller prior to launch. // Note that remaining tmpfs entries must be queued by the caller prior to launch.
func (s *SandboxConfig) Bwrap(sys SandboxSys) (*bwrap.Config, error) { func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) {
if s == nil { if s == nil {
return nil, errors.New("nil sandbox config") return nil, errors.New("nil sandbox config")
} }
@ -65,16 +67,20 @@ func (s *SandboxConfig) Bwrap(sys SandboxSys) (*bwrap.Config, error) {
sys.Println("syscall filter not configured, PROCEED WITH CAUTION") sys.Println("syscall filter not configured, PROCEED WITH CAUTION")
} }
var uid int
if !s.MapRealUID { if !s.MapRealUID {
uid = 65534 // mapped uid defaults to 65534 to work around file ownership checks due to a bwrap limitation
*uid = 65534
} else { } else {
uid = sys.Geteuid() // some programs fail to connect to dbus session running as a different uid, so a separate workaround
// is introduced to map priv-side caller uid in namespace
*uid = sys.Geteuid()
} }
conf := (&bwrap.Config{ conf := (&bwrap.Config{
Net: s.Net, Net: s.Net,
UserNS: s.UserNS, UserNS: s.UserNS,
UID: uid,
GID: uid,
Hostname: s.Hostname, Hostname: s.Hostname,
Clearenv: true, Clearenv: true,
SetEnv: s.Env, SetEnv: s.Env,
@ -93,7 +99,6 @@ func (s *SandboxConfig) Bwrap(sys SandboxSys) (*bwrap.Config, error) {
// for saving such a miniscule amount of memory // for saving such a miniscule amount of memory
Chmod: make(bwrap.ChmodConfig), Chmod: make(bwrap.ChmodConfig),
}). }).
SetUID(uid).SetGID(uid).
Procfs("/proc"). Procfs("/proc").
Tmpfs(Tmp, 4*1024) Tmpfs(Tmp, 4*1024)

View File

@ -18,13 +18,13 @@ var testCasesNixos = []sealTestCase{
AppID: 1, Groups: []string{}, Username: "u0_a1", AppID: 1, Groups: []string{}, Username: "u0_a1",
Outer: "/var/lib/persist/module/fortify/0/1", Outer: "/var/lib/persist/module/fortify/0/1",
Sandbox: &fst.SandboxConfig{ Sandbox: &fst.SandboxConfig{
UserNS: true, Net: true, MapRealUID: true, DirectWayland: true, Env: nil, UserNS: true, Net: true, MapRealUID: true, DirectWayland: true, Env: nil, AutoEtc: true,
Filesystem: []*fst.FilesystemConfig{ Filesystem: []*fst.FilesystemConfig{
{Src: "/bin", Must: true}, {Src: "/usr/bin", Must: true}, {Src: "/bin", Must: true}, {Src: "/usr/bin", Must: true},
{Src: "/nix/store", Must: true}, {Src: "/run/current-system", Must: true}, {Src: "/nix/store", Must: true}, {Src: "/run/current-system", Must: true},
{Src: "/sys/block"}, {Src: "/sys/bus"}, {Src: "/sys/class"}, {Src: "/sys/dev"}, {Src: "/sys/devices"}, {Src: "/sys/block"}, {Src: "/sys/bus"}, {Src: "/sys/class"}, {Src: "/sys/dev"}, {Src: "/sys/devices"},
{Src: "/run/opengl-driver", Must: true}, {Src: "/dev/dri", Device: true}, {Src: "/run/opengl-driver", Must: true}, {Src: "/dev/dri", Device: true},
}, AutoEtc: true, },
Override: []string{"/var/run/nscd"}, Override: []string{"/var/run/nscd"},
}, },
SystemBus: &dbus.Config{ SystemBus: &dbus.Config{
@ -56,12 +56,12 @@ var testCasesNixos = []sealTestCase{
}, },
system.New(1000001). system.New(1000001).
Ensure("/tmp/fortify.1971", 0711). Ensure("/tmp/fortify.1971", 0711).
Ephemeral(system.Process, "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1", 0711).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/1", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/1", acl.Read, acl.Write, acl.Execute).
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute). Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1", 0711).
Ephemeral(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", acl.Execute). Ephemeral(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/1", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/1", acl.Read, acl.Write, acl.Execute).
UpdatePermType(system.EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute). UpdatePermType(system.EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute).
Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse"). Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse").
CopyFile(nil, "/home/ophestra/xdg/config/pulse/cookie", 256, 256). CopyFile(nil, "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
@ -205,9 +205,9 @@ var testCasesNixos = []sealTestCase{
Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile"). Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv"). Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc"). Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", false, true).
Tmpfs("/run/user", 1048576). Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/1971", 8388608). Tmpfs("/run/user/1971", 8388608).
Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", false, true).
Bind("/var/lib/persist/module/fortify/0/1", "/var/lib/persist/module/fortify/0/1", false, true). Bind("/var/lib/persist/module/fortify/0/1", "/var/lib/persist/module/fortify/0/1", false, true).
CopyBind("/etc/passwd", []byte("u0_a1:x:1971:1971:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n")). CopyBind("/etc/passwd", []byte("u0_a1:x:1971:1971:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n")).
CopyBind("/etc/group", []byte("fortify:x:1971:\n")). CopyBind("/etc/group", []byte("fortify:x:1971:\n")).

View File

@ -29,12 +29,12 @@ var testCasesPd = []sealTestCase{
}, },
system.New(1000000). system.New(1000000).
Ensure("/tmp/fortify.1971", 0711). Ensure("/tmp/fortify.1971", 0711).
Ephemeral(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac", 0711).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute).
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute). Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", acl.Execute), Ephemeral(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac", 0711).
Ephemeral(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute),
(&bwrap.Config{ (&bwrap.Config{
Net: true, Net: true,
UserNS: true, UserNS: true,
@ -150,9 +150,9 @@ var testCasesPd = []sealTestCase{
Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile"). Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv"). Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc"). Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", false, true).
Tmpfs("/run/user", 1048576). Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/65534", 8388608). Tmpfs("/run/user/65534", 8388608).
Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", false, true).
Bind("/home/chronos", "/home/chronos", false, true). Bind("/home/chronos", "/home/chronos", false, true).
CopyBind("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")). CopyBind("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
CopyBind("/etc/group", []byte("fortify:x:65534:\n")). CopyBind("/etc/group", []byte("fortify:x:65534:\n")).
@ -212,12 +212,12 @@ var testCasesPd = []sealTestCase{
}, },
system.New(1000009). system.New(1000009).
Ensure("/tmp/fortify.1971", 0711). Ensure("/tmp/fortify.1971", 0711).
Ephemeral(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c", 0711).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/9", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/9", acl.Read, acl.Write, acl.Execute).
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute). Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
Ephemeral(system.Process, "/tmp/fortify.1971/ebf083d1b175911782d413369b64ce7c", 0711).
Ephemeral(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", acl.Execute). Ephemeral(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute).
Ensure("/tmp/fortify.1971/tmpdir/9", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/9", acl.Read, acl.Write, acl.Execute).
Ensure("/tmp/fortify.1971/wayland", 0711). Ensure("/tmp/fortify.1971/wayland", 0711).
Wayland(new(*os.File), "/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/1971/wayland-0", "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"). Wayland(new(*os.File), "/tmp/fortify.1971/wayland/ebf083d1b175911782d413369b64ce7c", "/run/user/1971/wayland-0", "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse"). Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/ebf083d1b175911782d413369b64ce7c/pulse").
@ -376,9 +376,9 @@ var testCasesPd = []sealTestCase{
Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile"). Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile").
Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv"). Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv").
Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc"). Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc").
Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", false, true).
Tmpfs("/run/user", 1048576). Tmpfs("/run/user", 1048576).
Tmpfs("/run/user/65534", 8388608). Tmpfs("/run/user/65534", 8388608).
Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", false, true).
Bind("/home/chronos", "/home/chronos", false, true). Bind("/home/chronos", "/home/chronos", false, true).
CopyBind("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")). CopyBind("/etc/passwd", []byte("chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n")).
CopyBind("/etc/group", []byte("fortify:x:65534:\n")). CopyBind("/etc/group", []byte("fortify:x:65534:\n")).

View File

@ -10,6 +10,7 @@ import (
"os" "os"
"path" "path"
"regexp" "regexp"
"strings"
"git.gensokyo.uk/security/fortify/acl" "git.gensokyo.uk/security/fortify/acl"
"git.gensokyo.uk/security/fortify/dbus" "git.gensokyo.uk/security/fortify/dbus"
@ -20,6 +21,26 @@ import (
"git.gensokyo.uk/security/fortify/internal/state" "git.gensokyo.uk/security/fortify/internal/state"
"git.gensokyo.uk/security/fortify/internal/sys" "git.gensokyo.uk/security/fortify/internal/sys"
"git.gensokyo.uk/security/fortify/system" "git.gensokyo.uk/security/fortify/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 ( var (
@ -27,65 +48,38 @@ var (
ErrUser = errors.New("invalid aid") ErrUser = errors.New("invalid aid")
ErrHome = errors.New("invalid home directory") ErrHome = errors.New("invalid home directory")
ErrName = errors.New("invalid username") ErrName = errors.New("invalid username")
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")
) )
var posixUsername = regexp.MustCompilePOSIX("^[a-z_]([A-Za-z0-9_-]{0,31}|[A-Za-z0-9_-]{0,30}\\$)$") var posixUsername = regexp.MustCompilePOSIX("^[a-z_]([A-Za-z0-9_-]{0,31}|[A-Za-z0-9_-]{0,30}\\$)$")
// appSeal stores copies of various parts of [fst.Config] // appSeal stores copies of various parts of [fst.Config]
type appSeal struct { type appSeal struct {
// string representation of [fst.ID] // passed through from [fst.Config]
id string
// dump dbus proxy message buffer
dbusMsg func()
// reverse-DNS style arbitrary identifier string from config;
// passed to wayland security-context-v1 as application ID
// and used as part of defaults in dbus session proxy
appID string
// final argv, passed to init
command []string command []string
// state instance initialised during seal; used during process lifecycle events // state instance initialised during seal; used during process lifecycle events
store state.Store store state.Store
// initial [fst.Config] gob stream for state data;
// this is prepared ahead of time as config is mutated during seal creation
ct io.WriterTo
// dump dbus proxy message buffer
dbusMsg func()
// whether [system.I] was committed; used during process lifecycle events // whether [system.I] was committed; used during process lifecycle events
needRevert bool needRevert bool
// whether state was inserted into [state.Store]; used during process lifecycle events // whether state was inserted into [state.Store]; used during process lifecycle events
stateInStore bool stateInStore bool
// process-specific share directory path ([os.TempDir])
share string
// process-specific share directory path ([fst.Paths] XDG_RUNTIME_DIR)
shareLocal string
// initial [fst.Config] gob stream for state data;
// this is prepared ahead of time as config is mutated during seal creation
ct io.WriterTo
// passed through from [fst.SandboxConfig];
// when this gets set no attempt is made to attach security-context-v1
// and the bare socket is mounted to the sandbox
directWayland bool
// mount tmpfs over these paths, runs right before extraPerms
override []string
// extra [acl.Update] ops, appended at the end of [system.I]
extraPerms []*sealedExtraPerm
// post fsu state
user appUser user appUser
// inner XDG_RUNTIME_DIR, default formatting via user
innerRuntimeDir string
// mapped uid and gid in user namespace
mapuid *stringPair[int]
sys *system.I sys *system.I
container *bwrap.Config container *bwrap.Config
bwrapSync *os.File bwrapSync *os.File
// prevents sharing from happening twice
shared bool
system.Enablements
fst.Paths
// protected by upstream mutex // protected by upstream mutex
} }
@ -107,13 +101,8 @@ type appUser struct {
username string username string
} }
type sealedExtraPerm struct {
name string
perms acl.Perms
ensure bool
}
func (seal *appSeal) finalise(sys sys.State, config *fst.Config, id string) error { func (seal *appSeal) finalise(sys sys.State, config *fst.Config, id string) error {
{
// encode initial configuration for state tracking // encode initial configuration for state tracking
ct := new(bytes.Buffer) ct := new(bytes.Buffer)
if err := gob.NewEncoder(ct).Encode(config); err != nil { if err := gob.NewEncoder(ct).Encode(config); err != nil {
@ -121,31 +110,21 @@ func (seal *appSeal) finalise(sys sys.State, config *fst.Config, id string) erro
"cannot encode initial config:") "cannot encode initial config:")
} }
seal.ct = ct seal.ct = ct
}
seal.Paths = sys.Paths() // pass through command slice; this value is never touched in the main process
// pass through config values
seal.id = id
seal.appID = config.ID
seal.command = config.Command seal.command = config.Command
{ // allowed aid range 0 to 9999, this is checked again in fsu
// mapped uid defaults to 65534 to work around file ownership checks due to a bwrap limitation
mapuid := 65534
if config.Confinement.Sandbox != nil && config.Confinement.Sandbox.MapRealUID {
// some programs fail to connect to dbus session running as a different uid, so a
// separate workaround is introduced to map priv-side caller uid in namespace
mapuid = sys.Geteuid()
}
seal.mapuid = newInt(mapuid)
seal.innerRuntimeDir = path.Join("/run/user", seal.mapuid.String())
}
// validate uid and set user info
if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 { if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 {
return fmsg.WrapError(ErrUser, return fmsg.WrapError(ErrUser,
fmt.Sprintf("aid %d out of range", config.Confinement.AppID)) fmt.Sprintf("aid %d out of range", config.Confinement.AppID))
} }
/*
Resolve post-fsu user state
*/
seal.user = appUser{ seal.user = appUser{
aid: newInt(config.Confinement.AppID), aid: newInt(config.Confinement.AppID),
data: config.Confinement.Outer, data: config.Confinement.Outer,
@ -166,15 +145,11 @@ func (seal *appSeal) finalise(sys sys.State, config *fst.Config, id string) erro
if seal.user.home == "" { if seal.user.home == "" {
seal.user.home = seal.user.data seal.user.home = seal.user.data
} }
// invoke fsu for full uid
if u, err := sys.Uid(seal.user.aid.unwrap()); err != nil { if u, err := sys.Uid(seal.user.aid.unwrap()); err != nil {
return err return err
} else { } else {
seal.user.uid = newInt(u) seal.user.uid = newInt(u)
} }
// resolve supplementary group ids from names
seal.user.supp = make([]string, len(config.Confinement.Groups)) seal.user.supp = make([]string, len(config.Confinement.Groups))
for i, name := range config.Confinement.Groups { for i, name := range config.Confinement.Groups {
if g, err := sys.LookupGroup(name); err != nil { if g, err := sys.LookupGroup(name); err != nil {
@ -185,33 +160,14 @@ func (seal *appSeal) finalise(sys sys.State, config *fst.Config, id string) erro
} }
} }
// build extra perms /*
seal.extraPerms = make([]*sealedExtraPerm, len(config.Confinement.ExtraPerms)) Resolve initial container state
for i, p := range config.Confinement.ExtraPerms { */
if p == nil {
continue
}
seal.extraPerms[i] = new(sealedExtraPerm) // permissive defaults
seal.extraPerms[i].name = p.Path
seal.extraPerms[i].perms = make(acl.Perms, 0, 3)
if p.Read {
seal.extraPerms[i].perms = append(seal.extraPerms[i].perms, acl.Read)
}
if p.Write {
seal.extraPerms[i].perms = append(seal.extraPerms[i].perms, acl.Write)
}
if p.Execute {
seal.extraPerms[i].perms = append(seal.extraPerms[i].perms, acl.Execute)
}
seal.extraPerms[i].ensure = p.Ensure
}
// map sandbox config to bwrap
if config.Confinement.Sandbox == nil { if config.Confinement.Sandbox == nil {
fmsg.Verbose("sandbox configuration not supplied, PROCEED WITH CAUTION") fmsg.Verbose("sandbox configuration not supplied, PROCEED WITH CAUTION")
// permissive defaults
conf := &fst.SandboxConfig{ conf := &fst.SandboxConfig{
UserNS: true, UserNS: true,
Net: true, Net: true,
@ -254,40 +210,326 @@ func (seal *appSeal) finalise(sys sys.State, config *fst.Config, id string) erro
config.Confinement.Sandbox = conf config.Confinement.Sandbox = conf
} }
seal.directWayland = config.Confinement.Sandbox.DirectWayland
if b, err := config.Confinement.Sandbox.Bwrap(sys); err != nil { var mapuid *stringPair[int]
{
var uid int
var err error
seal.container, err = config.Confinement.Sandbox.Bwrap(sys, &uid)
if err != nil {
return err return err
} else {
seal.container = b
} }
seal.override = config.Confinement.Sandbox.Override mapuid = newInt(uid)
if seal.container.SetEnv == nil { if seal.container.SetEnv == nil {
seal.container.SetEnv = make(map[string]string) seal.container.SetEnv = make(map[string]string)
} }
}
// open process state store /*
// the simple store only starts holding an open file after first action Initialise externals
// store activity begins after Start is called and must end before Wait */
seal.store = state.NewMulti(seal.RunDirPath)
// initialise system interface with os uid sc := sys.Paths()
seal.store = state.NewMulti(sc.RunDirPath)
seal.sys = system.New(seal.user.uid.unwrap()) seal.sys = system.New(seal.user.uid.unwrap())
seal.sys.IsVerbose = fmsg.Load seal.sys.IsVerbose = fmsg.Load
seal.sys.Verbose = fmsg.Verbose seal.sys.Verbose = fmsg.Verbose
seal.sys.Verbosef = fmsg.Verbosef seal.sys.Verbosef = fmsg.Verbosef
seal.sys.WrapErr = fmsg.WrapError seal.sys.WrapErr = fmsg.WrapError
// pass through enablements /*
seal.Enablements = config.Confinement.Enablements Work directories
*/
// this method calls all share methods in sequence // base fortify share path
if err := seal.setupShares([2]*dbus.Config{config.Confinement.SessionBus, config.Confinement.SystemBus}, sys); err != nil { seal.sys.Ensure(sc.SharePath, 0711)
return err
// outer paths used by the main process
seal.sys.Ensure(sc.RunDirPath, 0700)
seal.sys.UpdatePermType(system.User, sc.RunDirPath, acl.Execute)
seal.sys.Ensure(sc.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset
seal.sys.UpdatePermType(system.User, sc.RuntimePath, acl.Execute)
// outer process-specific share directory
sharePath := path.Join(sc.SharePath, id)
seal.sys.Ephemeral(system.Process, sharePath, 0711)
// similar to share but within XDG_RUNTIME_DIR
sharePathLocal := path.Join(sc.RunDirPath, id)
seal.sys.Ephemeral(system.Process, sharePathLocal, 0700)
seal.sys.UpdatePerm(sharePathLocal, acl.Execute)
// inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as post-fsu user
innerRuntimeDir := path.Join("/run/user", mapuid.String())
seal.container.Tmpfs("/run/user", 1*1024*1024)
seal.container.Tmpfs(innerRuntimeDir, 8*1024*1024)
seal.container.SetEnv[xdgRuntimeDir] = innerRuntimeDir
seal.container.SetEnv[xdgSessionClass] = "user"
seal.container.SetEnv[xdgSessionType] = "tty"
// outer path for inner /tmp
{
tmpdir := path.Join(sc.SharePath, "tmpdir")
seal.sys.Ensure(tmpdir, 0700)
seal.sys.UpdatePermType(system.User, tmpdir, acl.Execute)
tmpdirProc := path.Join(tmpdir, seal.user.aid.String())
seal.sys.Ensure(tmpdirProc, 01700)
seal.sys.UpdatePermType(system.User, tmpdirProc, acl.Read, acl.Write, acl.Execute)
seal.container.Bind(tmpdirProc, "/tmp", false, true)
} }
// verbose log seal information /*
Passwd database
*/
// look up shell
sh := "/bin/sh"
if s, ok := sys.LookupEnv(shell); ok {
seal.container.SetEnv[shell] = s
sh = s
}
// bind home directory
homeDir := "/var/empty"
if seal.user.home != "" {
homeDir = seal.user.home
}
username := "chronos"
if seal.user.username != "" {
username = seal.user.username
}
seal.container.Bind(seal.user.data, homeDir, false, true)
seal.container.Chdir = homeDir
seal.container.SetEnv["HOME"] = homeDir
seal.container.SetEnv["USER"] = username
// generate /etc/passwd and /etc/group
seal.container.CopyBind("/etc/passwd",
[]byte(username+":x:"+mapuid.String()+":"+mapuid.String()+":Fortify:"+homeDir+":"+sh+"\n"))
seal.container.CopyBind("/etc/group",
[]byte("fortify:x:"+mapuid.String()+":\n"))
/*
Display servers
*/
// pass $TERM to launcher
if t, ok := sys.LookupEnv(term); ok {
seal.container.SetEnv[term] = t
}
// set up wayland
if config.Confinement.Enablements.Has(system.EWayland) {
// outer wayland socket (usually `/run/user/%d/wayland-%d`)
var socketPath string
if name, ok := sys.LookupEnv(wl.WaylandDisplay); !ok {
fmsg.Verbose(wl.WaylandDisplay + " is not set, assuming " + wl.FallbackName)
socketPath = path.Join(sc.RuntimePath, wl.FallbackName)
} else if !path.IsAbs(name) {
socketPath = path.Join(sc.RuntimePath, name)
} else {
socketPath = name
}
innerPath := path.Join(innerRuntimeDir, wl.FallbackName)
seal.container.SetEnv[wl.WaylandDisplay] = wl.FallbackName
if !config.Confinement.Sandbox.DirectWayland { // set up security-context-v1
socketDir := path.Join(sc.SharePath, "wayland")
outerPath := path.Join(socketDir, id)
seal.sys.Ensure(socketDir, 0711)
appID := config.ID
if appID == "" {
// use instance ID in case app id is not set
appID = "uk.gensokyo.fortify." + id
}
seal.sys.Wayland(&seal.bwrapSync, outerPath, socketPath, appID, id)
seal.container.Bind(outerPath, innerPath)
} else { // bind mount wayland socket (insecure)
fmsg.Verbose("direct wayland access, PROCEED WITH CAUTION")
seal.container.Bind(socketPath, innerPath)
seal.sys.UpdatePermType(system.EWayland, socketPath, acl.Read, acl.Write, acl.Execute)
}
}
// set up X11
if config.Confinement.Enablements.Has(system.EX11) {
// discover X11 and grant user permission via the `ChangeHosts` command
if d, ok := sys.LookupEnv(display); !ok {
return fmsg.WrapError(ErrXDisplay,
"DISPLAY is not set")
} else {
seal.sys.ChangeHosts("#" + seal.user.uid.String())
seal.container.SetEnv[display] = d
seal.container.Bind("/tmp/.X11-unix", "/tmp/.X11-unix")
}
}
/*
PulseAudio server and authentication
*/
if config.Confinement.Enablements.Has(system.EPulse) {
// PulseAudio runtime directory (usually `/run/user/%d/pulse`)
pulseRuntimeDir := path.Join(sc.RuntimePath, "pulse")
// PulseAudio socket (usually `/run/user/%d/pulse/native`)
pulseSocket := path.Join(pulseRuntimeDir, "native")
if _, err := sys.Stat(pulseRuntimeDir); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot access PulseAudio directory %q:", pulseRuntimeDir))
}
return fmsg.WrapError(ErrPulseSocket,
fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir))
}
if s, err := sys.Stat(pulseSocket); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return fmsg.WrapErrorSuffix(err,
fmt.Sprintf("cannot access PulseAudio socket %q:", pulseSocket))
}
return fmsg.WrapError(ErrPulseSocket,
fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pulseRuntimeDir))
} else {
if m := s.Mode(); m&0o006 != 0o006 {
return fmsg.WrapError(ErrPulseMode,
fmt.Sprintf("unexpected permissions on %q:", pulseSocket), m)
}
}
// hard link pulse socket into target-executable share
innerPulseRuntimeDir := path.Join(sharePathLocal, "pulse")
innerPulseSocket := path.Join(innerRuntimeDir, "pulse", "native")
seal.sys.Link(pulseSocket, innerPulseRuntimeDir)
seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket)
seal.container.SetEnv[pulseServer] = "unix:" + innerPulseSocket
// publish current user's pulse cookie for target user
if src, err := discoverPulseCookie(sys); err != nil {
// not fatal
fmsg.Verbose(strings.TrimSpace(err.(*fmsg.BaseError).Message()))
} else {
innerDst := fst.Tmp + "/pulse-cookie"
seal.container.SetEnv[pulseCookie] = innerDst
payload := new([]byte)
seal.container.CopyBindRef(innerDst, &payload)
seal.sys.CopyFile(payload, src, 256, 256)
}
}
/*
D-Bus proxy
*/
if config.Confinement.Enablements.Has(system.EDBus) {
// ensure dbus session bus defaults
if config.Confinement.SessionBus == nil {
config.Confinement.SessionBus = dbus.NewConfig(config.ID, true, true)
}
// downstream socket paths
sessionPath, systemPath := path.Join(sharePath, "bus"), path.Join(sharePath, "system_bus_socket")
// configure dbus proxy
if f, err := seal.sys.ProxyDBus(
config.Confinement.SessionBus, config.Confinement.SystemBus,
sessionPath, systemPath,
); err != nil {
return err
} else {
seal.dbusMsg = f
}
// share proxy sockets
sessionInner := path.Join(innerRuntimeDir, "bus")
seal.container.SetEnv[dbusSessionBusAddress] = "unix:path=" + sessionInner
seal.container.Bind(sessionPath, sessionInner)
seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
if config.Confinement.SystemBus != nil {
systemInner := "/run/dbus/system_bus_socket"
seal.container.SetEnv[dbusSystemBusAddress] = "unix:path=" + systemInner
seal.container.Bind(systemPath, systemInner)
seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write)
}
}
/*
Miscellaneous
*/
// queue overriding tmpfs at the end of seal.container.Filesystem
for _, dest := range config.Confinement.Sandbox.Override {
seal.container.Tmpfs(dest, 8*1024)
}
// append ExtraPerms last
for _, p := range config.Confinement.ExtraPerms {
if p == nil {
continue
}
if p.Ensure {
seal.sys.Ensure(p.Path, 0700)
}
perms := make(acl.Perms, 0, 3)
if p.Read {
perms = append(perms, acl.Read)
}
if p.Write {
perms = append(perms, acl.Write)
}
if p.Execute {
perms = append(perms, acl.Execute)
}
seal.sys.UpdatePermType(system.User, p.Path, perms...)
}
// mount fortify in sandbox for init
seal.container.Bind(sys.MustExecutable(), path.Join(fst.Tmp, "sbin/fortify"))
seal.container.Symlink("fortify", path.Join(fst.Tmp, "sbin/init"))
fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, command: %s", fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, command: %s",
seal.user.uid, seal.user.username, config.Confinement.Groups, config.Command) seal.user.uid, seal.user.username, config.Confinement.Groups, config.Command)
return nil return nil
} }
// discoverPulseCookie attempts various standard methods to discover the current user's PulseAudio authentication cookie
func discoverPulseCookie(sys sys.State) (string, error) {
if p, ok := sys.LookupEnv(pulseCookie); ok {
return p, nil
}
// dotfile $HOME/.pulse-cookie
if p, ok := sys.LookupEnv(home); ok {
p = path.Join(p, ".pulse-cookie")
if s, err := sys.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 := sys.LookupEnv(xdgConfigHome); ok {
p = path.Join(p, "pulse", "cookie")
if s, err := sys.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))
}

View File

@ -1,339 +0,0 @@
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/sys"
"git.gensokyo.uk/security/fortify/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 sys.State) 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.user.aid.String())
seal.sys.Ensure(targetTmpdir, 01700)
seal.sys.UpdatePermType(system.User, targetTmpdir, acl.Read, acl.Write, acl.Execute)
seal.container.Bind(targetTmpdir, "/tmp", false, true)
/*
XDG runtime directory
*/
// mount tmpfs on inner runtime (e.g. `/run/user/%d`)
seal.container.Tmpfs("/run/user", 1*1024*1024)
seal.container.Tmpfs(seal.innerRuntimeDir, 8*1024*1024)
// point to inner runtime path `/run/user/%d`
seal.container.SetEnv[xdgRuntimeDir] = seal.innerRuntimeDir
seal.container.SetEnv[xdgSessionClass] = "user"
seal.container.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.container.SetEnv[shell] = s
sh = s
}
// bind home directory
homeDir := "/var/empty"
if seal.user.home != "" {
homeDir = seal.user.home
}
username := "chronos"
if seal.user.username != "" {
username = seal.user.username
}
seal.container.Bind(seal.user.data, homeDir, false, true)
seal.container.Chdir = homeDir
seal.container.SetEnv["HOME"] = homeDir
seal.container.SetEnv["USER"] = username
// generate /etc/passwd and /etc/group
seal.container.CopyBind("/etc/passwd",
[]byte(username+":x:"+seal.mapuid.String()+":"+seal.mapuid.String()+":Fortify:"+homeDir+":"+sh+"\n"))
seal.container.CopyBind("/etc/group",
[]byte("fortify:x:"+seal.mapuid.String()+":\n"))
/*
Display servers
*/
// pass $TERM to launcher
if t, ok := os.LookupEnv(term); ok {
seal.container.SetEnv[term] = t
}
// set up wayland
if seal.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.innerRuntimeDir, wl.FallbackName)
seal.container.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.appID
if appID == "" {
// use instance ID in case app id is not set
appID = "uk.gensokyo.fortify." + seal.id
}
seal.sys.Wayland(&seal.bwrapSync, outerPath, socketPath, appID, seal.id)
seal.container.Bind(outerPath, innerPath)
} else { // bind mount wayland socket (insecure)
fmsg.Verbose("direct wayland access, PROCEED WITH CAUTION")
seal.container.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.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.user.uid.String())
seal.container.SetEnv[display] = d
seal.container.Bind("/tmp/.X11-unix", "/tmp/.X11-unix")
}
}
/*
PulseAudio server and authentication
*/
if seal.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.innerRuntimeDir, "pulse", "native")
seal.sys.Link(ps, psi)
seal.container.Bind(psi, p)
seal.container.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 {
innerDst := fst.Tmp + "/pulse-cookie"
seal.container.SetEnv[pulseCookie] = innerDst
payload := new([]byte)
seal.container.CopyBindRef(innerDst, &payload)
seal.sys.CopyFile(payload, src, 256, 256)
}
}
/*
D-Bus proxy
*/
if seal.Has(system.EDBus) {
// ensure dbus session bus defaults
if bus[0] == nil {
bus[0] = dbus.NewConfig(seal.appID, 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.innerRuntimeDir, "bus")
seal.container.SetEnv[dbusSessionBusAddress] = "unix:path=" + sessionInner
seal.container.Bind(sessionPath, sessionInner)
seal.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
if bus[1] != nil {
systemInner := "/run/dbus/system_bus_socket"
seal.container.SetEnv[dbusSystemBusAddress] = "unix:path=" + systemInner
seal.container.Bind(systemPath, systemInner)
seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write)
}
}
/*
Miscellaneous
*/
// queue overriding tmpfs at the end of seal.container.Filesystem
for _, dest := range seal.override {
seal.container.Tmpfs(dest, 8*1024)
}
// mount fortify in sandbox for init
seal.container.Bind(os.MustExecutable(), path.Join(fst.Tmp, "sbin/fortify"))
seal.container.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 sys.State) (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))
}