Skip to content

Harden ProxySQL PostgreSQL protocol handling against malformed packets - P2#5429

Merged
renecannao merged 10 commits into
v3.0_pgsql-copy-error-recovery-issue-5415from
v3.0_strengthen-pgsql-protocol-validation
Mar 2, 2026
Merged

Harden ProxySQL PostgreSQL protocol handling against malformed packets - P2#5429
renecannao merged 10 commits into
v3.0_pgsql-copy-error-recovery-issue-5415from
v3.0_strengthen-pgsql-protocol-validation

Conversation

@rahim-kanji

@rahim-kanji rahim-kanji commented Mar 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Improve ProxySQL's resilience when handling malformed PostgreSQL protocol packets by adding comprehensive validation throughout the packet processing pipeline.

Problem

The PostgreSQL protocol implementation had several areas where insufficient validation of packet structure, length fields, and string terminators could lead to undefined behavior when receiving malformed data from clients.

Solution

Added defensive checks at critical points in the packet processing flow:

  1. Startup Packet Processing - Replaced unbounded string searches with bounded memory searches to safely handle missing null terminators in connection parameters.

  2. Simple Query Handling - Added minimum size validation and null terminator verification for 'Q' (Query) packets before processing.

  3. Extended Query Protocol - Added bounds checking for Describe and Close messages to prevent reads beyond packet boundaries.

  4. Data Stream Buffering - Enforced minimum allocation sizes to handle undersized length fields.

  5. Admin Interface - Added comprehensive validation for query length, buffer pointers, and proper cleanup handling.

Summary by CodeRabbit

  • Bug Fixes

    • Improved session/resync and reset robustness with safer notice handling, clearer resync/error semantics, stronger packet validation, and guarded memory cleanup to prevent crashes and leaks.
  • Public API

    • Small interface changes that alter call/return behavior; downstream integrations may need adjustments.
  • Tests

    • Added extensive malformed-packet tests and a comprehensive COPY error-recovery test suite.

The buffer2array() function could allocate less than 5 bytes for a packet when pkgsize was malformed to be smaller than the header size. This violated PostgreSQL protocol assumptions that packets are always >= 5 bytes.

Ensure minimum allocation of sizeof(header) bytes regardless of the claimed packet size in the header.
Replace strlen() with memchr() to safely search for null terminators within buffer bounds. If no null terminator is found, treat the packet as malformed and break out of the processing loop.
…rminator checking

* Add validation to reject packets smaller than 6 bytes before processing
* Add null terminator check
Improve error handling in admin_session_handler when processing PostgreSQL query packets:

* Fix potential underflow in query_length calculation when handling very small packets (< 2 bytes)
* Fix NULL pointer dereference in strcasecmp when query_no_space is not initialized due to early packet validation failures
@coderabbitai

coderabbitai Bot commented Mar 1, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changed PgSQL connection/session control flow and parsing: PgSQL_Connection::flush gains an is_resync flag; PgSQL_Session::switch_normal_to_fast_forward_mode returns bool; added defensive parsing/validation across protocol/stream handlers; notice-receiver reattachment points added; two TAP tests for malformed packets and COPY error recovery added.

Changes

Cohort / File(s) Summary
Public API Signatures
include/PgSQL_Connection.h, include/PgSQL_Session.h
Signature updates: void flush()void flush(bool is_resync = false) and switch_normal_to_fast_forward_mode(...) now returns bool. Call sites updated accordingly.
Connection State & Notice Handling
lib/PgSQL_Connection.cpp
Flush accepts is_resync; resync paths call flush(true) and set resync_failed instead of emitting immediate errors. PQsetNoticeReceiver attachments added at reset/resync points; notice handler early-return guard added.
Session Logic & Admin Safety
lib/PgSQL_Session.cpp, lib/Admin_Handler.cpp
Frontend query packet size and null-terminator validation; COPY protocol messages dropped when not in fast-forward; switch_normal_to_fast_forward_mode returns success/failure and refuses switch if pending packets exist; admin cleanup frees guarded by NULL checks.
Protocol / Stream Parsing
lib/PgSQL_Data_Stream.cpp, lib/PgSQL_Protocol.cpp, lib/PgSQL_Extended_Query_Message.cpp
Defensive parsing: ensure allocations at least header-sized; startup parameter parsing uses bounds-checked null-terminator detection (memchr) and pointer-derived offsets; added guard ensuring statement-type byte exists for describe/close messages.
Tests & Test Groups
test/tap/groups/groups.json, test/tap/tests/pgsql-test_malformed_packet-t.cpp, test/tap/tests/pgsql-reg_test_5415_copy_error_recovery-t.cpp
Added two TAP groups and large tests: comprehensive malformed-packet testing (pre/post-auth) and COPY error-recovery regression tests (new C++ TAP suites).

Sequence Diagram

sequenceDiagram
    participant Client
    participant PgSQL_Session
    participant PgSQL_Connection
    participant libpq as libpq/PQ
    participant NoticeHandler

    rect rgba(200,150,255,0.5)
    Note over Client,PgSQL_Connection: Resync flush flow (flush(is_resync=true))
    end

    Client->>PgSQL_Session: send query / trigger reset
    PgSQL_Session->>PgSQL_Connection: resync_start()
    PgSQL_Connection->>libpq: flush(is_resync=true)
    alt flush succeeds
        libpq-->>PgSQL_Connection: OK
        PgSQL_Connection->>NoticeHandler: PQsetNoticeReceiver(unhandled_notice_cb)
        PgSQL_Connection-->>PgSQL_Session: ASYNC_RESET_SESSION_END
    else flush fails (is_resync=true)
        libpq-->>PgSQL_Connection: Error
        PgSQL_Connection->>PgSQL_Connection: set resync_failed=true
        PgSQL_Connection-->>PgSQL_Session: continue/resync retry
    end

    PgSQL_Session->>PgSQL_Connection: resync_cont()
    PgSQL_Connection->>libpq: flush(is_resync=true)
    libpq-->>PgSQL_Connection: Ready / Error
    PgSQL_Connection->>NoticeHandler: PQsetNoticeReceiver(notice_handler_cb)
    PgSQL_Connection-->>PgSQL_Session: resync complete / resume
    PgSQL_Session-->>Client: normal operation resumed
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

Poem

🐰 I nibble at packets, hunt each null and frame,

I hop through resyncs, whispering flush by name.
Notices I reattach, guards against the night,
Tests scurry after me, keeping errors light.
🥕 Hooray — no crashes in the pale moonlight.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Harden ProxySQL PostgreSQL protocol handling against malformed packets - P2' clearly and specifically describes the main change: adding defensive validation across PostgreSQL protocol processing to improve resilience against malformed packets.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch v3.0_strengthen-pgsql-protocol-validation

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.

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances ProxySQL's robustness and stability when handling PostgreSQL protocol traffic by introducing comprehensive validation checks for various packet types. The changes address previous vulnerabilities where malformed client data could lead to undefined behavior or crashes, ensuring that ProxySQL gracefully handles invalid input and maintains operational integrity.

