diff --git a/UPGRADING b/UPGRADING index 52443866555c8..dd91049f98068 100644 --- a/UPGRADING +++ b/UPGRADING @@ -425,6 +425,11 @@ PHP 8.0 UPGRADE NOTES - PDO_ODBC: . The php.ini directive pdo_odbc.db2_instance_name has been removed +- Phar: + . Metadata associated with a phar will no longer be automatically unserialized, + to fix potential security vulnerabilities due to object instantiation, autoloading, etc. + RFC: https://wiki.php.net/rfc/phar_stop_autoloading_metadata + - Reflection: . The method signatures diff --git a/ext/phar/phar.c b/ext/phar/phar.c index 70f469cab5cf9..6434a75a75cef 100644 --- a/ext/phar/phar.c +++ b/ext/phar/phar.c @@ -21,6 +21,7 @@ #include "phar_internal.h" #include "SAPI.h" #include "func_interceptors.h" +#include "ext/standard/php_var.h" static void destroy_phar_data(zval *zv); @@ -229,20 +230,7 @@ void phar_destroy_phar_data(phar_archive_data *phar) /* {{{ */ HT_INVALIDATE(&phar->virtual_dirs); } - if (Z_TYPE(phar->metadata) != IS_UNDEF) { - if (phar->is_persistent) { - if (phar->metadata_len) { - /* for zip comments that are strings */ - free(Z_PTR(phar->metadata)); - } else { - zval_internal_ptr_dtor(&phar->metadata); - } - } else { - zval_ptr_dtor(&phar->metadata); - } - phar->metadata_len = 0; - ZVAL_UNDEF(&phar->metadata); - } + phar_metadata_tracker_free(&phar->metadata_tracker, phar->is_persistent); if (phar->fp) { php_stream_close(phar->fp); @@ -383,25 +371,7 @@ void destroy_phar_manifest_entry_int(phar_entry_info *entry) /* {{{ */ entry->fp = 0; } - if (Z_TYPE(entry->metadata) != IS_UNDEF) { - if (entry->is_persistent) { - if (entry->metadata_len) { - /* for zip comments that are strings */ - free(Z_PTR(entry->metadata)); - } else { - zval_internal_ptr_dtor(&entry->metadata); - } - } else { - zval_ptr_dtor(&entry->metadata); - } - entry->metadata_len = 0; - ZVAL_UNDEF(&entry->metadata); - } - - if (entry->metadata_str.s) { - smart_str_free(&entry->metadata_str); - entry->metadata_str.s = NULL; - } + phar_metadata_tracker_free(&entry->metadata_tracker, entry->is_persistent); pefree(entry->filename, entry->is_persistent); @@ -600,49 +570,153 @@ int phar_open_parsed_phar(char *fname, size_t fname_len, char *alias, size_t ali /* }}}*/ /** - * Parse out metadata from the manifest for a single file - * - * Meta-data is in this format: - * [len32][data...] + * Attempt to serialize the data. + * Callers are responsible for handling EG(exception) if one occurs. + */ +void phar_metadata_tracker_try_ensure_has_serialized_data(phar_metadata_tracker *tracker, int persistent) /* {{{ */ +{ + php_serialize_data_t metadata_hash; + smart_str metadata_str = {0}; + if (tracker->str || Z_ISUNDEF(tracker->val)) { + /* Already has serialized the value or there is no value */ + return; + } + /* Assert it should not be possible to create raw zvals in a persistent phar (i.e. from cache_list) */ + ZEND_ASSERT(!persistent); + + PHP_VAR_SERIALIZE_INIT(metadata_hash); + php_var_serialize(&metadata_str, &tracker->val, &metadata_hash); + PHP_VAR_SERIALIZE_DESTROY(metadata_hash); + if (!metadata_str.s) { + return; + } + tracker->str = metadata_str.s; +} +/* }}} */ + +/** + * Parse out metadata when phar_metadata_tracker_has_data is true. * - * data is the serialized zval + * Precondition: phar_metadata_tracker_has_data is true */ -int phar_parse_metadata(char **buffer, zval *metadata, uint32_t zip_metadata_len) /* {{{ */ +int phar_metadata_tracker_unserialize_or_copy(phar_metadata_tracker *tracker, zval *metadata, int persistent, HashTable *unserialize_options, const char* method_name) /* {{{ */ { - php_unserialize_data_t var_hash; + const zend_bool has_unserialize_options = unserialize_options != NULL && zend_array_count(unserialize_options) > 0; + /* It should be impossible to create a zval in a persistent phar/entry. */ + ZEND_ASSERT(!persistent || Z_ISUNDEF(tracker->val)); + + if (Z_ISUNDEF(tracker->val) || has_unserialize_options) { + if (EG(exception)) { + /* Because other parts of the phar code haven't been updated to check for exceptions after doing something that may throw, + * check for exceptions before potentially serializing/unserializing instead. */ + return FAILURE; + } + /* Persistent phars should always be unserialized. */ + const char *start; + /* Assert it should not be possible to create raw data in a persistent phar (i.e. from cache_list) */ - if (zip_metadata_len) { - const unsigned char *p; - unsigned char *p_buff = (unsigned char *)estrndup(*buffer, zip_metadata_len); - p = p_buff; + /* Precondition: This has serialized data, either from setMetadata or the phar file. */ + ZEND_ASSERT(tracker->str != NULL); ZVAL_NULL(metadata); - PHP_VAR_UNSERIALIZE_INIT(var_hash); + start = ZSTR_VAL(tracker->str); - if (!php_var_unserialize(metadata, &p, p + zip_metadata_len, &var_hash)) { - efree(p_buff); - PHP_VAR_UNSERIALIZE_DESTROY(var_hash); + php_unserialize_with_options(metadata, start, ZSTR_LEN(tracker->str), unserialize_options, method_name); + if (EG(exception)) { zval_ptr_dtor(metadata); ZVAL_UNDEF(metadata); return FAILURE; } - efree(p_buff); - PHP_VAR_UNSERIALIZE_DESTROY(var_hash); - - if (PHAR_G(persist)) { - /* lazy init metadata */ - zval_ptr_dtor(metadata); - Z_PTR_P(metadata) = pemalloc(zip_metadata_len, 1); - memcpy(Z_PTR_P(metadata), *buffer, zip_metadata_len); - return SUCCESS; - } + return SUCCESS; } else { - ZVAL_UNDEF(metadata); + /* TODO: what is the current/expected behavior when fetching an object set with setMetadata then getting it + * with getMetadata() and modifying a property? Previously, it was underdefined, and probably unimportant to support. */ + ZVAL_COPY(metadata, &tracker->val); } return SUCCESS; } /* }}}*/ +/** + * Check if this has any data, serialized or as a raw value. + */ +zend_bool phar_metadata_tracker_has_data(const phar_metadata_tracker *tracker, int persistent) /* {{{ */ +{ + ZEND_ASSERT(!persistent || Z_ISUNDEF(tracker->val)); + return !Z_ISUNDEF(tracker->val) || tracker->str != NULL; +} +/* }}} */ + +/** + * Free memory used to track the metadata and set all fields to be null/undef. + */ +void phar_metadata_tracker_free(phar_metadata_tracker *tracker, int persistent) /* {{{ */ +{ + /* Free the string before the zval in case the zval's destructor modifies the metadata */ + if (tracker->str) { + zend_string_release(tracker->str); + tracker->str = NULL; + } + if (!Z_ISUNDEF(tracker->val)) { + /* Here, copy the original zval to a different pointer without incrementing the refcount in case something uses the original while it's being freed. */ + zval zval_copy; + + ZEND_ASSERT(!persistent); + ZVAL_COPY_VALUE(&zval_copy, &tracker->val); + ZVAL_UNDEF(&tracker->val); + zval_ptr_dtor(&zval_copy); + } +} +/* }}} */ + +/** + * Free memory used to track the metadata and set all fields to be null/undef. + */ +void phar_metadata_tracker_copy(phar_metadata_tracker *dest, const phar_metadata_tracker *source, int persistent) /* {{{ */ +{ + ZEND_ASSERT(dest != source); + phar_metadata_tracker_free(dest, persistent); + + if (!Z_ISUNDEF(source->val)) { + ZEND_ASSERT(!persistent); + ZVAL_COPY(&dest->val, &source->val); + } + if (source->str) { + dest->str = zend_string_copy(source->str); + } +} +/* }}} */ + +/** + * Increment reference counts after a metadata entry was copied + */ +void phar_metadata_tracker_clone(phar_metadata_tracker *tracker) /* {{{ */ +{ + Z_TRY_ADDREF_P(&tracker->val); + if (tracker->str) { + tracker->str = zend_string_copy(tracker->str); + } +} +/* }}} */ + +/** + * Parse out metadata from the manifest for a single file, saving it into a string. + * + * Meta-data is in this format: + * [len32][data...] + * + * data is the serialized zval + */ +void phar_parse_metadata_lazy(const char *buffer, phar_metadata_tracker *tracker, uint32_t zip_metadata_len, int persistent) /* {{{ */ +{ + phar_metadata_tracker_free(tracker, persistent); + if (zip_metadata_len) { + /* lazy init metadata */ + tracker->str = zend_string_init(buffer, zip_metadata_len, persistent); + } +} +/* }}}*/ + /** * Size of fixed fields in the manifest. * See: http://php.net/manual/en/phar.fileformat.phar.php @@ -1028,7 +1102,6 @@ static int phar_parse_pharfile(php_stream *fp, char *fname, size_t fname_len, ch /* check whether we have meta data, zero check works regardless of byte order */ SAFE_PHAR_GET_32(buffer, endbuffer, len); if (mydata->is_persistent) { - mydata->metadata_len = len; if (!len) { /* FIXME: not sure why this is needed but removing it breaks tests */ SAFE_PHAR_GET_32(buffer, endbuffer, len); @@ -1037,9 +1110,8 @@ static int phar_parse_pharfile(php_stream *fp, char *fname, size_t fname_len, ch if(len > (size_t)(endbuffer - buffer)) { MAPPHAR_FAIL("internal corruption of phar \"%s\" (trying to read past buffer end)"); } - if (phar_parse_metadata(&buffer, &mydata->metadata, len) == FAILURE) { - MAPPHAR_FAIL("unable to read phar metadata in .phar file \"%s\""); - } + /* Don't implicitly call unserialize() on potentially untrusted input unless getMetadata() is called directly. */ + phar_parse_metadata_lazy(buffer, &mydata->metadata_tracker, len, mydata->is_persistent); buffer += len; /* set up our manifest */ @@ -1112,19 +1184,15 @@ static int phar_parse_pharfile(php_stream *fp, char *fname, size_t fname_len, ch } PHAR_GET_32(buffer, len); - if (entry.is_persistent) { - entry.metadata_len = len; - } else { - entry.metadata_len = 0; - } if (len > (size_t)(endbuffer - buffer)) { pefree(entry.filename, entry.is_persistent); MAPPHAR_FAIL("internal corruption of phar \"%s\" (truncated manifest entry)"); } - if (phar_parse_metadata(&buffer, &entry.metadata, len) == FAILURE) { - pefree(entry.filename, entry.is_persistent); - MAPPHAR_FAIL("unable to read file metadata in .phar file \"%s\""); - } + /* Don't implicitly call unserialize() on potentially untrusted input unless getMetadata() is called directly. */ + /* The same local variable entry is reused in a loop, so reset the state before reading data. */ + ZVAL_UNDEF(&entry.metadata_tracker.val); + entry.metadata_tracker.str = NULL; + phar_parse_metadata_lazy(buffer, &entry.metadata_tracker, len, entry.is_persistent); buffer += len; entry.offset = entry.offset_abs = offset; @@ -1133,39 +1201,21 @@ static int phar_parse_pharfile(php_stream *fp, char *fname, size_t fname_len, ch switch (entry.flags & PHAR_ENT_COMPRESSION_MASK) { case PHAR_ENT_COMPRESSED_GZ: if (!PHAR_G(has_zlib)) { - if (Z_TYPE(entry.metadata) != IS_UNDEF) { - if (entry.is_persistent) { - free(Z_PTR(entry.metadata)); - } else { - zval_ptr_dtor(&entry.metadata); - } - } + phar_metadata_tracker_free(&entry.metadata_tracker, entry.is_persistent); pefree(entry.filename, entry.is_persistent); MAPPHAR_FAIL("zlib extension is required for gz compressed .phar file \"%s\""); } break; case PHAR_ENT_COMPRESSED_BZ2: if (!PHAR_G(has_bz2)) { - if (Z_TYPE(entry.metadata) != IS_UNDEF) { - if (entry.is_persistent) { - free(Z_PTR(entry.metadata)); - } else { - zval_ptr_dtor(&entry.metadata); - } - } + phar_metadata_tracker_free(&entry.metadata_tracker, entry.is_persistent); pefree(entry.filename, entry.is_persistent); MAPPHAR_FAIL("bz2 extension is required for bzip2 compressed .phar file \"%s\""); } break; default: if (entry.uncompressed_filesize != entry.compressed_filesize) { - if (Z_TYPE(entry.metadata) != IS_UNDEF) { - if (entry.is_persistent) { - free(Z_PTR(entry.metadata)); - } else { - zval_ptr_dtor(&entry.metadata); - } - } + phar_metadata_tracker_free(&entry.metadata_tracker, entry.is_persistent); pefree(entry.filename, entry.is_persistent); MAPPHAR_FAIL("internal corruption of phar \"%s\" (compressed and uncompressed size does not match for uncompressed entry)"); } @@ -2673,9 +2723,11 @@ int phar_flush(phar_archive_data *phar, char *user_stub, zend_long len, int conv /* compress as necessary, calculate crcs, serialize meta-data, manifest size, and file sizes */ main_metadata_str.s = NULL; - if (Z_TYPE(phar->metadata) != IS_UNDEF) { + if (phar->metadata_tracker.str) { + smart_str_appendl(&main_metadata_str, ZSTR_VAL(phar->metadata_tracker.str), ZSTR_LEN(phar->metadata_tracker.str)); + } else if (!Z_ISUNDEF(phar->metadata_tracker.val)) { PHP_VAR_SERIALIZE_INIT(metadata_hash); - php_var_serialize(&main_metadata_str, &phar->metadata, &metadata_hash); + php_var_serialize(&main_metadata_str, &phar->metadata_tracker.val, &metadata_hash); PHP_VAR_SERIALIZE_DESTROY(metadata_hash); } new_manifest_count = 0; @@ -2710,23 +2762,18 @@ int phar_flush(phar_archive_data *phar, char *user_stub, zend_long len, int conv /* we use this to calculate API version, 1.1.1 is used for phars with directories */ has_dirs = 1; } - if (Z_TYPE(entry->metadata) != IS_UNDEF) { - if (entry->metadata_str.s) { - smart_str_free(&entry->metadata_str); - } - entry->metadata_str.s = NULL; + if (!Z_ISUNDEF(entry->metadata_tracker.val) && !entry->metadata_tracker.str) { + ZEND_ASSERT(!entry->is_persistent); + /* Assume serialization will succeed. TODO: Set error and throw if EG(exception) != NULL */ + smart_str buf = {0}; PHP_VAR_SERIALIZE_INIT(metadata_hash); - php_var_serialize(&entry->metadata_str, &entry->metadata, &metadata_hash); + php_var_serialize(&buf, &entry->metadata_tracker.val, &metadata_hash); PHP_VAR_SERIALIZE_DESTROY(metadata_hash); - } else { - if (entry->metadata_str.s) { - smart_str_free(&entry->metadata_str); - } - entry->metadata_str.s = NULL; + entry->metadata_tracker.str = buf.s; } /* 32 bits for filename length, length of filename, manifest + metadata, and add 1 for trailing / if a directory */ - offset += 4 + entry->filename_len + sizeof(entry_buffer) + (entry->metadata_str.s ? ZSTR_LEN(entry->metadata_str.s) : 0) + (entry->is_dir ? 1 : 0); + offset += 4 + entry->filename_len + sizeof(entry_buffer) + (entry->metadata_tracker.str ? ZSTR_LEN(entry->metadata_tracker.str) : 0) + (entry->is_dir ? 1 : 0); /* compress and rehash as necessary */ if ((oldfile && !entry->is_modified) || entry->is_dir) { @@ -2916,6 +2963,7 @@ int phar_flush(phar_archive_data *phar, char *user_stub, zend_long len, int conv /* now write the manifest */ ZEND_HASH_FOREACH_PTR(&phar->manifest, entry) { + const zend_string *metadata_str; if (entry->is_deleted || entry->is_mounted) { /* remove this from the new phar if deleted, ignore if mounted */ continue; @@ -2960,11 +3008,12 @@ int phar_flush(phar_archive_data *phar, char *user_stub, zend_long len, int conv phar_set_32(entry_buffer+8, entry->compressed_filesize); phar_set_32(entry_buffer+12, entry->crc32); phar_set_32(entry_buffer+16, entry->flags); - phar_set_32(entry_buffer+20, entry->metadata_str.s ? ZSTR_LEN(entry->metadata_str.s) : 0); + metadata_str = entry->metadata_tracker.str; + phar_set_32(entry_buffer+20, metadata_str ? ZSTR_LEN(metadata_str) : 0); if (sizeof(entry_buffer) != php_stream_write(newfile, entry_buffer, sizeof(entry_buffer)) - || (entry->metadata_str.s && - ZSTR_LEN(entry->metadata_str.s) != php_stream_write(newfile, ZSTR_VAL(entry->metadata_str.s), ZSTR_LEN(entry->metadata_str.s)))) { + || (metadata_str && + ZSTR_LEN(metadata_str) != php_stream_write(newfile, ZSTR_VAL(metadata_str), ZSTR_LEN(metadata_str)))) { if (closeoldfile) { php_stream_close(oldfile); } diff --git a/ext/phar/phar_internal.h b/ext/phar/phar_internal.h index e999d9d757466..f959cb29bc683 100644 --- a/ext/phar/phar_internal.h +++ b/ext/phar/phar_internal.h @@ -212,6 +212,17 @@ enum phar_fp_type { PHAR_TMP }; +/* + * Represents the metadata of the phar file or a file entry within the phar. + * Can contain any combination of serialized data and the value as needed. + */ +typedef struct _phar_metadata_tracker { + /* Can be IS_UNDEF or a regular value */ + zval val; + /* Nullable string with the serialized value, if the serialization was performed or read from a file. */ + zend_string *str; +} phar_metadata_tracker; + /* entry for one file in a phar file */ typedef struct _phar_entry_info { /* first bytes are exactly as in file */ @@ -223,8 +234,7 @@ typedef struct _phar_entry_info { /* remainder */ /* when changing compression, save old flags in case fp is NULL */ uint32_t old_flags; - zval metadata; - uint32_t metadata_len; /* only used for cached manifests */ + phar_metadata_tracker metadata_tracker; uint32_t filename_len; char *filename; enum phar_fp_type fp_type; @@ -239,7 +249,6 @@ typedef struct _phar_entry_info { int fp_refcount; char *tmp; phar_archive_data *phar; - smart_str metadata_str; char *link; /* symbolic link to another file */ char tar_type; /* position in the manifest */ @@ -291,8 +300,7 @@ struct _phar_archive_data { uint32_t sig_flags; uint32_t sig_len; char *signature; - zval metadata; - uint32_t metadata_len; /* only used for cached manifests */ + phar_metadata_tracker metadata_tracker; uint32_t phar_pos; /* if 1, then this alias was manually specified by the user and is not a permanent alias */ uint32_t is_temporary_alias:1; @@ -544,7 +552,16 @@ int phar_mount_entry(phar_archive_data *phar, char *filename, size_t filename_le zend_string *phar_find_in_include_path(char *file, size_t file_len, phar_archive_data **pphar); char *phar_fix_filepath(char *path, size_t *new_len, int use_cwd); phar_entry_info * phar_open_jit(phar_archive_data *phar, phar_entry_info *entry, char **error); -int phar_parse_metadata(char **buffer, zval *metadata, uint32_t zip_metadata_len); +void phar_parse_metadata_lazy(const char *buffer, phar_metadata_tracker *tracker, uint32_t zip_metadata_len, int persistent); +zend_bool phar_metadata_tracker_has_data(const phar_metadata_tracker* tracker, int persistent); +/* If this has data, free it and set all values to undefined. */ +void phar_metadata_tracker_free(phar_metadata_tracker* val, int persistent); +void phar_metadata_tracker_copy(phar_metadata_tracker* dest, const phar_metadata_tracker *source, int persistent); +void phar_metadata_tracker_clone(phar_metadata_tracker* tracker); +void phar_metadata_tracker_try_ensure_has_serialized_data(phar_metadata_tracker* tracker, int persistent); +int phar_metadata_tracker_unserialize_or_copy(phar_metadata_tracker* tracker, zval *value, int persistent, HashTable *unserialize_options, const char* method_name); +void phar_release_entry_metadata(phar_entry_info *entry); +void phar_release_archive_metadata(phar_archive_data *phar); void destroy_phar_manifest_entry(zval *zv); int phar_seek_efp(phar_entry_info *entry, zend_off_t offset, int whence, zend_off_t position, int follow_links); php_stream *phar_get_efp(phar_entry_info *entry, int follow_links); diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c index 74f1ea4b737e8..61a6473aca066 100644 --- a/ext/phar/phar_object.c +++ b/ext/phar/phar_object.c @@ -2266,10 +2266,7 @@ static zend_object *phar_convert_to_other(phar_archive_data *source, int convert phar->is_temporary_alias = source->is_temporary_alias; phar->alias = source->alias; - if (Z_TYPE(source->metadata) != IS_UNDEF) { - ZVAL_DUP(&phar->metadata, &source->metadata); - phar->metadata_len = 0; - } + phar_metadata_tracker_copy(&phar->metadata_tracker, &source->metadata_tracker, phar->is_persistent); /* first copy each file's uncompressed contents to a temporary file and set per-file flags */ ZEND_HASH_FOREACH_PTR(&source->manifest, entry) { @@ -2286,8 +2283,6 @@ static zend_object *phar_convert_to_other(phar_archive_data *source, int convert goto no_copy; } - newentry.metadata_str.s = NULL; - if (FAILURE == phar_copy_file_contents(&newentry, phar->fp)) { zend_hash_destroy(&(phar->manifest)); php_stream_close(phar->fp); @@ -2298,10 +2293,7 @@ static zend_object *phar_convert_to_other(phar_archive_data *source, int convert no_copy: newentry.filename = estrndup(newentry.filename, newentry.filename_len); - if (Z_TYPE(newentry.metadata) != IS_UNDEF) { - zval_copy_ctor(&newentry.metadata); - newentry.metadata_str.s = NULL; - } + phar_metadata_tracker_clone(&newentry.metadata_tracker); newentry.is_zip = phar->is_zip; newentry.is_tar = phar->is_tar; @@ -3444,10 +3436,7 @@ PHP_METHOD(Phar, copy) memcpy((void *) &newentry, oldentry, sizeof(phar_entry_info)); - if (Z_TYPE(newentry.metadata) != IS_UNDEF) { - zval_copy_ctor(&newentry.metadata); - newentry.metadata_str.s = NULL; - } + phar_metadata_tracker_clone(&newentry.metadata_tracker); newentry.filename = estrndup(newfile, newfile_len); newentry.filename_len = newfile_len; @@ -3951,32 +3940,60 @@ PHP_METHOD(Phar, hasMetadata) RETURN_THROWS(); } - RETURN_BOOL(Z_TYPE(phar_obj->archive->metadata) != IS_UNDEF); + RETURN_BOOL(phar_metadata_tracker_has_data(&phar_obj->archive->metadata_tracker, phar_obj->archive->is_persistent)); } /* }}} */ /* {{{ Returns the global metadata of the phar */ PHP_METHOD(Phar, getMetadata) { + HashTable *unserialize_options = NULL; + phar_metadata_tracker *tracker; PHAR_ARCHIVE_OBJECT(); - if (zend_parse_parameters_none() == FAILURE) { - RETURN_THROWS(); - } + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_HT(unserialize_options) + ZEND_PARSE_PARAMETERS_END(); - if (Z_TYPE(phar_obj->archive->metadata) != IS_UNDEF) { - if (phar_obj->archive->is_persistent) { - char *buf = estrndup((char *) Z_PTR(phar_obj->archive->metadata), phar_obj->archive->metadata_len); - /* assume success, we would have failed before */ - phar_parse_metadata(&buf, return_value, phar_obj->archive->metadata_len); - efree(buf); - } else { - ZVAL_COPY(return_value, &phar_obj->archive->metadata); - } + tracker = &phar_obj->archive->metadata_tracker; + if (phar_metadata_tracker_has_data(tracker, phar_obj->archive->is_persistent)) { + phar_metadata_tracker_unserialize_or_copy(tracker, return_value, phar_obj->archive->is_persistent, unserialize_options, "Phar::getMetadata"); } } /* }}} */ +/* {{{ Modifies the phar metadata or throws */ +static int serialize_metadata_or_throw(phar_metadata_tracker *tracker, int persistent, zval *metadata) +{ + php_serialize_data_t metadata_hash; + smart_str main_metadata_str = {0}; + + PHP_VAR_SERIALIZE_INIT(metadata_hash); + php_var_serialize(&main_metadata_str, metadata, &metadata_hash); + PHP_VAR_SERIALIZE_DESTROY(metadata_hash); + if (EG(exception)) { + /* Serialization can throw. Don't overwrite the original value or original string. */ + return FAILURE; + } + + phar_metadata_tracker_free(tracker, persistent); + if (EG(exception)) { + /* Destructor can throw. */ + zend_string_release(main_metadata_str.s); + return FAILURE; + } + + if (tracker->str) { + zend_throw_exception_ex(phar_ce_PharException, 0, "Metadata unexpectedly changed during setMetadata()"); + zend_string_release(main_metadata_str.s); + return FAILURE; + } + ZVAL_COPY(&tracker->val, metadata); + tracker->str = main_metadata_str.s; + return SUCCESS; +} + /* {{{ Sets the global metadata of the phar */ PHP_METHOD(Phar, setMetadata) { @@ -3998,12 +4015,12 @@ PHP_METHOD(Phar, setMetadata) zend_throw_exception_ex(phar_ce_PharException, 0, "phar \"%s\" is persistent, unable to copy on write", phar_obj->archive->fname); RETURN_THROWS(); } - if (Z_TYPE(phar_obj->archive->metadata) != IS_UNDEF) { - zval_ptr_dtor(&phar_obj->archive->metadata); - ZVAL_UNDEF(&phar_obj->archive->metadata); + + ZEND_ASSERT(!phar_obj->archive->is_persistent); /* Should no longer be persistent */ + if (serialize_metadata_or_throw(&phar_obj->archive->metadata_tracker, phar_obj->archive->is_persistent, metadata) != SUCCESS) { + RETURN_THROWS(); } - ZVAL_COPY(&phar_obj->archive->metadata, metadata); phar_obj->archive->is_modified = 1; phar_flush(phar_obj->archive, 0, 0, 0, &error); @@ -4030,20 +4047,18 @@ PHP_METHOD(Phar, delMetadata) RETURN_THROWS(); } - if (Z_TYPE(phar_obj->archive->metadata) != IS_UNDEF) { - zval_ptr_dtor(&phar_obj->archive->metadata); - ZVAL_UNDEF(&phar_obj->archive->metadata); - phar_obj->archive->is_modified = 1; - phar_flush(phar_obj->archive, 0, 0, 0, &error); + if (!phar_metadata_tracker_has_data(&phar_obj->archive->metadata_tracker, phar_obj->archive->is_persistent)) { + RETURN_TRUE; + } - if (error) { - zend_throw_exception_ex(phar_ce_PharException, 0, "%s", error); - efree(error); - RETURN_THROWS(); - } else { - RETURN_TRUE; - } + phar_metadata_tracker_free(&phar_obj->archive->metadata_tracker, phar_obj->archive->is_persistent); + phar_obj->archive->is_modified = 1; + phar_flush(phar_obj->archive, 0, 0, 0, &error); + if (error) { + zend_throw_exception_ex(phar_ce_PharException, 0, "%s", error); + efree(error); + RETURN_THROWS(); } else { RETURN_TRUE; } @@ -4630,28 +4645,25 @@ PHP_METHOD(PharFileInfo, hasMetadata) RETURN_THROWS(); } - RETURN_BOOL(Z_TYPE(entry_obj->entry->metadata) != IS_UNDEF); + RETURN_BOOL(phar_metadata_tracker_has_data(&entry_obj->entry->metadata_tracker, entry_obj->entry->is_persistent)); } /* }}} */ /* {{{ Returns the metadata of the entry */ PHP_METHOD(PharFileInfo, getMetadata) { + HashTable *unserialize_options = NULL; + phar_metadata_tracker *tracker; PHAR_ENTRY_OBJECT(); - if (zend_parse_parameters_none() == FAILURE) { - RETURN_THROWS(); - } + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_HT(unserialize_options) + ZEND_PARSE_PARAMETERS_END(); - if (Z_TYPE(entry_obj->entry->metadata) != IS_UNDEF) { - if (entry_obj->entry->is_persistent) { - char *buf = estrndup((char *) Z_PTR(entry_obj->entry->metadata), entry_obj->entry->metadata_len); - /* assume success, we would have failed before */ - phar_parse_metadata(&buf, return_value, entry_obj->entry->metadata_len); - efree(buf); - } else { - ZVAL_COPY(return_value, &entry_obj->entry->metadata); - } + tracker = &entry_obj->entry->metadata_tracker; + if (phar_metadata_tracker_has_data(tracker, entry_obj->entry->is_persistent)) { + phar_metadata_tracker_unserialize_or_copy(tracker, return_value, entry_obj->entry->is_persistent, unserialize_options, "PharFileInfo::getMetadata"); } } /* }}} */ @@ -4688,13 +4700,12 @@ PHP_METHOD(PharFileInfo, setMetadata) } /* re-populate after copy-on-write */ entry_obj->entry = zend_hash_str_find_ptr(&phar->manifest, entry_obj->entry->filename, entry_obj->entry->filename_len); - } - if (Z_TYPE(entry_obj->entry->metadata) != IS_UNDEF) { - zval_ptr_dtor(&entry_obj->entry->metadata); - ZVAL_UNDEF(&entry_obj->entry->metadata); + ZEND_ASSERT(!entry_obj->entry->is_persistent); /* Should no longer be persistent */ } - ZVAL_COPY(&entry_obj->entry->metadata, metadata); + if (serialize_metadata_or_throw(&entry_obj->entry->metadata_tracker, entry_obj->entry->is_persistent, metadata) != SUCCESS) { + RETURN_THROWS(); + } entry_obj->entry->is_modified = 1; entry_obj->entry->phar->is_modified = 1; @@ -4729,7 +4740,7 @@ PHP_METHOD(PharFileInfo, delMetadata) RETURN_THROWS(); } - if (Z_TYPE(entry_obj->entry->metadata) != IS_UNDEF) { + if (phar_metadata_tracker_has_data(&entry_obj->entry->metadata_tracker, entry_obj->entry->is_persistent)) { if (entry_obj->entry->is_persistent) { phar_archive_data *phar = entry_obj->entry->phar; @@ -4740,8 +4751,8 @@ PHP_METHOD(PharFileInfo, delMetadata) /* re-populate after copy-on-write */ entry_obj->entry = zend_hash_str_find_ptr(&phar->manifest, entry_obj->entry->filename, entry_obj->entry->filename_len); } - zval_ptr_dtor(&entry_obj->entry->metadata); - ZVAL_UNDEF(&entry_obj->entry->metadata); + /* multiple values may reference the metadata */ + phar_metadata_tracker_free(&entry_obj->entry->metadata_tracker, entry_obj->entry->is_persistent); entry_obj->entry->is_modified = 1; entry_obj->entry->phar->is_modified = 1; diff --git a/ext/phar/phar_object.stub.php b/ext/phar/phar_object.stub.php index f3439d851a235..fe5fed140602a 100644 --- a/ext/phar/phar_object.stub.php +++ b/ext/phar/phar_object.stub.php @@ -67,7 +67,7 @@ public function getAlias() {} public function getPath() {} /** @return mixed */ - public function getMetadata() {} + public function getMetadata(array $unserialize_options = []) {} /** @return bool */ public function getModified() {} @@ -303,7 +303,7 @@ public function getPath() {} * @return mixed * @alias Phar::getMetadata */ - public function getMetadata() {} + public function getMetadata(array $unserialize_options = []) {} /** * @return bool @@ -510,7 +510,7 @@ public function getCRC32() {} public function getContent() {} /** @return mixed */ - public function getMetadata() {} + public function getMetadata(array $unserialize_options = []) {} /** @return int */ public function getPharFlags() {} diff --git a/ext/phar/phar_object_arginfo.h b/ext/phar/phar_object_arginfo.h index 8c6294d324639..a4b446d7c5c35 100644 --- a/ext/phar/phar_object_arginfo.h +++ b/ext/phar/phar_object_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: fd4f05b74248e4f7efb234cac8e3a90e17037ee0 */ + * Stub hash: 586c79f097e9153b70f6c6e208daa08acc0ce1d4 */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Phar___construct, 0, 0, 1) ZEND_ARG_TYPE_INFO(0, filename, IS_STRING, 0) @@ -82,7 +82,9 @@ ZEND_END_ARG_INFO() #define arginfo_class_Phar_getPath arginfo_class_Phar___destruct -#define arginfo_class_Phar_getMetadata arginfo_class_Phar___destruct +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Phar_getMetadata, 0, 0, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, unserialize_options, IS_ARRAY, 0, "[]") +ZEND_END_ARG_INFO() #define arginfo_class_Phar_getModified arginfo_class_Phar___destruct @@ -252,7 +254,7 @@ ZEND_END_ARG_INFO() #define arginfo_class_PharData_getPath arginfo_class_Phar___destruct -#define arginfo_class_PharData_getMetadata arginfo_class_Phar___destruct +#define arginfo_class_PharData_getMetadata arginfo_class_Phar_getMetadata #define arginfo_class_PharData_getModified arginfo_class_Phar___destruct @@ -346,7 +348,7 @@ ZEND_END_ARG_INFO() #define arginfo_class_PharFileInfo_getContent arginfo_class_Phar___destruct -#define arginfo_class_PharFileInfo_getMetadata arginfo_class_Phar___destruct +#define arginfo_class_PharFileInfo_getMetadata arginfo_class_Phar_getMetadata #define arginfo_class_PharFileInfo_getPharFlags arginfo_class_Phar___destruct diff --git a/ext/phar/stream.c b/ext/phar/stream.c index bdc15683de783..91ed30cf03eca 100644 --- a/ext/phar/stream.c +++ b/ext/phar/stream.c @@ -223,13 +223,10 @@ static php_stream * phar_wrapper_open_url(php_stream_wrapper *wrapper, const cha idata->internal_file->flags |= Z_LVAL_P(pzoption); } if ((pzoption = zend_hash_str_find(pharcontext, "metadata", sizeof("metadata")-1)) != NULL) { - if (Z_TYPE(idata->internal_file->metadata) != IS_UNDEF) { - zval_ptr_dtor(&idata->internal_file->metadata); - ZVAL_UNDEF(&idata->internal_file->metadata); - } + phar_metadata_tracker_free(&idata->internal_file->metadata_tracker, idata->internal_file->is_persistent); metadata = pzoption; - ZVAL_COPY_DEREF(&idata->internal_file->metadata, metadata); + ZVAL_COPY_DEREF(&idata->internal_file->metadata_tracker.val, metadata); idata->phar->is_modified = 1; } } @@ -846,7 +843,7 @@ static int phar_wrapper_rename(php_stream_wrapper *wrapper, const char *url_from /* mark the old one for deletion */ entry->is_deleted = 1; entry->fp = NULL; - ZVAL_UNDEF(&entry->metadata); + ZVAL_UNDEF(&entry->metadata_tracker.val); entry->link = entry->tmp = NULL; source = entry; diff --git a/ext/phar/tar.c b/ext/phar/tar.c index 89b91b25f9d75..fb9cacfc73c4b 100644 --- a/ext/phar/tar.c +++ b/ext/phar/tar.c @@ -173,28 +173,25 @@ static int phar_tar_process_metadata(phar_entry_info *entry, php_stream *fp) /* return FAILURE; } - if (phar_parse_metadata(&metadata, &entry->metadata, entry->uncompressed_filesize) == FAILURE) { - /* if not valid serialized data, it is a regular string */ - efree(metadata); - php_stream_seek(fp, save, SEEK_SET); - return FAILURE; - } + phar_parse_metadata_lazy(metadata, &entry->metadata_tracker, entry->uncompressed_filesize, entry->is_persistent); if (entry->filename_len == sizeof(".phar/.metadata.bin")-1 && !memcmp(entry->filename, ".phar/.metadata.bin", sizeof(".phar/.metadata.bin")-1)) { - if (Z_TYPE(entry->phar->metadata) != IS_UNDEF) { + if (phar_metadata_tracker_has_data(&entry->phar->metadata_tracker, entry->phar->is_persistent)) { efree(metadata); return FAILURE; } - entry->phar->metadata = entry->metadata; - ZVAL_UNDEF(&entry->metadata); + entry->phar->metadata_tracker = entry->metadata_tracker; + entry->metadata_tracker.str = NULL; + ZVAL_UNDEF(&entry->metadata_tracker.val); } else if (entry->filename_len >= sizeof(".phar/.metadata/") + sizeof("/.metadata.bin") - 1 && NULL != (mentry = zend_hash_str_find_ptr(&(entry->phar->manifest), entry->filename + sizeof(".phar/.metadata/") - 1, entry->filename_len - (sizeof("/.metadata.bin") - 1 + sizeof(".phar/.metadata/") - 1)))) { - if (Z_TYPE(mentry->metadata) != IS_UNDEF) { + if (phar_metadata_tracker_has_data(&mentry->metadata_tracker, mentry->is_persistent)) { efree(metadata); return FAILURE; } /* transfer this metadata to the entry it refers */ - mentry->metadata = entry->metadata; - ZVAL_UNDEF(&entry->metadata); + mentry->metadata_tracker = entry->metadata_tracker; + entry->metadata_tracker.str = NULL; + ZVAL_UNDEF(&entry->metadata_tracker.val); } efree(metadata); @@ -864,19 +861,16 @@ static int phar_tar_writeheaders(zval *zv, void *argument) /* {{{ */ } /* }}} */ -int phar_tar_setmetadata(zval *metadata, phar_entry_info *entry, char **error) /* {{{ */ +int phar_tar_setmetadata(const phar_metadata_tracker *tracker, phar_entry_info *entry, char **error) /* {{{ */ { - php_serialize_data_t metadata_hash; - - if (entry->metadata_str.s) { - smart_str_free(&entry->metadata_str); - } + /* Copy the metadata from tracker to the new entry being written out to temporary files */ + const zend_string *serialized_str; + phar_metadata_tracker_copy(&entry->metadata_tracker, tracker, entry->is_persistent); + phar_metadata_tracker_try_ensure_has_serialized_data(&entry->metadata_tracker, entry->is_persistent); + serialized_str = entry->metadata_tracker.str; - entry->metadata_str.s = NULL; - PHP_VAR_SERIALIZE_INIT(metadata_hash); - php_var_serialize(&entry->metadata_str, metadata, &metadata_hash); - PHP_VAR_SERIALIZE_DESTROY(metadata_hash); - entry->uncompressed_filesize = entry->compressed_filesize = entry->metadata_str.s ? ZSTR_LEN(entry->metadata_str.s) : 0; + /* If there is no data, this will replace the metadata file (e.g. .phar/.metadata.bin) with an empty file */ + entry->uncompressed_filesize = entry->compressed_filesize = serialized_str ? ZSTR_LEN(serialized_str) : 0; if (entry->fp && entry->fp_type == PHAR_MOD) { php_stream_close(entry->fp); @@ -890,7 +884,7 @@ int phar_tar_setmetadata(zval *metadata, phar_entry_info *entry, char **error) / spprintf(error, 0, "phar error: unable to create temporary file"); return -1; } - if (ZSTR_LEN(entry->metadata_str.s) != php_stream_write(entry->fp, ZSTR_VAL(entry->metadata_str.s), ZSTR_LEN(entry->metadata_str.s))) { + if (serialized_str && ZSTR_LEN(serialized_str) != php_stream_write(entry->fp, ZSTR_VAL(serialized_str), ZSTR_LEN(serialized_str))) { spprintf(error, 0, "phar tar error: unable to write metadata to magic metadata file \"%s\"", entry->filename); zend_hash_str_del(&(entry->phar->manifest), entry->filename, entry->filename_len); return ZEND_HASH_APPLY_STOP; @@ -909,7 +903,7 @@ static int phar_tar_setupmetadata(zval *zv, void *argument) /* {{{ */ if (entry->filename_len >= sizeof(".phar/.metadata") && !memcmp(entry->filename, ".phar/.metadata", sizeof(".phar/.metadata")-1)) { if (entry->filename_len == sizeof(".phar/.metadata.bin")-1 && !memcmp(entry->filename, ".phar/.metadata.bin", sizeof(".phar/.metadata.bin")-1)) { - return phar_tar_setmetadata(&entry->phar->metadata, entry, error); + return phar_tar_setmetadata(&entry->phar->metadata_tracker, entry, error); } /* search for the file this metadata entry references */ if (entry->filename_len >= sizeof(".phar/.metadata/") + sizeof("/.metadata.bin") - 1 && !zend_hash_str_exists(&(entry->phar->manifest), entry->filename + sizeof(".phar/.metadata/") - 1, entry->filename_len - (sizeof("/.metadata.bin") - 1 + sizeof(".phar/.metadata/") - 1))) { @@ -927,7 +921,7 @@ static int phar_tar_setupmetadata(zval *zv, void *argument) /* {{{ */ /* now we are dealing with regular files, so look for metadata */ lookfor_len = spprintf(&lookfor, 0, ".phar/.metadata/%s/.metadata.bin", entry->filename); - if (Z_TYPE(entry->metadata) == IS_UNDEF) { + if (!phar_metadata_tracker_has_data(&entry->metadata_tracker, entry->is_persistent)) { zend_hash_str_del(&(entry->phar->manifest), lookfor, lookfor_len); efree(lookfor); return ZEND_HASH_APPLY_KEEP; @@ -935,7 +929,7 @@ static int phar_tar_setupmetadata(zval *zv, void *argument) /* {{{ */ if (NULL != (metadata = zend_hash_str_find_ptr(&(entry->phar->manifest), lookfor, lookfor_len))) { int ret; - ret = phar_tar_setmetadata(&entry->metadata, metadata, error); + ret = phar_tar_setmetadata(&entry->metadata_tracker, metadata, error); efree(lookfor); return ret; } @@ -952,7 +946,7 @@ static int phar_tar_setupmetadata(zval *zv, void *argument) /* {{{ */ return ZEND_HASH_APPLY_STOP; } - return phar_tar_setmetadata(&entry->metadata, metadata, error); + return phar_tar_setmetadata(&entry->metadata_tracker, metadata, error); } /* }}} */ @@ -1165,10 +1159,10 @@ int phar_tar_flush(phar_archive_data *phar, char *user_stub, zend_long len, int pass.free_fp = 1; pass.free_ufp = 1; - if (Z_TYPE(phar->metadata) != IS_UNDEF) { + if (phar_metadata_tracker_has_data(&phar->metadata_tracker, phar->is_persistent)) { phar_entry_info *mentry; if (NULL != (mentry = zend_hash_str_find_ptr(&(phar->manifest), ".phar/.metadata.bin", sizeof(".phar/.metadata.bin")-1))) { - if (ZEND_HASH_APPLY_KEEP != phar_tar_setmetadata(&phar->metadata, mentry, error)) { + if (ZEND_HASH_APPLY_KEEP != phar_tar_setmetadata(&phar->metadata_tracker, mentry, error)) { if (closeoldfile) { php_stream_close(oldfile); } @@ -1191,7 +1185,7 @@ int phar_tar_flush(phar_archive_data *phar, char *user_stub, zend_long len, int return EOF; } - if (ZEND_HASH_APPLY_KEEP != phar_tar_setmetadata(&phar->metadata, mentry, error)) { + if (ZEND_HASH_APPLY_KEEP != phar_tar_setmetadata(&phar->metadata_tracker, mentry, error)) { zend_hash_str_del(&(phar->manifest), ".phar/.metadata.bin", sizeof(".phar/.metadata.bin")-1); if (closeoldfile) { php_stream_close(oldfile); diff --git a/ext/phar/tests/bug69720.phpt b/ext/phar/tests/bug69720.phpt index cdb4741ff834c..5c9b76f367c11 100644 --- a/ext/phar/tests/bug69720.phpt +++ b/ext/phar/tests/bug69720.phpt @@ -11,10 +11,10 @@ try { echo $p->getMetadata(); foreach (new RecursiveIteratorIterator($p) as $file) { // $file is a PharFileInfo class, and inherits from SplFileInfo - $temp=""; + $temp=""; $temp= $file->getFileName() . "\n"; $temp.=file_get_contents($file->getPathName()) . "\n"; // display contents - var_dump($file->getMetadata()); + var_dump($file->getMetadata()); } } catch (Exception $e) { @@ -29,7 +29,7 @@ array(1) { ["whatever"]=> int(123) } -object(DateTime)#2 (3) { +object(DateTime)#6 (3) { ["date"]=> string(26) "2000-01-01 00:00:00.000000" ["timezone_type"]=> diff --git a/ext/phar/tests/bug69958.phpt b/ext/phar/tests/bug69958.phpt index 6df1178befb2c..15f1edc369abc 100644 --- a/ext/phar/tests/bug69958.phpt +++ b/ext/phar/tests/bug69958.phpt @@ -9,7 +9,7 @@ Still has memory leaks, see https://bugs.php.net/bug.php?id=70005 $tarphar = new PharData(__DIR__.'/bug69958.tar'); $phar = $tarphar->convertToData(Phar::TAR); --EXPECTF-- -Fatal error: Uncaught exception 'BadMethodCallException' with message 'phar "%s/bug69958.tar" exists and must be unlinked prior to conversion' in %s/bug69958.php:%d +Fatal error: Uncaught BadMethodCallException: phar "%s/bug69958.tar" exists and must be unlinked prior to conversion in %s/bug69958.php:%d Stack trace: #0 %s/bug69958.php(%d): PharData->convertToData(%d) #1 {main} diff --git a/ext/phar/tests/phar_metadata_write2.phpt b/ext/phar/tests/phar_metadata_write2.phpt new file mode 100644 index 0000000000000..52e63a290c4eb --- /dev/null +++ b/ext/phar/tests/phar_metadata_write2.phpt @@ -0,0 +1,47 @@ +--TEST-- +Phar with object in metadata +--SKIPIF-- + +--INI-- +phar.require_hash=0 +phar.readonly=0 +--FILE-- +"; + +$files = array(); +$files['a'] = array('cont' => 'a'); +include 'files/phar_test.inc'; + +foreach($files as $name => $cont) { + var_dump(file_get_contents($pname.'/'.$name)); +} + +$phar = new Phar($fname); +var_dump($phar->getMetadata()); +$phar->setMetadata((object) ['my' => 'friend']); +unset($phar); +// NOTE: Phar will use the cached value of metadata if setMetaData was called on that Phar path before. +// Save the writes to the phar and use a different file path. +$fname_new = "$fname.copy.php"; +copy($fname, $fname_new); +$phar = new Phar($fname_new); +var_dump($phar->getMetadata()); + +?> +--CLEAN-- + +--EXPECT-- +string(1) "a" +NULL +object(stdClass)#2 (1) { + ["my"]=> + string(6) "friend" +} diff --git a/ext/phar/tests/phar_metadata_write3.phpt b/ext/phar/tests/phar_metadata_write3.phpt new file mode 100644 index 0000000000000..d42f95c161251 --- /dev/null +++ b/ext/phar/tests/phar_metadata_write3.phpt @@ -0,0 +1,106 @@ +--TEST-- +Phar with unsafe object in metadata does not unserialize on reading a file. +--SKIPIF-- + +--INI-- +phar.require_hash=0 +phar.readonly=0 +--FILE-- +"; + +$files = array(); +$files['a'] = array('cont' => 'contents of file a'); +include 'files/phar_test.inc'; + +echo "Reading file contents through stream wrapper\n"; +foreach($files as $name => $cont) { + var_dump(file_get_contents($pname.'/'.$name)); +} + +$phar = new Phar($fname); +echo "Original metadata\n"; +var_dump($phar->getMetadata()); +$phar->setMetadata(new EchoesOnWakeup()); +unset($phar); +// NOTE: Phar will use the cached value of metadata if setMetaData was called on that Phar path before. +// Save the writes to the phar and use a different file path. +$fname_new = "$fname.copy.php"; +copy($fname, $fname_new); +$phar = new Phar($fname_new); +echo "Calling getMetadata\n"; +var_dump($phar->getMetadata()); +echo "Calling getMetadata with no allowed_classes\n"; +var_dump($phar->getMetadata(['allowed_classes' => []])); +echo "Calling getMetadata with EchoesOnWakeup allowed\n"; +var_dump($phar->getMetadata(['allowed_classes' => [EchoesOnWakeup::class]])); +// Part of this is a test that there are no unexpected behaviors when both selMetadata and getMetadata are used +$phar->setMetaData([new EchoesOnWakeup(), new stdClass()]); +echo "Calling getMetadata with too low max_depth\n"; +var_dump($phar->getMetadata(['max_depth' => 1])); +echo "Calling getMetadata with some allowed classes\n"; +var_dump($phar->getMetadata(['allowed_classes' => [EchoesOnWakeup::class]])); +echo "Calling getMetadata with no options returns the original metadata value\n"; +var_dump($phar->getMetadata()); +unset($phar); + +?> +--CLEAN-- + +--EXPECTF-- +Reading file contents through stream wrapper +string(18) "contents of file a" +Original metadata +NULL +Calling getMetadata +In wakeup +object(EchoesOnWakeup)#2 (0) { +} +Calling getMetadata with no allowed_classes +object(__PHP_Incomplete_Class)#2 (1) { + ["__PHP_Incomplete_Class_Name"]=> + string(14) "EchoesOnWakeup" +} +Calling getMetadata with EchoesOnWakeup allowed +In wakeup +object(EchoesOnWakeup)#2 (0) { +} +Calling getMetadata with too low max_depth + +Warning: Phar::getMetadata(): Maximum depth of 1 exceeded. The depth limit can be changed using the max_depth unserialize() option or the unserialize_max_depth ini setting in %sphar_metadata_write3.php on line 39 + +Notice: Phar::getMetadata(): Error at offset 34 of 59 bytes in %sphar_metadata_write3.php on line 39 +bool(false) +Calling getMetadata with some allowed classes +In wakeup +array(2) { + [0]=> + object(EchoesOnWakeup)#4 (0) { + } + [1]=> + object(__PHP_Incomplete_Class)#5 (1) { + ["__PHP_Incomplete_Class_Name"]=> + string(8) "stdClass" + } +} +Calling getMetadata with no options returns the original metadata value +array(2) { + [0]=> + object(EchoesOnWakeup)#2 (0) { + } + [1]=> + object(stdClass)#3 (0) { + } +} diff --git a/ext/phar/tests/phar_metadata_write4.phpt b/ext/phar/tests/phar_metadata_write4.phpt new file mode 100644 index 0000000000000..dfe46bab102cb --- /dev/null +++ b/ext/phar/tests/phar_metadata_write4.phpt @@ -0,0 +1,103 @@ +--TEST-- +Phar with object in metadata +--SKIPIF-- + +--INI-- +phar.require_hash=0 +phar.readonly=0 +--FILE-- +"; + +$files = array(); +$files['a'] = array('cont' => 'a', 'meta' => new EchoesOnWakeup()); +include 'files/phar_test.inc'; + +foreach($files as $name => $cont) { + var_dump(file_get_contents($pname.'/'.$name)); +} +unset($files); + +$phar = new Phar($fname); +echo "Loading metadata for 'a' without allowed_classes\n"; +var_dump($phar['a']->getMetadata(['allowed_classes' => []])); +echo "Loading metadata for 'a' with allowed_classes\n"; +var_dump($phar['a']->getMetadata(['allowed_classes' => true])); +unset($phar); +// NOTE: Phar will use the cached value of metadata if setMetaData was called on that Phar path before. +// Save the writes to the phar and use a different file path. +$fname_new = "$fname.copy.php"; +copy($fname, $fname_new); +$phar = new Phar($fname_new); +echo "Loading metadata from 'a' from the new phar\n"; +var_dump($phar['a']->getMetadata()); +echo "Loading metadata from 'a' from the new phar with unserialize options\n"; +var_dump($phar['a']->getMetadata(['allowed_classes' => true])); +// PharEntry->setMetaData will do the following: +// 1. serialize, checking for exceptions +// 2. free the original data, checking for exceptions or the data getting set from destructors or error handlers. +// 3. set the new data. +try { + var_dump($phar['a']->setMetadata(new ThrowsOnSerialize())); +} catch (RuntimeException $e) { + echo "Caught {$e->getMessage()} at {$e->getFile()}:{$e->getLine()}\n"; + unset($e); +} +var_dump($phar['a']->getMetadata([])); +var_dump($phar['a']->getMetadata(['allowed_classes' => false])); + +?> +--CLEAN-- + +--EXPECTF-- +In __destruct 1 +string(1) "a" +Loading metadata for 'a' without allowed_classes +object(__PHP_Incomplete_Class)#3 (1) { + ["__PHP_Incomplete_Class_Name"]=> + string(14) "EchoesOnWakeup" +} +Loading metadata for 'a' with allowed_classes +In __wakeup 2 +object(EchoesOnWakeup)#2 (0) { +} +In __destruct 2 +Loading metadata from 'a' from the new phar +In __wakeup 3 +object(EchoesOnWakeup)#3 (0) { +} +In __destruct 3 +Loading metadata from 'a' from the new phar with unserialize options +In __wakeup 2 +object(EchoesOnWakeup)#2 (0) { +} +In __destruct 2 +Caught In sleep at %sphar_metadata_write4.php:12 +In __wakeup 3 +object(EchoesOnWakeup)#3 (0) { +} +In __destruct 3 +object(__PHP_Incomplete_Class)#4 (1) { + ["__PHP_Incomplete_Class_Name"]=> + string(14) "EchoesOnWakeup" +} \ No newline at end of file diff --git a/ext/phar/tests/tar/all.phpt b/ext/phar/tests/tar/all.phpt index 66e9c1e951159..87d55eaaf62ef 100644 --- a/ext/phar/tests/tar/all.phpt +++ b/ext/phar/tests/tar/all.phpt @@ -3,8 +3,6 @@ Phar: test that creation of tar-based phar generates valid tar with all bells/wh --SKIPIF-- --INI-- phar.readonly=0 diff --git a/ext/phar/tests/tar/bug70417.phpt b/ext/phar/tests/tar/bug70417.phpt index 0202ca9472afe..f288efc8f57dc 100644 --- a/ext/phar/tests/tar/bug70417.phpt +++ b/ext/phar/tests/tar/bug70417.phpt @@ -13,7 +13,7 @@ if ($status !== 0) { --FILE-- /dev/null', $out); + exec('lsof -p ' . escapeshellarg(getmypid()) . ' 2> /dev/null', $out); // Note: valgrind can produce false positives for /usr/bin/lsof return count($out); } $filename = __DIR__ . '/bug70417.tar'; diff --git a/ext/phar/util.c b/ext/phar/util.c index 35322734d0992..6c084d84584da 100644 --- a/ext/phar/util.c +++ b/ext/phar/util.c @@ -1968,21 +1968,11 @@ static int phar_update_cached_entry(zval *data, void *argument) /* {{{ */ entry->tmp = estrdup(entry->tmp); } - entry->metadata_str.s = NULL; entry->filename = estrndup(entry->filename, entry->filename_len); entry->is_persistent = 0; - if (Z_TYPE(entry->metadata) != IS_UNDEF) { - if (entry->metadata_len) { - char *buf = estrndup((char *) Z_PTR(entry->metadata), entry->metadata_len); - /* assume success, we would have failed before */ - phar_parse_metadata((char **) &buf, &entry->metadata, entry->metadata_len); - efree(buf); - } else { - zval_copy_ctor(&entry->metadata); - entry->metadata_str.s = NULL; - } - } + /* Replace metadata with non-persistent clones of the metadata. */ + phar_metadata_tracker_clone(&entry->metadata_tracker); return ZEND_HASH_APPLY_KEEP; } /* }}} */ @@ -2017,16 +2007,7 @@ static void phar_copy_cached_phar(phar_archive_data **pphar) /* {{{ */ phar->signature = estrdup(phar->signature); } - if (Z_TYPE(phar->metadata) != IS_UNDEF) { - /* assume success, we would have failed before */ - if (phar->metadata_len) { - char *buf = estrndup((char *) Z_PTR(phar->metadata), phar->metadata_len); - phar_parse_metadata(&buf, &phar->metadata, phar->metadata_len); - efree(buf); - } else { - zval_copy_ctor(&phar->metadata); - } - } + phar_metadata_tracker_clone(&phar->metadata_tracker); zend_hash_init(&newmanifest, sizeof(phar_entry_info), zend_get_hash_value, destroy_phar_manifest_entry, 0); diff --git a/ext/phar/zip.c b/ext/phar/zip.c index b241c0589b4eb..52a387bdbcb30 100644 --- a/ext/phar/zip.c +++ b/ext/phar/zip.c @@ -242,16 +242,9 @@ int phar_parse_zipfile(php_stream *fp, char *fname, size_t fname_len, char *alia return FAILURE; } - mydata->metadata_len = PHAR_GET_16(locator.comment_len); - - if (phar_parse_metadata(&metadata, &mydata->metadata, PHAR_GET_16(locator.comment_len)) == FAILURE) { - mydata->metadata_len = 0; - /* if not valid serialized data, it is a regular string */ - - ZVAL_NEW_STR(&mydata->metadata, zend_string_init(metadata, PHAR_GET_16(locator.comment_len), mydata->is_persistent)); - } + phar_parse_metadata_lazy(metadata, &mydata->metadata_tracker, PHAR_GET_16(locator.comment_len), mydata->is_persistent); } else { - ZVAL_UNDEF(&mydata->metadata); + ZVAL_UNDEF(&mydata->metadata_tracker.val); } goto foundit; @@ -306,7 +299,7 @@ int phar_parse_zipfile(php_stream *fp, char *fname, size_t fname_len, char *alia zend_hash_destroy(&mydata->virtual_dirs); \ HT_INVALIDATE(&mydata->virtual_dirs); \ php_stream_close(fp); \ - zval_ptr_dtor(&mydata->metadata); \ + phar_metadata_tracker_free(&mydata->metadata_tracker, mydata->is_persistent); \ if (mydata->signature) { \ efree(mydata->signature); \ } \ @@ -328,7 +321,7 @@ int phar_parse_zipfile(php_stream *fp, char *fname, size_t fname_len, char *alia zend_hash_destroy(&mydata->virtual_dirs); \ HT_INVALIDATE(&mydata->virtual_dirs); \ php_stream_close(fp); \ - zval_ptr_dtor(&mydata->metadata); \ + phar_metadata_tracker_free(&mydata->metadata_tracker, mydata->is_persistent); \ if (mydata->signature) { \ efree(mydata->signature); \ } \ @@ -347,6 +340,9 @@ int phar_parse_zipfile(php_stream *fp, char *fname, size_t fname_len, char *alia phar_zip_central_dir_file zipentry; zend_off_t beforeus = php_stream_tell(fp); + ZVAL_UNDEF(&entry.metadata_tracker.val); + entry.metadata_tracker.str = NULL; + if (sizeof(zipentry) != php_stream_read(fp, (char *) &zipentry, sizeof(zipentry))) { PHAR_ZIP_FAIL("unable to read central directory entry, truncated"); } @@ -533,16 +529,9 @@ int phar_parse_zipfile(php_stream *fp, char *fname, size_t fname_len, char *alia } p = buf; - entry.metadata_len = PHAR_GET_16(zipentry.comment_len); - - if (phar_parse_metadata(&p, &(entry.metadata), PHAR_GET_16(zipentry.comment_len)) == FAILURE) { - entry.metadata_len = 0; - /* if not valid serialized data, it is a regular string */ - - ZVAL_NEW_STR(&entry.metadata, zend_string_init(buf, PHAR_GET_16(zipentry.comment_len), entry.is_persistent)); - } + phar_parse_metadata_lazy(buf, &entry.metadata_tracker, PHAR_GET_16(zipentry.comment_len), entry.is_persistent); } else { - ZVAL_UNDEF(&entry.metadata); + ZVAL_UNDEF(&entry.metadata_tracker.val); } if (!actual_alias && entry.filename_len == sizeof(".phar/alias.txt")-1 && !strncmp(entry.filename, ".phar/alias.txt", sizeof(".phar/alias.txt")-1)) { @@ -969,17 +958,9 @@ static int phar_zip_changed_apply_int(phar_entry_info *entry, void *arg) /* {{{ PHAR_SET_32(local.crc32, entry->crc32); continue_dir: /* set file metadata */ - if (Z_TYPE(entry->metadata) != IS_UNDEF) { - php_serialize_data_t metadata_hash; - - if (entry->metadata_str.s) { - smart_str_free(&entry->metadata_str); - } - entry->metadata_str.s = NULL; - PHP_VAR_SERIALIZE_INIT(metadata_hash); - php_var_serialize(&entry->metadata_str, &entry->metadata, &metadata_hash); - PHP_VAR_SERIALIZE_DESTROY(metadata_hash); - PHAR_SET_16(central.comment_len, ZSTR_LEN(entry->metadata_str.s)); + if (phar_metadata_tracker_has_data(&entry->metadata_tracker, entry->is_persistent)) { + phar_metadata_tracker_try_ensure_has_serialized_data(&entry->metadata_tracker, entry->is_persistent); + PHAR_SET_16(central.comment_len, entry->metadata_tracker.str ? ZSTR_LEN(entry->metadata_tracker.str) : 0); } entry->header_offset = php_stream_tell(p->filefp); @@ -1089,14 +1070,11 @@ static int phar_zip_changed_apply_int(phar_entry_info *entry, void *arg) /* {{{ entry->offset = entry->offset_abs = offset; entry->fp_type = PHAR_FP; - if (entry->metadata_str.s) { - if (ZSTR_LEN(entry->metadata_str.s) != php_stream_write(p->centralfp, ZSTR_VAL(entry->metadata_str.s), ZSTR_LEN(entry->metadata_str.s))) { + if (entry->metadata_tracker.str) { + if (ZSTR_LEN(entry->metadata_tracker.str) != php_stream_write(p->centralfp, ZSTR_VAL(entry->metadata_tracker.str), ZSTR_LEN(entry->metadata_tracker.str))) { spprintf(p->error, 0, "unable to write metadata as file comment for file \"%s\" while creating zip-based phar \"%s\"", entry->filename, entry->phar->fname); - smart_str_free(&entry->metadata_str); return ZEND_HASH_APPLY_STOP; } - - smart_str_free(&entry->metadata_str); } return ZEND_HASH_APPLY_KEEP; @@ -1109,8 +1087,7 @@ static int phar_zip_changed_apply(zval *zv, void *arg) /* {{{ */ } /* }}} */ -static int phar_zip_applysignature(phar_archive_data *phar, struct _phar_zip_pass *pass, - smart_str *metadata) /* {{{ */ +static int phar_zip_applysignature(phar_archive_data *phar, struct _phar_zip_pass *pass) /* {{{ */ { /* add signature for executable tars or tars explicitly set with setSignatureAlgorithm */ if (!phar->is_data || phar->sig_flags) { @@ -1132,8 +1109,8 @@ static int phar_zip_applysignature(phar_archive_data *phar, struct _phar_zip_pas tell = php_stream_tell(pass->centralfp); php_stream_seek(pass->centralfp, 0, SEEK_SET); php_stream_copy_to_stream_ex(pass->centralfp, newfile, tell, NULL); - if (metadata->s) { - php_stream_write(newfile, ZSTR_VAL(metadata->s), ZSTR_LEN(metadata->s)); + if (phar->metadata_tracker.str) { + php_stream_write(newfile, ZSTR_VAL(phar->metadata_tracker.str), ZSTR_LEN(phar->metadata_tracker.str)); } if (FAILURE == phar_create_signature(phar, newfile, &signature, &signature_length, pass->error)) { @@ -1189,13 +1166,11 @@ static int phar_zip_applysignature(phar_archive_data *phar, struct _phar_zip_pas int phar_zip_flush(phar_archive_data *phar, char *user_stub, zend_long len, int defaultstub, char **error) /* {{{ */ { char *pos; - smart_str main_metadata_str = {0}; static const char newstub[] = "manifest, phar_zip_changed_apply, (void *) &pass); - if (Z_TYPE(phar->metadata) != IS_UNDEF) { - /* set phar metadata */ - PHP_VAR_SERIALIZE_INIT(metadata_hash); - php_var_serialize(&main_metadata_str, &phar->metadata, &metadata_hash); - PHP_VAR_SERIALIZE_DESTROY(metadata_hash); - } + phar_metadata_tracker_try_ensure_has_serialized_data(&phar->metadata_tracker, phar->is_persistent); if (temperr) { if (error) { spprintf(error, 4096, "phar zip flush of \"%s\" failed: %s", phar->fname, temperr); @@ -1436,9 +1406,6 @@ int phar_zip_flush(phar_archive_data *phar, char *user_stub, zend_long len, int temperror: php_stream_close(pass.centralfp); nocentralerror: - if (Z_TYPE(phar->metadata) != IS_UNDEF) { - smart_str_free(&main_metadata_str); - } php_stream_close(pass.filefp); if (closeoldfile) { php_stream_close(oldfile); @@ -1446,7 +1413,7 @@ int phar_zip_flush(phar_archive_data *phar, char *user_stub, zend_long len, int return EOF; } - if (FAILURE == phar_zip_applysignature(phar, &pass, &main_metadata_str)) { + if (FAILURE == phar_zip_applysignature(phar, &pass)) { goto temperror; } @@ -1470,9 +1437,10 @@ int phar_zip_flush(phar_archive_data *phar, char *user_stub, zend_long len, int php_stream_close(pass.centralfp); - if (Z_TYPE(phar->metadata) != IS_UNDEF) { + phar_metadata_tracker_try_ensure_has_serialized_data(&phar->metadata_tracker, phar->is_persistent); + if (phar->metadata_tracker.str) { /* set phar metadata */ - PHAR_SET_16(eocd.comment_len, ZSTR_LEN(main_metadata_str.s)); + PHAR_SET_16(eocd.comment_len, ZSTR_LEN(phar->metadata_tracker.str)); if (sizeof(eocd) != php_stream_write(pass.filefp, (char *)&eocd, sizeof(eocd))) { if (error) { @@ -1481,15 +1449,12 @@ int phar_zip_flush(phar_archive_data *phar, char *user_stub, zend_long len, int goto nocentralerror; } - if (ZSTR_LEN(main_metadata_str.s) != php_stream_write(pass.filefp, ZSTR_VAL(main_metadata_str.s), ZSTR_LEN(main_metadata_str.s))) { + if (ZSTR_LEN(phar->metadata_tracker.str) != php_stream_write(pass.filefp, ZSTR_VAL(phar->metadata_tracker.str), ZSTR_LEN(phar->metadata_tracker.str))) { if (error) { spprintf(error, 4096, "phar zip flush of \"%s\" failed: unable to write metadata to zip comment", phar->fname); } goto nocentralerror; } - - smart_str_free(&main_metadata_str); - } else { if (sizeof(eocd) != php_stream_write(pass.filefp, (char *)&eocd, sizeof(eocd))) { if (error) { diff --git a/ext/standard/php_var.h b/ext/standard/php_var.h index cab0f6d4ddb43..2d9f5464f53b9 100644 --- a/ext/standard/php_var.h +++ b/ext/standard/php_var.h @@ -59,6 +59,8 @@ PHPAPI zend_long php_var_unserialize_get_cur_depth(php_unserialize_data_t d); #define PHP_VAR_UNSERIALIZE_DESTROY(d) \ php_var_unserialize_destroy(d) +PHPAPI void php_unserialize_with_options(zval *return_value, const char *buf, const size_t buf_len, HashTable *options, const char* function_name); + PHPAPI void var_replace(php_unserialize_data_t *var_hash, zval *ozval, zval *nzval); PHPAPI void var_push_dtor(php_unserialize_data_t *var_hash, zval *val); PHPAPI zval *var_tmp_var(php_unserialize_data_t *var_hashx); diff --git a/ext/standard/var.c b/ext/standard/var.c index e835666ddb3bc..a9b85903a5c48 100644 --- a/ext/standard/var.c +++ b/ext/standard/var.c @@ -1171,24 +1171,15 @@ PHP_FUNCTION(serialize) } /* }}} */ -/* {{{ Takes a string representation of variable and recreates it */ -PHP_FUNCTION(unserialize) +/* {{{ Takes a string representation of variable and recreates it, subject to the optional unserialize options HashTable */ +PHPAPI void php_unserialize_with_options(zval *return_value, const char *buf, const size_t buf_len, HashTable *options, const char* function_name) { - char *buf = NULL; - size_t buf_len; const unsigned char *p; php_unserialize_data_t var_hash; - zval *options = NULL; zval *retval; HashTable *class_hash = NULL, *prev_class_hash; zend_long prev_max_depth, prev_cur_depth; - ZEND_PARSE_PARAMETERS_START(1, 2) - Z_PARAM_STRING(buf, buf_len) - Z_PARAM_OPTIONAL - Z_PARAM_ARRAY(options) - ZEND_PARSE_PARAMETERS_END(); - if (buf_len == 0) { RETURN_FALSE; } @@ -1202,7 +1193,7 @@ PHP_FUNCTION(unserialize) if (options != NULL) { zval *classes, *max_depth; - classes = zend_hash_str_find_deref(Z_ARRVAL_P(options), "allowed_classes", sizeof("allowed_classes")-1); + classes = zend_hash_str_find_deref(options, "allowed_classes", sizeof("allowed_classes")-1); if (classes && Z_TYPE_P(classes) != IS_ARRAY && Z_TYPE_P(classes) != IS_TRUE && Z_TYPE_P(classes) != IS_FALSE) { php_error_docref(NULL, E_WARNING, "allowed_classes option should be array or boolean"); RETVAL_FALSE; @@ -1231,14 +1222,14 @@ PHP_FUNCTION(unserialize) } php_var_unserialize_set_allowed_classes(var_hash, class_hash); - max_depth = zend_hash_str_find_deref(Z_ARRVAL_P(options), "max_depth", sizeof("max_depth") - 1); + max_depth = zend_hash_str_find_deref(options, "max_depth", sizeof("max_depth") - 1); if (max_depth) { if (Z_TYPE_P(max_depth) != IS_LONG) { - zend_type_error("unserialize(): \"max_depth\" option must be of type int, %s given", zend_zval_type_name(max_depth)); + zend_type_error("%s(): \"max_depth\" option must be of type int, %s given", function_name, zend_zval_type_name(max_depth)); goto cleanup; } if (Z_LVAL_P(max_depth) < 0) { - zend_value_error("unserialize(): \"max_depth\" option must be greater than or equal to 0"); + zend_value_error("%s(): \"max_depth\" option must be greater than or equal to 0", function_name); goto cleanup; } @@ -1291,6 +1282,23 @@ PHP_FUNCTION(unserialize) } /* }}} */ +/* {{{ Takes a string representation of variable and recreates it */ +PHP_FUNCTION(unserialize) +{ + char *buf = NULL; + size_t buf_len; + HashTable *options = NULL; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_STRING(buf, buf_len) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_HT(options) + ZEND_PARSE_PARAMETERS_END(); + + php_unserialize_with_options(return_value, buf, buf_len, options, "unserialize"); +} +/* }}} */ + /* {{{ Returns the allocated by PHP memory */ PHP_FUNCTION(memory_get_usage) { zend_bool real_usage = 0;