Skip to content

Fix GH-24615: ArrayObject backing storage cycle causes unbounded recursion - #24620

Open
Marti2203 wants to merge 1 commit into
php:PHP-8.4from
Marti2203:fix-gh24615-arrayobject-cycle
Open

Marti2203 wants to merge 1 commit into
php:PHP-8.4from
Marti2203:fix-gh24615-arrayobject-cycle

Conversation

@Marti2203

Copy link
Copy Markdown
Contributor

spl_array_set_array() accepts another ArrayObject/ArrayIterator as storage (SPL_ARRAY_USE_OTHER) and only special-cases an object that uses itself (SPL_ARRAY_IS_SELF). Two objects that use each other as storage are accepted, and the next access to the storage recurses in spl_array_get_hash_table_ptr() until the stack overflows:

$a = new ArrayObject([1]);
$b = new ArrayObject($a);   // $b uses $a's storage
$a->exchangeArray($b);      // $a now uses $b's storage: a cycle
var_dump(count($a));        // stack overflow

The same cycle can be built through __construct(), __unserialize()/unserialize(), with ArrayIterator, and over longer chains.

Fix

Before accepting another ArrayObject/ArrayIterator as storage, follow its chain of SPL_ARRAY_USE_OTHER objects and throw an InvalidArgumentException ("Cannot use ArrayObject as storage of ArrayObject, as it would create a cycle") if the chain leads back to the object. This is done before anything is changed, so the object keeps its previous storage. Every path that sets the storage goes through spl_array_set_array(), so all of the above are covered. The walk needs no bound: each object has at most one storage object, and with this check the chains stay acyclic. Using an object as its own storage and sharing storage without a cycle still work.

Test

ext/spl/tests/gh24615.phpt builds two-object and three-object cycles (through exchangeArray(), __construct() and __unserialize(), with an ArrayIterator in the chain). It checks that each attempt throws and leaves the objects usable, and that self storage and acyclic sharing still work.

  • Without the fix: the test crashes (Termsig=11, stack overflow in spl_array_get_hash_table_ptr()).
  • With the fix: passes.
  • ext/spl/tests (debug build): 768 passed, 9 skipped, 0 failed.

The branch also merges cleanly into PHP-8.5 and master. On master, object storage for ArrayObject is deprecated, so the test will need the deprecation notices in its expected output (or E_DEPRECATED turned off) after merging up.

Fixes GH-24615

This issue was found by an LLM-Enhanced Static Analysis Engine and verified manually; the patch was manually reviewed.

🤖 Generated with Claude Code

…ecursion

spl_array_set_array() only rejected an ArrayObject using itself as
storage. Two objects using each other as storage were accepted, and the
next access recursed in spl_array_get_hash_table_ptr() until the stack
overflowed. Follow the chain of objects used as storage and throw an
InvalidArgumentException if it leads back to the object.
@Girgias

Girgias commented Oct 23, 2026

Copy link
Copy Markdown
Member

I'm not sure this is even worth fixing considering that using objects as backing storage has been deprecated and will be removed in the future.

@Marti2203

Copy link
Copy Markdown
Contributor Author

Thanks for taking a look. Agreed that this goes away once object backing storage is removed. I targeted PHP-8.4 because there it isn't deprecated yet and the result is a segfault rather than an error. If you'd rather not change 8.4 for this, I'm happy to close the PR and the issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants