Files
fenrir/internal/control/coexistence_linux.go

197 lines
5.5 KiB
Go

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package control
import (
"fmt"
"io"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"syscall"
)
func (s *Server) endpoint_snapshot() []endpoint {
s.endpointsMu.RLock()
defer s.endpointsMu.RUnlock()
return slices.Clone(s.endpoints)
}
// Every default instance has a private endpoint. The first available instance
// also serves the shared path, and a survivor claims it after that owner exits.
func (s *Server) claim_main() bool {
if s.options.SocketFile != "" || s.ctx.Err() != nil {
return false
}
s.endpointsMu.Lock()
defer s.endpointsMu.Unlock()
for _, endpoint := range s.endpoints {
if endpoint.path == s.main_path() {
return false
}
}
endpoint, err := listen(s.main_path())
if err != nil {
return false // Active readers and unrelated paths are preserved.
}
if s.ctx.Err() != nil {
endpoint.listener.Close()
if info, err := os.Lstat(endpoint.path); err == nil && os.SameFile(info, endpoint.info) {
_ = os.Remove(endpoint.path)
}
return false
}
s.endpoints = append(s.endpoints, endpoint)
s.workers.Go(func() { s.accept(endpoint) })
return true
}
func (s *Server) private_path(data instance) string {
path := filepath.Join(filepath.Dir(s.main_path()), fmt.Sprintf("fenrirscreenreader-%d.sock", data.PID))
if slices.Contains(data.SocketFiles, path) {
return path
}
return ""
}
// Ancestry identifies ordinary PTY children. Reattached Screen sessions and
// detached helpers can lose that ancestry, so also use the caller's own X11
// identity. Neither a single terminal nor a console is a safe default.
func (s *Server) route_target(pid, uid int) (string, error) {
instances := s.instances()
var terminals, consoles []instance
for _, data := range instances {
if s.private_path(data) == "" {
continue
}
if data.ScreenDriver == "vcsaDriver" && (data.uid == 0 || data.uid == uid) {
consoles = append(consoles, data)
} else if data.ScreenDriver == "ptyDriver" && (data.uid == uid || uid == 0) {
terminals = append(terminals, data)
}
}
seen := map[int]bool{}
var ancestors []int
var ancestorReader *instance
ancestry:
for ancestor := pid; ancestor > 1 && !seen[ancestor]; ancestor = parent_pid(ancestor) {
seen[ancestor] = true
ancestors = append(ancestors, ancestor)
for _, data := range terminals {
if data.PID == ancestor {
ancestorReader = &data
break ancestry
}
}
for _, data := range consoles {
if data.PID == ancestor {
ancestorReader = &data
break ancestry
}
}
}
readers := append(slices.Clone(terminals), consoles...)
if reader, handled, err := attached_screen_reader("/proc", ancestors, uid, readers); handled {
if err != nil {
return "", err
}
return s.destination(reader)
}
if ancestorReader != nil {
return s.destination(*ancestorReader)
}
identity := process_identity(pid, uid)
return s.route_identity(identity, uid, terminals, consoles)
}
type processIdentity struct {
window, display string
console bool
}
// Read only identity fields from the credentialed process, never a supplied PID
// or environment in the request text. Environment is a routing hint, not an
// authorization grant; the destination still checks the original SO_PEERCRED.
func process_identity(pid, uid int) processIdentity {
var identity processIdentity
info, err := os.Stat(fmt.Sprintf("/proc/%d", pid))
if err != nil {
return identity
}
if stat, ok := info.Sys().(*syscall.Stat_t); !ok || int(stat.Uid) != uid {
return identity
}
if data, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)); err == nil {
end := strings.LastIndexByte(string(data), ')')
if end >= 0 {
fields := strings.Fields(string(data[end+1:]))
if len(fields) > 4 {
tty, _ := strconv.ParseInt(fields[4], 10, 64)
device := uint32(tty)
major := (device >> 8) & 0xfff
minor := (device & 0xff) | ((device >> 12) & 0xfff00)
identity.console = major == 4 && minor >= 1 && minor <= 63
}
}
}
file, err := os.Open(fmt.Sprintf("/proc/%d/environ", pid))
if err != nil {
return identity
}
defer file.Close()
data, err := io.ReadAll(io.LimitReader(file, 1<<20))
if err != nil {
return identity
}
for _, entry := range strings.Split(string(data), "\x00") {
key, value, _ := strings.Cut(entry, "=")
switch key {
case "WINDOWID":
identity.window = value
case "DISPLAY":
identity.display = value
}
}
return identity
}
func (s *Server) route_identity(identity processIdentity, uid int, terminals, consoles []instance) (string, error) {
var matches []instance
if identity.console {
// A real controlling VT takes precedence over inherited X11 variables.
matches = consoles
} else if identity.window != "" && identity.display != "" {
for _, data := range terminals {
if data.uid != uid || data.WindowID != identity.window {
continue
}
target := process_identity(data.PID, data.uid)
if target.display == identity.display {
matches = append(matches, data)
}
}
}
if len(matches) == 1 {
return s.destination(matches[0])
}
return "", fmt.Errorf("caller does not identify a unique reader; run inside its managed terminal or use its private socket")
}
func (s *Server) destination(data instance) (string, error) {
if data.PID == os.Getpid() {
return "", nil
}
if !data.ConnectionHandoff {
return "", fmt.Errorf("reader %d cannot accept a connection handoff", data.PID)
}
path := s.private_path(data)
info, err := os.Lstat(path)
if err != nil || info.Mode()&os.ModeSocket == 0 {
return "", fmt.Errorf("reader %d private socket is unavailable", data.PID)
}
return path, nil
}