From 30ba0217d1a8726b90ce73412d7746089c37a914 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sat, 26 Sep 2026 18:15:08 -0400 Subject: [PATCH 1/7] ext/tidy: Reject tidyNode use after the document is reparsed tidyNode objects kept raw tidy pointers after parseString() replaced the document tree. Stamp each node with the document parse generation and reject use when the generations differ. Closes GH-23937 --- NEWS | 4 ++ ext/tidy/tests/reparse_node.phpt | 66 ++++++++++++++++++++++++++++++++ ext/tidy/tidy.c | 54 ++++++++++++++++++++------ 3 files changed, 113 insertions(+), 11 deletions(-) create mode 100644 ext/tidy/tests/reparse_node.phpt diff --git a/NEWS b/NEWS index a011c225aa69..4ffc2b8c0bcc 100644 --- a/NEWS +++ b/NEWS @@ -166,6 +166,10 @@ PHP NEWS lookup, and an unchecked CreateFileA() failure. (Ilia Alshanetsky) . Fix persistent stream context lifetime during shutdown (Levi Morrison) +- Tidy: + . Fixed a use-after-free when a tidyNode is used after its document is + reparsed. (Ilia Alshanetsky) + - XSL: . Fixed bug GH-23730 (use-after-free when XSLTProcessor::importStylesheet() is called during a transformation). (David Carlier) diff --git a/ext/tidy/tests/reparse_node.phpt b/ext/tidy/tests/reparse_node.phpt new file mode 100644 index 000000000000..7ba20b3939c7 --- /dev/null +++ b/ext/tidy/tests/reparse_node.phpt @@ -0,0 +1,66 @@ +--TEST-- +tidyNode objects are invalid after reparsing their document +--EXTENSIONS-- +tidy +--FILE-- +hasChildren(); + echo "unowned node: no error\n"; +} catch (Error $e) { + echo 'unowned node: ', $e::class, ': ', $e->getMessage(), "\n"; +} + +$tidy = tidy_parse_string('

one

two

'); +$node = $tidy->body()->child[0]; +var_dump($node->isHtml()); +var_dump($node->hasSiblings()); + +$tidy->parseString('

three

'); + +$operations = [ + 'string cast' => static fn() => (string) $node, + 'hasChildren' => static fn() => $node->hasChildren(), + 'hasSiblings' => static fn() => $node->hasSiblings(), + 'isComment' => static fn() => $node->isComment(), + 'isHtml' => static fn() => $node->isHtml(), + 'isText' => static fn() => $node->isText(), + 'isJste' => static fn() => $node->isJste(), + 'isAsp' => static fn() => $node->isAsp(), + 'isPhp' => static fn() => $node->isPhp(), + 'getParent' => static fn() => $node->getParent(), + 'getPreviousSibling' => static fn() => $node->getPreviousSibling(), + 'getNextSibling' => static fn() => $node->getNextSibling(), +]; + +foreach ($operations as $operation => $callback) { + try { + $callback(); + echo $operation, ": no error\n"; + } catch (Error $e) { + echo $operation, ': ', $e::class, ': ', $e->getMessage(), "\n"; + } +} + +var_dump($tidy->body()->child[0]->isHtml()); + +?> +--EXPECT-- +unowned node: Error: tidyNode object is not initialized +bool(true) +bool(true) +string cast: Error: tidyNode object is no longer valid after its document was reparsed +hasChildren: Error: tidyNode object is no longer valid after its document was reparsed +hasSiblings: Error: tidyNode object is no longer valid after its document was reparsed +isComment: Error: tidyNode object is no longer valid after its document was reparsed +isHtml: Error: tidyNode object is no longer valid after its document was reparsed +isText: Error: tidyNode object is no longer valid after its document was reparsed +isJste: Error: tidyNode object is no longer valid after its document was reparsed +isAsp: Error: tidyNode object is no longer valid after its document was reparsed +isPhp: Error: tidyNode object is no longer valid after its document was reparsed +getParent: Error: tidyNode object is no longer valid after its document was reparsed +getPreviousSibling: Error: tidyNode object is no longer valid after its document was reparsed +getNextSibling: Error: tidyNode object is no longer valid after its document was reparsed +bool(true) diff --git a/ext/tidy/tidy.c b/ext/tidy/tidy.c index b4af4ae811d6..4b9d0cb5d1c7 100644 --- a/ext/tidy/tidy.c +++ b/ext/tidy/tidy.c @@ -76,6 +76,11 @@ } \ obj = Z_TIDY_P(object); \ +#define TIDY_FETCH_VALID_NODE \ + TIDY_FETCH_ONLY_OBJECT; \ + if (tidy_node_validate(obj) != SUCCESS) { \ + RETURN_THROWS(); \ + } #define TIDY_SET_DEFAULT_CONFIG(_doc) \ if (TG(default_config) && TG(default_config)[0]) { \ php_tidy_load_config(_doc, TG(default_config)); \ @@ -102,12 +107,14 @@ struct _PHPTidyDoc { TidyDoc doc; TidyBuffer *errbuf; unsigned int ref_count; + size_t parse_generation; unsigned int initialized:1; }; struct _PHPTidyObj { TidyNode node; tidy_obj_type type; + size_t node_generation; PHPTidyDoc *ptdoc; zend_object std; }; @@ -302,12 +309,28 @@ static int _php_tidy_set_tidy_opt(TidyDoc doc, const char *optname, zval *value) return FAILURE; } +static zend_result tidy_node_validate(const PHPTidyObj *obj) +{ + if (!obj->ptdoc) { + zend_throw_error(NULL, "tidyNode object is not initialized"); + return FAILURE; + } + + if (obj->node_generation != obj->ptdoc->parse_generation) { + zend_throw_error(NULL, "tidyNode object is no longer valid after its document was reparsed"); + return FAILURE; + } + + return SUCCESS; +} + static void tidy_create_node_object(zval *zv, PHPTidyDoc *ptdoc, TidyNode node) { tidy_instantiate(tidy_ce_node, zv); PHPTidyObj *newobj = Z_TIDY_P(zv); newobj->node = node; newobj->type = is_node; + newobj->node_generation = ptdoc->parse_generation; newobj->ptdoc = ptdoc; newobj->ptdoc->ref_count++; tidy_add_node_default_properties(newobj); @@ -465,6 +488,7 @@ static zend_object *tidy_object_new(zend_class_entry *class_type, zend_object_ha intern->ptdoc = emalloc(sizeof(PHPTidyDoc)); intern->ptdoc->doc = tidyCreate(); intern->ptdoc->ref_count = 1; + intern->ptdoc->parse_generation = 0; intern->ptdoc->initialized = 0; intern->ptdoc->errbuf = emalloc(sizeof(TidyBuffer)); tidyBufInit(intern->ptdoc->errbuf); @@ -565,6 +589,9 @@ static zend_result tidy_node_cast_handler(zend_object *in, zval *out, int type) case IS_STRING: obj = php_tidy_fetch_object(in); + if (tidy_node_validate(obj) != SUCCESS) { + return FAILURE; + } tidyBufInit(&buf); if (obj->ptdoc && tidyNodeGetText(obj->ptdoc->doc, obj->node, &buf)) { ZVAL_STRINGL(out, (const char *) buf.bp, buf.size-1); @@ -621,6 +648,10 @@ static void tidy_add_node_default_properties(PHPTidyObj *obj) zval attribute, children, temp; const char *name; + if (tidy_node_validate(obj) != SUCCESS) { + return; + } + tidyBufInit(&buf); (void) tidyNodeGetText(obj->ptdoc->doc, obj->node, &buf); @@ -845,6 +876,7 @@ static int php_tidy_parse_string(PHPTidyObj *obj, const char *string, uint32_t l obj->ptdoc->initialized = 1; tidyBufInit(&buf); + obj->ptdoc->parse_generation++; tidyBufAttach(&buf, (byte *) string, len); if (tidyParseBuffer(obj->ptdoc->doc, &buf) < 0) { php_error_docref(NULL, E_WARNING, "%s", obj->ptdoc->errbuf->bp); @@ -1522,7 +1554,7 @@ PHP_FUNCTION(tidy_get_body) /* {{{ Returns true if this node has children */ PHP_METHOD(tidyNode, hasChildren) { - TIDY_FETCH_ONLY_OBJECT; + TIDY_FETCH_VALID_NODE; if (tidyGetChild(obj->node)) { RETURN_TRUE; @@ -1535,7 +1567,7 @@ PHP_METHOD(tidyNode, hasChildren) /* {{{ Returns true if this node has siblings */ PHP_METHOD(tidyNode, hasSiblings) { - TIDY_FETCH_ONLY_OBJECT; + TIDY_FETCH_VALID_NODE; if (obj->node && tidyGetNext(obj->node)) { RETURN_TRUE; @@ -1548,7 +1580,7 @@ PHP_METHOD(tidyNode, hasSiblings) /* {{{ Returns true if this node represents a comment */ PHP_METHOD(tidyNode, isComment) { - TIDY_FETCH_ONLY_OBJECT; + TIDY_FETCH_VALID_NODE; if (tidyNodeGetType(obj->node) == TidyNode_Comment) { RETURN_TRUE; @@ -1561,7 +1593,7 @@ PHP_METHOD(tidyNode, isComment) /* {{{ Returns true if this node is part of a HTML document */ PHP_METHOD(tidyNode, isHtml) { - TIDY_FETCH_ONLY_OBJECT; + TIDY_FETCH_VALID_NODE; switch (tidyNodeGetType(obj->node)) { case TidyNode_Start: @@ -1577,7 +1609,7 @@ PHP_METHOD(tidyNode, isHtml) /* {{{ Returns true if this node represents text (no markup) */ PHP_METHOD(tidyNode, isText) { - TIDY_FETCH_ONLY_OBJECT; + TIDY_FETCH_VALID_NODE; if (tidyNodeGetType(obj->node) == TidyNode_Text) { RETURN_TRUE; @@ -1590,7 +1622,7 @@ PHP_METHOD(tidyNode, isText) /* {{{ Returns true if this node is JSTE */ PHP_METHOD(tidyNode, isJste) { - TIDY_FETCH_ONLY_OBJECT; + TIDY_FETCH_VALID_NODE; if (tidyNodeGetType(obj->node) == TidyNode_Jste) { RETURN_TRUE; @@ -1603,7 +1635,7 @@ PHP_METHOD(tidyNode, isJste) /* {{{ Returns true if this node is ASP */ PHP_METHOD(tidyNode, isAsp) { - TIDY_FETCH_ONLY_OBJECT; + TIDY_FETCH_VALID_NODE; if (tidyNodeGetType(obj->node) == TidyNode_Asp) { RETURN_TRUE; @@ -1616,7 +1648,7 @@ PHP_METHOD(tidyNode, isAsp) /* {{{ Returns true if this node is PHP */ PHP_METHOD(tidyNode, isPhp) { - TIDY_FETCH_ONLY_OBJECT; + TIDY_FETCH_VALID_NODE; if (tidyNodeGetType(obj->node) == TidyNode_Php) { RETURN_TRUE; @@ -1629,7 +1661,7 @@ PHP_METHOD(tidyNode, isPhp) /* {{{ Returns the parent node if available or NULL */ PHP_METHOD(tidyNode, getParent) { - TIDY_FETCH_ONLY_OBJECT; + TIDY_FETCH_VALID_NODE; TidyNode parent_node = tidyGetParent(obj->node); if (parent_node) { @@ -1640,7 +1672,7 @@ PHP_METHOD(tidyNode, getParent) PHP_METHOD(tidyNode, getPreviousSibling) { - TIDY_FETCH_ONLY_OBJECT; + TIDY_FETCH_VALID_NODE; TidyNode previous_node = tidyGetPrev(obj->node); if (previous_node) { @@ -1650,7 +1682,7 @@ PHP_METHOD(tidyNode, getPreviousSibling) PHP_METHOD(tidyNode, getNextSibling) { - TIDY_FETCH_ONLY_OBJECT; + TIDY_FETCH_VALID_NODE; TidyNode next_node = tidyGetNext(obj->node); if (next_node) { From 03791b9dac0a1c55779e53204d36df23acde5c59 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Tue, 29 Sep 2026 17:11:55 +0100 Subject: [PATCH 2/7] Zend: use zend_is_callable_ex() in zend_execute.c --- Zend/zend_execute.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index ff3a5f0b3de9..9c414634a23e 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -1213,7 +1213,7 @@ static zend_always_inline bool zend_check_type_slow( const uint32_t type_mask = ZEND_TYPE_FULL_MASK(*type); if ((type_mask & MAY_BE_CALLABLE) && - zend_is_callable(arg, is_internal ? IS_CALLABLE_SUPPRESS_DEPRECATIONS : 0, NULL)) { + zend_is_callable_ex(arg, NULL, is_internal ? IS_CALLABLE_SUPPRESS_DEPRECATIONS : 0, NULL, NULL, NULL)) { return 1; } if ((type_mask & MAY_BE_STATIC) && zend_value_instanceof_static(arg)) { From dfbaf9e728d0b82f5e86580c49141742cf1f57f2 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Tue, 29 Sep 2026 17:14:56 +0100 Subject: [PATCH 3/7] Zend: change signature of zend_is_callable() The current signature is effectively useless every call uses the _ex variant to be able to grab the FCC and usually the error message. Something the current signature does not provide, and instead offers parameters that most call sites don't care about. Closes GH-23996 --- UPGRADING.INTERNALS | 4 ++++ Zend/zend_API.h | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index 82f273974cd5..24f6a1b96674 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -29,6 +29,10 @@ PHP 8.7 INTERNALS UPGRADE NOTES and C23, which means that ZEND_NORETURN needs to come first in the modifier list. Consider using noreturn (C11) / [[noreturn]] (C23) directly if header interoperability is not required. +- The signature of zend_is_callable() has been changed to + zend_is_callable(const zval *callable, zend_fcall_info_cache *fcc, char **error) + from zend_is_callable(const zval *callable, uint32_t check_flags, zend_string **callable_name). + This new signature covers the most common parameters used with zend_is_callable_ex(). ======================== 2. Build system changes diff --git a/Zend/zend_API.h b/Zend/zend_API.h index 5c4083052202..c94a71cf56b8 100644 --- a/Zend/zend_API.h +++ b/Zend/zend_API.h @@ -422,9 +422,9 @@ ZEND_API bool zend_is_callable_at_frame( const zval *callable, zend_object *object, const zend_execute_data *frame, uint32_t check_flags, zend_fcall_info_cache *fcc, char **error); ZEND_API bool zend_is_callable_ex(const zval *callable, zend_object *object, uint32_t check_flags, zend_string **callable_name, zend_fcall_info_cache *fcc, char **error); -static zend_always_inline bool zend_is_callable(const zval *callable, uint32_t check_flags, zend_string **callable_name) +static zend_always_inline bool zend_is_callable(const zval *callable, zend_fcall_info_cache *fcc, char **error) { - return zend_is_callable_ex(callable, NULL, check_flags, callable_name, NULL, NULL); + return zend_is_callable_ex(callable, NULL, 0, NULL, fcc, error); } ZEND_API const char *zend_get_module_version(const char *module_name); From 184402b670490735111363509fd810e42f327b71 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Tue, 29 Sep 2026 17:36:36 +0100 Subject: [PATCH 4/7] Zend: replace zend_is_callable_ex with non-ex version As the new signature is actually useful now --- Zend/zend_closures.c | 2 +- Zend/zend_vm_def.h | 4 ++-- Zend/zend_vm_execute.h | 24 ++++++++++++------------ ext/dom/xpath_callbacks.c | 4 ++-- ext/ffi/ffi.c | 2 +- ext/openssl/xp_ssl.c | 6 +++--- ext/pcntl/pcntl.c | 2 +- ext/pcre/php_pcre.c | 2 +- ext/pdo/pdo_stmt.c | 2 +- ext/reflection/php_reflection.c | 2 +- ext/standard/streamsfuncs.c | 2 +- main/streams/stream_errors.c | 2 +- 12 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Zend/zend_closures.c b/Zend/zend_closures.c index d4c0b5881369..9d5971fac479 100644 --- a/Zend/zend_closures.c +++ b/Zend/zend_closures.c @@ -399,7 +399,7 @@ static zend_result zend_create_closure_from_callable(zval *return_value, zval *c zend_function *mptr; zend_internal_function call; - if (!zend_is_callable_ex(callable, NULL, 0, NULL, &fcc, error)) { + if (!zend_is_callable(callable, &fcc, error)) { return FAILURE; } diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 9c9e262169f8..d1355208c287 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -3985,10 +3985,10 @@ ZEND_VM_HANDLER(118, ZEND_INIT_USER_CALL, CONST, CONST|TMP|CV, NUM) SAVE_OPLINE(); function_name = GET_OP2_ZVAL_PTR(BP_VAR_R); - if (zend_is_callable_ex(function_name, NULL, 0, NULL, &fcc, &error)) { + if (zend_is_callable(function_name, &fcc, &error)) { ZEND_ASSERT(!error); - /* Deprecation can be emitted from zend_is_callable_ex(), which can + /* Deprecation can be emitted from zend_is_callable(), which can * invoke a user error handler and throw an exception. * For the CONST and CV case we reuse the same exception block below * to make sure we don't increase VM size too much. */ diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 942f18eb7040..fe2df85c23e3 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -7668,10 +7668,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INIT_USER_CAL SAVE_OPLINE(); function_name = RT_CONSTANT(opline, opline->op2); - if (zend_is_callable_ex(function_name, NULL, 0, NULL, &fcc, &error)) { + if (zend_is_callable(function_name, &fcc, &error)) { ZEND_ASSERT(!error); - /* Deprecation can be emitted from zend_is_callable_ex(), which can + /* Deprecation can be emitted from zend_is_callable(), which can * invoke a user error handler and throw an exception. * For the CONST and CV case we reuse the same exception block below * to make sure we don't increase VM size too much. */ @@ -10444,10 +10444,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INIT_USER_CAL SAVE_OPLINE(); function_name = _get_zval_ptr_tmp(opline->op2.var EXECUTE_DATA_CC); - if (zend_is_callable_ex(function_name, NULL, 0, NULL, &fcc, &error)) { + if (zend_is_callable(function_name, &fcc, &error)) { ZEND_ASSERT(!error); - /* Deprecation can be emitted from zend_is_callable_ex(), which can + /* Deprecation can be emitted from zend_is_callable(), which can * invoke a user error handler and throw an exception. * For the CONST and CV case we reuse the same exception block below * to make sure we don't increase VM size too much. */ @@ -13108,10 +13108,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_INIT_USER_CAL SAVE_OPLINE(); function_name = _get_zval_ptr_cv_BP_VAR_R(opline->op2.var EXECUTE_DATA_CC); - if (zend_is_callable_ex(function_name, NULL, 0, NULL, &fcc, &error)) { + if (zend_is_callable(function_name, &fcc, &error)) { ZEND_ASSERT(!error); - /* Deprecation can be emitted from zend_is_callable_ex(), which can + /* Deprecation can be emitted from zend_is_callable(), which can * invoke a user error handler and throw an exception. * For the CONST and CV case we reuse the same exception block below * to make sure we don't increase VM size too much. */ @@ -60529,10 +60529,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_USER_CALL_SPE SAVE_OPLINE(); function_name = RT_CONSTANT(opline, opline->op2); - if (zend_is_callable_ex(function_name, NULL, 0, NULL, &fcc, &error)) { + if (zend_is_callable(function_name, &fcc, &error)) { ZEND_ASSERT(!error); - /* Deprecation can be emitted from zend_is_callable_ex(), which can + /* Deprecation can be emitted from zend_is_callable(), which can * invoke a user error handler and throw an exception. * For the CONST and CV case we reuse the same exception block below * to make sure we don't increase VM size too much. */ @@ -63305,10 +63305,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_USER_CALL_SPE SAVE_OPLINE(); function_name = _get_zval_ptr_tmp(opline->op2.var EXECUTE_DATA_CC); - if (zend_is_callable_ex(function_name, NULL, 0, NULL, &fcc, &error)) { + if (zend_is_callable(function_name, &fcc, &error)) { ZEND_ASSERT(!error); - /* Deprecation can be emitted from zend_is_callable_ex(), which can + /* Deprecation can be emitted from zend_is_callable(), which can * invoke a user error handler and throw an exception. * For the CONST and CV case we reuse the same exception block below * to make sure we don't increase VM size too much. */ @@ -65867,10 +65867,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_INIT_USER_CALL_SPE SAVE_OPLINE(); function_name = _get_zval_ptr_cv_BP_VAR_R(opline->op2.var EXECUTE_DATA_CC); - if (zend_is_callable_ex(function_name, NULL, 0, NULL, &fcc, &error)) { + if (zend_is_callable(function_name, &fcc, &error)) { ZEND_ASSERT(!error); - /* Deprecation can be emitted from zend_is_callable_ex(), which can + /* Deprecation can be emitted from zend_is_callable(), which can * invoke a user error handler and throw an exception. * For the CONST and CV case we reuse the same exception block below * to make sure we don't increase VM size too much. */ diff --git a/ext/dom/xpath_callbacks.c b/ext/dom/xpath_callbacks.c index e4ad6c59b3d1..e0f75fbf6a7c 100644 --- a/ext/dom/xpath_callbacks.c +++ b/ext/dom/xpath_callbacks.c @@ -204,7 +204,7 @@ static zend_result php_dom_xpath_callback_ns_update_method_handler( ZEND_HASH_FOREACH_STR_KEY_VAL(callable_ht, key, entry) { zend_fcall_info_cache* fcc = emalloc(sizeof(*fcc)); char *error; - if (!zend_is_callable_ex(entry, NULL, 0, NULL, fcc, &error)) { + if (!zend_is_callable(entry, fcc, &error)) { zend_argument_type_error(1, "must be an array with valid callbacks as values, %s", error); efree(fcc); efree(error); @@ -251,7 +251,7 @@ static zend_result php_dom_xpath_callback_ns_update_method_handler( char *error; zval tmp; ZVAL_STR(&tmp, name); - if (!zend_is_callable_ex(&tmp, NULL, 0, NULL, fcc, &error)) { + if (!zend_is_callable(&tmp, fcc, &error)) { zend_argument_type_error(1, "must be a callable, %s", error); efree(fcc); efree(error); diff --git a/ext/ffi/ffi.c b/ext/ffi/ffi.c index 37a0a215262b..0f6e7626d834 100644 --- a/ext/ffi/ffi.c +++ b/ext/ffi/ffi.c @@ -1006,7 +1006,7 @@ static void *zend_ffi_create_callback(zend_ffi_type *type, zval *value) /* {{{ * return NULL; } - if (!zend_is_callable_ex(value, NULL, 0, NULL, &fcc, &error)) { + if (!zend_is_callable(value, &fcc, &error)) { zend_throw_error(zend_ffi_exception_ce, "Attempt to assign an invalid callback, %s", error); return NULL; } diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 0466dd4bf653..854227318da0 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -2007,7 +2007,7 @@ static zend_result php_openssl_validate_and_allocate_psk_callback( char *is_callable_error = NULL; zend_fcall_info_cache fcc = {0}; - if (!zend_is_callable_ex(callable, NULL, 0, NULL, &fcc, &is_callable_error)) { + if (!zend_is_callable(callable, &fcc, &is_callable_error)) { if (is_callable_error) { zend_type_error("%s must be a valid callback, %s", callback_name, is_callable_error); @@ -2130,7 +2130,7 @@ static zend_result php_openssl_setup_server_early_data(php_stream *stream, char *is_callable_error = NULL; zend_fcall_info_cache fcc = {0}; - if (!zend_is_callable_ex(val, NULL, 0, NULL, &fcc, &is_callable_error)) { + if (!zend_is_callable(val, &fcc, &is_callable_error)) { if (is_callable_error) { zend_type_error("early_data_cb must be a valid callback, %s", is_callable_error); efree(is_callable_error); @@ -2318,7 +2318,7 @@ static zend_result php_openssl_validate_and_allocate_session_callback( /* Validate callable */ zend_fcall_info_cache fcc; - if (!zend_is_callable_ex(callable, NULL, 0, NULL, &fcc, &is_callable_error)) { + if (!zend_is_callable(callable, &fcc, &is_callable_error)) { if (is_callable_error) { zend_type_error("%s must be a valid callback, %s", callback_name, is_callable_error); efree(is_callable_error); diff --git a/ext/pcntl/pcntl.c b/ext/pcntl/pcntl.c index c73478fe5835..0a40bd0f2dfe 100644 --- a/ext/pcntl/pcntl.c +++ b/ext/pcntl/pcntl.c @@ -842,7 +842,7 @@ PHP_FUNCTION(pcntl_signal) RETURN_TRUE; } - if (!zend_is_callable_ex(handle, NULL, 0, NULL, NULL, NULL)) { + if (!zend_is_callable(handle, NULL, NULL)) { PCNTL_G(last_error) = EINVAL; zend_argument_type_error(2, "must be of type callable|int, %s given", zend_zval_value_name(handle)); diff --git a/ext/pcre/php_pcre.c b/ext/pcre/php_pcre.c index 4c63ab0920ae..fce1a21a6c29 100644 --- a/ext/pcre/php_pcre.c +++ b/ext/pcre/php_pcre.c @@ -2459,7 +2459,7 @@ PHP_FUNCTION(preg_replace_callback_array) /* Copy potential trampoline */ ZVAL_COPY_VALUE(&fci.function_name, replace); - if (!zend_is_callable_ex(replace, NULL, 0, NULL, &fcc, NULL)) { + if (!zend_is_callable(replace, &fcc, NULL)) { zend_argument_type_error(1, "must contain only valid callbacks"); goto error; } diff --git a/ext/pdo/pdo_stmt.c b/ext/pdo/pdo_stmt.c index 9774b197b7d9..4b56dbdbbd59 100644 --- a/ext/pdo/pdo_stmt.c +++ b/ext/pdo/pdo_stmt.c @@ -1116,7 +1116,7 @@ static bool pdo_get_fcc_from_zval(zend_fcall_info_cache *fcc, zval *callable) { } char *is_callable_error = NULL; - if (!zend_is_callable_ex(callable, NULL, 0, NULL, fcc, &is_callable_error)) { + if (!zend_is_callable(callable, fcc, &is_callable_error)) { if (is_callable_error) { zend_type_error("%s", is_callable_error); efree(is_callable_error); diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 7552b00c1151..b1968d9ff118 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -5142,7 +5142,7 @@ void reflection_class_new_lazy(INTERNAL_FUNCTION_PARAMETERS, /* Call trampoline has been cleared by zpp. Refetch it, because we want to deal * with it ourselves. It is important that it is not refetched on every call, * because calls may occur from different scopes. */ - zend_is_callable_ex(&fci.function_name, NULL, 0, NULL, &fcc, NULL); + zend_is_callable(&fci.function_name, &fcc, NULL); } obj = zend_object_make_lazy(obj, ce, &fci.function_name, &fcc, diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index 90341109210f..6502ac279e26 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -967,7 +967,7 @@ static zend_result parse_context_params(php_stream_context *context, const HashT zend_fcall_info_cache *fcc = emalloc(sizeof(*fcc)); char *error; - if (!zend_is_callable_ex(tmp, NULL, 0, NULL, fcc, &error)) { + if (!zend_is_callable(tmp, fcc, &error)) { zend_argument_type_error(1, "must be an array with valid callbacks as values, %s", error); efree(fcc); efree(error); diff --git a/main/streams/stream_errors.c b/main/streams/stream_errors.c index e16d3425a4d3..788dceff445b 100644 --- a/main/streams/stream_errors.c +++ b/main/streams/stream_errors.c @@ -369,7 +369,7 @@ static void php_stream_call_error_handler(const zval *handler, zval *errors_arra zend_fcall_info_cache fcc; char *is_callable_error = NULL; - if (!zend_is_callable_ex(handler, NULL, 0, NULL, &fcc, &is_callable_error)) { + if (!zend_is_callable(handler, &fcc, &is_callable_error)) { if (is_callable_error) { zend_type_error("stream error handler must be a valid callback, %s", is_callable_error); efree(is_callable_error); From 256c6284142027d29ea1ca65737b4cba7f5ca971 Mon Sep 17 00:00:00 2001 From: Jordi Kroon Date: Mon, 28 Sep 2026 19:40:11 +0000 Subject: [PATCH 5/7] Document missing deprecation entries for PHP 8.6 (#23972) Document the identifier deprecations introduced by #23615 and #23709 in UPGRADING and NEWS so they are covered by the PHP 8.6 migration guide. Cover the additional uses of "_" and the deprecation of "let" and "is" as identifiers. Place the NEWS entries in the current PHP 8.6 release section. Closes #23972 --- NEWS | 4 ++++ UPGRADING | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/NEWS b/NEWS index 2bc21110f911..cd01751fc78b 100644 --- a/NEWS +++ b/NEWS @@ -22,6 +22,10 @@ PHP NEWS . Fixed GH-23921 (Fibers start with error_reporting = 0 when the error_reporting INI directive is not set). (Girgias) . Fixed GH-23980 (ZEND_ASSERT violation @ ZEND_INCLUDE_OR_EVAL). (ndossche) + . Deprecated using "let" or "is" as the name of a class, function, or + constant, and as an alias in class_alias() and use declarations. (Girgias) + . Deprecated using "_" as a constant name and as an alias for a class or + constant in use declarations. (Girgias) - FFI: . Fixed crashes with FFI callbacks created from __call() trampolines diff --git a/UPGRADING b/UPGRADING index 63a1f5f30fbc..0e4a5288d680 100644 --- a/UPGRADING +++ b/UPGRADING @@ -555,6 +555,16 @@ PHP 8.6 UPGRADE NOTES RFC: https://wiki.php.net/rfc/deprecate-return-value-from-construct . Naming a function readonly is now deprecated. RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_the_possibility_to_name_a_function_readonly + . Using "let" or "is" as the name of a class, interface, trait, enum, + function, or constant (including via define()), as the alias passed to + class_alias(), or as the alias name in a use, use function, or use const + declaration is now deprecated. + RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_using_let_as_an_identifier + RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_using_is_as_an_identifier + . Using "_" as a constant name (via const or define()), or as the alias + name for a class or constant in a use declaration, is now deprecated. + This extends the PHP 8.4 deprecation of "_" as a class name. + RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_using_as_a_constant_and_compile_time_alias . Passing a 3rd argument to define() is now deprecated. RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_define_with_case_insensitive_being_specified . Calling is_a() or is_subclass_of() with a string as the first argument From 2cea4a358d0193e7afad630258f39e73d0977d27 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Sun, 27 Sep 2026 21:13:29 -0700 Subject: [PATCH 6/7] ext/gd: fix undefined behavior with GIFs with problematic LZW compression data Apply the changes from libgd/libgd@9fa3abd2e61da18ed2b889704e4e252f0f5a95fe in order to fix undefined behavior from out-of-bounds reads when creating a GIF with problematic LZW compression data. --- ext/gd/libgd/gd_gif_in.c | 8 +++++ ext/gd/tests/gif-oob.phpt | 63 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 ext/gd/tests/gif-oob.phpt diff --git a/ext/gd/libgd/gd_gif_in.c b/ext/gd/libgd/gd_gif_in.c index 14c27f3293cc..f2d9981688a8 100644 --- a/ext/gd/libgd/gd_gif_in.c +++ b/ext/gd/libgd/gd_gif_in.c @@ -517,12 +517,20 @@ LWZReadByte_(gdIOCtx *fd, LZW_STATIC_DATA *sd, char flag, int input_code_size, i /* Bad compressed data stream */ return -1; } + if(code >= (1 << MAX_LWZ_BITS)) { + /* Corrupted code */ + return -1; + } *sd->sp++ = sd->table[1][code]; if (code == sd->table[0][code]) { /* Oh well */ } code = sd->table[0][code]; } + if(code >= (1 << MAX_LWZ_BITS)) { + /* Corrupted code */ + return -1; + } *sd->sp++ = sd->firstcode = sd->table[1][code]; diff --git a/ext/gd/tests/gif-oob.phpt b/ext/gd/tests/gif-oob.phpt new file mode 100644 index 000000000000..73fd0427c8e8 --- /dev/null +++ b/ext/gd/tests/gif-oob.phpt @@ -0,0 +1,63 @@ +--TEST-- +GIF OOB array access when using code size of 12 +--EXTENSIONS-- +gd +--FILE-- + "GIF", + "version" => "89a", +]; +$fileHeader = implode("", $fileHeaderParts); + +$logicalScreenDescriptorParts = [ + // little-endian format + "width" => "\x04\x00", + "height" => "\x04\x00", + // packed data: global color table flag (most significant bit), + // color resolution (3 bits, only meaningful if global color table is enabled + // and we don't enable it here) + // sort flag (one bit, again only meaningful if global color table is enabled) + // size of global color table (3 bits) + "packed_info" => "\x00", + "background_color_index" => "\x00", + "pixel_aspect_ratio" => "\x00", +]; +$logicalScreenDescriptor = implode("", $logicalScreenDescriptorParts); + +$startImage = ","; + +$imgDescParts = [ + // little-ending format + "left" => "\x00\x00", + "top" => "\x00\x00", + "width" => "\x04\x00", + "height" => "\x04\x00", + // more packed data: local color table flag, interlace flag, sort flag, + // 2 bits reserved for future use, then 3 bits for size of local color + // table, which we don't have + "packed_info" => "\x00", +]; +$imgDesc = implode("", $imgDescParts); + +$imgDataParts = [ + "lzw_min_code_size" => "\x0c", // 12 + "sub_block_num_bytes" => "\x05", + // Data in the block: 3 12-bit codes, and then 4 trailing 0 bits + "sub_block_bytes" => "\xff\x5f\x00\x06\x40", + // end of data + "end" => "\x00" +]; +$imgData = implode("", $imgDataParts); + +$trailer = ";"; + +$source = $fileHeader . $logicalScreenDescriptor . $startImage . $imgDesc . $imgData . $trailer; +$img = imagecreatefromstring($source); +var_dump($img); + +?> +--EXPECTF-- +object(GdImage)#%d (0) { +} From c8d62b8a0b2ee50c487f958bf2ada75659fe41b3 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Mon, 28 Sep 2026 16:35:17 -0700 Subject: [PATCH 7/7] NEWS --- NEWS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS b/NEWS index 4ffc2b8c0bcc..40236c24a008 100644 --- a/NEWS +++ b/NEWS @@ -50,6 +50,8 @@ PHP NEWS - GD: . Fixed undefined behavior with GD2 images with `INT_MAX`-sized chunks. (DanielEScherzer, cmb) + . Fixed undefined behavior with GIFs with problematic LZW compression data. + (DanielEScherzer, vapier) - Intl: . Fixed cloning IntlDateFormatter and MessageFormatter losing PHP-side state