Skip to content

Commit 4ceea86

Browse files
committed
Merge branch 'PHP-8.0'
* PHP-8.0: MySQLnd: Support cursors in store/get result
2 parents 73533c7 + ab84623 commit 4ceea86

File tree

4 files changed

+225
-78
lines changed

4 files changed

+225
-78
lines changed

ext/mysqli/tests/bug77935.phpt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
--TEST--
2+
Bug #77935: Crash in mysqlnd_fetch_stmt_row_cursor when calling an SP with a cursor
3+
--SKIPIF--
4+
<?php
5+
require_once('skipif.inc');
6+
require_once('skipifconnectfailure.inc');
7+
?>
8+
--FILE--
9+
<?php
10+
require_once(__DIR__ . '/connect.inc');
11+
12+
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
13+
$db = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket);
14+
$db->query('DROP PROCEDURE IF EXISTS testSp');
15+
$db->query(<<<'SQL'
16+
CREATE
17+
PROCEDURE `testSp`()
18+
BEGIN
19+
DECLARE `cur` CURSOR FOR SELECT 1;
20+
OPEN `cur`;
21+
CLOSE `cur`;
22+
SELECT 1;
23+
END;
24+
SQL);
25+
26+
$stmt = $db->prepare("CALL testSp()");
27+
$stmt->execute();
28+
$result = $stmt->get_result();
29+
while ($row = $result->fetch_assoc()) {
30+
var_dump($row);
31+
}
32+
33+
?>
34+
--EXPECT--
35+
array(1) {
36+
[1]=>
37+
int(1)
38+
}

ext/mysqli/tests/mysqli_stmt_get_result.phpt

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ if (!function_exists('mysqli_stmt_get_result'))
109109

110110
mysqli_stmt_close($stmt);
111111

112-
// get_result cannot be used in PS cursor mode
112+
// get_result can be used in PS cursor mode
113113
if (!$stmt = mysqli_stmt_init($link))
114114
printf("[030] [%d] %s\n", mysqli_errno($link), mysqli_error($link));
115115

@@ -122,23 +122,10 @@ if (!function_exists('mysqli_stmt_get_result'))
122122
if (!mysqli_stmt_execute($stmt))
123123
printf("[033] [%d] %s\n", mysqli_stmt_errno($stmt), mysqli_stmt_error($stmt));
124124

