-
Notifications
You must be signed in to change notification settings - Fork 8k
Open
Description
Description
The following code:
<?php
// file: test_jit_repeat_uaf.php
$php = getenv('TEST_PHP_EXECUTABLE') ?: PHP_BINARY;
$output = [];
exec($php . ' -r "echo \"a\\tb\\tc\\n\";"', $output, $rc);
echo "Embedded tabs: \"$output[0]\"\n";
echo "Embedded tabs len: " . strlen($output[0]) . "\n";Run with:
USE_ZEND_ALLOC=0 ASAN_OPTIONS=detect_leaks=0 \
php --repeat 2 \
-d opcache.enable_cli=1 \
-d opcache.jit_buffer_size=64M \
-d opcache.jit=tracing \
-d opcache.protect_memory=1 \
-d opcache.jit_hot_func=1 \
-d opcache.jit_hot_loop=1 \
-d opcache.jit_hot_return=1 \
-d opcache.jit_hot_side_exit=1 \
-f test_jit_repeat_uaf.phpResulted in this output:
Executing for the first time...
Embedded tabs: "a b c"
Embedded tabs len: 5
Finished execution, repeating...
=================================================================
==232189==ERROR: AddressSanitizer: heap-use-after-free on address 0x50300005d194 at pc 0x55d3d159ea26
READ of size 4 at 0x50300005d194 thread T0
#0 zend_jit_rope_end ext/opcache/jit/zend_jit_helpers.c:3450
#1 <JIT-compiled code> (/dev/zero (deleted))
#2 zend_execute Zend/zend_vm_execute.h:115483
#3 zend_execute_script Zend/zend.c:1983
#4 php_execute_script_ex main/main.c:2665
#5 do_cli sapi/cli/php_cli.c:958
#6 main sapi/cli/php_cli.c:1369
0x50300005d194 is located 4 bytes inside of 32-byte region [0x50300005d190,0x50300005d1b0)
freed by thread T0 here: <trace unavailable due to ASAN check failure>
But I expected this output instead:
Executing for the first time...
Embedded tabs: "a b c"
Embedded tabs len: 5
Finished execution, repeating...
Embedded tabs: "a b c"
Embedded tabs len: 5
Analysis
zend_jit_rope_end()iterates over rope segments and readsZSTR_GET_COPYABLE_CONCAT_PROPERTIES(rope[i])(line 3450). On the second--repeatiteration, one of the rope segments ($output[0]from the string interpolation"...$output[0]...") points to azend_stringthat was freed duringphp_request_shutdown()after the first iteration.- JIT compiles the ROPE_END opcode during the first iteration (with
jit_hot_func=1, the threshold is 1 call). On the second iteration, the JIT-compiled code reuses stale pointers from the previous request's heap. - The bug does not reproduce without JIT (
opcache.jit=0). - The bug does not reproduce without
--repeat(single execution). - The bug does not reproduce with default
jit_hot_functhreshold (needs low threshold like 1 to trigger on first iteration). - The bug is not related to fibers, coroutines, or any specific extension — plain
exec()+ string interpolation is sufficient.
Configure flags used
--enable-zts --enable-address-sanitizer --enable-zend-test
(Also reproduced with a full set of extensions matching the standard CI build.)
PHP Version
PHP 8.6.0-dev (cli) (built: Mar 12 2026 06:51:37) (ZTS)
Copyright (c) The PHP Group
Zend Engine v4.6.0-dev, Copyright (c) Zend Technologies
with Zend OPcache v8.6.0-dev, Copyright (c), by Zend Technologies
Operating System
Ubuntu 24.04.1 LTS
Reactions are currently unavailable