Commit ec30f4c15d6 for php
commit ec30f4c15d68c4dfc247e80dcbc107b9e8d73338
Author: Jorg Sowa <jorg.sowa@gmail.com>
Date: Thu Nov 5 12:12:46 2026 +0100
JIT: Fix OP_DATA leak when ASSIGN_DIM fails
Free OP_DATA when appending to a full array, and when converting a float
or resource key throws, matching the VM.
zend_jit_fetch_dim_w_helper() no longer frees OP_DATA, which
zend_jit_assign_dim_helper() frees again after converting false to an
array. Generated ASSIGN_DIM code frees it through a dedicated fetch
helper and inline on append failure.
Closes GH-24581
diff --git a/NEWS b/NEWS
index 6d29a2e891b..6fde7abcbc4 100644
--- a/NEWS
+++ b/NEWS
@@ -15,6 +15,7 @@ PHP NEWS
is exhausted while compiling side traces). (Ilia Alshanetsky)
. Fixed bug GH-25164 (Function JIT loop calling a user function runs once
or never terminates). (Ilia Alshanetsky)
+ . Fixed memory leak in JIT ASSIGN_DIM when the assignment fails. (Jorg Sowa)
19 Nov 2026, PHP 8.4.28
diff --git a/ext/opcache/jit/zend_jit_helpers.c b/ext/opcache/jit/zend_jit_helpers.c
index 9504b5bf79e..b52f2cc8363 100644
--- a/ext/opcache/jit/zend_jit_helpers.c
+++ b/ext/opcache/jit/zend_jit_helpers.c
@@ -922,10 +922,6 @@ static zval* ZEND_FASTCALL zend_jit_fetch_dim_w_helper(zend_array *ht, zval *dim
ZVAL_NULL(EX_VAR(opline->result.var));
}
}
- if (opline->opcode == ZEND_ASSIGN_DIM
- && ((opline+1)->op1_type & (IS_VAR | IS_TMP_VAR))) {
- zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var));
- }
return NULL;
}
ZEND_FALLTHROUGH;
@@ -1003,13 +999,6 @@ static zval* ZEND_FASTCALL zend_jit_fetch_dim_w_helper(zend_array *ht, zval *dim
default:
zend_illegal_container_offset(ZSTR_KNOWN(ZEND_STR_ARRAY), dim, BP_VAR_R);
undef_result_after_exception();
- if (EG(opline_before_exception)
- && (EG(opline_before_exception)+1)->opcode == ZEND_OP_DATA
- && ((EG(opline_before_exception)+1)->op1_type & (IS_VAR|IS_TMP_VAR))) {
- zend_execute_data *execute_data = EG(current_execute_data);
-
- zval_ptr_dtor_nogc(EX_VAR((EG(opline_before_exception)+1)->op1.var));
- }
return NULL;
}
@@ -1472,6 +1461,20 @@ static void ZEND_FASTCALL zend_jit_fetch_dim_obj_rw_helper(zval *object_ptr, zva
// zend_jit_fetch_dim_obj_helper(object_ptr, dim, result, BP_VAR_UNSET);
//}
+static zval* ZEND_FASTCALL zend_jit_assign_dim_fetch_w_helper(zend_array *ht, zval *dim)
+{
+ zval *retval = zend_jit_fetch_dim_w_helper(ht, dim);
+
+ if (UNEXPECTED(!retval)) {
+ zend_execute_data *execute_data = EG(current_execute_data);
+ /* Throwing replaces EX(opline) with HANDLE_EXCEPTION */
+ const zend_op *opline = EG(exception) ? EG(opline_before_exception) : EX(opline);
+
+ zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var));
+ }
+ return retval;
+}
+
static void ZEND_FASTCALL zend_jit_assign_dim_helper(zval *object_ptr, zval *dim, zval *value, zval *result)
{
if (EXPECTED(Z_TYPE_P(object_ptr) == IS_OBJECT)) {
diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c
index 1afdb79267c..1cdd92e1240 100644
--- a/ext/opcache/jit/zend_jit_ir.c
+++ b/ext/opcache/jit/zend_jit_ir.c
@@ -3139,6 +3139,7 @@ static void zend_jit_setup_disasm(void)
REGISTER_HELPER(zend_jit_fetch_dim_isset_helper);
REGISTER_HELPER(zend_jit_fetch_dim_rw_helper);
REGISTER_HELPER(zend_jit_fetch_dim_w_helper);
+ REGISTER_HELPER(zend_jit_assign_dim_fetch_w_helper);
REGISTER_HELPER(zend_jit_fetch_dim_str_offset_r_helper);
REGISTER_HELPER(zend_jit_fetch_dim_str_r_helper);
REGISTER_HELPER(zend_jit_fetch_dim_str_is_helper);
@@ -12437,7 +12438,11 @@ static int zend_jit_fetch_dimension_address_inner(zend_jit_ctx *jit,
ir_END_list(*end_inputs);
break;
case BP_VAR_W:
- ref = ir_CALL_2(IR_ADDR, ir_CONST_FC_FUNC(zend_jit_fetch_dim_w_helper), ht_ref, ref);
+ if (opline->opcode == ZEND_ASSIGN_DIM && ((opline+1)->op1_type & (IS_VAR|IS_TMP_VAR))) {
+ ref = ir_CALL_2(IR_ADDR, ir_CONST_FC_FUNC(zend_jit_assign_dim_fetch_w_helper), ht_ref, ref);
+ } else {
+ ref = ir_CALL_2(IR_ADDR, ir_CONST_FC_FUNC(zend_jit_fetch_dim_w_helper), ht_ref, ref);
+ }
if_found = ir_IF(ref);
ir_IF_TRUE(if_found);
ir_refs_add(found_inputs, ir_END());
@@ -13373,6 +13378,7 @@ static int zend_jit_assign_dim(zend_jit_ctx *jit,
// JIT: zend_throw_error(NULL, "Cannot add element to the array as the next element is already occupied");
jit_SET_EX_OPLINE(jit, opline);
ir_CALL(IR_VOID, jit_STUB_FUNC_ADDR(jit, jit_stub_cannot_add_element, IR_FASTCALL_FUNC));
+ jit_FREE_OP(jit, (opline+1)->op1_type, (opline+1)->op1, val_info, NULL);
ir_END_list(end_inputs);
diff --git a/ext/opcache/tests/jit/assign_dim_018.phpt b/ext/opcache/tests/jit/assign_dim_018.phpt
new file mode 100644
index 00000000000..3fee8d5a70a
--- /dev/null
+++ b/ext/opcache/tests/jit/assign_dim_018.phpt
@@ -0,0 +1,31 @@
+--TEST--
+JIT ASSIGN_DIM: OP_DATA is freed when appending to a full array fails
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_update_protection=0
+opcache.jit=function
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+class D {
+ function __destruct() {
+ echo "dtor\n";
+ }
+}
+
+function test() {
+ $a = [PHP_INT_MAX => 1];
+ $a[] = new D;
+}
+
+try {
+ test();
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+dtor
+Error: Cannot add element to the array as the next element is already occupied
diff --git a/ext/opcache/tests/jit/assign_dim_019.phpt b/ext/opcache/tests/jit/assign_dim_019.phpt
new file mode 100644
index 00000000000..284757362a2
--- /dev/null
+++ b/ext/opcache/tests/jit/assign_dim_019.phpt
@@ -0,0 +1,34 @@
+--TEST--
+JIT ASSIGN_DIM: OP_DATA is freed when a float key conversion throws
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_update_protection=0
+opcache.jit=function
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+class D {
+ function __destruct() {
+ echo "dtor\n";
+ }
+}
+
+set_error_handler(function ($errno, $errstr) {
+ throw new Exception($errstr);
+});
+
+function test() {
+ $a[1.5] = new D;
+}
+
+try {
+ test();
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+dtor
+Exception: Implicit conversion from float 1.5 to int loses precision
diff --git a/ext/opcache/tests/jit/assign_dim_020.phpt b/ext/opcache/tests/jit/assign_dim_020.phpt
new file mode 100644
index 00000000000..98f5f984280
--- /dev/null
+++ b/ext/opcache/tests/jit/assign_dim_020.phpt
@@ -0,0 +1,34 @@
+--TEST--
+JIT ASSIGN_DIM: OP_DATA is freed when a resource key conversion throws
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_update_protection=0
+opcache.jit=function
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+class D {
+ function __destruct() {
+ echo "dtor\n";
+ }
+}
+
+set_error_handler(function ($errno, $errstr) {
+ throw new Exception($errstr);
+});
+
+function test() {
+ $a[STDIN] = new D;
+}
+
+try {
+ test();
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+dtor
+Exception: Resource ID#%d used as offset, casting to integer (%d)
diff --git a/ext/opcache/tests/jit/assign_dim_021.phpt b/ext/opcache/tests/jit/assign_dim_021.phpt
new file mode 100644
index 00000000000..2d3c89c6707
--- /dev/null
+++ b/ext/opcache/tests/jit/assign_dim_021.phpt
@@ -0,0 +1,42 @@
+--TEST--
+JIT ASSIGN_DIM: OP_DATA is freed once when a float key conversion throws on a false container
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_update_protection=0
+opcache.jit=function
+zend.exception_ignore_args=1
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+class D {
+ function __destruct() {
+ echo "dtor\n";
+ }
+}
+
+set_error_handler(function ($errno, $errstr) {
+ if (!str_starts_with($errstr, "Automatic conversion of false")) {
+ throw new Exception($errstr);
+ }
+});
+
+function id($x) {
+ return $x;
+}
+
+function test($o) {
+ $a = false;
+ $a[1.5] = id($o);
+}
+
+try {
+ test(new D);
+} catch (Exception $e) {
+ echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+dtor
+Exception: Implicit conversion from float 1.5 to int loses precision
diff --git a/ext/opcache/tests/jit/assign_dim_022.phpt b/ext/opcache/tests/jit/assign_dim_022.phpt
new file mode 100644
index 00000000000..de263bcb00c
--- /dev/null
+++ b/ext/opcache/tests/jit/assign_dim_022.phpt
@@ -0,0 +1,42 @@
+--TEST--
+JIT ASSIGN_DIM: OP_DATA is freed once when a resource key conversion throws on a false container
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_update_protection=0
+opcache.jit=function
+zend.exception_ignore_args=1
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+class D {
+ function __destruct() {
+ echo "dtor\n";
+ }
+}
+
+set_error_handler(function ($errno, $errstr) {
+ if (!str_starts_with($errstr, "Automatic conversion of false")) {
+ throw new Exception($errstr);
+ }
+});
+
+function id($x) {
+ return $x;
+}
+
+function test($o) {
+ $a = false;
+ $a[STDIN] = id($o);
+}
+
+try {
+ test(new D);
+} catch (Exception $e) {
+ echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+dtor
+Exception: Resource ID#%d used as offset, casting to integer (%d)
diff --git a/ext/opcache/tests/jit/assign_dim_023.phpt b/ext/opcache/tests/jit/assign_dim_023.phpt
new file mode 100644
index 00000000000..a398ba9f487
--- /dev/null
+++ b/ext/opcache/tests/jit/assign_dim_023.phpt
@@ -0,0 +1,30 @@
+--TEST--
+JIT ASSIGN_DIM: OP_DATA is freed when the key is an illegal offset
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_update_protection=0
+opcache.jit=function
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+class D {
+ function __destruct() {
+ echo "dtor\n";
+ }
+}
+
+function test() {
+ $a[[]] = new D;
+}
+
+try {
+ test();
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+dtor
+TypeError: Cannot access offset of type array on array
diff --git a/ext/opcache/tests/jit/assign_dim_024.phpt b/ext/opcache/tests/jit/assign_dim_024.phpt
new file mode 100644
index 00000000000..2111a2d2dce
--- /dev/null
+++ b/ext/opcache/tests/jit/assign_dim_024.phpt
@@ -0,0 +1,34 @@
+--TEST--
+JIT ASSIGN_DIM: OP_DATA is freed when an undefined key warning throws
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_update_protection=0
+opcache.jit=function
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+class D {
+ function __destruct() {
+ echo "dtor\n";
+ }
+}
+
+set_error_handler(function ($errno, $errstr) {
+ throw new Exception($errstr);
+});
+
+function test() {
+ $a[$k] = new D;
+}
+
+try {
+ test();
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+dtor
+Exception: Undefined variable $k
diff --git a/ext/opcache/tests/jit/assign_dim_025.phpt b/ext/opcache/tests/jit/assign_dim_025.phpt
new file mode 100644
index 00000000000..0b5a48c1684
--- /dev/null
+++ b/ext/opcache/tests/jit/assign_dim_025.phpt
@@ -0,0 +1,34 @@
+--TEST--
+JIT ASSIGN_DIM: OP_DATA is freed when the array is destroyed by a float key notice
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_update_protection=0
+opcache.jit=function
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+class D {
+ function __destruct() {
+ echo "dtor\n";
+ }
+}
+
+set_error_handler(function () {
+ $GLOBALS["a"] = null;
+ return true;
+});
+
+function test() {
+ $GLOBALS["a"] = [];
+ $a = &$GLOBALS["a"];
+ $a[1.5] = new D;
+ var_dump($a);
+}
+
+test();
+?>
+--EXPECT--
+dtor
+NULL
diff --git a/ext/opcache/tests/jit/assign_dim_026.phpt b/ext/opcache/tests/jit/assign_dim_026.phpt
new file mode 100644
index 00000000000..005fd427d36
--- /dev/null
+++ b/ext/opcache/tests/jit/assign_dim_026.phpt
@@ -0,0 +1,34 @@
+--TEST--
+JIT ASSIGN_DIM: OP_DATA is freed when the array is destroyed by an undefined key warning
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_update_protection=0
+opcache.jit=function
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+class D {
+ function __destruct() {
+ echo "dtor\n";
+ }
+}
+
+set_error_handler(function () {
+ $GLOBALS["a"] = null;
+ return true;
+});
+
+function test() {
+ $GLOBALS["a"] = [];
+ $a = &$GLOBALS["a"];
+ $a[$k] = new D;
+ var_dump($a);
+}
+
+test();
+?>
+--EXPECT--
+dtor
+NULL
diff --git a/ext/opcache/tests/jit/assign_dim_027.phpt b/ext/opcache/tests/jit/assign_dim_027.phpt
new file mode 100644
index 00000000000..b516b431e4a
--- /dev/null
+++ b/ext/opcache/tests/jit/assign_dim_027.phpt
@@ -0,0 +1,32 @@
+--TEST--
+JIT ASSIGN_DIM: OP_DATA is freed when appending to a full array fails and the result is used
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_update_protection=0
+opcache.jit=function
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+class D {
+ function __destruct() {
+ echo "dtor\n";
+ }
+}
+
+function test() {
+ $a = [PHP_INT_MAX => 1];
+ $x = ($a[] = new D);
+ var_dump($x);
+}
+
+try {
+ test();
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+dtor
+Error: Cannot add element to the array as the next element is already occupied