Commit 59de8a86e5f for php

commit 59de8a86e5fddee959cfb1c9301d9e707f6555cb
Author: ndossche <7771979+ndossche@users.noreply.github.com>
Date:   Sat Oct 3 11:29:36 2026 +0200

    Fix escape analysis for objects implementing ArrayAccess

    Objects that implement ArrayAccess can run arbitrayr code on isset(),
    dimensions accesses etc. SCCP can then run on stale property values that
    were manipulated by such ArrayAccess handlers.
    We also take into accuont that we don't know the interfaces yet of
    unlinked classes.

    Closes GH-24089.

    Co-authored-by: Arnaud Le Blanc <arnaud.lb@gmail.com>

diff --git a/NEWS b/NEWS
index 7d58f7fa5db..a6c157395c9 100644
--- a/NEWS
+++ b/NEWS
@@ -18,6 +18,7 @@ PHP                                                                        NEWS
   . Fix throwing behaviour with recursive arrays in VM. (ndossche)
   . Fixed skipped catch block when switching fibers during exception unwinding
     (Nicolas Grekas)
+  . Fix escape analysis for objects implementing ArrayAccess. (ndossche, Arnaud)

 - DOM:
   . Fixed bug GH-23352 (UAF reading an attribute value node retained across
diff --git a/Zend/Optimizer/escape_analysis.c b/Zend/Optimizer/escape_analysis.c
index 3c2864d14fb..78e667b97f4 100644
--- a/Zend/Optimizer/escape_analysis.c
+++ b/Zend/Optimizer/escape_analysis.c
@@ -272,6 +272,25 @@ static bool is_local_def(zend_op_array *op_array, zend_ssa *ssa, int def, int va
 }
 /* }}} */

+/* ArrayAccess methods receive the object, so dimension accesses on it let it escape. */
+static bool may_be_array_access(const zend_ssa_var_info *info) /* {{{ */
+{
+	if (!(info->type & MAY_BE_OBJECT)) {
+		return 0;
+	}
+
+	const zend_class_entry *ce = info->ce;
+	if (!ce || info->is_instanceof) {
+		return 1;
+	}
+	if (ce->ce_flags & ZEND_ACC_LINKED) {
+		return ce->arrayaccess_funcs_ptr != NULL;
+	}
+	/* The interfaces of unlinked classes are not known yet. */
+	return ce->num_interfaces || ce->parent_name;
+}
+/* }}} */
+
 static bool is_escape_use(zend_op_array *op_array, zend_ssa *ssa, int use, int var) /* {{{ */
 {
 	zend_ssa_op *ssa_op = ssa->ops + use;
@@ -291,18 +310,22 @@ static bool is_escape_use(zend_op_array *op_array, zend_ssa *ssa, int use, int v
 				}
 				break;
 			case ZEND_ISSET_ISEMPTY_DIM_OBJ:
-			case ZEND_ISSET_ISEMPTY_PROP_OBJ:
 			case ZEND_FETCH_DIM_R:
-			case ZEND_FETCH_OBJ_R:
 			case ZEND_FETCH_DIM_IS:
+			case ZEND_ASSIGN_DIM_OP:
+			case ZEND_ASSIGN_DIM:
+				if (may_be_array_access(&ssa->var_info[ssa_op->op1_use])) {
+					return 1;
+				}
+				break;
+			case ZEND_ISSET_ISEMPTY_PROP_OBJ:
+			case ZEND_FETCH_OBJ_R:
 			case ZEND_FETCH_OBJ_IS:
 				break;
 			case ZEND_ASSIGN_OP:
 				return 1;
-			case ZEND_ASSIGN_DIM_OP:
 			case ZEND_ASSIGN_OBJ_OP:
 			case ZEND_ASSIGN_STATIC_PROP_OP:
-			case ZEND_ASSIGN_DIM:
 			case ZEND_ASSIGN_OBJ:
 				break;
 			case ZEND_ASSIGN_OBJ_REF:
diff --git a/ext/opcache/tests/opt/escape_analysis_array_access.phpt b/ext/opcache/tests/opt/escape_analysis_array_access.phpt
new file mode 100644
index 00000000000..bb4254e37aa
--- /dev/null
+++ b/ext/opcache/tests/opt/escape_analysis_array_access.phpt
@@ -0,0 +1,76 @@
+--TEST--
+Escape analysis: objects implementing ArrayAccess escape through dimension accesses
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.optimization_level=-1
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+#[AllowDynamicProperties]
+class AA implements ArrayAccess {
+    function offsetExists($o): bool { $this->x = 3; return true; }
+    function offsetGet($o): mixed { $this->x = 2; return 42; }
+    function offsetSet($o, $v): void { $this->x = 4; }
+    function offsetUnset($o): void {}
+
+    static function fetch() {
+        $o = new self;
+        $o->x = 1;
+        $o[0];
+        return $o->x;
+    }
+
+    static function isset() {
+        $o = new self;
+        $o->x = 1;
+        return [isset($o[0]), $o->x];
+    }
+
+    static function assign() {
+        $o = new self;
+        $o->x = 1;
+        $o[0] = 1;
+        return $o->x;
+    }
+}
+
+#[AllowDynamicProperties] class B {}
+#[AllowDynamicProperties]
+class BB implements ArrayAccess {
+    function offsetExists($o): bool {
+        return true;
+    }
+
+    function offsetGet($o): mixed {
+        $this->x = 2;
+        return 1;
+    }
+
+    function offsetSet($o, $v): void {}
+
+    function offsetUnset($o): void {}
+
+    static function f($c) {
+        if ($c) { $o = new B; $o->x = 1; } else { $o = new self; $o->x = 1; }
+        $o[0];
+        return $o->x;
+    }
+}
+
+var_dump(AA::fetch());
+var_dump(AA::isset());
+var_dump(AA::assign());
+var_dump(BB::f(false));
+?>
+--EXPECT--
+int(2)
+array(2) {
+  [0]=>
+  bool(true)
+  [1]=>
+  int(3)
+}
+int(4)
+int(2)