Commit 47d5c0c7168 for php
commit 47d5c0c716860800dc9324cd0b73901e1dbc1606
Author: Pratik Bhujel <prateekbhujelpb@gmail.com>
Date: Tue Oct 27 17:57:35 2026 +0545
Fix GH-24577: CE-cache flag on interned class name visible before map_ptr offset (#24585)
When OPcache persists a script into shared memory,
zend_accel_pending_ce_cache_commit() (and zend_alloc_ce_cache() in Zend/zend.c)
writes the allocated map_ptr offset into type_name->gc.refcount and sets
IS_STR_CLASS_NAME_MAP_PTR on the shared interned string. Because these are
plain non-atomic stores to adjacent struct fields, the compiler or weakly
ordered CPU can make IS_STR_CLASS_NAME_MAP_PTR visible to concurrent worker
processes before the map_ptr offset overwrites the initial SHM interned
string refcount of 2.
Use explicit release and acquire memory barriers to prevent compiler
and CPU reordering between GC_SET_REFCOUNT and
GC_ADD_FLAGS(..., IS_STR_CLASS_NAME_MAP_PTR) across all interned
class cache writers and readers.
diff --git a/NEWS b/NEWS
index 131c02d4c24..01556b62951 100644
--- a/NEWS
+++ b/NEWS
@@ -40,7 +40,7 @@ PHP NEWS
- Curl:
. Fix use-after-free of CURLOPT_STDERR stream on cloned curl handle.
(lacatoire)
-
+
- Date:
. Fixed bug GH-20503 (Assertion failure with ext/date DateInterval property
hash construction). (heathdutton)
@@ -135,6 +135,8 @@ PHP NEWS
order, resulting in wrong functions possibly called
in different configurations). (ndossche)
. Fixed bug GH-24438 (Preserve indirect types in JIT side traces). (aasul)
+ . Fixed bug GH-24577 (CE-cache flag on interned class name visible before
+ map_ptr offset). (prateekbhujel)
- PDO:
. Fixed invalid ATTR_ORACLE_NULLS values being accepted and truncated.
diff --git a/Zend/zend.c b/Zend/zend.c
index 32028c9c5a0..1b1dd9e12e0 100644
--- a/Zend/zend.c
+++ b/Zend/zend.c
@@ -2101,6 +2101,7 @@ ZEND_API void zend_alloc_ce_cache(zend_string *type_name)
do {
ret = ZEND_MAP_PTR_NEW_OFFSET();
} while (ret <= 2);
- GC_ADD_FLAGS(type_name, IS_STR_CLASS_NAME_MAP_PTR);
GC_SET_REFCOUNT(type_name, ret);
+ ZEND_ATOMIC_FENCE_RELEASE();
+ GC_ADD_FLAGS(type_name, IS_STR_CLASS_NAME_MAP_PTR);
}
diff --git a/Zend/zend_atomic.h b/Zend/zend_atomic.h
index 5eeb212ec09..677d2ed0ccb 100644
--- a/Zend/zend_atomic.h
+++ b/Zend/zend_atomic.h
@@ -402,6 +402,19 @@ ZEND_API int zend_atomic_int_load(const zend_atomic_int *obj);
# define ZEND_ATOMIC_FENCE_ACQUIRE() ((void)0)
#endif
+static zend_always_inline uint32_t zend_atomic_load_acquire_32(const uint32_t *ptr)
+{
+#if defined(__GNUC__) || defined(__clang__) || defined(HAVE_GNUC_ATOMICS)
+ return __atomic_load_n(ptr, __ATOMIC_ACQUIRE);
+#else
+ uint32_t val = *(const volatile uint32_t *)ptr;
+ ZEND_ATOMIC_FENCE_ACQUIRE();
+ return val;
+#endif
+}
+
+#define ZEND_ATOMIC_LOAD_ACQUIRE_32(ptr) zend_atomic_load_acquire_32(ptr)
+
END_EXTERN_C()
#endif
diff --git a/Zend/zend_types.h b/Zend/zend_types.h
index b2e40e2a2c8..5d4f8749073 100644
--- a/Zend/zend_types.h
+++ b/Zend/zend_types.h
@@ -24,6 +24,7 @@
#include "zend_portability.h"
#include "zend_long.h"
+#include "zend_atomic.h"
#include <stdbool.h>
#include <stdint.h>
@@ -862,7 +863,13 @@ static zend_always_inline uint32_t zval_gc_info(uint32_t gc_type_info) {
#define OBJ_EXTRA_FLAGS(obj) ((obj)->extra_flags)
/* Fast class cache */
-#define ZSTR_HAS_CE_CACHE(s) (GC_FLAGS(s) & IS_STR_CLASS_NAME_MAP_PTR)
+static zend_always_inline bool zstr_has_ce_cache(const zend_string *s)
+{
+ uint32_t type_info = ZEND_ATOMIC_LOAD_ACQUIRE_32(&GC_TYPE_INFO(s));
+ return (type_info & (IS_STR_CLASS_NAME_MAP_PTR << GC_FLAGS_SHIFT)) != 0;
+}
+
+#define ZSTR_HAS_CE_CACHE(s) zstr_has_ce_cache(s)
#define ZSTR_GET_CE_CACHE(s) ZSTR_GET_CE_CACHE_EX(s, 1)
#define ZSTR_SET_CE_CACHE(s, ce) ZSTR_SET_CE_CACHE_EX(s, ce, 1)
diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c
index da3114ad60b..b5980f2b319 100644
--- a/ext/opcache/ZendAccelerator.c
+++ b/ext/opcache/ZendAccelerator.c
@@ -450,8 +450,9 @@ static void accel_interned_strings_restore_state(void)
/* Discard non-global CE_CACHE slots on reset. */
uintptr_t idx = (GC_REFCOUNT(s) - 1) / sizeof(void *);
if (idx >= ZCSG(map_ptr_last)) {
- GC_SET_REFCOUNT(s, 2);
GC_DEL_FLAGS(s, IS_STR_CLASS_NAME_MAP_PTR);
+ ZEND_ATOMIC_FENCE_RELEASE();
+ GC_SET_REFCOUNT(s, 2);
}
}
@@ -560,6 +561,7 @@ zend_string* ZEND_FASTCALL accel_new_interned_string(zend_string *str)
if (ZSTR_HAS_CE_CACHE(str) && !ZSTR_HAS_CE_CACHE(s)) {
ZEND_ASSERT(GC_FLAGS(str) & IS_STR_PERMANENT);
GC_SET_REFCOUNT(s, GC_REFCOUNT(str));
+ ZEND_ATOMIC_FENCE_RELEASE();
GC_ADD_FLAGS(s, IS_STR_CLASS_NAME_MAP_PTR);
}
diff --git a/ext/opcache/tests/gh24577.phpt b/ext/opcache/tests/gh24577.phpt
new file mode 100644
index 00000000000..12374425c85
--- /dev/null
+++ b/ext/opcache/tests/gh24577.phpt
@@ -0,0 +1,150 @@
+--TEST--
+Bug GH-24577: CE-cache flag on shared interned class name must not crash with concurrent access
+--EXTENSIONS--
+opcache
+pcntl
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.interned_strings_buffer=8
+opcache.file_update_protection=0
+--FILE--
+<?php
+$consumerFile = __DIR__ . '/gh24577_consumer.inc';
+$classesFile = __DIR__ . '/gh24577_classes.inc';
+$gateFile = __DIR__ . '/gh24577_gate.tmp';
+$readyPrefix = __DIR__ . '/gh24577_ready_';
+$numClasses = 64;
+$numReaders = 6;
+
+@unlink($gateFile);
+
+// 1. Consumer script: holds class-name string literals in SHM interned strings
+// buffer (with refcount = 2 and no CE-cache flag yet).
+$consumerCode = "<?php\nfunction gh24577_names(): array {\n return [\n";
+for ($i = 0; $i < $numClasses; $i++) {
+ $consumerCode .= sprintf(" 'ColdCacheClass%04d',\n", $i);
+}
+$consumerCode .= " ];\n}\n";
+$consumerCode .= <<<'PHP'
+function gh24577_probe(array $names): bool {
+ foreach ($names as $class) {
+ if (class_exists($class, false)) {
+ $obj = new $class();
+ if (!($obj instanceof $class)) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+PHP;
+file_put_contents($consumerFile, $consumerCode);
+touch($consumerFile, time() - 3600);
+
+// 2. Provider script: declares the classes, triggering CE-cache slot publication
+// on the pre-existing shared interned strings when persisted by the writer.
+$classesCode = "<?php\n";
+for ($i = 0; $i < $numClasses; $i++) {
+ $classesCode .= sprintf("class ColdCacheClass%04d {}\n", $i);
+}
+file_put_contents($classesFile, $classesCode);
+touch($classesFile, time() - 3600);
+
+// Step 1: Warm up ONLY the consumer script in OPcache SHM so its class-name
+// strings are interned in shared memory with refcount = 2.
+$warmup = pcntl_fork();
+if ($warmup === -1) {
+ die("pcntl_fork failed\n");
+} elseif ($warmup === 0) {
+ require $consumerFile;
+ $status = opcache_get_status();
+ exit(isset($status['scripts'][$consumerFile]) ? 0 : 2);
+}
+pcntl_waitpid($warmup, $wstatus);
+if (!pcntl_wifexited($wstatus) || pcntl_wexitstatus($wstatus) !== 0) {
+ die("Warmup failed\n");
+}
+
+// Step 2: Fork Readers (which load the cached consumer script first, acquiring
+// the shared interned strings) and the Writer (which persists $classesFile),
+// synchronized by a start barrier.
+$children = [];
+
+for ($r = 0; $r < $numReaders; $r++) {
+ $pid = pcntl_fork();
+ if ($pid === -1) {
+ die("pcntl_fork failed\n");
+ } elseif ($pid === 0) {
+ require $consumerFile;
+ $names = gh24577_names();
+ file_put_contents($readyPrefix . $r, '1');
+ while (!file_exists($gateFile)) {
+ usleep(100);
+ }
+ // Probe shared interned strings while writer is persisting $classesFile
+ for ($iter = 0; $iter < 16; $iter++) {
+ if (!gh24577_probe($names)) {
+ exit(3);
+ }
+ }
+ require $classesFile;
+ if (!gh24577_probe($names)) {
+ exit(4);
+ }
+ exit(0);
+ }
+ $children[] = $pid;
+}
+
+$writer = pcntl_fork();
+if ($writer === -1) {
+ die("pcntl_fork failed\n");
+} elseif ($writer === 0) {
+ file_put_contents($readyPrefix . 'writer', '1');
+ while (!file_exists($gateFile)) {
+ usleep(100);
+ }
+ require $classesFile;
+ $status = opcache_get_status();
+ exit(isset($status['scripts'][$classesFile]) ? 0 : 5);
+}
+$children[] = $writer;
+
+// Wait until all readers and the writer are ready at the barrier
+$expectedReady = array_map(fn($r) => $readyPrefix . $r, range(0, $numReaders - 1));
+$expectedReady[] = $readyPrefix . 'writer';
+foreach ($expectedReady as $readyFile) {
+ while (!file_exists($readyFile)) {
+ usleep(200);
+ }
+}
+
+// Release barrier so writer persists $classesFile while readers probe $names
+file_put_contents($gateFile, '1');
+
+$failed = false;
+foreach ($children as $pid) {
+ pcntl_waitpid($pid, $status);
+ if (!pcntl_wifexited($status) || pcntl_wexitstatus($status) !== 0) {
+ $failed = true;
+ }
+}
+
+foreach ($expectedReady as $readyFile) {
+ @unlink($readyFile);
+}
+@unlink($gateFile);
+@unlink($consumerFile);
+@unlink($classesFile);
+
+echo $failed ? "Failed\n" : "Done\n";
+?>
+--CLEAN--
+<?php
+foreach (glob(__DIR__ . '/gh24577_*') ?: [] as $f) {
+ @unlink($f);
+}
+?>
+--EXPECT--
+Done
diff --git a/ext/opcache/zend_persist.c b/ext/opcache/zend_persist.c
index 8b8312ff81e..8e9167ac7d4 100644
--- a/ext/opcache/zend_persist.c
+++ b/ext/opcache/zend_persist.c
@@ -343,6 +343,7 @@ void zend_accel_pending_ce_cache_commit(void)
ZEND_ASSERT(!ZSTR_HAS_CE_CACHE(type_name));
GC_SET_REFCOUNT(type_name, (uint32_t)Z_LVAL_P(zv));
+ ZEND_ATOMIC_FENCE_RELEASE();
GC_ADD_FLAGS(type_name, IS_STR_CLASS_NAME_MAP_PTR);
} ZEND_HASH_FOREACH_END();