Skip to content

ACP: newline JSON writer silently accepts nil-error short writes #1027

Description

@PierrunoYT

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions