Commit Graph

5 Commits

Author SHA1 Message Date
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
Markus Fritsche 833a0432b8 sicd: harden pump loop, EPIPE/diagnostic handling, signal fidelity
Review round 3 fixes:
- write_all helper: handle partial writes from signals
- EPIPE on write side: catch BrokenPipeError (child dead), not all OSError
- Read-side OSError: diagnose on stderr + exit 1 (not silent truncation)
- parse_netstring: sanity cap 1<<24, return content only (not tuple)
- 128+WTERMSIG for signal-killed children (shell convention)
- 24/24 pytest green
2026-07-23 01:01:59 +02:00
Claude (noether) 7f7f0c2ecb sicd: handle EPIPE and short writes at both write sites
write_all() loops until the full buffer is transferred — os.write's return value was
ignored at the payload write and in the pump loop, so a short write silently truncated
the stream. EPIPE at the payload write (child already dead) now reaps the child and
inherits its status instead of dying with a BrokenPipeError traceback; EPIPE in the pump
loop falls through to the same waitpid, which is standard `cmd | head` semantics.

Tests use deterministic fault injection rather than racing signals: a fork wrapper that
waits with waitid(WNOWAIT) so the child is provably dead but still reapable before the
parent writes, and an os.write cap of 7 bytes per call as the stand-in for a
signal-interrupted partial write.

Reviewed three times. Known gaps, tracked, NOT fixed here: signal-killed children still
collapse to exit 1 (no 128+WTERMSIG), and the pump loop's `except OSError: pass` is
broader than it should be.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 00:39:58 +02:00
Markus Fritsche 6584181f5c tests: add large-payload and nonexistent-command tests (2 red)
Adds tests that expose the pipe-buffer deadlock:
- 65,792 bytes (64 KiB + 1) hangs instead of completing
- 1 MiB body hangs
- Nonexistent command exits nonzero

15 passed, 2 failed (expected — deadlock not yet fixed)
2026-07-22 23:55:44 +02:00
Markus Fritsche b5e7e2b632 sicd: implement wire protocol (preamble + nested netstring peeling)
- Read preamble: magic byte \x00 + 4-byte BE length + netstring
- Split netstring at first NUL: command\0<payload>
- Exec command with payload + trailing stdin forwarded
- Nested payloads are netstrings for the next hop (one layer peeled per sicd)
- Bare -- survives as opaque data
- Malformed input: exit 1 with stderr diagnostic
- 14/14 pytest green
2026-07-22 23:44:00 +02:00