[php-src] master: Fix GH-22671: assert.bail must not unwind with a pending exception (#22679)

Ilia Alshanetsky via GitHub <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Committer: GitHub (web-flow)
Pusher: iliaal
Date: 2026-07-10T15:33:45-04:00

Commit: https://github.com/php/php-src/commit/65a69b4a09ff454e0ed2c597d23f4c9dc52d1507
Raw diff: https://github.com/php/php-src/commit/65a69b4a09ff454e0ed2c597d23f4c9dc52d1507.diff

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

Changed paths:
  A  ext/standard/tests/assert/gh22671.phpt
  M  NEWS
  M  ext/standard/assert.c


Diff:

diff --git a/NEWS b/NEWS
index ac709587f699..cd259f06660e 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 a976aeda1494..618f69a2cf74 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 000000000000..3487b0687ccd
--- /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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.