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 */