package app import ( "bytes" "encoding/gob" "errors" "fmt" "io" "io/fs" "path" "regexp" "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/state" "git.gensokyo.uk/security/fortify/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 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 command []string // state instance initialised during seal and used on process lifecycle events store state.Store // 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 // extra [acl.Update] ops, appended at the end of [system.I] extraPerms []*sealedExtraPerm // prevents sharing from happening twice shared bool // seal system-level component sys *appSealSys system.Enablements fst.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.appID = config.ID seal.command = config.Command // create seal system component seal.sys = new(appSealSys) { // 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 = a.os.Geteuid() } seal.sys.mapuid = newInt(mapuid) seal.sys.runtime = path.Join("/run/user", seal.sys.mapuid.String()) } // 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: newInt(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.unwrap()); err != nil { return err } else { seal.sys.user.uid = newInt(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 os uid seal.sys.I = system.New(seal.sys.user.uid.unwrap()) seal.sys.I.IsVerbose = fmsg.Load seal.sys.I.Verbose = fmsg.Verbose seal.sys.I.Verbosef = fmsg.Verbosef seal.sys.I.WrapErr = fmsg.WrapError // pass through enablements seal.Enablements = 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.uid, seal.sys.user.username, config.Confinement.Groups, config.Command) // seal app and release lock a.seal = seal return nil }