[php-src] Issue #24701: opcache.preload: use-after-free on class-typed trait constants (missing type addref in zend_do_traits_constant_binding)
| From: | sanklamm | Date: | Sun, 25 Oct 2026 20:11:52 +0000 |
| Subject: | [php-src] Issue #24701: opcache.preload: use-after-free on class-typed trait constants (missing type addref in zend_do_traits_constant_binding) | ||
| Groups: | php.bugs | ||
| Request: | Send a blank email to php-bugs+get-253188@lists.php.net to get a copy of this message | ||
Issue: https://github.com/php/php-src/issues/24701
Author: sanklamm
### Description
A trait constant with a class type (
protected const SomeEnum X = ...) causes a
use-after-free during opcache.preload. In our production setup (FrankenPHP worker mode,
Symfony container preload) this crashed roughly every second boot with zend_mm_heap
corrupted, free(): double free detected in tcache 2 or SIGSEGV. Removing the
constant's type (or the constant) makes it go away.
Minimal reproducer (no framework, plain CLI):
E.php
```php
<?php
namespace Repro\Domain;
enum StorageNameEnum: string { case A = 'a'; }
```
T.php
```php
<?php
namespace Repro\Export;
use Repro\Domain\StorageNameEnum;
trait CreatorTrait { protected const StorageNameEnum STORAGE = StorageNameEnum::A; }
```
A.php
```php
<?php
namespace Repro\Export;
class FirstCreator { use CreatorTrait; }
```
preload.php
```php
<?php
foreach (['E', 'T', 'A'] as $f) { opcache_compile_file(__DIR__ .
"/$f.php"); }
```
```
USE_ZEND_ALLOC=0 valgrind php -d opcache.enable_cli=1 -d opcache.preload=preload.php -r 'echo
"ok\n";'
```
Resulted in this output (symbolized with an unstripped build of the same version):
```
Invalid read of size 8
at accel_new_interned_string
by zend_persist_type_calc
by zend_persist_class_entry_calc.part.0
by zend_accel_script_persist_calc
by preload_script_in_shared_memory.part.0
by accel_preload
by accel_finish_startup_preload
by accel_post_startup
by zend_post_startup
by php_module_startup
Address 0x6ab1cb8 is 8 bytes inside a block of size 56 free'd
at free
by accel_new_interned_string
by zend_persist_type_calc
by zend_persist_class_entry_calc.part.0
by zend_accel_script_persist_calc
by preload_script_in_shared_memory.part.0
by accel_preload
...
Block was alloc'd at
at malloc
by __zend_malloc
by lex_scan
by zendlex
by zendparse
by zend_compile
by compile_file
by phar_compile_file
by preload_compile_file
by zif_opcache_compile_file
...
ok
```
6 invalid reads in total, all on the freed class name of the constant's type.
But I expected this output instead:
```
ok
```
with no valgrind errors. Changing the trait constant to protected const STORAGE =
StorageNameEnum::A; (untyped) gives 0 errors. With one or two classes using the trait the
count is the same (6).
**Analysis.** zend_do_traits_constant_binding() (Zend/zend_inheritance.c) copies each
trait constant into the using class with memcpy(ct, constant,
sizeof(zend_class_constant)) and sets ct->ce = ce, but does not take a
reference on the type (ct->type), unlike
zend_do_traits_property_binding(), which calls zend_type_copy_ctor().
Under preload the class name in the type is not interned yet, so when the script is persisted,
zend_persist_type_calc() → accel_new_interned_string() interns and
releases that same zend_string once for the trait's constant and once for each
copy. The second call reads the already freed string.
In our production image the freed block's first word (the Zend MM free-list pointer) was then
taken over as the CE-cache map_ptr offset of the interned enum name (the garbage happens to have
IS_STR_CLASS_NAME_MAP_PTR set), and zend_persist_class_entry() →
ZSTR_SET_CE_CACHE_EX(ce->name, NULL, 0) wrote 8 zero bytes at map_ptr_base +
garbage. Whether that segfaults or silently corrupts the heap depends on the memory layout;
with ASLR disabled it crashed deterministically.
**Possible fix (untested):** after the memcpy in
zend_do_traits_constant_binding(), call zend_type_copy_ctor(&ct->type, /*
use_arena */ true, /* persistent */ false);, as the trait property path does.
### PHP Version
```plain
PHP 8.5.12 (cli) (built: Oct 21 2026 15:48:53) (NTS)
Zend Engine v4.5.12
with Zend OPcache v8.5.12
```
Also reproduced with valgrind on the official Docker images php:8.5-zts (8.5.12 ZTS),
php:8.4-cli (8.4.27) and php:8.3-cli (8.3.36), and as boot crashes on
8.5.11 ZTS under FrankenPHP 1.12.7. The relevant code in
zend_do_traits_constant_binding() is unchanged on master.
### Operating System
Debian trixie (official php Docker images), host Linux 6.18 x86_64