Description
Overview
| Field |
Value |
| Target |
php @ 38956a3 |
| Defect type |
ZEND_ASSERT violation → SIGABRT (debug build only) |
| Crash site |
ZEND_INCLUDE_OR_EVAL_SPEC_TMP_HANDLER@Zend/zend_vm_execute.h:17655 — assert(!EG(exception)) |
| Reproduction |
reproduced (php --enable-debug + ASan, same commit) |
| Latest release |
compiled out in release: NDEBUG drops the assert (see Impact) |
| PoC |
06_php_zend_call_known.php (223 bytes) |
Full Output (debug-build reproduction)
Running the PoC on a --enable-debug + ASan php (CLI) terminates as follows.
Fatal error: Uncaught ArgumentCountError: Internal function var_dump() does not accept named variadic arguments in .../06_php_zend_call_known.php:1
Stack trace:
#0 .../06_php_zend_call_known.php(1): var_dump('{ include ( flo...', array: Object(class@anonymous))
#1 {main}
thrown in .../06_php_zend_call_known.php on line 1
Warning: include(): Failed opening 'Array' for inclusion ... (repeated)
php: Zend/zend_vm_execute.h:17655: const zend_op *ZEND_INCLUDE_OR_EVAL_SPEC_TMP_HANDLER(zend_execute_data *, const zend_op *): Assertion `!(executor_globals.exception)' failed.
==ABORTING (SIGABRT)
Command: USE_ZEND_ALLOC=0 ASAN_OPTIONS='symbolize=1:handle_segv=1' sapi/cli/php pocs/06_php_zend_call_known.php
Root Cause
The include/eval opcode handler runs while an exception is still pending, violating its invariant assert !EG(exception).
Frame by frame:
- The PoC's
var_dump("...", array: new class{...} ?: true) passes a named argument (array:) to the internal function var_dump(). Internal functions do not accept named variadic arguments, so the engine throws ArgumentCountError (stack frame #0).
- While that exception unwinds the stack, the anonymous class instance built as an argument is destroyed and its
__destruct() runs. The destructor body (include - require include [ new static ]) executes an include (ZEND_INCLUDE_OR_EVAL) opcode.
ZEND_INCLUDE_OR_EVAL_SPEC_TMP_HANDLER (zend_vm_execute.h:17655) asserts EG(exception)==NULL on entry. But the ArgumentCountError from step 1 is still pending, so the assert fails and the process abort()s (SIGABRT).
Broken invariant: "the include/eval opcode only runs with no pending exception" breaks on the destructor-during-unwinding path. The engine did not anticipate a destructor executing include/eval while an exception is being unwound.
Trigger
(a) Pass a named argument to an internal function to raise an exception, and (b) make one of the call arguments an object whose __destruct runs include/eval. During exception unwinding, the destructor's include trips the assert. The 223-byte PoC combines both conditions.
<?PHP var_dump ( "{ include ( float ) @ yield < yield } \uBefore binding \u \u \u \u \uCaught: \u \u \u \u \u \u" , array : new class { function __destruct ( ) { include - require include [ new static ] ; } } ? : true ) ;
Impact
- Debug build (
--enable-debug): ZEND_ASSERT violation → abort(), so this is a DoS (crash). It is a diagnostic assert that does not exist in release.
- Release build (NDEBUG): the assert is compiled out, so the handler proceeds with a pending exception. Whether this leads to memory corruption is undetermined — the handler may soon process the exception (HANDLE_EXCEPTION) harmlessly, though on release the crash surfaces at a different site. The real release impact (info disclosure / RCE) would need a separate release ASan build to assess.
- In short, what is confirmed is a debug-only internal-invariant violation; a release memory vulnerability is undetermined. Still, running include while an exception is pending is a genuine engine logic bug worth reporting upstream, even if it is not a security issue.
Reproduction
git clone https://github.com/php/php-src.git php-src && cd php-src
git checkout 38956a3
./buildconf --force
CC=clang CFLAGS="-g -O1 -fsanitize=address -fno-omit-frame-pointer" LDFLAGS="-fsanitize=address" \
./configure --disable-all --enable-cli --enable-debug --enable-tokenizer --enable-mbstring --enable-phar --enable-fileinfo
make -j$(nproc)
USE_ZEND_ALLOC=0 ASAN_OPTIONS='symbolize=1:handle_segv=1' sapi/cli/php pocs/06_php_zend_call_known.php
--enable-debug is required. Without it the assert is compiled out and the issue does not reproduce (the non-debug build handles it as a Warning loop on include failure).
PHP Version
PHP 8.6.0-dev (master, commit 38956a3f3eeb9d878d578efa4163b74fa20ebbfb)
Operating System
No response
Description
Overview
php@38956a3ZEND_ASSERTviolation → SIGABRT (debug build only)ZEND_INCLUDE_OR_EVAL_SPEC_TMP_HANDLER@Zend/zend_vm_execute.h:17655—assert(!EG(exception))--enable-debug+ ASan, same commit)06_php_zend_call_known.php(223 bytes)Full Output (debug-build reproduction)
Running the PoC on a
--enable-debug+ ASan php (CLI) terminates as follows.Command:
USE_ZEND_ALLOC=0 ASAN_OPTIONS='symbolize=1:handle_segv=1' sapi/cli/php pocs/06_php_zend_call_known.phpRoot Cause
The
include/evalopcode handler runs while an exception is still pending, violating its invariant assert!EG(exception).Frame by frame:
var_dump("...", array: new class{...} ?: true)passes a named argument (array:) to the internal functionvar_dump(). Internal functions do not accept named variadic arguments, so the engine throwsArgumentCountError(stack frame #0).__destruct()runs. The destructor body (include - require include [ new static ]) executes aninclude(ZEND_INCLUDE_OR_EVAL) opcode.ZEND_INCLUDE_OR_EVAL_SPEC_TMP_HANDLER(zend_vm_execute.h:17655) assertsEG(exception)==NULLon entry. But theArgumentCountErrorfrom step 1 is still pending, so the assert fails and the processabort()s (SIGABRT).Broken invariant: "the include/eval opcode only runs with no pending exception" breaks on the destructor-during-unwinding path. The engine did not anticipate a destructor executing include/eval while an exception is being unwound.
Trigger
(a) Pass a named argument to an internal function to raise an exception, and (b) make one of the call arguments an object whose
__destructrunsinclude/eval. During exception unwinding, the destructor's include trips the assert. The 223-byte PoC combines both conditions.Impact
--enable-debug):ZEND_ASSERTviolation →abort(), so this is a DoS (crash). It is a diagnostic assert that does not exist in release.Reproduction
--enable-debugis required. Without it the assert is compiled out and the issue does not reproduce (the non-debug build handles it as a Warning loop on include failure).PHP Version
Operating System
No response