From e318023a6b6d7d240ee8180548886e5f145f1d2f Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Mon, 31 Aug 2026 06:26:14 -0400 Subject: [PATCH 1/2] Revert the mysqlnd wireprotocol length-encoded bounds checks Reverted in favor of a more complete fix. --- ext/mysqli/tests/fake_server.inc | 57 ------------------- .../mysqlnd_ok_packet_message_over_read.phpt | 40 ------------- .../mysqlnd_rset_field_len_over_read.phpt | 42 -------------- .../mysqlnd_rset_field_len_past_packet.phpt | 40 ------------- ext/mysqlnd/mysqlnd_wireprotocol.c | 16 +----- 5 files changed, 2 insertions(+), 193 deletions(-) delete mode 100644 ext/mysqli/tests/mysqlnd_ok_packet_message_over_read.phpt delete mode 100644 ext/mysqli/tests/mysqlnd_rset_field_len_over_read.phpt delete mode 100644 ext/mysqli/tests/mysqlnd_rset_field_len_past_packet.phpt diff --git a/ext/mysqli/tests/fake_server.inc b/ext/mysqli/tests/fake_server.inc index 3af5c7459159..dad8bc52ddd1 100644 --- a/ext/mysqli/tests/fake_server.inc +++ b/ext/mysqli/tests/fake_server.inc @@ -721,19 +721,6 @@ function my_mysqli_test_auth_response_message_over_read(my_mysqli_fake_server_co $conn->read(); } -function my_mysqli_test_ok_packet_message_over_read(my_mysqli_fake_server_conn $conn): void -{ - $p = new my_mysqli_fake_packet(); - $p->full = "08000001" . "00" . "00" . "00" . "0200" . "0000" . "fa"; - - $conn->send_server_greetings(); - $conn->read_packets(1); - $conn->send_server_ok(); - $conn->read_packets(1); - $conn->send($p->to_bytes(), "Malicious OK Packet [message length past the packet size]"); - $conn->read(); -} - function my_mysqli_test_stmt_response_row_over_read_string(my_mysqli_fake_server_conn $conn): void { $rh = $conn->packet_generator->server_stmt_execute_items_response(); @@ -829,50 +816,6 @@ function my_mysqli_test_stmt_response_row_read_two_fields(my_mysqli_fake_server_ } } -function my_mysqli_test_rset_field_metadata_len_over_read(my_mysqli_fake_server_conn $conn): void -{ - $rh = $conn->packet_generator->server_tabular_query_response(); - - $qr2 = new my_mysqli_fake_packet(); - $qr2->packet_length = "0c0000"; - $qr2->packet_number = "02"; - $qr2->catalog_length_plus_name = "0161"; - $qr2->db_length_plus_name = "0162"; - $qr2->table_length_plus_name = "0163"; - $qr2->original_t = "0164"; - $qr2->name_length_plus_name = "0165"; - $qr2->original_n = "fcff"; - - $conn->send_server_greetings(); - $conn->read_packets(1); - $conn->send_server_ok(); - $conn->read_packets(1); - $conn->send($conn->packets_to_bytes([$rh[0], $qr2]), "Malicious Tabular Response [metadata string length past the packet size]"); - $conn->read(); -} - -function my_mysqli_test_rset_field_metadata_len_past_packet(my_mysqli_fake_server_conn $conn): void -{ - $rh = $conn->packet_generator->server_tabular_query_response(); - - $qr2 = new my_mysqli_fake_packet(); - $qr2->packet_length = "0c0000"; - $qr2->packet_number = "02"; - $qr2->catalog_length_plus_name = "0161"; - $qr2->db_length_plus_name = "0162"; - $qr2->table_length_plus_name = "0163"; - $qr2->original_t = "0164"; - $qr2->name_length_plus_name = "0165"; - $qr2->original_n = "0561"; - - $conn->send_server_greetings(); - $conn->read_packets(1); - $conn->send_server_ok(); - $conn->read_packets(1); - $conn->send($conn->packets_to_bytes([$rh[0], $qr2]), "Malicious Tabular Response [metadata string length past the packet size]"); - $conn->read(); -} - function my_mysqli_test_query_response_row_length_overflow(my_mysqli_fake_server_conn $conn): void { $rh = $conn->packet_generator->server_query_execute_data_response('strval'); diff --git a/ext/mysqli/tests/mysqlnd_ok_packet_message_over_read.phpt b/ext/mysqli/tests/mysqlnd_ok_packet_message_over_read.phpt deleted file mode 100644 index 8364251e8df4..000000000000 --- a/ext/mysqli/tests/mysqlnd_ok_packet_message_over_read.phpt +++ /dev/null @@ -1,40 +0,0 @@ ---TEST-- -mysqlnd OK packet message length buffer over-read ---EXTENSIONS-- -mysqli ---FILE-- -wait(); - -try { - $conn = new mysqli( $servername, $username, $password, "", $process->getPort()); - var_dump($conn->select_db("test")); -} catch (Exception $e) { - echo $e::class, ": ", $e->getMessage(), PHP_EOL; -} - -$process->terminate(); - -print "done!"; -?> ---EXPECTF-- -[*] Server started on 127.0.0.1:%d -[*] Connection established -[*] Sending - Server Greeting: %s -[*] Received: %s -[*] Sending - Server OK: %s -[*] Received: %s -[*] Sending - Malicious OK Packet [message length past the packet size]: %s - -Warning: mysqli::select_db(): OK packet message length is past the packet size in %s on line %d - -Warning: mysqli::select_db(): Error while reading INIT_DB's response packet. PID=%d in %s on line %d -mysqli_sql_exception: Malformed packet -done! diff --git a/ext/mysqli/tests/mysqlnd_rset_field_len_over_read.phpt b/ext/mysqli/tests/mysqlnd_rset_field_len_over_read.phpt deleted file mode 100644 index 274468ded63d..000000000000 --- a/ext/mysqli/tests/mysqlnd_rset_field_len_over_read.phpt +++ /dev/null @@ -1,42 +0,0 @@ ---TEST-- -mysqlnd result set field metadata string length buffer over-read (len clamped to packet size) ---EXTENSIONS-- -mysqli ---FILE-- -wait(); - -try { - $conn = new mysqli( $servername, $username, $password, "", $process->getPort()); - var_dump($conn->query("SELECT * from users")); -} catch (Exception $e) { - echo $e::class, ": ", $e->getMessage(), PHP_EOL; -} - -$conn->close(); - -$process->terminate(); - -print "done!"; -?> ---EXPECTF-- -[*] Server started on 127.0.0.1:%d -[*] Connection established -[*] Sending - Server Greeting: 580000000a352e352e352d31302e352e31382d4d6172696144420003000000473e3f6047257c6700fef7080200ff81150000000000000f0000006c6b55463f49335f686c6431006d7973716c5f6e61746976655f70617373776f7264 -[*] Received: %s -[*] Sending - Server OK: 0700000200000002000000 -[*] Received: %s -[*] Sending - Malicious Tabular Response [metadata string length past the packet size]: 01000001010c00000201610162016301640165fcff - -Warning: mysqli::query(): Premature end of data (mysqlnd_wireprotocol.c:%d) in %s on line %d - -Warning: mysqli::query(): Result set field packet %d bytes shorter than expected in %s on line %d -bool(false) -done! diff --git a/ext/mysqli/tests/mysqlnd_rset_field_len_past_packet.phpt b/ext/mysqli/tests/mysqlnd_rset_field_len_past_packet.phpt deleted file mode 100644 index 020d28d95b25..000000000000 --- a/ext/mysqli/tests/mysqlnd_rset_field_len_past_packet.phpt +++ /dev/null @@ -1,40 +0,0 @@ ---TEST-- -mysqlnd result set field metadata string length exceeds remaining packet bytes ---EXTENSIONS-- -mysqli ---FILE-- -wait(); - -try { - $conn = new mysqli( $servername, $username, $password, "", $process->getPort()); - var_dump($conn->query("SELECT * from users")); -} catch (Exception $e) { - echo $e::class, ": ", $e->getMessage(), PHP_EOL; -} - -$conn->close(); - -$process->terminate(); - -print "done!"; -?> ---EXPECTF-- -[*] Server started on 127.0.0.1:%d -[*] Connection established -[*] Sending - Server Greeting: %s -[*] Received: %s -[*] Sending - Server OK: %s -[*] Received: %s -[*] Sending - Malicious Tabular Response [metadata string length past the packet size]: %s - -Warning: mysqli::query(): Result set field metadata string length is past the packet size in %s on line %d -bool(false) -done! diff --git a/ext/mysqlnd/mysqlnd_wireprotocol.c b/ext/mysqlnd/mysqlnd_wireprotocol.c index f0f95a970899..64c2c7969619 100644 --- a/ext/mysqlnd/mysqlnd_wireprotocol.c +++ b/ext/mysqlnd/mysqlnd_wireprotocol.c @@ -878,12 +878,7 @@ php_mysqlnd_ok_read(MYSQLND_CONN_DATA * conn, void * _packet) /* There is a message */ if (packet->header.size > (size_t) (p - buf) && (net_len = php_mysqlnd_net_field_length(&p))) { - if ((p - buf) > packet->header.size || packet->header.size - (p - buf) < net_len) { - DBG_ERR_FMT("OK packet message length is past the packet size"); - php_error_docref(NULL, E_WARNING, "OK packet message length is past the packet size"); - DBG_RETURN(FAIL); - } - packet->message_len = net_len; + packet->message_len = MIN(net_len, buf_len - (p - begin)); packet->message = mnd_pestrndup((char *)p, packet->message_len, FALSE); } else { packet->message = NULL; @@ -1176,17 +1171,10 @@ void php_mysqlnd_rset_header_free_mem(void * _packet) /* }}} */ #define READ_RSET_FIELD(field_name) do { \ - BAIL_IF_NO_MORE_DATA; \ len = php_mysqlnd_net_field_length(&p); \ if (UNEXPECTED(len == MYSQLND_NULL_LENGTH)) { \ goto faulty_or_fake; \ } else if (len != 0) { \ - BAIL_IF_NO_MORE_DATA; \ - if (UNEXPECTED((p - begin) > packet->header.size || packet->header.size - (p - begin) < len)) { \ - DBG_ERR_FMT("Result set field metadata string length is past the packet size"); \ - php_error_docref(NULL, E_WARNING, "Result set field metadata string length is past the packet size"); \ - DBG_RETURN(FAIL); \ - } \ meta->field_name = (const char *)p; \ meta->field_name ## _length = len; \ p += len; \ @@ -1255,7 +1243,7 @@ php_mysqlnd_rset_field_read(MYSQLND_CONN_DATA * conn, void * _packet) READ_RSET_FIELD(name); READ_RSET_FIELD(org_name); - BAIL_IF_NO_MORE_DATA; + /* 1 byte length */ if (UNEXPECTED(12 != *p)) { DBG_ERR_FMT("Protocol error. Server sent false length. Expected 12 got %d", (int) *p); php_error_docref(NULL, E_WARNING, "Protocol error. Server sent false length. Expected 12"); From f74c740c6ca6e5e1a8ebd1d275c49ec66214fd70 Mon Sep 17 00:00:00 2001 From: Louis-Arnaud Date: Mon, 31 Aug 2026 14:21:14 +0200 Subject: [PATCH 2/2] ext/filter: Narrow the return type of filter_var_array() to array|false (#23403) The stub declares array|false|null, but null is unreachable. The function has two exits: RETURN_FALSE for an unknown filter id, and the array handler, which establishes an array on both of its branches before doing anything else. Its remaining exits throw. The sibling filter_input_array() is declared identically and does return null, deliberately, because its source superglobal may not exist. That case has no equivalent here, where the source is a required array parameter. null remains an ordinary element value in the returned array, which is likely where the wider union came from; that is the value type, not the return type. --- UPGRADING | 6 ++++++ Zend/Optimizer/zend_func_infos.h | 2 +- ext/filter/filter.stub.php | 2 +- ext/filter/filter_arginfo.h | 4 ++-- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/UPGRADING b/UPGRADING index 5a5cafc0234f..b6bf7cd0249c 100644 --- a/UPGRADING +++ b/UPGRADING @@ -639,6 +639,12 @@ PHP 8.6 UPGRADE NOTES 5. Changed Functions ======================================== +- Filter: + . filter_var_array() return type has been narrowed from array|false|null to + array|false. The function always establishes an array before filtering, so + null was never returned. filter_input_array() is unaffected: it still + returns null when the requested superglobal does not exist. + - GMP: . gmp_fact() now throws a ValueError if $num does not fit into an unsigned long. diff --git a/Zend/Optimizer/zend_func_infos.h b/Zend/Optimizer/zend_func_infos.h index cfe758db7961..b14de7888a94 100644 --- a/Zend/Optimizer/zend_func_infos.h +++ b/Zend/Optimizer/zend_func_infos.h @@ -95,7 +95,7 @@ static const func_info_t func_infos[] = { F1("finfo_buffer", MAY_BE_STRING|MAY_BE_FALSE), F1("mime_content_type", MAY_BE_STRING|MAY_BE_FALSE), F1("filter_input_array", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF|MAY_BE_FALSE|MAY_BE_NULL), - F1("filter_var_array", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF|MAY_BE_FALSE|MAY_BE_NULL), + F1("filter_var_array", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF|MAY_BE_FALSE), F1("filter_list", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_OF_STRING), F1("ftp_raw", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_OF_STRING|MAY_BE_NULL), F1("ftp_nlist", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_OF_STRING|MAY_BE_FALSE), diff --git a/ext/filter/filter.stub.php b/ext/filter/filter.stub.php index 4332f9261e98..16a6fe5c940c 100644 --- a/ext/filter/filter.stub.php +++ b/ext/filter/filter.stub.php @@ -310,7 +310,7 @@ function filter_var(mixed $value, int $filter = FILTER_DEFAULT, array|int $optio function filter_input_array(int $type, array|int $options = FILTER_DEFAULT, bool $add_empty = true): array|false|null {} /** @refcount 1 */ -function filter_var_array(array $array, array|int $options = FILTER_DEFAULT, bool $add_empty = true): array|false|null {} +function filter_var_array(array $array, array|int $options = FILTER_DEFAULT, bool $add_empty = true): array|false {} /** * @return array diff --git a/ext/filter/filter_arginfo.h b/ext/filter/filter_arginfo.h index 4e24ede41a63..891647362997 100644 --- a/ext/filter/filter_arginfo.h +++ b/ext/filter/filter_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit filter.stub.php instead. - * Stub hash: c3eb55dfec619af1e46be206f51a2b0893ed399f */ + * Stub hash: bd421586fdc068c456415b597d718787eb140517 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_filter_has_var, 0, 2, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, input_type, IS_LONG, 0) @@ -25,7 +25,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_filter_input_array, 0, 1, MAY_BE ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, add_empty, _IS_BOOL, 0, "true") ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_filter_var_array, 0, 1, MAY_BE_ARRAY|MAY_BE_FALSE|MAY_BE_NULL) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_filter_var_array, 0, 1, MAY_BE_ARRAY|MAY_BE_FALSE) ZEND_ARG_TYPE_INFO(0, array, IS_ARRAY, 0) ZEND_ARG_TYPE_MASK(0, options, MAY_BE_ARRAY|MAY_BE_LONG, "FILTER_DEFAULT") ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, add_empty, _IS_BOOL, 0, "true")