fix(pgwire): fix memory corruption from malformed wire messages - #6983
Conversation
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>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
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
left a comment
There was a problem hiding this comment.
The fixes are looking good, tests should be made understandable
[PR Coverage check]😍 pass : 42 / 49 (85.71%) file detail
|
Summary
Several code paths in
io.questdb.cutlass.pgwiretrusted 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.processInitMessageaccepted any signedintasmsgLen. Negative values passed themsgLen > availableToReadcheck and flowed intoprocessStartupMessage, wheremsgLimit = recvBufStart + msgLenpointed before the receive buffer.recvBufReadPos = msgLimitthen moved the read cursor backwards, andcompactRecvBufinvokedVect.memmove(recvBufStart, recvBufReadPos, recvBufWritePos - recvBufReadPos)with a source pointer outside the allocated buffer.PGCleartextPasswordAuthenticator.processPasswordMessagehad the same class of issue with itsmsgLen.msgLento sit between the protocol minimum and the capacity of the receive buffer; out-of-range values terminate the connection viaPGMessageProcessingException.DefaultPGCircuitBreakerRegistry.cancelcompared the index with<instead of<=, so aCancelRequestwithpid == circuitBreakers.size()indexed one slot past the logical end of theObjList. The method also readcircuitBreakers.getQuick(idx)andcb.getSecret()outside the registry spin lock, whileadd/remove/closeall hold it. The cancel path now runs the whole lookup, secret check andcb.cancel()under the same lock. It additionally rejects the-1sentinel thatclear()installs, closing the window betweenclear()and the nextinit()during which the slot held no real secret.Post-authentication
PGNonNullBinaryArrayView.addDimLenandPGNonNullVarcharArrayView.addDimLencomputedflatViewLength *= dimLenon signedints without overflow checking. Dimensions such as[65537, 65537]wrappedflatViewLengthto131073whileshapestill claimed~4.3Belements.setPtrAndCalculateStridesthen validated the on-wire byte count against the wrapped length and passed. Both methods now useMath.multiplyExact.PGPipelineEntry.setBindVariableAsArrayandsetBindVariableAsVarcharArraydid not check thatvalueSizestayed non-negative as it was decremented, and did not reject negativedimensionsordimensionSize. They now validatevalueSizebefore each field read, reject non-positive dimension counts and negative dimension sizes, and translateArithmeticExceptionfromaddDimLeninto aPGMessageProcessingException.Tradeoffs
setBindVariableAsArrayrun once per dimension perBind. Arrays typically have few dimensions, and this is not on a hot per-row path.cb.cancel()now runs while the lock is held.close()already callscb.cancel()under the same lock, so the contention pattern is unchanged.-1sentinel rejection incancelis 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
🤖 Generated with Claude Code