MySQLnd: Support cursors in store/get result

This fixes two related issues:

1. When a PS with cursor is used in store_result/get_result,
   perform a COM_FETCH with maximum number of rows rather than
   silently switching to an unbuffered result set (in the case of
   store_result) or erroring (in the case of get_result).
   In the future, we might want to make get_result unbuffered for
   PS with cursors, as using cursors with buffered result sets
   doesn't really make sense. Unlike store_result, get_result
   isn't very explicit about what kind of result set is desired.
2. If the client did not request a cursor, but the server reports
   that a cursor exists, ignore this and treat the PS as if it
   has no cursor (i.e. to not use COM_FETCH). It appears to be a
   server side bug that a cursor used inside an SP will be reported
   to the client, even though the client cannot use the cursor.

Fixes bug #64638, bug #72862, bug #77935.

Closes GH-6518.
This commit is contained in:
Nikita Popov 2020-12-16 12:12:06 +01:00
parent 315f3f8dc9
commit bc166844e3
5 changed files with 230 additions and 91 deletions

6
NEWS
View file

@ -8,6 +8,12 @@ PHP NEWS
- MySQLi:
. Fixed bug #67983 (mysqlnd with MYSQLI_OPT_INT_AND_FLOAT_NATIVE fails to
interpret bit columns). (Nikita)
. Fixed bug #64638 (Fetching resultsets from stored procedure with cursor
fails). (Nikita)
. Fixed bug #72862 (segfault using prepared statements on stored procedures
that use a cursor). (Nikita)
. Fixed bug #77935 (Crash in mysqlnd_fetch_stmt_row_cursor when calling an SP
with a cursor). (Nikita)
07 Jan 2021, PHP 7.4.14

View file

@ -0,0 +1,38 @@
--TEST--
Bug #77935: Crash in mysqlnd_fetch_stmt_row_cursor when calling an SP with a cursor
--SKIPIF--
<?php
require_once('skipif.inc');
require_once('skipifconnectfailure.inc');
?>
--FILE--
<?php
require_once(__DIR__ . '/connect.inc');
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$db = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket);
$db->query('DROP PROCEDURE IF EXISTS testSp');
$db->query(<<<'SQL'
CREATE
PROCEDURE `testSp`()
BEGIN
DECLARE `cur` CURSOR FOR SELECT 1;
OPEN `cur`;
CLOSE `cur`;
SELECT 1;
END;
SQL);
$stmt = $db->prepare("CALL testSp()");
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
var_dump($row);
}
?>
--EXPECT--
array(1) {
[1]=>
int(1)
}

View file

