Skip to content

fix(pgwire): fix memory corruption from malformed wire messages - #6983

Merged
bluestreak01 merged 9 commits into
masterfrom
vi_pg_fix
Apr 22, 2026
Merged

fix(pgwire): fix memory corruption from malformed wire messages#6983
bluestreak01 merged 9 commits into
masterfrom
vi_pg_fix

Conversation

@bluestreak01

Copy link
Copy Markdown
Member

Summary

Several code paths in io.questdb.cutlass.pgwire trusted length and dimension fields read from the wire without validating them. In the pre-auth handshake this allowed a network attacker to drive pointer arithmetic to positions outside the receive buffer; in the post-auth extended query protocol it allowed an authenticated client to wrap the flat length of an array to a value smaller than the shape implied.

Pre-authentication

  • PGCleartextPasswordAuthenticator.processInitMessage accepted any signed int as msgLen. Negative values passed the msgLen > availableToRead check and flowed into processStartupMessage, where msgLimit = recvBufStart + msgLen pointed before the receive buffer. recvBufReadPos = msgLimit then moved the read cursor backwards, and compactRecvBuf invoked Vect.memmove(recvBufStart, recvBufReadPos, recvBufWritePos - recvBufReadPos) with a source pointer outside the allocated buffer.
  • PGCleartextPasswordAuthenticator.processPasswordMessage had the same class of issue with its msgLen.
  • Both handlers now require msgLen to sit between the protocol minimum and the capacity of the receive buffer; out-of-range values terminate the connection via PGMessageProcessingException.
  • DefaultPGCircuitBreakerRegistry.cancel compared the index with < instead of <=, so a CancelRequest with pid == circuitBreakers.size() indexed one slot past the logical end of the ObjList. The method also read circuitBreakers.getQuick(idx) and cb.getSecret() outside the registry spin lock, while add/remove/close all hold it. The cancel path now runs the whole lookup, secret check and cb.cancel() under the same lock. It additionally rejects the -1 sentinel that clear() installs, closing the window between clear() and the next init() during which the slot held no real secret.

Post-authentication

  • PGNonNullBinaryArrayView.addDimLen and PGNonNullVarcharArrayView.addDimLen computed flatViewLength *= dimLen on signed ints without overflow checking. Dimensions such as [65537, 65537] wrapped flatViewLength to 131073 while shape still claimed ~4.3B elements. setPtrAndCalculateStrides then validated the on-wire byte count against the wrapped length and passed. Both methods now use Math.multiplyExact.
  • PGPipelineEntry.setBindVariableAsArray and setBindVariableAsVarcharArray did not check that valueSize stayed non-negative as it was decremented, and did not reject negative dimensions or dimensionSize. They now validate valueSize before each field read, reject non-positive dimension counts and negative dimension sizes, and translate ArithmeticException from addDimLen into a PGMessageProcessingException.

Tradeoffs

  • Well-formed clients already send lengths within the validated ranges, so there is no observable behavior change for legitimate traffic.
  • The pre-auth checks add two comparisons on the startup/password paths; those paths are not performance-sensitive.
  • The per-dimension checks in setBindVariableAsArray run once per dimension per Bind. Arrays typically have few dimensions, and this is not on a hot per-row path.
  • Moving the cancel path under the registry spin lock means cb.cancel() now runs while the lock is held. close() already calls cb.cancel() under the same lock, so the contention pattern is unchanged.
  • The -1 sentinel rejection in cancel is conservative: a real random secret equal to -1 (1 chance in 2^32) would be rejected. That is an acceptable cost to eliminate the clear/init window.

Test plan

  • mvn -pl core -Dtest='DoubleArrayParserTest,VarcharArrayParserTest,PGSecurityTest' test
  • mvn -pl core -Dtest='PGArraysTest,PGVarcharArrayBindVariablesTest' test
  • mvn -pl core -Dtest='PGJobContextTest' test
  • mvn -pl core test (full suite)
  • Manual: connect via pgjdbc and psql to confirm normal auth and query flow

🤖 Generated with Claude Code

Several code paths in io.questdb.cutlass.pgwire trusted length and
dimension fields from the wire without validating them, which allowed
an attacker to drive pointer arithmetic to positions outside the recv
buffer or to wrap integer multiplications in array parsing.

Pre-auth changes:

- PGCleartextPasswordAuthenticator.processInitMessage now rejects a
  signed msgLen that is below the protocol minimum (4 + 4 bytes) or
  above the recv buffer capacity before any pointer arithmetic.
  Previously, a negative msgLen passed the `msgLen > availableToRead`
  check, and processStartupMessage would assign a backward msgLimit
  to recvBufReadPos; compactRecvBuf then memmoved a large run of
  bytes starting before recvBufStart into the recv buffer.

- PGCleartextPasswordAuthenticator.processPasswordMessage applies the
  same class of bound check to its msgLen.

- DefaultPGCircuitBreakerRegistry.cancel had an off-by-one in the idx
  range check (`size() < idx` should be `<=`) and read the slot and
  secret without holding the registry lock. The whole lookup now runs
  under the same SimpleSpinLock that add/remove/close use. The secret
  check also rejects the -1 sentinel that clear() installs, so a
  cancel request arriving between clear() and the next init() can no
  longer succeed by sending secret = -1.

Post-auth changes:

- PGNonNullBinaryArrayView.addDimLen and PGNonNullVarcharArrayView
  .addDimLen compute flatViewLength with Math.multiplyExact so that
  attacker-supplied dimensions can no longer wrap flatViewLength to
  a small positive while shape still claims billions of elements.

