Skip to content

Commit f2c1329

Browse files
committed
Improve robustness of Phar*->setMetadata()
Add sanity checks for edge cases freeing metadata, when destructors or serializers modify the phar recursively. Typical use cases of php have read-only phars and would not be affected.
1 parent 7e59764 commit f2c1329

File tree

4 files changed

+74
-30
lines changed

4 files changed

+74
-30
lines changed

ext/phar/phar.c

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -652,15 +652,20 @@ zend_bool phar_metadata_tracker_has_data(const phar_metadata_tracker *tracker, i
652652
*/
653653
void phar_metadata_tracker_free(phar_metadata_tracker *tracker, int persistent) /* {{{ */
654654
{
655-
if (!Z_ISUNDEF(tracker->val)) {
656-
ZEND_ASSERT(!persistent);
657-
zval_ptr_dtor(&tracker->val);
658-
ZVAL_UNDEF(&tracker->val);
659-
}
655+
/* Free the string before the zval in case the zval's destructor modifies the metadata */
660656
if (tracker->str) {
661657
zend_string_release(tracker->str);
662658
tracker->str = NULL;
663659
}
660+
if (!Z_ISUNDEF(tracker->val)) {
661+
/* Here, copy the original zval to a different pointer without incrementing the refcount in case something uses the original while it's being freed. */
662+
zval zval_copy;
663+
664+
ZEND_ASSERT(!persistent);
665+
ZVAL_COPY_VALUE(&zval_copy, &tracker->val);
666+
ZVAL_UNDEF(&tracker->val);
667+
zval_ptr_dtor(&zval_copy);
668+
}
664669
}
665670
/* }}} */
666671

ext/phar/phar_object.c

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3963,13 +3963,42 @@ PHP_METHOD(Phar, getMetadata)
39633963
}
39643964
/* }}} */
39653965

3966+
/* {{{ Modifies the phar metadata or throws */
3967+
static int serialize_metadata_or_throw(phar_metadata_tracker *tracker, int persistent, zval *metadata)
3968+
{
3969+
php_serialize_data_t metadata_hash;
3970+
smart_str main_metadata_str = {0};
3971+
3972+
PHP_VAR_SERIALIZE_INIT(metadata_hash);
3973+
php_var_serialize(&main_metadata_str, metadata, &metadata_hash);
3974+
PHP_VAR_SERIALIZE_DESTROY(metadata_hash);
3975+
if (EG(exception)) {
3976+
/* Serialization can throw. Don't overwrite the original value or original string. */
3977+
return FAILURE;
3978+
}
3979+
3980+
phar_metadata_tracker_free(tracker, persistent);
3981+
if (EG(exception)) {
3982+
/* Destructor can throw. */
3983+
zend_string_release(main_metadata_str.s);
3984+
return FAILURE;
3985+
}
3986+
3987+
if (tracker->str) {
3988+
zend_throw_exception_ex(phar_ce_PharException, 0, "Metadata unexpectedly changed during setMetadata()");
3989+
zend_string_release(main_metadata_str.s);
3990+
return FAILURE;
3991+
}
3992+
ZVAL_COPY(&tracker->val, metadata);
3993+
tracker->str = main_metadata_str.s;
3994+
return SUCCESS;
3995+
}
3996+
39663997
/* {{{ Sets the global metadata of the phar */
39673998
PHP_METHOD(Phar, setMetadata)
39683999
{
39694000
char *error;
39704001
zval *metadata;
3971-
php_serialize_data_t metadata_hash;
3972-
smart_str main_metadata_str = {0};
39734002

39744003
PHAR_ARCHIVE_OBJECT();
39754004

@@ -3988,22 +4017,10 @@ PHP_METHOD(Phar, setMetadata)
39884017
}
39894018

39904019
ZEND_ASSERT(!phar_obj->archive->is_persistent); /* Should no longer be persistent */
3991-
3992-
phar_metadata_tracker_free(&phar_obj->archive->metadata_tracker, phar_obj->archive->is_persistent);
3993-
if (EG(exception)) {
3994-
/* Destructor can throw. */
3995-
return;
4020+
if (serialize_metadata_or_throw(&phar_obj->archive->metadata_tracker, phar_obj->archive->is_persistent, metadata) != SUCCESS) {
4021+
RETURN_THROWS();
39964022
}
39974023

3998-
PHP_VAR_SERIALIZE_INIT(metadata_hash);
3999-
php_var_serialize(&main_metadata_str, metadata, &metadata_hash);
4000-
PHP_VAR_SERIALIZE_DESTROY(metadata_hash);
4001-
if (EG(exception)) {
4002-
/* Serialization can throw. Don't overwrite the original string. */
4003-
return;
4004-
}
4005-
ZVAL_COPY(&phar_obj->archive->metadata_tracker.val, metadata);
4006-
phar_obj->archive->metadata_tracker.str = main_metadata_str.s;
40074024
phar_obj->archive->is_modified = 1;
40084025
phar_flush(phar_obj->archive, 0, 0, 0, &error);
40094026

@@ -4683,10 +4700,12 @@ PHP_METHOD(PharFileInfo, setMetadata)
46834700
}
46844701
/* re-populate after copy-on-write */
46854702
entry_obj->entry = zend_hash_str_find_ptr(&phar->manifest, entry_obj->entry->filename, entry_obj->entry->filename_len);
4703+
ZEND_ASSERT(!entry_obj->entry->is_persistent); /* Should no longer be persistent */
46864704
}
4687-
phar_metadata_tracker_free(&entry_obj->entry->metadata_tracker, entry_obj->entry->is_persistent);
46884705

