Skip to content

Commit b03776a

Browse files
kamil-tekielanikic
authored andcommitted
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.
1 parent 9353f11 commit b03776a

File tree

8 files changed

+309
-7
lines changed

8 files changed

+309
-7
lines changed

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ PHP NEWS
3030
. Fixed bug #79983 (openssl_encrypt / openssl_decrypt fail with OCB mode).
3131
(Nikita)
3232

33+
- MySQLi:
34+
. Fixed bug #79375 (mysqli_store_result does not report error from lock wait
35+
timeout). (Kamil Tekiela, Nikita)
36+
3337
29 Oct 2020, PHP 7.4.12
3438

3539
- Core:

ext/mysqli/mysqli_api.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1123,7 +1123,8 @@ void mysqli_stmt_fetch_mysqlnd(INTERNAL_FUNCTION_PARAMETERS)
11231123
}
11241124
MYSQLI_FETCH_RESOURCE_STMT(stmt, mysql_stmt, MYSQLI_STATUS_VALID);
11251125

1126-
if (FAIL == mysqlnd_stmt_fetch(stmt->stmt, &fetched_anything)) {
1126+
if (FAIL == mysqlnd_stmt_fetch(stmt->stmt, &fetched_anything)) {
1127+
MYSQLI_REPORT_STMT_ERROR(stmt->stmt);
11271128
RETURN_BOOL(FALSE);
11281129
} else if (fetched_anything == TRUE) {
11291130
RETURN_BOOL(TRUE);

ext/mysqli/tests/bug79375.phpt

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
--TEST--
2+
Bug #79375: mysqli_store_result does not report error from lock wait timeout
3+
--SKIPIF--
4+
<?php
5+
require_once('skipif.inc');
6+
require_once('skipifconnectfailure.inc');
7+
if (!defined('MYSQLI_STORE_RESULT_COPY_DATA')) die('skip requires mysqlnd');
8+
?>
9+
--FILE--
10+
<?php
11+
12+
require_once("connect.inc");
13+
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
14+
$mysqli = new my_mysqli($host, $user, $passwd, $db, $port, $socket);
15+
$mysqli2 = new my_mysqli($host, $user, $passwd, $db, $port, $socket);
16+
17+
$mysqli->query('DROP TABLE IF EXISTS test');
18+
$mysqli->query('CREATE TABLE test (first int) ENGINE = InnoDB');
19+
$mysqli->query('INSERT INTO test VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9)');
20+
21+
function testStmtStoreResult(mysqli $mysqli, string $name) {
22+
$mysqli->query("SET innodb_lock_wait_timeout = 1");
23+
$mysqli->query("START TRANSACTION");
24+
$query = "SELECT first FROM test WHERE first = 1 FOR UPDATE";
25+
echo "Running query on $name\n";
26+
$stmt = $mysqli->prepare($query);
27+
$stmt->execute();
28+
try {
29+
$stmt->store_result();
30+
echo "Got {$stmt->num_rows} for $name\n";
31+
} catch(mysqli_sql_exception $e) {
32+
echo $e->getMessage()."\n";
33+
}
34+
}
35+
function testStmtGetResult(mysqli $mysqli, string $name) {
36+
$mysqli->query("SET innodb_lock_wait_timeout = 1");
37+
$mysqli->query("START TRANSACTION");
38+
$query = "SELECT first FROM test WHERE first = 1 FOR UPDATE";
39+
echo "Running query on $name\n";
40+
$stmt = $mysqli->prepare($query);
41+
$stmt->execute();
42+
try {
43+
$res = $stmt->get_result();
44+
echo "Got {$res->num_rows} for $name\n";
45+
} catch(mysqli_sql_exception $e) {
46+
echo $e->getMessage()."\n";
47+
}
48+
}
49+
function testNormalQuery(mysqli $mysqli, string $name) {
50+
$mysqli->query("SET innodb_lock_wait_timeout = 1");
51+
$mysqli->query("START TRANSACTION");
52+
$query = "SELECT first FROM test WHERE first = 1 FOR UPDATE";
53+
echo "Running query on $name\n";
54+
try {
55+
$res = $mysqli->query($query);
56+
echo "Got {$res->num_rows} for $name\n";
57+
} catch(mysqli_sql_exception $e) {
58+
echo $e->getMessage()."\n";
59+
}
60+
}
61+
function testStmtUseResult(mysqli $mysqli, string $name) {
62+
$mysqli->query("SET innodb_lock_wait_timeout = 1");
63+
$mysqli->query("START TRANSACTION");
64+
$query = "SELECT first FROM test WHERE first = 1 FOR UPDATE";
65+
echo "Running query on $name\n";
66+
$stmt = $mysqli->prepare($query);
67+
$stmt->execute();
68+
try {
69+
$stmt->fetch(); // should throw an error
70+
$stmt->fetch();
71+
echo "Got {$stmt->num_rows} for $name\n";
72+
} catch (mysqli_sql_exception $e) {
73+
echo $e->getMessage()."\n";
74+
}
75+
}
76+
function testResultFetchRow(mysqli $mysqli, string $name) {
77+
$mysqli->query("SET innodb_lock_wait_timeout = 1");
78+
$mysqli->query("START TRANSACTION");
79+
$query = "SELECT first FROM test WHERE first = 1 FOR UPDATE";
80+
echo "Running query on $name\n";
81+
$res = $mysqli->query($query, MYSQLI_USE_RESULT);
82+
try {
83+
$res->fetch_row();
84+
$res->fetch_row();
85+
echo "Got {$res->num_rows} for $name\n";
86+
} catch(mysqli_sql_exception $e) {
87+
echo $e->getMessage()."\n";
88+
}
89+
}
90+
91+
testStmtStoreResult($mysqli, 'first connection');
92+
testStmtStoreResult($mysqli2, 'second connection');
93+
94+
$mysqli->close();
95+
$mysqli2->close();
96+
97+
echo "\n";
98+
// try it again for get_result
99+
$mysqli = new my_mysqli($host, $user, $passwd, $db, $port, $socket);
100+
$mysqli2 = new my_mysqli($host, $user, $passwd, $db, $port, $socket);
101+
102+
testStmtGetResult($mysqli, 'first connection');
103+
testStmtGetResult($mysqli2, 'second connection');
104+
105+
$mysqli->close();
106+
$mysqli2->close();
107+
108+
echo "\n";
109+
// try it again with unprepared query
110+
$mysqli = new my_mysqli($host, $user, $passwd, $db, $port, $socket);
111+
$mysqli2 = new my_mysqli($host, $user, $passwd, $db, $port, $socket);
112+
113+
testNormalQuery($mysqli, 'first connection');
114+
testNormalQuery($mysqli2, 'second connection');
115+
116+
$mysqli->close();
117+
$mysqli2->close();
118+
119+
echo "\n";
120+
// try it again with unprepared query
121+
$mysqli = new my_mysqli($host, $user, $passwd, $db, $port, $socket);
122+
$mysqli2 = new my_mysqli($host, $user, $passwd, $db, $port, $socket);
123+
124+
testStmtUseResult($mysqli, 'first connection');
125+
testStmtUseResult($mysqli2, 'second connection');
126+
127+
$mysqli->close();
128+
$mysqli2->close();
129+
130+
echo "\n";
131+
// try it again using fetch_row on a result object
132+
$mysqli = new my_mysqli($host, $user, $passwd, $db, $port, $socket);
133+
$mysqli2 = new my_mysqli($host, $user, $passwd, $db, $port, $socket);
134+
135+
testResultFetchRow($mysqli, 'first connection');
136+
testResultFetchRow($mysqli2, 'second connection');
137+
138+
$mysqli->close();
139+
$mysqli2->close();
140+
141+
?>
142+
--CLEAN--
143+
<?php
144+
require_once("clean_table.inc");
145+
?>
146+
--EXPECTF--
147+
Running query on first connection
148+
Got %d for first connection
149+
Running query on second connection
150+
Lock wait timeout exceeded; try restarting transaction
151+
152+
Running query on first connection
153+
Got %d for first connection
154+
Running query on second connection
155+
Lock wait timeout exceeded; try restarting transaction
156+
157+
Running query on first connection
158+
Got %d for first connection
159+
Running query on second connection
160+
Lock wait timeout exceeded; try restarting transaction
161+
162+
Running query on first connection
163+
Got %d for first connection
164+
Running query on second connection
165+
Lock wait timeout exceeded; try restarting transaction
166+
167+
Running query on first connection
168+
Got 1 for first connection
169+
Running query on second connection
170+
171+
Warning: mysqli_result::fetch_row(): Error while reading a row in %s on line %d
172+
Got 0 for second connection

ext/mysqlnd/mysqlnd_ps.c

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,11 @@ MYSQLND_METHOD(mysqlnd_stmt, store_result)(MYSQLND_STMT * const s)
121121
stmt->state = MYSQLND_STMT_USE_OR_STORE_CALLED;
122122
} else {
123123
COPY_CLIENT_ERROR(conn->error_info, result->stored_data->error_info);
124+
COPY_CLIENT_ERROR(stmt->error_info, result->stored_data->error_info);
124125
stmt->result->m.free_result_contents(stmt->result);
125126
stmt->result = NULL;
126127
stmt->state = MYSQLND_STMT_PREPARED;
128+
DBG_RETURN(NULL);
127129
}
128130

129131
DBG_RETURN(result);
@@ -178,7 +180,7 @@ MYSQLND_METHOD(mysqlnd_stmt, get_result)(MYSQLND_STMT * const s)
178180
break;
179181
}
180182

