Repository navigation
ZTS: copy_zend_constant() crashes with ZEND_RC_DEBUG when thread copies constant with attributes #21774
Description
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 8f3cdf6 ("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):
if (c->attributes != NULL) {
// Use the same attributes table
GC_ADDREF(c->attributes);
}This is incorrect for persistent constants because:
GC_ADDREFon persistent memory violates theZEND_RC_DEBUGinvariant- 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:
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
$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 gen_stub: Add support for attributes on constants in stubs #18735,
c->attributeswas alwaysNULLfor internal constants, so the code path was never reached - Currently only
E_STRICThas 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 4997418)
Also affects PHP 8.5 (where #18735 was merged).
P.S. Hm...
Here, the array is shared between different threads, but it seems the question of who will destroy it and when hasn’t been resolved.
gen_stub: Add support for attributes on constants in stubs (#18735)
Update to PHP-Parser 5.5.0 and add support for attributes on constants in
stubs. For now, I have only migrated over E_STRICT, once the support is in
place I'll do a larger migration of the existing deprecated constants.
In the process, fix the logic in `copy_zend_constant()` for copying attributes
when a constant is copied; just increase the reference count for the attributes
table rather than trying to duplicate the contents.
I don’t really understand this…
Activity
Revert GC_ADDREF on persistent constant attributes in copy_zend_constant
EdmondDantes commentedon Apr 16, 2026
It seems the reason is that during initialization, the flag needs to be set correctly so that this check is not performed. But it still feels a bit strange.