From bafe8fedee4186985727d8fa5440559782235dfc Mon Sep 17 00:00:00 2001 From: ndossche <7771979+ndossche@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:14:51 +0200 Subject: [PATCH 1/2] Fix throwing behaviour with recursive arrays in VM Behaviour change in 82479e89d0 overlooked this VM type spec optimization. The JIT had a follow-up already in a0a8624346. --- Zend/zend_vm_def.h | 6 +-- Zend/zend_vm_execute.h | 8 ++-- .../is_identical_nothrow_recursive_array.phpt | 39 +++++++++++++++++++ 3 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 ext/opcache/tests/is_identical_nothrow_recursive_array.phpt diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index b0bda46f8c62..a5605f9a43c0 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -10022,11 +10022,11 @@ ZEND_VM_HOT_TYPE_SPEC_HANDLER(ZEND_IS_NOT_EQUAL|ZEND_IS_NOT_IDENTICAL, (op1_info ZEND_VM_SMART_BRANCH(result, 0); } -ZEND_VM_TYPE_SPEC_HANDLER(ZEND_IS_IDENTICAL, op->op1_type == IS_CV && (op->op2_type & (IS_CONST|IS_CV)) && !(op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) && !(op2_info & (MAY_BE_UNDEF|MAY_BE_REF)), ZEND_IS_IDENTICAL_NOTHROW, CV, CONST|CV, SPEC(COMMUTATIVE)) +ZEND_VM_TYPE_SPEC_HANDLER(ZEND_IS_IDENTICAL, op->op1_type == IS_CV && (op->op2_type & (IS_CONST|IS_CV)) && !(op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) && !(op2_info & (MAY_BE_UNDEF|MAY_BE_REF)) && !((op1_info & op2_info) & MAY_BE_ARRAY_OF_ARRAY), ZEND_IS_IDENTICAL_NOTHROW, CV, CONST|CV, SPEC(COMMUTATIVE)) { /* This is declared below the specializations for MAY_BE_LONG/MAY_BE_DOUBLE so those will be used instead if possible. */ /* This optimizes $x === SOME_CONST_EXPR and $x === $y for non-refs and non-undef, which can't throw. */ - /* (Infinite recursion when comparing arrays is an uncatchable fatal error) */ + /* Comparing two arrays that may contain arrays is excluded, as recursion throws an Error. */ USE_OPLINE zval *op1, *op2; bool result; @@ -10038,7 +10038,7 @@ ZEND_VM_TYPE_SPEC_HANDLER(ZEND_IS_IDENTICAL, op->op1_type == IS_CV && (op->op2_t ZEND_VM_SMART_BRANCH(result, 0); } -ZEND_VM_TYPE_SPEC_HANDLER(ZEND_IS_NOT_IDENTICAL, op->op1_type == IS_CV && (op->op2_type & (IS_CONST|IS_CV)) && !(op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) && !(op2_info & (MAY_BE_UNDEF|MAY_BE_REF)), ZEND_IS_NOT_IDENTICAL_NOTHROW, CV, CONST|CV, SPEC(COMMUTATIVE)) +ZEND_VM_TYPE_SPEC_HANDLER(ZEND_IS_NOT_IDENTICAL, op->op1_type == IS_CV && (op->op2_type & (IS_CONST|IS_CV)) && !(op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) && !(op2_info & (MAY_BE_UNDEF|MAY_BE_REF)) && !((op1_info & op2_info) & MAY_BE_ARRAY_OF_ARRAY), ZEND_IS_NOT_IDENTICAL_NOTHROW, CV, CONST|CV, SPEC(COMMUTATIVE)) { USE_OPLINE zval *op1, *op2; diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 44def91de748..f17f7bc3b313 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -45931,7 +45931,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_IS_IDENTICAL_NOTHROW_SPEC_CV_C { /* This is declared below the specializations for MAY_BE_LONG/MAY_BE_DOUBLE so those will be used instead if possible. */ /* This optimizes $x === SOME_CONST_EXPR and $x === $y for non-refs and non-undef, which can't throw. */ - /* (Infinite recursion when comparing arrays is an uncatchable fatal error) */ + /* Comparing two arrays that may contain arrays is excluded, as recursion throws an Error. */ USE_OPLINE zval *op1, *op2; bool result; @@ -55094,7 +55094,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_IS_IDENTICAL_NOTHROW_SPEC_CV_C { /* This is declared below the specializations for MAY_BE_LONG/MAY_BE_DOUBLE so those will be used instead if possible. */ /* This optimizes $x === SOME_CONST_EXPR and $x === $y for non-refs and non-undef, which can't throw. */ - /* (Infinite recursion when comparing arrays is an uncatchable fatal error) */ + /* Comparing two arrays that may contain arrays is excluded, as recursion throws an Error. */ USE_OPLINE zval *op1, *op2; bool result; @@ -68363,7 +68363,7 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t break; } spec = 2885 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; - } else if (op->op1_type == IS_CV && (op->op2_type & (IS_CONST|IS_CV)) && !(op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) && !(op2_info & (MAY_BE_UNDEF|MAY_BE_REF))) { + } else if (op->op1_type == IS_CV && (op->op2_type & (IS_CONST|IS_CV)) && !(op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) && !(op2_info & (MAY_BE_UNDEF|MAY_BE_REF)) && !((op1_info & op2_info) & MAY_BE_ARRAY_OF_ARRAY)) { spec = 3110 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; } break; @@ -68381,7 +68381,7 @@ ZEND_API void ZEND_FASTCALL zend_vm_set_opcode_handler_ex(zend_op* op, uint32_t break; } spec = 3035 | SPEC_RULE_OP1 | SPEC_RULE_OP2 | SPEC_RULE_SMART_BRANCH | SPEC_RULE_COMMUTATIVE; - } else if (op->op1_type == IS_CV && (op->op2_type & (IS_CONST|IS_CV)) && !(op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) && !(op2_info & (MAY_BE_UNDEF|MAY_BE_REF))) { + } else if (op->op1_type == IS_CV && (op->op2_type & (IS_CONST|IS_CV)) && !(op1_info & (MAY_BE_UNDEF|MAY_BE_REF)) && !(op2_info & (MAY_BE_UNDEF|MAY_BE_REF)) && !((op1_info & op2_info) & MAY_BE_ARRAY_OF_ARRAY)) { spec = 3115 | SPEC_RULE_OP2 | SPEC_RULE_COMMUTATIVE; } break; diff --git a/ext/opcache/tests/is_identical_nothrow_recursive_array.phpt b/ext/opcache/tests/is_identical_nothrow_recursive_array.phpt new file mode 100644 index 000000000000..d2bc4561f848 --- /dev/null +++ b/ext/opcache/tests/is_identical_nothrow_recursive_array.phpt @@ -0,0 +1,39 @@ +--TEST-- +IS_IDENTICAL/IS_NOT_IDENTICAL must not use the NOTHROW handler for arrays that may contain arrays +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +--EXTENSIONS-- +opcache +--FILE-- +getMessage(), "\n"; +} + +try { + not_identical($x, [[]]); +} catch (Throwable $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Error: Nesting level too deep - recursive dependency? +Error: Nesting level too deep - recursive dependency? From 56a80f70ff21220d6429bb59a29e27f17cc1cfe9 Mon Sep 17 00:00:00 2001 From: ndossche <7771979+ndossche@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:19:20 +0200 Subject: [PATCH 2/2] include line nr so that we verify the exception is thrown at the right point --- .../tests/is_identical_nothrow_recursive_array.phpt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ext/opcache/tests/is_identical_nothrow_recursive_array.phpt b/ext/opcache/tests/is_identical_nothrow_recursive_array.phpt index d2bc4561f848..a9dcd5b057a9 100644 --- a/ext/opcache/tests/is_identical_nothrow_recursive_array.phpt +++ b/ext/opcache/tests/is_identical_nothrow_recursive_array.phpt @@ -25,15 +25,15 @@ $x = [&$x]; try { identical($x, [[]]); } catch (Throwable $e) { - echo $e::class, ": ", $e->getMessage(), "\n"; + echo $e::class, ": ", $e->getMessage(), " on line ", $e->getLine(), "\n"; } try { not_identical($x, [[]]); } catch (Throwable $e) { - echo $e::class, ": ", $e->getMessage(), "\n"; + echo $e::class, ": ", $e->getMessage(), " on line ", $e->getLine(), "\n"; } ?> --EXPECT-- -Error: Nesting level too deep - recursive dependency? -Error: Nesting level too deep - recursive dependency? +Error: Nesting level too deep - recursive dependency? on line 3 +Error: Nesting level too deep - recursive dependency? on line 9