Version / branch / commit
main at 1b5db1765672820caac1684b168c9898b5ba3593.
OS and environment
Linux x86-64, kernel 6.1, official Go 1.26.6. The behavior follows the generic io.Writer contract and is platform-independent.
Steps to reproduce
Construct an ACP connection with an io.Writer that accepts every byte except the final one and returns a nil error, then invoke the package writer:
type shortWriter struct{ bytes.Buffer }
func (w *shortWriter) Write(p []byte) (int, error) {
if len(p) == 0 { return 0, nil }
return w.Buffer.Write(p[:len(p)-1])
}
w := &shortWriter{}
conn := NewConn(bytes.NewReader(nil), w)
err := conn.write(rpcMessage{Method: "audit/repro"})
Focused audit reproduction:
go test ./internal/acp -run TestAuditReproACPWriteAcceptsNilErrorShortWrite -count=1 -v
--- PASS: TestAuditReproACPWriteAcceptsNilErrorShortWrite (0.00s)
The temporary reproducer confirmed that err == nil while the resulting bytes had no final newline and therefore were not a complete ndjson record. It was removed after verification and was not committed.
Relevant code: internal/acp/jsonrpc.go, (*Conn).write, approximately lines 440-454.
Expected behavior
Each JSON-RPC record is emitted completely, including its newline delimiter, or the caller receives an error. Nil-error short progress should be retried correctly or converted to io.ErrShortWrite.
Actual behavior
(*Conn).write checks only the returned error and ignores n. A legal short writer can omit part of a JSON-RPC record while the method returns nil. The peer may then block waiting for the delimiter or parse a later write as part of the truncated message.
Writes are mutex-serialized, which correctly prevents concurrent interleaving but does not make one short write complete. Common production file/pipe writers usually report a non-nil error, so no production truncation incident is claimed.
Suggested fix and regression tests
Use a complete-write helper or return io.ErrShortWrite for nil-error short progress. Keep the existing write mutex across the entire record.
Add deterministic tests for:
- one-byte/partial record writes;
- a missing newline caused by the final short byte;
- zero-progress nil-error writers;
- partial progress followed by success, according to the selected contract; and
- concurrent callers retaining whole-record serialization.
Reported from codebase audit finding COR-01; split from the daemon frame writer because the protocol and call sites are independent.
Version / branch / commit
mainat1b5db1765672820caac1684b168c9898b5ba3593.OS and environment
Linux x86-64, kernel 6.1, official Go 1.26.6. The behavior follows the generic
io.Writercontract and is platform-independent.Steps to reproduce
Construct an ACP connection with an
io.Writerthat accepts every byte except the final one and returns a nil error, then invoke the package writer:Focused audit reproduction:
The temporary reproducer confirmed that
err == nilwhile the resulting bytes had no final newline and therefore were not a complete ndjson record. It was removed after verification and was not committed.Relevant code:
internal/acp/jsonrpc.go,(*Conn).write, approximately lines 440-454.Expected behavior
Each JSON-RPC record is emitted completely, including its newline delimiter, or the caller receives an error. Nil-error short progress should be retried correctly or converted to
io.ErrShortWrite.Actual behavior
(*Conn).writechecks only the returned error and ignoresn. A legal short writer can omit part of a JSON-RPC record while the method returns nil. The peer may then block waiting for the delimiter or parse a later write as part of the truncated message.Writes are mutex-serialized, which correctly prevents concurrent interleaving but does not make one short write complete. Common production file/pipe writers usually report a non-nil error, so no production truncation incident is claimed.
Suggested fix and regression tests
Use a complete-write helper or return
io.ErrShortWritefor nil-error short progress. Keep the existing write mutex across the entire record.Add deterministic tests for:
Reported from codebase audit finding
COR-01; split from the daemon frame writer because the protocol and call sites are independent.