181-
if ((result = result->m.store_result(result, conn, MYSQLND_STORE_PS | MYSQLND_STORE_NO_COPY))) {
183+
if (result->m.store_result(result, conn, MYSQLND_STORE_PS | MYSQLND_STORE_NO_COPY)) {
182184
UPSERT_STATUS_SET_AFFECTED_ROWS(stmt->upsert_status, result->stored_data->row_count);
183185
stmt->state = MYSQLND_STMT_PREPARED;
184186
result->type = MYSQLND_RES_PS_BUF;
@@ -881,7 +883,9 @@ mysqlnd_stmt_fetch_row_unbuffered(MYSQLND_RES * result, void * param, const unsi
881883
} else if (ret == FAIL) {
882884
if (row_packet->error_info.error_no) {
883885
COPY_CLIENT_ERROR(conn->error_info, row_packet->error_info);
884-
COPY_CLIENT_ERROR(stmt->error_info, row_packet->error_info);
886+
if (stmt) {
887+
COPY_CLIENT_ERROR(stmt->error_info, row_packet->error_info);
888+
}
885889
}
886890
SET_CONNECTION_STATE(&conn->state, CONN_READY);
887891
result->unbuf->eof_reached = TRUE; /* so next time we won't get an error */

ext/mysqlnd/mysqlnd_result.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -907,7 +907,7 @@ MYSQLND_METHOD(mysqlnd_result_unbuffered, fetch_row)(MYSQLND_RES * result, void
907907
result->memory_pool->checkpoint = checkpoint;
908908

909909
DBG_INF_FMT("ret=%s fetched=%u", ret == PASS? "PASS":"FAIL", *fetched_anything);
910-
DBG_RETURN(PASS);
910+
DBG_RETURN(ret);
911911
}
912912
/* }}} */
913913

ext/pdo_mysql/mysql_statement.c

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,10 @@ static int pdo_mysql_stmt_execute_prepared_libmysql(pdo_stmt_t *stmt) /* {{{ */
257257

258258
/* if buffered, pre-fetch all the data */
259259
if (H->buffered) {
260-
mysql_stmt_store_result(S->stmt);
260+
if (mysql_stmt_store_result(S->stmt)) {
261+
pdo_mysql_error_stmt(stmt);
262+
PDO_DBG_RETURN(0);
263+
}
261264
}
262265
}
263266
}
@@ -300,6 +303,7 @@ static int pdo_mysql_stmt_execute_prepared_mysqlnd(pdo_stmt_t *stmt) /* {{{ */
300303
/* if buffered, pre-fetch all the data */
301304
if (H->buffered) {
302305
if (mysql_stmt_store_result(S->stmt)) {
306+
pdo_mysql_error_stmt(stmt);
303307
PDO_DBG_RETURN(0);
304308
}
305309
}
@@ -388,7 +392,8 @@ static int pdo_mysql_stmt_next_rowset(pdo_stmt_t *stmt) /* {{{ */
388392
/* if buffered, pre-fetch all the data */
389393
if (H->buffered) {
390394
if (mysql_stmt_store_result(S->stmt)) {
391-
PDO_DBG_RETURN(1);
395+
pdo_mysql_error_stmt(stmt);
396+
PDO_DBG_RETURN(0);
392397
}
393398
}
394399
}
@@ -623,6 +628,7 @@ static int pdo_mysql_stmt_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori
623628
PDO_DBG_INF_FMT("stmt=%p", S->stmt);
624629
if (S->stmt) {
625630
if (FAIL == mysqlnd_stmt_fetch(S->stmt, &fetched_anything) || fetched_anything == FALSE) {
631+
pdo_mysql_error_stmt(stmt);
626632
PDO_DBG_RETURN(0);
627633
}
628634

ext/pdo_mysql/tests/bug79375.phpt

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
--TEST--
2+
Bug #79375: mysqli_store_result does not report error from lock wait timeout
3+
--SKIPIF--
4+
<?php
5+
if (!extension_loaded('pdo') || !extension_loaded('pdo_mysql')) die('skip not loaded');
6+
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'skipif.inc');
7+
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
8+
MySQLPDOTest::skip();
9+
?>
10+
--FILE--
11+
<?php
12+
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
13+
14+
function createDB(): PDO {
15+
$db = MySQLPDOTest::factory();
16+
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
17+
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
18+
return $db;
19+
}
20+
21+
$db = createDB();
22+
$db2 = createDB();
23+
$db->query('DROP TABLE IF EXISTS test');
24+
$db->query('CREATE TABLE test (first int) ENGINE = InnoDB');
25+
$db->query('INSERT INTO test VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9)');
26+
27+
function testNormalQuery(PDO $db, string $name) {
28+
$db->exec("SET innodb_lock_wait_timeout = 1");
29+
$db->exec("START TRANSACTION");
30+
$query = "SELECT first FROM test WHERE first = 1 FOR UPDATE";
31+
echo "Running query on $name\n";
32+
try {
33+
$stmt = $db->query($query);
34+
echo "Got {$stmt->rowCount()} for $name\n";
35+
} catch (PDOException $e) {
36+
echo $e->getMessage()."\n";
37+
}
38+
}
39+
40+
function testPrepareExecute(PDO $db, string $name) {
41+
$db->exec("SET innodb_lock_wait_timeout = 1");
42+
$db->exec("START TRANSACTION");
43+
$query = "SELECT first FROM test WHERE first = 1 FOR UPDATE";
44+
echo "Running query on $name\n";
45+
$stmt = $db->prepare($query);
46+
try {
47+
$stmt->execute();
48+
echo "Got {$stmt->rowCount()} for $name\n";
49+
} catch (PDOException $e) {
50+
echo $e->getMessage()."\n";
51+
}
52+
}
53+
54+
function testUnbuffered(PDO $db, string $name) {
55+
$db->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
56+
$db->exec("SET innodb_lock_wait_timeout = 1");
57+
$db->exec("START TRANSACTION");
58+
$query = "SELECT first FROM test WHERE first = 1 FOR UPDATE";
59+
echo "Running query on $name\n";
60+
$stmt = $db->prepare($query);
61+
$stmt->execute();
62+
try {
63+
$rows = $stmt->fetchAll();
64+
$count = count($rows);
65+
echo "Got $count for $name\n";
66+
} catch (PDOException $e) {
67+
echo $e->getMessage()."\n";
68+
}
69+
}
70+
71+
testNormalQuery($db, 'first connection');
72+
testNormalQuery($db2, 'second connection');
73+
unset($db);
74+
unset($db2);
75+
echo "\n";
76+
77+
$db = createDB();
78+
$db2 = createDB();
79+
testPrepareExecute($db, 'first connection');
80+
testPrepareExecute($db2, 'second connection');
81+
unset($db);
82+
unset($db2);
83+
echo "\n";
84+
85+
$db = createDB();
86+
$db2 = createDB();
87+
testUnbuffered($db, 'first connection');
88+
testUnbuffered($db2, 'second connection');
89+
unset($db);
90+
unset($db2);
91+
echo "\n";
92+
93+
?>
94+
--CLEAN--
95+
<?php
96+
require __DIR__ . '/mysql_pdo_test.inc';
97+
MySQLPDOTest::dropTestTable();
98+
?>
99+
--EXPECT--
100+
Running query on first connection
101+
Got 1 for first connection
102+
Running query on second connection
103+
SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction
104+
105+
Running query on first connection
106+
Got 1 for first connection
107+
Running query on second connection
108+
SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction
109+
110+
Running query on first connection
111+
Got 1 for first connection
112+
Running query on second connection
113+
SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction

ext/pdo_mysql/tests/bug_74376.phpt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,7 @@ $stmt = $db->query("select (select 1 union select 2)");
2323

2424
print "ok";
2525
?>
26-
--EXPECT--
26+
--EXPECTF--
27+
28+
Warning: PDO::query(): SQLSTATE[21000]: Cardinality violation: 1242 Subquery returns more than 1 row in %s on line %d
2729
ok

0 commit comments

Comments
 (0)