From 854ab525d2c7ca84506c6012c738e56205385e4e Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Wed, 21 Oct 2026 16:20:15 -0400 Subject: [PATCH] ext/pdo: Fix GC freeing callbacks of a shared persistent connection Every PDO object attached to a persistent connection shares one pdo_dbh_t, and dbh_get_gc() reported the driver's GC data once per object. pdo_sqlite holds each user function, aggregate and collation callback once, so with two or more objects the collector subtracted more references than exist and freed live closures, crashing or returning wrong results. Report driver GC data only while a single object owns the handle, the same refcount check the rollback guard uses since GH-23962. While the connection is shared, cycles through those callbacks are left to request shutdown, as they already are with GC disabled. No other bundled driver implements get_gc. --- NEWS | 3 ++ ext/pdo/pdo_dbh.c | 3 +- .../tests/gc_persistent_shared.phpt | 30 +++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 ext/pdo_sqlite/tests/gc_persistent_shared.phpt diff --git a/NEWS b/NEWS index 12d15e699f1f..6794dbe0a246 100644 --- a/NEWS +++ b/NEWS @@ -89,6 +89,9 @@ PHP NEWS (Ilia Alshanetsky) . Fixed a duplicate warning when ATTR_STATEMENT_CLASS is rejected on a persistent connection. (Ilia Alshanetsky) + . Fixed crash when the garbage collector freed SQLite user functions, + aggregates, or collations of a persistent connection shared by several + PDO objects. (Ilia Alshanetsky) - Phpdbg: . Fix wrong command names. (ndossche, Weilin Du) diff --git a/ext/pdo/pdo_dbh.c b/ext/pdo/pdo_dbh.c index 804cbb644077..05ac39bb2838 100644 --- a/ext/pdo/pdo_dbh.c +++ b/ext/pdo/pdo_dbh.c @@ -1443,7 +1443,8 @@ static HashTable *dbh_get_gc(zend_object *object, zval **gc_data, int *gc_count) pdo_dbh_t *dbh = php_pdo_dbh_fetch_inner(object); zend_get_gc_buffer *gc_buffer = zend_get_gc_buffer_create(); zend_get_gc_buffer_add_zval(gc_buffer, &dbh->def_stmt_ctor_args); - if (dbh->methods && dbh->methods->get_gc) { + if (dbh->methods && dbh->methods->get_gc + && (!dbh->is_persistent || dbh->refcount <= 2)) { dbh->methods->get_gc(dbh, gc_buffer); } zend_get_gc_buffer_use(gc_buffer, gc_data, gc_count); diff --git a/ext/pdo_sqlite/tests/gc_persistent_shared.phpt b/ext/pdo_sqlite/tests/gc_persistent_shared.phpt new file mode 100644 index 000000000000..21854b94b07c --- /dev/null +++ b/ext/pdo_sqlite/tests/gc_persistent_shared.phpt @@ -0,0 +1,30 @@ +--TEST-- +GC must not free callbacks of a persistent connection shared by several PDO objects +--EXTENSIONS-- +pdo_sqlite +--FILE-- +pdo = Pdo\Sqlite::connect('sqlite::memory:', null, null, [PDO::ATTR_PERSISTENT => true]); + $this->pdo->createFunction("udf_$name", [$this, 'udf'], 1); + } + + public function udf($x) { + return $this->name . $x; + } +} + +$repos = [new Repo('one'), new Repo('two')]; +echo "collected: "; var_dump(gc_collect_cycles()); +echo "udf_one: "; var_dump($repos[0]->pdo->query("SELECT udf_one('!')")->fetchColumn()); +echo "udf_two: "; var_dump($repos[1]->pdo->query("SELECT udf_two('?')")->fetchColumn()); + +unset($repos[0]->pdo, $repos[1]->pdo); +?> +--EXPECT-- +collected: int(0) +udf_one: string(4) "one!" +udf_two: string(4) "two?"