close
Skip to content

Handle pre-protocol errors to prevent memory exhaustion#1248

Closed
lieut-data wants to merge 1 commit into
lib:masterfrom
lieut-data:master
Closed

Handle pre-protocol errors to prevent memory exhaustion#1248
lieut-data wants to merge 1 commit into
lib:masterfrom
lieut-data:master

Conversation

@lieut-data

@lieut-data lieut-data commented Jan 28, 2026

Copy link
Copy Markdown
Contributor

When PostgreSQL cannot fork a backend process (e.g., an external process limit), it sends a plain text error like Ecould not fork new process for connection: Resource temporarily unavailable before any protocol handshake. The 'E' byte gets parsed as an ErrorResponse message type, but the following bytes are plain text, not a binary length field.

This causes lib/pq to interpret ASCII text as a message length (e.g., "coul" = 0x636f756c = 1.6GB), leading to massive memory allocations and a likely OOM under connection pressure.

This fix matches libpq's behavior in fe-connect.c: if an ErrorResponse has msgLength < 8 or > 30000, treat it as a pre-protocol plain text error and read it as a null-terminated string instead.

There's a decent chance this is related to #638.

When PostgreSQL cannot fork a backend process (e.g., an external process
limit), it sends a plain text error like "Ecould not fork new process
for connection: Resource temporarily unavailable" before any protocol
handshake. The 'E' byte gets parsed as an ErrorResponse message type,
but the following bytes are plain text, not a binary length field.

This causes lib/pq to interpret ASCII text as a message length
(e.g., "coul" = 0x636f756c = 1.6GB), leading to massive memory
allocations and a likely OOM under connection pressure.

This fix matches libpq's behavior in fe-connect.c: if an ErrorResponse
has msgLength < 8 or > 30000, treat it as a pre-protocol plain text
error and read it as a null-terminated string instead.
@arp242

arp242 commented Jan 28, 2026

Copy link
Copy Markdown
Collaborator

Was just about to do a release, so fairly good timing on this.

Do these tweaks seem alright to you? c3c7698 (I can't update this PR because it's created from master in your fork instead of a branch; pushing from the git CLI doesn't let me update the master branch and I've never been able to figure out how to get around that other than using the GitHub web UI).

@arp242

arp242 commented Jan 28, 2026

Copy link
Copy Markdown
Collaborator

Merged via #1249

@lieut-data

Copy link
Copy Markdown
Contributor Author

Nice, thanks @arp242! I'll re-test with my project to verify, but it looks good!

@coderabbitai coderabbitai Bot mentioned this pull request Apr 4, 2026
arp242 added a commit that referenced this pull request Jul 11, 2026
…types

#1248 is not entirely correct, as it should only apply on startup
messages, as that check is done in PQconnectPoll():

	if (beresp == PqMsg_ErrorResponse && (msgLength < 8 || msgLength > MAX_ERRLEN)

We can re-use the txnStatus field to check if we're in the connection
phase.

In addition, in pqParseInput3(), it does limit the message length, but
only for some respose types:

	#define VALID_LONG_MESSAGE_TYPE(id) \
		((id) == PqMsg_CopyData || \
		(id) == PqMsg_DataRow || \
		(id) == PqMsg_ErrorResponse || \
		(id) == PqMsg_FunctionCallResponse || \
		(id) == PqMsg_NoticeResponse || \
		(id) == PqMsg_NotificationResponse || \
		(id) == PqMsg_RowDescription)

	if (msgLength > 30000 && !VALID_LONG_MESSAGE_TYPE(id))

So check for that as well.

Fixes #1324
Closes #1326
arp242 added a commit that referenced this pull request Jul 11, 2026
…types

#1248 is not entirely correct, as it should only apply on startup
messages, as that check is done in PQconnectPoll():

	if (beresp == PqMsg_ErrorResponse && (msgLength < 8 || msgLength > MAX_ERRLEN)

We can re-use the txnStatus field to check if we're in the connection
phase.

In addition, in pqParseInput3(), it does limit the message length, but
only for some respose types:

	#define VALID_LONG_MESSAGE_TYPE(id) \
		((id) == PqMsg_CopyData || \
		(id) == PqMsg_DataRow || \
		(id) == PqMsg_ErrorResponse || \
		(id) == PqMsg_FunctionCallResponse || \
		(id) == PqMsg_NoticeResponse || \
		(id) == PqMsg_NotificationResponse || \
		(id) == PqMsg_RowDescription)

	if (msgLength > 30000 && !VALID_LONG_MESSAGE_TYPE(id))

So check for that as well.

Fixes #1324
Closes #1326
arp242 added a commit that referenced this pull request Jul 11, 2026
…types

#1248 is not entirely correct, as it should only apply on startup
messages, as that check is done in PQconnectPoll():

	if (beresp == PqMsg_ErrorResponse && (msgLength < 8 || msgLength > MAX_ERRLEN)

We can re-use the txnStatus field to check if we're in the connection
phase.

In addition, in pqParseInput3(), it does limit the message length, but
only for some respose types:

	#define VALID_LONG_MESSAGE_TYPE(id) \
		((id) == PqMsg_CopyData || \
		(id) == PqMsg_DataRow || \
		(id) == PqMsg_ErrorResponse || \
		(id) == PqMsg_FunctionCallResponse || \
		(id) == PqMsg_NoticeResponse || \
		(id) == PqMsg_NotificationResponse || \
		(id) == PqMsg_RowDescription)

	if (msgLength > 30000 && !VALID_LONG_MESSAGE_TYPE(id))

So check for that as well.

Fixes #1324
Closes #1326
arp242 added a commit that referenced this pull request Jul 11, 2026
…types

#1248 is not entirely correct, as it should only apply on startup
messages, as that check is done in PQconnectPoll():

	if (beresp == PqMsg_ErrorResponse && (msgLength < 8 || msgLength > MAX_ERRLEN)

We can re-use the txnStatus field to check if we're in the connection
phase.

In addition, in pqParseInput3(), it does limit the message length, but
only for some respose types:

	#define VALID_LONG_MESSAGE_TYPE(id) \
		((id) == PqMsg_CopyData || \
		(id) == PqMsg_DataRow || \
		(id) == PqMsg_ErrorResponse || \
		(id) == PqMsg_FunctionCallResponse || \
		(id) == PqMsg_NoticeResponse || \
		(id) == PqMsg_NotificationResponse || \
		(id) == PqMsg_RowDescription)

	if (msgLength > 30000 && !VALID_LONG_MESSAGE_TYPE(id))

So check for that as well.

Fixes #1324
Closes #1326
arp242 added a commit that referenced this pull request Jul 11, 2026
…types

#1248 is not entirely correct, as it should only apply on startup
messages, as that check is done in PQconnectPoll():

	if (beresp == PqMsg_ErrorResponse && (msgLength < 8 || msgLength > MAX_ERRLEN)

We can re-use the txnStatus field to check if we're in the connection
phase.

In addition, in pqParseInput3(), it does limit the message length, but
only for some respose types:

	#define VALID_LONG_MESSAGE_TYPE(id) \
		((id) == PqMsg_CopyData || \
		(id) == PqMsg_DataRow || \
		(id) == PqMsg_ErrorResponse || \
		(id) == PqMsg_FunctionCallResponse || \
		(id) == PqMsg_NoticeResponse || \
		(id) == PqMsg_NotificationResponse || \
		(id) == PqMsg_RowDescription)

	if (msgLength > 30000 && !VALID_LONG_MESSAGE_TYPE(id))

So check for that as well.

Fixes #1324
Closes #1326
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.

2 participants