Highlights

  • Startup Packet Processing Hardening: Replaced unbounded string searches with bounded memory searches (memchr) to safely handle missing null terminators in PostgreSQL connection parameters, preventing potential out-of-bounds reads.
  • Simple Query Handling Validation: Implemented minimum size validation and null terminator verification for 'Q' (Query) packets before processing, enhancing resilience against malformed simple queries.
  • Data Stream Buffering Safety: Enforced minimum allocation sizes for incoming PostgreSQL packets to handle cases where reported length fields might be undersized, preventing buffer under-allocations.
  • Admin Interface Query Validation: Added comprehensive validation for query length, buffer pointers, and improved cleanup handling within the Admin Handler to prevent issues from malformed admin queries.
  • COPY Protocol Error Recovery: Introduced logic to gracefully handle and ignore late COPY protocol messages ('d', 'c', 'f') that might arrive after a COPY operation has already terminated due to an error, preventing spurious 'Feature not supported' errors.
  • Comprehensive Malformed Packet Testing: Added extensive new test suites to validate ProxySQL's PostgreSQL protocol handling stability against a wide array of malformed packets, both before and after authentication, ensuring robustness and preventing crashes.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • include/PgSQL_Connection.h
    • Modified the flush method signature to accept an optional is_resync boolean parameter.
  • include/PgSQL_Session.h
    • Changed the return type of switch_normal_to_fast_forward_mode from void to bool.
  • lib/Admin_Handler.cpp
    • Added validation for minimum query size and null pointer checks for hdr.data.ptr.
    • Introduced a null check for query_no_space before strcasecmp.
    • Implemented conditional l_free calls for query_no_space and query to prevent double-free or use-after-free issues.
  • lib/PgSQL_Connection.cpp
    • Set PQsetNoticeReceiver during ASYNC_RESET_SESSION_END and reset_session_start for consistent notice handling.
    • Modified the flush method to accept an is_resync parameter and conditionally set errors based on its value.
    • Refactored resync_start and resync_cont to utilize the updated flush method for improved code reuse and error handling.
  • lib/PgSQL_Data_Stream.cpp
    • Ensured l_alloc for incoming packets allocates at least sizeof(header) bytes, even if the reported pkgsize is smaller, to prevent undersized allocations and potential buffer overflows.
  • lib/PgSQL_Protocol.cpp
    • Updated load_conn_parameters to use memchr for bounded null-terminator searches for connection parameter names and values, preventing reads beyond packet boundaries when processing startup packets.
  • lib/PgSQL_Session.cpp
    • Added checks for minimum packet size and null terminators for simple query ('Q') packets to prevent malformed queries from causing issues.
    • Introduced logic to ignore late COPY protocol messages ('d', 'c', 'f') when the session is no longer in fast-forward mode, addressing a race condition during COPY error recovery.
    • Modified switch_normal_to_fast_forward_mode to return bool and added a check for pending packets in client_myds->PSarrayIN before switching to fast-forward mode, preventing unexpected packet sequences.
    • Added query retry logic to handle_post_sync_parse_message, handle_post_sync_describe_message, and handle_post_sync_execute_message.
  • test/tap/groups/groups.json
    • Added entries for pgsql-test_malformed_packet-t and pgsql-reg_test_5415_copy_error_recovery-t to the default-g4 test group.
  • test/tap/tests/pgsql-reg_test_5415_copy_error_recovery-t.cpp
    • Added a new regression test to verify proper session state recovery after failed COPY FROM STDIN operations, specifically addressing the handling of pipelined COPY messages.
  • test/tap/tests/pgsql-test_malformed_packet-t.cpp
    • Added a new comprehensive test suite to validate ProxySQL's PostgreSQL protocol handling stability against various malformed packets, covering both pre-authentication and post-authentication scenarios.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces significant hardening for ProxySQL's PostgreSQL protocol handling, addressing potential vulnerabilities from malformed packets. The changes are comprehensive, covering startup packet processing, simple and extended query protocols, data stream buffering, and the admin interface. Key improvements include replacing unbounded string searches with bounded memory searches, adding packet size validation, and ensuring the presence of null terminators. The addition of new regression and fuzzing-style tests is commendable and greatly increases confidence in the robustness of these changes. The code is well-written, and I've only identified a minor opportunity for refactoring to reduce code duplication. Overall, this is an excellent contribution that enhances the security and stability of the PostgreSQL protocol implementation.

