Nothing Special   »   [go: up one dir, main page]

Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Opcache corruption #8846

Closed
vnsavage opened this issue Jun 22, 2022 · 10 comments
Closed

Opcache corruption #8846

vnsavage opened this issue Jun 22, 2022 · 10 comments

Comments

@vnsavage
Copy link
Contributor
vnsavage commented Jun 22, 2022

Description

The following code:

  1. Clear PHP Opcache
  2. Add files:
$ cat test.php 
<?php
include( 'test-class1.php' );
include( 'test-class2.php' );

$ cat test-class1.php 
<?php
class test_class {
	public static $a = true;
}

$ cat test-class2.php 
<?php
var_dump( test_class::$a );
class test_class {
	public static $a = true;
}
  1. Issue a web request to test.php
  2. Remove the include -> "include( 'test-class1.php' )" from test.php
  3. Issue a second web request to test.php again

Resulted in this output:

First Request: Fatal error: Cannot declare class test_class, because the name is already in use in test-class2.php on line 5
Second Request: Fatal error: Uncaught Error: Class 'test_class' not found in test-class2.php:3 

But I expected this output instead:

First Request: Fatal error: Cannot declare class test_class, because the name is already in use in test-class2.php on line 5
Second request: bool(true)

PHP Version

PHP 8.0

Operating System

Linux

@cmb69
Copy link
Member
cmb69 commented Jun 23, 2022

Well, the problem is that on the first request, the test_class::$a in test-class2.php refers to test_class in test-class1.php. This reference is resolved, and stored in the op_array. On the second request, that test_class is no longer available, but OPcache doesn't automatically re-compile test-class2.php, so the error is raised. You can work around that by touching test-class2.php before doing the second request.

I think we should treat this as WONTFIX, since fixing this issue (and related issues) would make OPcache less efficient, and it seems to be a uncommon edge-case anyway.

@vnsavage
Copy link
Contributor Author
vnsavage commented Jun 24, 2022

It's very easy to reproduce with activating 2 different versions of a simple WordPress plugin twice, then realizing your error and deactivating one of the plugins. touching will probably fix it only if you 1) have opcache.validate_timestamps enabled and 2) also you know or are able to debug what the problem is. This is a very reduced test case that I posted but in practice it's not so obvious for most people what's happening.

@mvorisek
Copy link
Contributor

It is a bug, opcache should work /wo any side effect. Class can be loaded from cache only if the containing file was required/included in the current request/preload.

@cmb69
Copy link
Member
cmb69 commented Jun 25, 2022

One may argue that the user should cater to such changes by calling opcache_reset().

@vnsavage
Copy link
Contributor Author

It doesn't seem like this is how Opcache is supposed to work. If it were acceptable, which I don't think it is, that Opcache should be corrupted by certain user actions or have unexpected behavior, this should probably be well documented in a public visible document (e.g. https://www.php.net/opcache ) and all those cases described.

I don't think it's reasonable to expect a regular user or a developer or even a sys admin to be able to quickly see, diagnose and solve such a problem. Neither is calling opcache_reset() is a proper way to address any problem as it often causes more problems and is not scalable nor efficient. Also there are different environments you can consider, not only hosting your personal site, but it can also happen on places where you will not notice or resolve the problem quickly, like shared hosting services etc.

@IAMBANN
Copy link
IAMBANN commented Sep 22, 2022

Hi @vnsavage,
After issuing web request to test.php file including both the statements i.e. include( 'test-class1.php' ); and include( 'test-class2.php' ); it gives expected output with PHP v8.1

@vnsavage
Copy link
Contributor Author
vnsavage commented Sep 22, 2022

Hey @IAMBANN my tests show that it's still reproducible under PHP 8.1. Did you follow all the steps I listed, creating the exact same files and issuing 2 requests while modifying test.php between the 2 requests? Were you able to reproduce it with the earlier versions? Do you have opcache enabled?

@IAMBANN
Copy link
IAMBANN commented Oct 3, 2022

Hi @vnsavage,
Followed your steps and created the exact same files. After issuing web request to test.php, the expected output is still achieved while using PHP v7.4 and PHP v8.1 and also the opcache is enabled by following article.

@vnsavage
Copy link
Contributor Author
vnsavage commented Oct 4, 2022

Hey @IAMBANN

After issuing web request to test.php

The instructions call for 2 web requests to be issued, not one. The unexpected output appears only on the 2nd web request, and after the file modification, as that's the one using Opcache.

In order to try and stop this from going in circles, here is a list of commands you can run with a Docker image to reproduce it:

$ echo '<?php class test_class { public static $a = true; }' > test-class1.php
$ echo '<?php var_dump( test_class::$a ); class test_class { public static $a = true; }' > test-class2.php
$ echo '<?php include( "test-class1.php" ); include( "test-class2.php" );' > index.php
$ docker run  -p 8080:80  -d -e ERRORS=1 -v `pwd`:/var/www/html richarvey/nginx-php-fpm
$ curl http://localhost:8080
$ echo '<?php include( "test-class2.php" );' > index.php
$ curl http://localhost:8080
^ incorrect output is here

@iluuu1994
Copy link
Member

For context: https://www.npopov.com/2021/10/20/Early-binding-in-PHP.html

I had a look. This is what happens:

  1. Script 1 is compiled.
    • Class 1 is added to the class map directly since it has not been declared yet.
    • It is removed again after the compilation finishes.
  2. Script 1 executes.
    • Class 1 is added to the class map for this request.
  3. Script 2 is compiled
    • Class 2 is not added to the class map. Instead, it generates a RDK (runtime definition key) and adds an ZEND_DECLARE_CLASS opcode for declaration at runtime.
  4. Script 2 executes.
    • The var_dump executes.
    • The script fails due to redeclaration of the class.

On the second execution, only step 4 is executed. Usually this would work fine due to early binding, but since when the script was compiled the class was already declared no early binding is attempted.

Opcache actually already solves this problem with delayed early binding. When loading a script from opcache, all delayed early bindings are attempted again. However, for some reason delayed early binding is only implemented for classes with parent classes. I implemented added support for classes without parents here: #11227

This still has some subtle issues. In particular, the scripts are still order-dependent. That means if we compile files in one way, but then include them in another, we might get or not get delayed early binding. I think it would be better to consistently always delayed early bind classes when opcache is enabled, but I believe this would not be BC compatible, because with early binding, delayed early binding and then runtime definition we get a "3 level implicit topological sort" (meaning we can create three levels of inheritance in the same file in any order). Moving early binding to delayed early binding would remove one level. To be fair, this would already break without opcache, as we only have two levels with early binding and runtime definition.

@iluuu1994 iluuu1994 self-assigned this May 10, 2023
iluuu1994 added a commit to iluuu1994/php-src that referenced this issue May 10, 2023
iluuu1994 added a commit to iluuu1994/php-src that referenced this issue May 11, 2023
iluuu1994 added a commit to iluuu1994/php-src that referenced this issue May 12, 2023
iluuu1994 added a commit to iluuu1994/php-src that referenced this issue May 12, 2023
iluuu1994 added a commit to iluuu1994/php-src that referenced this issue May 12, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment