Skip to content

Commit a1a02ec

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

File tree

4 files changed

+104
-9
lines changed

4 files changed

+104
-9
lines changed

Zend/zend_inheritance.c

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3375,7 +3375,9 @@ ZEND_API zend_class_entry *zend_try_early_bind(zend_class_entry *ce, zend_class_
33753375
}
33763376

33773377
uint32_t is_cacheable = ce->ce_flags & ZEND_ACC_IMMUTABLE;
3378-
UPDATE_IS_CACHEABLE(parent_ce);
3378+
if (parent_ce) {
3379+
UPDATE_IS_CACHEABLE(parent_ce);
3380+
}
33793381
if (is_cacheable) {
33803382
if (zend_inheritance_cache_get && zend_inheritance_cache_add) {
33813383
zend_class_entry *ret = zend_inheritance_cache_get(ce, parent_ce, NULL);
@@ -3392,10 +3394,14 @@ ZEND_API zend_class_entry *zend_try_early_bind(zend_class_entry *ce, zend_class_
33923394
proto = ce;
33933395
}
33943396

3395-
orig_linking_class = CG(current_linking_class);
3396-
CG(current_linking_class) = NULL;
3397-
status = zend_can_early_bind(ce, parent_ce);
3398-
CG(current_linking_class) = orig_linking_class;
3397+
if (parent_ce) {
3398+
orig_linking_class = CG(current_linking_class);
3399+
CG(current_linking_class) = NULL;
3400+
status = zend_can_early_bind(ce, parent_ce);
3401+
CG(current_linking_class) = orig_linking_class;
3402+
} else {
3403+
status = INHERITANCE_SUCCESS;
3404+
}
33993405
if (EXPECTED(status != INHERITANCE_UNRESOLVED)) {
34003406
if (ce->ce_flags & ZEND_ACC_IMMUTABLE) {
34013407
/* Lazy class loading */
@@ -3420,9 +3426,11 @@ ZEND_API zend_class_entry *zend_try_early_bind(zend_class_entry *ce, zend_class_
34203426
zend_begin_record_errors();
34213427
}
34223428

3423-
zend_do_inheritance_ex(ce, parent_ce, status == INHERITANCE_SUCCESS);
3424-
if (parent_ce && parent_ce->num_interfaces) {
3425-
zend_do_inherit_interfaces(ce, parent_ce);
3429+
if (parent_ce) {
3430+
zend_do_inheritance_ex(ce, parent_ce, status == INHERITANCE_SUCCESS);
3431+
if (parent_ce->num_interfaces) {
3432+
zend_do_inherit_interfaces(ce, parent_ce);
3433+
}
34263434
}
34273435
zend_build_properties_info_table(ce);
34283436
if ((ce->ce_flags & (ZEND_ACC_IMPLICIT_ABSTRACT_CLASS|ZEND_ACC_INTERFACE|ZEND_ACC_TRAIT|ZEND_ACC_EXPLICIT_ABSTRACT_CLASS)) == ZEND_ACC_IMPLICIT_ABSTRACT_CLASS) {
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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,8 @@ static void zend_accel_do_delayed_early_binding(
360360
zend_class_entry *parent_ce = !(orig_ce->ce_flags & ZEND_ACC_LINKED)
361361
? zend_hash_find_ex_ptr(EG(class_table), early_binding->lc_parent_name, 1)
362362
: NULL;
363-
if (parent_ce || (orig_ce->ce_flags & ZEND_ACC_LINKED)) {
363+
if (parent_ce || (orig_ce->ce_flags & ZEND_ACC_LINKED)
364+
|| ZSTR_LEN(early_binding->lc_parent_name) == 0) {
364365
ce = zend_try_early_bind(orig_ce, parent_ce, early_binding->lcname, zv);
365366
}
366367
}

0 commit comments

Comments
 (0)