diff --git a/scheduler.c b/scheduler.c index e20d171..2fa44a2 100644 --- a/scheduler.c +++ b/scheduler.c @@ -1811,11 +1811,6 @@ ZEND_STACK_ALIGNED void fiber_entry(zend_fiber_transfer *transfer) EG(current_execute_data) = execute_data; EG(jit_trace_num) = 0; EG(error_reporting) = (int) error_reporting; - /* A fiber starts outside anyone's EH_THROW window (zend_replace_error_handling): - * inheriting one would paint this coroutine's warnings with a foreign - * exception class. */ - EG(error_handling) = EH_NORMAL; - EG(exception_class) = NULL; #ifdef ZEND_CHECK_STACK_LIMIT EG(stack_base) = zend_fiber_stack_base(internal_fiber_context->stack); diff --git a/tests/edge_cases/016-error_handling_window_not_shared.phpt b/tests/edge_cases/016-error_handling_window_not_shared.phpt index 5af9482..38c6a30 100644 --- a/tests/edge_cases/016-error_handling_window_not_shared.phpt +++ b/tests/edge_cases/016-error_handling_window_not_shared.phpt @@ -1,7 +1,7 @@ --TEST-- -An EH_THROW window opened by another coroutine does not repaint our warnings +An EH_THROW window does not follow a coroutine spawned inside it --EXTENSIONS-- -pdo_mysql +zlib --SKIPIF-- 3]); - } catch (Throwable $e) { - return 'connect ended'; +/* DirectoryIterator::__construct turns warnings into UnexpectedValueException for + * the duration of the directory open (zend_replace_error_handling), and the open + * goes through a userland wrapper — so a coroutine can be spawned, and the holder + * can park, while that mode is in force. The spawned coroutine is a separate flow + * of control: its own warning must stay a warning, whatever the holder is doing. + * PDO::__construct holds the same kind of window across its connect, which is how + * this surfaced downstream, but a wrapper reproduces it without a database. */ + +class WindowedDirectory +{ + public static ?object $spawned = null; + + public $context; + + public function dir_opendir(string $path, int $options): bool + { + self::$spawned = spawn(static function (): string { + try { + return 'suppressed, got ' . var_export(@gzdecode(''), true); + } catch (Throwable $e) { + return 'leaked ' . $e::class . ': ' . $e->getMessage(); + } + }); + + delay(50); + + return false; } - return 'connect ended'; -}); + public function dir_readdir(): string|false + { + return false; + } -$victim = spawn(function () { - delay(50); + public function dir_closedir(): bool + { + return true; + } +} + +stream_wrapper_register('windowed', WindowedDirectory::class); +$holder = spawn(static function (): string { try { - $decoded = @gzdecode(''); - return 'suppressed, got ' . var_export($decoded, true); + new DirectoryIterator('windowed://dir'); } catch (Throwable $e) { - return 'leaked ' . $e::class . ': ' . $e->getMessage(); + return 'holder: ' . $e::class; } + + return 'holder: no exception'; }); -echo await($victim), "\n"; -echo await($connector), "\n"; +echo await($holder), "\n"; +echo await(WindowedDirectory::$spawned), "\n"; echo "Done\n"; ?> --EXPECT-- +holder: UnexpectedValueException suppressed, got false -connect ended Done diff --git a/tests/edge_cases/017-error_handling_window_not_inherited_on_resume.phpt b/tests/edge_cases/017-error_handling_window_not_inherited_on_resume.phpt new file mode 100644 index 0000000..f6f818b --- /dev/null +++ b/tests/edge_cases/017-error_handling_window_not_inherited_on_resume.phpt @@ -0,0 +1,75 @@ +--TEST-- +An EH_THROW window opened while we were parked does not follow us on resume +--EXTENSIONS-- +zlib +--SKIPIF-- + +--FILE-- +getMessage(); + } +}); + +$holder = spawn(static function (): string { + delay(30); + + try { + new DirectoryIterator('windowed://dir'); + } catch (Throwable $e) { + return 'holder: ' . $e::class; + } + + return 'holder: no exception'; +}); + +echo await($victim), "\n"; +echo await($holder), "\n"; +echo "Done\n"; +?> +--EXPECT-- +suppressed, got false +holder: UnexpectedValueException +Done