Skip to content

Commit ac94d59

Browse files
committed
Fix GH-21411: opcache_compile_file() fails to early-bind classes without parents
1 parent 1b61d55 commit ac94d59

File tree

3 files changed

+92
-0
lines changed

3 files changed

+92
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
echo NoParentClass::VALUE . "\n";
3+
class NoParentClass {
4+
const VALUE = "hello";
5+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
--TEST--
2+
opcache_compile_file() should early-bind parentless classes
3+
--EXTENSIONS--
4+
opcache
5+
--INI--
6+
opcache.enable=1
7+
opcache.enable_cli=1
8+
--FILE--
9+
<?php
10+
$cacheDir = __DIR__ . '/opcache_early_bind_no_parent_cache';
11+
@mkdir($cacheDir, 0777, true);
12+
13+
$incFile = __DIR__ . '/opcache_compile_file_early_bind_no_parent.inc';
14+
$php = PHP_BINARY;
15+
16+
/* Detect opcache extension path for subprocess invocations.
17+
* Always search for the shared module since subprocesses won't inherit
18+
* the parent's zend_extension settings from run-tests.php. */
19+
$opcacheExt = '';
20+
$candidates = [
21+
ini_get('extension_dir') . '/opcache.so',
22+
ini_get('extension_dir') . '/opcache',
23+
dirname(PHP_BINARY, 3) . '/modules/opcache.so',
24+
dirname(PHP_BINARY, 2) . '/modules/opcache.so',
25+
];
26+
foreach ($candidates as $path) {
27+
$real = realpath($path);
28+
if ($real !== false) {
29+
$opcacheExt = '-d zend_extension=' . escapeshellarg($real);
30+
break;
31+
}
32+
}
33+
34+
$opcacheArgs = implode(' ', array_filter([
35+
'-n',
36+
$opcacheExt,
37+
'-d opcache.enable=1',
38+
'-d opcache.enable_cli=1',
39+
'-d opcache.file_cache=' . escapeshellarg($cacheDir),
40+
'-d opcache.file_cache_only=1',
41+
'-d opcache.file_update_protection=0',
42+
'-d opcache.jit=disable',
43+
]));
44+
45+
/* First invocation: compile the file into file cache */
46+
$cmd = "$php $opcacheArgs -r " . escapeshellarg(
47+
'opcache_compile_file(' . var_export($incFile, true) . '); echo "compiled\n";'
48+
);
49+
echo shell_exec($cmd);
50+
51+
/* Second invocation: include the file from file cache.
52+
* The parentless class must be early-bound for NoParentClass::VALUE
53+
* to be accessible before the ZEND_DECLARE_CLASS_DELAYED opcode runs. */
54+
$cmd = "$php $opcacheArgs -r " . escapeshellarg(
55+
'include ' . var_export($incFile, true) . '; echo "included\n";'
56+
);
57+
echo shell_exec($cmd);
58+
?>
59+
--CLEAN--
60+
<?php
61+
function removeDirRecursive($dir) {
62+
if (!is_dir($dir)) return;
63+
$iterator = new RecursiveIteratorIterator(
64+
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
65+
RecursiveIteratorIterator::CHILD_FIRST
66+
);
67+
foreach ($iterator as $fileinfo) {
68+
if ($fileinfo->isDir()) {
69+
@rmdir($fileinfo->getRealPath());
70+
} else {
71+
@unlink($fileinfo->getRealPath());
72+
}
73+
}
74+
@rmdir($dir);
75+
}
76+
removeDirRecursive(__DIR__ . '/opcache_early_bind_no_parent_cache');
77+
?>
78+
--EXPECT--
79+
compiled
80+
hello
81+
included

ext/opcache/zend_accelerator_util_funcs.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,12 @@ static void zend_accel_do_delayed_early_binding(
360360
: NULL;
361361
if (parent_ce || (orig_ce->ce_flags & ZEND_ACC_LINKED)) {
362362
ce = zend_try_early_bind(orig_ce, parent_ce, early_binding->lcname, zv);
363+
} else if (ZSTR_LEN(early_binding->lc_parent_name) == 0) {
364+
/* Parentless class: use the same binding path as the VM handler */
365+
zval lcname_zv[2];
366+
ZVAL_STR(&lcname_zv[0], early_binding->lcname);
367+
ZVAL_STR(&lcname_zv[1], early_binding->rtd_key);
368+
ce = zend_bind_class_in_slot(zv, lcname_zv, early_binding->lc_parent_name);
363369
}
364370
}
365371
if (ce && early_binding->cache_slot != (uint32_t) -1) {

0 commit comments

Comments
 (0)