Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions scheduler.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
71 changes: 48 additions & 23 deletions tests/edge_cases/016-error_handling_window_not_shared.phpt
Original file line number Diff line number Diff line change
@@ -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--
<?php
if (!function_exists('Async\spawn')) die("skip TrueAsync runtime not available\n");
Expand All @@ -13,38 +13,63 @@ use function Async\spawn;
use function Async\await;
use function Async\delay;

/* PDO::__construct replaces the error handling mode for the duration of the
* connect (zend_replace_error_handling(EH_THROW, pdo_exception_ce)), and the
* connect parks the coroutine. While it sleeps, a warning raised anywhere else
* must stay a warning: if the window were global, the engine would turn it into
* a PDOException carrying someone else's message. 10.255.255.1 is unroutable, so
* the connect stays parked until it times out. */
$connector = spawn(function () {
try {
new PDO('mysql:host=10.255.255.1;port=3306;dbname=x', 'u', 'p', [PDO::ATTR_TIMEOUT => 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
--TEST--
An EH_THROW window opened while we were parked does not follow us on resume
--EXTENSIONS--
zlib
--SKIPIF--
<?php
if (!function_exists('Async\spawn')) die("skip TrueAsync runtime not available\n");
?>
--FILE--
<?php

use function Async\spawn;
use function Async\await;
use function Async\delay;

/* Companion to 016. There the victim is spawned while the window is already open,
* so what protects it is starting outside one. Here it is parked before the window
* exists and wakes up in the middle of it, so what protects it is the context
* switch carrying the mode per fiber. */

class WindowedDirectory
{
public $context;

public function dir_opendir(string $path, int $options): bool
{
delay(100);

return false;
}

public function dir_readdir(): string|false
{
return false;
}

public function dir_closedir(): bool
{
return true;
}
}

stream_wrapper_register('windowed', WindowedDirectory::class);

$victim = spawn(static function (): string {
delay(10);
delay(60); // Wakes up while the holder sits inside the window.

try {
return 'suppressed, got ' . var_export(@gzdecode(''), true);
} catch (Throwable $e) {
return 'leaked ' . $e::class . ': ' . $e->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
Loading