@ -110,21 +110,7 @@ if (!function_exists('mysqli_stmt_get_result'))
mysqli_stmt_close($stmt);
if (!$stmt = mysqli_stmt_init($link))
printf("[032] [%d] %s\n", mysqli_errno($link), mysqli_error($link));
if (!mysqli_stmt_prepare($stmt, "SELECT id, label FROM test ORDER BY id LIMIT 2"))
printf("[033] [%d] %s\n", mysqli_stmt_errno($stmt), mysqli_stmt_error($stmt));
if (!mysqli_stmt_execute($stmt))
printf("[034] [%d] %s\n", mysqli_stmt_errno($stmt), mysqli_stmt_error($stmt));
$id = NULL;
$label = NULL;
if (true !== ($tmp = mysqli_stmt_bind_result($stmt, $id, $label)))
printf("[035] Expecting boolean/true, got %s/%s\n", gettype($tmp), var_export($tmp, 1));
// get_result cannot be used in PS cursor mode
// get_result can be used in PS cursor mode
if (!$stmt = mysqli_stmt_init($link))
printf("[030] [%d] %s\n", mysqli_errno($link), mysqli_error($link));
@ -137,24 +123,11 @@ if (!function_exists('mysqli_stmt_get_result'))
if (!mysqli_stmt_execute($stmt))
printf("[033] [%d] %s\n", mysqli_stmt_errno($stmt), mysqli_stmt_error($stmt));
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
try {
$res = mysqli_stmt_get_result($stmt);
// we expect no segfault if we try to fetch a row because get_result should throw an error or return false
mysqli_fetch_assoc($res);
} catch (\mysqli_sql_exception $e) {
echo $e->getMessage() . "\n";
$result = mysqli_stmt_get_result($stmt);
while ($row = mysqli_fetch_assoc($result)) {
var_dump($row);
}
try {
$res = $stmt->get_result();
// we expect no segfault if we try to fetch a row because get_result should throw an error or return false
$res->fetch_assoc();
} catch (\mysqli_sql_exception $e) {
echo $e->getMessage() . "\n";
}
mysqli_report(MYSQLI_REPORT_OFF);
if (!$stmt = mysqli_stmt_init($link))
printf("[034] [%d] %s\n", mysqli_errno($link), mysqli_error($link));
@ -213,8 +186,18 @@ Warning: mysqli_stmt_fetch(): invalid object or resource mysqli_stmt
Warning: mysqli_stmt_get_result(): invalid object or resource mysqli_stmt
in %s on line %d
mysqli_stmt_get_result() cannot be used with cursors
get_result() cannot be used with cursors
array(2) {
["id"]=>
int(1)
["label"]=>
string(1) "a"
}
array(2) {
["id"]=>
int(2)
["label"]=>
string(1) "b"
}
[040] [2014] [Commands out of sync; you can't run this command now]
[041] [0] []
array(2) {

View file

@ -0,0 +1,99 @@
--TEST--
PS using cursor and returning multiple result sets
--SKIPIF--
<?php
require_once('skipif.inc');
require_once('skipifconnectfailure.inc');
?>
--FILE--
<?php
require_once(__DIR__ . '/connect.inc');
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$db = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket);
$db->query('DROP PROCEDURE IF EXISTS testPs');
$db->query(<<<'SQL'
CREATE PROCEDURE testPs() BEGIN
DECLARE testCursor CURSOR FOR SELECT 'stuff';
OPEN testCursor;
CLOSE testCursor;
SELECT 1 as a, 2 as b;
SELECT 3 as a, 4 as b;
END
SQL
);
echo "use_result:\n";
$stmt = $db->prepare("call testPs()");
$stmt->execute();
$stmt->bind_result($v1, $v2);
while ($stmt->fetch()) {
var_dump($v1, $v2);
}
$stmt->next_result();
$stmt->bind_result($v1, $v2);
while ($stmt->fetch()) {
var_dump($v1, $v2);
}
$stmt->next_result();
echo "\nstore_result:\n";
$stmt = $db->prepare("call testPs()");
$stmt->execute();
$stmt->store_result();
$stmt->bind_result($v1, $v2);
while ($stmt->fetch()) {
var_dump($v1, $v2);
}
$stmt->next_result();
$stmt->store_result();
$stmt->bind_result($v1, $v2);
while ($stmt->fetch()) {
var_dump($v1, $v2);
}
$stmt->next_result();
echo "\nget_result:\n";
$stmt = $db->prepare("call testPs()");
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
var_dump($row);
}
$stmt->next_result();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
var_dump($row);
}
$stmt->next_result();
?>
--EXPECT--
use_result:
int(1)
int(2)
int(3)
int(4)
store_result:
int(1)
int(2)
int(3)
int(4)
get_result:
array(2) {
["a"]=>
int(1)
["b"]=>
int(2)
}
array(2) {
["a"]=>
int(3)
["b"]=>
int(4)
}

View file

