From 040ea4ab5f9e2390ead553104a3a569768c678a5 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sat, 13 Dec 2025 11:42:10 +0100 Subject: [PATCH 1/4] =?UTF-8?q?Revert=20"Fix=20GH-7737:=20openssl=5Fseal/o?= =?UTF-8?q?penssl=5Fopen=20do=20not=20handle=20tagged=20algorithm=E2=80=A6?= =?UTF-8?q?"=20(#20698)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 2ee5e6b432c7ed51ff7296522c123551975be95b. --- ext/openssl/openssl.c | 149 +++++++++++----------------------- ext/openssl/openssl.stub.php | 5 +- ext/openssl/openssl_arginfo.h | 4 +- ext/openssl/tests/gh7737.phpt | 57 ------------- 4 files changed, 50 insertions(+), 165 deletions(-) delete mode 100644 ext/openssl/tests/gh7737.phpt diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c index f770dc6deac8..2c09b89e3120 100644 --- a/ext/openssl/openssl.c +++ b/ext/openssl/openssl.c @@ -4171,24 +4171,22 @@ PHP_FUNCTION(openssl_verify) /* {{{ Seals data */ PHP_FUNCTION(openssl_seal) { - zval *pubkeys, *pubkey, *sealdata, *ekeys, *iv = NULL, *tag = NULL; + zval *pubkeys, *pubkey, *sealdata, *ekeys, *iv = NULL; HashTable *pubkeysht; - EVP_PKEY **pkeys = NULL; - int i, len1, len2, *eksl = NULL, nkeys = 0, iv_len; - unsigned char iv_buf[EVP_MAX_IV_LENGTH + 1], *buf = NULL, **eks = NULL; + EVP_PKEY **pkeys; + int i, len1, len2, *eksl, nkeys, iv_len; + unsigned char iv_buf[EVP_MAX_IV_LENGTH + 1], *buf = NULL, **eks; char * data; size_t data_len; char *method; size_t method_len; const EVP_CIPHER *cipher; - EVP_CIPHER_CTX *ctx = NULL; - size_t tag_len; + EVP_CIPHER_CTX *ctx; - if (zend_parse_parameters(ZEND_NUM_ARGS(), "szzas|z!z!", &data, &data_len, - &sealdata, &ekeys, &pubkeys, &method, &method_len, &iv, &tag) == FAILURE) { + if (zend_parse_parameters(ZEND_NUM_ARGS(), "szzas|z", &data, &data_len, + &sealdata, &ekeys, &pubkeys, &method, &method_len, &iv) == FAILURE) { RETURN_THROWS(); } - RETVAL_FALSE; PHP_OPENSSL_CHECK_SIZE_T_TO_INT(data_len, data, 1); @@ -4196,32 +4194,19 @@ PHP_FUNCTION(openssl_seal) nkeys = pubkeysht ? zend_hash_num_elements(pubkeysht) : 0; if (!nkeys) { zend_argument_must_not_be_empty_error(4); - goto clean_exit; + RETURN_THROWS(); } cipher = php_openssl_get_evp_cipher_by_name(method); if (!cipher) { php_error_docref(NULL, E_WARNING, "Unknown cipher algorithm"); - goto clean_exit; + RETURN_FALSE; } iv_len = EVP_CIPHER_iv_length(cipher); if (!iv && iv_len > 0) { zend_argument_value_error(6, "cannot be null for the chosen cipher algorithm"); - goto clean_exit; - } - - ctx = EVP_CIPHER_CTX_new(); - if (ctx == NULL || !EVP_EncryptInit(ctx,cipher,NULL,NULL)) { - php_openssl_store_errors(); - goto clean_exit; - } - - tag_len = EVP_CIPHER_CTX_get_tag_length(ctx); - if ((tag != NULL) != (tag_len > 0)) { - const char *imp = tag ? "cannot" : "must"; - zend_argument_value_error(7, "%s be specified for the chosen cipher algorithm", imp); - goto clean_exit; + RETURN_THROWS(); } pkeys = safe_emalloc(nkeys, sizeof(*pkeys), 0); @@ -4238,12 +4223,21 @@ PHP_FUNCTION(openssl_seal) if (!EG(exception)) { php_error_docref(NULL, E_WARNING, "Not a public key (%dth member of pubkeys)", i+1); } + RETVAL_FALSE; goto clean_exit; } eks[i] = emalloc(EVP_PKEY_size(pkeys[i]) + 1); i++; } ZEND_HASH_FOREACH_END(); + ctx = EVP_CIPHER_CTX_new(); + if (ctx == NULL || !EVP_EncryptInit(ctx,cipher,NULL,NULL)) { + EVP_CIPHER_CTX_free(ctx); + php_openssl_store_errors(); + RETVAL_FALSE; + goto clean_exit; + } + /* allocate one byte extra to make room for \0 */ buf = emalloc(data_len + EVP_CIPHER_CTX_block_size(ctx)); EVP_CIPHER_CTX_reset(ctx); @@ -4252,23 +4246,19 @@ PHP_FUNCTION(openssl_seal) !EVP_SealUpdate(ctx, buf, &len1, (unsigned char *)data, (int)data_len) || !EVP_SealFinal(ctx, buf + len1, &len2)) { efree(buf); + EVP_CIPHER_CTX_free(ctx); php_openssl_store_errors(); + RETVAL_FALSE; goto clean_exit; } - if (tag) { - zend_string *tag_str = zend_string_alloc(tag_len, 0); - EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, ZSTR_LEN(tag_str), ZSTR_VAL(tag_str)); - ZSTR_VAL(tag_str)[ZSTR_LEN(tag_str)] = 0; - ZEND_TRY_ASSIGN_REF_NEW_STR(tag, tag_str); - } - if (len1 + len2 > 0) { ZEND_TRY_ASSIGN_REF_NEW_STR(sealdata, zend_string_init((char*)buf, len1 + len2, 0)); efree(buf); ekeys = zend_try_array_init(ekeys); if (!ekeys) { + EVP_CIPHER_CTX_free(ctx); goto clean_exit; } @@ -4286,35 +4276,21 @@ PHP_FUNCTION(openssl_seal) } else { efree(buf); } - RETVAL_LONG(len1 + len2); + EVP_CIPHER_CTX_free(ctx); clean_exit: - if (ctx) { - EVP_CIPHER_CTX_free(ctx); - } - - if (pkeys) { - for (i=0; i 0) { if (!iv) { zend_argument_value_error(6, "cannot be null for the chosen cipher algorithm"); - goto clean_exit; + RETURN_THROWS(); } if ((size_t)cipher_iv_len != iv_len) { php_error_docref(NULL, E_WARNING, "IV length is invalid"); - goto clean_exit; + RETURN_FALSE; } iv_buf = (unsigned char *)iv; } else { @@ -4377,48 +4350,20 @@ PHP_FUNCTION(openssl_open) buf = emalloc(data_len + 1); ctx = EVP_CIPHER_CTX_new(); - if (ctx == NULL || !EVP_OpenInit(ctx, cipher, (unsigned char *)ekey, (int)ekey_len, iv_buf, pkey)) { - php_openssl_store_errors(); - goto clean_exit; - } - - tag_len = EVP_CIPHER_CTX_get_tag_length(ctx); - if ((tag != NULL) != (tag_len > 0)) { - const char *imp = tag ? "cannot" : "must"; - zend_argument_value_error(7, "%s be specified for the chosen cipher algorithm", imp); - goto clean_exit; - } - if (tag) { - if (ZSTR_LEN(tag) != tag_len) { - zend_argument_value_error(7, "must be %d bytes long", tag_len); - goto clean_exit; - } - - if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, ZSTR_LEN(tag), ZSTR_VAL(tag))) { - php_openssl_store_errors(); - goto clean_exit; - } - } - - if (EVP_OpenUpdate(ctx, buf, &len1, (unsigned char *)data, (int)data_len) && - EVP_OpenFinal(ctx, buf + len1, &len2) && (len1 + len2 > 0)) { + if (ctx != NULL && EVP_OpenInit(ctx, cipher, (unsigned char *)ekey, (int)ekey_len, iv_buf, pkey) && + EVP_OpenUpdate(ctx, buf, &len1, (unsigned char *)data, (int)data_len) && + EVP_OpenFinal(ctx, buf + len1, &len2) && (len1 + len2 > 0)) { buf[len1 + len2] = '\0'; ZEND_TRY_ASSIGN_REF_NEW_STR(opendata, zend_string_init((char*)buf, len1 + len2, 0)); RETVAL_TRUE; } else { php_openssl_store_errors(); + RETVAL_FALSE; } -clean_exit: - if (buf) { - efree(buf); - } - if (pkey) { - EVP_PKEY_free(pkey); - } - if (ctx) { - EVP_CIPHER_CTX_free(ctx); - } + efree(buf); + EVP_PKEY_free(pkey); + EVP_CIPHER_CTX_free(ctx); } /* }}} */ diff --git a/ext/openssl/openssl.stub.php b/ext/openssl/openssl.stub.php index 56e96a27a6ab..94902a4acf0d 100644 --- a/ext/openssl/openssl.stub.php +++ b/ext/openssl/openssl.stub.php @@ -628,15 +628,14 @@ function openssl_verify(string $data, string $signature, $public_key, string|int * @param string $sealed_data * @param array $encrypted_keys * @param string $iv - * @param string $tag */ -function openssl_seal(#[\SensitiveParameter] string $data, &$sealed_data, &$encrypted_keys, array $public_key, string $cipher_algo, &$iv = null, &$tag = null): int|false {} +function openssl_seal(#[\SensitiveParameter] string $data, &$sealed_data, &$encrypted_keys, array $public_key, string $cipher_algo, &$iv = null): int|false {} /** * @param string $output * @param OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $private_key */ -function openssl_open(string $data, #[\SensitiveParameter] &$output, string $encrypted_key, #[\SensitiveParameter] $private_key, string $cipher_algo, ?string $iv = null, ?string $tag = null): bool {} +function openssl_open(string $data, #[\SensitiveParameter] &$output, string $encrypted_key, #[\SensitiveParameter] $private_key, string $cipher_algo, ?string $iv = null): bool {} /** * @return array diff --git a/ext/openssl/openssl_arginfo.h b/ext/openssl/openssl_arginfo.h index 5ab604828e5c..796582c185bb 100644 --- a/ext/openssl/openssl_arginfo.h +++ b/ext/openssl/openssl_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 91d576073b79bf4b441e44eccb0deaa2b79a6949 */ + * Stub hash: 8233a8abc8ab7145d905d0fa51478edfe1e55a06 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_openssl_x509_export_to_file, 0, 2, _IS_BOOL, 0) ZEND_ARG_OBJ_TYPE_MASK(0, certificate, OpenSSLCertificate, MAY_BE_STRING, NULL) @@ -302,7 +302,6 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_openssl_seal, 0, 5, MAY_BE_LONG| ZEND_ARG_TYPE_INFO(0, public_key, IS_ARRAY, 0) ZEND_ARG_TYPE_INFO(0, cipher_algo, IS_STRING, 0) ZEND_ARG_INFO_WITH_DEFAULT_VALUE(1, iv, "null") - ZEND_ARG_INFO_WITH_DEFAULT_VALUE(1, tag, "null") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_openssl_open, 0, 5, _IS_BOOL, 0) @@ -312,7 +311,6 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_openssl_open, 0, 5, _IS_BOOL, 0) ZEND_ARG_INFO(0, private_key) ZEND_ARG_TYPE_INFO(0, cipher_algo, IS_STRING, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, iv, IS_STRING, 1, "null") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, tag, IS_STRING, 1, "null") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_openssl_get_md_methods, 0, 0, IS_ARRAY, 0) diff --git a/ext/openssl/tests/gh7737.phpt b/ext/openssl/tests/gh7737.phpt deleted file mode 100644 index 5136e3e295c1..000000000000 --- a/ext/openssl/tests/gh7737.phpt +++ /dev/null @@ -1,57 +0,0 @@ ---TEST-- -GitHub Bug#7737 - openssl_seal/open() does not handle ciphers with Tags (e.g. AES-256-CGM) ---EXTENSIONS-- -openssl ---SKIPIF-- - ---FILE-- - KEY_TYPE]); - define('KEY_PUBLIC', openssl_pkey_get_details($key)['key']); - define('KEY_PRIVATE', openssl_pkey_get_private($key)); -})(); - -echo 'Plaintext: '; var_dump(PLAINTEXT); - -$sealResult = openssl_seal(PLAINTEXT, - $sealedData, /* out */ - $sealedKeys, /* out */ - [KEY_PUBLIC], - CIPHER_ALGO, - $iv, /* out */ - $tag); /* out */ - -echo 'Seal Result: '; var_dump($sealResult); -echo 'Sealed Data: '; var_dump(strlen($sealedData)); -echo 'IV Length: '; var_dump(strlen($iv)); -echo 'Tag Length: '; var_dump(strlen($tag)); - -$unsealResult = openssl_open($sealedData, - $unsealedData, /* out */ - $sealedKeys[0], - KEY_PRIVATE, - CIPHER_ALGO, - $iv, - $tag); - -echo 'Unseal Result: '; var_dump($unsealResult); -echo 'Unsealed Data: '; var_dump($unsealedData); - -?> ---EXPECT-- -Plaintext: string(16) "Test Data String" -Seal Result: int(16) -Sealed Data: int(16) -IV Length: int(12) -Tag Length: int(16) -Unseal Result: bool(true) -Unsealed Data: string(16) "Test Data String" From 038e53420b35fe675153aacf43a5f7cefaec911c Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+ndossche@users.noreply.github.com> Date: Fri, 5 Dec 2025 18:51:42 +0100 Subject: [PATCH 2/4] standard: Fix error check for proc_open() command zval_get_string() can never return NULL, you need to use the try version to get NULL. This is observable because the process will still spawn even if an exception had occurred. To fix this, use the try variant. Closes GH-20650. --- NEWS | 3 +++ ext/standard/proc_open.c | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index be1c55a7c8e8..007faa024956 100644 --- a/NEWS +++ b/NEWS @@ -16,6 +16,9 @@ PHP NEWS - LDAP: . Fix memory leak in ldap_set_options(). (ndossche) +- Standard: + . Fix error check for proc_open() command. (ndossche) + 18 Dec 2025, PHP 8.3.29 - Core: diff --git a/ext/standard/proc_open.c b/ext/standard/proc_open.c index ce7710982774..96e5a4ced991 100644 --- a/ext/standard/proc_open.c +++ b/ext/standard/proc_open.c @@ -510,7 +510,7 @@ typedef struct _descriptorspec_item { } descriptorspec_item; static zend_string *get_valid_arg_string(zval *zv, int elem_num) { - zend_string *str = zval_get_string(zv); + zend_string *str = zval_try_get_string(zv); if (!str) { return NULL; } From 521c0c5cd233a8626eb62694169453b274cf44eb Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sun, 5 May 2024 15:29:58 +0200 Subject: [PATCH 3/4] Refactor path name processing in phar_build() --- ext/phar/phar_object.c | 40 ++++++++++++++++------------------------ 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c index 11a0dd17a4e5..9f0d566b4279 100644 --- a/ext/phar/phar_object.c +++ b/ext/phar/phar_object.c @@ -1411,7 +1411,6 @@ static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */ goto after_open_fp; case IS_OBJECT: if (instanceof_function(Z_OBJCE_P(value), spl_ce_SplFileInfo)) { - char *test = NULL; spl_filesystem_object *intern = PHAR_FETCH_INTERNAL_EX(value); if (!base_len) { @@ -1421,41 +1420,34 @@ static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */ switch (intern->type) { case SPL_FS_DIR: { + char *tmp; zend_string *test_str = spl_filesystem_object_get_path(intern); - fname_len = spprintf(&fname, 0, "%s%c%s", ZSTR_VAL(test_str), DEFAULT_SLASH, intern->u.dir.entry.d_name); + fname_len = spprintf(&tmp, 0, "%s%c%s", ZSTR_VAL(test_str), DEFAULT_SLASH, intern->u.dir.entry.d_name); zend_string_release_ex(test_str, /* persistent */ false); - if (php_stream_stat_path(fname, &ssb) == 0 && S_ISDIR(ssb.sb.st_mode)) { + if (php_stream_stat_path(tmp, &ssb) == 0 && S_ISDIR(ssb.sb.st_mode)) { /* ignore directories */ - efree(fname); + efree(tmp); return ZEND_HASH_APPLY_KEEP; } - test = expand_filepath(fname, NULL); - efree(fname); - - if (test) { - fname = test; - fname_len = strlen(fname); - } else { - zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "Could not resolve file path"); - return ZEND_HASH_APPLY_STOP; - } - - save = fname; - goto phar_spl_fileinfo; + fname = expand_filepath(tmp, NULL); + efree(tmp); + break; } case SPL_FS_INFO: case SPL_FS_FILE: fname = expand_filepath(ZSTR_VAL(intern->file_name), NULL); - if (!fname) { - zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "Could not resolve file path"); - return ZEND_HASH_APPLY_STOP; - } + break; + } - fname_len = strlen(fname); - save = fname; - goto phar_spl_fileinfo; + if (!fname) { + zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "Could not resolve file path"); + return ZEND_HASH_APPLY_STOP; } + + fname_len = strlen(fname); + save = fname; + goto phar_spl_fileinfo; } ZEND_FALLTHROUGH; default: From c9008f6dd867c4d6be9e9f5f6d0bb51fc1e4cbe4 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sun, 5 May 2024 20:58:06 +0200 Subject: [PATCH 4/4] Make buildFromIterator() work with custom SplFileInfo objects While it is possible to return a custom SplFileInfo object in the iterator used by buildFromIterator(), the data is not actually used from that object, instead the data from the underlying internal structure is used. This makes it impossible to override some metadata such as the path name and modification time. The main motivation comes from two reasons: - Consistency. We expect our custom methods to be called when having a custom object. - Support reproducibility. This is the original use case as requested in [1]. Add support for this by calling the getMTime() and getPathname() methods if they're overriden by a user class. [1] https://github.com/theseer/Autoload/issues/114. --- NEWS | 2 + UPGRADING | 5 + ext/phar/phar_internal.h | 3 +- ext/phar/phar_object.c | 103 +++++++++++---- ext/phar/stream.c | 2 +- .../getMTime.phpt | 81 ++++++++++++ .../getMTime_byRef.phpt | 70 +++++++++++ .../getMTime_errors.phpt | 117 ++++++++++++++++++ .../getPathname.phpt | 67 ++++++++++ .../getPathname_byRef.phpt | 58 +++++++++ .../getPathname_exception.phpt | 63 ++++++++++ .../getPathname_wrong_type.phpt | 58 +++++++++ ext/phar/util.c | 4 +- 13 files changed, 608 insertions(+), 25 deletions(-) create mode 100644 ext/phar/tests/buildFromIterator_user_overrides/getMTime.phpt create mode 100644 ext/phar/tests/buildFromIterator_user_overrides/getMTime_byRef.phpt create mode 100644 ext/phar/tests/buildFromIterator_user_overrides/getMTime_errors.phpt create mode 100644 ext/phar/tests/buildFromIterator_user_overrides/getPathname.phpt create mode 100644 ext/phar/tests/buildFromIterator_user_overrides/getPathname_byRef.phpt create mode 100644 ext/phar/tests/buildFromIterator_user_overrides/getPathname_exception.phpt create mode 100644 ext/phar/tests/buildFromIterator_user_overrides/getPathname_wrong_type.phpt diff --git a/NEWS b/NEWS index f2e81284bdf8..671d2b57a676 100644 --- a/NEWS +++ b/NEWS @@ -39,6 +39,8 @@ PHP NEWS . Support reference values in Phar::mungServer(). (ndossche) . Invalid values now throw in Phar::mungServer() instead of being silently ignored. (ndossche) + . Support overridden methods in SplFileInfo for getMTime() and getPathname() + when building a phar. (ndossche) - Reflection: . Fixed bug GH-20217 (ReflectionClass::isIterable() incorrectly returns true diff --git a/UPGRADING b/UPGRADING index 7f0fcaf8943a..4e511d094797 100644 --- a/UPGRADING +++ b/UPGRADING @@ -37,6 +37,11 @@ PHP 8.6 UPGRADE NOTES IntlNumberRangeFormatter::IDENTITY_FALLBACK_RANGE identity fallbacks. It is supported from icu 63. +- Phar: + . Overriding the getMTime() and getPathname() methods of SplFileInfo now + influences the result of the phar buildFrom family of functions. + This makes it possible to override the timestamp and names of files. + - Streams: . Added stream socket context option so_reuseaddr that allows disabling address reuse (SO_REUSEADDR) and explicitly uses SO_EXCLUSIVEADDRUSE on diff --git a/ext/phar/phar_internal.h b/ext/phar/phar_internal.h index 5fc435454531..144a3c1359f9 100644 --- a/ext/phar/phar_internal.h +++ b/ext/phar/phar_internal.h @@ -196,6 +196,7 @@ typedef struct _phar_metadata_tracker { typedef struct _phar_entry_info { /* first bytes are exactly as in file */ uint32_t uncompressed_filesize; + /* modification time */ uint32_t timestamp; uint32_t compressed_filesize; uint32_t crc32; @@ -464,7 +465,7 @@ void phar_entry_delref(phar_entry_data *idata); phar_entry_info *phar_get_entry_info(phar_archive_data *phar, char *path, size_t path_len, char **error, bool security); phar_entry_info *phar_get_entry_info_dir(phar_archive_data *phar, char *path, size_t path_len, char dir, char **error, bool security); -ZEND_ATTRIBUTE_NONNULL phar_entry_data *phar_get_or_create_entry_data(char *fname, size_t fname_len, char *path, size_t path_len, const char *mode, char allow_dir, char **error, bool security); +ZEND_ATTRIBUTE_NONNULL phar_entry_data *phar_get_or_create_entry_data(char *fname, size_t fname_len, char *path, size_t path_len, const char *mode, char allow_dir, char **error, bool security, uint32_t timestamp); ZEND_ATTRIBUTE_NONNULL zend_result phar_get_entry_data(phar_entry_data **ret, char *fname, size_t fname_len, char *path, size_t path_len, const char *mode, char allow_dir, char **error, bool security); ZEND_ATTRIBUTE_NONNULL_ARGS(1, 4) int phar_flush_ex(phar_archive_data *archive, zend_string *user_stub, bool is_default_stub, char **error); ZEND_ATTRIBUTE_NONNULL int phar_flush(phar_archive_data *archive, char **error); diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c index 9f0d566b4279..cd00880a0bf0 100644 --- a/ext/phar/phar_object.c +++ b/ext/phar/phar_object.c @@ -1341,6 +1341,44 @@ struct _phar_t { int count; }; +static zend_always_inline void phar_call_method_with_unwrap(zend_object *obj, const char *name, zval *rv) +{ + zend_call_method_with_0_params(obj, obj->ce, NULL, name, rv); + if (Z_ISREF_P(rv)) { + zend_unwrap_reference(rv); + } +} + +/* This is the same as phar_get_or_create_entry_data(), but allows overriding metadata via SplFileInfo. */ +static phar_entry_data *phar_build_entry_data(char *fname, size_t fname_len, char *path, size_t path_len, char **error, zval *file_info) +{ + uint32_t timestamp; + + /* Expects an instance of SplFileInfo if it is an object, which is verified in phar_build(). */ + if (Z_TYPE_P(file_info) == IS_OBJECT && Z_OBJCE_P(file_info)->type == ZEND_USER_CLASS) { + zval rv; + phar_call_method_with_unwrap(Z_OBJ_P(file_info), "getMTime", &rv); + + if (UNEXPECTED(Z_TYPE(rv) != IS_LONG)) { + /* Either it's a tentative type failure, an exception happened, or the function returned false to indicate failure. */ + *error = estrdup("getMTime() must return an int"); + return NULL; + } + + /* Sanity check bounds. See GH-14141. */ + if (ZEND_LONG_UINT_OVFL(Z_LVAL(rv))) { + *error = estrdup("timestamp is limited to 32-bit"); + return NULL; + } + + timestamp = Z_LVAL(rv); + } else { + timestamp = time(NULL); + } + + return phar_get_or_create_entry_data(fname, fname_len, path, path_len, "w+b", 0, error, true, timestamp); +} + static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */ { zval *value; @@ -1351,7 +1389,7 @@ static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */ php_stream *fp; size_t fname_len; size_t contents_len; - char *fname, *error = NULL, *base = ZSTR_VAL(p_obj->base), *save = NULL, *temp = NULL; + char *fname = NULL, *error = NULL, *base = ZSTR_VAL(p_obj->base), *save = NULL, *temp = NULL; zend_string *opened; char *str_key; zend_class_entry *ce = p_obj->c; @@ -1418,26 +1456,49 @@ static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */ return ZEND_HASH_APPLY_STOP; } - switch (intern->type) { - case SPL_FS_DIR: { - char *tmp; - zend_string *test_str = spl_filesystem_object_get_path(intern); - fname_len = spprintf(&tmp, 0, "%s%c%s", ZSTR_VAL(test_str), DEFAULT_SLASH, intern->u.dir.entry.d_name); - zend_string_release_ex(test_str, /* persistent */ false); - if (php_stream_stat_path(tmp, &ssb) == 0 && S_ISDIR(ssb.sb.st_mode)) { - /* ignore directories */ - efree(tmp); - return ZEND_HASH_APPLY_KEEP; + zend_string *tmp_dir_str = NULL; + + /* Take into account that SplFileObject may be overridden. + * The purpose here is to grab the path name of the file to add. */ + if (Z_OBJCE_P(value)->type == ZEND_USER_CLASS) { + zval rv; + phar_call_method_with_unwrap(Z_OBJ_P(value), "getPathname", &rv); + + if (UNEXPECTED(Z_TYPE(rv) != IS_STRING)) { + zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "getPathname() must return a string"); + return ZEND_HASH_APPLY_STOP; + } + tmp_dir_str = Z_STR(rv); + } else { + /* Not a user class, so we can grab the data internally quickly. */ + switch (intern->type) { + case SPL_FS_DIR: { + zend_string *test_str = spl_filesystem_object_get_path(intern); + const char slash = DEFAULT_SLASH; + tmp_dir_str = zend_string_concat3( + ZSTR_VAL(test_str), ZSTR_LEN(test_str), + &slash, 1, + intern->u.dir.entry.d_name, strlen(intern->u.dir.entry.d_name) + ); + zend_string_release_ex(test_str, /* persistent */ false); + break; } + case SPL_FS_INFO: + case SPL_FS_FILE: + fname = expand_filepath(ZSTR_VAL(intern->file_name), NULL); + break; + } + } - fname = expand_filepath(tmp, NULL); - efree(tmp); - break; + if (tmp_dir_str) { + if (php_stream_stat_path(ZSTR_VAL(tmp_dir_str), &ssb) == 0 && S_ISDIR(ssb.sb.st_mode)) { + /* ignore directories */ + zend_string_release_ex(tmp_dir_str, /* persistent */ false); + return ZEND_HASH_APPLY_KEEP; } - case SPL_FS_INFO: - case SPL_FS_FILE: - fname = expand_filepath(ZSTR_VAL(intern->file_name), NULL); - break; + + fname = expand_filepath(ZSTR_VAL(tmp_dir_str), NULL); + zend_string_release_ex(tmp_dir_str, /* persistent */ false); } if (!fname) { @@ -1578,7 +1639,7 @@ static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */ return ZEND_HASH_APPLY_KEEP; } - if (!(data = phar_get_or_create_entry_data(phar_obj->archive->fname, phar_obj->archive->fname_len, str_key, str_key_len, "w+b", 0, &error, true))) { + if (!(data = phar_build_entry_data(phar_obj->archive->fname, phar_obj->archive->fname_len, str_key, str_key_len, &error, value))) { zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s cannot be created: %s", str_key, error); efree(error); @@ -3536,7 +3597,7 @@ static void phar_add_file(phar_archive_data **pphar, zend_string *file_name, con } #endif - if (!(data = phar_get_or_create_entry_data((*pphar)->fname, (*pphar)->fname_len, filename, filename_len, "w+b", 0, &error, true))) { + if (!(data = phar_get_or_create_entry_data((*pphar)->fname, (*pphar)->fname_len, filename, filename_len, "w+b", 0, &error, true, time(NULL)))) { if (error) { zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s does not exist and cannot be created: %s", filename, error); efree(error); @@ -3608,7 +3669,7 @@ static void phar_mkdir(phar_archive_data **pphar, zend_string *dir_name) char *error; phar_entry_data *data; - if (!(data = phar_get_or_create_entry_data((*pphar)->fname, (*pphar)->fname_len, ZSTR_VAL(dir_name), ZSTR_LEN(dir_name), "w+b", 2, &error, true))) { + if (!(data = phar_get_or_create_entry_data((*pphar)->fname, (*pphar)->fname_len, ZSTR_VAL(dir_name), ZSTR_LEN(dir_name), "w+b", 2, &error, true, time(NULL)))) { if (error) { zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Directory %s does not exist and cannot be created: %s", ZSTR_VAL(dir_name), error); efree(error); diff --git a/ext/phar/stream.c b/ext/phar/stream.c index bb60f00d8f16..08da1847cd9c 100644 --- a/ext/phar/stream.c +++ b/ext/phar/stream.c @@ -191,7 +191,7 @@ static php_stream * phar_wrapper_open_url(php_stream_wrapper *wrapper, const cha /* strip leading "/" */ internal_file = estrndup(ZSTR_VAL(resource->path) + 1, ZSTR_LEN(resource->path) - 1); if (mode[0] == 'w' || (mode[0] == 'r' && mode[1] == '+')) { - if (NULL == (idata = phar_get_or_create_entry_data(ZSTR_VAL(resource->host), ZSTR_LEN(resource->host), internal_file, strlen(internal_file), mode, 0, &error, true))) { + if (NULL == (idata = phar_get_or_create_entry_data(ZSTR_VAL(resource->host), ZSTR_LEN(resource->host), internal_file, strlen(internal_file), mode, 0, &error, true, time(NULL)))) { if (error) { php_stream_wrapper_log_error(wrapper, options, "%s", error); efree(error); diff --git a/ext/phar/tests/buildFromIterator_user_overrides/getMTime.phpt b/ext/phar/tests/buildFromIterator_user_overrides/getMTime.phpt new file mode 100644 index 000000000000..eefe352a0cbb --- /dev/null +++ b/ext/phar/tests/buildFromIterator_user_overrides/getMTime.phpt @@ -0,0 +1,81 @@ +--TEST-- +buildFromIterator with user overrides - getMTime() +--EXTENSIONS-- +phar +--INI-- +phar.readonly=0 +phar.require_hash=0 +--CREDITS-- +Arne Blankerts +N. Dossche +--FILE-- +getPathname() . " ]\n"; + return new MySplFileInfo(parent::current()->getPathname()); + } +} + +$workdir = __DIR__.'/getMTime'; +mkdir($workdir . '/content', recursive: true); +file_put_contents($workdir . '/content/hello.txt', "Hello world."); + +$phar = new \Phar($workdir . '/test.phar'); +$phar->startBuffering(); +$phar->buildFromIterator( + new RecursiveIteratorIterator( + new MyIterator($workdir . '/content', FilesystemIterator::SKIP_DOTS) + ), + $workdir +); +$phar->stopBuffering(); + + +$result = new \Phar($workdir . '/test.phar', 0, 'test.phar'); +var_dump($result['content/hello.txt']); +var_dump($result['content/hello.txt']->getATime()); +var_dump($result['content/hello.txt']->getMTime()); +var_dump($result['content/hello.txt']->getCTime()); + +?> +--CLEAN-- + +--EXPECTF-- +[ Found: %shello.txt ] +[MTime] +object(PharFileInfo)#%d (2) { + ["pathName":"SplFileInfo":private]=> + string(%d) "phar://%shello.txt" + ["fileName":"SplFileInfo":private]=> + string(%d) "hello.txt" +} +int(123) +int(123) +int(123) diff --git a/ext/phar/tests/buildFromIterator_user_overrides/getMTime_byRef.phpt b/ext/phar/tests/buildFromIterator_user_overrides/getMTime_byRef.phpt new file mode 100644 index 000000000000..a7f81709a34e --- /dev/null +++ b/ext/phar/tests/buildFromIterator_user_overrides/getMTime_byRef.phpt @@ -0,0 +1,70 @@ +--TEST-- +buildFromIterator with user overrides - getMTime() by ref +--EXTENSIONS-- +phar +--INI-- +phar.readonly=0 +phar.require_hash=0 +--CREDITS-- +Arne Blankerts +N. Dossche +--FILE-- +getPathname() . " ]\n"; + return new MySplFileInfo(parent::current()->getPathname()); + } +} + +$workdir = __DIR__.'/getMTime_byRef'; +mkdir($workdir . '/content', recursive: true); +file_put_contents($workdir . '/content/hello.txt', "Hello world."); + +$phar = new \Phar($workdir . '/test.phar'); +$phar->startBuffering(); +$phar->buildFromIterator( + new RecursiveIteratorIterator( + new MyIterator($workdir . '/content', FilesystemIterator::SKIP_DOTS) + ), + $workdir +); +$phar->stopBuffering(); + + +$result = new \Phar($workdir . '/test.phar', 0, 'test.phar'); +var_dump($result['content/hello.txt']); +var_dump($result['content/hello.txt']->getATime()); +var_dump($result['content/hello.txt']->getMTime()); +var_dump($result['content/hello.txt']->getCTime()); + +?> +--CLEAN-- + +--EXPECTF-- +[ Found: %shello.txt ] +[MTime] +object(PharFileInfo)#%d (2) { + ["pathName":"SplFileInfo":private]=> + string(%d) "phar://%shello.txt" + ["fileName":"SplFileInfo":private]=> + string(%d) "hello.txt" +} +int(123) +int(123) +int(123) diff --git a/ext/phar/tests/buildFromIterator_user_overrides/getMTime_errors.phpt b/ext/phar/tests/buildFromIterator_user_overrides/getMTime_errors.phpt new file mode 100644 index 000000000000..f43a7a496a76 --- /dev/null +++ b/ext/phar/tests/buildFromIterator_user_overrides/getMTime_errors.phpt @@ -0,0 +1,117 @@ +--TEST-- +buildFromIterator with user overrides - errors in getMTime() +--EXTENSIONS-- +phar +--SKIPIF-- + +--INI-- +phar.readonly=0 +phar.require_hash=0 +--CREDITS-- +Arne Blankerts +N. Dossche +--FILE-- +getPathname() . " ]\n"; + if ($counter === 1) { + return new MySplFileInfo1(parent::current()->getPathname()); + } else if ($counter === 2) { + return new MySplFileInfo2(parent::current()->getPathname()); + } else if ($counter === 3) { + return new MySplFileInfo3(parent::current()->getPathname()); + } else if ($counter === 4) { + return new MySplFileInfo4(parent::current()->getPathname()); + } + } +} + +$workdir = __DIR__.'/getMTime_errors'; +mkdir($workdir . '/content', recursive: true); +file_put_contents($workdir . '/content/hello.txt', "Hello world."); + +for ($i = 0; $i < 4; $i++) { + echo "--- Iteration $i ---\n"; + try { + $phar = new \Phar($workdir . "/test$i.phar"); + $phar->startBuffering(); + $phar->buildFromIterator( + new RecursiveIteratorIterator( + new MyIterator($workdir . '/content', FilesystemIterator::SKIP_DOTS) + ), + $workdir + ); + $phar->stopBuffering(); + } catch (Throwable $e) { + echo $e->getMessage(), "\n"; + if ($previous = $e->getPrevious()) { + echo "Previous: ", $previous->getMessage(), "\n"; + } + } +} + +?> +--CLEAN-- + +--EXPECTF-- +--- Iteration 0 --- +[ Found: %shello.txt ] +[MTime] +Entry content%chello.txt cannot be created: timestamp is limited to 32-bit +--- Iteration 1 --- +[ Found: %shello.txt ] +[MTime] +Entry content%chello.txt cannot be created: getMTime() must return an int +--- Iteration 2 --- +[ Found: %shello.txt ] +[MTime] +Entry content%chello.txt cannot be created: getMTime() must return an int +Previous: Throwing an exception inside getMTime() +--- Iteration 3 --- +[ Found: %shello.txt ] +[MTime] +Entry content%chello.txt cannot be created: getMTime() must return an int diff --git a/ext/phar/tests/buildFromIterator_user_overrides/getPathname.phpt b/ext/phar/tests/buildFromIterator_user_overrides/getPathname.phpt new file mode 100644 index 000000000000..144eed1d874a --- /dev/null +++ b/ext/phar/tests/buildFromIterator_user_overrides/getPathname.phpt @@ -0,0 +1,67 @@ +--TEST-- +buildFromIterator with user overrides - getPathname() +--EXTENSIONS-- +phar +--INI-- +phar.readonly=0 +phar.require_hash=0 +--CREDITS-- +Arne Blankerts +N. Dossche +--FILE-- +getPathname() . " ]\n"; + return new MyGlobIterator(parent::current()->getPath() . '/*'); + } +} + +$workdir = __DIR__.'/getPathname'; +mkdir($workdir . '/content', recursive: true); +file_put_contents($workdir . '/content/hello1.txt', "Hello world 1."); +file_put_contents($workdir . '/content/hello2.txt', "Hello world 2."); + +$phar = new \Phar($workdir . "/test.phar"); +$phar->startBuffering(); +$phar->buildFromIterator( + new RecursiveIteratorIterator( + new MyIterator($workdir . '/content', FilesystemIterator::SKIP_DOTS) + ), + $workdir +); +$phar->stopBuffering(); + +$result = new \Phar($workdir . '/test.phar', 0, 'test.phar'); +var_dump(isset($result['content/hello1.txt'])); +var_dump(isset($result['content/hello2.txt'])); + +?> +--CLEAN-- + +--EXPECTF-- +[ Found: %shello%d.txt ] +[getPathname] +string(%d) "%shello1.txt" +[ Found: %shello%d.txt ] +[getPathname] +string(%d) "%shello1.txt" +bool(false) +bool(true) diff --git a/ext/phar/tests/buildFromIterator_user_overrides/getPathname_byRef.phpt b/ext/phar/tests/buildFromIterator_user_overrides/getPathname_byRef.phpt new file mode 100644 index 000000000000..0ae2946ce1ad --- /dev/null +++ b/ext/phar/tests/buildFromIterator_user_overrides/getPathname_byRef.phpt @@ -0,0 +1,58 @@ +--TEST-- +buildFromIterator with user overrides - getPathname() by ref +--EXTENSIONS-- +phar +--INI-- +phar.readonly=0 +phar.require_hash=0 +--CREDITS-- +Arne Blankerts +N. Dossche +--FILE-- +getPathname() . " ]\n"; + return new MyGlobIterator(parent::current()->getPath() . '/*'); + } +} + +$workdir = __DIR__.'/getPathname_byRef'; +mkdir($workdir . '/content', recursive: true); +file_put_contents($workdir . '/content/hello.txt', "Hello world 1."); + +$phar = new \Phar($workdir . "/test.phar"); +$phar->startBuffering(); +$phar->buildFromIterator( + new RecursiveIteratorIterator( + new MyIterator($workdir . '/content', FilesystemIterator::SKIP_DOTS) + ), + $workdir +); +$phar->stopBuffering(); + +$result = new \Phar($workdir . '/test.phar', 0, 'test.phar'); +var_dump(isset($result['content/hello.txt'])); + +?> +--CLEAN-- + +--EXPECTF-- +[ Found: %scontent%chello.txt ] +[getPathname] +bool(true) diff --git a/ext/phar/tests/buildFromIterator_user_overrides/getPathname_exception.phpt b/ext/phar/tests/buildFromIterator_user_overrides/getPathname_exception.phpt new file mode 100644 index 000000000000..86b66050a973 --- /dev/null +++ b/ext/phar/tests/buildFromIterator_user_overrides/getPathname_exception.phpt @@ -0,0 +1,63 @@ +--TEST-- +buildFromIterator with user overrides - exception in getPathname() +--EXTENSIONS-- +phar +--INI-- +phar.readonly=0 +phar.require_hash=0 +--CREDITS-- +Arne Blankerts +N. Dossche +--FILE-- +getPathname() . " ]\n"; + return new MyGlobIterator(parent::current()->getPath() . '/*'); + } +} + +$workdir = __DIR__.'/getPathname_exception'; +mkdir($workdir . '/content', recursive: true); +file_put_contents($workdir . '/content/hello.txt', "Hello world."); + +$phar = new \Phar($workdir . "/test.phar"); +$phar->startBuffering(); +try { + $phar->buildFromIterator( + new RecursiveIteratorIterator( + new MyIterator($workdir . '/content', FilesystemIterator::SKIP_DOTS) + ), + $workdir + ); +} catch (Throwable $e) { + echo $e->getMessage(), "\n"; + if ($previous = $e->getPrevious()) { + echo "Previous: ", $previous->getMessage(), "\n"; + } +} +$phar->stopBuffering(); + +?> +--CLEAN-- + +--EXPECTF-- +[ Found: %shello.txt ] +[getPathname] +string(%d) "%shello.txt" +getPathname() must return a string +Previous: exception in getPathname() diff --git a/ext/phar/tests/buildFromIterator_user_overrides/getPathname_wrong_type.phpt b/ext/phar/tests/buildFromIterator_user_overrides/getPathname_wrong_type.phpt new file mode 100644 index 000000000000..dfb3fb5f2c55 --- /dev/null +++ b/ext/phar/tests/buildFromIterator_user_overrides/getPathname_wrong_type.phpt @@ -0,0 +1,58 @@ +--TEST-- +buildFromIterator with user overrides - wrong return type in getPathname() +--EXTENSIONS-- +phar +--INI-- +phar.readonly=0 +phar.require_hash=0 +--CREDITS-- +Arne Blankerts +N. Dossche +--FILE-- +getPathname() . " ]\n"; + return new MyGlobIterator(parent::current()->getPath() . '/*'); + } +} + +$workdir = __DIR__.'/getPathname_wrong_type'; +mkdir($workdir . '/content', recursive: true); +file_put_contents($workdir . '/content/hello.txt', "Hello world."); + +$phar = new \Phar($workdir . "/test.phar"); +$phar->startBuffering(); +try { + $phar->buildFromIterator( + new RecursiveIteratorIterator( + new MyIterator($workdir . '/content', FilesystemIterator::SKIP_DOTS) + ), + $workdir + ); +} catch (Throwable $e) { + echo $e->getMessage(), "\n"; +} +$phar->stopBuffering(); + +?> +--CLEAN-- + +--EXPECTF-- +[ Found: %scontent%chello.txt ] +[getPathname] +getPathname() must return a string diff --git a/ext/phar/util.c b/ext/phar/util.c index 60607006f671..82c784e62171 100644 --- a/ext/phar/util.c +++ b/ext/phar/util.c @@ -606,7 +606,7 @@ ZEND_ATTRIBUTE_NONNULL zend_result phar_get_entry_data(phar_entry_data **ret, ch /** * Create a new dummy file slot within a writeable phar for a newly created file */ -ZEND_ATTRIBUTE_NONNULL phar_entry_data *phar_get_or_create_entry_data(char *fname, size_t fname_len, char *path, size_t path_len, const char *mode, char allow_dir, char **error, bool security) /* {{{ */ +ZEND_ATTRIBUTE_NONNULL phar_entry_data *phar_get_or_create_entry_data(char *fname, size_t fname_len, char *path, size_t path_len, const char *mode, char allow_dir, char **error, bool security, uint32_t timestamp) /* {{{ */ { phar_archive_data *phar; phar_entry_info *entry, etemp; @@ -668,7 +668,7 @@ ZEND_ATTRIBUTE_NONNULL phar_entry_data *phar_get_or_create_entry_data(char *fnam phar_add_virtual_dirs(phar, path, path_len); etemp.is_modified = 1; - etemp.timestamp = time(0); + etemp.timestamp = timestamp; etemp.is_crc_checked = 1; etemp.phar = phar; etemp.filename = zend_string_init(path, path_len, false);