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] 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; }