Skip to content

OPCache with Enum and Callback functions results in segmentation fault #10914

Closed

Description

@miguelantoniosantos

Description

The following code:

ExampleEnum.php

<?php

enum ExampleEnum: string
{
    case FIRST= 'FIRST_VALUE';
}

Trigger.php

<?php
function auxiliarFunction(string $enumEntry) {}

function triggerException(callable $callback) {
     $callback();
}

function main() {
    triggerException(function () {
          auxiliarFunction(ExampleEnum::FIRST->value); //This will not run and trigger a SIGSEGV - core dumped
    });
}

This was first spotted on Symfony 5.4 codebase, with a callback of the TagAwareCacheInterface.
If OPCache is disabled, the error doesn't occur.
Replacing the enum with a string variable fixes the issue.

PHP Version

PHP 8.1.11 (fpm-fcgi)

Operating System

Ubuntu 20.04.5 LTS

Activity

  1. miguelantoniosantos commented on Mar 23, 2023

    @miguelantoniosantos
    Author

    We also found out that ExampleEnum::cases() passed with the function () use (), also causes a segmentation fault.

    $cases = ExampleEnum::cases();
    triggerException(function () use ($cases) {
         auxiliarFunction($cases); //This will not run and trigger a SIGSEGV - core dumped
    });

    We mitigated the issue with

    $casesWorkAround = ExampleEnum::cases();
    $cases = [];
    foreach ($casesWorkAround as $caseEntry) {
        $cases [] = $caseEntry->value;
    }

    If needed I can create a docker with a symfony project that recreates this issue.

  2. ndossche commented on Mar 23, 2023

    @ndossche
    Member

    I've been unable to reproduce this so far. I tested this without Symfony though, just with the two provided files.
    Are you using the JIT or not? If you're using the JIT, does it also reproduce without?
    What extensions did you use when you could reproduce this?
    Does this always reproduce reliably?

    If needed I can create a docker with a symfony project that recreates this issue.

    This would be very helpful!

    Alternatively, are you able to provide a backtrace from GDB?
    To find out how to generate a backtrace, please read http://bugs.php.net/bugs-generating-backtrace.php for Linux/macos and http://bugs.php.net/bugs-generating-backtrace-win32.php for Windows.

  3. miguelantoniosantos commented on Mar 24, 2023

    @miguelantoniosantos
    Author

    We are able to recreate this with a 100% crash rate.
    I will fetch the php -i details; backtrace and recreate the issue with docker.

    I expect to have this ready at max in 7 days, will try today but I can't guarantee it

  4. miguelantoniosantos commented on Mar 24, 2023

    @miguelantoniosantos
    Author

    After some debugging it seems like this:

    • Write Enum::cases to redis cache, throws segsev
    • After that any access to the enum inside the callback used by redis cache function will throw segsev

    I wasn't able to recreate a docker instance with it.
    I believe I setup the core dump correctly here is the link (size: 349MB): <snip, removed coredump file>

    Gets weirder in every debug, I am able to duplicate it constantly on our codebase but I am not allowed to share it...

    Might be an issue with Symfony, or the redis, but xdebug dies on the Enum::cases or Enum::Entry->value.

    PHP Ini: https://gist.github.com/miguelantoniosantos/200ebb03dbc1996cbbc6c7edc8510d18

  5. ndossche commented on Mar 24, 2023

    @ndossche
    Member

    Thanks. It's definitely not a JIT issue because that's disabled. Since you mention this happens with redis, this makes me wonder if it's an issue with that extension instead of PHP itself.
    I assume you're using https://github.com/phpredis/phpredis ?
    I can open the coredump with a multiarch gdb build, but can't do much because I'm on x86 instead of ARM.
    Is it possible to load the coredump on the machine it was generated on and run the gdb command: bt full ?

  6. miguelantoniosantos commented on Mar 24, 2023

    @miguelantoniosantos
    Author

    I am able to do bt full on a similar machine (machine that generate it is already terminated, build image is the same)
    We are using predis and not PHPRedis.

    If you need any more info from me, please guide me as my knowledge of gdb is far lost.

  7. ndossche commented on Mar 24, 2023

    @ndossche
    Member

    Interesting how it's not a PECL extension causing the issue. bt full on a similar machine is probably fine too as their image is the same.

  8. miguelantoniosantos commented on Mar 24, 2023

    @miguelantoniosantos
    Author

    For more details on the machine we use a AWS c6g.large, with Ubuntu 20.04.5 ARM base image, nothing was compiled from source, all installed from ppa:ondrej/php

    The Redis server is ElastiCache Redis, and we made sure the server had sufficient RAM and CPU to handle the requests.

  9. ndossche commented on Mar 24, 2023

    @ndossche
    Member

    I'm not sure if I actually explained the previous instructions properly.
    Can you please share the output of bt full in gdb with your core dump loaded? I don't have access to an ARM machine and so I can't do much on my system with the coredump you provided.
    To load the core dump you can use gdb path/to/binary path/to/coredump. Then you can type in the bt full command.

  10. 23 remaining items

  11. ndossche commented on Jul 11, 2023

    @ndossche
    Member

    With protect_memory enabled, this is the minimal reproducer, which will crash instantly if enum.php is preloaded:

    <?php
    $x = new ReflectionEnum(ExampleEnum::class);
    var_dump($x->getCases()[0]->getValue());

    No need to externals dependencies.

    The external dependencies just created the "lucky" heap layout that made it able to hit the issue without protect_memory.

  12. iluuu1994 commented on Jul 11, 2023

    @iluuu1994
    Member

    @nielsdos I haven't tried it, but I think this line needs to be fixed:

    ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&ce->constants_table, name, constant) {

    We're accessing the class constants directly, instead of using CE_CONSTANTS_TABLE(ce), which separates the table if it contains constant ASTs.

    Thank you a lot for the analysis!

    Edit: And it looks like getCase will have the same issue.

  13. ndossche commented on Jul 11, 2023

    @ndossche
    Member

    @iluuu1994 Yeah I also found that. I just wasn't sure about the overhead but if this is the general approach I'll go with that.
    I'm checking all code now.

  14. iluuu1994 commented on Jul 11, 2023

    @iluuu1994
    Member

    @nielsdos Class constants should only be evaluated once. If we copy the constant AST and evaluate it then, we'll lose the value and evaluate it again on the next access. I think, for enum cases this would mean getting non-unique case values. So I don't think we have another option.

  15. ndossche commented on Jul 11, 2023

    @ndossche
    Member

    Both seemed to work so thanks for your explanation on why we need separation. I'll fix it using the separation.

  16. iluuu1994 commented on Jul 11, 2023

    @iluuu1994
    Member

    Thank you a lot @nielsdos!

  17. added a commit that references this issue on Jul 11, 2023
    e6abc93
  18. magnetik commented on Jul 11, 2023

    @magnetik

    Awesome @nielsdos ! Thanks for the fix. looking forward to have it :)

  19. added 2 commits that reference this issue on Jul 11, 2023
    bc42179
    832a3d1
  20. added a commit that references this issue on Jul 11, 2023
    62a9408
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions