convex-db: harden Postgres wire decoder against malformed pre-auth frames#596
Open
PrazwalR wants to merge 1 commit into
Open
convex-db: harden Postgres wire decoder against malformed pre-auth frames#596PrazwalR wants to merge 1 commit into
PrazwalR wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR hardens convex-db’s PostgreSQL wire-protocol decoder (PgMessageDecoder) against malformed pre-authentication frames to reduce the risk of unauthenticated denial-of-service (OOM / decoder crashes) on the Postgres port.
Changes:
- Add a maximum message size (
MAX_MESSAGE_LENGTH, 32MB) plus minimum-length checks for startup and regular frames, rejecting invalid lengths viaCorruptedFrameException. - Validate several wire-supplied counts/lengths (e.g. negative
paramCount,numParams, etc.) before allocating arrays / reading parameter values. - Add a new
PgMessageDecoderTestusing NettyEmbeddedChannelto assert malformed inputs are rejected withCorruptedFrameException.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| convex-db/src/main/java/convex/db/psql/PgMessageDecoder.java | Adds message-length caps and additional validation / bounds checks to reject malformed frames early. |
| convex-db/src/test/java/convex/db/psql/PgMessageDecoderTest.java | Adds regression tests for oversized/undersized frames, negative counts/lengths, and unterminated C-strings. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
189
to
193
| int start = buf.readerIndex(); | ||
| int limit = buf.writerIndex(); | ||
| int end = start; | ||
| while (buf.getByte(end) != 0) { | ||
| while (end < limit && buf.getByte(end) != 0) { | ||
| end++; |
Comment on lines
+142
to
145
| if (paramLen < 0 || paramLen > MAX_MESSAGE_LENGTH) | ||
| throw new CorruptedFrameException("Invalid paramLen: " + paramLen); | ||
| paramValues[i] = new byte[paramLen]; | ||
| in.readBytes(paramValues[i]); |
Comment on lines
+155
to
+159
| byte[] noNul = "SELECT 1".getBytes(StandardCharsets.UTF_8); | ||
| ByteBuf frame = Unpooled.buffer(); | ||
| frame.writeByte(PgMessage.QUERY); | ||
| frame.writeInt(noNul.length + 4); // valid length, but no NUL terminator in body | ||
| frame.writeBytes(noNul); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
PgMessageDecoderruns on raw bytes from any client that reaches the Postgresport, before authentication (default is trust mode,
PgServer.javabuilds thepipeline as
PgMessageDecoder+PgProtocolHandlerwith noLengthFieldBasedFrameDecoderand no max frame size). Several wire-suppliedlength/count fields were used unchecked:
length(decodeStartup,decodeRegular): a client candeclare a length near
Integer.MAX_VALUEand stream bytes, forcing Netty'scumulator to buffer up to ~2 GB per connection → heap exhaustion / OOM. A tiny
lengthalso madelength - 4negative, skipping the "wait for full message"guard and mis-parsing the frame.
paramCount,numParamFormats,numParams,numResultFormats, per-paramparamLen) were used directly asarray sizes →
NegativeArraySizeException(cheap per-connection crash).readCString) scanned with no upper bound past theframe →
IndexOutOfBoundsException.This is a pre-authentication DoS surface. Corroborates #555 (convex-db memory
use).
Fix
Validate all wire-supplied lengths/counts in
PgMessageDecoderbeforeallocating or looping. Reject malformed input with
CorruptedFrameException,which Netty routes to the existing
PgProtocolHandler.exceptionCaught(closesthe channel cleanly). Changes:
MAX_MESSAGE_LENGTH) plus minimum-length checks on startup andregular frames.
paramCount/numParamFormats/numParams/numResultFormats, and anyparamLen < 0other than the-1NULL sentinel(also capped at
MAX_MESSAGE_LENGTH).readCStringto the buffer's readable region; throw on a missing NUL.No changes to
PgProtocolHandleror the SQL-rewrite hacks (separate #560). TheCURRENT_DATABASEstring concatenation and the absence of a connection limitare noted follow-ups, out of scope here.
Tests
New
PgMessageDecoderTest(NettyEmbeddedChannel, 9 cases): oversized/too-smalllength on startup and regular frames, negative
numParams/paramLen, negativePARSE
paramCount, unterminated C-string, and a well-formed startup + queryregression. All assert
CorruptedFrameExceptionrather than OOM / crash.mvn -pl convex-db test -Dtest=PgMessageDecoderTest # 9/9 pass
mvn -q -pl convex-db test -Dtest=PgServerTest # no regression