Projects
home:rottame:vhosts-ng:php74
php7
php7-CVE-2025-1218.patch
Log In
Username
Password
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File php7-CVE-2025-1218.patch of Package php7
From 114dbb74368e6a6cd6e48ddb4a76dbe8728b84e2 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka <bukka@php.net> Date: Sun, 12 Jul 2026 23:37:54 +0200 Subject: [PATCH] Fix GHSA-r6x9-5r99-36j7: Various packet overreads in mysqlnd wireprotocol Co-authored-by: Nora Dossche <7771979+ndossche@users.noreply.github.com> Index: php-7.4.33/ext/mysqlnd/mysqlnd_ps.c =================================================================== --- php-7.4.33.orig/ext/mysqlnd/mysqlnd_ps.c +++ php-7.4.33/ext/mysqlnd/mysqlnd_ps.c @@ -373,6 +373,9 @@ mysqlnd_stmt_prepare_read_eof(MYSQLND_ST if (FAIL == (ret = PACKET_READ(conn, &fields_eof))) { if (stmt->result) { stmt->result->m.free_result_contents(stmt->result); + /* The memset() below resets the statement, so release what it still owns first. */ + conn->m->free_reference(conn); + mnd_efree(stmt->execute_cmd_buffer.buffer); /* XXX: This will crash, because we will null also the methods. But seems it happens in extreme cases or doesn't. Should be fixed by exporting a function (from mysqlnd_driver.c?) to do the reset. Index: php-7.4.33/ext/mysqlnd/mysqlnd_ps_codec.c =================================================================== --- php-7.4.33.orig/ext/mysqlnd/mysqlnd_ps_codec.c +++ php-7.4.33/ext/mysqlnd/mysqlnd_ps_codec.c @@ -65,7 +65,9 @@ static inline int ps_fetch_is_packet_ove return 0; } size_t length_len = *row - p; - if (length_len > pack_len || length > pack_len - length_len) { + /* This assert should never fire, otherwise we invoked UB earlier */ + ZEND_ASSERT(length_len <= pack_len); + if (length > pack_len - length_len) { ps_fetch_over_read_error(row); return 1; } @@ -159,6 +161,18 @@ ps_fetch_from_1_to_8_bytes(zval * zv, co } /* }}} */ +/* The declared length has to cover the fixed offsets that the field type reads. */ +static inline zend_bool ps_fetch_is_length_too_short(const zend_uchar ** row, const zend_ulong length, + const unsigned int min_length) +{ + if (UNEXPECTED(length < min_length)) { + php_error_docref(NULL, E_WARNING, "Malformed server packet. Field length is too short for the field type"); + *row = NULL; + return 1; + } + return 0; +} + /* {{{ ps_fetch_null */ static void @@ -263,11 +277,15 @@ ps_fetch_time(zval * zv, const MYSQLND_F const zend_uchar *p = *row; DBG_ENTER("ps_fetch_time"); - if ((length = php_mysqlnd_net_field_length(row))) { + if ((length = php_mysqlnd_net_field_length(row, pack_len))) { if (UNEXPECTED(ps_fetch_is_packet_over_read_with_variable_length(pack_len, row, p, length))) { return; } + if (UNEXPECTED(ps_fetch_is_length_too_short(row, length, 8))) { + return; + } + const zend_uchar * to = *row; t.time_type = MYSQLND_TIMESTAMP_TIME; @@ -277,7 +295,7 @@ ps_fetch_time(zval * zv, const MYSQLND_F t.hour = (unsigned int) to[5]; t.minute = (unsigned int) to[6]; t.second = (unsigned int) to[7]; - t.second_part = (length > 8) ? (zend_ulong) sint4korr(to+8) : 0; + t.second_part = (length >= 12) ? (zend_ulong) sint4korr(to+8) : 0; t.year = t.month= 0; if (t.day) { /* Convert days to hours at once */ @@ -325,11 +343,15 @@ ps_fetch_date(zval * zv, const MYSQLND_F const zend_uchar *p = *row; DBG_ENTER("ps_fetch_date"); - if ((length = php_mysqlnd_net_field_length(row))) { + if ((length = php_mysqlnd_net_field_length(row, pack_len))) { if (UNEXPECTED(ps_fetch_is_packet_over_read_with_variable_length(pack_len, row, p, length))) { return; } + if (UNEXPECTED(ps_fetch_is_length_too_short(row, length, 4))) { + return; + } + const zend_uchar * to = *row; t.time_type = MYSQLND_TIMESTAMP_DATE; @@ -341,7 +363,7 @@ ps_fetch_date(zval * zv, const MYSQLND_F t.month = (unsigned int) to[2]; t.day = (unsigned int) to[3]; - (*row)+= length; + (*row) += length; } else { memset(&t, 0, sizeof(t)); t.time_type = MYSQLND_TIMESTAMP_DATE; @@ -367,11 +389,15 @@ ps_fetch_datetime(zval * zv, const MYSQL const zend_uchar *p = *row; DBG_ENTER("ps_fetch_datetime"); - if ((length = php_mysqlnd_net_field_length(row))) { + if ((length = php_mysqlnd_net_field_length(row, pack_len))) { if (UNEXPECTED(ps_fetch_is_packet_over_read_with_variable_length(pack_len, row, p, length))) { return; } + if (UNEXPECTED(ps_fetch_is_length_too_short(row, length, 4))) { + return; + } + const zend_uchar * to = *row; t.time_type = MYSQLND_TIMESTAMP_DATETIME; @@ -381,16 +407,16 @@ ps_fetch_datetime(zval * zv, const MYSQL t.month = (unsigned int) to[2]; t.day = (unsigned int) to[3]; - if (length > 4) { + if (length >= 7) { t.hour = (unsigned int) to[4]; t.minute = (unsigned int) to[5]; t.second = (unsigned int) to[6]; } else { t.hour = t.minute = t.second= 0; } - t.second_part = (length > 7) ? (zend_ulong) sint4korr(to+7) : 0; + t.second_part = (length >= 11) ? (zend_ulong) sint4korr(to+7) : 0; - (*row)+= length; + (*row) += length; } else { memset(&t, 0, sizeof(t)); t.time_type = MYSQLND_TIMESTAMP_DATETIME; @@ -431,7 +457,7 @@ ps_fetch_string(zval * zv, const MYSQLND to write \0 to the row buffer */ const zend_uchar *p = *row; - const zend_ulong length = php_mysqlnd_net_field_length(row); + const zend_ulong length = php_mysqlnd_net_field_length(row, pack_len); if (UNEXPECTED(ps_fetch_is_packet_over_read_with_variable_length(pack_len, row, p, length))) { return; } @@ -451,7 +477,7 @@ static void ps_fetch_bit(zval * zv, const MYSQLND_FIELD * const field, const unsigned int pack_len, const zend_uchar ** row) { const zend_uchar *p = *row; - const zend_ulong length = php_mysqlnd_net_field_length(row); + const zend_ulong length = php_mysqlnd_net_field_length(row, pack_len); if (UNEXPECTED(ps_fetch_is_packet_over_read_with_variable_length(pack_len, row, p, length))) { return; } Index: php-7.4.33/ext/mysqlnd/mysqlnd_wireprotocol.c =================================================================== --- php-7.4.33.orig/ext/mysqlnd/mysqlnd_wireprotocol.c +++ php-7.4.33/ext/mysqlnd/mysqlnd_wireprotocol.c @@ -26,11 +26,18 @@ #include "mysqlnd_statistics.h" #include "mysqlnd_debug.h" -#define BAIL_IF_NO_MORE_DATA \ - if (UNEXPECTED((size_t)(p - begin) > packet->header.size)) { \ +#define BAIL_PREMATURE_END do { \ php_error_docref(NULL, E_WARNING, "Premature end of data (mysqlnd_wireprotocol.c:%u)", __LINE__); \ goto premature_end; \ - } \ + } while (0) + +/* Bail out unless the packet still has the required number of bytes left to read. */ +#define BAIL_IF_NOT_ENOUGH_DATA_EX(min_required_bytes) \ + if (UNEXPECTED((size_t)(p - begin) + (min_required_bytes) > packet->header.size)) { \ + BAIL_PREMATURE_END; \ + } + +#define BAIL_IF_NOT_ENOUGH_DATA BAIL_IF_NOT_ENOUGH_DATA_EX(1) static const char *unknown_sqlstate= "HY000"; @@ -92,10 +99,14 @@ static enum_mysqlnd_collected_stats pack /* {{{ php_mysqlnd_net_field_length Get next field's length */ zend_ulong -php_mysqlnd_net_field_length(const zend_uchar **packet) +php_mysqlnd_net_field_length(const zend_uchar **packet, size_t remaining_size) { register const zend_uchar *p= (const zend_uchar *)*packet; + if (UNEXPECTED(remaining_size == 0)) { + return MYSQLND_INVALID_NET_FIELD_LENGTH; + } + if (*p < 251) { (*packet)++; return (zend_ulong) *p; @@ -106,14 +117,23 @@ php_mysqlnd_net_field_length(const zend_ (*packet)++; return MYSQLND_NULL_LENGTH; case 252: - (*packet) += 3; - return (zend_ulong) uint2korr(p+1); + if (EXPECTED(remaining_size >= 3)) { + (*packet) += 3; + return (zend_ulong) uint2korr(p+1); + } + return MYSQLND_INVALID_NET_FIELD_LENGTH; case 253: - (*packet) += 4; - return (zend_ulong) uint3korr(p+1); + if (EXPECTED(remaining_size >= 4)) { + (*packet) += 4; + return (zend_ulong) uint3korr(p+1); + } + return MYSQLND_INVALID_NET_FIELD_LENGTH; default: - (*packet) += 9; - return (zend_ulong) uint4korr(p+1); + if (EXPECTED(remaining_size >= 9)) { + (*packet) += 9; + return (zend_ulong) uint4korr(p+1); + } + return MYSQLND_INVALID_NET_FIELD_LENGTH; } } /* }}} */ @@ -121,29 +141,56 @@ php_mysqlnd_net_field_length(const zend_ /* {{{ php_mysqlnd_net_field_length_ll Get next field's length */ -uint64_t -php_mysqlnd_net_field_length_ll(const zend_uchar **packet) +MYSQLND_OPTIONAL_UINT64_T +php_mysqlnd_net_field_length_ll(const zend_uchar **packet, size_t remaining_size) { register const zend_uchar *p = (zend_uchar *)*packet; + MYSQLND_OPTIONAL_UINT64_T result; + result.has_value = 0; + result.value = 0; + + if (UNEXPECTED(remaining_size == 0)) { + return result; + } + if (*p < 251) { (*packet)++; - return (uint64_t) *p; + result.value = (uint64_t) *p; + result.has_value = 1; + return result; } switch (*p) { case 251: (*packet)++; - return (uint64_t) MYSQLND_NULL_LENGTH; + result.value = MYSQLND_NULL_LENGTH; + result.has_value = 1; + return result; + case 252: - (*packet) += 3; - return (uint64_t) uint2korr(p + 1); + if (EXPECTED(remaining_size >= 3)) { + (*packet) += 3; + result.value = (uint64_t) uint2korr(p+1); + result.has_value = 1; + } + return result; + case 253: - (*packet) += 4; - return (uint64_t) uint3korr(p + 1); + if (EXPECTED(remaining_size >= 4)) { + (*packet) += 4; + result.value = (uint64_t) uint3korr(p+1); + result.has_value = 1; + } + return result; + default: - (*packet) += 9; - return (uint64_t) uint8korr(p + 1); + if (EXPECTED(remaining_size >= 9)) { + (*packet) += 9; + result.value = uint8korr(p+1); + result.has_value = 1; + } + return result; } } /* }}} */ @@ -339,22 +386,14 @@ php_mysqlnd_greet_read(MYSQLND_CONN_DATA if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, sizeof(buf), "greeting", PROT_GREET_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; packet->authentication_plugin_data.s = packet->intern_auth_plugin_data; packet->authentication_plugin_data.l = sizeof(packet->intern_auth_plugin_data); - if (packet->header.size < sizeof(buf)) { - /* - Null-terminate the string, so strdup can work even if the packets have a string at the end, - which is not ASCIIZ - */ - buf[packet->header.size] = '\0'; - } - packet->protocol_version = uint1korr(p); p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; if (ERROR_MARKER == packet->protocol_version) { php_mysqlnd_read_error_from_line(p, packet->header.size - 1, @@ -372,39 +411,38 @@ php_mysqlnd_greet_read(MYSQLND_CONN_DATA DBG_RETURN(PASS); } - packet->server_version = estrdup((char *)p); - p+= strlen(packet->server_version) + 1; /* eat the '\0' */ - BAIL_IF_NO_MORE_DATA; + /* This server version string MUST be NUL terminated, search for a NUL byte in the remaining space. */ + const char *version_nul_byte = memchr(p, '\0', packet->header.size - (p - begin)); + if (!version_nul_byte) { + BAIL_PREMATURE_END; + } + packet->server_version = estrndup((char *) p, version_nul_byte - (const char *) p); + p = (unsigned char *) version_nul_byte + 1; /* eat the '\0' */ + + BAIL_IF_NOT_ENOUGH_DATA_EX(4 + SCRAMBLE_LENGTH_323 + 1 + 2 + 1 + 2 + 13); packet->thread_id = uint4korr(p); p+=4; - BAIL_IF_NO_MORE_DATA; memcpy(packet->authentication_plugin_data.s, p, SCRAMBLE_LENGTH_323); p+= SCRAMBLE_LENGTH_323; - BAIL_IF_NO_MORE_DATA; /* pad1 */ p++; - BAIL_IF_NO_MORE_DATA; packet->server_capabilities = uint2korr(p); p+= 2; - BAIL_IF_NO_MORE_DATA; DBG_INF_FMT("4.1 server_caps=%u\n", (uint32_t) packet->server_capabilities); packet->charset_no = uint1korr(p); p++; - BAIL_IF_NO_MORE_DATA; packet->server_status = uint2korr(p); p+= 2; - BAIL_IF_NO_MORE_DATA; /* pad2 */ pad_start = p; p+= 13; - BAIL_IF_NO_MORE_DATA; if ((size_t) (p - buf) < packet->header.size) { /* auth_plugin_data is split into two parts */ @@ -426,6 +464,9 @@ php_mysqlnd_greet_read(MYSQLND_CONN_DATA /* And a length of the server scramble in one byte */ packet->authentication_plugin_data.l = uint1korr(pad_start + 2); if (packet->authentication_plugin_data.l > SCRAMBLE_LENGTH) { + /* the rest of the scramble has to be in the packet */ + BAIL_IF_NOT_ENOUGH_DATA_EX(packet->authentication_plugin_data.l - SCRAMBLE_LENGTH); + /* more data*/ char * new_auth_plugin_data = emalloc(packet->authentication_plugin_data.l); @@ -439,7 +480,9 @@ php_mysqlnd_greet_read(MYSQLND_CONN_DATA } if (packet->server_capabilities & CLIENT_PLUGIN_AUTH) { - BAIL_IF_NO_MORE_DATA; + /* This is actually checked above and it needs to pass to set extended server capabilities + * so it should never bail. */ + BAIL_IF_NOT_ENOUGH_DATA; /* The server is 5.5.x and supports authentication plugins */ packet->auth_protocol = estrdup((char *)p); p+= strlen(packet->auth_protocol) + 1; /* eat the '\0' */ @@ -454,9 +497,8 @@ php_mysqlnd_greet_read(MYSQLND_CONN_DATA DBG_RETURN(PASS); premature_end: - DBG_ERR_FMT("GREET packet %d bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "GREET packet "MYSQLND_SZ_T_SPEC" bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("GREET packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "GREET packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -661,7 +703,7 @@ php_mysqlnd_auth_response_read(MYSQLND_C if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, buf_len, "OK", PROT_OK_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; /* zero-terminate the buffer for safety. We are sure there is place for the \0 @@ -672,7 +714,7 @@ php_mysqlnd_auth_response_read(MYSQLND_C /* Should be always 0x0 or ERROR_MARKER for error */ packet->response_code = uint1korr(p); p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; if (ERROR_MARKER == packet->response_code) { php_mysqlnd_read_error_from_line(p, packet->header.size - 1, @@ -684,9 +726,14 @@ php_mysqlnd_auth_response_read(MYSQLND_C if (0xFE == packet->response_code) { /* Authentication Switch Response */ if (packet->header.size > (size_t) (p - buf)) { - packet->new_auth_protocol = mnd_pestrdup((char *)p, FALSE); - packet->new_auth_protocol_len = strlen(packet->new_auth_protocol); - p+= packet->new_auth_protocol_len + 1; /* +1 for the \0 */ + /* The plugin name MUST be NUL terminated, search for a NUL byte in the remaining space. */ + const char *auth_protocol_nul_byte = memchr(p, '\0', packet->header.size - (p - buf)); + if (!auth_protocol_nul_byte) { + BAIL_PREMATURE_END; + } + packet->new_auth_protocol_len = auth_protocol_nul_byte - (const char *) p; + packet->new_auth_protocol = mnd_pestrndup((char *) p, packet->new_auth_protocol_len, FALSE); + p = (zend_uchar *) auth_protocol_nul_byte + 1; /* eat the '\0' */ packet->new_auth_protocol_data_len = packet->header.size - (size_t) (p - buf); if (packet->new_auth_protocol_data_len) { @@ -697,27 +744,37 @@ php_mysqlnd_auth_response_read(MYSQLND_C DBG_INF_FMT("Server salt : [%d][%.*s]", packet->new_auth_protocol_data_len, packet->new_auth_protocol_data_len, packet->new_auth_protocol_data); } } else { - zend_ulong net_len; /* Everything was fine! */ - packet->affected_rows = php_mysqlnd_net_field_length_ll(&p); - BAIL_IF_NO_MORE_DATA; + zend_ulong net_len; + MYSQLND_OPTIONAL_UINT64_T len_ll; - packet->last_insert_id = php_mysqlnd_net_field_length_ll(&p); - BAIL_IF_NO_MORE_DATA; + len_ll = php_mysqlnd_net_field_length_ll(&p, packet->header.size - (p - begin)); + if (UNEXPECTED(!len_ll.has_value)) { + BAIL_PREMATURE_END; + } + packet->affected_rows = len_ll.value; + + len_ll = php_mysqlnd_net_field_length_ll(&p, packet->header.size - (p - begin)); + if (UNEXPECTED(!len_ll.has_value)) { + BAIL_PREMATURE_END; + } + packet->last_insert_id = len_ll.value; + + BAIL_IF_NOT_ENOUGH_DATA_EX(4); packet->server_status = uint2korr(p); p+= 2; - BAIL_IF_NO_MORE_DATA; packet->warning_count = uint2korr(p); p+= 2; - BAIL_IF_NO_MORE_DATA; /* There is a message */ - if (packet->header.size > (size_t) (p - buf) && (net_len = php_mysqlnd_net_field_length(&p))) { - /* p can get past packet size when getting field length so it needs to be checked first - * and after that it can be checked that the net_len is not greater than the packet size */ - if ((p - buf) > packet->header.size || packet->header.size - (p - buf) < net_len) { + if (packet->header.size > (size_t) (p - buf) && (net_len = php_mysqlnd_net_field_length(&p, packet->header.size - (p - begin)))) { + /* In older versions of mysqlnd we had to perform an extra check because reading the field length + * could overread the OK packet. Now reading the length won't go out of bounds and we only have + * to check whether the message length is in bounds. */ + ZEND_ASSERT((p - buf) <= packet->header.size); + if (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); @@ -736,9 +793,8 @@ php_mysqlnd_auth_response_read(MYSQLND_C DBG_RETURN(PASS); premature_end: - DBG_ERR_FMT("OK packet %d bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "AUTH_RESPONSE packet "MYSQLND_SZ_T_SPEC" bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("OK packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "AUTH_RESPONSE packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -831,12 +887,12 @@ php_mysqlnd_ok_read(MYSQLND_CONN_DATA * if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, buf_len, "OK", PROT_OK_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; /* Should be always 0x0 or ERROR_MARKER for error */ packet->field_count = uint1korr(p); p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; if (ERROR_MARKER == packet->field_count) { php_mysqlnd_read_error_from_line(p, packet->header.size - 1, @@ -846,22 +902,33 @@ php_mysqlnd_ok_read(MYSQLND_CONN_DATA * DBG_RETURN(PASS); } /* Everything was fine! */ - packet->affected_rows = php_mysqlnd_net_field_length_ll(&p); - BAIL_IF_NO_MORE_DATA; + MYSQLND_OPTIONAL_UINT64_T len_ll; + + len_ll = php_mysqlnd_net_field_length_ll(&p, packet->header.size - (p - begin)); + if (UNEXPECTED(!len_ll.has_value)) { + BAIL_PREMATURE_END; + } + packet->affected_rows = len_ll.value; + + len_ll = php_mysqlnd_net_field_length_ll(&p, packet->header.size - (p - begin)); + if (UNEXPECTED(!len_ll.has_value)) { + BAIL_PREMATURE_END; + } + packet->last_insert_id = len_ll.value; - packet->last_insert_id = php_mysqlnd_net_field_length_ll(&p); - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA_EX(4); packet->server_status = uint2korr(p); p+= 2; - BAIL_IF_NO_MORE_DATA; packet->warning_count = uint2korr(p); p+= 2; - BAIL_IF_NO_MORE_DATA; /* There is a message */ - if (packet->header.size > (size_t) (p - buf) && (net_len = php_mysqlnd_net_field_length(&p))) { + if (packet->header.size > (size_t) (p - buf) && (net_len = php_mysqlnd_net_field_length(&p, packet->header.size - (p - begin)))) { + if (UNEXPECTED(net_len == MYSQLND_INVALID_NET_FIELD_LENGTH)) { + BAIL_PREMATURE_END; + } packet->message_len = MIN(net_len, buf_len - (p - begin)); packet->message = mnd_pestrndup((char *)p, packet->message_len, FALSE); } else { @@ -873,13 +940,10 @@ php_mysqlnd_ok_read(MYSQLND_CONN_DATA * packet->affected_rows, packet->last_insert_id, packet->server_status, packet->warning_count); - BAIL_IF_NO_MORE_DATA; - DBG_RETURN(PASS); premature_end: - DBG_ERR_FMT("OK packet %d bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "OK packet "MYSQLND_SZ_T_SPEC" bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("OK packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "OK packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -924,12 +988,12 @@ php_mysqlnd_eof_read(MYSQLND_CONN_DATA * if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, buf_len, "EOF", PROT_EOF_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; /* Should be always EODATA_MARKER */ packet->field_count = uint1korr(p); p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; if (ERROR_MARKER == packet->field_count) { php_mysqlnd_read_error_from_line(p, packet->header.size - 1, @@ -945,28 +1009,25 @@ php_mysqlnd_eof_read(MYSQLND_CONN_DATA * according to the Docs@Forge!!! */ if (packet->header.size > 1) { + BAIL_IF_NOT_ENOUGH_DATA_EX(4); + packet->warning_count = uint2korr(p); p+= 2; - BAIL_IF_NO_MORE_DATA; packet->server_status = uint2korr(p); p+= 2; - BAIL_IF_NO_MORE_DATA; } else { packet->warning_count = 0; packet->server_status = 0; } - BAIL_IF_NO_MORE_DATA; - DBG_INF_FMT("EOF packet: fields=%u status=%u warnings=%u", packet->field_count, packet->server_status, packet->warning_count); DBG_RETURN(PASS); premature_end: - DBG_ERR_FMT("EOF packet %d bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "EOF packet "MYSQLND_SZ_T_SPEC" bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("EOF packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "EOF packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -1056,13 +1117,14 @@ php_mysqlnd_rset_header_read(MYSQLND_CON const zend_uchar * p = buf; const zend_uchar * const begin = buf; size_t len; + MYSQLND_OPTIONAL_UINT64_T len_ll; DBG_ENTER("php_mysqlnd_rset_header_read"); if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, buf_len, "resultset header", PROT_RSET_HEADER_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; /* Don't increment. First byte is ERROR_MARKER on error, but otherwise is starting byte @@ -1071,7 +1133,7 @@ php_mysqlnd_rset_header_read(MYSQLND_CON if (ERROR_MARKER == *p) { /* Error */ p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; php_mysqlnd_read_error_from_line(p, packet->header.size - 1, packet->error_info.error, sizeof(packet->error_info.error), &packet->error_info.error_no, packet->error_info.sqlstate @@ -1079,8 +1141,11 @@ php_mysqlnd_rset_header_read(MYSQLND_CON DBG_RETURN(PASS); } - packet->field_count = php_mysqlnd_net_field_length(&p); - BAIL_IF_NO_MORE_DATA; + zend_ulong field_count = php_mysqlnd_net_field_length(&p, packet->header.size - (p - begin)); + if (UNEXPECTED(field_count == MYSQLND_INVALID_NET_FIELD_LENGTH)) { + BAIL_PREMATURE_END; + } + packet->field_count = field_count; switch (packet->field_count) { case MYSQLND_NULL_LENGTH: @@ -1088,7 +1153,7 @@ php_mysqlnd_rset_header_read(MYSQLND_CON /* First byte in the packet is the field count. Thus, the name is size - 1. And we add 1 for a trailing \0. - Because we have BAIL_IF_NO_MORE_DATA before the switch, we are guaranteed + Because we have BAIL_IF_NOT_ENOUGH_DATA before the switch, we are guaranteed that packet->header.size is > 0. Which means that len can't underflow, that would lead to 0 byte allocation but 2^32 or 2^64 bytes copied. */ @@ -1105,24 +1170,34 @@ php_mysqlnd_rset_header_read(MYSQLND_CON break; case 0x00: DBG_INF("UPSERT"); - packet->affected_rows = php_mysqlnd_net_field_length_ll(&p); - BAIL_IF_NO_MORE_DATA; - packet->last_insert_id = php_mysqlnd_net_field_length_ll(&p); - BAIL_IF_NO_MORE_DATA; + len_ll = php_mysqlnd_net_field_length_ll(&p, packet->header.size - (p - begin)); + if (UNEXPECTED(!len_ll.has_value)) { + BAIL_PREMATURE_END; + } + packet->affected_rows = len_ll.value; + + len_ll = php_mysqlnd_net_field_length_ll(&p, packet->header.size - (p - begin)); + if (UNEXPECTED(!len_ll.has_value)) { + BAIL_PREMATURE_END; + } + packet->last_insert_id = len_ll.value; + + BAIL_IF_NOT_ENOUGH_DATA_EX(4); packet->server_status = uint2korr(p); p+=2; - BAIL_IF_NO_MORE_DATA; packet->warning_count = uint2korr(p); p+=2; - BAIL_IF_NO_MORE_DATA; + + len = php_mysqlnd_net_field_length(&p, packet->header.size - (p - begin)); + /* Check for additional textual data */ - if (packet->header.size > (size_t) (p - buf) && (len = php_mysqlnd_net_field_length(&p))) { - /* p can get past packet size when getting field length so it needs to be checked first - * and after that it can be checked that the len is not greater than the packet size */ - if ((p - buf) > packet->header.size || packet->header.size - (p - buf) < len) { + if (len && len != MYSQLND_INVALID_NET_FIELD_LENGTH) { + /* This checks both whether reading the len was successful + * and that the len is not greater than the packet size */ + if (packet->header.size - (p - buf) < len) { size_t local_file_name_over_read = ((p - buf) - packet->header.size) + len; DBG_ERR_FMT("RSET_HEADER packet additional data length is past %zu bytes the packet size", local_file_name_over_read); @@ -1150,13 +1225,11 @@ php_mysqlnd_rset_header_read(MYSQLND_CON /* Result set */ break; } - BAIL_IF_NO_MORE_DATA; DBG_RETURN(ret); premature_end: - DBG_ERR_FMT("RSET_HEADER packet %d bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "RSET_HEADER packet "MYSQLND_SZ_T_SPEC" bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("RSET_HEADER packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "RSET_HEADER packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -1177,8 +1250,8 @@ void php_mysqlnd_rset_header_free_mem(vo /* }}} */ #define READ_RSET_FIELD(field_name) do { \ - len = php_mysqlnd_net_field_length(&p); \ - if (UNEXPECTED(len == MYSQLND_NULL_LENGTH)) { \ + zend_ulong len = php_mysqlnd_net_field_length(&p, packet->header.size - (p - begin)); \ + if (UNEXPECTED(len > packet->header.size - (p - begin))) { \ goto faulty_or_fake; \ } else if (len != 0) { \ meta->field_name = (const char *)p; \ @@ -1222,11 +1295,11 @@ php_mysqlnd_rset_field_read(MYSQLND_CONN DBG_RETURN(PASS); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; if (ERROR_MARKER == *p) { /* Error */ p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; php_mysqlnd_read_error_from_line(p, packet->header.size - 1, packet->error_info.error, sizeof(packet->error_info.error), &packet->error_info.error_no, packet->error_info.sqlstate @@ -1291,9 +1364,8 @@ php_mysqlnd_rset_field_read(MYSQLND_CONN /* COM_FIELD_LIST is no longer supported so def should not be present */ - if (packet->header.size > (size_t) (p - buf) && - (len = php_mysqlnd_net_field_length(&p)) && - len != MYSQLND_NULL_LENGTH) + len = php_mysqlnd_net_field_length(&p, packet->header.size - (p - begin)); + if (len < MYSQLND_INVALID_NET_FIELD_LENGTH) { DBG_ERR_FMT("Protocol error. Server sent default for unsupported field list"); php_error_docref(NULL, E_WARNING, @@ -1361,9 +1433,8 @@ faulty_or_fake: " The server is faulty"); DBG_RETURN(FAIL); premature_end: - DBG_ERR_FMT("RSET field packet %d bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "Result set field packet "MYSQLND_SZ_T_SPEC" bytes " - "shorter than expected", p - begin - packet->header.size); + DBG_ERR_FMT("RSET field packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "Result set field packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -1597,12 +1668,12 @@ php_mysqlnd_rowp_read_text_protocol_aux( for (i = 0, current_field = start_field; current_field < end_field; current_field++, i++) { /* php_mysqlnd_net_field_length() call should be after *this_field_len_pos = p; */ - const zend_ulong len = php_mysqlnd_net_field_length((const zend_uchar **) &p); + zend_ulong len = php_mysqlnd_net_field_length((const zend_uchar **) &p, packet_end - p); /* NULL or NOT NULL, this is the question! */ if (len == MYSQLND_NULL_LENGTH) { ZVAL_NULL(current_field); - } else if (p > packet_end || len > packet_end - p) { + } else if (len > packet_end - p) { php_error_docref(NULL, E_WARNING, "Malformed server packet. Field length pointing after end of packet"); for (j = 0, current_field = start_field; j < i; current_field++, j++) { zval_ptr_dtor(current_field); @@ -1816,7 +1887,8 @@ php_mysqlnd_rowp_read(MYSQLND_CONN_DATA } else if (EODATA_MARKER == *p && data_size < 8) { /* EOF */ packet->eof = TRUE; p++; - if (data_size > 1) { + /* the marker is followed by 2 bytes of warnings and 2 bytes of status */ + if (data_size >= 5) { packet->warning_count = uint2korr(p); p += 2; packet->server_status = uint2korr(p); @@ -1951,12 +2023,12 @@ php_mysqlnd_prepare_read(MYSQLND_CONN_DA if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, buf_len, "prepare", PROT_PREPARE_RESP_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; data_size = packet->header.size; packet->error_code = uint1korr(p); p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; if (ERROR_MARKER == packet->error_code) { php_mysqlnd_read_error_from_line(p, data_size - 1, @@ -1976,23 +2048,23 @@ php_mysqlnd_prepare_read(MYSQLND_CONN_DA DBG_RETURN(FAIL); } + BAIL_IF_NOT_ENOUGH_DATA_EX(8); + packet->stmt_id = uint4korr(p); p += 4; - BAIL_IF_NO_MORE_DATA; /* Number of columns in result set */ packet->field_count = uint2korr(p); p += 2; - BAIL_IF_NO_MORE_DATA; packet->param_count = uint2korr(p); p += 2; - BAIL_IF_NO_MORE_DATA; if (data_size > 9) { + BAIL_IF_NOT_ENOUGH_DATA_EX(3); + /* 0x0 filler sent by the server for 5.0+ clients */ p++; - BAIL_IF_NO_MORE_DATA; packet->warning_count = uint2korr(p); } @@ -2000,13 +2072,10 @@ php_mysqlnd_prepare_read(MYSQLND_CONN_DA DBG_INF_FMT("Prepare packet read: stmt_id=%u fields=%u params=%u", packet->stmt_id, packet->field_count, packet->param_count); - BAIL_IF_NO_MORE_DATA; - DBG_RETURN(PASS); premature_end: - DBG_ERR_FMT("PREPARE packet %d bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "PREPARE packet "MYSQLND_SZ_T_SPEC" bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("PREPARE packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "PREPARE packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -2033,7 +2102,7 @@ php_mysqlnd_chg_user_read(MYSQLND_CONN_D if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, buf_len, "change user response", PROT_CHG_USER_RESP_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; /* Don't increment. First byte is ERROR_MARKER on error, but otherwise is starting byte @@ -2058,11 +2127,16 @@ php_mysqlnd_chg_user_read(MYSQLND_CONN_D packet->error_info.sqlstate ); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; if (packet->response_code == 0xFE && packet->header.size > (size_t) (p - buf)) { - packet->new_auth_protocol = mnd_pestrdup((char *)p, FALSE); - packet->new_auth_protocol_len = strlen(packet->new_auth_protocol); - p+= packet->new_auth_protocol_len + 1; /* +1 for the \0 */ + /* The plugin name MUST be NUL terminated, search for a NUL byte in the remaining space. */ + const char *auth_protocol_nul_byte = memchr(p, '\0', packet->header.size - (p - buf)); + if (!auth_protocol_nul_byte) { + BAIL_PREMATURE_END; + } + packet->new_auth_protocol_len = auth_protocol_nul_byte - (const char *) p; + packet->new_auth_protocol = mnd_pestrndup((char *) p, packet->new_auth_protocol_len, FALSE); + p = (zend_uchar *) auth_protocol_nul_byte + 1; /* eat the '\0' */ packet->new_auth_protocol_data_len = packet->header.size - (size_t) (p - buf); if (packet->new_auth_protocol_data_len) { packet->new_auth_protocol_data = mnd_emalloc(packet->new_auth_protocol_data_len); @@ -2074,9 +2148,8 @@ php_mysqlnd_chg_user_read(MYSQLND_CONN_D DBG_RETURN(PASS); premature_end: - DBG_ERR_FMT("CHANGE_USER packet %d bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "CHANGE_USER packet "MYSQLND_SZ_T_SPEC" bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("CHANGE_USER packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "CHANGE_USER packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -2146,10 +2219,10 @@ php_mysqlnd_sha256_pk_request_response_r if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, sizeof(buf), "SHA256_PK_REQUEST_RESPONSE", PROT_SHA256_PK_REQUEST_RESPONSE_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; packet->public_key_len = packet->header.size - (p - buf); packet->public_key = mnd_emalloc(packet->public_key_len + 1); @@ -2159,9 +2232,8 @@ php_mysqlnd_sha256_pk_request_response_r DBG_RETURN(PASS); premature_end: - DBG_ERR_FMT("OK packet %d bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "SHA256_PK_REQUEST_RESPONSE packet "MYSQLND_SZ_T_SPEC" bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("OK packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "SHA256_PK_REQUEST_RESPONSE packet shorter than expected"); DBG_RETURN(FAIL); } /* }}} */ @@ -2223,11 +2295,11 @@ php_mysqlnd_cached_sha2_result_read(MYSQ if (FAIL == mysqlnd_read_packet_header_and_body(&(packet->header), pfc, vio, stats, error_info, connection_state, buf, sizeof(buf), "PROT_CACHED_SHA2_RESULT_PACKET", PROT_CACHED_SHA2_RESULT_PACKET)) { DBG_RETURN(FAIL); } - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; packet->response_code = uint1korr(p); p++; - BAIL_IF_NO_MORE_DATA; + BAIL_IF_NOT_ENOUGH_DATA; if (ERROR_MARKER == packet->response_code) { php_mysqlnd_read_error_from_line(p, packet->header.size - 1, @@ -2239,9 +2311,14 @@ php_mysqlnd_cached_sha2_result_read(MYSQ if (0xFE == packet->response_code) { /* Authentication Switch Response */ if (packet->header.size > (size_t) (p - buf)) { - packet->new_auth_protocol = mnd_pestrdup((char *)p, FALSE); - packet->new_auth_protocol_len = strlen(packet->new_auth_protocol); - p+= packet->new_auth_protocol_len + 1; /* +1 for the \0 */ + /* The plugin name MUST be NUL terminated, search for a NUL byte in the remaining space. */ + const char *auth_protocol_nul_byte = memchr(p, '\0', packet->header.size - (p - buf)); + if (!auth_protocol_nul_byte) { + BAIL_PREMATURE_END; + } + packet->new_auth_protocol_len = auth_protocol_nul_byte - (const char *) p; + packet->new_auth_protocol = mnd_pestrndup((char *) p, packet->new_auth_protocol_len, FALSE); + p = (zend_uchar *) auth_protocol_nul_byte + 1; /* eat the '\0' */ packet->new_auth_protocol_data_len = packet->header.size - (size_t) (p - buf); if (packet->new_auth_protocol_data_len) { @@ -2258,20 +2335,27 @@ php_mysqlnd_cached_sha2_result_read(MYSQ DBG_ERR_FMT("Unexpected response code %d", packet->response_code); } - /* This is not really the response code, but we reuse the field. */ + BAIL_IF_NOT_ENOUGH_DATA; + + /* This is not really the response code, but we reuse the field. This should be either 0x03 if + * fast auth is used or 0x04 if full auth should be done. */ packet->response_code = uint1korr(p); - p++; - BAIL_IF_NO_MORE_DATA; - packet->result = uint1korr(p); - BAIL_IF_NO_MORE_DATA; + /* This should be removed in master and it is really kept for just in case scenario that should + * never happen because mysql-server sends only byte that is stored above. The result seems not + * to be used so there is not much point to set it in any case. */ + if (UNEXPECTED((size_t)(p - begin) + 2 <= packet->header.size)) { + p++; + packet->result = uint1korr(p); + } else { + packet->result = 0; + } DBG_RETURN(PASS); premature_end: - DBG_ERR_FMT("OK packet %d bytes shorter than expected", p - begin - packet->header.size); - php_error_docref(NULL, E_WARNING, "SHA256_PK_REQUEST_RESPONSE packet "MYSQLND_SZ_T_SPEC" bytes shorter than expected", - p - begin - packet->header.size); + DBG_ERR_FMT("OK packet shorter than expected"); + php_error_docref(NULL, E_WARNING, "SHA256_PK_REQUEST_RESPONSE packet shorter than expected"); DBG_RETURN(FAIL); } Index: php-7.4.33/ext/mysqlnd/mysqlnd_wireprotocol.h =================================================================== --- php-7.4.33.orig/ext/mysqlnd/mysqlnd_wireprotocol.h +++ php-7.4.33/ext/mysqlnd/mysqlnd_wireprotocol.h @@ -299,9 +299,19 @@ typedef struct st_mysqlnd_packet_cached_ unsigned int error_no; } MYSQLND_PACKET_CACHED_SHA2_RESULT; +/* The following structure implements an optional value, + * which forces the caller to check if the value was correctly read, for safety reasons. */ +typedef struct st_mysqlnd_optional_uint64_t { + uint64_t value; + zend_bool has_value; +} MYSQLND_OPTIONAL_UINT64_T; -zend_ulong php_mysqlnd_net_field_length(const zend_uchar **packet); +/* must not be equal to MYSQLND_NULL_LENGTH, but larger than 2**24 */ +#define MYSQLND_INVALID_NET_FIELD_LENGTH ((zend_ulong) -2) + +/* Returns MYSQLND_INVALID_NET_FIELD_LENGTH on error */ +zend_ulong php_mysqlnd_net_field_length(const zend_uchar **packet, size_t remaining_size); zend_uchar * php_mysqlnd_net_store_length(zend_uchar *packet, const uint64_t length); size_t php_mysqlnd_net_store_length_size(uint64_t length);
Locations
Projects
Search
Status Monitor
Help
Open Build Service
OBS Manuals
API Documentation
OBS Portal
Reporting a Bug
Contact
Mailing List
Forums
Chat (IRC)
Twitter
Open Build Service (OBS)
is an
openSUSE project
.