Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Zend/zend.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
12 changes: 11 additions & 1 deletion Zend/zend_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

#include "zend_portability.h"
#include "zend_long.h"
#include "zend_atomic.h"
#include <stdbool.h>
#include <stdint.h>

Expand Down Expand Up @@ -862,7 +863,16 @@ 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)
{
if (GC_FLAGS(s) & IS_STR_CLASS_NAME_MAP_PTR) {
ZEND_ATOMIC_FENCE_ACQUIRE();
return true;
}
return false;
}

#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)

Expand Down
4 changes: 3 additions & 1 deletion ext/opcache/ZendAccelerator.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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);
}

Expand Down
150 changes: 150 additions & 0 deletions ext/opcache/tests/gh24577.phpt
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions ext/opcache/zend_persist.c
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Loading