Commit efde160ef4d for php.net

commit efde160ef4db5e87879117624b600608102eeb6a
Author: Niels Dossche <7771979+ndossche@users.noreply.github.com>
Date:   Thu Nov 13 22:54:44 2025 +0100

    phar: Fix SplFileInfo::openFile() in write mode

    This stopped working after e735d2bc3b because fp_refcount is increased,
    making phar think that the file has open read pointers.
    To fix this, the refcount shouldn't be increased but that would
    re-introduce the previous bug.
    Instead, we need to add a field that "locks" the existence of the
    internal entry separate from the refcount.

    Closes GH-20473.

diff --git a/NEWS b/NEWS
index d96969b286f..ad754d97bdb 100644
--- a/NEWS
+++ b/NEWS
@@ -32,6 +32,7 @@ PHP                                                                        NEWS
 - Phar:
   . Fixed bug GH-20732 (Phar::LoadPhar undefined behavior when reading fails).
     (ndossche)
+  . Fix SplFileInfo::openFile() in write mode. (ndossche)

 - SPL:
   . Fixed bug GH-20678 (resource created by GlobIterator crashes with fclose()).
diff --git a/ext/phar/phar.c b/ext/phar/phar.c
index 4c2cab27dce..6e06ca5e082 100644
--- a/ext/phar/phar.c
+++ b/ext/phar/phar.c
@@ -418,7 +418,7 @@ void phar_entry_remove(phar_entry_data *idata, char **error) /* {{{ */

 	phar = idata->phar;

-	if (idata->internal_file->fp_refcount < 2) {
+	if (idata->internal_file->fp_refcount < 2 && idata->internal_file->fileinfo_lock_count == 0) {
 		if (idata->fp && idata->fp != idata->phar->fp && idata->fp != idata->phar->ufp && idata->fp != idata->internal_file->fp) {
 			php_stream_close(idata->fp);
 		}
diff --git a/ext/phar/phar_internal.h b/ext/phar/phar_internal.h
index 9f8a46b65ec..4afa72db231 100644
--- a/ext/phar/phar_internal.h
+++ b/ext/phar/phar_internal.h
@@ -249,6 +249,7 @@ typedef struct _phar_entry_info {
 	php_stream               *fp;
 	php_stream               *cfp;
 	int                      fp_refcount;
+	unsigned int             fileinfo_lock_count;
 	char                     *tmp;
 	phar_archive_data        *phar;
 	char                     *link; /* symbolic link to another file */
diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c
index 7c1bb629378..6d61adee776 100644
--- a/ext/phar/phar_object.c
+++ b/ext/phar/phar_object.c
@@ -4492,7 +4492,7 @@ PHP_METHOD(PharFileInfo, __construct)

 	entry_obj->entry = entry_info;
 	if (!entry_info->is_persistent && !entry_info->is_temp_dir) {
-		++entry_info->fp_refcount;
+		++entry_info->fileinfo_lock_count;
 		/* The phar data must exist to keep the alias locked. */
 		ZEND_ASSERT(!phar_data->is_persistent);
 		++phar_data->refcount;
@@ -4540,7 +4540,7 @@ PHP_METHOD(PharFileInfo, __destruct)
 		efree(entry);
 		entry_obj->entry = NULL;
 	} else if (!entry->is_persistent) {
-		--entry->fp_refcount;
+		--entry->fileinfo_lock_count;
 		/* The entry itself still lives in the manifest,
 		 * which will either be freed here if the file info was the last reference; or freed later. */
 		entry_obj->entry = NULL;
diff --git a/ext/phar/tar.c b/ext/phar/tar.c
index 3bbaad59682..4424ad43d89 100644
--- a/ext/phar/tar.c
+++ b/ext/phar/tar.c
@@ -721,7 +721,7 @@ static int phar_tar_writeheaders_int(phar_entry_info *entry, void *argument) /*
 	}

 	if (entry->is_deleted) {
-		if (entry->fp_refcount <= 0) {
+		if (entry->fp_refcount <= 0 && entry->fileinfo_lock_count == 0) {
 			return ZEND_HASH_APPLY_REMOVE;
 		} else {
 			/* we can't delete this in-memory until it is closed */
diff --git a/ext/phar/tests/SplFileInfo_openFile_write.phpt b/ext/phar/tests/SplFileInfo_openFile_write.phpt
new file mode 100644
index 00000000000..f63baf5c7ad
--- /dev/null
+++ b/ext/phar/tests/SplFileInfo_openFile_write.phpt
@@ -0,0 +1,31 @@
+--TEST--
+SplFileInfo::openFile() in write mode
+--EXTENSIONS--
+phar
+--INI--
+phar.readonly=0
+--FILE--
+<?php
+
+$phar = new Phar(__DIR__.'/SplFileInfo_openFile_write.phar');
+$phar->addFromString('test', 'contents');
+var_dump($phar['test']->openFile('w'));
+
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__.'/SplFileInfo_openFile_write.phar');
+?>
+--EXPECTF--
+object(SplFileObject)#%d (%d) {
+  ["pathName":"SplFileInfo":private]=>
+  string(%d) "phar://%stest"
+  ["fileName":"SplFileInfo":private]=>
+  string(4) "test"
+  ["openMode":"SplFileObject":private]=>
+  string(1) "w"
+  ["delimiter":"SplFileObject":private]=>
+  string(1) ","
+  ["enclosure":"SplFileObject":private]=>
+  string(1) """
+}
diff --git a/ext/phar/tests/gh17808.phpt b/ext/phar/tests/gh17808.phpt
index 03e54ff264b..a5c13a5405e 100644
--- a/ext/phar/tests/gh17808.phpt
+++ b/ext/phar/tests/gh17808.phpt
@@ -5,18 +5,26 @@
 zlib
 --FILE--
 <?php
-$fname = __DIR__.'/tar/files/Structures_Graph-1.0.3.tgz';
+$fname = __DIR__.'/tar/files/gh17808.tgz';
+copy(__DIR__.'/tar/files/Structures_Graph-1.0.3.tgz', $fname);
 $tar = new PharData($fname);
 foreach (new RecursiveIteratorIterator($tar) as $file) {
 }
-var_dump("$file");
+var_dump($file);
 var_dump(strlen($file->getContent()));
-unlink("$file");
+unlink($file);
 var_dump($file->getATime());
 ?>
+--CLEAN--
+<?php
+@unlink(__DIR__.'/tar/files/gh17808.tgz');
+?>
 --EXPECTF--
-string(%d) "phar://%spackage.xml"
+object(PharFileInfo)#%d (%d) {
+  ["pathName":"SplFileInfo":private]=>
+  string(%d) "phar://%spackage.xml"
+  ["fileName":"SplFileInfo":private]=>
+  string(11) "package.xml"
+}
 int(6747)
-
-Warning: unlink(): phar error: "package.xml" in phar %s, has open file pointers, cannot unlink in %s on line %d
 int(33188)
diff --git a/ext/phar/zip.c b/ext/phar/zip.c
index b5133063e44..d168c2a73ff 100644
--- a/ext/phar/zip.c
+++ b/ext/phar/zip.c
@@ -833,7 +833,7 @@ static int phar_zip_changed_apply_int(phar_entry_info *entry, void *arg) /* {{{
 	}

 	if (entry->is_deleted) {
-		if (entry->fp_refcount <= 0) {
+		if (entry->fp_refcount <= 0 && entry->fileinfo_lock_count == 0) {
 			return ZEND_HASH_APPLY_REMOVE;
 		} else {
 			/* we can't delete this in-memory until it is closed */