// Package control exposes a local-only command channel into a running // gpu-turnstile service: a named pipe on Windows, a unix socket on Linux. // It lets unprivileged local users ask the service to do privileged work // that is safe to offer — currently triggering an update check, whose // payload is signature-verified regardless of who asks. The channel never // accepts data beyond a one-word command, and the server rate-limits // triggers, so the worst a local user can cause is a cheap, throttled // check and a GPU-idle-gated restart onto a signed binary. // // Abuse hardening: the command read is capped (4 KiB), each connection is // force-closed after connTimeout so a stalled client cannot pin a goroutine // (or a Windows pipe instance) forever, and concurrently served connections // are capped at maxConns — beyond that, connections are closed on arrival. // On Windows the pipe's ACL additionally denies network logons, so the // channel cannot be reached from another machine. // // Protocol: the client writes one command line, the server answers with // one reply line ("OK ..." or "ERR ...") and hangs up. package control import ( "bufio" "errors" "fmt" "io" "strings" "time" ) // CmdUpdateNow asks the service to check for, stage and (once the GPU is // idle) restart onto a signed update immediately. const CmdUpdateNow = "update-now" // CmdStatus asks for a one-line JSON status snapshot (monitor mode). const CmdStatus = "status" // CmdReloadEnv asks the service to re-read and validate its config file, // and to restart onto it (once the GPU is idle) when it changed. const CmdReloadEnv = "reload-env" // ErrUnavailable means no running service offers the control channel. var ErrUnavailable = errors.New("control channel unavailable") // Handler answers one command; the returned string is sent back as one // line. It must start with "OK " or "ERR ". type Handler func(cmd string) string // connTimeout bounds one connection's lifetime: a client that stops // mid-command or never reads the reply would otherwise pin its goroutine // (and on Windows one of the pipe instances) indefinitely. A var so tests // can shrink it. var connTimeout = 10 * time.Second // maxConns caps concurrently served connections; beyond it, new // connections are closed on arrival. Bound on the goroutines a local // flood can pile up. const maxConns = 32 var connSem = make(chan struct{}, maxConns) // serve dispatches connection handling under the concurrency cap. It // returns false when the cap is reached — the caller must then close the // connection itself. func serve(c io.ReadWriteCloser, h Handler) bool { select { case connSem <- struct{}{}: go func() { defer func() { <-connSem }() serveConn(c, h) }() return true default: return false } } // forceCloser is implemented by connections that can be torn down // abortively, unblocking pending reads and writes (Windows pipe: // DisconnectNamedPipe; unix socket: a deadline in the past). The // connection watchdog uses it; normal closes still flush the reply. type forceCloser interface { ForceClose() error } // serveConn runs the line protocol on one accepted connection. func serveConn(c io.ReadWriteCloser, h Handler) { defer c.Close() if fc, ok := c.(forceCloser); ok { timer := time.AfterFunc(connTimeout, func() { fc.ForceClose() }) defer timer.Stop() } line, err := bufio.NewReader(io.LimitReader(c, 4096)).ReadString('\n') cmd := strings.TrimSpace(line) if cmd == "" { if err != nil { return } fmt.Fprintln(c, "ERR empty command") return } fmt.Fprintln(c, h(cmd)) } // readReply writes cmd and reads the server's one-line reply. func readReply(c io.ReadWriteCloser, cmd string) (string, error) { if _, err := fmt.Fprintln(c, cmd); err != nil { return "", err } // The server hangs up after its reply; a broken-pipe error after the // last byte still leaves the reply in the buffer. data, _ := io.ReadAll(io.LimitReader(c, 4096)) line := strings.TrimSpace(string(data)) if line == "" { return "", ErrUnavailable } return line, nil }