[php-src] master: Zend: Fix GH-22257 type confusion in Exception::getTraceAsString().

David Carlier <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: David Carlier (devnexen)
Date: 2026-06-26T13:32:03+01:00

Commit: https://github.com/php/php-src/commit/57096b612d1f5636b89592b0092cc477c94226c7
Raw diff: https://github.com/php/php-src/commit/57096b612d1f5636b89592b0092cc477c94226c7.diff

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

Changed paths:
  A  Zend/tests/gh22257.phpt
  M  NEWS
  M  ext/standard/var_unserializer.re


Diff:

diff --git a/NEWS b/NEWS
index 2462f0a73cc6..9df461452c1c 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 000000000000..9bbf3b1f8d35
--- /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 484cb5aa8fc9..4a9b278c116c 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;
 			}
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.