Skip to content

fix(session): avoid double-free of stmt_meta pkt in LargePacket (#5639)#5808

Merged
renecannao merged 2 commits into
v3.0-260523from
issue_5639
May 23, 2026
Merged

fix(session): avoid double-free of stmt_meta pkt in LargePacket (#5639)#5808
renecannao merged 2 commits into
v3.0-260523from
issue_5639

Conversation

@renecannao

@renecannao renecannao commented May 23, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Fixes the SIGSEGV reported in ProxySQL Crash Error: signal 11: #5639 — a double-free of the COM_STMT_EXECUTE packet buffer in MySQL_Session::handler_WCD_SS_MCQ_qpo_LargePacket() when an oversized binary STMT_EXECUTE packet trips the mysql-max_allowed_packet check.
  • Adds a TAP regression test (reg_test_5639_stmt_execute_max_allowed_packet-t) registered in mysql84-g6 and mysql95-g1.

Root cause

MySQL_Protocol::get_binds_from_pkt() aliases stmt_meta->pkt = pkt.ptr (see the long-standing FIXME at lib/MySQL_Protocol.cpp:2873). Inside LargePacket, RequestEnd(NULL, …) calls Query_Info::end(), which free()s stmt_meta->pkt (the fix introduced for bug #796 at lib/MySQL_Session.cpp:455). The handler then calls l_free(pkt->size, pkt->ptr) — which is free(pkt->ptr) per the macro in include/proxysql_mem.h:51 — on the same buffer. Double-free.

The reporter's +0x10e crash offset in v3.0.7 lands on this exact l_free call site. With jemalloc the double-free silently corrupts the arena and only crashes intermittently under stress (consistent with the KILL CONNECTION churn the reporter sees alongside the crashes). Under ASAN the abort is deterministic and the two free chains match the reporter's stack exactly.

The fix

lib/MySQL_Session.cpp in handler_WCD_SS_MCQ_qpo_LargePacket:

```cpp
const bool stmt_meta_owns_pkt =
(CurrentQuery.stmt_meta != NULL && CurrentQuery.stmt_meta->pkt == pkt->ptr);
RequestEnd(NULL, 1153, errmsg.c_str());
if (!stmt_meta_owns_pkt) {
l_free(pkt->size, pkt->ptr);
}
```

Captures the alias before `RequestEnd()` (which clears `stmt_meta`) and skips the second free when the alias was present. For the COM_QUERY path through LargePacket, `stmt_meta` is always NULL (it is only set inside the STMT_EXECUTE handler at `:3587`), so the previous behaviour is preserved there.

Test plan

  • New TAP test `reg_test_5639_stmt_execute_max_allowed_packet-t` passes against `origin/v3.0` head (release build): 8/8
  • Same test passes against tag `v3.0.7` (release build): 8/8 — confirms minimal repro doesn't crash via stock release because jemalloc hides the corruption
  • Same test, build with `NOJEMALLOC=1 WITHASAN=1`, WITHOUT this fix: ASAN `double-free` abort at `lib/MySQL_Session.cpp:6377`, freed-by at `Query_Info::end():455`, allocation at `MySQL_Data_Stream::buffer2array():1329` — exactly matches the bug shape
  • Same test, ASAN debug build, WITH this fix: 8/8 pass, no ASAN report
  • Same test, ASAN debug build, WITH this fix, 50 iterations in a tight loop: 0 failures, no ASAN reports, proxysql process stable
  • CI run on this branch

Summary by CodeRabbit

  • Bug Fixes

    • Prevented a crash when executing prepared statements that send packets larger than the configured max_allowed_packet; the frontend connection now remains usable and subsequent connections are unaffected.
  • Tests

    • Added a regression test verifying oversized prepared-statement packets produce the expected error and that connections stay usable; test was added to CI group mappings.

Review Change Stack

When handler_WCD_SS_MCQ_qpo_LargePacket() is reached via the
COM_STMT_EXECUTE path, MySQL_Protocol::get_binds_from_pkt() has
already aliased stmt_meta->pkt to pkt->ptr (see the FIXME at
MySQL_Protocol.cpp:2873). The subsequent
RequestEnd() -> Query_Info::end() free()s stmt_meta->pkt (the
fix introduced for bug #796 at MySQL_Session.cpp:455), and then
LargePacket's own l_free(pkt->size, pkt->ptr) frees the same
buffer a second time.

With jemalloc this silently corrupts the arena and surfaces later
as the SIGSEGV reported in #5639, with the crashing frame landing
on the l_free call site (+0x10e into LargePacket). Under ASAN the
double-free aborts deterministically with a backtrace whose two
free() chains match the report exactly.

Fix: capture whether CurrentQuery.stmt_meta aliases pkt before
calling RequestEnd() (which clears stmt_meta), and skip the
second free when the alias was present. For the COM_QUERY path
through LargePacket, stmt_meta is always NULL, so the previous
behaviour is preserved there.

Also add reg_test_5639_stmt_execute_max_allowed_packet-t which
exercises the STMT_EXECUTE > mysql-max_allowed_packet path and
asserts the session and process survive.
@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a2eeb867-5ffe-49da-9438-9752daff8218

📥 Commits

Reviewing files that changed from the base of the PR and between b3f7d29 and d8c32f1.

📒 Files selected for processing (1)
  • test/tap/groups/groups.json
📜 Recent review details
🔇 Additional comments (1)
test/tap/groups/groups.json (1)

264-264: Confirm corresponding regression test file exists

test/tap/groups/groups.json line 264 maps "reg_test_5639_stmt_execute_max_allowed_packet-t" to ["mysql84-g6","mysql95-g1", and the corresponding test file test/tap/tests/reg_test_5639_stmt_execute_max_allowed_packet-t.cpp is present in the repository.


📝 Walkthrough

Walkthrough

Adds a guard in the large-packet error handler to avoid double-free when COM_STMT_EXECUTE reuses the packet buffer, and a regression test that forces an oversized STMT_EXECUTE to assert ER_NET_PACKET_TOO_LARGE while ensuring connections remain usable.

Changes

COM_STMT_EXECUTE Large Packet Overflow

Layer / File(s) Summary
Double-free prevention in large-packet handler
lib/MySQL_Session.cpp
Adds a guard in handler_WCD_SS_MCQ_qpo_LargePacket to detect when CurrentQuery.stmt_meta->pkt aliases pkt->ptr. After calling RequestEnd(), conditionally calls l_free(pkt->size, pkt->ptr) only when the buffer is not aliased, preventing double-free during COM_STMT_EXECUTE error paths.
Regression test for packet overflow error handling
test/tap/tests/reg_test_5639_stmt_execute_max_allowed_packet-t.cpp, test/tap/groups/groups.json
New regression test with helper functions that lowers mysql-max_allowed_packet, prepares and binds an oversized VARCHAR parameter to trigger binary STMT_EXECUTE overflow, asserts rejection with ER_NET_PACKET_TOO_LARGE (1153), verifies the original connection remains usable, confirms fresh connections work, and restores the original setting. Test is registered in mysql84-g6 and mysql95-g1 test groups.

🎯 3 (Moderate) | ⏱️ ~20 minutes

🐰 I nudged a packet, too big to be free,
It bounced back gently, not burst into glee.
One careful l_free, no double surprise,
Connections stay happy, no panic in eyes.
A rabbit hops off, content and spry.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: avoiding a double-free of stmt_meta packet buffer in the LargePacket handler, addressing the core issue in the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue_5639

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 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 fixes a double-free vulnerability in MySQL_Session::handler_WCD_SS_MCQ_qpo_LargePacket by ensuring the packet buffer is not freed twice when aliased by stmt_meta. A new regression test is added to verify that oversized COM_STMT_EXECUTE packets are handled correctly without crashing. Feedback indicates that similar logic should be applied to other handlers like OK_msg and error_msg to prevent potential double-frees in those paths, and suggests using the MYSQL_QUERY() macro in the test suite for consistent error handling.

Comment thread lib/MySQL_Session.cpp
Comment on lines +6382 to +6387
const bool stmt_meta_owns_pkt =
(CurrentQuery.stmt_meta != NULL && CurrentQuery.stmt_meta->pkt == pkt->ptr);
RequestEnd(NULL, 1153, errmsg.c_str());
l_free(pkt->size,pkt->ptr);
if (!stmt_meta_owns_pkt) {
l_free(pkt->size,pkt->ptr);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The fix correctly addresses the double-free in the LargePacket handler by detecting the alias before RequestEnd() frees it. However, the same double-free vulnerability appears to exist in handler_WCD_SS_MCQ_qpo_OK_msg() (lines 6332-6341) and handler_WCD_SS_MCQ_qpo_error_msg() (lines 6352-6358), which also call RequestEnd() followed by l_free() on the same packet. If a COM_STMT_EXECUTE is blocked or redirected to return an OK or error message via query rules, those handlers will also trigger a double-free of the aliased packet buffer. Consider applying this logic to those handlers as well to fully resolve the issue for all COM_STMT_EXECUTE paths.

Comment on lines +76 to +77
mysql_query(admin, buf);
mysql_query(admin, "LOAD MYSQL VARIABLES TO RUNTIME");

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

For consistency with the rest of the test (e.g., lines 103 and 105), consider using the MYSQL_QUERY() macro here instead of calling mysql_query() directly. This ensures uniform error reporting if these queries fail during the restoration process.

Suggested change
mysql_query(admin, buf);
mysql_query(admin, "LOAD MYSQL VARIABLES TO RUNTIME");
MYSQL_QUERY(admin, buf);
MYSQL_QUERY(admin, "LOAD MYSQL VARIABLES TO RUNTIME");

@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)
test/tap/tests/reg_test_5639_stmt_execute_max_allowed_packet-t.cpp (1)

27-29: ⚡ Quick win

Use UPPER_SNAKE_CASE for file-scope constants.

These constexpr names don’t follow the repository constant naming convention.

♻️ Proposed rename
-constexpr unsigned int kMaxAllowedPacket = 8192;  // minimum allowed by ProxySQL
-constexpr size_t kBoundStringSize = 12 * 1024;    // guarantees pkt > kMaxAllowedPacket
-constexpr unsigned int kRestoreMaxAllowedPacket = 67108864;  // ProxySQL default
+constexpr unsigned int MAX_ALLOWED_PACKET = 8192;  // minimum allowed by ProxySQL
+constexpr size_t BOUND_STRING_SIZE = 12 * 1024;    // guarantees pkt > MAX_ALLOWED_PACKET
+constexpr unsigned int RESTORE_MAX_ALLOWED_PACKET = 67108864;  // ProxySQL default
@@
-	snprintf(buf, sizeof(buf), "SET mysql-max_allowed_packet=%u", kRestoreMaxAllowedPacket);
+	snprintf(buf, sizeof(buf), "SET mysql-max_allowed_packet=%u", RESTORE_MAX_ALLOWED_PACKET);
@@
-		snprintf(buf, sizeof(buf), "SET mysql-max_allowed_packet=%u", kMaxAllowedPacket);
+		snprintf(buf, sizeof(buf), "SET mysql-max_allowed_packet=%u", MAX_ALLOWED_PACKET);
@@
-	ok(true, "Lowered mysql-max_allowed_packet to %u", kMaxAllowedPacket);
+	ok(true, "Lowered mysql-max_allowed_packet to %u", MAX_ALLOWED_PACKET);
@@
-	// packet exceeds kMaxAllowedPacket. The total wire packet for a single
+	// packet exceeds MAX_ALLOWED_PACKET. The total wire packet for a single
@@
-	// kBoundStringSize > kMaxAllowedPacket comfortably crosses the limit. ---
-	std::string big(kBoundStringSize, 'A');
+	// BOUND_STRING_SIZE > MAX_ALLOWED_PACKET comfortably crosses the limit. ---
+	std::string big(BOUND_STRING_SIZE, 'A');

As per coding guidelines **/*.{cpp,h,hpp}: “Constants and macros should use UPPER_SNAKE_CASE”.

Also applies to: 75-76, 102-103, 106-107, 136-139

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/tap/tests/reg_test_5639_stmt_execute_max_allowed_packet-t.cpp` around
lines 27 - 29, Rename file-scope constexpr constants to UPPER_SNAKE_CASE: change
kMaxAllowedPacket -> MAX_ALLOWED_PACKET, kBoundStringSize -> BOUND_STRING_SIZE,
and kRestoreMaxAllowedPacket -> RESTORE_MAX_ALLOWED_PACKET, update all usages
accordingly (including any test helpers or asserts that reference these
symbols), and apply the same UPPER_SNAKE_CASE renaming to the other file-scope
constexpr constants in this file so naming follows the repository guideline.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@test/tap/tests/reg_test_5639_stmt_execute_max_allowed_packet-t.cpp`:
- Around line 27-29: Rename file-scope constexpr constants to UPPER_SNAKE_CASE:
change kMaxAllowedPacket -> MAX_ALLOWED_PACKET, kBoundStringSize ->
BOUND_STRING_SIZE, and kRestoreMaxAllowedPacket -> RESTORE_MAX_ALLOWED_PACKET,
update all usages accordingly (including any test helpers or asserts that
reference these symbols), and apply the same UPPER_SNAKE_CASE renaming to the
other file-scope constexpr constants in this file so naming follows the
repository guideline.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9c030fc9-1810-4f9e-b721-8421ba64d99a

📥 Commits

Reviewing files that changed from the base of the PR and between 9719d4a and b3f7d29.

📒 Files selected for processing (3)
  • lib/MySQL_Session.cpp
  • test/tap/groups/groups.json
  • test/tap/tests/reg_test_5639_stmt_execute_max_allowed_packet-t.cpp
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: CI-builds / builds (ubuntu22,-tap)
  • GitHub Check: CI-builds / builds (ubuntu24,-tap-genai-gcov)
  • GitHub Check: run / trigger
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{cpp,h,hpp}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{cpp,h,hpp}: Class names should use PascalCase with protocol prefixes (MySQL_, PgSQL_, ProxySQL_)
Member variables should use snake_case
Constants and macros should use UPPER_SNAKE_CASE
Use C++17 features; conditional compilation should use #ifdef PROXYSQL31, #ifdef PROXYSQL40, #ifdef PROXYSQLFFTO, #ifdef PROXYSQLTSDB, #ifdef PROXYSQLCLICKHOUSE (PROXYSQLGENAI no longer guards core code)
Use RAII for resource management and jemalloc for memory allocation
Use pthread mutexes for synchronization and std::atomic<> for counters

Files:

  • test/tap/tests/reg_test_5639_stmt_execute_max_allowed_packet-t.cpp
  • lib/MySQL_Session.cpp
test/tap/tests/**/*.cpp

📄 CodeRabbit inference engine (CLAUDE.md)

Test files should follow naming pattern test_*.cpp or *-t.cpp in test/tap/tests/

Files:

  • test/tap/tests/reg_test_5639_stmt_execute_max_allowed_packet-t.cpp
🧠 Learnings (1)
📚 Learning: 2026-01-20T09:34:19.124Z
Learnt from: yuji-hatakeyama
Repo: sysown/proxysql PR: 5307
File: test/tap/tests/reg_test_5306-show_warnings_with_comment-t.cpp:39-48
Timestamp: 2026-01-20T09:34:19.124Z
Learning: In ProxySQL's TAP test suite, resource leaks (e.g., not calling mysql_close() on early return paths) are commonly tolerated because test processes are short-lived and OS frees resources on exit. This pattern applies to all C++ test files under test/tap/tests. When reviewing, recognize this as a project-wide test convention and focus on test correctness and isolation rather than insisting on fixing such leaks in these test files.

Applied to files:

  • test/tap/tests/reg_test_5639_stmt_execute_max_allowed_packet-t.cpp
🔇 Additional comments (3)
lib/MySQL_Session.cpp (1)

6376-6387: LGTM!

test/tap/tests/reg_test_5639_stmt_execute_max_allowed_packet-t.cpp (1)

1-26: LGTM!

Also applies to: 31-74, 77-195

test/tap/groups/groups.json (1)

277-277: LGTM!

CI-lint-groups-json requires keys in groups.json to be sorted.
@sonarqubecloud

Copy link
Copy Markdown

@renecannao
renecannao changed the base branch from v3.0 to v3.0-260523 May 23, 2026 08:25
@renecannao
renecannao merged commit 1d52499 into v3.0-260523 May 23, 2026
4 of 5 checks passed
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.

1 participant