4689-
ZVAL_COPY(&entry_obj->entry->metadata_tracker.val, metadata);
4706+
if (serialize_metadata_or_throw(&entry_obj->entry->metadata_tracker, entry_obj->entry->is_persistent, metadata) != SUCCESS) {
4707+
RETURN_THROWS();
4708+
}
46904709

46914710
entry_obj->entry->is_modified = 1;
46924711
entry_obj->entry->phar->is_modified = 1;

ext/phar/tests/bug69958.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Still has memory leaks, see https://bugs.php.net/bug.php?id=70005
99
$tarphar = new PharData(__DIR__.'/bug69958.tar');
1010
$phar = $tarphar->convertToData(Phar::TAR);
1111
--EXPECTF--
12-
Fatal error: Uncaught exception 'BadMethodCallException' with message 'phar "%s/bug69958.tar" exists and must be unlinked prior to conversion' in %s/bug69958.php:%d
12+
Fatal error: Uncaught BadMethodCallException: phar "%s/bug69958.tar" exists and must be unlinked prior to conversion in %s/bug69958.php:%d
1313
Stack trace:
1414
#0 %s/bug69958.php(%d): PharData->convertToData(%d)
1515
#1 {main}

ext/phar/tests/phar_metadata_write4.phpt

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ phar.readonly=0
1111
<?php
1212
class EchoesOnWakeup {
1313
public function __wakeup() {
14-
echo "In wakeup\n";
14+
echo "In __wakeup " . spl_object_id($this) . "\n";
15+
}
16+
public function __destruct() {
17+
echo "In __destruct " . spl_object_id($this) . "\n";
1518
}
1619
}
1720
class ThrowsOnSerialize {
@@ -30,6 +33,7 @@ include 'files/phar_test.inc';
3033
foreach($files as $name => $cont) {
3134
var_dump(file_get_contents($pname.'/'.$name));
3235
}
36+
unset($files);
3337

3438
$phar = new Phar($fname);
3539
echo "Loading metadata for 'a' without allowed_classes\n";
@@ -46,12 +50,18 @@ echo "Loading metadata from 'a' from the new phar\n";
4650
var_dump($phar['a']->getMetadata());
4751
echo "Loading metadata from 'a' from the new phar with unserialize options\n";
4852
var_dump($phar['a']->getMetadata(['allowed_classes' => true]));
53+
// PharEntry->setMetaData will do the following:
54+
// 1. serialize, checking for exceptions
55+
// 2. free the original data, checking for exceptions or the data getting set from destructors or error handlers.
56+
// 3. set the new data.
4957
try {
5058
var_dump($phar['a']->setMetadata(new ThrowsOnSerialize()));
5159
} catch (RuntimeException $e) {
5260
echo "Caught {$e->getMessage()} at {$e->getFile()}:{$e->getLine()}\n";
61+
unset($e);
5362
}
5463
var_dump($phar['a']->getMetadata([]));
64+
var_dump($phar['a']->getMetadata(['allowed_classes' => false]));
5565

5666
?>
5767
--CLEAN--
@@ -60,24 +70,34 @@ unlink(__DIR__ . '/' . basename(__FILE__, '.clean.php') . '.phar.php');
6070
unlink(__DIR__ . '/' . basename(__FILE__, '.clean.php') . '.phar.php.copy.php');
6171
?>
6272
--EXPECTF--
73+
In __destruct 1
6374
string(1) "a"
6475
Loading metadata for 'a' without allowed_classes
6576
object(__PHP_Incomplete_Class)#3 (1) {
6677
["__PHP_Incomplete_Class_Name"]=>
6778
string(14) "EchoesOnWakeup"
6879
}
6980
Loading metadata for 'a' with allowed_classes
70-
In wakeup
81+
In __wakeup 2
7182
object(EchoesOnWakeup)#2 (0) {
7283
}
84+
In __destruct 2
7385
Loading metadata from 'a' from the new phar
74-
In wakeup
86+
In __wakeup 3
7587
object(EchoesOnWakeup)#3 (0) {
7688
}
89+
In __destruct 3
7790
Loading metadata from 'a' from the new phar with unserialize options
78-
In wakeup
91+
In __wakeup 2
7992
object(EchoesOnWakeup)#2 (0) {
8093
}
81-
Caught In sleep at %sphar_metadata_write4.php:9
82-
object(ThrowsOnSerialize)#3 (0) {
94+
In __destruct 2
95+
Caught In sleep at %sphar_metadata_write4.php:12
96+
In __wakeup 3
97+
object(EchoesOnWakeup)#3 (0) {
98+
}
99+
In __destruct 3
100+
object(__PHP_Incomplete_Class)#4 (1) {
101+
["__PHP_Incomplete_Class_Name"]=>
102+
string(14) "EchoesOnWakeup"
83103
}

0 commit comments

Comments
 (0)