]> granicus.if.org Git - php/commitdiff
Fix bug #79375
authorDharman <tekiela246@gmail.com>
Sun, 20 Sep 2020 15:32:47 +0000 (16:32 +0100)
committerNikita Popov <nikita.ppv@gmail.com>
Wed, 28 Oct 2020 10:01:47 +0000 (11:01 +0100)
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.

NEWS
ext/mysqli/mysqli_api.c
ext/mysqli/tests/bug79375.phpt [new file with mode: 0644]
ext/mysqlnd/mysqlnd_ps.c
ext/mysqlnd/mysqlnd_result.c
ext/pdo_mysql/mysql_statement.c
ext/pdo_mysql/tests/bug79375.phpt [new file with mode: 0644]
ext/pdo_mysql/tests/bug_74376.phpt

diff --git a/NEWS b/NEWS
index 8e6bde304e44c498c98fb6a20b452965ea5bc4d7..c848134c7cc5708b59c65141e95d86f912382feb 100644 (file)
--- 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:
index 0e30f5783f35ccbeec5d56900a6fcc8928cd06b6..e164e701b42d811d81eb49edf8bfc0d4d2007f3f 100644 (file)
@@ -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 (file)
index 0000000..6c61763
--- /dev/null
@@ -0,0 +1,172 @@
+--TEST--
+Bug #79375: mysqli_store_result does not report error from lock wait timeout
+--SKIPIF--
+<?php
+require_once('skipif.inc');
+require_once('skipifconnectfailure.inc');
+if (!defined('MYSQLI_STORE_RESULT_COPY_DATA')) die('skip requires mysqlnd');
+?>
+--FILE--
+<?php
+
+require_once("connect.inc");
+mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
+$mysqli = new my_mysqli($host, $user, $passwd, $db, $port, $socket);
+$mysqli2 = new my_mysqli($host, $user, $passwd, $db, $port, $socket);
+
+$mysqli->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--
+<?php
+    require_once("clean_table.inc");
+?>
+--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
index de4e19a402ffccea9300a3a74159bc7102583700..d4d34ee500efa3199c96469d6ab7124e78d0152b 100644 (file)
@@ -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 */
index 69e47759cafda4c073cf6d47f694a54fef336ed1..f25330632779d912490445a6dc01f5020a9cc9f6 100644 (file)
@@ -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);
 }
 /* }}} */
 
index 9b880ea1fa0169f635d57a6e39118b1b9474b2f5..f3811abfe60ad5f37361a5238c750762e955cc57 100644 (file)
@@ -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 (file)
index 0000000..c7905a7
--- /dev/null
@@ -0,0 +1,113 @@
+--TEST--
+Bug #79375: mysqli_store_result does not report error from lock wait timeout
+--SKIPIF--
+<?php
+if (!extension_loaded('pdo') || !extension_loaded('pdo_mysql')) die('skip not loaded');
+require_once(__DIR__ . DIRECTORY_SEPARATOR . 'skipif.inc');
+require_once(__DIR__ . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
+MySQLPDOTest::skip();
+?>
+--FILE--
+<?php
+require_once(__DIR__ . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
+
+function createDB(): PDO {
+    $db = MySQLPDOTest::factory();
+    $db->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--
+<?php
+require __DIR__ . '/mysql_pdo_test.inc';
+MySQLPDOTest::dropTestTable();
+?>
+--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
index 5abc546d15cc46f09a33e1050cfffb1455f4bb91..77e06414302f234bb603618adda32e45f99f0e17 100644 (file)
@@ -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