[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).
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.