125-
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
126-
try {
127-
$res = mysqli_stmt_get_result($stmt);
128-
// we expect no segfault if we try to fetch a row because get_result should throw an error or return false
129-
mysqli_fetch_assoc($res);
130-
} catch (\mysqli_sql_exception $e) {
131-
echo $e->getMessage() . "\n";
132-
}
133-
134-
try {
135-
$res = $stmt->get_result();
136-
// we expect no segfault if we try to fetch a row because get_result should throw an error or return false
137-
$res->fetch_assoc();
138-
} catch (\mysqli_sql_exception $e) {
139-
echo $e->getMessage() . "\n";
125+
$result = mysqli_stmt_get_result($stmt);
126+
while ($row = mysqli_fetch_assoc($result)) {
127+
var_dump($row);
140128
}
141-
mysqli_report(MYSQLI_REPORT_OFF);
142129

143130
if (!$stmt = mysqli_stmt_init($link))
144131
printf("[034] [%d] %s\n", mysqli_errno($link), mysqli_error($link));
@@ -196,8 +183,18 @@ if (!function_exists('mysqli_stmt_get_result'))
196183
mysqli_stmt object is not fully initialized
197184
mysqli_stmt object is not fully initialized
198185
mysqli_stmt object is not fully initialized
199-
mysqli_stmt_get_result() cannot be used with cursors
200-
get_result() cannot be used with cursors
186+
array(2) {
187+
["id"]=>
188+
int(1)
189+
["label"]=>
190+
string(1) "a"
191+
}
192+
array(2) {
193+
["id"]=>
194+
int(2)
195+
["label"]=>
196+
string(1) "b"
197+
}
201198
[040] [2014] [Commands out of sync; you can't run this command now]
202199
[041] [0] []
203200
array(2) {
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
--TEST--
2+
PS using cursor and returning multiple result sets
3+
--SKIPIF--
4+
<?php
5+
require_once('skipif.inc');
6+
require_once('skipifconnectfailure.inc');
7+
?>
8+
--FILE--
9+
<?php
10+
require_once(__DIR__ . '/connect.inc');
11+
12+
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
13+
$db = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket);
14+
$db->query('DROP PROCEDURE IF EXISTS testPs');
15+
$db->query(<<<'SQL'
16+
CREATE PROCEDURE testPs() BEGIN
17+
DECLARE testCursor CURSOR FOR SELECT 'stuff';
18+
OPEN testCursor;
19+
CLOSE testCursor;
20+
SELECT 1 as a, 2 as b;
21+
SELECT 3 as a, 4 as b;
22+
END
23+
SQL
24+
);
25+
26+
echo "use_result:\n";
27+
$stmt = $db->prepare("call testPs()");
28+
$stmt->execute();
29+
$stmt->bind_result($v1, $v2);
30+
while ($stmt->fetch()) {
31+
var_dump($v1, $v2);
32+
}
33+
34+
$stmt->next_result();
35+
$stmt->bind_result($v1, $v2);
36+
while ($stmt->fetch()) {
37+
var_dump($v1, $v2);
38+
}
39+
$stmt->next_result();
40+
41+
echo "\nstore_result:\n";
42+
$stmt = $db->prepare("call testPs()");
43+
$stmt->execute();
44+
$stmt->store_result();
45+
$stmt->bind_result($v1, $v2);
46+
while ($stmt->fetch()) {
47+
var_dump($v1, $v2);
48+
}
49+
50+
$stmt->next_result();
51+
$stmt->store_result();
52+
$stmt->bind_result($v1, $v2);
53+
while ($stmt->fetch()) {
54+
var_dump($v1, $v2);
55+
}
56+
$stmt->next_result();
57+
58+
echo "\nget_result:\n";
59+
$stmt = $db->prepare("call testPs()");
60+
$stmt->execute();
61+
$result = $stmt->get_result();
62+
while ($row = $result->fetch_assoc()) {
63+
var_dump($row);
64+
}
65+
66+
$stmt->next_result();
67+
$result = $stmt->get_result();
68+
while ($row = $result->fetch_assoc()) {
69+
var_dump($row);
70+
}
71+
$stmt->next_result();
72+
73+
?>
74+
--EXPECT--
75+
use_result:
76+
int(1)
77+
int(2)
78+
int(3)
79+
int(4)
80+
81+
store_result:
82+
int(1)
83+
int(2)
84+
int(3)
85+
int(4)
86+
87+
get_result:
88+
array(2) {
89+
["a"]=>
90+
int(1)
91+
["b"]=>
92+
int(2)
93+
}
94+
array(2) {
95+
["a"]=>
96+
int(3)
97+
["b"]=>
98+
int(4)
99+
}

ext/mysqlnd/mysqlnd_ps.c

Lines changed: 72 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,36 @@ enum_func_status mysqlnd_stmt_execute_batch_generate_request(MYSQLND_STMT * cons
3737

3838
static void mysqlnd_stmt_separate_result_bind(MYSQLND_STMT * const stmt);
3939

40+
static enum_func_status mysqlnd_stmt_send_cursor_fetch_command(
41+
const MYSQLND_STMT_DATA *stmt, unsigned max_rows)
42+
{
43+
MYSQLND_CONN_DATA *conn = stmt->conn;
44+
zend_uchar buf[MYSQLND_STMT_ID_LENGTH /* statement id */ + 4 /* number of rows to fetch */];
45+
const MYSQLND_CSTRING payload = {(const char*) buf, sizeof(buf)};
46+
47+
int4store(buf, stmt->stmt_id);
48+
int4store(buf + MYSQLND_STMT_ID_LENGTH, max_rows);
49+
50+
if (conn->command->stmt_fetch(conn, payload) == FAIL) {
51+
COPY_CLIENT_ERROR(stmt->error_info, *conn->error_info);
52+
return FAIL;
53+
}
54+
return PASS;
55+
}
56+
57+
static zend_bool mysqlnd_stmt_check_state(const MYSQLND_STMT_DATA *stmt)
58+
{
59+
const MYSQLND_CONN_DATA *conn = stmt->conn;
60+
if (stmt->state != MYSQLND_STMT_WAITING_USE_OR_STORE) {
61+
return 0;
62+
}
63+
if (stmt->cursor_exists) {
64+
return GET_CONNECTION_STATE(&conn->state) == CONN_READY;
65+
} else {
66+
return GET_CONNECTION_STATE(&conn->state) == CONN_FETCHING_DATA;
67+
}
68+
}
69+
4070
/* {{{ mysqlnd_stmt::store_result */
4171
static MYSQLND_RES *
4272
MYSQLND_METHOD(mysqlnd_stmt, store_result)(MYSQLND_STMT * const s)
@@ -57,14 +87,8 @@ MYSQLND_METHOD(mysqlnd_stmt, store_result)(MYSQLND_STMT * const s)
5787
DBG_RETURN(NULL);
5888
}
5989

60-
if (stmt->cursor_exists) {
61-
/* Silently convert buffered to unbuffered, for now */
62-
DBG_RETURN(s->m->use_result(s));
63-
}
64-
6590
/* Nothing to store for UPSERT/LOAD DATA*/
66-
if (GET_CONNECTION_STATE(&conn->state) != CONN_FETCHING_DATA || stmt->state != MYSQLND_STMT_WAITING_USE_OR_STORE)
67-
{
91+
if (!mysqlnd_stmt_check_state(stmt)) {
6892
SET_CLIENT_ERROR(conn->error_info, CR_COMMANDS_OUT_OF_SYNC, UNKNOWN_SQLSTATE, mysqlnd_out_of_sync);
6993
DBG_RETURN(NULL);
7094
}
@@ -75,6 +99,12 @@ MYSQLND_METHOD(mysqlnd_stmt, store_result)(MYSQLND_STMT * const s)
7599
SET_EMPTY_ERROR(conn->error_info);
76100
MYSQLND_INC_CONN_STATISTIC(conn->stats, STAT_PS_BUFFERED_SETS);
77101

102+
if (stmt->cursor_exists) {
103+
if (mysqlnd_stmt_send_cursor_fetch_command(stmt, -1) == FAIL) {
104+
DBG_RETURN(NULL);
105+
}
106+
}
107+
78108
result = stmt->result;
79109
result->type = MYSQLND_RES_PS_BUF;
80110
/* result->m.row_decoder = php_mysqlnd_rowp_read_binary_protocol; */
@@ -127,19 +157,8 @@ MYSQLND_METHOD(mysqlnd_stmt, get_result)(MYSQLND_STMT * const s)
127157
DBG_RETURN(NULL);
128158
}
129159

130-
if (stmt->cursor_exists) {
131-
/* Prepared statement cursors are not supported as of yet */
132-
char * msg;
133-
mnd_sprintf(&msg, 0, "%s() cannot be used with cursors", get_active_function_name());
134-
SET_CLIENT_ERROR(stmt->error_info, CR_NOT_IMPLEMENTED, UNKNOWN_SQLSTATE, msg);
135-
if (msg) {
136-
mnd_sprintf_free(msg);
137-
}
138-
DBG_RETURN(NULL);
139-
}
140-
141160
/* Nothing to store for UPSERT/LOAD DATA*/
142-
if (GET_CONNECTION_STATE(&conn->state) != CONN_FETCHING_DATA || stmt->state != MYSQLND_STMT_WAITING_USE_OR_STORE) {
161+
if (!mysqlnd_stmt_check_state(stmt)) {
143162
SET_CLIENT_ERROR(stmt->error_info, CR_COMMANDS_OUT_OF_SYNC, UNKNOWN_SQLSTATE, mysqlnd_out_of_sync);
144163
DBG_RETURN(NULL);
145164
}
@@ -148,6 +167,12 @@ MYSQLND_METHOD(mysqlnd_stmt, get_result)(MYSQLND_STMT * const s)
148167
SET_EMPTY_ERROR(conn->error_info);
149168
MYSQLND_INC_CONN_STATISTIC(conn->stats, STAT_BUFFERED_SETS);
150169

170+
if (stmt->cursor_exists) {
171+
if (mysqlnd_stmt_send_cursor_fetch_command(stmt, -1) == FAIL) {
172+
DBG_RETURN(NULL);
173+
}
174+
}
175+
151176
do {
152177
result = conn->m->result_init(stmt->result->field_count);
153178
if (!result) {
@@ -551,28 +576,30 @@ mysqlnd_stmt_execute_parse_response(MYSQLND_STMT * const s, enum_mysqlnd_parse_e
551576
DBG_INF_FMT("server_status=%u cursor=%u", UPSERT_STATUS_GET_SERVER_STATUS(stmt->upsert_status),
552577
UPSERT_STATUS_GET_SERVER_STATUS(stmt->upsert_status) & SERVER_STATUS_CURSOR_EXISTS);
553578

554-
if (UPSERT_STATUS_GET_SERVER_STATUS(stmt->upsert_status) & SERVER_STATUS_CURSOR_EXISTS) {
555-
DBG_INF("cursor exists");
556-
stmt->cursor_exists = TRUE;
557-
SET_CONNECTION_STATE(&conn->state, CONN_READY);
558-
/* Only cursor read */
559-
stmt->default_rset_handler = s->m->use_result;
560-
DBG_INF("use_result");
561-
} else if (stmt->flags & CURSOR_TYPE_READ_ONLY) {
562-
DBG_INF("asked for cursor but got none");
563-
/*
564-
We have asked for CURSOR but got no cursor, because the condition
565-
above is not fulfilled. Then...
566-
567-
This is a single-row result set, a result set with no rows, EXPLAIN,
568-
SHOW VARIABLES, or some other command which either a) bypasses the
569-
cursors framework in the server and writes rows directly to the
570-
network or b) is more efficient if all (few) result set rows are
571-
precached on client and server's resources are freed.
572-
*/
573-
/* preferred is buffered read */
574-
stmt->default_rset_handler = s->m->store_result;
575-
DBG_INF("store_result");
579+
if (stmt->flags & CURSOR_TYPE_READ_ONLY) {
580+
if (UPSERT_STATUS_GET_SERVER_STATUS(stmt->upsert_status) & SERVER_STATUS_CURSOR_EXISTS) {
581+
DBG_INF("cursor exists");
582+
stmt->cursor_exists = TRUE;
583+
SET_CONNECTION_STATE(&conn->state, CONN_READY);
584+
/* Only cursor read */
585+
stmt->default_rset_handler = s->m->use_result;
586+
DBG_INF("use_result");
587+
} else {
588+
DBG_INF("asked for cursor but got none");
589+
/*
590+
We have asked for CURSOR but got no cursor, because the condition
591+
above is not fulfilled. Then...
592+
593+
This is a single-row result set, a result set with no rows, EXPLAIN,
594+
SHOW VARIABLES, or some other command which either a) bypasses the
595+
cursors framework in the server and writes rows directly to the
596+
network or b) is more efficient if all (few) result set rows are
597+
precached on client and server's resources are freed.
598+
*/
599+
/* preferred is buffered read */
600+
stmt->default_rset_handler = s->m->store_result;
601+
DBG_INF("store_result");
602+
}
576603
} else {
577604
DBG_INF("no cursor");
578605
/* preferred is unbuffered read */
@@ -718,11 +745,7 @@ MYSQLND_METHOD(mysqlnd_stmt, use_result)(MYSQLND_STMT * s)
718745
}
719746
DBG_INF_FMT("stmt=%lu", stmt->stmt_id);
720747

721-
if (!stmt->field_count ||
722-
(!stmt->cursor_exists && GET_CONNECTION_STATE(&conn->state) != CONN_FETCHING_DATA) ||
723-
(stmt->cursor_exists && GET_CONNECTION_STATE(&conn->state) != CONN_READY) ||
724-
(stmt->state != MYSQLND_STMT_WAITING_USE_OR_STORE))
725-
{
748+
if (!stmt->field_count || !mysqlnd_stmt_check_state(stmt)) {
726749
SET_CLIENT_ERROR(conn->error_info, CR_COMMANDS_OUT_OF_SYNC, UNKNOWN_SQLSTATE, mysqlnd_out_of_sync);
727750
DBG_ERR("command out of sync");
728751
DBG_RETURN(NULL);
@@ -752,7 +775,6 @@ mysqlnd_fetch_stmt_row_cursor(MYSQLND_RES * result, zval **row_ptr, const unsign
752775
enum_func_status ret;
753776
MYSQLND_STMT_DATA * stmt = result->unbuf->stmt;
754777
MYSQLND_CONN_DATA * conn = stmt->conn;
755-
zend_uchar buf[MYSQLND_STMT_ID_LENGTH /* statement id */ + 4 /* number of rows to fetch */];
756778
MYSQLND_PACKET_ROW * row_packet;
757779
void *checkpoint;
758780

@@ -777,18 +799,9 @@ mysqlnd_fetch_stmt_row_cursor(MYSQLND_RES * result, zval **row_ptr, const unsign
777799
SET_EMPTY_ERROR(stmt->error_info);
778800
SET_EMPTY_ERROR(conn->error_info);
779801

780-
int4store(buf, stmt->stmt_id);
781-
int4store(buf + MYSQLND_STMT_ID_LENGTH, 1); /* for now fetch only one row */
782-
783-
{
784-
const MYSQLND_CSTRING payload = {(const char*) buf, sizeof(buf)};
785-
786-
ret = conn->command->stmt_fetch(conn, payload);
787-
if (ret == FAIL) {
788-
COPY_CLIENT_ERROR(stmt->error_info, *conn->error_info);
789-
DBG_RETURN(FAIL);
790-
}
791-
802+
/* for now fetch only one row */
803+
if (mysqlnd_stmt_send_cursor_fetch_command(stmt, 1) == FAIL) {
804+
DBG_RETURN(FAIL);
792805
}
793806

794807
checkpoint = result->memory_pool->checkpoint;

0 commit comments

Comments
 (0)