From 3064540134148ca469c4757ec658d690d8cd90b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CLamentXU123=E2=80=9D?= <108666168+LamentXU123@users.noreply.github.com> Date: Mon, 25 May 2026 16:59:28 +0800 Subject: [PATCH 1/4] ext/phar: improve .phar madic directory preservation logic in phar::addEmptyDir() Now, the .phar directory is a magic dir for phar files, and in phar::addEmptyDir(), users couldn't create a dir naming .phar The implementation is: ```c if (zend_string_starts_with_literal(dir_name, ".phar")) { zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Cannot create a directory in magic \".phar\" directory"); RETURN_THROWS(); ``` This has two bugs. Firstly, people can use /.phar to create the .phar dir. The leading / will be ignored. (no need to concern about ../ though, it will be ignored.) ```php addEmptyDir('/.phar'); var_dump(is_dir('phar://' . __DIR__ . '/test.phar/.phar')); ``` Will return true with the .phar dir created, while if the dir is .phar it will raise an error. Secondly, it only matches the prefix. That means, /.pharxxx will not be allowed to create, which is not a magic dir. ```php addEmptyDir('.pharx'); ``` This will raise an error. ``` PHP Fatal error: Uncaught BadMethodCallException: Cannot create a directory in magic ".phar" directory in C:\Users\admin\Desktop\bench.php:3 ``` This PR fix both by 1. adding a trailing check of the path to make .pharx valid 2. adding a check to /.phar Closes GH-22146. --- NEWS | 5 +++++ ext/phar/phar_object.c | 13 ++++++++++--- ext/phar/tests/mkdir.phpt | 9 +++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/NEWS b/NEWS index dbf83143dce3..49b61d236967 100644 --- a/NEWS +++ b/NEWS @@ -17,6 +17,11 @@ PHP NEWS IntlCalendar::equals(), ::before(), ::after(), and ::isEquivalentTo(). (Weilin Du) +- Phar: + . Fixed a bypass of the magic ".phar" directory protection in + Phar::addEmptyDir() for paths starting with "/.phar", while allowing + non-magic directory names that merely share the ".phar" prefix. (Weilin Du) + - Zlib: . Fixed memory leak if deflate initialization fails and there is a dict. (ndossche) diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c index bb96c783e931..a2d70a1a000f 100644 --- a/ext/phar/phar_object.c +++ b/ext/phar/phar_object.c @@ -3863,9 +3863,16 @@ PHP_METHOD(Phar, addEmptyDir) PHAR_ARCHIVE_OBJECT(); - if (zend_string_starts_with_literal(dir_name, ".phar")) { - zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Cannot create a directory in magic \".phar\" directory"); - RETURN_THROWS(); + if ( + zend_string_starts_with_literal(dir_name, ".phar") + || zend_string_starts_with_literal(dir_name, "/.phar") + ) { + size_t prefix_len = (ZSTR_VAL(dir_name)[0] == '/') + sizeof(".phar")-1; + char next_char = ZSTR_VAL(dir_name)[prefix_len]; + if (next_char == '/' || next_char == '\\' || next_char == '\0') { + zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Cannot create a directory in magic \".phar\" directory"); + RETURN_THROWS(); + } } phar_mkdir(&phar_obj->archive, dir_name); diff --git a/ext/phar/tests/mkdir.phpt b/ext/phar/tests/mkdir.phpt index 1ffdc7fe252d..2c1586b0de5c 100644 --- a/ext/phar/tests/mkdir.phpt +++ b/ext/phar/tests/mkdir.phpt @@ -24,6 +24,13 @@ $a->addEmptyDir('.phar'); } catch (Exception $e) { echo $e->getMessage(),"\n"; } +try { +$a->addEmptyDir('/.phar'); +} catch (Exception $e) { +echo $e->getMessage(),"\n"; +} +$a->addEmptyDir('/.pharx'); +var_dump(is_dir($pname . '/.pharx')); ?> --CLEAN-- Date: Sat, 30 May 2026 13:08:23 +0200 Subject: [PATCH 2/4] ext/openssl: use helper function for ValueError --- ext/openssl/openssl.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c index 65e63265dfc6..e7f29c6a354b 100644 --- a/ext/openssl/openssl.c +++ b/ext/openssl/openssl.c @@ -216,7 +216,7 @@ PHP_METHOD(Openssl_Psk, __construct) ZEND_PARSE_PARAMETERS_END(); if (ZSTR_LEN(psk) == 0) { - zend_argument_value_error(1, "must not be empty"); + zend_argument_must_not_be_empty_error(1); RETURN_THROWS(); } if (ZSTR_LEN(psk) > PHP_OPENSSL_PSK_MAX_PSK_LEN) { From f7c63f7f479c2d98740646092354a27753b55457 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Sat, 30 May 2026 13:40:35 +0200 Subject: [PATCH 3/4] ext/openssl: convert PSK callback to FCC Prevent rechecking the given zval callable every single time --- ext/openssl/xp_ssl.c | 56 +++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index f94059673ed0..f90d7549ae76 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -183,8 +183,8 @@ static const unsigned char php_openssl_tls13_aes256gcmsha384_id[] = { 0x13, 0x02 /* Holds PSK callbacks */ typedef struct _php_openssl_psk_callbacks_t { int refcount; - zval client_cb; - zval server_cb; + zend_fcall_info_cache client_cb; + zend_fcall_info_cache server_cb; } php_openssl_psk_callbacks_t; /* Holds session callback */ @@ -1621,7 +1621,7 @@ static SSL_SESSION *php_openssl_psk_build_session(SSL *ssl, /** * Invoke a user PHP callback (psk_client_cb or psk_server_cb). */ -static zend_result php_openssl_call_psk_cb(php_stream *stream, zval *cb, +static zend_result php_openssl_call_psk_cb(php_stream *stream, zend_fcall_info_cache *fcc, const unsigned char *identity, size_t identity_len, zval *result) { @@ -1639,13 +1639,14 @@ static zend_result php_openssl_call_psk_cb(php_stream *stream, zval *cb, } ZVAL_UNDEF(&retval); - call_user_function(NULL, NULL, cb, &retval, argc, args); + + zend_call_known_fcc(fcc, &retval, argc, args, NULL); if (identity != NULL) { zval_ptr_dtor(&args[1]); } - if (EG(exception)) { + if (Z_ISUNDEF(retval)) { ZVAL_UNDEF(result); return FAILURE; } @@ -1686,7 +1687,7 @@ static unsigned int php_openssl_psk_client_cb(SSL *ssl, const char *hint, php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t *)stream->abstract; if (sslsock == NULL || sslsock->psk_callbacks == NULL - || Z_ISUNDEF(sslsock->psk_callbacks->client_cb)) { + || !ZEND_FCC_INITIALIZED(sslsock->psk_callbacks->client_cb)) { return 0; } @@ -1737,7 +1738,7 @@ static unsigned int php_openssl_psk_server_cb(SSL *ssl, const char *identity, php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t *)stream->abstract; if (sslsock == NULL || sslsock->psk_callbacks == NULL - || Z_ISUNDEF(sslsock->psk_callbacks->server_cb)) { + || !ZEND_FCC_INITIALIZED(sslsock->psk_callbacks->server_cb)) { return 0; } @@ -1793,7 +1794,7 @@ static int php_openssl_psk_use_session_cb(SSL *ssl, const EVP_MD *md, php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t *)stream->abstract; if (sslsock == NULL || sslsock->psk_callbacks == NULL - || Z_ISUNDEF(sslsock->psk_callbacks->client_cb)) { + || !ZEND_FCC_INITIALIZED(sslsock->psk_callbacks->client_cb)) { return 1; } @@ -1860,7 +1861,7 @@ static int php_openssl_psk_find_session_cb(SSL *ssl, const unsigned char *identi php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t *)stream->abstract; if (sslsock == NULL || sslsock->psk_callbacks == NULL - || Z_ISUNDEF(sslsock->psk_callbacks->server_cb)) { + || !ZEND_FCC_INITIALIZED(sslsock->psk_callbacks->server_cb)) { return 1; } @@ -1896,9 +1897,10 @@ static int php_openssl_psk_find_session_cb(SSL *ssl, const unsigned char *identi /* PSK setup */ static zend_result php_openssl_validate_and_allocate_psk_callback( - php_openssl_netstream_data_t *sslsock, zval *callable, - const char *callback_name, bool is_persistent) + php_openssl_netstream_data_t *sslsock, const zval *callable, + bool is_client, bool is_persistent) { + const char *callback_name = is_client ? "psk_client_cb" : "psk_server_cb"; if (is_persistent) { php_error_docref(NULL, E_WARNING, "%s is not supported for persistent streams", callback_name); @@ -1906,7 +1908,8 @@ static zend_result php_openssl_validate_and_allocate_psk_callback( } char *is_callable_error = NULL; - if (!zend_is_callable_ex(callable, NULL, 0, NULL, NULL, &is_callable_error)) { + zend_fcall_info_cache fcc = {0}; + if (!zend_is_callable_ex(callable, NULL, 0, NULL, &fcc, &is_callable_error)) { if (is_callable_error) { zend_type_error("%s must be a valid callback, %s", callback_name, is_callable_error); @@ -1918,12 +1921,16 @@ static zend_result php_openssl_validate_and_allocate_psk_callback( } if (!sslsock->psk_callbacks) { - sslsock->psk_callbacks = (php_openssl_psk_callbacks_t *)pemalloc( - sizeof(php_openssl_psk_callbacks_t), is_persistent); - ZVAL_UNDEF(&sslsock->psk_callbacks->client_cb); - ZVAL_UNDEF(&sslsock->psk_callbacks->server_cb); + sslsock->psk_callbacks = (php_openssl_psk_callbacks_t *)pecalloc( + 1, sizeof(php_openssl_psk_callbacks_t), is_persistent); sslsock->psk_callbacks->refcount = 1; } + zend_fcc_addref(&fcc); + if (is_client) { + sslsock->psk_callbacks->client_cb = fcc; + } else { + sslsock->psk_callbacks->server_cb = fcc; + } return SUCCESS; } @@ -1938,12 +1945,10 @@ static zend_result php_openssl_setup_client_psk(php_stream *stream, } if (FAILURE == php_openssl_validate_and_allocate_psk_callback( - sslsock, val, "psk_client_cb", php_stream_is_persistent(stream))) { + sslsock, val, true, php_stream_is_persistent(stream))) { return FAILURE; } - ZVAL_COPY(&sslsock->psk_callbacks->client_cb, val); - #ifndef OPENSSL_NO_PSK SSL_CTX_set_psk_client_callback(sslsock->ctx, php_openssl_psk_client_cb); #endif @@ -1962,12 +1967,10 @@ static zend_result php_openssl_setup_server_psk(php_stream *stream, } if (FAILURE == php_openssl_validate_and_allocate_psk_callback( - sslsock, val, "psk_server_cb", php_stream_is_persistent(stream))) { + sslsock, val, false, php_stream_is_persistent(stream))) { return FAILURE; } - ZVAL_COPY(&sslsock->psk_callbacks->server_cb, val); - #ifndef OPENSSL_NO_PSK SSL_CTX_set_psk_server_callback(sslsock->ctx, php_openssl_psk_server_cb); #endif @@ -3092,8 +3095,13 @@ static int php_openssl_sockop_close(php_stream *stream, int close_handle) /* {{{ } if (sslsock->psk_callbacks && --sslsock->psk_callbacks->refcount == 0) { - zval_ptr_dtor(&sslsock->psk_callbacks->client_cb); - zval_ptr_dtor(&sslsock->psk_callbacks->server_cb); + if (ZEND_FCC_INITIALIZED(sslsock->psk_callbacks->client_cb)) { + zend_fcc_dtor(&sslsock->psk_callbacks->client_cb); + } + + if (ZEND_FCC_INITIALIZED(sslsock->psk_callbacks->server_cb)) { + zend_fcc_dtor(&sslsock->psk_callbacks->server_cb); + } pefree(sslsock->psk_callbacks, php_stream_is_persistent(stream)); } From 425cd3d6a15bb5fec5724a9f89130029bdd9aa4f Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Sat, 30 May 2026 14:42:02 +0200 Subject: [PATCH 4/4] ext/openssl: convert SESSION callback to FCC Prevent rechecking the given zval callable every single time --- ext/openssl/xp_ssl.c | 121 +++++++++++++++++++++++++------------------ 1 file changed, 71 insertions(+), 50 deletions(-) diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index f90d7549ae76..9486513fab74 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -190,9 +190,9 @@ typedef struct _php_openssl_psk_callbacks_t { /* Holds session callback */ typedef struct _php_openssl_session_callbacks_t { int refcount; - zval new_cb; - zval get_cb; - zval remove_cb; + zend_fcall_info_cache new_cb; + zend_fcall_info_cache get_cb; + zend_fcall_info_cache remove_cb; } php_openssl_session_callbacks_t; /* This implementation is very closely tied to the that of the native @@ -2004,15 +2004,11 @@ static int php_openssl_session_new_cb(SSL *ssl, SSL_SESSION *session) SSL_SESSION_up_ref(session); zval args[2]; - zval retval; ZVAL_RES(&args[0], stream->res); php_openssl_session_object_init(&args[1], session); - if (call_user_function(EG(function_table), NULL, &sslsock->session_callbacks->new_cb, - &retval, 2, args) == SUCCESS) { - zval_ptr_dtor(&retval); - } + zend_call_known_fcc(&sslsock->session_callbacks->new_cb, NULL, 2, args, NULL); zval_ptr_dtor(&args[1]); @@ -2045,22 +2041,21 @@ static SSL_SESSION *php_openssl_session_get_cb(SSL *ssl, const unsigned char *se SSL_SESSION *session = NULL; - if (call_user_function(EG(function_table), NULL, &sslsock->session_callbacks->get_cb, - &retval, 2, args) == SUCCESS) { - if (php_openssl_is_session_ce(&retval)) { - /* Get session from object and increment ref since OpenSSL will own it */ - php_openssl_session_object *obj = Z_OPENSSL_SESSION_P(&retval); - if (obj->session) { - SSL_SESSION_up_ref(obj->session); - session = obj->session; - } - } else if (Z_TYPE(retval) != IS_NULL) { - zend_type_error("session_get_cb return type must be null or OpenSSLSession"); + zend_call_known_fcc(&sslsock->session_callbacks->get_cb, &retval, 2, args, NULL); + zval_ptr_dtor(&args[1]); + + if (php_openssl_is_session_ce(&retval)) { + /* Get session from object and increment ref since OpenSSL will own it */ + php_openssl_session_object *obj = Z_OPENSSL_SESSION_P(&retval); + if (obj->session) { + SSL_SESSION_up_ref(obj->session); + session = obj->session; } + } else if (Z_TYPE(retval) != IS_NULL) { + zend_type_error("session_get_cb return type must be null or OpenSSLSession"); } zval_ptr_dtor(&retval); - zval_ptr_dtor(&args[1]); *copy = 0; return session; @@ -2085,28 +2080,41 @@ static void php_openssl_session_remove_cb(SSL_CTX *ctx, SSL_SESSION *session) const unsigned char *session_id = SSL_SESSION_get_id(session, &session_id_len); zval args[2]; - zval retval; ZVAL_RES(&args[0], stream->res); ZVAL_STRINGL(&args[1], (char *)session_id, session_id_len); - if (call_user_function(EG(function_table), NULL, &sslsock->session_callbacks->remove_cb, - &retval, 2, args) == SUCCESS) { - zval_ptr_dtor(&retval); - } - + zend_call_known_fcc(&sslsock->session_callbacks->remove_cb, NULL, 2, args, NULL); zval_ptr_dtor(&args[1]); } + +enum php_openssl_session_callback_type { + PHP_OPENSSL_NEW_CB, + PHP_OPENSSL_GET_CB, + PHP_OPENSSL_REMOVE_CB, +}; /** * Validate callable and allocate callback structure if needed. */ -static zend_result php_openssl_validate_and_allocate_callback( - php_openssl_netstream_data_t *sslsock, zval *callable, - const char *callback_name, bool is_persistent) +static zend_result php_openssl_validate_and_allocate_session_callback( + php_openssl_netstream_data_t *sslsock, const zval *callable, + enum php_openssl_session_callback_type cb_type, bool is_persistent) { - zend_fcall_info_cache fcc; char *is_callable_error = NULL; + const char *callback_name; + + switch (cb_type) { + case PHP_OPENSSL_NEW_CB: + callback_name = "session_new_cb"; + break; + case PHP_OPENSSL_GET_CB: + callback_name = "session_get_cb"; + break; + case PHP_OPENSSL_REMOVE_CB: + callback_name = "session_remove_cb"; + break; + } /* Callbacks not supported for persistent streams */ if (is_persistent) { @@ -2116,6 +2124,7 @@ static zend_result php_openssl_validate_and_allocate_callback( } /* Validate callable */ + zend_fcall_info_cache fcc; if (!zend_is_callable_ex(callable, NULL, 0, NULL, &fcc, &is_callable_error)) { if (is_callable_error) { zend_type_error("%s must be a valid callback, %s", callback_name, is_callable_error); @@ -2128,14 +2137,24 @@ static zend_result php_openssl_validate_and_allocate_callback( /* Allocate callback structure if not already allocated */ if (!sslsock->session_callbacks) { - sslsock->session_callbacks = (php_openssl_session_callbacks_t *)pemalloc( - sizeof(php_openssl_session_callbacks_t), is_persistent); - ZVAL_UNDEF(&sslsock->session_callbacks->new_cb); - ZVAL_UNDEF(&sslsock->session_callbacks->get_cb); - ZVAL_UNDEF(&sslsock->session_callbacks->remove_cb); + sslsock->session_callbacks = (php_openssl_session_callbacks_t *)pecalloc( + 1, sizeof(php_openssl_session_callbacks_t), is_persistent); sslsock->session_callbacks->refcount = 1; } + zend_fcc_addref(&fcc); + switch (cb_type) { + case PHP_OPENSSL_NEW_CB: + sslsock->session_callbacks->new_cb = fcc; + break; + case PHP_OPENSSL_GET_CB: + sslsock->session_callbacks->get_cb = fcc; + break; + case PHP_OPENSSL_REMOVE_CB: + sslsock->session_callbacks->remove_cb = fcc; + break; + } + return SUCCESS; } @@ -2159,12 +2178,11 @@ static zend_result php_openssl_setup_client_session(php_stream *stream, } if (GET_VER_OPT("session_new_cb")) { - if (FAILURE == php_openssl_validate_and_allocate_callback( - sslsock, val, "session_new_cb", is_persistent)) { + if (FAILURE == php_openssl_validate_and_allocate_session_callback( + sslsock, val, PHP_OPENSSL_NEW_CB, is_persistent)) { return FAILURE; } - ZVAL_COPY(&sslsock->session_callbacks->new_cb, val); SSL_CTX_sess_set_new_cb(sslsock->ctx, php_openssl_session_new_cb); enable_client_cache = true; } @@ -2207,11 +2225,10 @@ static zend_result php_openssl_setup_server_session(php_stream *stream, /* Check for session_get_cb first (determines cache mode) */ if (GET_VER_OPT("session_get_cb")) { - if (FAILURE == php_openssl_validate_and_allocate_callback( - sslsock, val, "session_new_cb", is_persistent)) { + if (FAILURE == php_openssl_validate_and_allocate_session_callback( + sslsock, val, PHP_OPENSSL_GET_CB, is_persistent)) { return FAILURE; } - ZVAL_COPY(&sslsock->session_callbacks->get_cb, val); has_get_cb = true; } @@ -2227,11 +2244,10 @@ static zend_result php_openssl_setup_server_session(php_stream *stream, /* Check for session_new_cb */ if (GET_VER_OPT("session_new_cb")) { - if (FAILURE == php_openssl_validate_and_allocate_callback( - sslsock, val, "session_new_cb", is_persistent)) { + if (FAILURE == php_openssl_validate_and_allocate_session_callback( + sslsock, val, PHP_OPENSSL_NEW_CB, is_persistent)) { return FAILURE; } - ZVAL_COPY(&sslsock->session_callbacks->new_cb, val); has_new_cb = true; if (!has_session_id_context && @@ -2249,12 +2265,11 @@ static zend_result php_openssl_setup_server_session(php_stream *stream, /* Check for session_remove_cb (optional) */ if (GET_VER_OPT("session_remove_cb")) { - if (FAILURE == php_openssl_validate_and_allocate_callback( - sslsock, val, "session_remove_cb", is_persistent)) { + if (FAILURE == php_openssl_validate_and_allocate_session_callback( + sslsock, val, PHP_OPENSSL_REMOVE_CB, is_persistent)) { return FAILURE; } - ZVAL_COPY(&sslsock->session_callbacks->remove_cb, val); has_remove_cb = true; } @@ -3088,9 +3103,15 @@ static int php_openssl_sockop_close(php_stream *stream, int close_handle) /* {{{ } if (sslsock->session_callbacks && --sslsock->session_callbacks->refcount == 0) { - zval_ptr_dtor(&sslsock->session_callbacks->new_cb); - zval_ptr_dtor(&sslsock->session_callbacks->get_cb); - zval_ptr_dtor(&sslsock->session_callbacks->remove_cb); + if (ZEND_FCC_INITIALIZED(sslsock->session_callbacks->new_cb)) { + zend_fcc_dtor(&sslsock->session_callbacks->new_cb); + } + if (ZEND_FCC_INITIALIZED(sslsock->session_callbacks->get_cb)) { + zend_fcc_dtor(&sslsock->session_callbacks->get_cb); + } + if (ZEND_FCC_INITIALIZED(sslsock->session_callbacks->remove_cb)) { + zend_fcc_dtor(&sslsock->session_callbacks->remove_cb); + } pefree(sslsock->session_callbacks, php_stream_is_persistent(stream)); }