Skip to content

pg: a backend message longer than MaxReceiveBytes fails the connection, and its length can no longer wrap - #315

Merged
MDA2AV merged 2 commits into
mainfrom
fix/pg-message-length
Oct 12, 2026
Merged

MDA2AV merged 2 commits into
mainfrom
fix/pg-message-length

Conversation

@MDA2AV

@MDA2AV MDA2AV commented Oct 7, 2026

Copy link
Copy Markdown
Owner

A pg server that sent a message with a length near int.MaxValue could crash the whole process: one 13-byte DataRow was enough.

TryReadMessage checked for a complete message with position + 1 + length > buffer.Length. A length near int.MaxValue wraps that sum negative, so a message announcing 2 GiB was taken as already arrived. Its body became a 2 GiB span over the 64 KiB receive buffer, and _scan was set to int.MinValue. During startup, the next parse then threw IndexOutOfRangeException. During a query, ReadFirstField decoded the DataRow's first field straight out of that span. Reading past the buffer killed the process with a fatal AccessViolationException, which no handler can catch.

A declared length above MaxReceiveBytes, which the docs already call the hard ceiling on one backend message, now fails the connection with a PgException that names the message. The completeness check is now a subtraction that cannot overflow. Negative lengths were already refused as malformed, and still are. No new state.

Tests

New, in PgFramingTests (no postgres needed):

  • pg: a backend message whose length overflows the frame fails the connection with a clear error
  • control: the same fake server's well-formed startup connects

Both run against FakeServer, a new harness helper: a loopback listener that runs a script per accepted connection on its own thread. Here the script answers the startup message with an AuthenticationOk whose length is int.MaxValue. The control's script answers with a well-formed AuthenticationOk and ReadyForQuery, so the refusal is about the length and not the fake server.

main this PR
startup answered with a length of int.MaxValue IndexOutOfRangeException: Index was outside the bounds of the array. PgException: backend message 'R' of 2147483647 bytes exceeds 67108864 bytes
DataRow with that length mid-query (probe, not committed) process aborts: Fatal error. System.AccessViolationException in ReadFirstField PgException: backend message 'D' of 2147483647 bytes exceeds 67108864 bytes
control: well-formed startup connected connected

Suites: Unit 63 (1 pending, as on main), Pg 8, Redis 3, with both sidecars running (nothing skipped).

Hot path, barely: one extra integer compare per backend message.

FakeServer.cs is byte-identical in the other hostile-server branches (SCRAM iterations, RESP depth, open deadline), so merging them adds the file once.

MDA2AV added 2 commits October 7, 2026 21:48
…n, and its length can no longer wrap

TryReadMessage checked for a complete message with position + 1 + length > buffer.Length. A
length near int.MaxValue wraps that sum negative, so a 9-byte message announcing 2 GiB was taken
as fully arrived, with a body span of 2 GiB over the 64 KiB receive buffer and _scan set to
int.MinValue. During startup the next parse threw IndexOutOfRangeException. A DataRow framed that
way during a query was worse: ReadFirstField decoded its first field straight out of that span,
and the process died with a fatal AccessViolationException.

A declared length above MaxReceiveBytes, the documented ceiling on one backend message, now fails
the connection with a PgException that names it, and the completeness check is a subtraction
that cannot overflow. Negative lengths were already refused as malformed. No new state.

New tests in PgFramingTests run against a FakeServer, a new harness helper for scripted loopback
peers: a startup answered with an AuthenticationOk whose length is int.MaxValue, and a control in
which the same server's well-formed startup connects. Confirmed both ways: on f298af0 the test gets
IndexOutOfRangeException, and with the fix a PgException that names the oversized message. The
DataRow case was checked with a probe that is not committed: on f298af0 the test process aborted,
and with the fix the query fails with the same PgException.
# Conflicts:
#	tests/Ioxide.Tests.Pg/Program.cs
@MDA2AV
MDA2AV merged commit 59036bd into main Oct 12, 2026
1 check passed
@MDA2AV MDA2AV mentioned this pull request Oct 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant