Commit 57096b612d1 for php.net

commit 57096b612d1f5636b89592b0092cc477c94226c7
Author: David Carlier <devnexen@gmail.com>
Date:   Tue Jun 9 16:02:00 2026 +0100

    Zend: Fix GH-22257 type confusion in Exception::getTraceAsString().

    A crafted, deliberately truncated unserialize() payload can leave
    Exception::$trace holding a non-array value, since the typed-property
    check is skipped on the parse failure path. getTraceAsString() then
    reinterpreted the object as a HashTable, causing an out-of-bounds read.
    Guard against a non-array trace and return an empty string instead.

    Fix #22257

    close GH-22263

diff --git a/NEWS b/NEWS
index 2462f0a73cc..9df461452c1 100644
--- a/NEWS
+++ b/NEWS
@@ -34,6 +34,8 @@ PHP                                                                        NEWS
     built extensions under AddressSanitizer). (iliaal)
   . TSRM: use local-exec TLS in PIE executables. (henderkes)
   . perf: make all static extensions use TSRMG_STATIC. (henderkes)
+  . Fixed bug GH-22257 (type confusion in Exception::getTraceAsString()).
+    (David Carlier)

 - BCMath:
   . Added NUL-byte validation to BCMath functions. (jorgsowa)
diff --git a/Zend/tests/gh22257.phpt b/Zend/tests/gh22257.phpt
new file mode 100644
index 00000000000..9bbf3b1f8d3
--- /dev/null
+++ b/Zend/tests/gh22257.phpt
@@ -0,0 +1,38 @@
+--TEST--
+GH-22257 (Type confusion / OOB read unserializing an Exception with a non-array trace)
+--CREDITS--
+Igor Sak-Sakovskiy (Positive Technologies)
+--FILE--
+<?php
+/* A crafted, deliberately truncated payload makes the nested value of the typed
+ * "array $trace" property fail to unserialize. On that failure path the slot used
+ * to keep the half-built (non-array) value, and the partially-built Exception was
+ * then exposed to getTraceAsString() through SplHeap's delayed __unserialize(),
+ * type-confusing the object as a HashTable. The slot is now reset to the property
+ * default, so the run completes without an out-of-bounds read. */
+$n = "\x00";
+try {
+    unserialize(
+        'O:9:"Exception":1:{s:16:"' . $n . 'Exception' . $n . 'trace";' .
+        'O:8:"stdClass":2:{s:1:"0";O:10:"SplMaxHeap":2:{i:0;a:0:{}i:1;a:2:{' .
+        's:5:"flags";i:0;s:13:"heap_elements";a:2:{i:0;s:0:"";i:1;R:1;}}}z}}'
+    );
+} catch (\Throwable $e) {
+    for (; $e; $e = $e->getPrevious()) {
+        printf("%s: %s\n", $e::class, $e->getMessage());
+    }
+}
+
+/* By-ref type violation: the slot is reset to the property default. */
+class Test { public int $i; public array $a; }
+try {
+    var_dump(unserialize('O:4:"Test":2:{s:1:"i";N;s:1:"a";R:2;}'));
+} catch (\Throwable $e) {
+    printf("%s: %s\n", $e::class, $e->getMessage());
+}
+echo "OK\n";
+?>
+--EXPECTF--
+Warning: unserialize(): Error at offset %d of %d bytes in %s on line %d
+TypeError: Cannot assign null to property Test::$i of type int
+OK
diff --git a/ext/standard/var_unserializer.re b/ext/standard/var_unserializer.re
index 484cb5aa8fc..4a9b278c116 100644
--- a/ext/standard/var_unserializer.re
+++ b/ext/standard/var_unserializer.re
@@ -152,6 +152,17 @@ static zend_never_inline void var_push_dtor_value(php_unserialize_data_t *var_ha
 	}
 }

+static zend_always_inline void var_restore_prop_default(php_unserialize_data_t *var_hash, zend_object *obj, zend_property_info *info, zval *data)
+{
+	/* A partially/incorrectly unserialized value may violate the property's
+	 * declared type, so restore the default and keep the slot consistent. */
+	zval *tmp = &obj->ce->default_properties_table[OBJ_PROP_TO_NUM(info->offset)];
+	if (Z_REFCOUNTED_P(data)) {
+		var_push_dtor_value(var_hash, data);
+	}
+	ZVAL_COPY_OR_DUP_PROP(data, tmp);
+}
+
 static zend_always_inline zval *tmp_var(php_unserialize_data_t *var_hashx, zend_long num)
 {
     var_dtor_entries *var_hash;
@@ -677,18 +688,19 @@ second_try:
 		}

 		if (!php_var_unserialize_internal(data, p, max, var_hash)) {
-			if (info && Z_ISREF_P(data)) {
-				/* Add type source even if we failed to unserialize.
-				 * The data is still stored in the property. */
-				ZEND_REF_ADD_TYPE_SOURCE(Z_REF_P(data), info);
+			if (info) {
+				if (Z_ISREF_P(data)) {
+					ZEND_REF_ADD_TYPE_SOURCE(Z_REF_P(data), info);
+				} else {
+					var_restore_prop_default(var_hash, obj, info, data);
+				}
 			}
 			goto failure;
 		}

 		if (UNEXPECTED(info)) {
 			if (!zend_verify_prop_assignable_by_ref(info, data, /* strict */ 1)) {
-				zval_ptr_dtor(data);
-				ZVAL_UNDEF(data);
+				var_restore_prop_default(var_hash, obj, info, data);
 				goto failure;
 			}