fortify/internal/app/seal.go
Ophestra Umiker eae3034260
All checks were successful
Tests / Go tests (push) Successful in 39s
Nix / NixOS tests (push) Successful in 3m26s
state: expose aids and use instance id as key
Fortify state store instances was specific to aids due to outdated design decisions carried over from the ego rewrite. That no longer makes sense in the current application, so the interface now enables a single store object to manage all transient state.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-12-19 21:36:17 +09:00

243 lines
6.6 KiB
Go

package app
import (
"errors"
"fmt"
"io/fs"
"path"
"regexp"
"strconv"
"git.ophivana.moe/security/fortify/dbus"
"git.ophivana.moe/security/fortify/fst"
"git.ophivana.moe/security/fortify/internal/fmsg"
"git.ophivana.moe/security/fortify/internal/linux"
"git.ophivana.moe/security/fortify/internal/state"
"git.ophivana.moe/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
// dbus proxy message buffer retriever
dbusMsg func(f func(msgbuf []string))
// 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
// wayland socket direct access
directWayland bool
// prevents sharing from happening twice
shared bool
// seal system-level component
sys *appSealSys
linux.Paths
// protected by upstream mutex
}
// 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)
// 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))
} else {
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) {
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
}
}
}
// map sandbox config to bwrap
if config.Confinement.Sandbox == nil {
fmsg.VPrintln("sandbox configuration not supplied, PROCEED WITH CAUTION")
// permissive defaults
conf := &fst.SandboxConfig{
UserNS: true,
Net: true,
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 "/run":
case "/tmp":
case "/mnt":
case "/etc":
default:
b = append(b, &fst.FilesystemConfig{Src: p, Write: true, Must: true})
}
}
conf.Filesystem = append(conf.Filesystem, b...)
}
// bind entries in /run
if d, err := a.os.ReadDir("/run"); err != nil {
return err
} else {
b := make([]*fst.FilesystemConfig, 0, len(d))
for _, ent := range d {
name := ent.Name()
switch name {
case "user":
case "dbus":
default:
p := "/run/" + name
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})
}
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.shareAll([2]*dbus.Config{config.Confinement.SessionBus, config.Confinement.SystemBus}, a.os); err != nil {
return err
}
// verbose log seal information
fmsg.VPrintf("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
a.ct = newAppCt(config)
return nil
}