Comment thread lib/PgSQL_Session.cpp
Comment on lines +5992 to +5999
// set query retries
mybe->server_myds->query_retries_on_failure = pgsql_thread___query_retries_on_failure;
// if a number of retries is set in mysql_query_rules, that takes priority
if (qpo) {
if (qpo->retries >= 0) {
mybe->server_myds->query_retries_on_failure = qpo->retries;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

This block of code for setting query retries is duplicated in handle_post_sync_describe_message (line 6146) and handle_post_sync_execute_message (line 6433).

To improve maintainability and reduce redundancy, consider refactoring this logic into a private helper function within the PgSQL_Session class.

For example, you could add:

void PgSQL_Session::set_backend_query_retries(PgSQL_Backend* backend) {
    if (!backend || !backend->server_myds) {
        return;
    }
    backend->server_myds->query_retries_on_failure = pgsql_thread___query_retries_on_failure;
    if (qpo && qpo->retries >= 0) {
        backend->server_myds->query_retries_on_failure = qpo->retries;
    }
}

Then, you can replace the duplicated blocks in all three locations with a single call: set_backend_query_retries(mybe);

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (2)
lib/Admin_Handler.cpp (1)

4734-4739: Consider decoupling query_no_space free-size from mutable query_length.

These null checks are good. A small follow-up would make cleanup more robust: track query_no_space allocation length separately, since query_length is mutated later for rewritten queries.

Proposed refactor
 	char *query=NULL;
 	unsigned int query_length = 0;
 	char* query_no_space = NULL;
+	unsigned int query_no_space_length_alloc = 0;
 	unsigned int query_no_space_length = 0;
@@
 	query_no_space=(char *)l_alloc(query_length);
+	query_no_space_length_alloc = query_length;
 	memcpy(query_no_space,query,query_length);
@@
 	if (query_no_space) {
-		l_free(query_length, query_no_space);
+		l_free(query_no_space_length_alloc, query_no_space);
 	}
 	if (query) {
 		l_free(query_length, query);
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/Admin_Handler.cpp` around lines 4734 - 4739, The cleanup currently uses
the mutable query_length when freeing both query_no_space and query, which can
be wrong because query_length is modified later; introduce a separate size
variable (e.g., query_no_space_len) when you allocate query_no_space and use
that fixed length when calling l_free(query_no_space_len, query_no_space) in the
Admin_Handler.cpp cleanup block, leaving the existing l_free(query_length,
query) call unchanged; update allocations and any size computations to set
query_no_space_len at creation and clear pointers/lengths after free to avoid
double-free or size-mismatch issues.
lib/PgSQL_Session.cpp (1)

5992-6000: Consider extracting repeated retry-policy assignment into a helper.

The same retry-selection block appears in Parse/Describe/Execute post-sync paths; centralizing it would reduce drift risk and keep behavior aligned.

Also applies to: 6146-6154, 6433-6441

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/PgSQL_Session.cpp` around lines 5992 - 6000, Extract the repeated logic
that sets mybe->server_myds->query_retries_on_failure into a small helper (e.g.,
set_query_retries_from_qpo) that takes the target server_myds pointer, the
default pgsql_thread___query_retries_on_failure, and the qpo struct (with
qpo->retries), then use that helper in the current block and the
Parse/Describe/Execute post-sync paths to replace duplicated code; ensure the
helper applies the default when qpo is null or qpo->retries < 0 and assign the
resolved value back to mybe->server_myds->query_retries_on_failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/Admin_Handler.cpp`:
- Around line 2879-2885: The validation currently checks query_length and
hdr.data.ptr but not that the payload is null-terminated; update the same guard
in Admin_Handler.cpp to also verify that the last byte is '\0' before accepting
the packet. Concretely, after confirming query_length >= 2 and hdr.data.ptr !=
NULL, check that ((unsigned char*)hdr.data.ptr)[query_length - 1] == '\0'; if
not, call proxy_warning("Query not null-terminated: %u bytes\n", query_length),
call SPA->send_error_msg_to_client(sess, "Malformed query packet"), set
run_query = false and goto __run_query (same flow as the existing error branch).

In `@lib/PgSQL_Connection.cpp`:
- Line 1938: The notice_handler_cb callback can dereference a NULL query_result
during reset_session_start because PQsetNoticeReceiver is set before
query_result is initialized; update PgSQL_Connection::notice_handler_cb to first
check if this->query_result (or the member used) is non-null before accessing or
calling methods on it, and if it's null simply return / ignore the notice (or
log safely), ensuring notice_handler_cb safely handles the uninitialized state
when PQsetNoticeReceiver is installed; no other behavior changes required.

In `@lib/PgSQL_Session.cpp`:
- Around line 2076-2091: Move the malformed-Q packet validation to before any
query parsing or special-query handling: perform the pkt.size < 6 check, compute
query_len = pkt.size - 5 and set query_ptr = (char*)pkt.ptr + 5, and verify
query_ptr[query_len - 1] == '\0' before calling CurrentQuery.begin() and
handler_special_queries(); on failure call l_free(pkt.size, pkt.ptr), set
handler_ret = -1 and return handler_ret (same behavior as current checks) so
undersized or non-null-terminated packets never reach CurrentQuery.begin() or
handler_special_queries().

In `@test/tap/tests/pgsql-test_malformed_packet-t.cpp`:
- Around line 992-993: The ADMIN malformed-packet test is calling
run_malformed_packet_tests with cl.pgsql_host instead of the admin host, so
replace the host argument with the admin host (use cl.admin_host) while keeping
the admin port (cl.pgsql_admin_port) and the "ADMIN" role; update the other
identical calls at the nearby occurrences (the ones flagged around 1012-1013) so
all ADMIN runs target cl.admin_host rather than cl.pgsql_host.
- Around line 144-145: The variable handled_gracefully is tautological because
it accepts all recv() outcomes; change the check to assert meaningful outcomes:
when bytes_received <= 0 assert the connection was closed (handled as error),
and when bytes_received > 0 inspect the received buffer (e.g., check the first
byte or use the existing parser function) to ensure it is an ERROR/ERR response
rather than treating it as valid protocol data; update the assertions that use
handled_gracefully (including the similar block around the other occurrence at
lines 608-611) to validate the actual error-response semantics instead of using
a combined boolean that is always true.
- Around line 546-572: The test treats an authentication failure as a passing
condition; change the logic so that if authentication did not succeed (detected
via auth_type from read_message), the test fails immediately and does not
attempt the "post-authentication malformed packet" step. Specifically, in the
block using auth_type and auth_data (and the later similar block), replace the
branch that sends malformed_data when auth_type == 'E' with a failing ok(0, ...)
message indicating authentication did not succeed (use test_name.c_str()) and
return; only call send(sock, malformed_data.data(), malformed_data.size(), 0)
after confirming successful authentication from read_message. Ensure both
occurrences (around variables auth_type/auth_data, send, malformed_data) are
updated.

---

Nitpick comments:
In `@lib/Admin_Handler.cpp`:
- Around line 4734-4739: The cleanup currently uses the mutable query_length
when freeing both query_no_space and query, which can be wrong because
query_length is modified later; introduce a separate size variable (e.g.,
query_no_space_len) when you allocate query_no_space and use that fixed length
when calling l_free(query_no_space_len, query_no_space) in the Admin_Handler.cpp
cleanup block, leaving the existing l_free(query_length, query) call unchanged;
update allocations and any size computations to set query_no_space_len at
creation and clear pointers/lengths after free to avoid double-free or
size-mismatch issues.

In `@lib/PgSQL_Session.cpp`:
- Around line 5992-6000: Extract the repeated logic that sets
mybe->server_myds->query_retries_on_failure into a small helper (e.g.,
set_query_retries_from_qpo) that takes the target server_myds pointer, the
default pgsql_thread___query_retries_on_failure, and the qpo struct (with
qpo->retries), then use that helper in the current block and the
Parse/Describe/Execute post-sync paths to replace duplicated code; ensure the
helper applies the default when qpo is null or qpo->retries < 0 and assign the
resolved value back to mybe->server_myds->query_retries_on_failure.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5b721a6 and 1767938.

📒 Files selected for processing (10)
  • include/PgSQL_Connection.h
  • include/PgSQL_Session.h
  • lib/Admin_Handler.cpp
  • lib/PgSQL_Connection.cpp
  • lib/PgSQL_Data_Stream.cpp
  • lib/PgSQL_Protocol.cpp
  • lib/PgSQL_Session.cpp
  • test/tap/groups/groups.json
  • test/tap/tests/pgsql-reg_test_5415_copy_error_recovery-t.cpp
  • test/tap/tests/pgsql-test_malformed_packet-t.cpp

Comment thread lib/Admin_Handler.cpp
Comment thread lib/PgSQL_Connection.cpp
Comment thread lib/PgSQL_Session.cpp Outdated
Comment thread test/tap/tests/pgsql-test_malformed_packet-t.cpp Outdated
Comment thread test/tap/tests/pgsql-test_malformed_packet-t.cpp
Comment thread test/tap/tests/pgsql-test_malformed_packet-t.cpp Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
lib/PgSQL_Connection.cpp (1)

921-931: ⚠️ Potential issue | 🟡 Minor

Restore notice receiver on reset timeout and failure paths.

Line 922 restores unhandled_notice_cb only in ASYNC_RESET_SESSION_END, but both ASYNC_RESET_SESSION_TIMEOUT and ASYNC_RESET_SESSION_FAILED (lines 928-930) bypass that state. On timeout or failure, the connection remains active without restoring the notice receiver, leaving notice_handler_cb installed if it was active before reset started.

Proposed fix
 case ASYNC_RESET_SESSION_FAILED:
 case ASYNC_RESET_SESSION_SUCCESSFUL:
 case ASYNC_RESET_SESSION_TIMEOUT:
+	PQsetNoticeReceiver(pgsql_conn, &PgSQL_Connection::unhandled_notice_cb, this);
 	break;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/PgSQL_Connection.cpp` around lines 921 - 931, The switch handling reset
states fails to restore the notice receiver except in ASYNC_RESET_SESSION_END;
ensure PQsetNoticeReceiver(pgsql_conn, &PgSQL_Connection::unhandled_notice_cb,
this) is invoked on the failure and timeout paths as well (i.e., in the
ASYNC_RESET_SESSION_FAILED and ASYNC_RESET_SESSION_TIMEOUT handling) so the
connection doesn't retain the previous notice_handler_cb; locate the switch
around ASYNC_RESET_SESSION_END / ASYNC_RESET_SESSION_FAILED /
ASYNC_RESET_SESSION_TIMEOUT in PgSQL_Connection and add the same
PQsetNoticeReceiver call before calling NEXT_IMMEDIATE or returning when
is_error_present()/failure/timeout occur.
lib/PgSQL_Session.cpp (1)

5695-5696: ⚠️ Potential issue | 🟠 Major

Use bounded formatting for std::string_view in log output.

Line 5695-5696 passes command.data() to %s where command is a std::string_view. Since std::string_view data is not guaranteed to be null-terminated, this causes unbounded memory access. Use %.*s with command.size() instead (similar to the correct usage at line 5685 in the same function).

🛠️ Proposed fix
-	proxy_info("Received command '%s'%s. Switching to Fast Forward mode (Session Type:0x%02X)\n",
-		command.data(), client_info.c_str(), session_type);
+	proxy_info("Received command '%.*s'%s. Switching to Fast Forward mode (Session Type:0x%02X)\n",
+		static_cast<int>(command.size()), command.data(), client_info.c_str(), session_type);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/PgSQL_Session.cpp` around lines 5695 - 5696, The proxy_info call using
proxy_info("Received command '%s'%s. Switching to Fast Forward mode (Session
Type:0x%02X)\n", command.data(), client_info.c_str(), session_type) is unsafe
because command is a std::string_view and may not be null-terminated; change the
format to use a bounded string specifier (%.*s) and pass
static_cast<int>(command.size()) before command.data(), keeping
client_info.c_str() and session_type as-is so the logged command is safely
length-limited (refer to proxy_info, command, client_info, session_type).
♻️ Duplicate comments (1)
test/tap/tests/pgsql-test_malformed_packet-t.cpp (1)

144-149: ⚠️ Potential issue | 🟠 Major

Graceful-handling assertions still accept every recv() outcome.

At Line 144 and Line 613, the logic currently allows both error/timeout and success paths, so the assertion is effectively non-discriminating.

Proposed fix
-    bool connection_closed = (bytes_received <= 0);
-    bool got_response = (bytes_received > 0);
-    bool handled_gracefully = connection_closed || got_response;
+    const bool recv_failed = (bytes_received < 0);
+    const bool connection_closed = (bytes_received == 0);
+    const bool got_response = (bytes_received > 0);
+    const bool handled_gracefully = !recv_failed && (connection_closed || got_response);
@@
-    bool connection_closed = (bytes_received <= 0);
+    const bool recv_failed = (bytes_received < 0);
+    const bool connection_closed = (bytes_received == 0);
     bool got_error_response = (bytes_received > 0 && buffer[0] == 'E');
     bool got_other_response = (bytes_received > 0 && buffer[0] != 'E');
@@
-    bool handled_gracefully = connection_closed || got_error_response || got_other_response;
+    const bool handled_gracefully = !recv_failed &&
+                                    (connection_closed || got_error_response || got_other_response);

Also applies to: 613-621

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/tap/tests/pgsql-test_malformed_packet-t.cpp` around lines 144 - 149, The
current combined boolean handled_gracefully (using connection_closed,
got_response) makes the test accept every recv() outcome; replace the single
ok(handled_gracefully, ...) check with an explicit branch that asserts the exact
expected outcome: if bytes_received <= 0 then call ok(connection_closed, "%s:
Malformed packet caused connection to close (received: %ld bytes)",
test_name.c_str(), (long)bytes_received); else call ok(got_response, "%s:
Malformed packet produced a response (received: %ld bytes)", test_name.c_str(),
(long)bytes_received); this uses the existing variables connection_closed,
got_response, bytes_received and test_name and ensures the test distinguishes
the two paths rather than accepting all outcomes.
🧹 Nitpick comments (1)
lib/PgSQL_Session.cpp (1)

5993-6000: Consider extracting duplicated retry-policy setup into one helper.

The same query_retries_on_failure block is repeated in Parse/Describe/Execute post-sync paths. A small helper would reduce drift risk.

♻️ Refactor sketch
+inline void PgSQL_Session::apply_query_retry_policy(PgSQL_Data_Stream* server_myds) {
+	server_myds->query_retries_on_failure = pgsql_thread___query_retries_on_failure;
+	if (qpo && qpo->retries >= 0) {
+		server_myds->query_retries_on_failure = qpo->retries;
+	}
+}
-	// set query retries
-	mybe->server_myds->query_retries_on_failure = pgsql_thread___query_retries_on_failure;
-	// if a number of retries is set in mysql_query_rules, that takes priority
-	if (qpo) {
-		if (qpo->retries >= 0) {
-			mybe->server_myds->query_retries_on_failure = qpo->retries;
-		}
-	}
+	apply_query_retry_policy(mybe->server_myds);

Also applies to: 6147-6154, 6435-6442

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/PgSQL_Session.cpp` around lines 5993 - 6000, Extract the repeated logic
that sets mybe->server_myds->query_retries_on_failure into a small helper (e.g.,
set_query_retries_on_failure or apply_query_retry_policy) which takes the
backend/session struct and the qpo (query options) pointer, defaults to
pgsql_thread___query_retries_on_failure and overrides with qpo->retries when qpo
is non-null and qpo->retries >= 0; then replace the three duplicated blocks in
the Parse, Describe and Execute post-sync paths with calls to that helper using
the existing symbols mybe, mybe->server_myds,
pgsql_thread___query_retries_on_failure and qpo to avoid drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/tap/tests/pgsql-test_malformed_packet-t.cpp`:
- Around line 482-490: The header reads use inconsistent recv() checks: the
1-byte type read checks "n <= 0" while the 4-byte length read checks "n != 4",
which can allow short reads to slip through; update both header reads to use the
same exact-read pattern used for the message body (the recv_exact-style loop) so
they loop until the expected number of bytes are received or an error/EOF
occurs. Specifically, replace the single-call recv(sock, type_buf, 1, 0) and
recv(sock, len_buf, 4, 0) with a small helper-style loop (or call an existing
recv_exact) that attempts to read exactly 1 and exactly 4 bytes respectively,
returning false on short read or error; make this change where type_buf and
len_buf are read so behavior matches the body-read loop.

---

Outside diff comments:
In `@lib/PgSQL_Connection.cpp`:
- Around line 921-931: The switch handling reset states fails to restore the
notice receiver except in ASYNC_RESET_SESSION_END; ensure
PQsetNoticeReceiver(pgsql_conn, &PgSQL_Connection::unhandled_notice_cb, this) is
invoked on the failure and timeout paths as well (i.e., in the
ASYNC_RESET_SESSION_FAILED and ASYNC_RESET_SESSION_TIMEOUT handling) so the
connection doesn't retain the previous notice_handler_cb; locate the switch
around ASYNC_RESET_SESSION_END / ASYNC_RESET_SESSION_FAILED /
ASYNC_RESET_SESSION_TIMEOUT in PgSQL_Connection and add the same
PQsetNoticeReceiver call before calling NEXT_IMMEDIATE or returning when
is_error_present()/failure/timeout occur.

In `@lib/PgSQL_Session.cpp`:
- Around line 5695-5696: The proxy_info call using proxy_info("Received command
'%s'%s. Switching to Fast Forward mode (Session Type:0x%02X)\n", command.data(),
client_info.c_str(), session_type) is unsafe because command is a
std::string_view and may not be null-terminated; change the format to use a
bounded string specifier (%.*s) and pass static_cast<int>(command.size()) before
command.data(), keeping client_info.c_str() and session_type as-is so the logged
command is safely length-limited (refer to proxy_info, command, client_info,
session_type).

---

Duplicate comments:
In `@test/tap/tests/pgsql-test_malformed_packet-t.cpp`:
- Around line 144-149: The current combined boolean handled_gracefully (using
connection_closed, got_response) makes the test accept every recv() outcome;
replace the single ok(handled_gracefully, ...) check with an explicit branch
that asserts the exact expected outcome: if bytes_received <= 0 then call
ok(connection_closed, "%s: Malformed packet caused connection to close
(received: %ld bytes)", test_name.c_str(), (long)bytes_received); else call
ok(got_response, "%s: Malformed packet produced a response (received: %ld
bytes)", test_name.c_str(), (long)bytes_received); this uses the existing
variables connection_closed, got_response, bytes_received and test_name and
ensures the test distinguishes the two paths rather than accepting all outcomes.

---

Nitpick comments:
In `@lib/PgSQL_Session.cpp`:
- Around line 5993-6000: Extract the repeated logic that sets
mybe->server_myds->query_retries_on_failure into a small helper (e.g.,
set_query_retries_on_failure or apply_query_retry_policy) which takes the
backend/session struct and the qpo (query options) pointer, defaults to
pgsql_thread___query_retries_on_failure and overrides with qpo->retries when qpo
is non-null and qpo->retries >= 0; then replace the three duplicated blocks in
the Parse, Describe and Execute post-sync paths with calls to that helper using
the existing symbols mybe, mybe->server_myds,
pgsql_thread___query_retries_on_failure and qpo to avoid drift.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1767938 and a2eb295.

📒 Files selected for processing (4)
  • lib/Admin_Handler.cpp
  • lib/PgSQL_Connection.cpp
  • lib/PgSQL_Session.cpp
  • test/tap/tests/pgsql-test_malformed_packet-t.cpp

Comment thread test/tap/tests/pgsql-test_malformed_packet-t.cpp Outdated
@rahim-kanji
rahim-kanji force-pushed the v3.0_strengthen-pgsql-protocol-validation branch from a2eb295 to 8c9af56 Compare March 1, 2026 21:19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/PgSQL_Session.cpp (1)

5695-5697: ⚠️ Potential issue | 🟠 Major

Use bounded formatting for std::string_view in logs.

At line 5695, command.data() is printed with %s, but command is a std::string_view which is not guaranteed to be null-terminated. This can cause buffer over-read. The same function already demonstrates the correct approach (line 5683) using %.*s with the size.

🛠️ Proposed fix
-	proxy_info("Received command '%s'%s. Switching to Fast Forward mode (Session Type:0x%02X)\n",
-		command.data(), client_info.c_str(), session_type);
+	proxy_info("Received command '%.*s'%s. Switching to Fast Forward mode (Session Type:0x%02X)\n",
+		(int)command.size(), command.data(), client_info.c_str(), session_type);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/PgSQL_Session.cpp` around lines 5695 - 5697, The log call in proxy_info
uses command.data() with "%s" but command is a std::string_view and may not be
null-terminated; change the format to use bounded string formatting "%.*s" and
pass (int)command.size() before command.data() so the output is safe (e.g.,
update the proxy_info invocation that currently formats command, client_info and
session_type in the block that sets session_fast_forward). Ensure you keep
client_info.c_str() and session_type as before and only replace the "%s" usage
for the command argument.
♻️ Duplicate comments (1)
test/tap/tests/pgsql-test_malformed_packet-t.cpp (1)

144-149: ⚠️ Potential issue | 🟠 Major

handled_gracefully is still tautological in both phases.

Line 146 and Line 627 combine predicates that cover every recv() outcome, so these assertions cannot fail and don’t validate malformed-packet handling semantics.

🔧 Proposed fix
-    bool connection_closed = (bytes_received <= 0);
-    bool got_response = (bytes_received > 0);
-    bool handled_gracefully = connection_closed || got_response;
+    bool connection_closed = (bytes_received == 0);
+    bool timeout = (bytes_received < 0 && (errno == EAGAIN || errno == EWOULDBLOCK));
+    bool got_error_response = (bytes_received > 0 && buffer[0] == 'E');
+    bool handled_gracefully = connection_closed || timeout || got_error_response;
@@
-    bool connection_closed = (bytes_received <= 0);
-    bool got_error_response = (bytes_received > 0 && buffer[0] == 'E');
-    bool got_other_response = (bytes_received > 0 && buffer[0] != 'E');
-    bool handled_gracefully = connection_closed || got_error_response || got_other_response;
+    bool connection_closed = (bytes_received == 0);
+    bool timeout = (bytes_received < 0 && (errno == EAGAIN || errno == EWOULDBLOCK));
+    bool got_protocol_response = (bytes_received > 0);
+    bool handled_gracefully = connection_closed || timeout || got_protocol_response;

Also applies to: 621-631

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/tap/tests/pgsql-test_malformed_packet-t.cpp` around lines 144 - 149, The
current assertion uses handled_gracefully = connection_closed || got_response
which is tautological and can never fail; instead make the assertion check the
phase-specific expected outcome: replace the combined predicate with an explicit
check depending on the test phase (e.g., if this is the "expect-close" phase
assert connection_closed (bytes_received <= 0), and if this is the
"expect-response" phase assert got_response (bytes_received > 0)); update the
ok(...) call to reflect the chosen expectation (use test_name.c_str() and
bytes_received in the message) and remove the always-true handled_gracefully
variable so the test actually validates malformed-packet handling.
🧹 Nitpick comments (3)
lib/PgSQL_Connection.cpp (1)

1216-1234: Minor: Log message may lack context in resync mode.

When is_resync is true and flush fails, set_error_from_PQerrorMessage() is skipped, but the proxy_error on line 2231 still calls get_error_code_with_message(). This may produce a less informative log since the error info wasn't populated.

Consider logging PQerrorMessage(pgsql_conn) directly in resync mode to capture the actual failure reason:

🔧 Suggested improvement
 	else {
 		if (!is_resync) {
 			set_error_from_PQerrorMessage();
+			proxy_error("Failed to flush data to backend. %s\n", get_error_code_with_message().c_str());
 		} else {
 			resync_failed = true;
+			proxy_error("Failed to flush data to backend during resync. %s\n", PQerrorMessage(pgsql_conn));
 		}
-		proxy_error("Failed to flush data to backend. %s\n", get_error_code_with_message().c_str());
 		async_exit_status = PG_EVENT_NONE;
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/PgSQL_Connection.cpp` around lines 1216 - 1234, The flush path in
PgSQL_Connection::flush skips set_error_from_PQerrorMessage() when is_resync is
true but still calls proxy_error with get_error_code_with_message(), which can
be empty; update the failure branch so that when is_resync is true you capture
the raw PQerrorMessage(pgsql_conn) (or include it in the proxy_error call)
instead of relying solely on get_error_code_with_message(), and ensure
resync_failed remains set; reference PgSQL_Connection::flush, the is_resync
flag, set_error_from_PQerrorMessage(), proxy_error(),
get_error_code_with_message(), PQerrorMessage(), and resync_failed to locate and
implement this change.
lib/PgSQL_Session.cpp (2)

5681-5687: Add a defensive null guard before reading PSarrayIN->len.

At Line 5683, client_myds->PSarrayIN is dereferenced directly. Given this PR’s hardening goals, a guard here avoids a crash if queue initialization is unexpectedly missing on an edge path.

🛡️ Proposed hardening
-	if (client_myds->PSarrayIN->len) {
+	if (client_myds && client_myds->PSarrayIN && client_myds->PSarrayIN->len) {
 		proxy_error("Cannot switch to fast forward mode: unexpected pending packets in client_myds->PSarrayIN (len=%u). Command: %.*s\n",
 			client_myds->PSarrayIN->len, (int)command.size(), command.data());
 		return false;
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/PgSQL_Session.cpp` around lines 5681 - 5687, Add a defensive null check
for client_myds->PSarrayIN before dereferencing its len in the fast-forward
switch path: verify that client_myds and client_myds->PSarrayIN are non-null
(the check around the existing proxy_error/return false block inside the same
function in PgSQL_Session.cpp), and if PSarrayIN is null, log a clear
proxy_error indicating missing/NULL PSarrayIN and return false; otherwise keep
the existing len check and behavior.

5993-6000: Retry policy assignment is duplicated in three post-sync paths.

The same query_retries_on_failure initialization logic is repeated in parse/describe/execute handlers. Extracting to one helper will reduce drift risk and keep retry behavior consistent.

Also applies to: 6147-6154, 6434-6441

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/PgSQL_Session.cpp` around lines 5993 - 6000, The repeated initialization
of mybe->server_myds->query_retries_on_failure from
pgsql_thread___query_retries_on_failure with an override from qpo->retries
(mysql_query_rules) is duplicated in the parse/describe/execute post-sync paths;
refactor by creating a single helper (e.g., a function like
set_query_retries_on_failure(SessionBackend* mybe, QueryParserOptions* qpo) or
similar) that encapsulates: set to pgsql_thread___query_retries_on_failure, then
if qpo && qpo->retries >= 0 set to qpo->retries, and call that helper from the
three locations (currently containing the duplicated blocks around
mybe->server_myds->query_retries_on_failure) to ensure consistent behavior and
reduce duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@lib/PgSQL_Session.cpp`:
- Around line 5695-5697: The log call in proxy_info uses command.data() with
"%s" but command is a std::string_view and may not be null-terminated; change
the format to use bounded string formatting "%.*s" and pass (int)command.size()
before command.data() so the output is safe (e.g., update the proxy_info
invocation that currently formats command, client_info and session_type in the
block that sets session_fast_forward). Ensure you keep client_info.c_str() and
session_type as before and only replace the "%s" usage for the command argument.

---

Duplicate comments:
In `@test/tap/tests/pgsql-test_malformed_packet-t.cpp`:
- Around line 144-149: The current assertion uses handled_gracefully =
connection_closed || got_response which is tautological and can never fail;
instead make the assertion check the phase-specific expected outcome: replace
the combined predicate with an explicit check depending on the test phase (e.g.,
if this is the "expect-close" phase assert connection_closed (bytes_received <=
0), and if this is the "expect-response" phase assert got_response
(bytes_received > 0)); update the ok(...) call to reflect the chosen expectation
(use test_name.c_str() and bytes_received in the message) and remove the
always-true handled_gracefully variable so the test actually validates
malformed-packet handling.

---

Nitpick comments:
In `@lib/PgSQL_Connection.cpp`:
- Around line 1216-1234: The flush path in PgSQL_Connection::flush skips
set_error_from_PQerrorMessage() when is_resync is true but still calls
proxy_error with get_error_code_with_message(), which can be empty; update the
failure branch so that when is_resync is true you capture the raw
PQerrorMessage(pgsql_conn) (or include it in the proxy_error call) instead of
relying solely on get_error_code_with_message(), and ensure resync_failed
remains set; reference PgSQL_Connection::flush, the is_resync flag,
set_error_from_PQerrorMessage(), proxy_error(), get_error_code_with_message(),
PQerrorMessage(), and resync_failed to locate and implement this change.

In `@lib/PgSQL_Session.cpp`:
- Around line 5681-5687: Add a defensive null check for client_myds->PSarrayIN
before dereferencing its len in the fast-forward switch path: verify that
client_myds and client_myds->PSarrayIN are non-null (the check around the
existing proxy_error/return false block inside the same function in
PgSQL_Session.cpp), and if PSarrayIN is null, log a clear proxy_error indicating
missing/NULL PSarrayIN and return false; otherwise keep the existing len check
and behavior.
- Around line 5993-6000: The repeated initialization of
mybe->server_myds->query_retries_on_failure from
pgsql_thread___query_retries_on_failure with an override from qpo->retries
(mysql_query_rules) is duplicated in the parse/describe/execute post-sync paths;
refactor by creating a single helper (e.g., a function like
set_query_retries_on_failure(SessionBackend* mybe, QueryParserOptions* qpo) or
similar) that encapsulates: set to pgsql_thread___query_retries_on_failure, then
if qpo && qpo->retries >= 0 set to qpo->retries, and call that helper from the
three locations (currently containing the duplicated blocks around
mybe->server_myds->query_retries_on_failure) to ensure consistent behavior and
reduce duplication.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a2eb295 and 8c9af56.

📒 Files selected for processing (4)
  • lib/Admin_Handler.cpp
  • lib/PgSQL_Connection.cpp
  • lib/PgSQL_Session.cpp
  • test/tap/tests/pgsql-test_malformed_packet-t.cpp

Comment on lines +131 to +132
ssize_t bytes_sent = send(sock, data.data(), data.size(), 0);
if (bytes_sent < 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# First, let's check if the file exists and get its total line count
file="test/tap/tests/pgsql-test_malformed_packet-t.cpp"
if [ -f "$file" ]; then
    echo "=== File info ==="
    wc -l "$file"
    echo
fi

# Read the specific lines mentioned in the review
echo "=== Lines 131-132 ==="
sed -n '128,135p' "$file"
echo

echo "=== Lines 540-541 ==="
sed -n '537,544p' "$file"
echo

echo "=== Lines 557-558 ==="
sed -n '554,561p' "$file"
echo

echo "=== Lines 603-604 ==="
sed -n '600,607p' "$file"

Repository: sysown/proxysql

Length of output: 1382


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="test/tap/tests/pgsql-test_malformed_packet-t.cpp"

echo "=== Search for send_exact or similar helpers ==="
rg -n --type=cpp 'send_exact|while.*send|short.write' "$file" || echo "No matches found"
echo

echo "=== All send() calls in the file ==="
rg -n --type=cpp '\bsend\s*\(' "$file"
echo

echo "=== Check file for any short-write checks ==="
rg -n --type=cpp 'sent.*size|bytes_sent.*size|!=' "$file" | head -20

Repository: sysown/proxysql

Length of output: 960


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="test/tap/tests/pgsql-test_malformed_packet-t.cpp"

echo "=== Context around lines 131-132 and 132-140 (after send) ==="
sed -n '130,145p' "$file"
echo

echo "=== Context around lines 603-604 and after (showing what happens with 'sent' var) ==="
sed -n '602,620p' "$file"
echo

echo "=== Check if there are any retry mechanisms or loops for send ==="
rg -n --type=cpp -B2 -A5 'while.*\(|for.*\(' "$file" | head -30

Repository: sysown/proxysql

Length of output: 2936


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="test/tap/tests/pgsql-test_malformed_packet-t.cpp"

echo "=== Check packet building to understand payload expectations ==="
rg -n --type=cpp 'build_.*packet|malformed_data|startup|password_pkt' "$file" | head -30

Repository: sysown/proxysql

Length of output: 1650


Add short-write handling to all send() calls to prevent truncated test payloads.

Socket writes only check for errors (< 0) but ignore partial writes. If send() returns fewer bytes than requested, the truncated packet proceeds to the server, compromising the test's malformed packet coverage and causing flaky behavior depending on socket buffer conditions.

Affected locations: 131-132, 540-541, 557-558, 603-604.

Implement a helper like send_exact() that loops until all bytes are sent or an error occurs:

Proposed fix
+bool send_exact(int sock, const void* buf, size_t len) {
+    const uint8_t* ptr = static_cast<const uint8_t*>(buf);
+    size_t sent = 0;
+    while (sent < len) {
+        ssize_t n = send(sock, ptr + sent, len - sent, 0);
+        if (n <= 0) return false;
+        sent += static_cast<size_t>(n);
+    }
+    return true;
+}
@@
-    ssize_t bytes_sent = send(sock, data.data(), data.size(), 0);
-    if (bytes_sent < 0) {
+    if (!send_exact(sock, data.data(), data.size())) {
@@
-    if (send(sock, startup.data(), startup.size(), 0) < 0) {
+    if (!send_exact(sock, startup.data(), startup.size())) {
@@
-    if (send(sock, password_pkt.data(), password_pkt.size(), 0) < 0) {
+    if (!send_exact(sock, password_pkt.data(), password_pkt.size())) {
@@
-    ssize_t sent = send(sock, malformed_data.data(), malformed_data.size(), 0);
-    if (sent < 0) {
+    if (!send_exact(sock, malformed_data.data(), malformed_data.size())) {

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
lib/PgSQL_Extended_Query_Message.cpp (1)

178-180: Clarify the statement-type comment to match wire format.

The guard is correct, but Line 178 says “null-terminated string” for statement type. This field is a single byte ('S'/'P'), so the comment is misleading.

Suggested comment-only diff
-	// Validate remaining length for statement type (at least 1 byte for null-terminated string)
+	// Validate remaining length for statement type (exactly 1 byte)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/PgSQL_Extended_Query_Message.cpp` around lines 178 - 180, The comment
about the "statement type" is incorrect: that field is a single byte ('S' or
'P'), not a null-terminated string. Update the comment near the guard that
checks "if (offset >= pkt_len)" to state that the code is validating at least
one byte remains for the single-byte statement type (use variable names offset
and pkt_len), and mention valid values ('S'/'P') to clarify the wire format.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@lib/PgSQL_Extended_Query_Message.cpp`:
- Around line 178-180: The comment about the "statement type" is incorrect: that
field is a single byte ('S' or 'P'), not a null-terminated string. Update the
comment near the guard that checks "if (offset >= pkt_len)" to state that the
code is validating at least one byte remains for the single-byte statement type
(use variable names offset and pkt_len), and mention valid values ('S'/'P') to
clarify the wire format.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8c9af56 and ed8890b.

📒 Files selected for processing (1)
  • lib/PgSQL_Extended_Query_Message.cpp

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
test/tap/tests/pgsql-test_malformed_packet-t.cpp (2)

131-132: ⚠️ Potential issue | 🟠 Major

Handle partial send() writes; current checks can silently transmit truncated packets.

All four call sites only check < 0. A short positive write is currently treated as success, which can invalidate malformed-packet assertions.

Proposed fix
+bool send_exact(int sock, const void* buf, size_t len) {
+    const uint8_t* ptr = static_cast<const uint8_t*>(buf);
+    size_t sent = 0;
+    while (sent < len) {
+        ssize_t n = send(sock, ptr + sent, len - sent, 0);
+        if (n <= 0) return false;
+        sent += static_cast<size_t>(n);
+    }
+    return true;
+}
@@
-    ssize_t bytes_sent = send(sock, data.data(), data.size(), 0);
-    if (bytes_sent < 0) {
+    if (!send_exact(sock, data.data(), data.size())) {
@@
-    if (send(sock, startup.data(), startup.size(), 0) < 0) {
+    if (!send_exact(sock, startup.data(), startup.size())) {
@@
-    if (send(sock, password_pkt.data(), password_pkt.size(), 0) < 0) {
+    if (!send_exact(sock, password_pkt.data(), password_pkt.size())) {
@@
-    ssize_t sent = send(sock, malformed_data.data(), malformed_data.size(), 0);
-    if (sent < 0) {
+    if (!send_exact(sock, malformed_data.data(), malformed_data.size())) {

Also applies to: 543-544, 560-561, 606-607

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/tap/tests/pgsql-test_malformed_packet-t.cpp` around lines 131 - 132, The
send() calls (using variables sock, data.data(), data.size(), and bytes_sent)
only check for bytes_sent < 0 and therefore treat short positive writes as full
success, which can produce truncated packets; update each call site to loop
until all bytes are written (track an offset/remaining bytes and call send
repeatedly) or treat any bytes_sent != (ssize_t)remaining as an error, ensuring
you adjust the buffer pointer (data.data() + offset) and remaining length
accordingly; apply this fix to all four sites referenced (the sends at the
current location and the ones around the other mentioned line ranges).

603-631: ⚠️ Potential issue | 🟠 Major

Phase 2 assertion can pass before malformed packet impact is observed.

After AuthenticationOk, the server may still emit startup completion messages (S/K/Z). Current logic sends malformed data immediately and accepts any bytes_received > 0, so unrelated protocol traffic can produce false positives.

Proposed fix
     // Authentication successful - now we have an authenticated connection
+    // Drain until ReadyForQuery to ensure subsequent response corresponds to malformed packet
+    bool ready_for_query = false;
+    for (int i = 0; i < 16; ++i) {
+        char msg_type = 0;
+        std::vector<uint8_t> msg_data;
+        if (!read_message(sock, msg_type, msg_data)) break;
+        if (msg_type == 'Z') {
+            ready_for_query = true;
+            break;
+        }
+        if (msg_type == 'E') break;
+    }
+    if (!ready_for_query) {
+        close(sock);
+        ok(0, "%s: Did not reach ReadyForQuery before malformed packet", test_name.c_str());
+        return;
+    }

     // Step 5: Send the malformed packet on the authenticated connection
@@
-    bool got_protocol_response = (bytes_received > 0);
-    bool handled_gracefully = connection_closed || timeout || got_protocol_response;
+    bool got_error_response = (bytes_received > 0 && buffer[0] == 'E');
+    bool handled_gracefully = connection_closed || timeout || got_error_response;
@@
-    ok(handled_gracefully, "%s: Phase 2 malformed packet sent (closed=%d, timeout=%d, response=%d, first=0x%02X)",
-       test_name.c_str(), (int)connection_closed, (int)timeout, (int)got_protocol_response,
+    ok(handled_gracefully, "%s: Phase 2 malformed packet handled (closed=%d, timeout=%d, error=%d, first=0x%02X)",
+       test_name.c_str(), (int)connection_closed, (int)timeout, (int)got_error_response,
        bytes_received > 0 ? (unsigned char)buffer[0] : 0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/tap/tests/pgsql-test_malformed_packet-t.cpp` around lines 603 - 631, The
current Phase 2 check (using recv once and treating any bytes_received>0 as
success) can be a false positive because post-authentication protocol noise
(ParameterStatus 'S', BackendKeyData 'K', ReadyForQuery 'Z') may arrive before
the malformed packet is processed; update the logic around the recv loop that
sends malformed_data and reads responses (variables/functions: malformed_data,
send, recv, buffer, bytes_received, connection_closed, timeout,
got_protocol_response, handled_gracefully) to loop reading until either (a) the
connection is closed, (b) a timeout elapses, or (c) a definite error/errant
message indicating the malformed packet was handled (inspect the first byte of
each message in buffer[0] and ignore ordinary startup/post-auth messages like
'S','K','Z' while continuing to wait), and only set handled_gracefully true when
you observe connection close, timeout, or an actual error response for the
malformed packet rather than any incidental protocol message; ensure the loop
has a bounded overall timeout to avoid hanging.
🧹 Nitpick comments (1)
lib/PgSQL_Session.cpp (1)

5993-6000: Extract repeated query-retry policy setup into one helper.

The same retry-selection block appears in three post-sync handlers. Centralizing it will reduce drift risk.

♻️ Suggested refactor
+inline void PgSQL_Session::apply_query_retry_policy(PgSQL_Data_Stream* myds) {
+    myds->query_retries_on_failure = pgsql_thread___query_retries_on_failure;
+    if (qpo && qpo->retries >= 0) {
+        myds->query_retries_on_failure = qpo->retries;
+    }
+}
-    // set query retries
-    mybe->server_myds->query_retries_on_failure = pgsql_thread___query_retries_on_failure;
-    // if a number of retries is set in mysql_query_rules, that takes priority
-    if (qpo) {
-        if (qpo->retries >= 0) {
-            mybe->server_myds->query_retries_on_failure = qpo->retries;
-        }
-    }
+    apply_query_retry_policy(mybe->server_myds);

Also applies to: 6147-6154, 6434-6441

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/PgSQL_Session.cpp` around lines 5993 - 6000, Extract the repeated
retry-selection block into a single helper (e.g., a static function
apply_query_retries_policy(MyBackend* mybe, QueryOptions* qpo) or similar) that
sets mybe->server_myds->query_retries_on_failure to
pgsql_thread___query_retries_on_failure by default and overrides it when qpo &&
qpo->retries >= 0; then replace the duplicate blocks found in the three
post-sync handlers with a call to this helper (referencing mybe, qpo and the
field server_myds->query_retries_on_failure and the constant
pgsql_thread___query_retries_on_failure).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@test/tap/tests/pgsql-test_malformed_packet-t.cpp`:
- Around line 131-132: The send() calls (using variables sock, data.data(),
data.size(), and bytes_sent) only check for bytes_sent < 0 and therefore treat
short positive writes as full success, which can produce truncated packets;
update each call site to loop until all bytes are written (track an
offset/remaining bytes and call send repeatedly) or treat any bytes_sent !=
(ssize_t)remaining as an error, ensuring you adjust the buffer pointer
(data.data() + offset) and remaining length accordingly; apply this fix to all
four sites referenced (the sends at the current location and the ones around the
other mentioned line ranges).
- Around line 603-631: The current Phase 2 check (using recv once and treating
any bytes_received>0 as success) can be a false positive because
post-authentication protocol noise (ParameterStatus 'S', BackendKeyData 'K',
ReadyForQuery 'Z') may arrive before the malformed packet is processed; update
the logic around the recv loop that sends malformed_data and reads responses
(variables/functions: malformed_data, send, recv, buffer, bytes_received,
connection_closed, timeout, got_protocol_response, handled_gracefully) to loop
reading until either (a) the connection is closed, (b) a timeout elapses, or (c)
a definite error/errant message indicating the malformed packet was handled
(inspect the first byte of each message in buffer[0] and ignore ordinary
startup/post-auth messages like 'S','K','Z' while continuing to wait), and only
set handled_gracefully true when you observe connection close, timeout, or an
actual error response for the malformed packet rather than any incidental
protocol message; ensure the loop has a bounded overall timeout to avoid
hanging.

---

Nitpick comments:
In `@lib/PgSQL_Session.cpp`:
- Around line 5993-6000: Extract the repeated retry-selection block into a
single helper (e.g., a static function apply_query_retries_policy(MyBackend*
mybe, QueryOptions* qpo) or similar) that sets
mybe->server_myds->query_retries_on_failure to
pgsql_thread___query_retries_on_failure by default and overrides it when qpo &&
qpo->retries >= 0; then replace the duplicate blocks found in the three
post-sync handlers with a call to this helper (referencing mybe, qpo and the
field server_myds->query_retries_on_failure and the constant
pgsql_thread___query_retries_on_failure).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ed8890b and 5bf5680.

📒 Files selected for processing (3)
  • lib/PgSQL_Extended_Query_Message.cpp
  • lib/PgSQL_Session.cpp
  • test/tap/tests/pgsql-test_malformed_packet-t.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/PgSQL_Extended_Query_Message.cpp

…cing

* Fix test assertions to properly validate malformed packet handling
@rahim-kanji
rahim-kanji force-pushed the v3.0_strengthen-pgsql-protocol-validation branch from 5bf5680 to d0ad4b6 Compare March 2, 2026 08:24
@sonarqubecloud

sonarqubecloud Bot commented Mar 2, 2026

Copy link
Copy Markdown

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