From b131c0cb1137c27049249f4e66f370badf318a5d Mon Sep 17 00:00:00 2001 From: Dharman Date: Wed, 2 Dec 2020 21:24:20 +0000 Subject: [PATCH 1/8] Lack of result does not indicate OOS error --- ext/mysqlnd/mysqlnd_ps.c | 5 ++++- ext/pdo_mysql/mysql_statement.c | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ext/mysqlnd/mysqlnd_ps.c b/ext/mysqlnd/mysqlnd_ps.c index 55da87fdf0b60..6d9f98873be87 100644 --- a/ext/mysqlnd/mysqlnd_ps.c +++ b/ext/mysqlnd/mysqlnd_ps.c @@ -1116,7 +1116,10 @@ MYSQLND_METHOD(mysqlnd_stmt, fetch)(MYSQLND_STMT * const s, zend_bool * const fe } DBG_INF_FMT("stmt=%lu", stmt->stmt_id); - if (!stmt->result || stmt->state < MYSQLND_STMT_WAITING_USE_OR_STORE) { + if (!stmt->result) { + DBG_RETURN(FAIL); + } + else if (stmt->state < MYSQLND_STMT_WAITING_USE_OR_STORE) { SET_CLIENT_ERROR(stmt->error_info, CR_COMMANDS_OUT_OF_SYNC, UNKNOWN_SQLSTATE, mysqlnd_out_of_sync); DBG_ERR("command out of sync"); DBG_RETURN(FAIL); diff --git a/ext/pdo_mysql/mysql_statement.c b/ext/pdo_mysql/mysql_statement.c index 00a2583cdf7e6..f6a57a04a11df 100644 --- a/ext/pdo_mysql/mysql_statement.c +++ b/ext/pdo_mysql/mysql_statement.c @@ -656,7 +656,6 @@ static int pdo_mysql_stmt_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori #endif /* PDO_USE_MYSQLND */ if (!S->result) { - strcpy(stmt->error_code, "HY000"); PDO_DBG_RETURN(0); } #ifdef PDO_USE_MYSQLND From e4bb6076c5d435efd1c9e27ad272d743d158bd68 Mon Sep 17 00:00:00 2001 From: Dharman Date: Wed, 2 Dec 2020 22:21:37 +0000 Subject: [PATCH 2/8] Create bug80458.phpt --- ext/pdo_mysql/tests/bug80458.phpt | 83 +++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 ext/pdo_mysql/tests/bug80458.phpt diff --git a/ext/pdo_mysql/tests/bug80458.phpt b/ext/pdo_mysql/tests/bug80458.phpt new file mode 100644 index 0000000000000..a567aee2087d8 --- /dev/null +++ b/ext/pdo_mysql/tests/bug80458.phpt @@ -0,0 +1,83 @@ +--TEST-- +Bug #80458 PDOStatement::fetchAll() throws for upsert queries +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); +$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); + +$db->query('DROP TABLE IF EXISTS test'); +$db->query('CREATE TABLE test (first int) ENGINE = InnoDB'); +$res = $db->query('INSERT INTO test(first) VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9)'); +var_dump($res->fetchAll()); + +$stmt = $db->prepare('DELETE FROM test WHERE first=1'); +$stmt->execute(); +var_dump($stmt->fetchAll()); + +$stmt2 = $db->prepare('DELETE FROM test WHERE first=2'); +$stmt2->execute(); +foreach($stmt2 as $row){ + // expect nothing +} + +$stmt3 = $db->prepare('DELETE FROM test WHERE first=3'); +$stmt3->execute(); +if(false !== $stmt3->fetch(PDO::FETCH_ASSOC)) { + print("Expecting false"); +} + +$stmt = $db->prepare('SELECT first FROM test WHERE first=4'); +$stmt->execute(); +var_dump($stmt->fetchAll()); + +$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, true); + +$stmt3 = $db->prepare('DELETE FROM test WHERE first=5'); +$stmt3->execute(); +if(false !== $stmt3->fetch(PDO::FETCH_ASSOC)) { + print("Expecting false"); +} + +$stmt = $db->prepare('SELECT first FROM test WHERE first=6'); +$stmt->execute(); +var_dump($stmt->fetchAll()); + +?> +--CLEAN-- + +--EXPECT-- +array(0) { +} +array(0) { +} +array(1) { + [0]=> + array(2) { + ["first"]=> + int(4) + [0]=> + int(4) + } +} +array(1) { + [0]=> + array(2) { + ["first"]=> + string(1) "6" + [0]=> + string(1) "6" + } +} \ No newline at end of file From 1f33d2c09b191f5b0a717c48106bc5fbe64cea62 Mon Sep 17 00:00:00 2001 From: Dharman Date: Thu, 3 Dec 2020 12:40:25 +0000 Subject: [PATCH 3/8] Add check in PDO to see whether a result exists for STMT --- ext/mysqlnd/mysqlnd_ps.c | 5 +- ext/pdo_mysql/mysql_statement.c | 6 ++ ext/pdo_mysql/tests/bug80458.phpt | 107 +++++++++++++++++++++++++----- 3 files changed, 97 insertions(+), 21 deletions(-) diff --git a/ext/mysqlnd/mysqlnd_ps.c b/ext/mysqlnd/mysqlnd_ps.c index 6d9f98873be87..55da87fdf0b60 100644 --- a/ext/mysqlnd/mysqlnd_ps.c +++ b/ext/mysqlnd/mysqlnd_ps.c @@ -1116,10 +1116,7 @@ MYSQLND_METHOD(mysqlnd_stmt, fetch)(MYSQLND_STMT * const s, zend_bool * const fe } DBG_INF_FMT("stmt=%lu", stmt->stmt_id); - if (!stmt->result) { - DBG_RETURN(FAIL); - } - else if (stmt->state < MYSQLND_STMT_WAITING_USE_OR_STORE) { + if (!stmt->result || stmt->state < MYSQLND_STMT_WAITING_USE_OR_STORE) { SET_CLIENT_ERROR(stmt->error_info, CR_COMMANDS_OUT_OF_SYNC, UNKNOWN_SQLSTATE, mysqlnd_out_of_sync); DBG_ERR("command out of sync"); DBG_RETURN(FAIL); diff --git a/ext/pdo_mysql/mysql_statement.c b/ext/pdo_mysql/mysql_statement.c index f6a57a04a11df..4f3e0e2736e1c 100644 --- a/ext/pdo_mysql/mysql_statement.c +++ b/ext/pdo_mysql/mysql_statement.c @@ -619,6 +619,12 @@ static int pdo_mysql_stmt_param_hook(pdo_stmt_t *stmt, struct pdo_bound_param_da static int pdo_mysql_stmt_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori, zend_long offset) /* {{{ */ { pdo_mysql_stmt *S = (pdo_mysql_stmt*)stmt->driver_data; + + if(S->stmt && !mysql_stmt_result_metadata(S->stmt)) { + pdo_mysql_error_stmt(stmt); + PDO_DBG_RETURN(0); + } + #ifdef PDO_USE_MYSQLND zend_bool fetched_anything; diff --git a/ext/pdo_mysql/tests/bug80458.phpt b/ext/pdo_mysql/tests/bug80458.phpt index a567aee2087d8..d79c8dbbc858b 100644 --- a/ext/pdo_mysql/tests/bug80458.phpt +++ b/ext/pdo_mysql/tests/bug80458.phpt @@ -17,41 +17,84 @@ $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); $db->query('DROP TABLE IF EXISTS test'); $db->query('CREATE TABLE test (first int) ENGINE = InnoDB'); -$res = $db->query('INSERT INTO test(first) VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9)'); +$res = $db->query('INSERT INTO test(first) VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),(12),(13),(14)'); var_dump($res->fetchAll()); $stmt = $db->prepare('DELETE FROM test WHERE first=1'); $stmt->execute(); var_dump($stmt->fetchAll()); -$stmt2 = $db->prepare('DELETE FROM test WHERE first=2'); +$res = $db->query('DELETE FROM test WHERE first=2'); +var_dump($res->fetchAll()); + +$stmt2 = $db->prepare('DELETE FROM test WHERE first=3'); $stmt2->execute(); foreach($stmt2 as $row){ // expect nothing } -$stmt3 = $db->prepare('DELETE FROM test WHERE first=3'); +$stmt3 = $db->prepare('DELETE FROM test WHERE first=4'); $stmt3->execute(); -if(false !== $stmt3->fetch(PDO::FETCH_ASSOC)) { - print("Expecting false"); -} +var_dump($stmt3->fetch(PDO::FETCH_ASSOC)); -$stmt = $db->prepare('SELECT first FROM test WHERE first=4'); +$stmt = $db->prepare('SELECT first FROM test WHERE first=5'); $stmt->execute(); var_dump($stmt->fetchAll()); +$db->exec('DROP PROCEDURE IF EXISTS empty'); +$db->exec('CREATE PROCEDURE empty() BEGIN DELETE FROM test WHERE first=6; END;'); +$stmt4 = $db->prepare('CALL empty()'); +$stmt4->execute(); +var_dump($stmt4->fetchAll()); +$db->exec('DROP PROCEDURE IF EXISTS empty'); + +$db->exec('DROP PROCEDURE IF EXISTS ret'); +$db->exec('CREATE PROCEDURE ret() BEGIN SELECT first FROM test WHERE first=7; END;'); +$stmt5 = $db->prepare('CALL ret()'); +$stmt5->execute(); +var_dump($stmt5->fetchAll()); +$stmt5->nextRowset(); // needed to fetch the empty result set of CALL +$db->exec('DROP PROCEDURE IF EXISTS ret'); + +/* With emulated prepares */ $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, true); -$stmt3 = $db->prepare('DELETE FROM test WHERE first=5'); -$stmt3->execute(); -if(false !== $stmt3->fetch(PDO::FETCH_ASSOC)) { - print("Expecting false"); +$stmt = $db->prepare('DELETE FROM test WHERE first=8'); +$stmt->execute(); +var_dump($stmt->fetchAll()); + +$res = $db->query('DELETE FROM test WHERE first=9'); +var_dump($res->fetchAll()); + +$stmt2 = $db->prepare('DELETE FROM test WHERE first=10'); +$stmt2->execute(); +foreach($stmt2 as $row){ + // expect nothing } -$stmt = $db->prepare('SELECT first FROM test WHERE first=6'); +$stmt3 = $db->prepare('DELETE FROM test WHERE first=11'); +$stmt3->execute(); +var_dump($stmt3->fetch(PDO::FETCH_ASSOC)); + +$stmt = $db->prepare('SELECT first FROM test WHERE first=12'); $stmt->execute(); var_dump($stmt->fetchAll()); +$db->exec('DROP PROCEDURE IF EXISTS empty'); +$db->exec('CREATE PROCEDURE empty() BEGIN DELETE FROM test WHERE first=13; END;'); +$stmt4 = $db->prepare('CALL empty()'); +$stmt4->execute(); +var_dump($stmt4->fetchAll()); +$db->exec('DROP PROCEDURE IF EXISTS empty'); + +$db->exec('DROP PROCEDURE IF EXISTS ret'); +$db->exec('CREATE PROCEDURE ret() BEGIN SELECT first FROM test WHERE first=14; END;'); +$stmt5 = $db->prepare('CALL ret()'); +$stmt5->execute(); +var_dump($stmt5->fetchAll()); +$stmt5->nextRowset(); // needed to fetch the empty result set of CALL +$db->exec('DROP PROCEDURE IF EXISTS ret'); + ?> --CLEAN-- array(2) { ["first"]=> - int(4) + int(5) [0]=> - int(4) + int(5) } } +array(0) { +} array(1) { [0]=> array(2) { ["first"]=> - string(1) "6" + int(7) [0]=> - string(1) "6" + int(7) } -} \ No newline at end of file +} +array(0) { +} +array(0) { +} +bool(false) +array(1) { + [0]=> + array(2) { + ["first"]=> + string(2) "12" + [0]=> + string(2) "12" + } +} +array(0) { +} +array(1) { + [0]=> + array(2) { + ["first"]=> + string(2) "14" + [0]=> + string(2) "14" + } +} From b2e32795b306010f8d282ea78cec48a11d0f31dd Mon Sep 17 00:00:00 2001 From: Dharman Date: Thu, 3 Dec 2020 12:55:40 +0000 Subject: [PATCH 4/8] Apparently there was a leak --- ext/pdo_mysql/mysql_statement.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ext/pdo_mysql/mysql_statement.c b/ext/pdo_mysql/mysql_statement.c index 4f3e0e2736e1c..c4f4edde77368 100644 --- a/ext/pdo_mysql/mysql_statement.c +++ b/ext/pdo_mysql/mysql_statement.c @@ -620,9 +620,14 @@ static int pdo_mysql_stmt_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori { pdo_mysql_stmt *S = (pdo_mysql_stmt*)stmt->driver_data; - if(S->stmt && !mysql_stmt_result_metadata(S->stmt)) { - pdo_mysql_error_stmt(stmt); - PDO_DBG_RETURN(0); + if(S->stmt) { + MYSQL_RES *prepare_meta_result; + if(!(prepare_meta_result = mysql_stmt_result_metadata(S->stmt))) { + pdo_mysql_error_stmt(stmt); + PDO_DBG_RETURN(0); + } else { + mysql_free_result(prepare_meta_result); + } } #ifdef PDO_USE_MYSQLND From 760788c8b987e73892392cc962a82c2bc9c81de3 Mon Sep 17 00:00:00 2001 From: Dharman Date: Thu, 3 Dec 2020 13:39:52 +0000 Subject: [PATCH 5/8] Removed function call. Check for the result directly --- ext/pdo_mysql/mysql_statement.c | 10 ++-------- ext/pdo_mysql/tests/bug80458.phpt | 8 ++++++++ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/ext/pdo_mysql/mysql_statement.c b/ext/pdo_mysql/mysql_statement.c index c4f4edde77368..0c065c58348c3 100644 --- a/ext/pdo_mysql/mysql_statement.c +++ b/ext/pdo_mysql/mysql_statement.c @@ -620,14 +620,8 @@ static int pdo_mysql_stmt_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori { pdo_mysql_stmt *S = (pdo_mysql_stmt*)stmt->driver_data; - if(S->stmt) { - MYSQL_RES *prepare_meta_result; - if(!(prepare_meta_result = mysql_stmt_result_metadata(S->stmt))) { - pdo_mysql_error_stmt(stmt); - PDO_DBG_RETURN(0); - } else { - mysql_free_result(prepare_meta_result); - } + if(S->stmt && S->stmt->data && !S->stmt->data->result) { + PDO_DBG_RETURN(0); } #ifdef PDO_USE_MYSQLND diff --git a/ext/pdo_mysql/tests/bug80458.phpt b/ext/pdo_mysql/tests/bug80458.phpt index d79c8dbbc858b..a89aa2e812f3c 100644 --- a/ext/pdo_mysql/tests/bug80458.phpt +++ b/ext/pdo_mysql/tests/bug80458.phpt @@ -54,9 +54,11 @@ $stmt5 = $db->prepare('CALL ret()'); $stmt5->execute(); var_dump($stmt5->fetchAll()); $stmt5->nextRowset(); // needed to fetch the empty result set of CALL +var_dump($stmt5->fetchAll()); $db->exec('DROP PROCEDURE IF EXISTS ret'); /* With emulated prepares */ +print("Emulated prepares\n"); $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, true); $stmt = $db->prepare('DELETE FROM test WHERE first=8'); @@ -93,6 +95,7 @@ $stmt5 = $db->prepare('CALL ret()'); $stmt5->execute(); var_dump($stmt5->fetchAll()); $stmt5->nextRowset(); // needed to fetch the empty result set of CALL +var_dump($stmt5->fetchAll()); $db->exec('DROP PROCEDURE IF EXISTS ret'); ?> @@ -131,6 +134,9 @@ array(1) { } array(0) { } +Emulated prepares +array(0) { +} array(0) { } bool(false) @@ -154,3 +160,5 @@ array(1) { string(2) "14" } } +array(0) { +} From 6575116dd6372e4ce135efdabc1211f951381dda Mon Sep 17 00:00:00 2001 From: Dharman Date: Thu, 3 Dec 2020 14:33:16 +0000 Subject: [PATCH 6/8] empty is a reserved keyword in MySQL 8 --- ext/pdo_mysql/tests/bug80458.phpt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ext/pdo_mysql/tests/bug80458.phpt b/ext/pdo_mysql/tests/bug80458.phpt index a89aa2e812f3c..272ab0e730f65 100644 --- a/ext/pdo_mysql/tests/bug80458.phpt +++ b/ext/pdo_mysql/tests/bug80458.phpt @@ -41,12 +41,12 @@ $stmt = $db->prepare('SELECT first FROM test WHERE first=5'); $stmt->execute(); var_dump($stmt->fetchAll()); -$db->exec('DROP PROCEDURE IF EXISTS empty'); -$db->exec('CREATE PROCEDURE empty() BEGIN DELETE FROM test WHERE first=6; END;'); -$stmt4 = $db->prepare('CALL empty()'); +$db->exec('DROP PROCEDURE IF EXISTS nores'); +$db->exec('CREATE PROCEDURE nores() BEGIN DELETE FROM test WHERE first=6; END;'); +$stmt4 = $db->prepare('CALL nores()'); $stmt4->execute(); var_dump($stmt4->fetchAll()); -$db->exec('DROP PROCEDURE IF EXISTS empty'); +$db->exec('DROP PROCEDURE IF EXISTS nores'); $db->exec('DROP PROCEDURE IF EXISTS ret'); $db->exec('CREATE PROCEDURE ret() BEGIN SELECT first FROM test WHERE first=7; END;'); @@ -82,12 +82,12 @@ $stmt = $db->prepare('SELECT first FROM test WHERE first=12'); $stmt->execute(); var_dump($stmt->fetchAll()); -$db->exec('DROP PROCEDURE IF EXISTS empty'); -$db->exec('CREATE PROCEDURE empty() BEGIN DELETE FROM test WHERE first=13; END;'); -$stmt4 = $db->prepare('CALL empty()'); +$db->exec('DROP PROCEDURE IF EXISTS nores'); +$db->exec('CREATE PROCEDURE nores() BEGIN DELETE FROM test WHERE first=13; END;'); +$stmt4 = $db->prepare('CALL nores()'); $stmt4->execute(); var_dump($stmt4->fetchAll()); -$db->exec('DROP PROCEDURE IF EXISTS empty'); +$db->exec('DROP PROCEDURE IF EXISTS nores'); $db->exec('DROP PROCEDURE IF EXISTS ret'); $db->exec('CREATE PROCEDURE ret() BEGIN SELECT first FROM test WHERE first=14; END;'); From 24081e7d57a42b2a3c76063470a8628f55e4fdfd Mon Sep 17 00:00:00 2001 From: Dharman Date: Fri, 4 Dec 2020 15:08:40 +0000 Subject: [PATCH 7/8] Remove the unnecessary if statement --- ext/pdo_mysql/mysql_statement.c | 5 +---- ext/pdo_mysql/tests/bug80458.phpt | 24 +++++++++++++++++++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/ext/pdo_mysql/mysql_statement.c b/ext/pdo_mysql/mysql_statement.c index 0c065c58348c3..c7da3805e9009 100644 --- a/ext/pdo_mysql/mysql_statement.c +++ b/ext/pdo_mysql/mysql_statement.c @@ -620,7 +620,7 @@ static int pdo_mysql_stmt_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori { pdo_mysql_stmt *S = (pdo_mysql_stmt*)stmt->driver_data; - if(S->stmt && S->stmt->data && !S->stmt->data->result) { + if (!S->result) { PDO_DBG_RETURN(0); } @@ -660,9 +660,6 @@ static int pdo_mysql_stmt_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori } #endif /* PDO_USE_MYSQLND */ - if (!S->result) { - PDO_DBG_RETURN(0); - } #ifdef PDO_USE_MYSQLND if (!S->stmt && S->current_data) { mnd_free(S->current_data); diff --git a/ext/pdo_mysql/tests/bug80458.phpt b/ext/pdo_mysql/tests/bug80458.phpt index 272ab0e730f65..86e171d302d27 100644 --- a/ext/pdo_mysql/tests/bug80458.phpt +++ b/ext/pdo_mysql/tests/bug80458.phpt @@ -17,7 +17,7 @@ $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); $db->query('DROP TABLE IF EXISTS test'); $db->query('CREATE TABLE test (first int) ENGINE = InnoDB'); -$res = $db->query('INSERT INTO test(first) VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),(12),(13),(14)'); +$res = $db->query('INSERT INTO test(first) VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),(12),(13),(14),(15),(16)'); var_dump($res->fetchAll()); $stmt = $db->prepare('DELETE FROM test WHERE first=1'); @@ -98,6 +98,17 @@ $stmt5->nextRowset(); // needed to fetch the empty result set of CALL var_dump($stmt5->fetchAll()); $db->exec('DROP PROCEDURE IF EXISTS ret'); +$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); +$db->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); + +$stmt = $db->prepare('DELETE FROM test WHERE first=15'); +$stmt->execute(); +var_dump($stmt->fetchAll()); + +$stmt = $db->prepare('SELECT first FROM test WHERE first=16'); +$stmt->execute(); +var_dump($stmt->fetchAll()); + ?> --CLEAN-- + array(2) { + ["first"]=> + int(16) + [0]=> + int(16) + } +} From eeddb5ece1fa6ae9b57ec2c95ef9ccfc5d0ead21 Mon Sep 17 00:00:00 2001 From: Dharman Date: Fri, 4 Dec 2020 15:11:40 +0000 Subject: [PATCH 8/8] Tiny refactoring to remove double macro --- ext/pdo_mysql/mysql_statement.c | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/ext/pdo_mysql/mysql_statement.c b/ext/pdo_mysql/mysql_statement.c index c7da3805e9009..616c5bf9c44f2 100644 --- a/ext/pdo_mysql/mysql_statement.c +++ b/ext/pdo_mysql/mysql_statement.c @@ -637,6 +637,10 @@ static int pdo_mysql_stmt_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori PDO_DBG_RETURN(1); } + + if (!S->stmt && S->current_data) { + mnd_free(S->current_data); + } #else int ret; @@ -660,12 +664,6 @@ static int pdo_mysql_stmt_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori } #endif /* PDO_USE_MYSQLND */ -#ifdef PDO_USE_MYSQLND - if (!S->stmt && S->current_data) { - mnd_free(S->current_data); - } -#endif /* PDO_USE_MYSQLND */ - if ((S->current_data = mysql_fetch_row(S->result)) == NULL) { if (!S->H->buffered && mysql_errno(S->H->server)) { pdo_mysql_error_stmt(stmt);