From 6fa8a25a405495c868ef643f41a7b837762b21b3 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Thu, 31 Jul 2025 09:18:23 +0200 Subject: [PATCH] Prevent throwing in running generator Generator::throw() on a running generator is not allowed. It throws "Cannot resume an already running generator" when trying to resume the generator to handle the provided exception. However, when calling Generator::throw() on a generator with a non-Generator delegate, we release the delegate regardless. If a Fiber was suspended in the delegate, this causes use after frees when the Fiber is resumed. Fix this by throwing "Cannot resume an already running generator" earlier. Fixes GH-19326 Closes GH-19327 --- NEWS | 2 ++ Zend/tests/gh19326.phpt | 36 ++++++++++++++++++++++++++++++++++++ Zend/zend_generators.c | 14 ++++++++++++-- 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 Zend/tests/gh19326.phpt diff --git a/NEWS b/NEWS index 9f1f48a9ee9..0b261679e78 100644 --- a/NEWS +++ b/NEWS @@ -15,6 +15,8 @@ PHP NEWS causes assertion failure). (nielsdos) . Fixed bug GH-19306 (Generator can be resumed while fetching next value from delegated Generator). (Arnaud) + . Fixed bug GH-19326 (Calling Generator::throw() on a running generator with + a non-Generator delegate crashes). (Arnaud) - FTP: . Fix theoretical issues with hrtime() not being available. (nielsdos) diff --git a/Zend/tests/gh19326.phpt b/Zend/tests/gh19326.phpt new file mode 100644 index 00000000000..335fdd382ea --- /dev/null +++ b/Zend/tests/gh19326.phpt @@ -0,0 +1,36 @@ +--TEST-- +GH-19326: Calling Generator::throw() on a running generator with a non-Generator delegate crashes +--FILE-- +rewind(); + +$fiber = new Fiber(function () use ($b) { + $b->next(); +}); + +$fiber->start(); + +try { + $b->throw(new Exception('test')); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +$fiber->resume(); + +?> +--EXPECT-- +Cannot resume an already running generator diff --git a/Zend/zend_generators.c b/Zend/zend_generators.c index 3ba582fef6e..eeab16b9a13 100644 --- a/Zend/zend_generators.c +++ b/Zend/zend_generators.c @@ -492,8 +492,14 @@ ZEND_API zend_execute_data *zend_generator_check_placeholder_frame(zend_execute_ return ptr; } -static void zend_generator_throw_exception(zend_generator *generator, zval *exception) +static zend_result zend_generator_throw_exception(zend_generator *generator, zval *exception) { + if (generator->flags & ZEND_GENERATOR_CURRENTLY_RUNNING) { + zval_ptr_dtor(exception); + zend_throw_error(NULL, "Cannot resume an already running generator"); + return FAILURE; + } + zend_execute_data *original_execute_data = EG(current_execute_data); /* Throw the exception in the context of the generator. Decrementing the opline @@ -519,6 +525,8 @@ static void zend_generator_throw_exception(zend_generator *generator, zval *exce } EG(current_execute_data) = original_execute_data; + + return SUCCESS; } static void zend_generator_add_child(zend_generator *generator, zend_generator *child) @@ -1026,7 +1034,9 @@ ZEND_METHOD(Generator, throw) if (generator->execute_data) { zend_generator *root = zend_generator_get_current(generator); - zend_generator_throw_exception(root, exception); + if (zend_generator_throw_exception(root, exception) == FAILURE) { + return; + } zend_generator_resume(generator);