[php-src] Issue #24701: opcache.preload: use-after-free on class-typed trait constants (missing type addref in zend_do_traits_constant_binding)

From: 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

« previous php.bugs (#253188) next »