@ -40,6 +40,36 @@ enum_func_status mysqlnd_stmt_execute_batch_generate_request(MYSQLND_STMT * cons
static void mysqlnd_stmt_separate_result_bind(MYSQLND_STMT * const stmt);
static void mysqlnd_stmt_separate_one_result_bind(MYSQLND_STMT * const stmt, const unsigned int param_no);
static enum_func_status mysqlnd_stmt_send_cursor_fetch_command(
const MYSQLND_STMT_DATA *stmt, unsigned max_rows)
{
MYSQLND_CONN_DATA *conn = stmt->conn;
zend_uchar buf[MYSQLND_STMT_ID_LENGTH /* statement id */ + 4 /* number of rows to fetch */];
const MYSQLND_CSTRING payload = {(const char*) buf, sizeof(buf)};
int4store(buf, stmt->stmt_id);
int4store(buf + MYSQLND_STMT_ID_LENGTH, max_rows);
if (conn->command->stmt_fetch(conn, payload) == FAIL) {
COPY_CLIENT_ERROR(stmt->error_info, *conn->error_info);
return FAIL;
}
return PASS;
}
static zend_bool mysqlnd_stmt_check_state(const MYSQLND_STMT_DATA *stmt)
{
const MYSQLND_CONN_DATA *conn = stmt->conn;
if (stmt->state != MYSQLND_STMT_WAITING_USE_OR_STORE) {
return 0;
}
if (stmt->cursor_exists) {
return GET_CONNECTION_STATE(&conn->state) == CONN_READY;
} else {
return GET_CONNECTION_STATE(&conn->state) == CONN_FETCHING_DATA;
}
}
/* {{{ mysqlnd_stmt::store_result */
static MYSQLND_RES *
MYSQLND_METHOD(mysqlnd_stmt, store_result)(MYSQLND_STMT * const s)
@ -60,14 +90,8 @@ MYSQLND_METHOD(mysqlnd_stmt, store_result)(MYSQLND_STMT * const s)
DBG_RETURN(NULL);
}
if (stmt->cursor_exists) {
/* Silently convert buffered to unbuffered, for now */
DBG_RETURN(s->m->use_result(s));
}
/* Nothing to store for UPSERT/LOAD DATA*/
if (GET_CONNECTION_STATE(&conn->state) != CONN_FETCHING_DATA || stmt->state != MYSQLND_STMT_WAITING_USE_OR_STORE)
{
if (!mysqlnd_stmt_check_state(stmt)) {
SET_CLIENT_ERROR(conn->error_info, CR_COMMANDS_OUT_OF_SYNC, UNKNOWN_SQLSTATE, mysqlnd_out_of_sync);
DBG_RETURN(NULL);
}
@ -78,6 +102,12 @@ MYSQLND_METHOD(mysqlnd_stmt, store_result)(MYSQLND_STMT * const s)
SET_EMPTY_ERROR(conn->error_info);
MYSQLND_INC_CONN_STATISTIC(conn->stats, STAT_PS_BUFFERED_SETS);
if (stmt->cursor_exists) {
if (mysqlnd_stmt_send_cursor_fetch_command(stmt, -1) == FAIL) {
DBG_RETURN(NULL);
}
}
result = stmt->result;
result->type = MYSQLND_RES_PS_BUF;
/* result->m.row_decoder = php_mysqlnd_rowp_read_binary_protocol; */
@ -152,19 +182,8 @@ MYSQLND_METHOD(mysqlnd_stmt, get_result)(MYSQLND_STMT * const s)
DBG_RETURN(NULL);
}
if (stmt->cursor_exists) {
/* Prepared statement cursors are not supported as of yet */
char * msg;
mnd_sprintf(&msg, 0, "%s() cannot be used with cursors", get_active_function_name());
SET_CLIENT_ERROR(stmt->error_info, CR_NOT_IMPLEMENTED, UNKNOWN_SQLSTATE, msg);
if (msg) {
mnd_sprintf_free(msg);
}
DBG_RETURN(NULL);
}
/* Nothing to store for UPSERT/LOAD DATA*/
if (GET_CONNECTION_STATE(&conn->state) != CONN_FETCHING_DATA || stmt->state != MYSQLND_STMT_WAITING_USE_OR_STORE) {
if (!mysqlnd_stmt_check_state(stmt)) {
SET_CLIENT_ERROR(stmt->error_info, CR_COMMANDS_OUT_OF_SYNC, UNKNOWN_SQLSTATE, mysqlnd_out_of_sync);
DBG_RETURN(NULL);
}
@ -173,6 +192,12 @@ MYSQLND_METHOD(mysqlnd_stmt, get_result)(MYSQLND_STMT * const s)
SET_EMPTY_ERROR(conn->error_info);
MYSQLND_INC_CONN_STATISTIC(conn->stats, STAT_BUFFERED_SETS);
if (stmt->cursor_exists) {
if (mysqlnd_stmt_send_cursor_fetch_command(stmt, -1) == FAIL) {
DBG_RETURN(NULL);
}
}
do {
result = conn->m->result_init(stmt->result->field_count);
if (!result) {
@ -561,28 +586,30 @@ mysqlnd_stmt_execute_parse_response(MYSQLND_STMT * const s, enum_mysqlnd_parse_e
DBG_INF_FMT("server_status=%u cursor=%u", UPSERT_STATUS_GET_SERVER_STATUS(stmt->upsert_status),
UPSERT_STATUS_GET_SERVER_STATUS(stmt->upsert_status) & SERVER_STATUS_CURSOR_EXISTS);
if (UPSERT_STATUS_GET_SERVER_STATUS(stmt->upsert_status) & SERVER_STATUS_CURSOR_EXISTS) {
DBG_INF("cursor exists");
stmt->cursor_exists = TRUE;
SET_CONNECTION_STATE(&conn->state, CONN_READY);
/* Only cursor read */
stmt->default_rset_handler = s->m->use_result;
DBG_INF("use_result");
} else if (stmt->flags & CURSOR_TYPE_READ_ONLY) {
DBG_INF("asked for cursor but got none");
/*
We have asked for CURSOR but got no cursor, because the condition
above is not fulfilled. Then...
if (stmt->flags & CURSOR_TYPE_READ_ONLY) {
if (UPSERT_STATUS_GET_SERVER_STATUS(stmt->upsert_status) & SERVER_STATUS_CURSOR_EXISTS) {
DBG_INF("cursor exists");
stmt->cursor_exists = TRUE;
SET_CONNECTION_STATE(&conn->state, CONN_READY);
/* Only cursor read */
stmt->default_rset_handler = s->m->use_result;
DBG_INF("use_result");
} else {
DBG_INF("asked for cursor but got none");
/*
We have asked for CURSOR but got no cursor, because the condition
above is not fulfilled. Then...
This is a single-row result set, a result set with no rows, EXPLAIN,
SHOW VARIABLES, or some other command which either a) bypasses the
cursors framework in the server and writes rows directly to the
network or b) is more efficient if all (few) result set rows are
precached on client and server's resources are freed.
*/
/* preferred is buffered read */
stmt->default_rset_handler = s->m->store_result;
DBG_INF("store_result");
This is a single-row result set, a result set with no rows, EXPLAIN,
SHOW VARIABLES, or some other command which either a) bypasses the
cursors framework in the server and writes rows directly to the
network or b) is more efficient if all (few) result set rows are
precached on client and server's resources are freed.
*/
/* preferred is buffered read */
stmt->default_rset_handler = s->m->store_result;
DBG_INF("store_result");
}
} else {
DBG_INF("no cursor");
/* preferred is unbuffered read */
@ -940,11 +967,7 @@ MYSQLND_METHOD(mysqlnd_stmt, use_result)(MYSQLND_STMT * s)
}
DBG_INF_FMT("stmt=%lu", stmt->stmt_id);
if (!stmt->field_count ||
(!stmt->cursor_exists && GET_CONNECTION_STATE(&conn->state) != CONN_FETCHING_DATA) ||
(stmt->cursor_exists && GET_CONNECTION_STATE(&conn->state) != CONN_READY) ||
(stmt->state != MYSQLND_STMT_WAITING_USE_OR_STORE))
{
if (!stmt->field_count || !mysqlnd_stmt_check_state(stmt)) {
SET_CLIENT_ERROR(conn->error_info, CR_COMMANDS_OUT_OF_SYNC, UNKNOWN_SQLSTATE, mysqlnd_out_of_sync);
DBG_ERR("command out of sync");
DBG_RETURN(NULL);
@ -974,7 +997,6 @@ mysqlnd_fetch_stmt_row_cursor(MYSQLND_RES * result, void * param, const unsigned
MYSQLND_STMT * s = (MYSQLND_STMT *) param;
MYSQLND_STMT_DATA * stmt = s? s->data : NULL;
MYSQLND_CONN_DATA * conn = stmt? stmt->conn : NULL;
zend_uchar buf[MYSQLND_STMT_ID_LENGTH /* statement id */ + 4 /* number of rows to fetch */];
MYSQLND_PACKET_ROW * row_packet;
DBG_ENTER("mysqlnd_fetch_stmt_row_cursor");
@ -998,18 +1020,9 @@ mysqlnd_fetch_stmt_row_cursor(MYSQLND_RES * result, void * param, const unsigned
SET_EMPTY_ERROR(stmt->error_info);
SET_EMPTY_ERROR(conn->error_info);
int4store(buf, stmt->stmt_id);
int4store(buf + MYSQLND_STMT_ID_LENGTH, 1); /* for now fetch only one row */
{
const MYSQLND_CSTRING payload = {(const char*) buf, sizeof(buf)};
ret = conn->command->stmt_fetch(conn, payload);
if (ret == FAIL) {
COPY_CLIENT_ERROR(stmt->error_info, *conn->error_info);
DBG_RETURN(FAIL);
}
/* for now fetch only one row */
if (mysqlnd_stmt_send_cursor_fetch_command(stmt, 1) == FAIL) {
DBG_RETURN(FAIL);
}
row_packet->skip_extraction = stmt->result_bind? FALSE:TRUE;