From 58eada000aa8d3289a95f57f8cb6bbd15e817a56 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sun, 14 Jun 2026 12:01:11 -0400 Subject: [PATCH] Fix SplObjectStorage getHash() guard leaking across a bailout The getHash() recursion guard increments a request-persistent counter around the userland getHash() call but decrements it only on the normal return path. A bailout inside an overridden getHash() (out-of-memory, timeout, or any fatal) skips the decrement, and the counter is never reset per request, so on a persistent SAPI every later request on the same worker wrongly throws "Modification of SplObjectStorage during getHash() is prohibited". Decrement inside a zend_catch and re-raise the bailout so the counter is balanced on every path. --- ext/spl/spl_observer.c | 10 ++++- .../spl_object_storage_gethash_bailout.phpt | 42 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 sapi/cli/tests/spl_object_storage_gethash_bailout.phpt diff --git a/ext/spl/spl_observer.c b/ext/spl/spl_observer.c index f897ab1350cc..64d5011975d2 100644 --- a/ext/spl/spl_observer.c +++ b/ext/spl/spl_observer.c @@ -94,8 +94,16 @@ static zend_result spl_object_storage_get_hash(zend_hash_key *key, spl_SplObject ZVAL_OBJ(¶m, obj); ZVAL_UNDEF(&rv); spl_object_storage_get_hash_depth++; - zend_call_method_with_1_params(&intern->std, intern->std.ce, &intern->fptr_get_hash, "getHash", &rv, ¶m); + bool bailout = false; + zend_try { + zend_call_method_with_1_params(&intern->std, intern->std.ce, &intern->fptr_get_hash, "getHash", &rv, ¶m); + } zend_catch { + bailout = true; + } zend_end_try(); spl_object_storage_get_hash_depth--; + if (UNEXPECTED(bailout)) { + zend_bailout(); + } if (UNEXPECTED(Z_ISUNDEF(rv))) { /* An exception has occurred */ return FAILURE; diff --git a/sapi/cli/tests/spl_object_storage_gethash_bailout.phpt b/sapi/cli/tests/spl_object_storage_gethash_bailout.phpt new file mode 100644 index 000000000000..e7dceb027128 --- /dev/null +++ b/sapi/cli/tests/spl_object_storage_gethash_bailout.phpt @@ -0,0 +1,42 @@ +--TEST-- +SplObjectStorage getHash() depth counter is reset after a bailout in a user getHash() +--SKIPIF-- + +--INI-- +allow_url_fopen=1 +--FILE-- +offsetSet(new stdClass()); + echo "poison"; +} else { + $s = new SplObjectStorage(); + $s->offsetSet(new stdClass()); + echo "check-ok count=", count($s); +} +PHP; + +php_cli_server_start($code, 'router.php'); + +$base = 'http://' . PHP_CLI_SERVER_ADDRESS; +// Request 1 bails out (OOM) inside the overridden getHash() mid-offsetSet. +@file_get_contents($base . '/poison'); +// A later request on the same worker must not be poisoned by a stuck counter. +echo @file_get_contents($base . '/check'), "\n"; +echo @file_get_contents($base . '/check'), "\n"; +?> +--EXPECT-- +check-ok count=1 +check-ok count=1