// 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...) lookup := s.screenReader if lookup == nil { lookup = func(ancestors []int, uid int, readers []instance) (instance, bool, error) { return attached_screen_reader("/proc", ancestors, uid, readers) } } if reader, handled, err := lookup(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 }