From fc100ce777cf897502ab767352c941eeba051862 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Mon, 24 Aug 2026 18:58:51 -0400 Subject: [PATCH] ext/pdo: Report bound params/columns to GC and fix driver_params refcount PDOStatement's get_gc handler never reported bound_params or bound_columns, so reference cycles through bindParam()/bindColumn() arguments stayed invisible to the collector, and since 5b8d0dc6ae0 register_bound_param() copied driver_params while really_register_bound_param() added another reference, leaving one zval with two references that no collector could reclaim. Report both hash tables, drop the extra addref, and release the caller's reference on the registration failure path. --- NEWS | 5 +++ ext/pdo/pdo_stmt.c | 24 ++++++++++++--- ext/pdo/tests/get_gc_bound_params.phpt | 42 ++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 ext/pdo/tests/get_gc_bound_params.phpt diff --git a/NEWS b/NEWS index e7f45c810b23..79139490fc59 100644 --- a/NEWS +++ b/NEWS @@ -57,6 +57,11 @@ PHP NEWS is_cacheable_stream_path()). (ndossche) - PDO: + . Fixed a leaked reference to the driver options passed to + PDOStatement::bindParam() and PDOStatement::bindColumn(). + (Ilia Alshanetsky) + . Fixed PDOStatement not reporting its bound parameters and columns to the + cycle collector. (Ilia Alshanetsky) . Fixed PDOStatement::getColumnMeta() reading out of bounds for an invalid column index. (Ilia Alshanetsky) . Fixed PDOStatement::bindColumn() registering a binding for a column name diff --git a/ext/pdo/pdo_stmt.c b/ext/pdo/pdo_stmt.c index 97d1a058fd52..0a81c207a340 100644 --- a/ext/pdo/pdo_stmt.c +++ b/ext/pdo/pdo_stmt.c @@ -287,10 +287,6 @@ static bool really_register_bound_param(struct pdo_bound_param_data *param, pdo_ param->stmt = stmt; param->is_param = is_param; - if (Z_REFCOUNTED(param->driver_params)) { - Z_ADDREF(param->driver_params); - } - if (!is_param && param->name && stmt->columns) { /* try to map the name to the column */ int i; @@ -376,6 +372,7 @@ static bool really_register_bound_param(struct pdo_bound_param_data *param, pdo_ } /* param->parameter is freed by hash dtor */ ZVAL_UNDEF(¶m->parameter); + ZVAL_UNDEF(¶m->driver_params); return 0; } } @@ -1462,6 +1459,9 @@ static void register_bound_param(INTERNAL_FUNCTION_PARAMETERS, int is_param) /* if (!Z_ISUNDEF(param.parameter)) { zval_ptr_dtor(&(param.parameter)); } + if (!Z_ISUNDEF(param.driver_params)) { + zval_ptr_dtor(&(param.driver_params)); + } RETURN_FALSE; } @@ -2108,6 +2108,22 @@ static HashTable *dbstmt_get_gc(zend_object *object, zval **gc_data, int *gc_cou zend_get_gc_buffer *gc_buffer = zend_get_gc_buffer_create(); zend_get_gc_buffer_add_zval(gc_buffer, &stmt->database_object_handle); zend_get_gc_buffer_add_zval(gc_buffer, &stmt->fetch.into); + if (stmt->bound_params) { + zval *val; + ZEND_HASH_FOREACH_VAL(stmt->bound_params, val) { + struct pdo_bound_param_data *param = Z_PTR_P(val); + zend_get_gc_buffer_add_zval(gc_buffer, ¶m->parameter); + zend_get_gc_buffer_add_zval(gc_buffer, ¶m->driver_params); + } ZEND_HASH_FOREACH_END(); + } + if (stmt->bound_columns) { + zval *val; + ZEND_HASH_FOREACH_VAL(stmt->bound_columns, val) { + struct pdo_bound_param_data *param = Z_PTR_P(val); + zend_get_gc_buffer_add_zval(gc_buffer, ¶m->parameter); + zend_get_gc_buffer_add_zval(gc_buffer, ¶m->driver_params); + } ZEND_HASH_FOREACH_END(); + } zend_get_gc_buffer_use(gc_buffer, gc_data, gc_count); /** diff --git a/ext/pdo/tests/get_gc_bound_params.phpt b/ext/pdo/tests/get_gc_bound_params.phpt new file mode 100644 index 000000000000..ac9e8e54a9ab --- /dev/null +++ b/ext/pdo/tests/get_gc_bound_params.phpt @@ -0,0 +1,42 @@ +--TEST-- +PDOStatement::get_gc() must report bound params and columns for cycle collection +--EXTENSIONS-- +pdo +pdo_sqlite +--FILE-- +exec('CREATE TABLE test(a INT)'); +$stmt = $db->prepare('INSERT INTO test VALUES (?)'); +$stmt->execute([1]); +$stmt2 = $db->prepare('SELECT a FROM test'); +$stmt2->execute(); +$stmt2->fetch(PDO::FETCH_ASSOC); +for ($i = 0; $i < 3; $i++) { + $val = 1; + $tracked = new Tracked(); + $tracked->stmt = $stmt; + $stmt->bindParam(1, $val, PDO::PARAM_INT, 0, $tracked); + $col = null; + $tracked2 = new Tracked(); + $tracked2->stmt = $stmt2; + $stmt2->bindColumn('a', $col, PDO::PARAM_STR, 0, $tracked2); + unset($tracked, $tracked2); +} +unset($stmt, $stmt2, $db); +gc_collect_cycles(); +echo "end\n"; +--EXPECT-- +collected +collected +collected +collected +collected +collected +end