From 05e9698db475a59e94a77f24442afcdb7857e989 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Tue, 9 Jun 2026 16:02:00 +0100 Subject: [PATCH 1/4] 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 --- Zend/tests/gh22257.phpt | 33 +++++++++++++++++++++++++++++++++ Zend/zend_exceptions.c | 5 +++-- 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 Zend/tests/gh22257.phpt diff --git a/Zend/tests/gh22257.phpt b/Zend/tests/gh22257.phpt new file mode 100644 index 000000000000..1d5ec7e4ea03 --- /dev/null +++ b/Zend/tests/gh22257.phpt @@ -0,0 +1,33 @@ +--TEST-- +GH-22257 (Type confusion / OOB read in Exception::getTraceAsString() with a non-array trace) +--CREDITS-- +012git012 +--FILE-- + +--EXPECTF-- +Warning: unserialize(): Error at offset %d of %d bytes in %s on line %d +bool(false) +OK diff --git a/Zend/zend_exceptions.c b/Zend/zend_exceptions.c index a1301b8c20b2..7b9d9226c6c3 100644 --- a/Zend/zend_exceptions.c +++ b/Zend/zend_exceptions.c @@ -685,8 +685,9 @@ ZEND_METHOD(Exception, getTraceAsString) } ZVAL_DEREF(trace); - /* Type should be guaranteed by property type. */ - ZEND_ASSERT(Z_TYPE_P(trace) == IS_ARRAY); + if (UNEXPECTED(Z_TYPE_P(trace) != IS_ARRAY)) { + RETURN_EMPTY_STRING(); + } RETURN_NEW_STR(zend_trace_to_string(Z_ARRVAL_P(trace), /* include_main */ true)); } /* }}} */ From 303ee1b6d083b7898f944643f34275f2245894d2 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Tue, 9 Jun 2026 19:47:29 +0100 Subject: [PATCH 2/4] apply @ndossche suggestion --- Zend/tests/gh22257.phpt | 33 +++++++++++++++------------------ Zend/zend_exceptions.c | 3 ++- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/Zend/tests/gh22257.phpt b/Zend/tests/gh22257.phpt index 1d5ec7e4ea03..df25c441f56d 100644 --- a/Zend/tests/gh22257.phpt +++ b/Zend/tests/gh22257.phpt @@ -7,27 +7,24 @@ GH-22257 (Type confusion / OOB read in Exception::getTraceAsString() with a non- /* A crafted, deliberately truncated payload leaves Exception::$trace holding a * non-array value (the typed-property check is skipped on the parse failure path). * The half-built object is then exposed to getTraceAsString() through SplHeap's - * delayed __unserialize(), which used to type-confuse the object as a HashTable. */ + * delayed __unserialize(), which used to type-confuse the object as a HashTable. + * It must throw instead of reading out of bounds. */ $n = "\x00"; -$r = 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' . - '}}' -); -var_dump($r); +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()); + } +} echo "OK\n"; ?> --EXPECTF-- Warning: unserialize(): Error at offset %d of %d bytes in %s on line %d -bool(false) +Exception: Invalid serialization data for SplMaxHeap object +Error: Exception trace is not an array OK diff --git a/Zend/zend_exceptions.c b/Zend/zend_exceptions.c index 7b9d9226c6c3..34588dbdacbd 100644 --- a/Zend/zend_exceptions.c +++ b/Zend/zend_exceptions.c @@ -686,7 +686,8 @@ ZEND_METHOD(Exception, getTraceAsString) ZVAL_DEREF(trace); if (UNEXPECTED(Z_TYPE_P(trace) != IS_ARRAY)) { - RETURN_EMPTY_STRING(); + zend_throw_error(NULL, "Exception trace is not an array"); + RETURN_THROWS(); } RETURN_NEW_STR(zend_trace_to_string(Z_ARRVAL_P(trace), /* include_main */ true)); } From fd6d35db4553e955d4269e68049833e926e46208 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Wed, 10 Jun 2026 15:38:28 +0100 Subject: [PATCH 3/4] CREDITS when due --- Zend/tests/gh22257.phpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Zend/tests/gh22257.phpt b/Zend/tests/gh22257.phpt index df25c441f56d..1888e36b6bf2 100644 --- a/Zend/tests/gh22257.phpt +++ b/Zend/tests/gh22257.phpt @@ -1,7 +1,7 @@ --TEST-- GH-22257 (Type confusion / OOB read in Exception::getTraceAsString() with a non-array trace) --CREDITS-- -012git012 +Igor Sak-Sakovskiy (Positive Technologies) --FILE-- Date: Wed, 10 Jun 2026 19:00:51 +0100 Subject: [PATCH 4/4] suggestion 2 --- Zend/tests/gh22257.phpt | 15 +++++++-------- Zend/zend_exceptions.c | 6 ++---- ext/standard/var_unserializer.re | 15 +++++++++++---- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/Zend/tests/gh22257.phpt b/Zend/tests/gh22257.phpt index 1888e36b6bf2..aaee9da835d6 100644 --- a/Zend/tests/gh22257.phpt +++ b/Zend/tests/gh22257.phpt @@ -1,14 +1,15 @@ --TEST-- -GH-22257 (Type confusion / OOB read in Exception::getTraceAsString() with a non-array trace) +GH-22257 (Type confusion / OOB read unserializing an Exception with a non-array trace) --CREDITS-- Igor Sak-Sakovskiy (Positive Technologies) --FILE-- --EXPECTF-- Warning: unserialize(): Error at offset %d of %d bytes in %s on line %d -Exception: Invalid serialization data for SplMaxHeap object -Error: Exception trace is not an array OK diff --git a/Zend/zend_exceptions.c b/Zend/zend_exceptions.c index 34588dbdacbd..a1301b8c20b2 100644 --- a/Zend/zend_exceptions.c +++ b/Zend/zend_exceptions.c @@ -685,10 +685,8 @@ ZEND_METHOD(Exception, getTraceAsString) } ZVAL_DEREF(trace); - if (UNEXPECTED(Z_TYPE_P(trace) != IS_ARRAY)) { - zend_throw_error(NULL, "Exception trace is not an array"); - RETURN_THROWS(); - } + /* Type should be guaranteed by property type. */ + ZEND_ASSERT(Z_TYPE_P(trace) == IS_ARRAY); RETURN_NEW_STR(zend_trace_to_string(Z_ARRVAL_P(trace), /* include_main */ true)); } /* }}} */ diff --git a/ext/standard/var_unserializer.re b/ext/standard/var_unserializer.re index d5019d94dc0c..29d2f41f6e2b 100644 --- a/ext/standard/var_unserializer.re +++ b/ext/standard/var_unserializer.re @@ -677,10 +677,17 @@ 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 { + /* "partially unserialized" value might violate the property + * declared type so we restore the default + */ + zval *tmp = &obj->ce->default_properties_table[OBJ_PROP_TO_NUM(info->offset)]; + zval_ptr_dtor(data); + ZVAL_COPY_OR_DUP(data, tmp); + } } goto failure; }