diff --git a/fst/app.go b/fst/app.go index 31ba484..95d00dd 100644 --- a/fst/app.go +++ b/fst/app.go @@ -24,10 +24,10 @@ type RunState struct { // Paths contains environment-dependent paths used by fortify. 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"` - // XDG_RUNTIME_DIR value e.g. /run/user/%d + // XDG_RUNTIME_DIR value (usually `/run/user/%d`) 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"` } diff --git a/fst/config.go b/fst/config.go index 8910779..d96934d 100644 --- a/fst/config.go +++ b/fst/config.go @@ -10,9 +10,11 @@ const Tmp = "/.fortify" // Config is used to seal an app 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"` - // value passed through to the child process as its argv + // final argv, passed to init Command []string `json:"command"` Confinement ConfinementConfig `json:"confinement"` @@ -32,7 +34,7 @@ type ConfinementConfig struct { Outer string `json:"home"` // bwrap sandbox confinement configuration Sandbox *SandboxConfig `json:"sandbox"` - // extra acl entries to append + // extra acl ops, runs after everything else ExtraPerms []*ExtraPermConfig `json:"extra_perms,omitempty"` // reference to a system D-Bus proxy configuration, diff --git a/fst/sandbox.go b/fst/sandbox.go index 27df5f9..aaa018b 100644 --- a/fst/sandbox.go +++ b/fst/sandbox.go @@ -26,7 +26,8 @@ type SandboxConfig struct { NoNewSession bool `json:"no_new_session,omitempty"` // map target user uid to privileged user uid in the user namespace 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"` // final environment variables @@ -39,7 +40,8 @@ type SandboxConfig struct { Etc string `json:"etc,omitempty"` // automatically set up /etc symlinks 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"` } @@ -56,7 +58,7 @@ type SandboxSys interface { // 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. -func (s *SandboxConfig) Bwrap(sys SandboxSys) (*bwrap.Config, error) { +func (s *SandboxConfig) Bwrap(sys SandboxSys, uid *int) (*bwrap.Config, error) { if s == nil { 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") } - var uid int if !s.MapRealUID { - uid = 65534 + // mapped uid defaults to 65534 to work around file ownership checks due to a bwrap limitation + *uid = 65534 } 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{ Net: s.Net, UserNS: s.UserNS, + UID: uid, + GID: uid, Hostname: s.Hostname, Clearenv: true, SetEnv: s.Env, @@ -93,7 +99,6 @@ func (s *SandboxConfig) Bwrap(sys SandboxSys) (*bwrap.Config, error) { // for saving such a miniscule amount of memory Chmod: make(bwrap.ChmodConfig), }). - SetUID(uid).SetGID(uid). Procfs("/proc"). Tmpfs(Tmp, 4*1024) diff --git a/internal/app/app_nixos_test.go b/internal/app/app_nixos_test.go index 02d9aa2..7ebd32e 100644 --- a/internal/app/app_nixos_test.go +++ b/internal/app/app_nixos_test.go @@ -18,13 +18,13 @@ var testCasesNixos = []sealTestCase{ AppID: 1, Groups: []string{}, Username: "u0_a1", Outer: "/var/lib/persist/module/fortify/0/1", 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{ {Src: "/bin", Must: true}, {Src: "/usr/bin", 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: "/run/opengl-driver", Must: true}, {Src: "/dev/dri", Device: true}, - }, AutoEtc: true, + }, Override: []string{"/var/run/nscd"}, }, SystemBus: &dbus.Config{ @@ -56,12 +56,12 @@ var testCasesNixos = []sealTestCase{ }, system.New(1000001). 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", 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). + 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). Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse"). 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/zshenv", "/etc/zshenv"). Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc"). - Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", false, true). Tmpfs("/run/user", 1048576). 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). 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")). diff --git a/internal/app/app_pd_test.go b/internal/app/app_pd_test.go index fb74ae1..2a03d35 100644 --- a/internal/app/app_pd_test.go +++ b/internal/app/app_pd_test.go @@ -29,12 +29,12 @@ var testCasesPd = []sealTestCase{ }, system.New(1000000). 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", 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{ Net: true, UserNS: true, @@ -150,9 +150,9 @@ var testCasesPd = []sealTestCase{ Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile"). Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv"). Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc"). - Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", false, true). Tmpfs("/run/user", 1048576). Tmpfs("/run/user/65534", 8388608). + Bind("/tmp/fortify.1971/tmpdir/0", "/tmp", 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/group", []byte("fortify:x:65534:\n")). @@ -212,12 +212,12 @@ var testCasesPd = []sealTestCase{ }, system.New(1000009). 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", 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). + 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). 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"). @@ -376,9 +376,9 @@ var testCasesPd = []sealTestCase{ Symlink(fst.Tmp+"/etc/zprofile", "/etc/zprofile"). Symlink(fst.Tmp+"/etc/zshenv", "/etc/zshenv"). Symlink(fst.Tmp+"/etc/zshrc", "/etc/zshrc"). - Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", false, true). Tmpfs("/run/user", 1048576). Tmpfs("/run/user/65534", 8388608). + Bind("/tmp/fortify.1971/tmpdir/9", "/tmp", 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/group", []byte("fortify:x:65534:\n")). diff --git a/internal/app/seal.go b/internal/app/seal.go index 3487963..23f1c0c 100644 --- a/internal/app/seal.go +++ b/internal/app/seal.go @@ -10,6 +10,7 @@ import ( "os" "path" "regexp" + "strings" "git.gensokyo.uk/security/fortify/acl" "git.gensokyo.uk/security/fortify/dbus" @@ -20,6 +21,26 @@ import ( "git.gensokyo.uk/security/fortify/internal/state" "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 ( @@ -27,65 +48,38 @@ var ( ErrUser = errors.New("invalid aid") ErrHome = errors.New("invalid home directory") 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}\\$)$") // appSeal stores copies of various parts of [fst.Config] type appSeal struct { - // string representation of [fst.ID] - 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 + // passed through from [fst.Config] command []string // state instance initialised during seal; used during process lifecycle events 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 needRevert bool // whether state was inserted into [state.Store]; used during process lifecycle events 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 - // inner XDG_RUNTIME_DIR, default formatting via user - innerRuntimeDir string - // mapped uid and gid in user namespace - mapuid *stringPair[int] - + user appUser sys *system.I container *bwrap.Config bwrapSync *os.File - // prevents sharing from happening twice - shared bool - - system.Enablements - fst.Paths - // protected by upstream mutex } @@ -107,45 +101,30 @@ type appUser struct { 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 { - // encode initial configuration for state tracking - ct := new(bytes.Buffer) - if err := gob.NewEncoder(ct).Encode(config); err != nil { - return fmsg.WrapErrorSuffix(err, - "cannot encode initial config:") + { + // encode initial configuration for state tracking + ct := new(bytes.Buffer) + if err := gob.NewEncoder(ct).Encode(config); err != nil { + return fmsg.WrapErrorSuffix(err, + "cannot encode initial config:") + } + seal.ct = ct } - seal.ct = ct - seal.Paths = sys.Paths() - - // pass through config values - seal.id = id - seal.appID = config.ID + // pass through command slice; this value is never touched in the main process seal.command = config.Command - { - // 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 + // allowed aid range 0 to 9999, this is checked again in fsu if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 { return fmsg.WrapError(ErrUser, fmt.Sprintf("aid %d out of range", config.Confinement.AppID)) } + + /* + Resolve post-fsu user state + */ + seal.user = appUser{ aid: newInt(config.Confinement.AppID), 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 == "" { seal.user.home = seal.user.data } - - // invoke fsu for full uid if u, err := sys.Uid(seal.user.aid.unwrap()); err != nil { return err } else { seal.user.uid = newInt(u) } - - // resolve supplementary group ids from names seal.user.supp = make([]string, len(config.Confinement.Groups)) for i, name := range config.Confinement.Groups { 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)) - for i, p := range config.Confinement.ExtraPerms { - if p == nil { - continue - } + /* + Resolve initial container state + */ - seal.extraPerms[i] = new(sealedExtraPerm) - 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 + // permissive defaults if config.Confinement.Sandbox == nil { fmsg.Verbose("sandbox configuration not supplied, PROCEED WITH CAUTION") - // permissive defaults conf := &fst.SandboxConfig{ UserNS: true, Net: true, @@ -254,40 +210,326 @@ func (seal *appSeal) finalise(sys sys.State, config *fst.Config, id string) erro config.Confinement.Sandbox = conf } - seal.directWayland = config.Confinement.Sandbox.DirectWayland - if b, err := config.Confinement.Sandbox.Bwrap(sys); err != nil { - return err - } else { - seal.container = b - } - seal.override = config.Confinement.Sandbox.Override - if seal.container.SetEnv == nil { - seal.container.SetEnv = make(map[string]string) + + var mapuid *stringPair[int] + { + var uid int + var err error + seal.container, err = config.Confinement.Sandbox.Bwrap(sys, &uid) + if err != nil { + return err + } + mapuid = newInt(uid) + if seal.container.SetEnv == nil { + seal.container.SetEnv = make(map[string]string) + } } - // open process state store - // the simple store only starts holding an open file after first action - // store activity begins after Start is called and must end before Wait - seal.store = state.NewMulti(seal.RunDirPath) + /* + Initialise externals + */ - // 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.IsVerbose = fmsg.Load seal.sys.Verbose = fmsg.Verbose seal.sys.Verbosef = fmsg.Verbosef seal.sys.WrapErr = fmsg.WrapError - // pass through enablements - seal.Enablements = config.Confinement.Enablements + /* + Work directories + */ - // this method calls all share methods in sequence - if err := seal.setupShares([2]*dbus.Config{config.Confinement.SessionBus, config.Confinement.SystemBus}, sys); err != nil { - return err + // base fortify share path + seal.sys.Ensure(sc.SharePath, 0711) + + // 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", seal.user.uid, seal.user.username, config.Confinement.Groups, config.Command) 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)) +} diff --git a/internal/app/share.go b/internal/app/share.go deleted file mode 100644 index 2094b48..0000000 --- a/internal/app/share.go +++ /dev/null @@ -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)) -}