-
Notifications
You must be signed in to change notification settings - Fork 7.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changing the on-disk Phar after mapPhar()
reads incorrect files with the phar://
protocol
#17125
Comments
Right, and if I understand correctly: to fix this we should only use the cached file offsets if the file is still the same. The alternative of unconditionally using the cached offsets together with the cached phar would not work because that will still not allow auto-updating phars to work. |
Do you mean because of Windows not allowing to delete / rename a file that is still open? Because my expectation would be that the state of the files within a Phar is internally consistent, independent of whether I load a file before or after the Phar is replaced on disk. It should not silently use a new version (an error would be fine, caching the actual contents would be preferred). If I want to access the new contents, I would |
I'm getting confused about this ticket, but I think I'm starting to understand your perspective. Let's make sure we're on the same page. I understand that there is a bug here in that there's a mismatch between the cached offsets and when the contents are being read, resulting in the contents being garbled. I saw two possible fixes:
I thought the proper fix would be option 1, but you seem to imply the proper fix is option 2. I would have to recheck how this works again internally, but I'm not so sure that the phar file contents are cached (e.g. if a particular file in the phar was never read before). |
Yes.
Yes. At least that would be a reasonable expectation from the user's perspective: They executed Consider the following case for the updater logic. The stub contains: <?php
spl_autoload_register(static function ($class) {
include 'phar://my-app.phar/lib/' . str_replace('\\', '/', $class) . '.php';
});
Phar::mapPhar('my-app.phar');
include 'phar://my-app.phar/startup.php';
__HALT_COMPILER();
?> And then the updater logic does something like this: $eventDispatcher->dispatch(new UpdateIncoming($newVersion));
$tmp = tempnam();
copy("http://example.com/my-app_{$newVersion}.phar", $tmp);
if (rename($tmp, $_SERVER['argv'][0])) {
$eventDispatcher->dispatch(new UpdateSuccessful($newVersion)); // triggers the autoloader for `UpdateSuccessful`
} else {
$eventDispatcher->dispatch(new UpdateFailed($newVersion)); // triggers the autoloader for `UpdateFailed`
} And the new version of the app changes the
They are not. What I was suggesting as a possible solution, without looking at it in detail, was doing something like this: static function mapPhar($mapAs) {
$file = open($_SERVER['argv'][0], 'r');
$offsets = calculateOffsets($file);
self::$mappingTable[$mapAs] = [
'offsets' => $offsets,
'file' => $file,
];
} And then reusing the cached |
I just realize there is a related issue when deleting the Phar: <?php
$a = new Phar(__DIR__ . '/output.phar');
$a->addFromString(
'foo.php',
<<<'EOT'
<?php
echo "this is foo.php\n";
EOT
);
$a->addFromString(
'bar.php',
<<<'EOT'
<?php
echo "this is bar.php\n";
EOT
);
$a->setStub(
<<<'EOT'
<?php
Phar::mapPhar('my-app.phar');
include 'phar://my-app.phar/foo.php';
unlink(realpath($_SERVER['argv'][0]));
include 'phar://my-app.phar/bar.php';
__HALT_COMPILER(); ?>
EOT
); results in:
I would expect opening |
Thanks for the detailed explanation. I've analysed this and I know why it doesn't work. Lines 263 to 272 in 26244c7
This means that the next include, after the rename, will reopen the phar from disk. EDIT: actually, there is a check for |
If we solely rely on whether the phar is alias, then the condition should be changed to
So the flag approach is probably the more correct one. This would look something like the following and would fix the opening post code too while not introducing test regressions: diff --git a/ext/phar/phar.c b/ext/phar/phar.c
index a4a9b7d2139..2ae06609c4b 100644
--- a/ext/phar/phar.c
+++ b/ext/phar/phar.c
@@ -260,7 +260,7 @@ bool phar_archive_delref(phar_archive_data *phar) /* {{{ */
PHAR_G(last_phar) = NULL;
PHAR_G(last_phar_name) = PHAR_G(last_alias) = NULL;
- if (phar->fp && (!(phar->flags & PHAR_FILE_COMPRESSION_MASK) || !phar->alias)) {
+ if (phar->fp && (!(phar->flags & (PHAR_FILE_MAPPED | PHAR_FILE_COMPRESSION_MASK)) || !phar->alias)) {
/* close open file handle - allows removal or rename of
the file on windows, which has greedy locking
only close if the archive was not already compressed. If it
@@ -2329,7 +2329,9 @@ zend_result phar_open_executed_filename(char *alias, size_t alias_len, char **er
return FAILURE;
}
- if (phar_open_parsed_phar(ZSTR_VAL(fname), ZSTR_LEN(fname), alias, alias_len, 0, REPORT_ERRORS, NULL, 0) == SUCCESS) {
+ phar_archive_data *phar;
+ if (phar_open_parsed_phar(ZSTR_VAL(fname), ZSTR_LEN(fname), alias, alias_len, 0, REPORT_ERRORS, &phar, 0) == SUCCESS) {
+ phar->flags |= PHAR_FILE_MAPPED;
return SUCCESS;
}
@@ -2362,7 +2364,11 @@ zend_result phar_open_executed_filename(char *alias, size_t alias_len, char **er
fname = actual;
}
- zend_result ret = phar_open_from_fp(fp, ZSTR_VAL(fname), ZSTR_LEN(fname), alias, alias_len, REPORT_ERRORS, NULL, error);
+ zend_result ret = phar_open_from_fp(fp, ZSTR_VAL(fname), ZSTR_LEN(fname), alias, alias_len, REPORT_ERRORS, &phar, error);
+
+ if (ret == SUCCESS) {
+ phar->flags |= PHAR_FILE_MAPPED;
+ }
if (actual) {
zend_string_release_ex(actual, 0);
diff --git a/ext/phar/phar_internal.h b/ext/phar/phar_internal.h
index 2f9ae7c1c84..a32b2a6400f 100644
--- a/ext/phar/phar_internal.h
+++ b/ext/phar/phar_internal.h
@@ -51,6 +51,7 @@
#define PHAR_FILE_COMPRESSED_NONE 0x00000000
#define PHAR_FILE_COMPRESSED_GZ 0x00100000
#define PHAR_FILE_COMPRESSED_BZ2 0x00200000
+#define PHAR_FILE_MAPPED 0x01000000
#define PHAR_SIG_MD5 0x0001
#define PHAR_SIG_SHA1 0x0002
Please make a sanity check here if you can. |
Description
The following code:
Running
php a.phar
resulted in this output:But I expected this output instead:
The reason for this appears to be that the file offsets and lengths are cached during the
mapPhar()
, but each time the phar is opened withphar://X.phar
it is reopened from disk without validating that it's still the same phar / instead of reopening it from a cached file descriptor.This is an issue for self-updating phars where the autoloader needs to load a class after the update has been performed.
PHP Version
PHP 8.3.14
Operating System
Ubuntu 24.04
The text was updated successfully, but these errors were encountered: