From 13e0f0ee2220f9eadb786372ae51b980d43c328d Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 6 Oct 2026 21:40:57 +0200 Subject: [PATCH] Separate the storage of ArrayObject and ArrayIterator from the payload ArrayObject::__serialize() and ArrayIterator::__serialize() return their storage without separating it, and these classes write to it in place. So writing to such an object after deepclone_to_array() changed the payload, and every clone built from it later. --- CHANGELOG.md | 8 ++++++ deepclone.c | 10 +++++++ .../deepclone_to_array_spl_array_storage.phpt | 27 +++++++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 tests/deepclone_to_array_spl_array_storage.phpt diff --git a/CHANGELOG.md b/CHANGELOG.md index 14a0fe6..7f15448 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this extension will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed + +- `deepclone_to_array()` separates the storage that `ArrayObject` and + `ArrayIterator` return from `__serialize()`, as these classes write to it in + place: changing such an object after the call changed the payload too. + ## [0.8.7] - 2026-09-30 ### Fixed diff --git a/deepclone.c b/deepclone.c index d5ef5c5..c56f7f2 100644 --- a/deepclone.c +++ b/deepclone.c @@ -61,6 +61,7 @@ #include "Zend/zend_interfaces.h" #include "ext/spl/spl_iterators.h" #include "ext/spl/spl_exceptions.h" +#include "ext/spl/spl_array.h" #include "ext/spl/spl_heap.h" #include "ext/spl/spl_observer.h" @@ -3004,6 +3005,15 @@ static void dc_process_object(dc_ctx *ctx, zval *src, zval *dst, zval *mask_dst) zval_ptr_dtor(&props_zval); return; } + /* ArrayObject and ArrayIterator return their storage without separating it, so that writing to the object later would change the payload */ + if (instanceof_function(ce, spl_ce_ArrayObject) || instanceof_function(ce, spl_ce_ArrayIterator)) { + zval *storage = zend_hash_index_find(Z_ARRVAL(retval), 1); + if (storage && Z_TYPE_P(storage) == IS_ARRAY && Z_REFCOUNTED_P(storage) && Z_REFCOUNT_P(storage) > 1) { + SEPARATE_ARRAY(&retval); + storage = zend_hash_index_find(Z_ARRVAL(retval), 1); + SEPARATE_ARRAY(storage); + } + } if (has_unserialize) { zval_ptr_dtor(&props_zval); ZVAL_COPY_VALUE(&props_zval, &retval); diff --git a/tests/deepclone_to_array_spl_array_storage.phpt b/tests/deepclone_to_array_spl_array_storage.phpt new file mode 100644 index 0000000..d78de81 --- /dev/null +++ b/tests/deepclone_to_array_spl_array_storage.phpt @@ -0,0 +1,27 @@ +--TEST-- +Writing to an ArrayObject or ArrayIterator after deepclone_to_array() leaves the payload unchanged +--EXTENSIONS-- +deepclone +--FILE-- + [1]]); +$ai = new RecursiveArrayIterator([1]); +$empty = new ArrayObject([1]); +unset($empty[0]); +$data = deepclone_to_array([$ao, $ai, $empty]); + +$ao[] = 2; +$ao['list'][] = 2; +$ai[] = 2; +$empty[] = 2; + +$clone = deepclone_from_array($data); +var_dump($clone[0]->getArrayCopy() === [1, 'list' => [1]]); +var_dump($clone[1]->getArrayCopy() === [1]); +var_dump($clone[2]->getArrayCopy() === []); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true)