Files

135 lines
4.1 KiB
Go

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package control
import (
"fmt"
"os"
"path/filepath"
"strconv"
"syscall"
"testing"
)
type screenFixture struct {
root, executable string
t *testing.T
}
func new_screen_fixture(t *testing.T) screenFixture {
t.Helper()
f := screenFixture{root: t.TempDir(), t: t}
f.executable = filepath.Join(f.root, "screen")
if err := os.WriteFile(f.executable, nil, 0700); err != nil {
t.Fatal(err)
}
f.process(100, 1, 0, "/usr/bin/SCREEN", f.executable)
f.process(120, 100, 0, "notifier", "")
f.process(300, 1, 0, "reader", "")
return f
}
func (f screenFixture) process(pid, parent int, tty uint32, command, executable string) {
f.t.Helper()
path := filepath.Join(f.root, strconv.Itoa(pid))
if err := os.MkdirAll(filepath.Join(path, "fd"), 0700); err != nil {
f.t.Fatal(err)
}
// A parenthesized name checks the /proc/stat field boundary too.
stat := fmt.Sprintf("%d (process (name)) S %d %d %d %d 0 0", pid, parent, pid, pid, int32(tty))
for name, value := range map[string]string{"stat": stat, "cmdline": command + "\x00"} {
if err := os.WriteFile(filepath.Join(path, name), []byte(value), 0600); err != nil {
f.t.Fatal(err)
}
}
if executable != "" {
if err := os.Symlink(executable, filepath.Join(path, "exe")); err != nil {
f.t.Fatal(err)
}
}
}
func (f screenFixture) attach(pid, reader int, device string, sameExecutable bool) {
f.t.Helper()
info, err := os.Stat(device)
if err != nil {
f.t.Fatal(err)
}
executable := f.executable
if !sameExecutable {
executable = filepath.Join(f.root, "other")
if err := os.WriteFile(executable, nil, 0700); err != nil {
f.t.Fatal(err)
}
}
f.process(pid, reader, uint32(info.Sys().(*syscall.Stat_t).Rdev), "screen", executable)
if err := os.Symlink(device, filepath.Join(f.root, "100", "fd", strconv.Itoa(pid))); err != nil {
f.t.Fatal(err)
}
}
func TestScreenAttachmentRouting(t *testing.T) {
for _, scenario := range []string{"attached", "detached", "other executable", "ambiguous", "duplicate displays", "reattached", "no reader", "wrong uid"} {
t.Run(scenario, func(t *testing.T) {
f := new_screen_fixture(t)
uid := os.Geteuid()
readers := []instance{{PID: 300}, {PID: 400}}
if scenario != "detached" {
f.attach(200, 300, "/dev/null", scenario != "other executable")
}
if scenario == "ambiguous" {
f.process(400, 1, 0, "reader", "")
f.attach(201, 400, "/dev/zero", true)
}
if scenario == "duplicate displays" {
f.attach(201, 300, "/dev/zero", true)
}
if scenario == "no reader" {
readers = nil
}
if scenario == "wrong uid" {
uid++
}
if scenario == "reattached" {
if err := os.Remove(filepath.Join(f.root, "100", "fd", "200")); err != nil {
t.Fatal(err)
}
f.process(400, 1, 0, "reader", "")
f.attach(201, 400, "/dev/zero", true)
}
reader, handled, err := attached_screen_reader(f.root, []int{120, 100}, uid, readers)
valid := scenario == "attached" || scenario == "duplicate displays" || scenario == "reattached"
if handled != (scenario != "wrong uid") || (err == nil && handled) != valid {
t.Fatal(reader, handled, err)
}
if valid {
want := 300
if scenario == "reattached" {
want = 400
}
if reader.PID != want {
t.Fatal("wrong reader", reader.PID, want)
}
}
})
}
}
func TestScreenRoutingIgnoresEnvironmentWithoutServerAncestor(t *testing.T) {
f := new_screen_fixture(t)
f.attach(200, 300, "/dev/null", true)
t.Setenv("STY", "100.session")
if _, handled, err := attached_screen_reader(f.root, []int{120}, os.Geteuid(), []instance{{PID: 300}}); handled || err != nil {
t.Fatal("environment selected an unrelated Screen session", handled, err)
}
// A broken ancestry cycle must terminate without choosing an unrelated reader.
f.process(301, 302, 0, "cycle", "")
f.process(302, 301, 0, "cycle", "")
f.attach(201, 301, "/dev/zero", true)
reader, handled, err := attached_screen_reader(f.root, []int{120, 100}, os.Geteuid(), []instance{{PID: 300}})
if !handled || err != nil || reader.PID != 300 {
t.Fatal(reader, handled, err)
}
}