From 3e9a9979f016496a0804255aa3ce02e5297aa1b3 Mon Sep 17 00:00:00 2001 From: Nikita Massalitin <17572243+lohmag@users.noreply.github.com> Date: Sun, 25 Oct 2026 23:16:06 +0200 Subject: [PATCH 1/4] mysqlnd: Do not compress packets smaller than 512 bytes --- .../mysqli_compression_packet_sizes.phpt | 39 +++++++++++++++++++ ext/mysqlnd/mysqlnd_protocol_frame_codec.c | 3 +- ext/mysqlnd/mysqlnd_wireprotocol.h | 2 + 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 ext/mysqli/tests/mysqli_compression_packet_sizes.phpt diff --git a/ext/mysqli/tests/mysqli_compression_packet_sizes.phpt b/ext/mysqli/tests/mysqli_compression_packet_sizes.phpt new file mode 100644 index 000000000000..51a748cfe313 --- /dev/null +++ b/ext/mysqli/tests/mysqli_compression_packet_sizes.phpt @@ -0,0 +1,39 @@ +--TEST-- +Compressed protocol: packets on both sides of the compression threshold +--EXTENSIONS-- +mysqli +--SKIPIF-- + +--FILE-- +query("SHOW SESSION STATUS LIKE 'Compression'")->fetch_row()[1]); + +foreach ([1, 230, 235, 236, 237, 238, 240, 600, 4000, 100000, 3000000] as $length) { + $value = substr(str_repeat('compressed protocol packet ', intdiv($length, 27) + 1), 0, $length); + $row = $link->query("SELECT '$value' AS v, LENGTH('$value') AS l")->fetch_assoc(); + if ($row['v'] !== $value || (int) $row['l'] !== $length) { + printf("[002] Round trip failed for %d bytes\n", $length); + } +} + +for ($i = 0; $i < 200; $i++) { + $value = bin2hex(random_bytes(1 + $i * 37)); + if ($link->query("SELECT '$value' AS v")->fetch_assoc()['v'] !== $value) { + printf("[003] Round trip failed for %d bytes\n", strlen($value)); + } +} + +$link->close(); +print "done!"; +?> +--EXPECT-- +string(2) "ON" +done! diff --git a/ext/mysqlnd/mysqlnd_protocol_frame_codec.c b/ext/mysqlnd/mysqlnd_protocol_frame_codec.c index 70a81befdd71..2663a101ed0a 100644 --- a/ext/mysqlnd/mysqlnd_protocol_frame_codec.c +++ b/ext/mysqlnd/mysqlnd_protocol_frame_codec.c @@ -55,7 +55,8 @@ static ssize_t write_compressed_packet( /* here we need to compress the data and then write it, first comes the compressed header */ size_t tmp_complen = to_be_sent; size_t payload_size; - if (PASS == pfc->data->m.encode((compress_buf + COMPRESSED_HEADER_SIZE + MYSQLND_HEADER_SIZE), &tmp_complen, + if (to_be_sent >= MYSQLND_MIN_COMPRESS_LENGTH + && PASS == pfc->data->m.encode((compress_buf + COMPRESSED_HEADER_SIZE + MYSQLND_HEADER_SIZE), &tmp_complen, uncompressed_payload, to_be_sent)) { int3store(compress_buf + MYSQLND_HEADER_SIZE, to_be_sent); diff --git a/ext/mysqlnd/mysqlnd_wireprotocol.h b/ext/mysqlnd/mysqlnd_wireprotocol.h index ad452ec4f928..134c389ced04 100644 --- a/ext/mysqlnd/mysqlnd_wireprotocol.h +++ b/ext/mysqlnd/mysqlnd_wireprotocol.h @@ -18,6 +18,8 @@ #define MYSQLND_HEADER_SIZE 4 #define COMPRESSED_HEADER_SIZE 3 +/* Packets smaller than this are sent uncompressed: they barely shrink. */ +#define MYSQLND_MIN_COMPRESS_LENGTH 512 #define MYSQLND_NULL_LENGTH (zend_ulong) ~0 From 3d3b1c94765a9d3b59cad35fae18eb3a32624967 Mon Sep 17 00:00:00 2001 From: Nikita Massalitin <17572243+lohmag@users.noreply.github.com> Date: Mon, 26 Oct 2026 15:53:05 +0200 Subject: [PATCH 2/4] Use incompressible data in the compression packet size test --- ext/mysqli/tests/mysqli_compression_packet_sizes.phpt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/ext/mysqli/tests/mysqli_compression_packet_sizes.phpt b/ext/mysqli/tests/mysqli_compression_packet_sizes.phpt index 51a748cfe313..c9b11216dd26 100644 --- a/ext/mysqli/tests/mysqli_compression_packet_sizes.phpt +++ b/ext/mysqli/tests/mysqli_compression_packet_sizes.phpt @@ -24,12 +24,19 @@ foreach ([1, 230, 235, 236, 237, 238, 240, 600, 4000, 100000, 3000000] as $lengt } } +$stmt = $link->prepare("SELECT HEX(?)"); for ($i = 0; $i < 200; $i++) { - $value = bin2hex(random_bytes(1 + $i * 37)); - if ($link->query("SELECT '$value' AS v")->fetch_assoc()['v'] !== $value) { + $value = random_bytes(1 + $i * 37); + $stmt->bind_param('s', $value); + $stmt->execute(); + $stmt->bind_result($hex); + $stmt->fetch(); + $stmt->free_result(); + if ($hex !== strtoupper(bin2hex($value))) { printf("[003] Round trip failed for %d bytes\n", strlen($value)); } } +$stmt->close(); $link->close(); print "done!"; From c25b7e8dac33b92e8f220cf09ea83436fb0c35f6 Mon Sep 17 00:00:00 2001 From: Nikita Massalitin <17572243+lohmag@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:27:05 +0200 Subject: [PATCH 3/4] Drop the closing calls from the compression packet size test --- ext/mysqli/tests/mysqli_compression_packet_sizes.phpt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/ext/mysqli/tests/mysqli_compression_packet_sizes.phpt b/ext/mysqli/tests/mysqli_compression_packet_sizes.phpt index c9b11216dd26..b102dd0cc2d8 100644 --- a/ext/mysqli/tests/mysqli_compression_packet_sizes.phpt +++ b/ext/mysqli/tests/mysqli_compression_packet_sizes.phpt @@ -36,11 +36,6 @@ for ($i = 0; $i < 200; $i++) { printf("[003] Round trip failed for %d bytes\n", strlen($value)); } } -$stmt->close(); - -$link->close(); -print "done!"; ?> --EXPECT-- string(2) "ON" -done! From 814d6250f6ef02fedfff9f9611396a97bca81620 Mon Sep 17 00:00:00 2001 From: Nikita Massalitin <17572243+lohmag@users.noreply.github.com> Date: Thu, 29 Oct 2026 21:30:29 +0200 Subject: [PATCH 4/4] Lower the compression threshold to 170 and test it on the wire --- ext/mysqli/tests/fake_server.inc | 22 ++++++++++ .../mysqli_compression_packet_sizes.phpt | 41 ------------------ .../tests/mysqli_compression_threshold.phpt | 43 +++++++++++++++++++ ext/mysqlnd/mysqlnd_wireprotocol.h | 2 +- 4 files changed, 66 insertions(+), 42 deletions(-) delete mode 100644 ext/mysqli/tests/mysqli_compression_packet_sizes.phpt create mode 100644 ext/mysqli/tests/mysqli_compression_threshold.phpt diff --git a/ext/mysqli/tests/fake_server.inc b/ext/mysqli/tests/fake_server.inc index 2a4d17f946e8..8fb2f5a4aeba 100644 --- a/ext/mysqli/tests/fake_server.inc +++ b/ext/mysqli/tests/fake_server.inc @@ -630,6 +630,15 @@ class my_mysqli_fake_server_conn fprintf(STDERR, "[*] Received: %s\n", bin2hex($data)); } + public function read_compressed_packet(): void + { + $header = $this->read_bytes(7); + $compressed_length = ord($header[0]) | (ord($header[1]) << 8) | (ord($header[2]) << 16); + $uncompressed_length = ord($header[4]) | (ord($header[5]) << 8) | (ord($header[6]) << 16); + $this->read_bytes($compressed_length); + fprintf(STDERR, "[*] Received compressed packet: %d bytes, uncompressed length %d\n", $compressed_length, $uncompressed_length); + } + public function close() { fclose($this->conn); @@ -1445,6 +1454,19 @@ function my_mysqli_test_stmt_response_field_len_premature(my_mysqli_fake_server_ $conn->read(65536); } +function my_mysqli_test_compressed_queries(my_mysqli_fake_server_conn $conn): void +{ + $conn->send_server_greetings(); + $conn->read(); + $conn->send_server_ok(); + for ($i = 0; $i < 2; $i++) { + $conn->read_compressed_packet(); + $ok = $conn->packet_generator->server_ok("01")->to_bytes(); + $conn->send(substr(pack('V', strlen($ok)), 0, 3) . "\x01\x00\x00\x00" . $ok, "Compressed Server OK"); + } + $conn->read_compressed_packet(); +} + function run_fake_server(string $test_function, int|string $port = 0): void { $host = '127.0.0.1'; diff --git a/ext/mysqli/tests/mysqli_compression_packet_sizes.phpt b/ext/mysqli/tests/mysqli_compression_packet_sizes.phpt deleted file mode 100644 index b102dd0cc2d8..000000000000 --- a/ext/mysqli/tests/mysqli_compression_packet_sizes.phpt +++ /dev/null @@ -1,41 +0,0 @@ ---TEST-- -Compressed protocol: packets on both sides of the compression threshold ---EXTENSIONS-- -mysqli ---SKIPIF-- - ---FILE-- -query("SHOW SESSION STATUS LIKE 'Compression'")->fetch_row()[1]); - -foreach ([1, 230, 235, 236, 237, 238, 240, 600, 4000, 100000, 3000000] as $length) { - $value = substr(str_repeat('compressed protocol packet ', intdiv($length, 27) + 1), 0, $length); - $row = $link->query("SELECT '$value' AS v, LENGTH('$value') AS l")->fetch_assoc(); - if ($row['v'] !== $value || (int) $row['l'] !== $length) { - printf("[002] Round trip failed for %d bytes\n", $length); - } -} - -$stmt = $link->prepare("SELECT HEX(?)"); -for ($i = 0; $i < 200; $i++) { - $value = random_bytes(1 + $i * 37); - $stmt->bind_param('s', $value); - $stmt->execute(); - $stmt->bind_result($hex); - $stmt->fetch(); - $stmt->free_result(); - if ($hex !== strtoupper(bin2hex($value))) { - printf("[003] Round trip failed for %d bytes\n", strlen($value)); - } -} -?> ---EXPECT-- -string(2) "ON" diff --git a/ext/mysqli/tests/mysqli_compression_threshold.phpt b/ext/mysqli/tests/mysqli_compression_threshold.phpt new file mode 100644 index 000000000000..9c679c0da883 --- /dev/null +++ b/ext/mysqli/tests/mysqli_compression_threshold.phpt @@ -0,0 +1,43 @@ +--TEST-- +Compressed protocol: packets below the threshold are sent uncompressed +--EXTENSIONS-- +mysqli +--SKIPIF-- +info(); +} +if (!str_contains(ob_get_clean(), 'Compression => supported')) { + die('skip mysqlnd is built without compression support'); +} +?> +--FILE-- +wait(); + +$link = mysqli_init(); +$link->real_connect('127.0.0.1', 'root', '', '', $process->getPort(), null, MYSQLI_CLIENT_COMPRESS); +var_dump($link->query("SELECT '" . str_repeat('a', 80) . "'")); +var_dump($link->query("SELECT '" . str_repeat('a', 1000) . "'")); +$link->close(); + +$process->terminate(true); +?> +--EXPECTF-- +[*] Server started on 127.0.0.1:%d +[*] Connection established +[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 +[*] Received: %s +[*] Sending - Server OK: 0700000200000002000000 +[*] Received compressed packet: 94 bytes, uncompressed length 0 +[*] Sending - Compressed Server OK: 0b0000010000000700000100000002000000 +bool(true) +[*] Received compressed packet: %d bytes, uncompressed length 1014 +[*] Sending - Compressed Server OK: 0b0000010000000700000100000002000000 +bool(true) +[*] Received compressed packet: 5 bytes, uncompressed length 0 +[*] Server finished diff --git a/ext/mysqlnd/mysqlnd_wireprotocol.h b/ext/mysqlnd/mysqlnd_wireprotocol.h index 134c389ced04..db3e77019e3d 100644 --- a/ext/mysqlnd/mysqlnd_wireprotocol.h +++ b/ext/mysqlnd/mysqlnd_wireprotocol.h @@ -19,7 +19,7 @@ #define MYSQLND_HEADER_SIZE 4 #define COMPRESSED_HEADER_SIZE 3 /* Packets smaller than this are sent uncompressed: they barely shrink. */ -#define MYSQLND_MIN_COMPRESS_LENGTH 512 +#define MYSQLND_MIN_COMPRESS_LENGTH 170 #define MYSQLND_NULL_LENGTH (zend_ulong) ~0