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>
This commit is contained in:
@@ -18,15 +18,29 @@ func readNetstring(buf []byte) ([]byte, []byte, bool) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -125,6 +139,7 @@ func main() {
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user