From: Dharman Date: Sun, 20 Sep 2020 15:32:47 +0000 (+0100) Subject: Fix bug #79375 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=b03776adb5bbb9b54731a44377632fcc94a59d2f;p=php Fix bug #79375 Make sure deadlock errors are properly propagated and reports in a number of places in mysqli and PDO MySQL. This also fixes a memory and a segfault that can occur under these conditions. --- diff --git a/NEWS b/NEWS index 8e6bde304e..c848134c7c 100644 --- a/NEWS +++ b/NEWS @@ -30,6 +30,10 @@ PHP NEWS . Fixed bug #79983 (openssl_encrypt / openssl_decrypt fail with OCB mode). (Nikita) +- MySQLi: + . Fixed bug #79375 (mysqli_store_result does not report error from lock wait + timeout). (Kamil Tekiela, Nikita) + 29 Oct 2020, PHP 7.4.12 - Core: diff --git a/ext/mysqli/mysqli_api.c b/ext/mysqli/mysqli_api.c index 0e30f5783f..e164e701b4 100644 --- a/ext/mysqli/mysqli_api.c +++ b/ext/mysqli/mysqli_api.c @@ -1123,7 +1123,8 @@ void mysqli_stmt_fetch_mysqlnd(INTERNAL_FUNCTION_PARAMETERS) } MYSQLI_FETCH_RESOURCE_STMT(stmt, mysql_stmt, MYSQLI_STATUS_VALID); - if (FAIL == mysqlnd_stmt_fetch(stmt->stmt, &fetched_anything)) { + if (FAIL == mysqlnd_stmt_fetch(stmt->stmt, &fetched_anything)) { + MYSQLI_REPORT_STMT_ERROR(stmt->stmt); RETURN_BOOL(FALSE); } else if (fetched_anything == TRUE) { RETURN_BOOL(TRUE); diff --git a/ext/mysqli/tests/bug79375.phpt b/ext/mysqli/tests/bug79375.phpt new file mode 100644 index 0000000000..6c6176311d --- /dev/null +++ b/ext/mysqli/tests/bug79375.phpt @@ -0,0 +1,172 @@ +--TEST-- +Bug #79375: mysqli_store_result does not report error from lock wait timeout +--SKIPIF-- + +--FILE-- +query('DROP TABLE IF EXISTS test'); +$mysqli->query('CREATE TABLE test (first int) ENGINE = InnoDB'); +$mysqli->query('INSERT INTO test VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9)'); + +function testStmtStoreResult(mysqli $mysqli, string $name) { + $mysqli->query("SET innodb_lock_wait_timeout = 1"); + $mysqli->query("START TRANSACTION"); + $query = "SELECT first FROM test WHERE first = 1 FOR UPDATE"; + echo "Running query on $name\n"; + $stmt = $mysqli->prepare($query); + $stmt->execute(); + try { + $stmt->store_result(); + echo "Got {$stmt->num_rows} for $name\n"; + } catch(mysqli_sql_exception $e) { + echo $e->getMessage()."\n"; + } +} +function testStmtGetResult(mysqli $mysqli, string $name) { + $mysqli->query("SET innodb_lock_wait_timeout = 1"); + $mysqli->query("START TRANSACTION"); + $query = "SELECT first FROM test WHERE first = 1 FOR UPDATE"; + echo "Running query on $name\n"; + $stmt = $mysqli->prepare($query); + $stmt->execute(); + try { + $res = $stmt->get_result(); + echo "Got {$res->num_rows} for $name\n"; + } catch(mysqli_sql_exception $e) { + echo $e->getMessage()."\n"; + } +} +function testNormalQuery(mysqli $mysqli, string $name) { + $mysqli->query("SET innodb_lock_wait_timeout = 1"); + $mysqli->query("START TRANSACTION"); + $query = "SELECT first FROM test WHERE first = 1 FOR UPDATE"; + echo "Running query on $name\n"; + try { + $res = $mysqli->query($query); + echo "Got {$res->num_rows} for $name\n"; + } catch(mysqli_sql_exception $e) { + echo $e->getMessage()."\n"; + } +} +function testStmtUseResult(mysqli $mysqli, string $name) { + $mysqli->query("SET innodb_lock_wait_timeout = 1"); + $mysqli->query("START TRANSACTION"); + $query = "SELECT first FROM test WHERE first = 1 FOR UPDATE"; + echo "Running query on $name\n"; + $stmt = $mysqli->prepare($query); + $stmt->execute(); + try { + $stmt->fetch(); // should throw an error + $stmt->fetch(); + echo "Got {$stmt->num_rows} for $name\n"; + } catch (mysqli_sql_exception $e) { + echo $e->getMessage()."\n"; + } +} +function testResultFetchRow(mysqli $mysqli, string $name) { + $mysqli->query("SET innodb_lock_wait_timeout = 1"); + $mysqli->query("START TRANSACTION"); + $query = "SELECT first FROM test WHERE first = 1 FOR UPDATE"; + echo "Running query on $name\n"; + $res = $mysqli->query($query, MYSQLI_USE_RESULT); + try { + $res->fetch_row(); + $res->fetch_row(); + echo "Got {$res->num_rows} for $name\n"; + } catch(mysqli_sql_exception $e) { + echo $e->getMessage()."\n"; + } +} + +testStmtStoreResult($mysqli, 'first connection'); +testStmtStoreResult($mysqli2, 'second connection'); + +$mysqli->close(); +$mysqli2->close(); + +echo "\n"; +// try it again for get_result +$mysqli = new my_mysqli($host, $user, $passwd, $db, $port, $socket); +$mysqli2 = new my_mysqli($host, $user, $passwd, $db, $port, $socket); + +testStmtGetResult($mysqli, 'first connection'); +testStmtGetResult($mysqli2, 'second connection'); + +$mysqli->close(); +$mysqli2->close(); + +echo "\n"; +// try it again with unprepared query +$mysqli = new my_mysqli($host, $user, $passwd, $db, $port, $socket); +$mysqli2 = new my_mysqli($host, $user, $passwd, $db, $port, $socket); + +testNormalQuery($mysqli, 'first connection'); +testNormalQuery($mysqli2, 'second connection'); + +$mysqli->close(); +$mysqli2->close(); + +echo "\n"; +// try it again with unprepared query +$mysqli = new my_mysqli($host, $user, $passwd, $db, $port, $socket); +$mysqli2 = new my_mysqli($host, $user, $passwd, $db, $port, $socket); + +testStmtUseResult($mysqli, 'first connection'); +testStmtUseResult($mysqli2, 'second connection'); + +$mysqli->close(); +$mysqli2->close(); + +echo "\n"; +// try it again using fetch_row on a result object +$mysqli = new my_mysqli($host, $user, $passwd, $db, $port, $socket); +$mysqli2 = new my_mysqli($host, $user, $passwd, $db, $port, $socket); + +testResultFetchRow($mysqli, 'first connection'); +testResultFetchRow($mysqli2, 'second connection'); + +$mysqli->close(); +$mysqli2->close(); + +?> +--CLEAN-- + +--EXPECTF-- +Running query on first connection +Got %d for first connection +Running query on second connection +Lock wait timeout exceeded; try restarting transaction + +Running query on first connection +Got %d for first connection +Running query on second connection +Lock wait timeout exceeded; try restarting transaction + +Running query on first connection +Got %d for first connection +Running query on second connection +Lock wait timeout exceeded; try restarting transaction + +Running query on first connection +Got %d for first connection +Running query on second connection +Lock wait timeout exceeded; try restarting transaction + +Running query on first connection +Got 1 for first connection +Running query on second connection + +Warning: mysqli_result::fetch_row(): Error while reading a row in %s on line %d +Got 0 for second connection diff --git a/ext/mysqlnd/mysqlnd_ps.c b/ext/mysqlnd/mysqlnd_ps.c index de4e19a402..d4d34ee500 100644 --- a/ext/mysqlnd/mysqlnd_ps.c +++ b/ext/mysqlnd/mysqlnd_ps.c @@ -121,9 +121,11 @@ MYSQLND_METHOD(mysqlnd_stmt, store_result)(MYSQLND_STMT * const s) stmt->state = MYSQLND_STMT_USE_OR_STORE_CALLED; } else { COPY_CLIENT_ERROR(conn->error_info, result->stored_data->error_info); + COPY_CLIENT_ERROR(stmt->error_info, result->stored_data->error_info); stmt->result->m.free_result_contents(stmt->result); stmt->result = NULL; stmt->state = MYSQLND_STMT_PREPARED; + DBG_RETURN(NULL); } DBG_RETURN(result); @@ -178,7 +180,7 @@ MYSQLND_METHOD(mysqlnd_stmt, get_result)(MYSQLND_STMT * const s) break; } - if ((result = result->m.store_result(result, conn, MYSQLND_STORE_PS | MYSQLND_STORE_NO_COPY))) { + if (result->m.store_result(result, conn, MYSQLND_STORE_PS | MYSQLND_STORE_NO_COPY)) { UPSERT_STATUS_SET_AFFECTED_ROWS(stmt->upsert_status, result->stored_data->row_count); stmt->state = MYSQLND_STMT_PREPARED; result->type = MYSQLND_RES_PS_BUF; @@ -881,7 +883,9 @@ mysqlnd_stmt_fetch_row_unbuffered(MYSQLND_RES * result, void * param, const unsi } else if (ret == FAIL) { if (row_packet->error_info.error_no) { COPY_CLIENT_ERROR(conn->error_info, row_packet->error_info); - COPY_CLIENT_ERROR(stmt->error_info, row_packet->error_info); + if (stmt) { + COPY_CLIENT_ERROR(stmt->error_info, row_packet->error_info); + } } SET_CONNECTION_STATE(&conn->state, CONN_READY); result->unbuf->eof_reached = TRUE; /* so next time we won't get an error */ diff --git a/ext/mysqlnd/mysqlnd_result.c b/ext/mysqlnd/mysqlnd_result.c index 69e47759ca..f253306327 100644 --- a/ext/mysqlnd/mysqlnd_result.c +++ b/ext/mysqlnd/mysqlnd_result.c @@ -907,7 +907,7 @@ MYSQLND_METHOD(mysqlnd_result_unbuffered, fetch_row)(MYSQLND_RES * result, void result->memory_pool->checkpoint = checkpoint; DBG_INF_FMT("ret=%s fetched=%u", ret == PASS? "PASS":"FAIL", *fetched_anything); - DBG_RETURN(PASS); + DBG_RETURN(ret); } /* }}} */ diff --git a/ext/pdo_mysql/mysql_statement.c b/ext/pdo_mysql/mysql_statement.c index 9b880ea1fa..f3811abfe6 100644 --- a/ext/pdo_mysql/mysql_statement.c +++ b/ext/pdo_mysql/mysql_statement.c @@ -257,7 +257,10 @@ static int pdo_mysql_stmt_execute_prepared_libmysql(pdo_stmt_t *stmt) /* {{{ */ /* if buffered, pre-fetch all the data */ if (H->buffered) { - mysql_stmt_store_result(S->stmt); + if (mysql_stmt_store_result(S->stmt)) { + pdo_mysql_error_stmt(stmt); + PDO_DBG_RETURN(0); + } } } } @@ -300,6 +303,7 @@ static int pdo_mysql_stmt_execute_prepared_mysqlnd(pdo_stmt_t *stmt) /* {{{ */ /* if buffered, pre-fetch all the data */ if (H->buffered) { if (mysql_stmt_store_result(S->stmt)) { + pdo_mysql_error_stmt(stmt); PDO_DBG_RETURN(0); } } @@ -388,7 +392,8 @@ static int pdo_mysql_stmt_next_rowset(pdo_stmt_t *stmt) /* {{{ */ /* if buffered, pre-fetch all the data */ if (H->buffered) { if (mysql_stmt_store_result(S->stmt)) { - PDO_DBG_RETURN(1); + pdo_mysql_error_stmt(stmt); + PDO_DBG_RETURN(0); } } } @@ -623,6 +628,7 @@ static int pdo_mysql_stmt_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori PDO_DBG_INF_FMT("stmt=%p", S->stmt); if (S->stmt) { if (FAIL == mysqlnd_stmt_fetch(S->stmt, &fetched_anything) || fetched_anything == FALSE) { + pdo_mysql_error_stmt(stmt); PDO_DBG_RETURN(0); } diff --git a/ext/pdo_mysql/tests/bug79375.phpt b/ext/pdo_mysql/tests/bug79375.phpt new file mode 100644 index 0000000000..c7905a7018 --- /dev/null +++ b/ext/pdo_mysql/tests/bug79375.phpt @@ -0,0 +1,113 @@ +--TEST-- +Bug #79375: mysqli_store_result does not report error from lock wait timeout +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); + return $db; +} + +$db = createDB(); +$db2 = createDB(); +$db->query('DROP TABLE IF EXISTS test'); +$db->query('CREATE TABLE test (first int) ENGINE = InnoDB'); +$db->query('INSERT INTO test VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9)'); + +function testNormalQuery(PDO $db, string $name) { + $db->exec("SET innodb_lock_wait_timeout = 1"); + $db->exec("START TRANSACTION"); + $query = "SELECT first FROM test WHERE first = 1 FOR UPDATE"; + echo "Running query on $name\n"; + try { + $stmt = $db->query($query); + echo "Got {$stmt->rowCount()} for $name\n"; + } catch (PDOException $e) { + echo $e->getMessage()."\n"; + } +} + +function testPrepareExecute(PDO $db, string $name) { + $db->exec("SET innodb_lock_wait_timeout = 1"); + $db->exec("START TRANSACTION"); + $query = "SELECT first FROM test WHERE first = 1 FOR UPDATE"; + echo "Running query on $name\n"; + $stmt = $db->prepare($query); + try { + $stmt->execute(); + echo "Got {$stmt->rowCount()} for $name\n"; + } catch (PDOException $e) { + echo $e->getMessage()."\n"; + } +} + +function testUnbuffered(PDO $db, string $name) { + $db->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); + $db->exec("SET innodb_lock_wait_timeout = 1"); + $db->exec("START TRANSACTION"); + $query = "SELECT first FROM test WHERE first = 1 FOR UPDATE"; + echo "Running query on $name\n"; + $stmt = $db->prepare($query); + $stmt->execute(); + try { + $rows = $stmt->fetchAll(); + $count = count($rows); + echo "Got $count for $name\n"; + } catch (PDOException $e) { + echo $e->getMessage()."\n"; + } +} + +testNormalQuery($db, 'first connection'); +testNormalQuery($db2, 'second connection'); +unset($db); +unset($db2); +echo "\n"; + +$db = createDB(); +$db2 = createDB(); +testPrepareExecute($db, 'first connection'); +testPrepareExecute($db2, 'second connection'); +unset($db); +unset($db2); +echo "\n"; + +$db = createDB(); +$db2 = createDB(); +testUnbuffered($db, 'first connection'); +testUnbuffered($db2, 'second connection'); +unset($db); +unset($db2); +echo "\n"; + +?> +--CLEAN-- + +--EXPECT-- +Running query on first connection +Got 1 for first connection +Running query on second connection +SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction + +Running query on first connection +Got 1 for first connection +Running query on second connection +SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction + +Running query on first connection +Got 1 for first connection +Running query on second connection +SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction diff --git a/ext/pdo_mysql/tests/bug_74376.phpt b/ext/pdo_mysql/tests/bug_74376.phpt index 5abc546d15..77e0641430 100644 --- a/ext/pdo_mysql/tests/bug_74376.phpt +++ b/ext/pdo_mysql/tests/bug_74376.phpt @@ -23,5 +23,7 @@ $stmt = $db->query("select (select 1 union select 2)"); print "ok"; ?> ---EXPECT-- +--EXPECTF-- + +Warning: PDO::query(): SQLSTATE[21000]: Cardinality violation: 1242 Subquery returns more than 1 row in %s on line %d ok