From eac8424cd249f515150e6b9ed3fb42fb7fdfba85 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Thu, 1 Oct 2026 06:24:51 -0400 Subject: [PATCH] ext/pdo: Defer persistent handle cleanup to the last owner GH-23962 stopped a persistent PDO instance from rolling back a connection another instance still holds, but its destruction still ran the driver's persistent_shutdown handler and released the handle's failed query statement. The surviving instance then lost the SQLite functions and collations registered through the destroyed one, and its errorCode() and errorInfo() reverted to 00000. Both now wait for the last instance, like the rollback. --- NEWS | 3 ++ ext/pdo/pdo_dbh.c | 4 +- .../persistent_shared_handle_teardown.phpt | 47 +++++++++++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 ext/pdo_sqlite/tests/persistent_shared_handle_teardown.phpt diff --git a/NEWS b/NEWS index 9aa7771c056f..9a2e8885e327 100644 --- a/NEWS +++ b/NEWS @@ -139,6 +139,9 @@ PHP NEWS cycle collector. (Ilia Alshanetsky) . Fixed PDOStatement::setFetchMode() not applying changes atomically. (Ilia Alshanetsky) + . Fixed destroying a persistent PDO instance discarding SQLite functions, + collations and error information still in use by another instance. + (Ilia Alshanetsky) - PDO_DBLIB: . Fixed bug GH-23741 (segfault after a failed query inside a PDO diff --git a/ext/pdo/pdo_dbh.c b/ext/pdo/pdo_dbh.c index d3fcf6101249..cc5e5ed4b50a 100644 --- a/ext/pdo/pdo_dbh.c +++ b/ext/pdo/pdo_dbh.c @@ -1467,7 +1467,7 @@ static void dbh_free(pdo_dbh_t *dbh, bool free_persistent) { int i; - if (dbh->query_stmt) { + if (dbh->query_stmt && (!dbh->is_persistent || dbh->refcount <= 2)) { zval_ptr_dtor(&dbh->query_stmt_zval); dbh->query_stmt = NULL; } @@ -1529,7 +1529,7 @@ static void pdo_dbh_free_storage(zend_object *std) dbh->in_txn = false; } - if (dbh->is_persistent && dbh->methods && dbh->methods->persistent_shutdown) { + if (dbh->is_persistent && dbh->refcount <= 2 && dbh->methods && dbh->methods->persistent_shutdown) { dbh->methods->persistent_shutdown(dbh); } zend_object_std_dtor(std); diff --git a/ext/pdo_sqlite/tests/persistent_shared_handle_teardown.phpt b/ext/pdo_sqlite/tests/persistent_shared_handle_teardown.phpt new file mode 100644 index 000000000000..cb12b5bf8b07 --- /dev/null +++ b/ext/pdo_sqlite/tests/persistent_shared_handle_teardown.phpt @@ -0,0 +1,47 @@ +--TEST-- +Destroying a persistent PDO instance keeps handle state used by another instance +--EXTENSIONS-- +pdo_sqlite +--FILE-- + true]; + +$a = new PDO('sqlite::memory:', null, null, $options); +$b = new PDO('sqlite::memory:', null, null, $options); + +$a->sqliteCreateFunction('answer', fn() => 42, 0); +$a->sqliteCreateCollation('reverse', fn($x, $y) => strcmp($y, $x)); +$a->exec('CREATE TABLE test (id INTEGER UNIQUE)'); +$a->exec('INSERT INTO test VALUES (1)'); +try { + $a->query('INSERT INTO test VALUES (1)'); +} catch (PDOException $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} + +unset($a); + +var_dump($b->errorCode()); +var_dump($b->query('SELECT answer()')->fetchColumn()); +var_dump($b->query("SELECT 'a' UNION SELECT 'b' ORDER BY 1 COLLATE reverse")->fetchAll(PDO::FETCH_COLUMN)); + +unset($b); + +$c = new PDO('sqlite::memory:', null, null, $options); +try { + $c->query('SELECT answer()'); +} catch (PDOException $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} +?> +--EXPECT-- +PDOException: SQLSTATE[23000]: Integrity constraint violation: 19 UNIQUE constraint failed: test.id +string(5) "23000" +int(42) +array(2) { + [0]=> + string(1) "b" + [1]=> + string(1) "a" +} +PDOException: SQLSTATE[HY000]: General error: 1 no such function: answer