[php-src] Issue #21419: JIT heap-use-after-free in zend_jit_rope_end with --repeat

[email protected] (EdmondDantes)
Newsgroups php.bugs
Message-ID <[email protected]>
Issue: https://github.com/php/php-src/issues/21419
Author: EdmondDantes

### Description

The following code:

```php
<?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:

```bash
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.php
```

Resulted 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 reads `ZSTR_GET_COPYABLE_CONCAT_PROPERTIES(rope[i])` (line 3450). On the second `--repeat` iteration, one of the rope segments (`$output[0]` from the string interpolation `"...$output[0]..."`) points to a `zend_string` that was freed during `php_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_func` threshold (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

```plain
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
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.