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>
274 lines
7.5 KiB
Go
274 lines
7.5 KiB
Go
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/gob"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"path"
|
|
"regexp"
|
|
"strconv"
|
|
|
|
"git.gensokyo.uk/security/fortify/acl"
|
|
"git.gensokyo.uk/security/fortify/dbus"
|
|
"git.gensokyo.uk/security/fortify/fst"
|
|
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
|
"git.gensokyo.uk/security/fortify/internal"
|
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
|
"git.gensokyo.uk/security/fortify/internal/linux"
|
|
"git.gensokyo.uk/security/fortify/internal/state"
|
|
"git.gensokyo.uk/security/fortify/internal/system"
|
|
)
|
|
|
|
var (
|
|
ErrConfig = errors.New("no configuration to seal")
|
|
ErrUser = errors.New("invalid aid")
|
|
ErrHome = errors.New("invalid home directory")
|
|
ErrName = errors.New("invalid username")
|
|
)
|
|
|
|
var posixUsername = regexp.MustCompilePOSIX("^[a-z_]([A-Za-z0-9_-]{0,31}|[A-Za-z0-9_-]{0,30}\\$)$")
|
|
|
|
// appSeal seals the application with child-related information
|
|
type appSeal struct {
|
|
// app unique ID string representation
|
|
id string
|
|
// dump dbus proxy message buffer
|
|
dbusMsg func()
|
|
|
|
// freedesktop application ID
|
|
fid string
|
|
// argv to start process with in the final confined environment
|
|
command []string
|
|
// persistent process state store
|
|
store state.Store
|
|
|
|
// process-specific share directory path
|
|
share string
|
|
// process-specific share directory path local to XDG_RUNTIME_DIR
|
|
shareLocal string
|
|
|
|
// pass-through enablement tracking from config
|
|
et system.Enablements
|
|
// initial config gob encoding buffer
|
|
ct io.WriterTo
|
|
// wayland socket direct access
|
|
directWayland bool
|
|
// extra UpdatePerm ops
|
|
extraPerms []*sealedExtraPerm
|
|
|
|
// prevents sharing from happening twice
|
|
shared bool
|
|
// seal system-level component
|
|
sys *appSealSys
|
|
|
|
linux.Paths
|
|
|
|
// protected by upstream mutex
|
|
}
|
|
|
|
type sealedExtraPerm struct {
|
|
name string
|
|
perms acl.Perms
|
|
ensure bool
|
|
}
|
|
|
|
// Seal seals the app launch context
|
|
func (a *app) Seal(config *fst.Config) error {
|
|
a.lock.Lock()
|
|
defer a.lock.Unlock()
|
|
|
|
if a.seal != nil {
|
|
panic("app sealed twice")
|
|
}
|
|
|
|
if config == nil {
|
|
return fmsg.WrapError(ErrConfig,
|
|
"attempted to seal app with nil config")
|
|
}
|
|
|
|
// create seal
|
|
seal := new(appSeal)
|
|
|
|
// 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
|
|
|
|
// fetch system constants
|
|
seal.Paths = a.os.Paths()
|
|
|
|
// pass through config values
|
|
seal.id = a.id.String()
|
|
seal.fid = config.ID
|
|
seal.command = config.Command
|
|
|
|
// create seal system component
|
|
seal.sys = new(appSealSys)
|
|
|
|
// mapped uid
|
|
if config.Confinement.Sandbox != nil && config.Confinement.Sandbox.MapRealUID {
|
|
seal.sys.mappedID = a.os.Geteuid()
|
|
} else {
|
|
seal.sys.mappedID = 65534
|
|
}
|
|
seal.sys.mappedIDString = strconv.Itoa(seal.sys.mappedID)
|
|
seal.sys.runtime = path.Join("/run/user", seal.sys.mappedIDString)
|
|
|
|
// validate uid and set user info
|
|
if config.Confinement.AppID < 0 || config.Confinement.AppID > 9999 {
|
|
return fmsg.WrapError(ErrUser,
|
|
fmt.Sprintf("aid %d out of range", config.Confinement.AppID))
|
|
}
|
|
seal.sys.user = appUser{
|
|
aid: config.Confinement.AppID,
|
|
as: strconv.Itoa(config.Confinement.AppID),
|
|
data: config.Confinement.Outer,
|
|
home: config.Confinement.Inner,
|
|
username: config.Confinement.Username,
|
|
}
|
|
if seal.sys.user.username == "" {
|
|
seal.sys.user.username = "chronos"
|
|
} else if !posixUsername.MatchString(seal.sys.user.username) ||
|
|
len(seal.sys.user.username) >= internal.Sysconf_SC_LOGIN_NAME_MAX() {
|
|
return fmsg.WrapError(ErrName,
|
|
fmt.Sprintf("invalid user name %q", seal.sys.user.username))
|
|
}
|
|
if seal.sys.user.data == "" || !path.IsAbs(seal.sys.user.data) {
|
|
return fmsg.WrapError(ErrHome,
|
|
fmt.Sprintf("invalid home directory %q", seal.sys.user.data))
|
|
}
|
|
if seal.sys.user.home == "" {
|
|
seal.sys.user.home = seal.sys.user.data
|
|
}
|
|
|
|
// invoke fsu for full uid
|
|
if u, err := a.os.Uid(seal.sys.user.aid); err != nil {
|
|
return fmsg.WrapErrorSuffix(err,
|
|
"cannot obtain uid from fsu:")
|
|
} else {
|
|
seal.sys.user.uid = u
|
|
seal.sys.user.us = strconv.Itoa(u)
|
|
}
|
|
|
|
// resolve supplementary group ids from names
|
|
seal.sys.user.supp = make([]string, len(config.Confinement.Groups))
|
|
for i, name := range config.Confinement.Groups {
|
|
if g, err := a.os.LookupGroup(name); err != nil {
|
|
return fmsg.WrapError(err,
|
|
fmt.Sprintf("unknown group %q", name))
|
|
} else {
|
|
seal.sys.user.supp[i] = g.Gid
|
|
}
|
|
}
|
|
|
|
// build extra perms
|
|
seal.extraPerms = make([]*sealedExtraPerm, len(config.Confinement.ExtraPerms))
|
|
for i, p := range config.Confinement.ExtraPerms {
|
|
if p == nil {
|
|
continue
|
|
}
|
|
|
|
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
|
|
if config.Confinement.Sandbox == nil {
|
|
fmsg.Verbose("sandbox configuration not supplied, PROCEED WITH CAUTION")
|
|
|
|
// permissive defaults
|
|
conf := &fst.SandboxConfig{
|
|
UserNS: true,
|
|
Net: true,
|
|
Syscall: new(bwrap.SyscallPolicy),
|
|
NoNewSession: true,
|
|
AutoEtc: true,
|
|
}
|
|
// bind entries in /
|
|
if d, err := a.os.ReadDir("/"); err != nil {
|
|
return err
|
|
} else {
|
|
b := make([]*fst.FilesystemConfig, 0, len(d))
|
|
for _, ent := range d {
|
|
p := "/" + ent.Name()
|
|
switch p {
|
|
case "/proc":
|
|
case "/dev":
|
|
case "/tmp":
|
|
case "/mnt":
|
|
case "/etc":
|
|
|
|
default:
|
|
b = append(b, &fst.FilesystemConfig{Src: p, Write: true, Must: true})
|
|
}
|
|
}
|
|
conf.Filesystem = append(conf.Filesystem, b...)
|
|
}
|
|
|
|
// hide nscd from sandbox if present
|
|
nscd := "/var/run/nscd"
|
|
if _, err := a.os.Stat(nscd); !errors.Is(err, fs.ErrNotExist) {
|
|
conf.Override = append(conf.Override, nscd)
|
|
}
|
|
// bind GPU stuff
|
|
if config.Confinement.Enablements.Has(system.EX11) || config.Confinement.Enablements.Has(system.EWayland) {
|
|
conf.Filesystem = append(conf.Filesystem, &fst.FilesystemConfig{Src: "/dev/dri", Device: true})
|
|
}
|
|
// opportunistically bind kvm
|
|
conf.Filesystem = append(conf.Filesystem, &fst.FilesystemConfig{Src: "/dev/kvm", Device: true})
|
|
|
|
config.Confinement.Sandbox = conf
|
|
}
|
|
seal.directWayland = config.Confinement.Sandbox.DirectWayland
|
|
if b, err := config.Confinement.Sandbox.Bwrap(a.os); err != nil {
|
|
return err
|
|
} else {
|
|
seal.sys.bwrap = b
|
|
}
|
|
seal.sys.override = config.Confinement.Sandbox.Override
|
|
if seal.sys.bwrap.SetEnv == nil {
|
|
seal.sys.bwrap.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 system interface with full uid
|
|
seal.sys.I = system.New(seal.sys.user.uid)
|
|
|
|
// pass through enablements
|
|
seal.et = config.Confinement.Enablements
|
|
|
|
// this method calls all share methods in sequence
|
|
if err := seal.setupShares([2]*dbus.Config{config.Confinement.SessionBus, config.Confinement.SystemBus}, a.os); err != nil {
|
|
return err
|
|
}
|
|
|
|
// verbose log seal information
|
|
fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, command: %s",
|
|
seal.sys.user.us, seal.sys.user.username, config.Confinement.Groups, config.Command)
|
|
|
|
// seal app and release lock
|
|
a.seal = seal
|
|
return nil
|
|
}
|