diff options
| author | Ophestra <cat@gensokyo.uk> | 2025-03-25 01:52:49 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2025-03-25 01:52:49 +0900 |
| commit | 5c4058d5ac7e7944973ca8216c258fc50c194e22 (patch) | |
| tree | c06597903233c75eca5a1edae8b06d93f0f81774 /internal/app/seal.go | |
| parent | e732dca7629edb503df0c65df5f37a99600d567d (diff) | |
app: run in native sandbox
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/app/seal.go')
| -rw-r--r-- | internal/app/seal.go | 181 |
1 files changed, 102 insertions, 79 deletions
diff --git a/internal/app/seal.go b/internal/app/seal.go index 30702124..a25897dc 100644 --- a/internal/app/seal.go +++ b/internal/app/seal.go @@ -2,24 +2,28 @@ package app import ( "bytes" + "context" "encoding/gob" "errors" "fmt" "io" "io/fs" + "maps" "os" "path" "regexp" + "slices" "strings" "sync/atomic" + "syscall" "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/sys" + "git.gensokyo.uk/security/fortify/sandbox" "git.gensokyo.uk/security/fortify/system" "git.gensokyo.uk/security/fortify/wl" ) @@ -65,19 +69,19 @@ type outcome struct { // copied from [sys.State] response runDirPath string - // passed through from [fst.Config] - command []string - // initial [fst.Config] gob stream for state data; - // this is prepared ahead of time as config is mutated during seal creation + // this is prepared ahead of time as config is clobbered during seal creation ct io.WriterTo // dump dbus proxy message buffer dbusMsg func() - user fsuUser - sys *system.I - container *bwrap.Config - bwrapSync *os.File + user fsuUser + sys *system.I + ctx context.Context + + container *sandbox.Params + env map[string]string + sync *os.File f atomic.Bool } @@ -100,7 +104,17 @@ type fsuUser struct { username string } -func (seal *outcome) finalise(sys sys.State, config *fst.Config) error { +func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Config) error { + if seal.ctx != nil { + panic("finalise called twice") + } + seal.ctx = ctx + + shellPath := "/bin/sh" + if s, ok := sys.LookupEnv(shell); ok && path.IsAbs(s) { + shellPath = s + } + { // encode initial configuration for state tracking ct := new(bytes.Buffer) @@ -111,9 +125,6 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error { seal.ct = ct } - // pass through command slice; this value is never touched in the main process - seal.command = config.Command - // 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, @@ -167,12 +178,24 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error { if config.Confinement.Sandbox == nil { fmsg.Verbose("sandbox configuration not supplied, PROCEED WITH CAUTION") + // fsu clears the environment so resolve paths early + if !path.IsAbs(config.Path) { + if len(config.Args) > 0 { + if p, err := sys.LookPath(config.Args[0]); err != nil { + return fmsg.WrapError(err, err.Error()) + } else { + config.Path = p + } + } else { + config.Path = shellPath + } + } + conf := &fst.SandboxConfig{ - UserNS: true, - Net: true, - Syscall: new(bwrap.SyscallPolicy), - NoNewSession: true, - AutoEtc: true, + Userns: true, + Net: true, + Tty: true, + AutoEtc: true, } // bind entries in / if d, err := sys.ReadDir("/"); err != nil { @@ -198,7 +221,7 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error { // hide nscd from sandbox if present nscd := "/var/run/nscd" if _, err := sys.Stat(nscd); !errors.Is(err, fs.ErrNotExist) { - conf.Override = append(conf.Override, nscd) + conf.Cover = append(conf.Cover, nscd) } // bind GPU stuff if config.Confinement.Enablements.Has(system.EX11) || config.Confinement.Enablements.Has(system.EWayland) { @@ -210,17 +233,29 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error { config.Confinement.Sandbox = conf } - var mapuid *stringPair[int] + var mapuid, mapgid *stringPair[int] { - var uid int + var uid, gid int var err error - seal.container, err = config.Confinement.Sandbox.Bwrap(sys, &uid) + seal.container, seal.env, err = config.Confinement.Sandbox.ToContainer(sys, &uid, &gid) if err != nil { - return err + return fmsg.WrapErrorSuffix(err, + "cannot initialise container configuration:") + } + if !path.IsAbs(config.Path) { + return fmsg.WrapError(syscall.EINVAL, + "invalid program path") } + if len(config.Args) == 0 { + config.Args = []string{config.Path} + } + seal.container.Path = config.Path + seal.container.Args = config.Args + mapuid = newInt(uid) - if seal.container.SetEnv == nil { - seal.container.SetEnv = make(map[string]string) + mapgid = newInt(gid) + if seal.env == nil { + seal.env = make(map[string]string) } } @@ -255,35 +290,27 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error { // 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" + seal.container.Tmpfs("/run/user", 1<<12, 0755) + seal.container.Tmpfs(innerRuntimeDir, 1<<23, 0755) + seal.env[xdgRuntimeDir] = innerRuntimeDir + seal.env[xdgSessionClass] = "user" + seal.env[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) + tmpdirInst := path.Join(tmpdir, seal.user.aid.String()) + seal.sys.Ensure(tmpdirInst, 01700) + seal.sys.UpdatePermType(system.User, tmpdirInst, acl.Read, acl.Write, acl.Execute) + seal.container.Bind(tmpdirInst, "/tmp", sandbox.BindWritable) } /* 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 @@ -292,27 +319,25 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error { 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 + seal.container.Bind(seal.user.data, homeDir, sandbox.BindWritable) + seal.container.Dir = homeDir + seal.env["HOME"] = homeDir + seal.env["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")) + seal.container.Place("/etc/passwd", + []byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Fortify:"+homeDir+":"+shellPath+"\n")) + seal.container.Place("/etc/group", + []byte("fortify:x:"+mapgid.String()+":\n")) /* Display servers */ - // pass $TERM to launcher + // pass $TERM for proper terminal I/O in shell if t, ok := sys.LookupEnv(term); ok { - seal.container.SetEnv[term] = t + seal.env[term] = t } - // set up wayland if config.Confinement.Enablements.Has(system.EWayland) { // outer wayland socket (usually `/run/user/%d/wayland-%d`) var socketPath string @@ -326,7 +351,7 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error { } innerPath := path.Join(innerRuntimeDir, wl.FallbackName) - seal.container.SetEnv[wl.WaylandDisplay] = wl.FallbackName + seal.env[wl.WaylandDisplay] = wl.FallbackName if !config.Confinement.Sandbox.DirectWayland { // set up security-context-v1 socketDir := path.Join(sc.SharePath, "wayland") @@ -337,25 +362,23 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error { // use instance ID in case app id is not set appID = "uk.gensokyo.fortify." + seal.id.String() } - seal.sys.Wayland(&seal.bwrapSync, outerPath, socketPath, appID, seal.id.String()) - seal.container.Bind(outerPath, innerPath) + seal.sys.Wayland(&seal.sync, outerPath, socketPath, appID, seal.id.String()) + seal.container.Bind(outerPath, innerPath, 0) } else { // bind mount wayland socket (insecure) fmsg.Verbose("direct wayland access, PROCEED WITH CAUTION") - seal.container.Bind(socketPath, innerPath) + seal.container.Bind(socketPath, innerPath, 0) 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") + seal.env[display] = d + seal.container.Bind("/tmp/.X11-unix", "/tmp/.X11-unix", 0) } } @@ -396,8 +419,8 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error { 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 + seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0) + seal.env[pulseServer] = "unix:" + innerPulseSocket // publish current user's pulse cookie for target user if src, err := discoverPulseCookie(sys); err != nil { @@ -405,9 +428,9 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error { 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.env[pulseCookie] = innerDst + var payload *[]byte + seal.container.PlaceP(innerDst, &payload) seal.sys.CopyFile(payload, src, 256, 256) } } @@ -437,13 +460,13 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error { // share proxy sockets sessionInner := path.Join(innerRuntimeDir, "bus") - seal.container.SetEnv[dbusSessionBusAddress] = "unix:path=" + sessionInner - seal.container.Bind(sessionPath, sessionInner) + seal.env[dbusSessionBusAddress] = "unix:path=" + sessionInner + seal.container.Bind(sessionPath, sessionInner, 0) 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.env[dbusSystemBusAddress] = "unix:path=" + systemInner + seal.container.Bind(systemPath, systemInner, 0) seal.sys.UpdatePerm(systemPath, acl.Read, acl.Write) } } @@ -452,9 +475,8 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error { Miscellaneous */ - // queue overriding tmpfs at the end of seal.container.Filesystem - for _, dest := range config.Confinement.Sandbox.Override { - seal.container.Tmpfs(dest, 8*1024) + for _, dest := range config.Confinement.Sandbox.Cover { + seal.container.Tmpfs(dest, 1<<13, 0755) } // append ExtraPerms last @@ -480,12 +502,13 @@ func (seal *outcome) finalise(sys sys.State, config *fst.Config) error { 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/init0")) + // flatten and sort env for deterministic behaviour + seal.container.Env = make([]string, 0, len(seal.env)) + maps.All(seal.env)(func(k string, v string) bool { seal.container.Env = append(seal.container.Env, k+"="+v); return true }) + slices.Sort(seal.container.Env) - fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, command: %s", - seal.user.uid, seal.user.username, config.Confinement.Groups, config.Command) + fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s", + seal.user.uid, seal.user.username, config.Confinement.Groups, seal.container.Args) return nil } |
