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;
}