Skip to content

Segfault in zend_try_early_bind when a parent class autoloads its cached child during linking (opcache) #24534

Open

Description

@wimwinterberg

Description

With opcache enabled, a worker segfaults when a parent class is linked first and, during its variance check, autoloads its own child class whose file is already cached in opcache. Opcache then performs delayed early binding of the cached child against the parent that is still being linked, and copies from a NULL pointer.

Without opcache (opcache.enable=0) the same code works.

Reproduced with the official images php:8.3-cli (8.3.36), php:8.4-cli (8.4.26) and php:8.5-cli (8.5.12), and in production on 8.4.27 FPM (Ubuntu 24.04, deb.sury.org). JIT is not involved (opcache.jit=disable in production).

src/EntityInterface.php

<?php
interface EntityInterface
{
    public static function getById(int $id): ?self;
}

src/Entity.php

<?php
abstract class Entity implements EntityInterface
{
    protected array $data = [];
    protected bool $loaded = false;
}

src/UserBase.php

<?php
abstract class UserBase extends Entity
{
    public const TABLE = 'user';

    protected ?int $userId = null;
    protected ?string $name = null;
    protected ?int $position = null;

    public static function getById(int $id): ?User
    {
        return null;
    }
}

src/User.php

<?php
class User extends UserBase
{
}

public/autoload.php

<?php
spl_autoload_register(static function (string $class): void {
    $file = __DIR__ . '/../src/' . $class . '.php';
    if (is_file($file)) {
        require $file;
    }
});

public/base-first.php

<?php
require __DIR__ . '/autoload.php';
echo 'base-first: ', UserBase::TABLE, "\n";

public/child-first.php

<?php
require __DIR__ . '/autoload.php';
echo 'child-first: ', User::TABLE, "\n";

Run the built-in server with opcache and request the pages in this order:

php -d opcache.enable=1 -d opcache.enable_cli=1 -S 127.0.0.1:8080 -t public &
curl http://127.0.0.1:8080/base-first.php   # 200 "base-first: user" (User.php compiled fresh)
curl http://127.0.0.1:8080/child-first.php  # 200 "child-first: user" (User.php now cached)
curl http://127.0.0.1:8080/base-first.php   # server crashes

(On the official images opcache has to be loaded with -d zend_extension=opcache for 8.3/8.4.)

Resulted in this output:

base-first: user
child-first: user
base-first:

and the server process exits with signal 11 (exit code 139).

But I expected this output instead:

base-first: user
child-first: user
base-first: user

Backtrace from a production core dump (PHP 8.4.27 FPM, binary without debug symbols, frame names from .dynsym):

#0  __memmove_avx_unaligned (libc)      rsi (src) = 0x0, rdx (n) = 0x28
#1  ?? (php-fpm8.4)
#2  zend_try_early_bind ()
#3  ?? () from opcache.so
#4  compile_filename ()
#5-#10  execute_ex / zend_call_function / zend_lookup_class_ex   (autoload of the child)
#11 zend_lookup_class_ex ()
#12 ?? () from opcache.so
#13 zend_do_link_class ()                                          (linking the parent)
#14 zend_bind_class_in_slot ()
#15-#29 execute_ex / zend_execute_script / php_execute_script_ex

PHP stack at the crash: UserBase::TABLE -> autoload UserBase -> linking UserBase -> autoload User -> include of the cached User.php.

Workaround in user land: reference the child class (User::TABLE) instead of the parent, so the child is always linked first.

PHP Version

PHP 8.3.36, 8.4.26, 8.4.27, 8.5.12

Operating System

Debian (official Docker images), Ubuntu 24.04

Activity

  1. self-assigned this
    on Oct 22, 2026
  2. srayuth089 commented on Oct 25, 2026

    @srayuth089

    Independent confirmation using the class/autoload fixture and request order from this report, served through Apache mod_proxy_fcgi to official PHP-FPM images:

    • With a one-child static pool, GET /base-first.php, GET /child-first.php, GET /base-first.php returned 200, 200, 503 on PHP 8.2.35, 8.3.36, 8.4.27, and 8.5.12. Each run logged the FPM child exiting on SIGSEGV and a replacement starting. This adds confirmation on the 8.2 branch.
    • On PHP 8.4.27 with two static children and the image default opcache.validate_timestamps=1, the first and final base-first.php requests ran in PID 8; child-first.php ran in PID 7. The final request returned 503 and PID 8 exited on SIGSEGV. The cache-warming request therefore did not need to hit the worker that later crashed in this setup.
    • Repeating requests caused more worker crashes and 503 responses. The effect demonstrated here is worker termination and availability loss only; I did not demonstrate code execution or data disclosure.

    These are independent reproductions of the reported trigger, not a separate root cause or a claim of original discovery. The test used public, unauthenticated fixture routes and ordinary GET requests; whether a deployed application exposes a matching class/autoload path is application-dependent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions