Files
sic/cmd/sicd/main.go
T
Claude (noether) 3982706ebb sicd: reject malformed netstrings (security), and retire an unsatisfiable read-error test
FINDING 1 (security) — the Go daemon accepted netstrings the Python reference REJECTS and
then EXECUTED them, so a malformed frame that fails closed on a Python hop ran on a Go hop.
parse_netstring now matches gateway/sicd exactly: reject non-digit lengths (incl. a leading
'+'/'-'), reject lengths > 1<<24, reject trailing bytes after the closing comma. The three
cases now exit 1 with a diagnostic instead of exec'ing. Both suites pin them (the Python
suite didn't either, which is how this slipped through green tests).

FINDING 2 (read-error path) — TestStdinReadErrorDiagnosedNotSwallowed is SKIPPED, with the
rationale in the skip string, because its premise is false in Go. It injected a fault by
closing a pty master mid-pump, assuming the slave read returns EIO — true for CPython's
os.read(), but Go's runtime poller delivers a clean EOF instead (probed directly: slave.Read
-> io.EOF, io.Copy -> nil; a socketpair peer close is EOF too — SO_LINGER/RST is TCP-only).
No fd mechanism available to the test produces a distinguishable READ error on sicd's stdin.
That is not a gap in main.go: the trailing stdin is UNFRAMED by design, so a truncated source
and a clean end are BOTH EOF and cannot be told apart — the pump treats EOF as a legitimate
end and already reaps-then-exits-nonzero on a genuine non-EPIPE error (kept in main.go), which
just doesn't arise via pty/socket close in Go.

Two local models burned 20+ grind attempts trying to satisfy this test before the premise was
probed — a textbook "a failure that looks like incapability is instrumentation." Proper
coverage for the reap-on-real-error branch = refactor the pump into a unit-testable func fed by
an io.Reader that returns a non-EOF error; left as a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 07:22:08 +02:00

160 lines
3.7 KiB
Go

package main
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
"strconv"
"syscall"
)
func readNetstring(buf []byte) ([]byte, []byte, bool) {
i := bytes.IndexByte(buf, ':')
if i < 0 {
return nil, nil, false
}
// Reject non-digit characters (including leading '+' or '-')
for _, c := range buf[:i] {
if c < '0' || c > '9' {
return nil, nil, false
}
}
n, err := strconv.Atoi(string(buf[:i]))
if err != nil || n < 0 {
return nil, nil, false
}
// Sanity cap: reject lengths > 1<<24
if n > 1<<24 {
return nil, nil, false
}
start := i + 1
end := start + n
if len(buf) < end+1 || buf[end] != ',' {
return nil, nil, false
}
// Reject trailing data after the netstring's closing comma
if len(buf) > end+1 {
return nil, nil, false
}
return buf[start:end], buf[end+1:], true
}
func waitForChild(cmd *exec.Cmd) int {
if err := cmd.Wait(); err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
if ws, ok := exitErr.Sys().(syscall.WaitStatus); ok && ws.Signaled() {
return 128 + int(ws.Signal())
}
return exitErr.ExitCode()
}
fmt.Fprintf(os.Stderr, "sicd: wait: %v\n", err)
return 1
}
return 0
}
func main() {
signal.Ignore(syscall.SIGPIPE)
magic := make([]byte, 1)
if _, err := io.ReadFull(os.Stdin, magic); err != nil {
fmt.Fprintf(os.Stderr, "sicd: read magic: %v\n", err)
os.Exit(1)
}
if magic[0] != 0x00 {
fmt.Fprintf(os.Stderr, "sicd: invalid magic byte: 0x%02x\n", magic[0])
os.Exit(1)
}
lenBytes := make([]byte, 4)
if _, err := io.ReadFull(os.Stdin, lenBytes); err != nil {
fmt.Fprintf(os.Stderr, "sicd: read length: %v\n", err)
os.Exit(1)
}
nsLen := binary.BigEndian.Uint32(lenBytes)
nsBuf := make([]byte, nsLen)
if _, err := io.ReadFull(os.Stdin, nsBuf); err != nil {
fmt.Fprintf(os.Stderr, "sicd: read netstring: %v\n", err)
os.Exit(1)
}
content, _, ok := readNetstring(nsBuf)
if !ok {
fmt.Fprintf(os.Stderr, "sicd: invalid netstring\n")
os.Exit(1)
}
nulIdx := bytes.IndexByte(content, 0x00)
if nulIdx < 0 {
fmt.Fprintf(os.Stderr, "sicd: content missing NUL separator\n")
os.Exit(1)
}
command := content[:nulIdx]
payload := content[nulIdx+1:]
argv := bytes.Split(command, []byte(" "))
argvStr := make([]string, len(argv))
for i, a := range argv {
argvStr[i] = string(a)
}
if len(argvStr) == 0 {
fmt.Fprintf(os.Stderr, "sicd: empty command\n")
os.Exit(1)
}
cmd := exec.Command(argvStr[0], argvStr[1:]...)
stdinPipe, err := cmd.StdinPipe()
if err != nil {
fmt.Fprintf(os.Stderr, "sicd: create stdin pipe: %v\n", err)
os.Exit(1)
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
fmt.Fprintf(os.Stderr, "sicd: exec %s: %v\n", argvStr[0], err)
os.Exit(1)
}
if err := writeAll(stdinPipe, payload); err != nil {
if errors.Is(err, syscall.EPIPE) {
// Child died before reading all payload.
} else {
fmt.Fprintf(os.Stderr, "sicd: write payload: %v\n", err)
}
stdinPipe.Close()
os.Exit(waitForChild(cmd))
}
// Pump remaining stdin to child. EPIPE on the write side means the
// child stopped reading (| head semantics) — that is not an error.
// Any other error (EIO, etc.) means sicd's own stdin failed mid-stream
// and the transfer was truncated — exit non-zero with a diagnostic.
if _, err := io.Copy(stdinPipe, os.Stdin); err != nil && !errors.Is(err, syscall.EPIPE) {
fmt.Fprintf(os.Stderr, "sicd: forward stdin: %v\n", err)
stdinPipe.Close()
waitForChild(cmd)
os.Exit(1)
}
stdinPipe.Close()
os.Exit(waitForChild(cmd))
}
func writeAll(w io.Writer, data []byte) error {
for off := 0; off < len(data); {
n, err := w.Write(data[off:])
if err != nil {
return err
}
off += n
}
return nil
}