fix(compiler): restore zend callback frames after caught exceptions

- Add try-catch block around generated ZEND_FUNCTION/ZEND_METHOD wrappers
- Convert C++ exceptions to normal Zend exception propagation at outermost wrapper
- Prevent C++ exceptions from escaping through Zend's C frames during cleanup
- Ensure EG(exception) is properly handled for frame cleanup in ZendVM
- Add proper exception handling for native TypePHP code unwinding
- Maintain callback boundary integrity for dynamic method calls
pull/48/head
韩天峰 2 weeks ago
parent 6e5fadcbc6
commit 8aa8cdb065
  1. 12
      src/Translator.php
  2. 67
      tests/compiler/exception/dynamic-callback-frame-restored.phpt

@ -3359,7 +3359,13 @@ CODE;
array $implicitMethodArgs = []
): string
{
$cppCode = '';
// A generated ZEND_FUNCTION/ZEND_METHOD is a callback boundary owned by
// ZendVM. Native TypePHP code uses C++ exceptions so its local RAII
// objects unwind correctly, but that exception must not escape through
// Zend's C frames: those frames perform their cleanup after the handler
// returns with EG(exception) set. Convert back to normal Zend exception
// propagation at the outermost wrapper.
$cppCode = 'try {' . PHP_EOL;
$callParams = '';
if ($functionDef->argCountRequired > 0) {
$cppCode .= $this->genWrapperRequiredArgCountCheck($functionDef, $displayName);
@ -3449,6 +3455,10 @@ CODE;
} else {
$cppCode .= $this->getIndent() . $fn . '(' . $callParams . ');' . PHP_EOL;
}
$cppCode .= '}' . PHP_EOL;
$cppCode .= 'catch (zend_object *) {' . PHP_EOL;
$cppCode .= $this->getIndent() . '/* EG(exception) is already set; return control to ZendVM for frame cleanup. */' . PHP_EOL;
$cppCode .= '}' . PHP_EOL;
$cppCode .= '}' . PHP_EOL . PHP_EOL;
return $cppCode;

@ -0,0 +1,67 @@
--TEST--
Zend callback frames are restored after caught exceptions
--FILE--
<?php
final class CallbackFrameProbe
{
public function __clone()
{
throw new DomainException('clone');
}
public function fail(): void
{
throw new DomainException('reflection');
}
}
function callback_frame_is_stale(Throwable $exception, string $function): bool
{
foreach ($exception->getTrace() as $frame) {
if (($frame['function'] ?? '') === $function) {
return true;
}
}
return false;
}
function main(): void
{
try {
$copy = clone new CallbackFrameProbe();
} catch (DomainException $exception) {
}
try {
throw new RuntimeException('after clone');
} catch (RuntimeException $exception) {
echo 'clone=', callback_frame_is_stale($exception, '__clone') ? 'stale' : 'clean', "\n";
}
try {
array_map(static function (int $value): int {
throw new DomainException('array_map');
}, [1]);
} catch (DomainException $exception) {
}
try {
throw new RuntimeException('after array_map');
} catch (RuntimeException $exception) {
echo 'array_map=', callback_frame_is_stale($exception, '{closure}') ? 'stale' : 'clean', "\n";
}
try {
(new ReflectionMethod(CallbackFrameProbe::class, 'fail'))->invoke(new CallbackFrameProbe());
} catch (DomainException $exception) {
}
try {
throw new RuntimeException('after reflection');
} catch (RuntimeException $exception) {
echo 'reflection=', callback_frame_is_stale($exception, 'fail') ? 'stale' : 'clean', "\n";
}
}
?>
--EXPECT--
clone=clean
array_map=clean
reflection=clean
Loading…
Cancel
Save