|
| 1 | +--TEST-- |
| 2 | +Bug GH-24577: CE-cache flag on shared interned class name must not crash with concurrent access |
| 3 | +--EXTENSIONS-- |
| 4 | +opcache |
| 5 | +pcntl |
| 6 | +--INI-- |
| 7 | +opcache.enable=1 |
| 8 | +opcache.enable_cli=1 |
| 9 | +opcache.interned_strings_buffer=8 |
| 10 | +opcache.file_update_protection=0 |
| 11 | +--FILE-- |
| 12 | +<?php |
| 13 | +$consumerFile = __DIR__ . '/gh24577_consumer.inc'; |
| 14 | +$classesFile = __DIR__ . '/gh24577_classes.inc'; |
| 15 | +$gateFile = __DIR__ . '/gh24577_gate.tmp'; |
| 16 | +$readyPrefix = __DIR__ . '/gh24577_ready_'; |
| 17 | +$numClasses = 64; |
| 18 | +$numReaders = 6; |
| 19 | + |
| 20 | +@unlink($gateFile); |
| 21 | + |
| 22 | +// 1. Consumer script: holds class-name string literals in SHM interned strings |
| 23 | +// buffer (with refcount = 2 and no CE-cache flag yet). |
| 24 | +$consumerCode = "<?php\nfunction gh24577_names(): array {\n return [\n"; |
| 25 | +for ($i = 0; $i < $numClasses; $i++) { |
| 26 | + $consumerCode .= sprintf(" 'ColdCacheClass%04d',\n", $i); |
| 27 | +} |
| 28 | +$consumerCode .= " ];\n}\n"; |
| 29 | +$consumerCode .= <<<'PHP' |
| 30 | +function gh24577_probe(array $names): bool { |
| 31 | + foreach ($names as $class) { |
| 32 | + if (class_exists($class, false)) { |
| 33 | + $obj = new $class(); |
| 34 | + if (!($obj instanceof $class)) { |
| 35 | + return false; |
| 36 | + } |
| 37 | + } |
| 38 | + } |
| 39 | + return true; |
| 40 | +} |
| 41 | +PHP; |
| 42 | +file_put_contents($consumerFile, $consumerCode); |
| 43 | +touch($consumerFile, time() - 3600); |
| 44 | + |
| 45 | +// 2. Provider script: declares the classes, triggering CE-cache slot publication |
| 46 | +// on the pre-existing shared interned strings when persisted by the writer. |
| 47 | +$classesCode = "<?php\n"; |
| 48 | +for ($i = 0; $i < $numClasses; $i++) { |
| 49 | + $classesCode .= sprintf("class ColdCacheClass%04d {}\n", $i); |
| 50 | +} |
| 51 | +file_put_contents($classesFile, $classesCode); |
| 52 | +touch($classesFile, time() - 3600); |
| 53 | + |
| 54 | +// Step 1: Warm up ONLY the consumer script in OPcache SHM so its class-name |
| 55 | +// strings are interned in shared memory with refcount = 2. |
| 56 | +$warmup = pcntl_fork(); |
| 57 | +if ($warmup === -1) { |
| 58 | + die("pcntl_fork failed\n"); |
| 59 | +} elseif ($warmup === 0) { |
| 60 | + require $consumerFile; |
| 61 | + $status = opcache_get_status(); |
| 62 | + exit(isset($status['scripts'][$consumerFile]) ? 0 : 2); |
| 63 | +} |
| 64 | +pcntl_waitpid($warmup, $wstatus); |
| 65 | +if (!pcntl_wifexited($wstatus) || pcntl_wexitstatus($wstatus) !== 0) { |
| 66 | + die("Warmup failed\n"); |
| 67 | +} |
| 68 | + |
| 69 | +// Step 2: Fork Readers (which load the cached consumer script first, acquiring |
| 70 | +// the shared interned strings) and the Writer (which persists $classesFile), |
| 71 | +// synchronized by a start barrier. |
| 72 | +$children = []; |
| 73 | + |
| 74 | +for ($r = 0; $r < $numReaders; $r++) { |
| 75 | + $pid = pcntl_fork(); |
| 76 | + if ($pid === -1) { |
| 77 | + die("pcntl_fork failed\n"); |
| 78 | + } elseif ($pid === 0) { |
| 79 | + require $consumerFile; |
| 80 | + $names = gh24577_names(); |
| 81 | + file_put_contents($readyPrefix . $r, '1'); |
| 82 | + while (!file_exists($gateFile)) { |
| 83 | + usleep(100); |
| 84 | + } |
| 85 | + // Probe shared interned strings while writer is persisting $classesFile |
| 86 | + for ($iter = 0; $iter < 16; $iter++) { |
| 87 | + if (!gh24577_probe($names)) { |
| 88 | + exit(3); |
| 89 | + } |
| 90 | + } |
| 91 | + require $classesFile; |
| 92 | + if (!gh24577_probe($names)) { |
| 93 | + exit(4); |
| 94 | + } |
| 95 | + exit(0); |
| 96 | + } |
| 97 | + $children[] = $pid; |
| 98 | +} |
| 99 | + |
| 100 | +$writer = pcntl_fork(); |
| 101 | +if ($writer === -1) { |
| 102 | + die("pcntl_fork failed\n"); |
| 103 | +} elseif ($writer === 0) { |
| 104 | + file_put_contents($readyPrefix . 'writer', '1'); |
| 105 | + while (!file_exists($gateFile)) { |
| 106 | + usleep(100); |
| 107 | + } |
| 108 | + require $classesFile; |
| 109 | + $status = opcache_get_status(); |
| 110 | + exit(isset($status['scripts'][$classesFile]) ? 0 : 5); |
| 111 | +} |
| 112 | +$children[] = $writer; |
| 113 | + |
| 114 | +// Wait until all readers and the writer are ready at the barrier |
| 115 | +$expectedReady = array_map(fn($r) => $readyPrefix . $r, range(0, $numReaders - 1)); |
| 116 | +$expectedReady[] = $readyPrefix . 'writer'; |
| 117 | +foreach ($expectedReady as $readyFile) { |
| 118 | + while (!file_exists($readyFile)) { |
| 119 | + usleep(200); |
| 120 | + } |
| 121 | +} |
| 122 | + |
| 123 | +// Release barrier so writer persists $classesFile while readers probe $names |
| 124 | +file_put_contents($gateFile, '1'); |
| 125 | + |
| 126 | +$failed = false; |
| 127 | +foreach ($children as $pid) { |
| 128 | + pcntl_waitpid($pid, $status); |
| 129 | + if (!pcntl_wifexited($status) || pcntl_wexitstatus($status) !== 0) { |
| 130 | + $failed = true; |
| 131 | + } |
| 132 | +} |
| 133 | + |
| 134 | +foreach ($expectedReady as $readyFile) { |
| 135 | + @unlink($readyFile); |
| 136 | +} |
| 137 | +@unlink($gateFile); |
| 138 | +@unlink($consumerFile); |
| 139 | +@unlink($classesFile); |
| 140 | + |
| 141 | +echo $failed ? "Failed\n" : "Done\n"; |
| 142 | +?> |
| 143 | +--CLEAN-- |
| 144 | +<?php |
| 145 | +foreach (glob(__DIR__ . '/gh24577_*') ?: [] as $f) { |
| 146 | + @unlink($f); |
| 147 | +} |
| 148 | +?> |
| 149 | +--EXPECT-- |
| 150 | +Done |
0 commit comments