// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later package tentacle import ( "bufio" "context" "crypto/rand" "errors" "fmt" "io" "os" "os/exec" "path/filepath" "strings" "sync" "time" ) // Delegation owns a transient user service, not the reader process. Its small // guardian exits on pipe EOF, including reader death; systemd then kills the // whole service subtree. No service or example is installed or enabled on disk. type Delegation struct { Parent string input io.WriteCloser output io.ReadCloser done chan struct{} waitError error once sync.Once unit string directory string process *os.Process } func StartDelegation(ctx context.Context, program []string) (*Delegation, error) { if len(program) == 0 || !filepath.IsAbs(program[0]) { return nil, ErrContainment } var identity [16]byte if _, err := rand.Read(identity[:]); err != nil { return nil, err } unit := fmt.Sprintf("cthulhu-tentacles-%x.service", identity) directory, err := os.MkdirTemp("", "cthulhu-tentacles-") if err != nil { return nil, err } keepDirectory := false defer func() { if !keepDirectory { os.Remove(filepath.Join(directory, "launch")) os.Remove(directory) } }() args := []string{"--user", "--quiet", "--wait", "--pipe", "--collect", "--unit=" + unit, "--property=Delegate=cpu memory pids", "--property=DelegateSubgroup=reader", "--property=KillMode=control-group", "--property=TimeoutStopSec=3s"} for _, name := range []string{"DISPLAY", "XAUTHORITY", "DBUS_SESSION_BUS_ADDRESS", "AT_SPI_BUS_ADDRESS", "XDG_RUNTIME_DIR", "XDG_SESSION_TYPE", "XDG_SESSION_DESKTOP", "XDG_DATA_HOME", "XDG_CONFIG_HOME", "XDG_CACHE_HOME", "LANG", "LANGUAGE", "LC_ALL", "SPEECHD_ADDRESS", "SPEECHD_SOCKET"} { if value, ok := os.LookupEnv(name); ok { args = append(args, "--setenv="+name+"="+value) } } args = append(append(args, "--"), program...) args = append(args, filepath.Join(directory, "launch")) command := exec.Command("systemd-run", args...) // No inherited terminal input or unbounded diagnostics. The handshake below // is the only stdout message; failure becomes a bounded reader status. input, err := command.StdinPipe() if err != nil { return nil, err } output, err := command.StdoutPipe() if err != nil { input.Close() return nil, err } if err := command.Start(); err != nil { input.Close() output.Close() return nil, err } delegation := &Delegation{input: input, output: output, done: make(chan struct{}), unit: unit, directory: directory, process: command.Process} go func() { delegation.waitError = command.Wait(); close(delegation.done) }() readDone := make(chan error, 1) go func() { scanner := bufio.NewScanner(output) scanner.Buffer(make([]byte, 4096), 4096) if !scanner.Scan() { readDone <- ErrContainment return } parent := scanner.Text() if !strings.HasPrefix(parent, "/sys/fs/cgroup/") || filepath.Clean(parent) != parent || filepath.Base(parent) != unit { readDone <- ErrContainment return } delegation.Parent = parent readDone <- nil }() startup, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() select { case err = <-readDone: case <-startup.Done(): err = startup.Err() output.Close() <-readDone } if err != nil { cleanup, cancel := context.WithTimeout(context.Background(), 4*time.Second) defer cancel() delegation.Close(cleanup) return nil, fmt.Errorf("%w: could not start delegated user service", ErrContainment) } keepDirectory = true return delegation, nil } func (delegation *Delegation) Done() <-chan struct{} { return delegation.done } func (delegation *Delegation) Close(ctx context.Context) error { delegation.once.Do(func() { delegation.input.Close() }) select { case <-delegation.done: delegation.output.Close() os.Remove(filepath.Join(delegation.directory, "launch")) os.Remove(delegation.directory) return delegation.waitError case <-ctx.Done(): // Stop only the unique service this owner created. Never stop another reader. stopCtx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() stopError := exec.CommandContext(stopCtx, "systemctl", "--user", "--no-block", "stop", delegation.unit).Run() delegation.output.Close() _ = delegation.process.Kill() select { case <-delegation.done: case <-time.After(time.Second): } return errors.Join(ctx.Err(), stopError) } } // RunGuardian is a private helper. stdin is a reader-owned lifetime pipe, never // a command protocol. The guardian has no speech, keyboard, or settings access. func RunGuardian(input io.Reader, output io.Writer, socketPath string) error { parent, err := DelegatedParent() if err != nil { return err } err = serve_launches(input, output, socketPath, parent) // The reader may have died; the guardian also removes its empty private // socket directory after the listener unlinks the socket. if filepath.Base(socketPath) == "launch" && strings.HasPrefix(filepath.Base(filepath.Dir(socketPath)), "cthulhu-tentacles-") { os.Remove(filepath.Dir(socketPath)) } return err }