Merge branch 'PHP-8.1'

* PHP-8.1:
  Fix bug GH-8058 - mysqlnd segfault when prepare fails
This commit is contained in:
Kamil Tekiela 2022-02-14 12:04:11 +00:00
commit b582427ff5
No known key found for this signature in database
GPG key ID: 0760BDAB1E89A1E3
3 changed files with 57 additions and 54 deletions

View file

@ -0,0 +1,40 @@
--TEST--
GH-8058 (NULL pointer dereference in mysqlnd package (#81706))
--EXTENSIONS--
mysqli
--SKIPIF--
<?php
require_once 'skipifconnectfailure.inc';
?>
--FILE--
<?php
require_once "connect.inc";
mysqli_report(MYSQLI_REPORT_OFF);
$mysqli = new my_mysqli($host, $user, $passwd, $db, $port, $socket);
// There should be no segfault due to NULL deref
$stmt = $mysqli->prepare("select 1,2,3");
$stmt->bind_result($a,$a,$a);
$stmt->prepare("");
$stmt->prepare("select ".str_repeat("'A',", 0x1201)."1");
unset($stmt); // trigger dtor
// There should be no memory leak
$stmt = $mysqli->prepare("select 1,2,3");
$stmt->bind_result($a,$a,$a);
$stmt->prepare("");
$stmt->prepare("select 1");
unset($stmt); // trigger dtor
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$stmt = $mysqli->prepare("select 1,2,3");
try {
// We expect an exception to be thrown
$stmt->prepare("");
} catch (mysqli_sql_exception $e) {
var_dump($e->getMessage());
}
?>
--EXPECT--
string(15) "Query was empty"

View file

@ -197,18 +197,13 @@ require_once('skipifconnectfailure.inc');
/* stmt_affected_rows is not really meant for SELECT! */ /* stmt_affected_rows is not really meant for SELECT! */
if (mysqli_stmt_prepare($stmt, 'SELECT unknown_column FROM this_table_does_not_exist') || if (mysqli_stmt_prepare($stmt, 'SELECT unknown_column FROM this_table_does_not_exist') ||
mysqli_stmt_execute($stmt)) mysqli_stmt_execute($stmt))
printf("[041] The invalid SELECT statement is issued on purpose\n"); printf("[041] Expecting SELECT statement to fail on purpose\n");
if (-1 !== ($tmp = mysqli_stmt_affected_rows($stmt))) if (-1 !== ($tmp = mysqli_stmt_affected_rows($stmt)))
printf("[042] Expecting int/-1, got %s/%s\n", gettype($tmp), $tmp); printf("[042] Expecting int/-1, got %s/%s\n", gettype($tmp), $tmp);
if ($IS_MYSQLND) { if (true !== ($tmp = mysqli_stmt_store_result($stmt)))
if (false !== ($tmp = mysqli_stmt_store_result($stmt))) printf("[043] Expecting boolean/true, got %s/%s\n", gettype($tmp), $tmp);
printf("[043] Expecting boolean/false, got %s\%s\n", gettype($tmp), $tmp);
} else {
if (true !== ($tmp = mysqli_stmt_store_result($stmt)))
printf("[043] Libmysql does not care if the previous statement was bogus, expecting boolean/true, got %s\%s\n", gettype($tmp), $tmp);
}
if (0 !== ($tmp = mysqli_stmt_num_rows($stmt))) if (0 !== ($tmp = mysqli_stmt_num_rows($stmt)))
printf("[044] Expecting int/0, got %s/%s\n", gettype($tmp), $tmp); printf("[044] Expecting int/0, got %s/%s\n", gettype($tmp), $tmp);

View file

@ -369,12 +369,10 @@ mysqlnd_stmt_prepare_read_eof(MYSQLND_STMT * s)
/* {{{ mysqlnd_stmt::prepare */ /* {{{ mysqlnd_stmt::prepare */
static enum_func_status static enum_func_status
MYSQLND_METHOD(mysqlnd_stmt, prepare)(MYSQLND_STMT * const s, const char * const query, const size_t query_len) MYSQLND_METHOD(mysqlnd_stmt, prepare)(MYSQLND_STMT * s, const char * const query, const size_t query_len)
{ {
MYSQLND_STMT_DATA * stmt = s? s->data : NULL; MYSQLND_STMT_DATA * stmt = s? s->data : NULL;
MYSQLND_CONN_DATA * conn = stmt? stmt->conn : NULL; MYSQLND_CONN_DATA * conn = stmt? stmt->conn : NULL;
MYSQLND_STMT * s_to_prepare = s;
MYSQLND_STMT_DATA * stmt_to_prepare = stmt;
DBG_ENTER("mysqlnd_stmt::prepare"); DBG_ENTER("mysqlnd_stmt::prepare");
if (!stmt || !conn) { if (!stmt || !conn) {
@ -390,25 +388,15 @@ MYSQLND_METHOD(mysqlnd_stmt, prepare)(MYSQLND_STMT * const s, const char * const
SET_EMPTY_ERROR(conn->error_info); SET_EMPTY_ERROR(conn->error_info);
if (stmt->state > MYSQLND_STMT_INITTED) { if (stmt->state > MYSQLND_STMT_INITTED) {
/* See if we have to clean the wire */
if (stmt->state == MYSQLND_STMT_WAITING_USE_OR_STORE) {
/* Do implicit use_result and then flush the result */
stmt->default_rset_handler = s->m->use_result;
stmt->default_rset_handler(s);
}
/* No 'else' here please :) */
if (stmt->state > MYSQLND_STMT_WAITING_USE_OR_STORE && stmt->result) {
stmt->result->m.skip_result(stmt->result);
}
/* /*
Create a new test statement, which we will prepare, but if anything Create a new prepared statement and destroy the previous one.
fails, we will scrap it.
*/ */
s_to_prepare = conn->m->stmt_init(conn); s->m->dtor(s, TRUE);
if (!s_to_prepare) { s = conn->m->stmt_init(conn);
if (!s) {
goto fail; goto fail;
} }
stmt_to_prepare = s_to_prepare->data; stmt = s->data;
} }
{ {
@ -422,13 +410,13 @@ MYSQLND_METHOD(mysqlnd_stmt, prepare)(MYSQLND_STMT * const s, const char * const
} }
} }
if (FAIL == mysqlnd_stmt_read_prepare_response(s_to_prepare)) { if (FAIL == mysqlnd_stmt_read_prepare_response(s)) {
goto fail; goto fail;
} }
if (stmt_to_prepare->param_count) { if (stmt->param_count) {
if (FAIL == mysqlnd_stmt_skip_metadata(s_to_prepare) || if (FAIL == mysqlnd_stmt_skip_metadata(s) ||
FAIL == mysqlnd_stmt_prepare_read_eof(s_to_prepare)) FAIL == mysqlnd_stmt_prepare_read_eof(s))
{ {
goto fail; goto fail;
} }
@ -439,51 +427,31 @@ MYSQLND_METHOD(mysqlnd_stmt, prepare)(MYSQLND_STMT * const s, const char * const
Beware that SHOW statements bypass the PS framework and thus they send Beware that SHOW statements bypass the PS framework and thus they send
no metadata at prepare. no metadata at prepare.
*/ */
if (stmt_to_prepare->field_count) { if (stmt->field_count) {
MYSQLND_RES * result = conn->m->result_init(stmt_to_prepare->field_count); MYSQLND_RES * result = conn->m->result_init(stmt->field_count);
if (!result) { if (!result) {
SET_OOM_ERROR(conn->error_info); SET_OOM_ERROR(conn->error_info);
goto fail; goto fail;
} }
/* Allocate the result now as it is needed for the reading of metadata */ /* Allocate the result now as it is needed for the reading of metadata */
stmt_to_prepare->result = result; stmt->result = result;
result->conn = conn->m->get_reference(conn); result->conn = conn->m->get_reference(conn);
result->type = MYSQLND_RES_PS_BUF; result->type = MYSQLND_RES_PS_BUF;
if (FAIL == result->m.read_result_metadata(result, conn) || if (FAIL == result->m.read_result_metadata(result, conn) ||
FAIL == mysqlnd_stmt_prepare_read_eof(s_to_prepare)) FAIL == mysqlnd_stmt_prepare_read_eof(s))
{ {
goto fail; goto fail;
} }
} }
if (stmt_to_prepare != stmt) {
/* swap */
size_t real_size = sizeof(MYSQLND_STMT) + mysqlnd_plugin_count() * sizeof(void *);
char * tmp_swap = mnd_emalloc(real_size);
memcpy(tmp_swap, s, real_size);
memcpy(s, s_to_prepare, real_size);
memcpy(s_to_prepare, tmp_swap, real_size);
mnd_efree(tmp_swap);
{
MYSQLND_STMT_DATA * tmp_swap_data = stmt_to_prepare;
stmt_to_prepare = stmt;
stmt = tmp_swap_data;
}
s_to_prepare->m->dtor(s_to_prepare, TRUE);
}
stmt->state = MYSQLND_STMT_PREPARED; stmt->state = MYSQLND_STMT_PREPARED;
DBG_INF("PASS"); DBG_INF("PASS");
DBG_RETURN(PASS); DBG_RETURN(PASS);
fail: fail:
if (stmt_to_prepare != stmt && s_to_prepare) {
s_to_prepare->m->dtor(s_to_prepare, TRUE);
}
stmt->state = MYSQLND_STMT_INITTED;
DBG_INF("FAIL"); DBG_INF("FAIL");
DBG_RETURN(FAIL); DBG_RETURN(FAIL);
} }