Commit 65a69b4a09f for php.net
commit 65a69b4a09ff454e0ed2c597d23f4c9dc52d1507
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Fri Jul 10 15:33:45 2026 -0400
Fix GH-22671: assert.bail must not unwind with a pending exception (#22679)
zend_throw_unwind_exit() requires !EG(exception), but assert.bail called it
right after zend_exception_error(), which can itself re-throw while printing
the callback's exception (a throwing __toString, a throwing __destruct when
the exception is released, or the call-stack guard re-tripping at the stack
limit). Only throw the unwind exit when no exception is pending; otherwise let
the re-thrown exception propagate, matching exit() and phar.
Fixes GH-22671
diff --git a/NEWS b/NEWS
index ac709587f69..cd259f06660 100644
--- a/NEWS
+++ b/NEWS
@@ -80,6 +80,8 @@ PHP NEWS
- Standard:
. Fixed sleep() and usleep() to reject values that overflow the underlying
unsigned int timeout. (Weilin Du)
+ . Fixed bug GH-22671 (assert.bail aborts the process when the assert callback
+ throws an exception whose reporting re-throws). (iliaal)
- Streams:
. Fixed bug GH-21468 (Segfault in file_get_contents w/ a https URL
diff --git a/ext/standard/assert.c b/ext/standard/assert.c
index a976aeda149..618f69a2cf7 100644
--- a/ext/standard/assert.c
+++ b/ext/standard/assert.c
@@ -245,7 +245,9 @@ PHP_FUNCTION(assert)
* exception so we can avoid bailout and use unwind_exit. */
zend_exception_error(EG(exception), E_WARNING);
}
- zend_throw_unwind_exit();
+ if (!EG(exception)) {
+ zend_throw_unwind_exit();
+ }
RETURN_THROWS();
} else {
RETURN_FALSE;
diff --git a/ext/standard/tests/assert/gh22671.phpt b/ext/standard/tests/assert/gh22671.phpt
new file mode 100644
index 00000000000..3487b0687cc
--- /dev/null
+++ b/ext/standard/tests/assert/gh22671.phpt
@@ -0,0 +1,40 @@
+--TEST--
+GH-22671 (assert.bail must not call zend_throw_unwind_exit with a pending exception)
+--INI--
+zend.assertions=1
+assert.bail=1
+assert.exception=0
+assert.callback=cb
+error_reporting=0
+--FILE--
+<?php
+register_shutdown_function(function () {
+ echo "shutdown reached\n";
+});
+
+class Inner extends Exception {
+ public function __destruct() {
+ throw new Exception("thrown from __destruct");
+ }
+}
+
+class Boom extends Exception {
+ public function __toString(): string {
+ throw new Inner("inner");
+ }
+}
+
+function cb() {
+ throw new Boom("boom");
+}
+
+// The callback throws Boom; the bail path prints it, which re-throws from
+// Boom::__toString(), and releasing that exception re-throws again from
+// Inner::__destruct(). The bail path must not reach zend_throw_unwind_exit()
+// while an exception is pending, otherwise the process aborts.
+assert(false);
+
+echo "not reached\n";
+?>
+--EXPECT--
+shutdown reached