From 0ff737de781b083937a0481c1e0750363adb8f69 Mon Sep 17 00:00:00 2001 From: PuH4ck3rX Date: Sat, 11 Jul 2026 07:53:24 +0800 Subject: [PATCH] Fix serialize() object lifetime across nested callbacks --- NEWS | 2 + .../serialize_nested_magic_callback_uaf.phpt | 43 +++++++++++++++++++ ext/standard/var.c | 16 ++----- 3 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 ext/standard/tests/serialize/serialize_nested_magic_callback_uaf.phpt diff --git a/NEWS b/NEWS index 7324e5c591aa..f232f0ab7dec 100644 --- a/NEWS +++ b/NEWS @@ -81,6 +81,8 @@ PHP NEWS segfaults). (iliaal) - Standard: + . Fixed use-after-free in serialize() when a nested magic callback releases + the current object. (PuH4ck3rX) . Fixed bug GH-22360 (convert.base64-encode corruption on incremental flush). (David Carlier) . Fixed bug GH-22395 (base_convert() outputs at most 64 characters). diff --git a/ext/standard/tests/serialize/serialize_nested_magic_callback_uaf.phpt b/ext/standard/tests/serialize/serialize_nested_magic_callback_uaf.phpt new file mode 100644 index 000000000000..ee4a4cba9fa9 --- /dev/null +++ b/ext/standard/tests/serialize/serialize_nested_magic_callback_uaf.phpt @@ -0,0 +1,43 @@ +--TEST-- +serialize() retains objects across nested magic callbacks +--FILE-- +a->b); + $GLOBALS['spray'] = new Spray(); + return []; + } +} + +$a = new A(); +$b = new B(); +$c = new C(); + +$a->b = $b; +$b->c = $c; +$c->a = $a; + +unset($b, $c); +echo serialize($a), "\n"; + +?> +--EXPECT-- +O:1:"A":1:{s:1:"b";O:1:"B":2:{s:1:"c";O:1:"C":0:{}s:3:"pad";i:123;}} diff --git a/ext/standard/var.c b/ext/standard/var.c index acb605d2eabe..85e7201c51e6 100644 --- a/ext/standard/var.c +++ b/ext/standard/var.c @@ -726,10 +726,7 @@ PHP_FUNCTION(var_export) static void php_var_serialize_intern(smart_str *buf, zval *struc, php_serialize_data_t var_hash, bool in_rcn_array, bool is_root); -/** - * @param bool in_rcn_array Whether the element appears in a potentially nested array with RC > 1. - */ -static inline zend_long php_add_var_hash(php_serialize_data_t data, zval *var, bool in_rcn_array) /* {{{ */ +static inline zend_long php_add_var_hash(php_serialize_data_t data, zval *var) /* {{{ */ { zval *zv; zend_ulong key; @@ -741,13 +738,6 @@ static inline zend_long php_add_var_hash(php_serialize_data_t data, zval *var, b /* pass */ } else if (Z_TYPE_P(var) != IS_OBJECT) { return 0; - } else if (!in_rcn_array - && Z_REFCOUNT_P(var) == 1 - && (Z_OBJ_P(var)->properties == NULL || GC_REFCOUNT(Z_OBJ_P(var)->properties) == 1) - /* __serialize and __sleep may arbitrarily increase the refcount */ - && Z_OBJCE_P(var)->__serialize == NULL - && zend_hash_find_known_hash(&Z_OBJCE_P(var)->function_table, ZSTR_KNOWN(ZEND_STR_SLEEP)) == NULL) { - return 0; } /* References to objects are treated as if the reference didn't exist */ @@ -1008,7 +998,7 @@ static void php_var_serialize_nested_data(smart_str *buf, zval *struc, HashTable * since we already wrote the length of the array before */ if (Z_TYPE_P(data) == IS_ARRAY) { if (UNEXPECTED(Z_TYPE_P(struc) == IS_ARRAY && Z_ARR_P(data) == Z_ARR_P(struc))) { - php_add_var_hash(var_hash, struc, in_rcn_array); + php_add_var_hash(var_hash, struc); smart_str_appendl(buf, "N;", 2); } else { php_var_serialize_intern(buf, data, var_hash, in_rcn_array, false); @@ -1059,7 +1049,7 @@ static void php_var_serialize_intern(smart_str *buf, zval *struc, php_serialize_ return; } - if (var_hash && (var_already = php_add_var_hash(var_hash, struc, in_rcn_array))) { + if (var_hash && (var_already = php_add_var_hash(var_hash, struc))) { if (var_already == -1) { /* Reference to an object that failed to serialize, replace with null. */ smart_str_appendl(buf, "N;", 2);