diff options
| author | Ophestra <cat@gensokyo.uk> | 2025-08-28 01:07:51 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2025-08-28 01:07:51 +0900 |
| commit | b14690aa77b3d5f86c4d1b1965675d6d82789e32 (patch) | |
| tree | 5765f705fd69dae7a65a0666451ac71a729a6aa8 /internal/app/seal_linux.go | |
| parent | d0b6852cd7db2bb1214c3267719d3ac108c0a53e (diff) | |
internal/app: remove seal interface
This further cleans up the package for the restructure.
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/app/seal_linux.go')
| -rw-r--r-- | internal/app/seal_linux.go | 590 |
1 files changed, 0 insertions, 590 deletions
diff --git a/internal/app/seal_linux.go b/internal/app/seal_linux.go deleted file mode 100644 index be18a2ae..00000000 --- a/internal/app/seal_linux.go +++ /dev/null @@ -1,590 +0,0 @@ -package app - -import ( - "bytes" - "context" - "encoding/gob" - "errors" - "fmt" - "io" - "io/fs" - "os" - "path" - "slices" - "strconv" - "strings" - "sync/atomic" - "syscall" - "time" - - "hakurei.app/container" - "hakurei.app/hst" - "hakurei.app/internal/app/state" - "hakurei.app/internal/hlog" - "hakurei.app/internal/sys" - "hakurei.app/system" - "hakurei.app/system/acl" - "hakurei.app/system/dbus" - "hakurei.app/system/wayland" -) - -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 ( - ErrIdent = errors.New("invalid identity") - 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") -) - -// outcome stores copies of various parts of [hst.Config] -type outcome struct { - // copied from initialising [app] - id *stringPair[state.ID] - // copied from [sys.State] - runDirPath *container.Absolute - - // initial [hst.Config] gob stream for state data; - // this is prepared ahead of time as config is clobbered during seal creation - ct io.WriterTo - // dump dbus proxy message buffer - dbusMsg func() - - user hsuUser - sys *system.I - ctx context.Context - - waitDelay time.Duration - container *container.Params - env map[string]string - sync *os.File - - f atomic.Bool -} - -// shareHost holds optional share directory state that must not be accessed directly -type shareHost struct { - // whether XDG_RUNTIME_DIR is used post hsu - useRuntimeDir bool - // process-specific directory in tmpdir, empty if unused - sharePath *container.Absolute - // process-specific directory in XDG_RUNTIME_DIR, empty if unused - runtimeSharePath *container.Absolute - - seal *outcome - sc hst.Paths -} - -// ensureRuntimeDir must be called if direct access to paths within XDG_RUNTIME_DIR is required -func (share *shareHost) ensureRuntimeDir() { - if share.useRuntimeDir { - return - } - share.useRuntimeDir = true - share.seal.sys.Ensure(share.sc.RunDirPath.String(), 0700) - share.seal.sys.UpdatePermType(system.User, share.sc.RunDirPath.String(), acl.Execute) - share.seal.sys.Ensure(share.sc.RuntimePath.String(), 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset - share.seal.sys.UpdatePermType(system.User, share.sc.RuntimePath.String(), acl.Execute) -} - -// instance returns a process-specific share path within tmpdir -func (share *shareHost) instance() *container.Absolute { - if share.sharePath != nil { - return share.sharePath - } - share.sharePath = share.sc.SharePath.Append(share.seal.id.String()) - share.seal.sys.Ephemeral(system.Process, share.sharePath.String(), 0711) - return share.sharePath -} - -// runtime returns a process-specific share path within XDG_RUNTIME_DIR -func (share *shareHost) runtime() *container.Absolute { - if share.runtimeSharePath != nil { - return share.runtimeSharePath - } - share.ensureRuntimeDir() - share.runtimeSharePath = share.sc.RunDirPath.Append(share.seal.id.String()) - share.seal.sys.Ephemeral(system.Process, share.runtimeSharePath.String(), 0700) - share.seal.sys.UpdatePerm(share.runtimeSharePath.String(), acl.Execute) - return share.runtimeSharePath -} - -// hsuUser stores post-hsu credentials and metadata -type hsuUser struct { - identity *stringPair[int] - // target uid resolved by hid:aid - uid *stringPair[int] - - // supplementary group ids - supp []string - - // app user home directory - home *container.Absolute - // passwd database username - username string -} - -func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Config) error { - if seal.ctx != nil { - panic("finalise called twice") - } - seal.ctx = ctx - - if config == nil { - return hlog.WrapErr(syscall.EINVAL, syscall.EINVAL.Error()) - } - if config.Home == nil { - return hlog.WrapErr(os.ErrInvalid, "invalid path to home directory") - } - - { - // encode initial configuration for state tracking - ct := new(bytes.Buffer) - if err := gob.NewEncoder(ct).Encode(config); err != nil { - return hlog.WrapErrSuffix(err, - "cannot encode initial config:") - } - seal.ct = ct - } - - // allowed identity range 0 to 9999, this is checked again in hsu - if config.Identity < 0 || config.Identity > 9999 { - return hlog.WrapErr(ErrIdent, - fmt.Sprintf("identity %d out of range", config.Identity)) - } - - seal.user = hsuUser{ - identity: newInt(config.Identity), - home: config.Home, - username: config.Username, - } - if seal.user.username == "" { - seal.user.username = "chronos" - } else if !isValidUsername(seal.user.username) { - return hlog.WrapErr(ErrName, - fmt.Sprintf("invalid user name %q", seal.user.username)) - } - if u, err := sys.Uid(seal.user.identity.unwrap()); err != nil { - return err - } else { - seal.user.uid = newInt(u) - } - seal.user.supp = make([]string, len(config.Groups)) - for i, name := range config.Groups { - if g, err := sys.LookupGroup(name); err != nil { - return hlog.WrapErr(err, - fmt.Sprintf("unknown group %q", name)) - } else { - seal.user.supp[i] = g.Gid - } - } - - // permissive defaults - if config.Container == nil { - hlog.Verbose("container configuration not supplied, PROCEED WITH CAUTION") - - if config.Shell == nil { - config.Shell = container.AbsFHSRoot.Append("bin", "sh") - s, _ := sys.LookupEnv(shell) - if a, err := container.NewAbs(s); err == nil { - config.Shell = a - } - } - - // hsu clears the environment so resolve paths early - if config.Path == nil { - if len(config.Args) > 0 { - if p, err := sys.LookPath(config.Args[0]); err != nil { - return hlog.WrapErr(err, err.Error()) - } else if config.Path, err = container.NewAbs(p); err != nil { - return hlog.WrapErr(err, err.Error()) - } - } else { - config.Path = config.Shell - } - } - - conf := &hst.ContainerConfig{ - Userns: true, - HostNet: true, - HostAbstract: true, - Tty: true, - - Filesystem: []hst.FilesystemConfigJSON{ - // autoroot, includes the home directory - {&hst.FSBind{ - Target: container.AbsFHSRoot, - Source: container.AbsFHSRoot, - Write: true, - Special: true, - }}, - }, - } - - // bind GPU stuff - if config.Enablements.Unwrap()&(system.EX11|system.EWayland) != 0 { - conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("dri"), Device: true, Optional: true}}) - } - // opportunistically bind kvm - conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("kvm"), Device: true, Optional: true}}) - - // hide nscd from container if present - nscd := container.AbsFHSVar.Append("run/nscd") - if _, err := sys.Stat(nscd.String()); !errors.Is(err, fs.ErrNotExist) { - conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSEphemeral{Target: nscd}}) - } - - // do autoetc last - conf.Filesystem = append(conf.Filesystem, - hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{ - Target: container.AbsFHSEtc, - Source: container.AbsFHSEtc, - Special: true, - }}, - ) - - config.Container = conf - } - - // late nil checks for pd behaviour - if config.Shell == nil { - return hlog.WrapErr(syscall.EINVAL, "invalid shell path") - } - if config.Path == nil { - return hlog.WrapErr(syscall.EINVAL, "invalid program path") - } - - var mapuid, mapgid *stringPair[int] - { - var uid, gid int - var err error - seal.container, seal.env, err = newContainer(config.Container, sys, seal.id.String(), &uid, &gid) - seal.waitDelay = config.Container.WaitDelay - if err != nil { - return hlog.WrapErrSuffix(err, - "cannot initialise container configuration:") - } - if len(config.Args) == 0 { - config.Args = []string{config.Path.String()} - } - seal.container.Path = config.Path - seal.container.Args = config.Args - - mapuid = newInt(uid) - mapgid = newInt(gid) - if seal.env == nil { - seal.env = make(map[string]string, 1<<6) - } - } - - // inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as mapped uid - innerRuntimeDir := container.AbsFHSRunUser.Append(mapuid.String()) - seal.env[xdgRuntimeDir] = innerRuntimeDir.String() - seal.env[xdgSessionClass] = "user" - seal.env[xdgSessionType] = "tty" - - share := &shareHost{seal: seal, sc: sys.Paths()} - seal.runDirPath = share.sc.RunDirPath - seal.sys = system.New(seal.user.uid.unwrap()) - seal.sys.Ensure(share.sc.SharePath.String(), 0711) - - { - runtimeDir := share.sc.SharePath.Append("runtime") - seal.sys.Ensure(runtimeDir.String(), 0700) - seal.sys.UpdatePermType(system.User, runtimeDir.String(), acl.Execute) - runtimeDirInst := runtimeDir.Append(seal.user.identity.String()) - seal.sys.Ensure(runtimeDirInst.String(), 0700) - seal.sys.UpdatePermType(system.User, runtimeDirInst.String(), acl.Read, acl.Write, acl.Execute) - seal.container.Tmpfs(container.AbsFHSRunUser, 1<<12, 0755) - seal.container.Bind(runtimeDirInst, innerRuntimeDir, container.BindWritable) - } - - { - tmpdir := share.sc.SharePath.Append("tmpdir") - seal.sys.Ensure(tmpdir.String(), 0700) - seal.sys.UpdatePermType(system.User, tmpdir.String(), acl.Execute) - tmpdirInst := tmpdir.Append(seal.user.identity.String()) - seal.sys.Ensure(tmpdirInst.String(), 01700) - seal.sys.UpdatePermType(system.User, tmpdirInst.String(), acl.Read, acl.Write, acl.Execute) - // mount inner /tmp from share so it shares persistence and storage behaviour of host /tmp - seal.container.Bind(tmpdirInst, container.AbsFHSTmp, container.BindWritable) - } - - { - username := "chronos" - if seal.user.username != "" { - username = seal.user.username - } - seal.container.Dir = seal.user.home - seal.env["HOME"] = seal.user.home.String() - seal.env["USER"] = username - seal.env[shell] = config.Shell.String() - - seal.container.Place(container.AbsFHSEtc.Append("passwd"), - []byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Hakurei:"+seal.user.home.String()+":"+config.Shell.String()+"\n")) - seal.container.Place(container.AbsFHSEtc.Append("group"), - []byte("hakurei:x:"+mapgid.String()+":\n")) - } - - // pass TERM for proper terminal I/O in initial process - if t, ok := sys.LookupEnv(term); ok { - seal.env[term] = t - } - - if config.Enablements.Unwrap()&system.EWayland != 0 { - // outer wayland socket (usually `/run/user/%d/wayland-%d`) - var socketPath *container.Absolute - if name, ok := sys.LookupEnv(wayland.WaylandDisplay); !ok { - hlog.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName) - socketPath = share.sc.RuntimePath.Append(wayland.FallbackName) - } else if a, err := container.NewAbs(name); err != nil { - socketPath = share.sc.RuntimePath.Append(name) - } else { - socketPath = a - } - - innerPath := innerRuntimeDir.Append(wayland.FallbackName) - seal.env[wayland.WaylandDisplay] = wayland.FallbackName - - if !config.DirectWayland { // set up security-context-v1 - appID := config.ID - if appID == "" { - // use instance ID in case app id is not set - appID = "app.hakurei." + seal.id.String() - } - // downstream socket paths - outerPath := share.instance().Append("wayland") - seal.sys.Wayland(&seal.sync, outerPath.String(), socketPath.String(), appID, seal.id.String()) - seal.container.Bind(outerPath, innerPath, 0) - } else { // bind mount wayland socket (insecure) - hlog.Verbose("direct wayland access, PROCEED WITH CAUTION") - share.ensureRuntimeDir() - seal.container.Bind(socketPath, innerPath, 0) - seal.sys.UpdatePermType(system.EWayland, socketPath.String(), acl.Read, acl.Write, acl.Execute) - } - } - - if config.Enablements.Unwrap()&system.EX11 != 0 { - if d, ok := sys.LookupEnv(display); !ok { - return hlog.WrapErr(ErrXDisplay, - "DISPLAY is not set") - } else { - socketDir := container.AbsFHSTmp.Append(".X11-unix") - - // the socket file at `/tmp/.X11-unix/X%d` is typically owned by the priv user - // and not accessible by the target user - var socketPath *container.Absolute - if len(d) > 1 && d[0] == ':' { // `:%d` - if n, err := strconv.Atoi(d[1:]); err == nil && n >= 0 { - socketPath = socketDir.Append("X" + strconv.Itoa(n)) - } - } else if len(d) > 5 && strings.HasPrefix(d, "unix:") { // `unix:%s` - if a, err := container.NewAbs(d[5:]); err == nil { - socketPath = a - } - } - if socketPath != nil { - if _, err := sys.Stat(socketPath.String()); err != nil { - if !errors.Is(err, fs.ErrNotExist) { - return hlog.WrapErrSuffix(err, - fmt.Sprintf("cannot access X11 socket %q:", socketPath)) - } - } else { - seal.sys.UpdatePermType(system.EX11, socketPath.String(), acl.Read, acl.Write, acl.Execute) - if !config.Container.HostAbstract { - d = "unix:" + socketPath.String() - } - } - } - - seal.sys.ChangeHosts("#" + seal.user.uid.String()) - seal.env[display] = d - seal.container.Bind(socketDir, socketDir, 0) - } - } - - if config.Enablements.Unwrap()&system.EPulse != 0 { - // PulseAudio runtime directory (usually `/run/user/%d/pulse`) - pulseRuntimeDir := share.sc.RuntimePath.Append("pulse") - // PulseAudio socket (usually `/run/user/%d/pulse/native`) - pulseSocket := pulseRuntimeDir.Append("native") - - if _, err := sys.Stat(pulseRuntimeDir.String()); err != nil { - if !errors.Is(err, fs.ErrNotExist) { - return hlog.WrapErrSuffix(err, - fmt.Sprintf("cannot access PulseAudio directory %q:", pulseRuntimeDir)) - } - return hlog.WrapErr(ErrPulseSocket, - fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir)) - } - - if s, err := sys.Stat(pulseSocket.String()); err != nil { - if !errors.Is(err, fs.ErrNotExist) { - return hlog.WrapErrSuffix(err, - fmt.Sprintf("cannot access PulseAudio socket %q:", pulseSocket)) - } - return hlog.WrapErr(ErrPulseSocket, - fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pulseRuntimeDir)) - } else { - if m := s.Mode(); m&0o006 != 0o006 { - return hlog.WrapErr(ErrPulseMode, - fmt.Sprintf("unexpected permissions on %q:", pulseSocket), m) - } - } - - // hard link pulse socket into target-executable share - innerPulseRuntimeDir := share.runtime().Append("pulse") - innerPulseSocket := innerRuntimeDir.Append("pulse", "native") - seal.sys.Link(pulseSocket.String(), innerPulseRuntimeDir.String()) - seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0) - seal.env[pulseServer] = "unix:" + innerPulseSocket.String() - - // publish current user's pulse cookie for target user - if src, err := discoverPulseCookie(sys); err != nil { - // not fatal - hlog.Verbose(strings.TrimSpace(err.(*hlog.BaseError).Message())) - } else { - innerDst := hst.AbsTmp.Append("/pulse-cookie") - seal.env[pulseCookie] = innerDst.String() - var payload *[]byte - seal.container.PlaceP(innerDst, &payload) - seal.sys.CopyFile(payload, src, 256, 256) - } - } - - if config.Enablements.Unwrap()&system.EDBus != 0 { - // ensure dbus session bus defaults - if config.SessionBus == nil { - config.SessionBus = dbus.NewConfig(config.ID, true, true) - } - - // downstream socket paths - sessionPath, systemPath := share.instance().Append("bus"), share.instance().Append("system_bus_socket") - - // configure dbus proxy - if f, err := seal.sys.ProxyDBus( - config.SessionBus, config.SystemBus, - sessionPath.String(), systemPath.String(), - ); err != nil { - return err - } else { - seal.dbusMsg = f - } - - // share proxy sockets - sessionInner := innerRuntimeDir.Append("bus") - seal.env[dbusSessionBusAddress] = "unix:path=" + sessionInner.String() - seal.container.Bind(sessionPath, sessionInner, 0) - seal.sys.UpdatePerm(sessionPath.String(), acl.Read, acl.Write) - if config.SystemBus != nil { - systemInner := container.AbsFHSRun.Append("dbus/system_bus_socket") - seal.env[dbusSystemBusAddress] = "unix:path=" + systemInner.String() - seal.container.Bind(systemPath, systemInner, 0) - seal.sys.UpdatePerm(systemPath.String(), acl.Read, acl.Write) - } - } - - // mount root read-only as the final setup Op - seal.container.Remount(container.AbsFHSRoot, syscall.MS_RDONLY) - - // append ExtraPerms last - for _, p := range config.ExtraPerms { - if p == nil || p.Path == nil { - continue - } - - if p.Ensure { - seal.sys.Ensure(p.Path.String(), 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.String(), perms...) - } - - // flatten and sort env for deterministic behaviour - seal.container.Env = make([]string, 0, len(seal.env)) - for k, v := range seal.env { - if strings.IndexByte(k, '=') != -1 { - return hlog.WrapErr(syscall.EINVAL, - fmt.Sprintf("invalid environment variable %s", k)) - } - seal.container.Env = append(seal.container.Env, k+"="+v) - } - slices.Sort(seal.container.Env) - - if hlog.Load() { - hlog.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s, ops: %d", - seal.user.uid, seal.user.username, config.Groups, seal.container.Args, len(*seal.container.Ops)) - } - - 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, hlog.WrapErrSuffix(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, hlog.WrapErrSuffix(err, - fmt.Sprintf("cannot access PulseAudio cookie %q:", p)) - } - // not found, try next method - } else if !s.IsDir() { - return p, nil - } - } - - return "", hlog.WrapErr(ErrPulseCookie, - fmt.Sprintf("cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)", - pulseCookie, xdgConfigHome, home)) -} |
