[php-src] Issue #21774: ZTS: copy_zend_constant() crashes with ZEND_RC_DEBUG when thread copies constant with attributes
[email protected] (EdmondDantes)
| Newsgroups | php.bugs |
|---|---|
| Message-ID | <oOQwzAS2USffsoldU2CO3fqifuXTxWqEceOq59ZRxQw@main.internal.php.net> |
Issue: https://github.com/php/php-src/issues/21774
Author: EdmondDantes
## Description
When a new OS thread is created in ZTS mode (via TSRM), `executor_globals_ctor()` calls `zend_copy_constants()` to copy the constant table for the new thread. For constants that have attributes (e.g. `E_STRICT` with `#[Deprecated]`), `copy_zend_constant()` calls `GC_ADDREF(c->attributes)` on a persistent (`pemalloc`-allocated) `HashTable`.
With `ZEND_RC_DEBUG=1`, `zend_gc_addref()` asserts that the target is not persistent memory:
```
(zval_gc_flags((p)->u.type_info) & (GC_PERSISTENT|GC_PERSISTENT_LOCAL)) != GC_PERSISTENT
```
This assertion fails because `c->attributes` on internal persistent constants is allocated via `pemalloc` and has `GC_PERSISTENT` flag set.
## Root cause
Commit 8f3cdf6236 ("gen_stub: Add support for attributes on constants in stubs", #18735) changed `copy_zend_constant()` from `zend_array_dup(c->attributes)` to `GC_ADDREF(c->attributes)`:
```c
if (c->attributes != NULL) {
// Use the same attributes table
GC_ADDREF(c->attributes);
}
```
This is incorrect for persistent constants because:
1. `GC_ADDREF` on persistent memory violates the `ZEND_RC_DEBUG` invariant
2. A non-atomic refcount increment on shared persistent memory is a data race between threads
## How to reproduce
Build PHP with ZTS and RC_DEBUG:
```bash
CFLAGS="-DZEND_RC_DEBUG=1" ./configure --enable-debug --enable-zts
make -j$(nproc)
```
Then run any code that creates a new OS thread. Reproduced with both `ext/parallel` and a custom extension:
**With ext/parallel:**
```php
<?php
$runtime = new \parallel\Runtime();
$future = $runtime->run(function() {
return "hello";
});
echo $future->value();
```
**Output:**
```
>>> (zval_gc_flags((p)->u.type_info) & (GC_PERSISTENT|GC_PERSISTENT_LOCAL)) != GC_PERSISTENT
at Zend/zend_types.h:1355
--- C backtrace ---
#0 zend_gc_addref Zend/zend_types.h:1355
#1 copy_zend_constant Zend/zend_constants.c:88
#2 zend_hash_copy Zend/zend_hash.c:2296
#3 zend_copy_constants Zend/zend_constants.c:98
#4 executor_globals_ctor Zend/zend.c:810
#5 allocate_new_resource TSRM/TSRM.c:406
#6 ts_resource_ex TSRM/TSRM.c:461
#7 php_parallel_scheduler_setup ext/parallel/src/scheduler.c:164
php: Zend/zend_types.h:1355: zend_gc_addref: Assertion failed.
Aborted
```
**Expected output:**
```
hello
```
## Notes
- The bug is only visible with `ZEND_RC_DEBUG=1` (used in CI ASAN builds), but the underlying data race exists regardless
- Before #18735, `c->attributes` was always `NULL` for internal constants, so the code path was never reached
- Currently only `E_STRICT` has the `#[Deprecated]` attribute, which is sufficient to trigger the crash
- This affects any extension that creates OS threads in ZTS mode (ext/parallel, or any future threading extension)
## PHP Version
PHP 8.6.0-dev (master at 4997418b1d)
Also affects PHP 8.5 (where #18735 was merged).