Commit 7302ff4f529 for php
commit 7302ff4f529f828b166a85c1702f915cda78e859
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Sun Nov 8 11:17:57 2026 -0500
ext/dba: Fix dba_close() not closing a dba_popen() connection
dba_close() looked up the persistent entry with connection->info after
dba_close_connection() had cleared it, so the entry was never removed and
the database file and its lock stayed held. Count the connections on the
entry's refcount again, as PHP 8.3 did, and close the handle when
dba_close() releases the last one, unless another persistent handle to the
same file shares its stream. Lock conflict checks now also see persistent
handles still in use, since closing one of two connections with the same
key removed the other's DBA_G(connections) entry.
Closes GH-25350
diff --git a/NEWS b/NEWS
index f13aa1a9817..0896086c19e 100644
--- a/NEWS
+++ b/NEWS
@@ -25,6 +25,10 @@ PHP NEWS
- Curl:
. Fixed curl_reset() retaining CURLOPT_PRIVATE after reset. (lacatoire)
+- DBA:
+ . Fixed dba_close() not closing a database opened with dba_popen().
+ (Ilia Alshanetsky)
+
- DOM:
. Fixed bug GH-25268 (NULL php_libxml_ref_obj dereference in
dom_objects_store_clone_obj when cloning Dom\Notation). (David Carlier)
diff --git a/ext/dba/dba.c b/ext/dba/dba.c
index 29e996e4f32..b64fdc2ce40 100644
--- a/ext/dba/dba.c
+++ b/ext/dba/dba.c
@@ -271,6 +271,22 @@ static void dba_close_info(dba_info *info)
info = NULL;
}
+static bool dba_info_shares_stream(const dba_info *info)
+{
+ ZEND_HASH_FOREACH_PTR(&EG(persistent_list), zend_resource *le) {
+ if (le->type != le_pdb || le->ptr == NULL || le->ptr == info) {
+ continue;
+ }
+ const dba_info *other = le->ptr;
+ if ((info->fp && (info->fp == other->fp || info->fp == other->lock.fp))
+ || (info->lock.fp && (info->lock.fp == other->fp || info->lock.fp == other->lock.fp))) {
+ return true;
+ }
+ } ZEND_HASH_FOREACH_END();
+
+ return false;
+}
+
static int remove_pconnection_from_list(zval *zv, void *p)
{
zend_resource *le = Z_RES_P(zv);
@@ -297,6 +313,11 @@ static void dba_close_connection(dba_connection *connection)
if (!persistent) {
dba_close_info(connection->info);
+ } else if (connection->hash) {
+ zend_resource *le = zend_hash_find_ptr(&EG(persistent_list), connection->hash);
+ if (le) {
+ GC_DELREF(le);
+ }
}
connection->info = NULL;
@@ -501,6 +522,15 @@ static dba_info *php_dba_find(const zend_string *path)
}
} ZEND_HASH_FOREACH_END();
+ ZEND_HASH_FOREACH_PTR(&EG(persistent_list), zend_resource *le) {
+ if (le->type == le_pdb && GC_REFCOUNT(le) > 1) {
+ dba_info *info = le->ptr;
+ if (info && zend_string_equals(path, info->path)) {
+ return info;
+ }
+ }
+ } ZEND_HASH_FOREACH_END();
+
return NULL;
}
/* }}} */
@@ -596,6 +626,7 @@ static void php_dba_open(INTERNAL_FUNCTION_PARAMETERS, bool persistent)
efree(resource_key);
RETURN_FALSE;
}
+ GC_ADDREF(le);
object_init_ex(return_value, dba_connection_ce);
dba_connection *connection = Z_DBA_CONNECTION_P(return_value);
@@ -951,13 +982,15 @@ static void php_dba_open(INTERNAL_FUNCTION_PARAMETERS, bool persistent)
connection->info->hnd = hptr;
if (persistent) {
- if (zend_register_persistent_resource(resource_key, resource_key_len, connection->info, le_pdb) == NULL) {
+ zend_resource *le = zend_register_persistent_resource(resource_key, resource_key_len, connection->info, le_pdb);
+ if (le == NULL) {
php_error_docref(NULL, E_WARNING, "Could not register persistent resource");
efree(resource_key);
dba_close_connection(connection);
zval_ptr_dtor(return_value);
RETURN_FALSE;
}
+ GC_ADDREF(le);
}
zend_hash_add_new(&DBA_G(connections), connection->hash, return_value);
@@ -1000,12 +1033,16 @@ PHP_FUNCTION(dba_close)
connection = Z_DBA_CONNECTION_P(id);
CHECK_DBA_CONNECTION(connection->info);
- bool persistent = connection->info->flags & DBA_PERSISTENT;
+ dba_info *info = connection->info;
+ zend_resource *le = NULL;
+ if ((info->flags & DBA_PERSISTENT) && connection->hash) {
+ le = zend_hash_find_ptr(&EG(persistent_list), connection->hash);
+ }
dba_close_connection(connection);
- if (persistent) {
- zend_hash_apply_with_argument(&EG(persistent_list), remove_pconnection_from_list, (void *) connection->info);
+ if (le && GC_REFCOUNT(le) == 1 && !dba_info_shares_stream(info)) {
+ zend_hash_apply_with_argument(&EG(persistent_list), remove_pconnection_from_list, (void *) info);
}
}
/* }}} */
diff --git a/ext/dba/tests/dba_popen_close.phpt b/ext/dba/tests/dba_popen_close.phpt
new file mode 100644
index 00000000000..9e9b9cb78a1
--- /dev/null
+++ b/ext/dba/tests/dba_popen_close.phpt
@@ -0,0 +1,70 @@
+--TEST--
+dba_close() releases a persistent connection once no other connection uses it
+--EXTENSIONS--
+dba
+--SKIPIF--
+<?php
+$handler = "flatfile";
+require_once(__DIR__ .'/skipif.inc');
+if (PHP_OS_FAMILY === 'Solaris') die("skip Solaris flock() emulation cannot detect a lock held by the same process");
+?>
+--CONFLICTS--
+dba
+--FILE--
+<?php
+$handler = "flatfile";
+require_once(__DIR__ .'/test.inc');
+
+function lock_free(string $file): bool {
+ $db = @dba_open($file, 'wlt', 'flatfile');
+ if ($db === false) {
+ return false;
+ }
+ dba_close($db);
+ return true;
+}
+
+$db1 = dba_popen($db_filename, 'cl', 'flatfile');
+dba_insert('key', 'value', $db1);
+$db2 = dba_popen($db_filename, 'cl', 'flatfile');
+
+dba_close($db2);
+echo "first still open: ", dba_fetch('key', $db1), "\n";
+echo "lock free after closing one of two: "; var_dump(lock_free($db_filename));
+
+dba_close($db1);
+echo "lock free after closing both: "; var_dump(lock_free($db_filename));
+
+$db3 = dba_popen($db_filename, 'cl', 'flatfile');
+unset($db3);
+$db4 = dba_popen($db_filename, 'cl', 'flatfile');
+echo "reused after implicit release: ", dba_fetch('key', $db4), "\n";
+dba_close($db4);
+echo "lock free after closing the reused connection: "; var_dump(lock_free($db_filename));
+
+$db5 = dba_popen($db_filename, 'cl', 'flatfile');
+$db6 = dba_popen($db_filename, 'cl', 'flatfile');
+dba_close($db5);
+var_dump(@dba_open($db_filename, 'wlt', 'flatfile'));
+echo "conflict seen after closing the earlier connection: ", error_get_last()['message'], "\n";
+dba_close($db6);
+
+$reader1 = dba_popen($db_filename, 'rl', 'flatfile');
+$reader2 = dba_popen($db_filename, 'rd', 'flatfile');
+dba_close($reader1);
+echo "connection sharing the stream still reads: ", dba_fetch('key', $reader2), "\n";
+dba_close($reader2);
+?>
+--CLEAN--
+<?php
+require(__DIR__ .'/clean.inc');
+?>
+--EXPECT--
+first still open: value
+lock free after closing one of two: bool(false)
+lock free after closing both: bool(true)
+reused after implicit release: value
+lock free after closing the reused connection: bool(true)
+bool(false)
+conflict seen after closing the earlier connection: dba_open(): Driver initialization failed for handler: flatfile: Unable to establish lock (database file already open)
+connection sharing the stream still reads: value