- PGPipelineEntry.setBindVariableAsArray and setBindVariableAsVarchar
  Array validate the remaining valueSize before each field read,
  reject non-positive dimensions and negative dimensionSize values,
  and translate ArithmeticException from addDimLen into a proper
  PGMessageProcessingException.

Tests DoubleArrayParserTest, VarcharArrayParserTest, PGSecurityTest,
PGArraysTest and PGVarcharArrayBindVariablesTest pass with the
changes in place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bluestreak01 bluestreak01 added Postgres Wire Issues or changes relating to Postgres wire protocol Security Bug Incorrect or unexpected behavior labels Apr 16, 2026
@coderabbitai

coderabbitai Bot commented Apr 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e17b5305-5756-4964-b592-87607ff12d28

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch vi_pg_fix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

bluestreak01 and others added 5 commits April 16, 2026 22:43
Review follow-up on PR #6983.

PGNonNullBinaryArrayView.addDimLen and PGNonNullVarcharArrayView.addDimLen
now compute Math.multiplyExact before mutating shape. Previously shape.add
ran first, so an overflow-triggered ArithmeticException left the view with
shape holding one extra dim and flatViewLength unchanged. The current
callers in PGPipelineEntry discard the view on exception, but the
invariant is cheaper to preserve than to defend.

New tests:

- DefaultPGCircuitBreakerRegistryTest covers cancel() against a negative
  idx, idx equal to the configured limit, idx beyond it, an empty slot,
  the -1 sentinel secret from clear(), a mismatched secret, and the happy
  path.

- PGJobContextTest gains three hex scripts that replay malformed
  pre-auth messages: a StartupMessage with msgLen = 0xFFFFFFFF, a
  StartupMessage with msgLen = 4 (below the 8-byte protocol minimum),
  and a PasswordMessage with msgLen = 0xFFFFFFFF. All three expect the
  server to close the connection before any pointer arithmetic runs.

A direct unit test for addDimLen was considered but skipped: the method
is package-private, the test module cannot reach it without adding
`opens io.questdb.cutlass.pgwire;` to the main module-info, and the
overflow behaviour rests on Math.multiplyExact which is a JDK primitive.
The existing PGArraysTest and PGVarcharArrayBindVariablesTest continue
to cover the happy path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous assertion `cb.getCancelledFlag() == null || ...` short-
circuited on the first disjunct because the cancelled flag is only
populated by setCancelledFlag() which the test does not call, so the
test passed even if registry.cancel() never ran.

The test now asserts checkIfTripped() is false before the cancel and
true after, which genuinely observes the effect of cancel() flipping
powerUpTime to Long.MIN_VALUE.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds four hex-script tests in PGJobContextTest to close coverage gaps
identified in review:

testBadInitMessageLengthTooLarge and testBadPasswordLengthTooLarge send
msgLen=0x00200000 (2 MB) against the 1 MB default receive buffer. The
prior tests only exercised negative and below-minimum lengths, leaving
the "too large" branch of the new upper-bound check in
processInitMessage and processPasswordMessage untested.

testBindBinaryArrayFlatLengthOverflow is a direct regression for the
[65537, 65537] case that motivated the Math.multiplyExact guard in
PGNonNullBinaryArrayView.addDimLen. The test drives a full Parse +
Bind + Execute + Sync through the wire and asserts the server
responds with ErrorResponse("array size overflow") + ReadyForQuery.
Verified to fail when the guard is reverted to an unchecked *=.

testBindBinaryArrayDimensionSizeNegative exercises the new
dimensionSize < 0 guard in setBindVariableAsArray by sending
dim[0]=-1. The server must reject before the value reaches addDimLen.

Unit tests against PGNonNullBinaryArrayView.addDimLen and
PGNonNullVarcharArrayView.addDimLen were not viable: those methods are
package-private and JPMS does not allow split packages across the
io.questdb and io.questdb.test modules. The hex-script tests cover
the same regressions end-to-end through the actual wire path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@RaphDal RaphDal left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fixes are looking good, tests should be made understandable

Comment thread core/src/main/java/io/questdb/cutlass/pgwire/PGPipelineEntry.java Outdated
@RaphDal
RaphDal self-requested a review April 22, 2026 08:08
RaphDal
RaphDal previously approved these changes Apr 22, 2026
@mtopolnik

Copy link
Copy Markdown
Contributor

[PR Coverage check]

😍 pass : 42 / 49 (85.71%)

file detail

path covered line new line coverage
🔵 io/questdb/cutlass/pgwire/PGNonNullBinaryArrayView.java 4 6 66.67%
🔵 io/questdb/cutlass/pgwire/PGPipelineEntry.java 18 23 78.26%
🔵 io/questdb/cutlass/pgwire/PGCleartextPasswordAuthenticator.java 6 6 100.00%
🔵 io/questdb/cutlass/pgwire/DefaultPGCircuitBreakerRegistry.java 12 12 100.00%
🔵 io/questdb/cutlass/pgwire/PGNonNullVarcharArrayView.java 2 2 100.00%

@bluestreak01
bluestreak01 merged commit 3bfe763 into master Apr 22, 2026
51 checks passed
@bluestreak01
bluestreak01 deleted the vi_pg_fix branch April 22, 2026 18:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Incorrect or unexpected behavior Postgres Wire Issues or changes relating to Postgres wire protocol Security

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants