Skip to content

sqlite3: Fix double execution of queries when fetchArray is called #8942

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion ext/sqlite3/php_sqlite3_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,9 @@ struct _php_sqlite3_stmt_object {
php_sqlite3_db_object *db_obj;
zval db_obj_zval;

int initialised;
unsigned initialised:1;
unsigned has_stepped:1;
unsigned last_step_result:30;

/* Keep track of the zvals for bound parameters */
HashTable *bound_params;
Expand Down
42 changes: 31 additions & 11 deletions ext/sqlite3/sqlite3.c
Original file line number Diff line number Diff line change
Expand Up @@ -591,17 +591,21 @@ PHP_METHOD(SQLite3, query)
ZVAL_OBJ(&result->stmt_obj_zval, Z_OBJ(stmt));

return_code = sqlite3_step(result->stmt_obj->stmt);
result->stmt_obj->has_stepped = 1;
ZEND_ASSERT(return_code >= 0 && return_code < (1 << 30));
result->stmt_obj->last_step_result = return_code;

switch (return_code) {
case SQLITE_ROW: /* Valid Row */
case SQLITE_DONE: /* Valid but no results */
sqlite3_reset(result->stmt_obj->stmt);
ZEND_FALLTHROUGH;
case SQLITE_ROW: /* Valid Row */
{
php_sqlite3_free_list *free_item;
free_item = emalloc(sizeof(php_sqlite3_free_list));
free_item->stmt_obj = stmt_obj;
free_item->stmt_obj_zval = stmt;
zend_llist_add_element(&(db_obj->free_list), &free_item);
sqlite3_reset(result->stmt_obj->stmt);
break;
}
default:
Expand All @@ -610,6 +614,7 @@ PHP_METHOD(SQLite3, query)
}
sqlite3_finalize(stmt_obj->stmt);
stmt_obj->initialised = 0;
stmt_obj->has_stepped = 0;
zval_ptr_dtor(return_value);
RETURN_FALSE;
}
Expand Down Expand Up @@ -1432,6 +1437,7 @@ PHP_METHOD(SQLite3Stmt, reset)
SQLITE3_CHECK_INITIALIZED(stmt_obj->db_obj, stmt_obj->initialised, SQLite3);
SQLITE3_CHECK_INITIALIZED_STMT(stmt_obj->stmt, SQLite3Stmt);

stmt_obj->has_stepped = 0;
if (sqlite3_reset(stmt_obj->stmt) != SQLITE_OK) {
php_sqlite3_error(stmt_obj->db_obj, "Unable to reset statement: %s", sqlite3_errmsg(sqlite3_db_handle(stmt_obj->stmt)));
RETURN_FALSE;
Expand Down Expand Up @@ -1782,12 +1788,16 @@ PHP_METHOD(SQLite3Stmt, execute)
}

return_code = sqlite3_step(stmt_obj->stmt);
stmt_obj->has_stepped = 1;
ZEND_ASSERT(return_code >= 0 && return_code < (1 << 30));
stmt_obj->last_step_result = return_code;

switch (return_code) {
case SQLITE_ROW: /* Valid Row */
case SQLITE_DONE: /* Valid but no results */
{
sqlite3_reset(stmt_obj->stmt);
ZEND_FALLTHROUGH;
case SQLITE_ROW: /* Valid Row */
{
object_init_ex(return_value, php_sqlite3_result_entry);
result = Z_SQLITE3_RESULT_P(return_value);

Expand All @@ -1800,9 +1810,6 @@ PHP_METHOD(SQLite3Stmt, execute)

break;
}
case SQLITE_ERROR:
sqlite3_reset(stmt_obj->stmt);
ZEND_FALLTHROUGH;
default:
if (!EG(exception)) {
php_sqlite3_error(stmt_obj->db_obj, "Unable to execute statement: %s", sqlite3_errmsg(sqlite3_db_handle(stmt_obj->stmt)));
Expand Down Expand Up @@ -1941,7 +1948,14 @@ PHP_METHOD(SQLite3Result, fetchArray)

SQLITE3_CHECK_INITIALIZED(result_obj->db_obj, result_obj->stmt_obj->initialised, SQLite3Result)

ret = sqlite3_step(result_obj->stmt_obj->stmt);
if (result_obj->stmt_obj->has_stepped) {
ret = result_obj->stmt_obj->last_step_result;
} else {
ret = sqlite3_step(result_obj->stmt_obj->stmt);
result_obj->stmt_obj->has_stepped = 1;
ZEND_ASSERT(ret >= 0 && ret < (1 << 30));
result_obj->stmt_obj->last_step_result = ret;
}
switch (ret) {
case SQLITE_ROW:
/* If there was no return value then just skip fetching */
Expand Down Expand Up @@ -1985,6 +1999,7 @@ PHP_METHOD(SQLite3Result, fetchArray)
zend_symtable_add_new(Z_ARR_P(return_value), result_obj->column_names[i], &data);
}
}
result_obj->stmt_obj->has_stepped = 0;
break;

case SQLITE_DONE:
Expand Down Expand Up @@ -2021,6 +2036,7 @@ PHP_METHOD(SQLite3Result, reset)

sqlite3result_clear_column_names_cache(result_obj);

result_obj->stmt_obj->has_stepped = 0;
if (sqlite3_reset(result_obj->stmt_obj->stmt) != SQLITE_OK) {
RETURN_FALSE;
}
Expand All @@ -2047,6 +2063,7 @@ PHP_METHOD(SQLite3Result, finalize)
zend_llist_del_element(&(result_obj->db_obj->free_list), &result_obj->stmt_obj_zval,
(int (*)(void *, void *)) php_sqlite3_compare_stmt_zval_free);
} else {
result_obj->stmt_obj->has_stepped = 0;
sqlite3_reset(result_obj->stmt_obj->stmt);
}

Expand Down Expand Up @@ -2271,11 +2288,13 @@ static void php_sqlite3_result_object_free_storage(zend_object *object) /* {{{ *
sqlite3result_clear_column_names_cache(intern);

if (!Z_ISNULL(intern->stmt_obj_zval)) {
zval_ptr_dtor(&intern->stmt_obj_zval);
if (intern->stmt_obj && intern->stmt_obj->initialised) {
sqlite3_reset(intern->stmt_obj->stmt);
if (GC_REFCOUNT(&intern->stmt_obj->zo) == 0) {
sqlite3_reset(intern->stmt_obj->stmt);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the bit I'm unsure about - to me it makes sense logically that we are only safe to reset the statement if the refcount on it is 0, but grepping the rest of the codebase I don't see much precedent for checking refcounts from extensions.

But, that said, the statement object is already inside free_list so we can be reasonably assured that it will be reset at some later point - so can we just remove this? if I remove this entire if (intern->stmt_obj block, I don't get any complaints from a debug build about detected memory leaks.

intern->stmt_obj->has_stepped = 0;
}
}

zval_ptr_dtor(&intern->stmt_obj_zval);
}

zend_object_std_dtor(&intern->zo);
Expand Down Expand Up @@ -2312,6 +2331,7 @@ static zend_object *php_sqlite3_stmt_object_new(zend_class_entry *class_type) /*
object_properties_init(&intern->zo, class_type);

intern->zo.handlers = &sqlite3_stmt_object_handlers;
intern->has_stepped = 0;

return &intern->zo;
}
Expand Down
26 changes: 26 additions & 0 deletions ext/sqlite3/tests/bug64531_1.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
--TEST--
Bug #64531 (SQLite3Result::fetchArray runs the query again) - SQLite3->query
--SKIPIF--
<?php
if (!extension_loaded('sqlite3')) die('skip sqlite3 extension not available');
?>
--FILE--
<?php
$conn = new SQLite3(':memory:');
$conn->exec("CREATE TABLE foo (id INT)");

$res = $conn->query("INSERT INTO foo VALUES (1)");

$res->fetchArray();
$res->fetchArray();

$res = $conn->query("SELECT * FROM foo");
while (($row = $res->fetchArray(SQLITE3_NUM))) {
var_dump($row);
}
?>
--EXPECT--
array(1) {
[0]=>
int(1)
}
30 changes: 30 additions & 0 deletions ext/sqlite3/tests/bug64531_2.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
--TEST--
Bug #64531 (SQLite3Result::fetchArray runs the query again) - SQLite3->query
--SKIPIF--
<?php
if (!extension_loaded('sqlite3')) die('skip sqlite3 extension not available');
?>
--FILE--
<?php
function testing($val) {
echo "Testing with: $val\n";
return true;
}

$conn = new SQLite3(':memory:');
$conn->exec("CREATE TABLE foo (id INT)");
for ($i = 1; $i <= 3; $i++) {
$conn->exec("INSERT INTO foo VALUES ($i)");
}
$conn->createFunction('testing', 'testing', 1);

$res = $conn->query('SELECT * FROM foo WHERE testing(id)');
$arr = $res->fetchArray();
$arr = $res->fetchArray();
$arr = $res->fetchArray();
$arr = $res->fetchArray();
?>
--EXPECT--
Testing with: 1
Testing with: 2
Testing with: 3
26 changes: 26 additions & 0 deletions ext/sqlite3/tests/bug64531_3.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
--TEST--
Bug #64531 (SQLite3Result::fetchArray runs the query again) - SQLite3Stmt->execute
--SKIPIF--
<?php
if (!extension_loaded('sqlite3')) die('skip sqlite3 extension not available');
?>
--FILE--
<?php
$conn = new SQLite3(':memory:');
$conn->exec("CREATE TABLE foo (id INT)");

$stmt = $conn->prepare("INSERT INTO foo VALUES (1)");
$res = $stmt->execute();
$res->fetchArray();
$res->fetchArray();

$res = $conn->query("SELECT * FROM foo");
while (($row = $res->fetchArray(SQLITE3_NUM))) {
var_dump($row);
}
?>
--EXPECT--
array(1) {
[0]=>
int(1)
}
66 changes: 66 additions & 0 deletions ext/sqlite3/tests/bug64531_4.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
--TEST--
Bug #64531 (SQLite3Result::fetchArray runs the query again) - SQLite3Stmt->execute (reused)
--SKIPIF--
<?php
if (!extension_loaded('sqlite3')) die('skip sqlite3 extension not available');
?>
--FILE--
<?php
$conn = new SQLite3(':memory:');
$conn->exec("CREATE TABLE foo (id INT)");

function testing($val) {
echo $val;
debug_print_backtrace();
return $val;
}

$conn->createFunction('testing', 'testing', 1);

$stmt = $conn->prepare("INSERT INTO foo VALUES (testing(?))");

$stmt->bindValue(1, 1);

$res = $stmt->execute(); // Should run
$res->fetchArray(); // Should not run
$res->fetchArray(); // Should not run
$res->fetchArray(); // Should not run

$stmt->bindValue(1, 2);

$res = $stmt->execute(); // Should run
$res->fetchArray(); // Should not run
$res->fetchArray(); // Should not run
$res->fetchArray(); // Should not run

$stmt->bindValue(1, 3);

$res2 = $stmt->execute(); // Should run
$res2->fetchArray(); // Should not run
$res2->fetchArray(); // Should not run
$res2->fetchArray(); // Should not run

$res = $conn->query("SELECT * FROM foo");
while (($row = $res->fetchArray(SQLITE3_NUM))) {
var_dump($row);
}
?>
--EXPECTF--
1#0 [internal function]: testing(1)
#1 %s(17): SQLite3Stmt->execute()
2#0 [internal function]: testing(2)
#1 %s(24): SQLite3Stmt->execute()
3#0 [internal function]: testing(3)
#1 %s(31): SQLite3Stmt->execute()
array(1) {
[0]=>
int(1)
}
array(1) {
[0]=>
int(2)
}
array(1) {
[0]=>
int(3)
}
31 changes: 31 additions & 0 deletions ext/sqlite3/tests/bug64531_5.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
--TEST--
Bug #64531 (SQLite3Result::fetchArray runs the query again) - SQLite3Stmt->prepare
--SKIPIF--
<?php
if (!extension_loaded('sqlite3')) die('skip sqlite3 extension not available');
?>
--FILE--
<?php
function testing($val) {
echo "Testing with: $val\n";
return true;
}

$conn = new SQLite3(':memory:');
$conn->exec("CREATE TABLE foo (id INT)");
for ($i = 1; $i <= 3; $i++) {
$conn->exec("INSERT INTO foo VALUES ($i)");
}
$conn->createFunction('testing', 'testing', 1);

$stmt = $conn->prepare("SELECT * FROM foo WHERE testing(id)");
$res = $stmt->execute();
$arr = $res->fetchArray();
$arr = $res->fetchArray();
$arr = $res->fetchArray();
$arr = $res->fetchArray();
?>
--EXPECT--
Testing with: 1
Testing with: 2
Testing with: 3
34 changes: 34 additions & 0 deletions ext/sqlite3/tests/bug79293.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
--TEST--
Bug #79293 (SQLite3Result::fetchArray() may fetch rows after last row)
--SKIPIF--
<?php
if (!extension_loaded('sqlite3')) die('skip sqlite3 extension not available');
?>
--FILE--
<?php
$db = new SQLite3(':memory:');
$db->exec("CREATE TABLE foo (bar INT)");
for ($i = 1; $i <= 3; $i++) {
$db->exec("INSERT INTO foo VALUES ($i)");
}

$res = $db->query("SELECT * FROM foo");
while (($row = $res->fetchArray(SQLITE3_ASSOC))) {
var_dump($row);
}
var_dump($res->fetchArray(SQLITE3_ASSOC));
?>
--EXPECT--
array(1) {
["bar"]=>
int(1)
}
array(1) {
["bar"]=>
int(2)
}
array(1) {
["bar"]=>
int(3)
}
bool(false)
Loading