From e35973b226c219ea7a0136f33bd236e4a17b7f3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Jaramago=20Fern=C3=A1ndez?= Date: Wed, 29 Oct 2025 13:03:38 +0100 Subject: [PATCH 1/8] Add boundary checks in 'MySQL_Protocol::get_binds_from_pkt' These checks should prevent crashes when packets are received holding invalid lengths in the processed params. --- include/MySQL_Protocol.h | 4 +++- lib/MySQL_Protocol.cpp | 33 +++++++++++++++++++++++++++------ lib/MySQL_Session.cpp | 2 +- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/include/MySQL_Protocol.h b/include/MySQL_Protocol.h index 7b74c6839..db0a433c2 100644 --- a/include/MySQL_Protocol.h +++ b/include/MySQL_Protocol.h @@ -232,7 +232,9 @@ class MySQL_Protocol { bool generate_STMT_PREPARE_RESPONSE(uint8_t sequence_id, MySQL_STMT_Global_info *stmt_info, uint32_t _stmt_id=0); void generate_STMT_PREPARE_RESPONSE_OK(uint8_t sequence_id, uint32_t stmt_id); - stmt_execute_metadata_t * get_binds_from_pkt(void *ptr, unsigned int size, MySQL_STMT_Global_info *stmt_info, stmt_execute_metadata_t **stmt_meta); + stmt_execute_metadata_t * get_binds_from_pkt( + PtrSize_t& pkt, MySQL_STMT_Global_info *stmt_info, stmt_execute_metadata_t **stmt_meta + ); bool generate_COM_QUERY_from_COM_FIELD_LIST(PtrSize_t *pkt); diff --git a/lib/MySQL_Protocol.cpp b/lib/MySQL_Protocol.cpp index 94cb980f2..5fa973615 100644 --- a/lib/MySQL_Protocol.cpp +++ b/lib/MySQL_Protocol.cpp @@ -2672,9 +2672,11 @@ void * MySQL_Protocol::Query_String_to_packet(uint8_t sid, std::string *s, unsig // // returns stmt_meta, or a new one // See https://dev.mysql.com/doc/internals/en/com-stmt-execute.html for reference -stmt_execute_metadata_t * MySQL_Protocol::get_binds_from_pkt(void *ptr, unsigned int size, MySQL_STMT_Global_info *stmt_info, stmt_execute_metadata_t **stmt_meta) { +stmt_execute_metadata_t * MySQL_Protocol::get_binds_from_pkt( + PtrSize_t& pkt, MySQL_STMT_Global_info *stmt_info, stmt_execute_metadata_t **stmt_meta +) { stmt_execute_metadata_t *ret=NULL; //return NULL in case of failure - if (size<14) { + if (pkt.size < 14) { // some error! return ret; } @@ -2682,7 +2684,7 @@ stmt_execute_metadata_t * MySQL_Protocol::get_binds_from_pkt(void *ptr, unsigned if (num_params==2) { PROXY_TRACE(); } - char *p=(char *)ptr+5; + char *p=(char *)pkt.ptr+5; if (*stmt_meta) { // this PS was executed at least once, and we already have metadata ret=*stmt_meta; } else { // this is the first time that this PS is executed @@ -2700,12 +2702,12 @@ stmt_execute_metadata_t * MySQL_Protocol::get_binds_from_pkt(void *ptr, unsigned // * binds[X].buffer does NOT point to a new allocated buffer // * binds[X].buffer points to offset inside the original packet // FIXME: there is still no free for pkt, so that will be a memory leak that needs to be fixed - ret->pkt=ptr; + ret->pkt=pkt.ptr; uint8_t new_params_bound_flag; if (num_params) { uint16_t i; size_t null_bitmap_length=(num_params+7)/8; - if (size < (14+1+null_bitmap_length)) { + if (pkt.size < (14+1+null_bitmap_length)) { // some data missing? delete ret; return NULL; @@ -2790,6 +2792,14 @@ stmt_execute_metadata_t * MySQL_Protocol::get_binds_from_pkt(void *ptr, unsigned } for (i=0;i static_cast(pkt.ptr) + pkt.size) { + // Required to prevent double-free in dtor + if (ret->pkt) { ret->pkt = NULL; } + // Only free when metadata not obtained from cache (i.e. first execute) + if (!*stmt_meta) { delete ret; } + + return NULL; + } unsigned long *_l = 0; my_bool * _is_null; void *_data = (*myds)->sess->SLDH->get(ret->stmt_id, i, &_l, &_is_null); @@ -2896,6 +2906,16 @@ stmt_execute_metadata_t * MySQL_Protocol::get_binds_from_pkt(void *ptr, unsigned if (l>1) { PROXY_TRACE(); } + + if (p + l > static_cast(pkt.ptr) + pkt.size || len > pkt.size) { + // Required to prevent double-free in dtor + if (ret->pkt) { ret->pkt = NULL; } + // Only free when metadata not obtained from cache (i.e. first execute) + if (!*stmt_meta) { delete ret; } + + return NULL; + } + p+=l; binds[i].buffer=p; p+=len; @@ -2926,7 +2946,8 @@ stmt_execute_metadata_t * MySQL_Protocol::get_binds_from_pkt(void *ptr, unsigned #endif */ if (ret) - ret->size=size; + ret->size=pkt.size; + return ret; } diff --git a/lib/MySQL_Session.cpp b/lib/MySQL_Session.cpp index 22b8f19dc..1d328f33f 100644 --- a/lib/MySQL_Session.cpp +++ b/lib/MySQL_Session.cpp @@ -3445,7 +3445,7 @@ void MySQL_Session::handler___status_WAITING_CLIENT_DATA___STATE_SLEEP___MYSQL_C if (stmt_meta==NULL) { // we couldn't find any metadata stmt_meta_found=false; } - stmt_meta=client_myds->myprot.get_binds_from_pkt(pkt.ptr,pkt.size,stmt_info, &stmt_meta); + stmt_meta=client_myds->myprot.get_binds_from_pkt(pkt,stmt_info, &stmt_meta); if (stmt_meta==NULL) { l_free(pkt.size,pkt.ptr); client_myds->setDSS_STATE_QUERY_SENT_NET(); From 64b16172c726dbc946dde05e9a53c160ac346593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Jaramago=20Fern=C3=A1ndez?= Date: Wed, 29 Oct 2025 16:53:04 +0100 Subject: [PATCH 2/8] Change 'COM_STMT_EXECUTE' params length decoding According to MySQL protocol, variable length strings are encoded using length encoded integers. For reference, see: - https://dev.mysql.com/doc/dev/mysql-server/9.4.0/page_protocol_com_stmt_execute.html - https://dev.mysql.com/doc/dev/mysql-server/9.4.0/page_protocol_basic_dt_integers.html#a_protocol_type_int2 The protocol specifies that values greater than 2^24 (16777216) should be encoded using '0xFE + 8-byte integer'. Yet, in reality MySQL ignores the upper section of these 8-byte integers, treating them effectively like '4-bytes'. For the sake of compatibility this commit changes the decoding behavior for 'COM_STMT_EXECUTE' to match MySQL one. This different is subtle but important, since in practice MySQL itself doesn't use the '8 bytes' from the field. This means that connectors that are compatible with MySQL could find issues when sending these packets through ProxySQL (like NodeJS 'mysql2' connector which writes the 8-bytes as a 4-bytes duplication, motivating these changes), situation that could result in rejection due to malformed packet detection (or crashes/invalid handling in the worse case scenario). The previous decoding function is now renamed into 'mysql_decode_length_ll' to honor MySQL naming 'net_field_length_ll'. For now, this protocol change is limited to 'COM_STMT_EXECUTE'. --- include/MySQL_Protocol.h | 3 ++- include/proxysql_macros.h | 2 +- lib/MySQL_Protocol.cpp | 4 ++-- lib/MySQL_Query_Cache.cpp | 8 ++++---- lib/MySQL_encode.cpp | 10 +++++++++- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/include/MySQL_Protocol.h b/include/MySQL_Protocol.h index db0a433c2..946fcc2d5 100644 --- a/include/MySQL_Protocol.h +++ b/include/MySQL_Protocol.h @@ -75,7 +75,8 @@ class MySQL_ResultSet { }; -uint8_t mysql_decode_length(unsigned char *ptr, uint64_t *len); +uint8_t mysql_decode_length(unsigned char *ptr, uint32_t *len); +uint8_t mysql_decode_length_ll(unsigned char *ptr, uint64_t *len); /** * @brief ProxySQL replacement function for 'mysql_stmt_close'. Closes a diff --git a/include/proxysql_macros.h b/include/proxysql_macros.h index f83cca736..4d1e4f376 100644 --- a/include/proxysql_macros.h +++ b/include/proxysql_macros.h @@ -41,7 +41,7 @@ */ // copy 4 bytes -#define CPY4(x) *((uint32_t *)x) +#define CPY4(x) *((uint32_t *)(x)) // (un)set blocking mode on a file descriptor #define ioctl_FIONBIO(fd, mode) \ diff --git a/lib/MySQL_Protocol.cpp b/lib/MySQL_Protocol.cpp index 5fa973615..f83ff0700 100644 --- a/lib/MySQL_Protocol.cpp +++ b/lib/MySQL_Protocol.cpp @@ -1640,7 +1640,7 @@ bool MySQL_Protocol::PPHR_2(unsigned char *pkt, unsigned int len, bool& ret, MyP if (vars1.capabilities & CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA) { uint64_t passlen64; - int pass_len_enc=mysql_decode_length(pkt,&passlen64); + int pass_len_enc=mysql_decode_length_ll(pkt,&passlen64); vars1.pass_len = passlen64; pkt += pass_len_enc; if (vars1.pass_len > (len - (pkt - vars1._ptr))) { @@ -2901,7 +2901,7 @@ stmt_execute_metadata_t * MySQL_Protocol::get_binds_from_pkt( case MYSQL_TYPE_GEOMETRY: { uint8_t l=0; - uint64_t len; + uint32_t len { 0 }; l=mysql_decode_length((unsigned char *)p, &len); if (l>1) { PROXY_TRACE(); diff --git a/lib/MySQL_Query_Cache.cpp b/lib/MySQL_Query_Cache.cpp index 94334e130..81d8d7368 100644 --- a/lib/MySQL_Query_Cache.cpp +++ b/lib/MySQL_Query_Cache.cpp @@ -117,7 +117,7 @@ unsigned char* ok_to_eof_packet(const MySQL_QC_entry_t* entry) { // Find the spot in which the first EOF needs to be placed it += sizeof(mysql_hdr); uint64_t c_count = 0; - int c_count_len = mysql_decode_length(reinterpret_cast(it), &c_count); + int c_count_len = mysql_decode_length_ll(reinterpret_cast(it), &c_count); it += c_count_len; mysql_hdr column_hdr; @@ -196,7 +196,7 @@ bool MySQL_Query_Cache::set(uint64_t user_hash, const unsigned char* kp, uint32_ unsigned char* it = vp; it += sizeof(mysql_hdr); uint64_t c_count = 0; - int c_count_len = mysql_decode_length(const_cast(it), &c_count); + int c_count_len = mysql_decode_length_ll(const_cast(it), &c_count); it += c_count_len; for (uint64_t i = 0; i < c_count; i++) { @@ -230,10 +230,10 @@ bool MySQL_Query_Cache::set(uint64_t user_hash, const unsigned char* kp, uint32_ unsigned char* payload_temp = payload + 1; // skip affected_rows - payload_temp += mysql_decode_length(payload_temp, nullptr); + payload_temp += mysql_decode_length_ll(payload_temp, nullptr); // skip last_insert_id - payload_temp += mysql_decode_length(payload_temp, nullptr); + payload_temp += mysql_decode_length_ll(payload_temp, nullptr); // skip stats_flags payload_temp += sizeof(uint16_t); diff --git a/lib/MySQL_encode.cpp b/lib/MySQL_encode.cpp index 384a415c2..601824f53 100644 --- a/lib/MySQL_encode.cpp +++ b/lib/MySQL_encode.cpp @@ -221,7 +221,15 @@ uint64_t CPY8(unsigned char *ptr) { * poiter to the variable to store the length * returns the bytes length of th field */ -uint8_t mysql_decode_length(unsigned char *ptr, uint64_t *len) { +uint8_t mysql_decode_length(unsigned char *ptr, uint32_t *len) { + if (*ptr <= 0xfb) { if (len) { *len = CPY1(ptr); }; return 1; } + if (*ptr == 0xfc) { if (len) { *len = CPY2(ptr+1); }; return 3; } + if (*ptr == 0xfd) { if (len) { *len = CPY3(ptr+1); }; return 4; } + if (*ptr == 0xfe) { if (len) { *len = CPY4(ptr+1); }; return 9; } + return 0; // never reaches here +} + +uint8_t mysql_decode_length_ll(unsigned char *ptr, uint64_t *len) { if (*ptr <= 0xfb) { if (len) { *len = CPY1(ptr); }; return 1; } if (*ptr == 0xfc) { if (len) { *len = CPY2(ptr+1); }; return 3; } if (*ptr == 0xfd) { if (len) { *len = CPY3(ptr+1); }; return 4; } From bea0d14f4da69f369ec4732c88466671552475a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Jaramago=20Fern=C3=A1ndez?= Date: Wed, 29 Oct 2025 17:24:35 +0100 Subject: [PATCH 3/8] Add regression test for malformed 'COM_STMT_EXECUTE' packets The test exercises the boundary checks for 'COM_STMT_EXECUTE' params decoding and ensures the correct processing of malformed packets which only mangle the upper '4-bytes' of the param length-encoded integer. --- .../reg_test_stmt_inv_param_offset-t.cpp | 417 ++++++++++++++++++ 1 file changed, 417 insertions(+) create mode 100644 test/tap/tests/reg_test_stmt_inv_param_offset-t.cpp diff --git a/test/tap/tests/reg_test_stmt_inv_param_offset-t.cpp b/test/tap/tests/reg_test_stmt_inv_param_offset-t.cpp new file mode 100644 index 000000000..95367f7e8 --- /dev/null +++ b/test/tap/tests/reg_test_stmt_inv_param_offset-t.cpp @@ -0,0 +1,417 @@ +/** + * @file reg_test_stmt_inv_param_offset-t.cpp + * @brief Ensures valid handling of malformed COM_STMT_EXECUTE packets. + * @details Checks that the following flows are covered: + * + * - Malformed STMT_EXECUTE (1-param) with mangled UPPER 4-bytes for param-length are executed normally. + * - Malformed STMT_EXECUTE (2-params) with mangled UPPER 4-bytes for param-length are executed normally. + * - Malformed STMT_EXECUTE (1-param) with mangled LOWER 4-bytes for param-length are detected as malformed. + * - Malformed STMT_EXECUTE (2-params) with mangled LOWER 4-bytes for param-length are detected as malformed. + * + * Packets with 1 and 2 params are tested to cover different regression flows: + * + * - Invalid allocation attempt due to big param length (1 param). + * - Invalid memory access due to invalid offset-jump (length) during packet processing (2 >= params). + */ + +#include +#include +#include +#include +#include + +#include "mysql.h" + +#include "tap.h" +#include "command_line.h" +#include "utils.h" + +using std::vector; +using std::string; +using std::pair; + +/** + * @brief SUCCESS (Invalid 4 upper-bytes) - Hardcoded STMT_EXECUTE packet header. + */ +unsigned char SUCCESS_FAKE_EXEC_HEADER_1[] = { + 0xff,0xff,0xff,0x00,0x17,0x01,0x00,0x00, 0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x01, + 0xfd,0x00,0xfe,0x00,0x00,0x10,0x01,0x00, 0x00,0x10,0x01 // 0x00, 0x00,0x00,0x00 +}; + +/** + * @brief Hardcoded STMT_EXECUTE packet header - continuation. + */ +unsigned char SUCCESS_FAKE_EXEC_HEADER_1_2[] = { + 0x18,0x00,0x10,0x01 +}; + +/** + * @brief SUCCESS (Invalid 4 upper-bytes) - Hardcoded STMT_EXECUTE packet header. + */ +unsigned char SUCCESS_FAKE_EXEC_HEADER_2[] = { + 0xff,0xff,0xff,0x00,0x17,0x02,0x00,0x00, 0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x01, + 0xfd,0x00,0x03,0x00,0xfe,0x00,0x00,0x10, 0x01,0x00,0x00,0x10,0x01, // 0x00,0x00,0x00,0x00 +}; + +/** + * @brief Hardcoded STMT_EXECUTE packet header - continuation. + */ +unsigned char SUCCESS_FAKE_EXEC_HEADER_2_1[] = { + 0x1e,0x00,0x10,0x01 +}; + +/** + * @brief FAIL (Invalid 4 upper-bytes) - Hardcoded STMT_EXECUTE packet header. + */ +unsigned char FAIL_FAKE_EXEC_HEADER_1[] = { + 0xff,0xff,0xff,0x00,0x17,0x01,0x00,0x00, 0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x01, + 0xfd,0x00,0xfe,0x00,0x00,0x10,0x02,0x00, 0x00,0x10,0x01 +}; + +/** + * @brief Hardcoded STMT_EXECUTE packet header - continuation. + */ +unsigned char FAIL_FAKE_EXEC_HEADER_1_2[] = { + 0x18,0x00,0x10,0x01 +}; + +/** + * @brief FAIL (Invalid 4 upper-bytes) - Hardcoded STMT_EXECUTE packet header. + */ +unsigned char FAIL_FAKE_EXEC_HEADER_2[] = { + 0xff,0xff,0xff,0x00,0x17,0x02,0x00,0x00, 0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x01, + 0xfd,0x00,0x03,0x00,0xfe,0x00,0x00,0x10, 0x02,0x00,0x00,0x10,0x01 +}; + +/** + * @brief Hardcoded STMT_EXECUTE packet header - continuation. + */ +unsigned char FAIL_FAKE_EXEC_HEADER_2_1[] = { + 0x1e,0x00,0x10,0x01 +}; + +struct hdr_t { + unsigned char* hdr_1 { nullptr }; + size_t size_1 { 0 }; + unsigned char* hdr_2 { nullptr }; + size_t size_2 { 0 }; +}; + +struct q_params_t { + const char* q { nullptr }; + const vector p {}; + hdr_t hdrs {}; + const vector d {}; +}; + +struct stmt_params_t { + MYSQL_STMT* stmt; + vector p; + hdr_t hdrs {}; + const vector d {}; +}; + +rc_t prepare_stmt(MYSQL* conn, const q_params_t& qp) { + MYSQL_STMT* stmt = mysql_stmt_init(conn); + if (!stmt) { + diag("'mysql_stmt_init' failed error=\"out of memory\""); + return { EXIT_FAILURE, {} }; + } + + diag("Issuing STMT PREPARE query=\"%s\"", qp.q); + int my_err = mysql_stmt_prepare(stmt, qp.q, strlen(qp.q)); + if (my_err) { + diag( + "'mysql_stmt_prepare' failed query=\"%s\" err=\"%d\" msg=\"%s\"", + qp.q, mysql_stmt_errno(stmt), mysql_stmt_error(stmt) + ); + return { EXIT_FAILURE, { stmt, qp.p, qp.hdrs, qp.d } }; + } + + return { EXIT_SUCCESS, { stmt, qp.p, qp.hdrs, qp.d } }; +} + +uint32_t perform_real_execute(const stmt_params_t& sp) { + int myerr = 0; + + if ((myerr=mysql_stmt_bind_param(sp.stmt, const_cast(sp.p.data())))) { + diag("'mysql_stmt_bind_param' failed error=\"%s\"", mysql_stmt_error(sp.stmt)); + return EXIT_FAILURE; + } + + if (mysql_stmt_execute(sp.stmt)) { + diag("'mysql_stmt_execute' failed error=\"%s\"", mysql_stmt_error(sp.stmt)); + return EXIT_FAILURE; + } else { + return EXIT_SUCCESS; + } +} + + +int perform_fake_execute(MYSQL* mysql, const hdr_t& hdr, const vector& data) { + size_t pkt_size_1 { 16 * 1024 * 1024 - 1 + 4 }; + + int rc = write(mysql->net.fd, hdr.hdr_1, hdr.size_1); + diag("1 - Header written to socket len=%d", rc); + if (rc == -1) { return rc; } + + size_t body_size_1 { pkt_size_1 - hdr.size_1 }; + rc = write(mysql->net.fd, data.data(), body_size_1); + diag("1 - Body written to socket len=%d", rc); + if (rc == -1) { return rc; } + + rc = write(mysql->net.fd, hdr.hdr_2, hdr.size_2); + diag("2 - Header written to socket len=%d", rc); + + size_t body_size_2 { data.size() - body_size_1 }; + rc = write(mysql->net.fd, data.data() + body_size_1, body_size_2); + diag("2 - Body written to socket len=%d body_size_2=%ld", rc, body_size_2); + + return rc; +} + +int prepare_server_defaults(const CommandLine& cl) { + MYSQL* proxy = mysql_init(NULL); + + if (!mysql_real_connect(proxy, cl.host, cl.username, cl.password, NULL, cl.port, NULL, 0)) { + fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxy)); + return EXIT_FAILURE; + } + + int rc = 0; + if ((rc=mysql_query_t(proxy, "/* hostgroup=0 */ SET GLOBAL max_allowed_packet=20971520"))) { + diag("'mysql_query' failed error=\"%s\"", mysql_error(proxy)); + } + + mysql_close(proxy); + + return rc; +} + +void check_execute(MYSQL* proxy, const vector& stmts, bool exp_scs) { + size_t exec_count { 0 }; + + for (const stmt_params_t& sp : stmts) { + if (getenv("REG_TEST_STMT_INV_PARAM_OFFSET___REAL_EXECUTE")) { + uint32_t rc = perform_real_execute(sp); + + ok( + rc == EXIT_SUCCESS, + "Real execute for packet capture should succeed error=\"%s\"", + mysql_error(proxy) + ); + } else { + errno = 0; + + int rc = perform_fake_execute(proxy, sp.hdrs, sp.d); + ok ( + rc > 0, + "%ld - Fake EXECUTE written correctly error=\"%s\"", + exec_count, strerror(errno) + ); + + char res_buf[200] { 0 }; // OK ~= 11 bytes || ERR ~= 50 bytes + rc = read(proxy->net.fd, &res_buf, sizeof(res_buf)); + ok ( + rc > 0, + "%ld - Fake EXECUTE response read error=\"%s\" packet_str=\"%s\"", + exec_count, strerror(errno), res_buf + 7 + ); + + bool act_scs = strncasecmp(res_buf + 7, "#28000", strlen("#28000")) != 0; + ok( + act_scs == exp_scs, + "%ld - Fake EXECUTE success should match expected exp_scs=%d act_scs=%d", + exec_count, exp_scs, act_scs + ); + exec_count += 1; + } + } +} + +int main(int argc, char** argv) { + CommandLine cl; + + if (cl.getEnv()) { + diag("Failed to get the required environmental variables."); + return EXIT_FAILURE; + } + + if (prepare_server_defaults(cl)) { + diag("Failed to prepare server defaults, exiting..."); + return EXIT_FAILURE; + } + + plan(24); + + MYSQL* proxy_1 = mysql_init(NULL); + MYSQL* proxy_2 = mysql_init(NULL); + MYSQL* admin = mysql_init(NULL); + + diag( + "Creating MySQL connection host=\"%s\" port=%d user=\"%s\" pass=\"%s\"", + cl.host, cl.port, cl.username, cl.password + ); + if (!mysql_real_connect(proxy_1, cl.host, cl.username, cl.password, NULL, cl.port, NULL, 0)) { + fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxy_1)); + return EXIT_FAILURE; + } + diag( + "Creating MySQL connection host=\"%s\" port=%d user=\"%s\" pass=\"%s\"", + cl.host, cl.port, cl.username, cl.password + ); + if (!mysql_real_connect(proxy_2, cl.host, cl.username, cl.password, NULL, cl.port, NULL, 0)) { + fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxy_2)); + return EXIT_FAILURE; + } + + diag( + "Creating Admin MySQL connection host=\"%s\" port=%d user=\"%s\" pass=\"%s\"", + cl.host, cl.admin_port, cl.admin_username, cl.admin_password + ); + if (!mysql_real_connect(admin, cl.host, cl.admin_username, cl.admin_password, NULL, cl.admin_port, NULL, 0)) { + fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(admin)); + return EXIT_FAILURE; + } + + MYSQL_QUERY(proxy_1, "CREATE DATABASE IF NOT EXISTS test"); + MYSQL_QUERY(proxy_2, "USE test"); + + MYSQL_QUERY(proxy_1, + "CREATE TABLE IF NOT EXISTS test.test_stmt_inv__large_col_1 (" + "id INT AUTO_INCREMENT PRIMARY KEY," + "data_column LONGTEXT" + ")" + ); + + MYSQL_QUERY(proxy_1, + "CREATE TABLE IF NOT EXISTS test.test_stmt_inv__large_col_2 (" + "id INT AUTO_INCREMENT PRIMARY KEY," + "data_column LONGTEXT," + "data_int INT" + ")" + ); + + const char* q1 { "INSERT INTO test.test_stmt_inv__large_col_1 (data_column) VALUES (?)" }; + const char* q2 { "INSERT INTO test.test_stmt_inv__large_col_2 (data_column,data_int) VALUES (?,?)" }; + + MYSQL_BIND b1 {}; + vector p1(17 * 1024 * 1024, 'a'); + uint64_t p1_len { p1.size() }; + b1.buffer_type = MYSQL_TYPE_VAR_STRING; + b1.buffer = p1.data(); + b1.buffer_length = p1_len; + b1.is_null = 0; + b1.length = &p1_len; + + MYSQL_BIND b2 {}; + uint64_t p2 { 7 }; + b2.buffer_type = MYSQL_TYPE_LONG; + b2.buffer = reinterpret_cast(&p2); + b2.is_null = 0; + b2.length = 0; + + const hdr_t scs_hdr_1 { + SUCCESS_FAKE_EXEC_HEADER_1, sizeof(SUCCESS_FAKE_EXEC_HEADER_1), + SUCCESS_FAKE_EXEC_HEADER_1_2, sizeof(SUCCESS_FAKE_EXEC_HEADER_1_2) + }; + const hdr_t scs_hdr_2 { + SUCCESS_FAKE_EXEC_HEADER_2, sizeof(SUCCESS_FAKE_EXEC_HEADER_2), + SUCCESS_FAKE_EXEC_HEADER_2_1, sizeof(SUCCESS_FAKE_EXEC_HEADER_2_1) + }; + const hdr_t fail_hdr_1 { + FAIL_FAKE_EXEC_HEADER_1, sizeof(FAIL_FAKE_EXEC_HEADER_1), + FAIL_FAKE_EXEC_HEADER_1_2, sizeof(FAIL_FAKE_EXEC_HEADER_1_2) + }; + const hdr_t fail_hdr_2 { + FAIL_FAKE_EXEC_HEADER_2, sizeof(FAIL_FAKE_EXEC_HEADER_2), + FAIL_FAKE_EXEC_HEADER_2_1, sizeof(FAIL_FAKE_EXEC_HEADER_2_1) + }; + + vector p1_2 {}; + std::copy(std::begin(p1), std::end(p1), std::back_inserter(p1_2)); + vector vp2 { static_cast(p2), 0, 0, 0 }; + std::copy(std::begin(vp2), std::end(vp2), std::back_inserter(p1_2)); + + const vector scs_q_params { + { q1, { b1 }, scs_hdr_1, p1 }, + { q2, { b1, b2 }, scs_hdr_2, p1_2 } + }; + const vector fail_hdrs { fail_hdr_1, fail_hdr_2 }; + vector> scs_stmt_params {}; + + std::transform(scs_q_params.begin(), scs_q_params.end(), std::back_inserter(scs_stmt_params), + [&proxy_1] (const q_params_t& qp) -> rc_t { + return prepare_stmt(proxy_1, qp); + } + ); + + vector> scs_stmt_params_2 {}; + vector> fail_stmt_params_2 {}; + + std::transform(scs_q_params.begin(), scs_q_params.end(), std::back_inserter(scs_stmt_params_2), + [&proxy_2] (const q_params_t& qp) -> rc_t { + return prepare_stmt(proxy_2, qp); + } + ); + + vector v_stmts {}; + vector v_stmts_2 {}; + + for (const auto& sp : scs_stmt_params) { + if (sp.first) { + diag("Exiting due to failed prepare error=\"%s\"", mysql_stmt_error(sp.second.stmt)); + goto cleanup; + } else { + v_stmts.push_back(sp.second); + } + } + for (const auto& sp : scs_stmt_params_2) { + if (sp.first) { + diag("Exiting due to failed prepare error=\"%s\"", mysql_stmt_error(sp.second.stmt)); + goto cleanup; + } else { + v_stmts_2.push_back(sp.second); + } + } + + { + // Check for SUCCESS for payloads with invalid UPPER 4-bytes in string - CONN 1 + check_execute(proxy_1, v_stmts, true); + + for (size_t i = 0; i < scs_stmt_params.size(); i++) { + v_stmts[i].hdrs = fail_hdrs[i]; + } + + // Check for FAILURE for payloads with invalid LOWER 4-bytes in string - CONN 1 + check_execute(proxy_1, v_stmts, false); + + vector v_stmts_2_inv { v_stmts_2 }; + + for (size_t i = 0; i < scs_stmt_params_2.size(); i++) { + v_stmts_2_inv[i].hdrs = fail_hdrs[i]; + } + + // Check for FAILURE for payloads with invalid LOWER 4-bytes in string - CONN 2 + check_execute(proxy_2, v_stmts_2_inv, false); + // Check for SUCCESS for payloads with invalid UPPER 4-bytes in string - CONN 2 + check_execute(proxy_2, v_stmts_2, true); + } + +cleanup: + MYSQL_QUERY(proxy_1, "DELETE FROM test.test_stmt_inv__large_col_1"); + MYSQL_QUERY(proxy_1, "DELETE FROM test.test_stmt_inv__large_col_2"); + + for (const auto& sp : scs_stmt_params) { + mysql_stmt_close(sp.second.stmt); + } + for (const auto& sp : scs_stmt_params_2) { + mysql_stmt_close(sp.second.stmt); + } + + mysql_close(proxy_1); + mysql_close(proxy_2); + mysql_close(admin); + + return exit_status(); +} From e1679a21a732f705c884798e1a8de0acd8ba56ee Mon Sep 17 00:00:00 2001 From: Miro Stauder Date: Tue, 4 Nov 2025 17:33:19 +0000 Subject: [PATCH 4/8] add tap test to groups.json Signed-off-by: Miro Stauder --- test/tap/groups/groups.json | 1 + 1 file changed, 1 insertion(+) diff --git a/test/tap/groups/groups.json b/test/tap/groups/groups.json index 9bc24803b..f972143b2 100644 --- a/test/tap/groups/groups.json +++ b/test/tap/groups/groups.json @@ -104,6 +104,7 @@ "reg_test_sql_calc_found_rows-t" : [ "default-g2","mysql-auto_increment_delay_multiplex=0-g2","mysql-multiplexing=false-g2","mysql-query_digests=0-g2","mysql-query_digests_keep_comment=1-g2" ], "reg_test__ssl_client_busy_wait-t" : [ "default-g2","mysql-auto_increment_delay_multiplex=0-g2","mysql-multiplexing=false-g2","mysql-query_digests=0-g2","mysql-query_digests_keep_comment=1-g2" ], "reg_test_stmt_resultset_err_no_rows_libmysql-t" : [ "default-g2","mysql-auto_increment_delay_multiplex=0-g2","mysql-multiplexing=false-g2","mysql-query_digests=0-g2","mysql-query_digests_keep_comment=1-g2" ], + "reg_test_stmt_inv_param_offset-t" : [ "default-g2","mysql-auto_increment_delay_multiplex=0-g2","mysql-multiplexing=false-g2","mysql-query_digests=0-g2","mysql-query_digests_keep_comment=1-g2" ], "reg_test_stmt_resultset_err_no_rows_php-t" : [ "default-g3","mysql-auto_increment_delay_multiplex=0-g3","mysql-multiplexing=false-g3","mysql-query_digests=0-g3","mysql-query_digests_keep_comment=1-g3" ], "reg_test_stmt_resultset_err_no_rows-t" : [ "default-g3","mysql-auto_increment_delay_multiplex=0-g3","mysql-multiplexing=false-g3","mysql-query_digests=0-g3","mysql-query_digests_keep_comment=1-g3" ], "savepoint-3749-t" : [ "default-g3","mysql-auto_increment_delay_multiplex=0-g3","mysql-multiplexing=false-g3","mysql-query_digests=0-g3","mysql-query_digests_keep_comment=1-g3" ], From b89ea4db66ac042428ee2978088bab85a8f64cc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Jaramago=20Fern=C3=A1ndez?= Date: Wed, 5 Nov 2025 12:16:37 +0100 Subject: [PATCH 5/8] Fix credentials in one conn for 'reg_test_stmt_inv_param_offset-t' Connection requires root credentials for modifying 'max_allowed_packet'. --- test/tap/tests/reg_test_stmt_inv_param_offset-t.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tap/tests/reg_test_stmt_inv_param_offset-t.cpp b/test/tap/tests/reg_test_stmt_inv_param_offset-t.cpp index 95367f7e8..069cb8ce9 100644 --- a/test/tap/tests/reg_test_stmt_inv_param_offset-t.cpp +++ b/test/tap/tests/reg_test_stmt_inv_param_offset-t.cpp @@ -173,7 +173,7 @@ int perform_fake_execute(MYSQL* mysql, const hdr_t& hdr, const vector& dat int prepare_server_defaults(const CommandLine& cl) { MYSQL* proxy = mysql_init(NULL); - if (!mysql_real_connect(proxy, cl.host, cl.username, cl.password, NULL, cl.port, NULL, 0)) { + if (!mysql_real_connect(proxy, cl.host, cl.root_username, cl.root_password, NULL, cl.port, NULL, 0)) { fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxy)); return EXIT_FAILURE; } From 69cf5576f36de05e2498abd34b2ffa33d4e83ace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Jaramago=20Fern=C3=A1ndez?= Date: Wed, 5 Nov 2025 17:51:34 +0100 Subject: [PATCH 6/8] Fix conn selection for 'reg_test_stmt_inv_param_offset-t' Since the test requires to modify 'max_allowed_packet' to work, it cannot reuse previous connections from the conn_pool. New connections must be created, to ensure that the new defaults are used. --- test/tap/tests/reg_test_stmt_inv_param_offset-t.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/tap/tests/reg_test_stmt_inv_param_offset-t.cpp b/test/tap/tests/reg_test_stmt_inv_param_offset-t.cpp index 069cb8ce9..963bb68dc 100644 --- a/test/tap/tests/reg_test_stmt_inv_param_offset-t.cpp +++ b/test/tap/tests/reg_test_stmt_inv_param_offset-t.cpp @@ -275,7 +275,13 @@ int main(int argc, char** argv) { } MYSQL_QUERY(proxy_1, "CREATE DATABASE IF NOT EXISTS test"); + + diag("Starting trx in new connection; required for 'max_allowed_packet' conn=%p", proxy_1); + MYSQL_QUERY(proxy_1, "/* create_new_connection=1 */ BEGIN"); + MYSQL_QUERY(proxy_2, "USE test"); + diag("Starting trx in new connection; required for 'max_allowed_packet' conn=%p", proxy_2); + MYSQL_QUERY(proxy_2, "/* create_new_connection=1 */ BEGIN"); MYSQL_QUERY(proxy_1, "CREATE TABLE IF NOT EXISTS test.test_stmt_inv__large_col_1 (" @@ -399,6 +405,9 @@ int main(int argc, char** argv) { } cleanup: + MYSQL_QUERY(proxy_1, "ROLLBACK"); + MYSQL_QUERY(proxy_2, "ROLLBACK"); + MYSQL_QUERY(proxy_1, "DELETE FROM test.test_stmt_inv__large_col_1"); MYSQL_QUERY(proxy_1, "DELETE FROM test.test_stmt_inv__large_col_2"); From 09036827561c3558ee1ca1b3f53b21631216f23a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Jaramago=20Fern=C3=A1ndez?= Date: Wed, 5 Nov 2025 17:56:45 +0100 Subject: [PATCH 7/8] Improve verbosity for 'reg_test_stmt_inv_param_offset-t.cpp' Replace 'MYSQL_QUERY' macro with verbose alternative 'MYSQL_QUERY_T'. --- .../reg_test_stmt_inv_param_offset-t.cpp | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/tap/tests/reg_test_stmt_inv_param_offset-t.cpp b/test/tap/tests/reg_test_stmt_inv_param_offset-t.cpp index 963bb68dc..72307a81b 100644 --- a/test/tap/tests/reg_test_stmt_inv_param_offset-t.cpp +++ b/test/tap/tests/reg_test_stmt_inv_param_offset-t.cpp @@ -274,23 +274,23 @@ int main(int argc, char** argv) { return EXIT_FAILURE; } - MYSQL_QUERY(proxy_1, "CREATE DATABASE IF NOT EXISTS test"); + MYSQL_QUERY_T(proxy_1, "CREATE DATABASE IF NOT EXISTS test"); - diag("Starting trx in new connection; required for 'max_allowed_packet' conn=%p", proxy_1); - MYSQL_QUERY(proxy_1, "/* create_new_connection=1 */ BEGIN"); + diag("Start_Ting trx in new connection; required for 'max_allowed_packet' conn=%p", proxy_1); + MYSQL_QUERY_T(proxy_1, "/* create_new_connection=1 */ BEGIN"); - MYSQL_QUERY(proxy_2, "USE test"); - diag("Starting trx in new connection; required for 'max_allowed_packet' conn=%p", proxy_2); - MYSQL_QUERY(proxy_2, "/* create_new_connection=1 */ BEGIN"); + MYSQL_QUERY_T(proxy_2, "USE test"); + diag("Start_Ting trx in new connection; required for 'max_allowed_packet' conn=%p", proxy_2); + MYSQL_QUERY_T(proxy_2, "/* create_new_connection=1 */ BEGIN"); - MYSQL_QUERY(proxy_1, + MYSQL_QUERY_T(proxy_1, "CREATE TABLE IF NOT EXISTS test.test_stmt_inv__large_col_1 (" "id INT AUTO_INCREMENT PRIMARY KEY," "data_column LONGTEXT" ")" ); - MYSQL_QUERY(proxy_1, + MYSQL_QUERY_T(proxy_1, "CREATE TABLE IF NOT EXISTS test.test_stmt_inv__large_col_2 (" "id INT AUTO_INCREMENT PRIMARY KEY," "data_column LONGTEXT," @@ -405,11 +405,11 @@ int main(int argc, char** argv) { } cleanup: - MYSQL_QUERY(proxy_1, "ROLLBACK"); - MYSQL_QUERY(proxy_2, "ROLLBACK"); + MYSQL_QUERY_T(proxy_1, "ROLLBACK"); + MYSQL_QUERY_T(proxy_2, "ROLLBACK"); - MYSQL_QUERY(proxy_1, "DELETE FROM test.test_stmt_inv__large_col_1"); - MYSQL_QUERY(proxy_1, "DELETE FROM test.test_stmt_inv__large_col_2"); + MYSQL_QUERY_T(proxy_1, "DELETE FROM test.test_stmt_inv__large_col_1"); + MYSQL_QUERY_T(proxy_1, "DELETE FROM test.test_stmt_inv__large_col_2"); for (const auto& sp : scs_stmt_params) { mysql_stmt_close(sp.second.stmt); From 1a874de9ef58e7112dc22b177fc63731331de909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Jaramago=20Fern=C3=A1ndez?= Date: Wed, 5 Nov 2025 17:58:08 +0100 Subject: [PATCH 8/8] Ensure existence of testing database for 'reg_test_proclist_use_after_free-t.cpp' --- test/tap/tests/reg_test_proclist_use_after_free-t.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/test/tap/tests/reg_test_proclist_use_after_free-t.cpp b/test/tap/tests/reg_test_proclist_use_after_free-t.cpp index ff675c0cd..37e656cdb 100644 --- a/test/tap/tests/reg_test_proclist_use_after_free-t.cpp +++ b/test/tap/tests/reg_test_proclist_use_after_free-t.cpp @@ -35,6 +35,7 @@ int main(int argc, char** argv) { return EXIT_FAILURE; } + MYSQL_QUERY_T(proxy, "CREATE DATABASE IF NOT EXISTS test"); MYSQL_QUERY_T(proxy, "CREATE TABLE IF NOT EXISTS test.auto_inc_multiplex " "(c1 INT NOT NULL AUTO_INCREMENT PRIMARY KEY, c2 VARCHAR(100), c3 VARCHAR(100))"