Skip to content

[var-exporter] hydration bug with jit enabled #54053

Closed
@verfriemelt-dot-org

Description

@verfriemelt-dot-org

Symfony version(s) affected

7.0.3

Description

heyho

(this was tested with php 8.3.3 and php 8.3.2)

while enabling lazyghosts with doctrine i noticed some weird errors in production directly after cache-warmup. after some tinkering around i came to the conclusion, that the cache contents are just wrong while having jit enabled.
i tried to reduce the project as far as i could, while still beeing able to reproduce the error which i am going to outline here:

sample code: https://github.com/verfriemelt-dot-org/lazyghost-trait-bug

while loading the cached objects here

public static function getClassResetters($class)
    {
        $classProperties = [];

        if ((self::$classReflectors[$class] ??= new \ReflectionClass($class))->isInternal()) {
            $propertyScopes = [];
        } else {
            $propertyScopes = Hydrator::$propertyScopes[$class] ??= Hydrator::getPropertyScopes($class);
        }

it would receive weird data defined and will eventually fail here:

$reset($instance, $skippedProperties);
with

In LazyGhostTrait.php line 47:

  [Error]
  Value of type null is not callable

after some trail and error getting to now the error, i noticed that this is dependend on the current jit settings with php.

opcache.enable=1
opcache.enable_cli=1
opcache.jit_buffer_size=256M
opcache.jit=on

on is the same as tracing for that matter. if you run make function in the provided project, you'll get a cached result looking like this:

 private const LAZY_OBJECT_PROPERTY_SCOPES = [
     "\0".parent::class."\0".'apps' => [parent::class, 'apps', null],
     "\0".parent::class."\0".'code' => [parent::class, 'code', null],
     "\0".parent::class."\0".'config' => [parent::class, 'config', null],
     "\0".parent::class."\0".'created' => [parent::class, 'created', null],
     "\0".parent::class."\0".'currentRevisionNumber' => [parent::class, 'currentRevisionNumber', null],
     "\0".parent::class."\0".'inUse' => [parent::class, 'inUse', null],
     "\0".parent::class."\0".'name' => [parent::class, 'name', null],
     "\0".parent::class."\0".'token' => [parent::class, 'token', null],
     "\0".parent::class."\0".'updated' => [parent::class, 'updated', null],
     'apps' => [parent::class, 'apps', null],
     'code' => [parent::class, 'code', null],
     'config' => [parent::class, 'config', null],
     'created' => [parent::class, 'created', null],
     'currentRevisionNumber' => [parent::class, 'currentRevisionNumber', null],
     'inUse' => [parent::class, 'inUse', null],
     'name' => [parent::class, 'name', null],
     'token' => [parent::class, 'token', null],
     'updated' => [parent::class, 'updated', null],
 ];

which works as expected. while running make tracing we get this wrong defintion.

 private const LAZY_OBJECT_PROPERTY_SCOPES = [
     "\0".parent::class."\0".'apps' => [parent::class, 'apps', 'apps'],
     "\0".parent::class."\0".'code' => [parent::class, 'code', 'code'],
     "\0".parent::class."\0".'config' => [parent::class, 'config', 'config'],
     "\0".parent::class."\0".'created' => [parent::class, 'created', 'created'],
     "\0".parent::class."\0".'currentRevisionNumber' => [parent::class, 'currentRevisionNumber', 'currentRevisionNumber'],
     "\0".parent::class."\0".'inUse' => [parent::class, 'inUse', 'inUse'],
     "\0".parent::class."\0".'name' => [parent::class, 'name', 'name'],
     "\0".parent::class."\0".'token' => [parent::class, 'token', 'token'],
     "\0".parent::class."\0".'updated' => [parent::class, 'updated', 'updated'],
     'apps' => [parent::class, 'apps', 'apps'],
     'code' => [parent::class, 'code', 'code'],
     'config' => [parent::class, 'config', 'config'],
     'created' => [parent::class, 'created', 'created'],
     'currentRevisionNumber' => [parent::class, 'currentRevisionNumber', 'currentRevisionNumber'],
     'inUse' => [parent::class, 'inUse', 'inUse'],
     'name' => [parent::class, 'name', 'name'],
     'token' => [parent::class, 'token', 'token'],
     'updated' => [parent::class, 'updated', 'updated'],
 ];

the line beeing responsible for that can be found within Hydrator::getPropertyScopes() located here: https://github.com/symfony/symfony/blob/7.1/src/Symfony/Component/VarExporter/Internal/Hydrator.php#L265

            if (\ReflectionProperty::IS_PRIVATE & $flags) {
                $propertyScopes["\0$class\0$name"] = $propertyScopes[$name] = [$class, $name, $flags & \ReflectionProperty::IS_READONLY ? $class : null, $property];
                continue;
            }

while toying around with that, i got it working as expected with pulling the ternary operation out of the line:

            if (\ReflectionProperty::IS_PRIVATE & $flags) {
                $tmp = null;
                if ($flags & \ReflectionProperty::IS_READONLY) {
                    $tmp = $class;
                }
                $propertyScopes["\0$class\0$name"] = $propertyScopes[$name] = [$class, $name, $tmp, $property];
                continue;
            }

will work just fine.
also

            if (\ReflectionProperty::IS_PRIVATE & $flags) {
                $propertyScopes["\0$class\0$name"] = $propertyScopes[$name] = [$class, $name, $flags & \ReflectionProperty::IS_READONLY ? $class : null, $property];
                var_dump([$class, $name, $flags & \ReflectionProperty::IS_READONLY ? $class : null, $property]);
                continue;
            }

will segfault with my system 🤔 so actually i suspect a php jit bug here.
i hope we can resolv that issue. it was quite some work to track that down

How to reproduce

checkout the linked repo and the makefile within

Possible Solution

No response

Additional Context

No response

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