Skip to content

Opcache: optimizer overwrites the operator of ASSIGN_STATIC_PROP_OP, so $c::$$n += 3 segfaults or computes |= #24234

Description

@waroad

With opcache enabled, a compound assignment to a static property with a variable class name ($c::$$n += 2) runs with a corrupted operator: UBSan reports an out-of-bounds index in zend_binary_op(), and a release build segfaults or quietly applies a different operator.

<?php
class K { public static $s = 5; }
function f() { $c = "K"; $n = "s"; $c::$$n += 2; return K::$s; }
var_dump(f());
php -n -d opcache.enable_cli=1 -d opcache.file_update_protection=0 reduced.php
Zend/zend_execute.c:1668:24: runtime error: index 24 out of bounds for type '<unknown> *[12]'
    #0 in zend_binary_op Zend/zend_execute.c:1668
    #1 in ZEND_ASSIGN_STATIC_PROP_OP_SPEC_HANDLER Zend/zend_vm_execute.h:822
    #2 in execute_ex Zend/zend_vm_execute.h:110590
    #3 in zend_execute Zend/zend_vm_execute.h:116021
    #4 in zend_execute_script Zend/zend.c:1980

I expected int(7) (5 + 2), the same as with opcache off. opcache.file_update_protection=0 is only there because the file is new; with an older file, -d opcache.enable_cli=1 alone is enough.

Found at commit b527e1b, and the UBSan report still appears at c792847. A release build segfaults on this script (exit 139, no output). With one more cache slot in the function, the corrupted operator happens to be a valid one (ZEND_BW_OR). Then both builds print int(7) with no diagnostic, while opcache off and opcache.optimization_level=0 print int(8):

<?php
class K { public static $s = 5; }
function g() {}
function f() { g(); $c = "K"; $n = "s"; $c::$$n += 3; }
f();
var_dump(K::$s);

The opcode dump (opcache.opt_debug_level=0x30000) shows ASSIGN_STATIC_PROP_OP (ADD) before the optimizer and ASSIGN_STATIC_PROP_OP (ASSIGN_STATIC_PROP) after it. My guess is that zend_optimizer_update_op2_const() and zend_optimizer_update_op1_const() in Zend/Optimizer/zend_optimizer.c treat ASSIGN_STATIC_PROP_OP like the other static-prop opcodes and write the cache slot into opline->extended_value. For this opcode that field holds the operator, and the slot belongs in (opline+1)->extended_value. The op1 path can be hit with K::${N} += 1 (where N is a user constant) at opcache.optimization_level=0x4001.

Found with an LLM-based testing tool. As CONTRIBUTING.md asks, the paragraphs in > quotes were written with Claude; I ran every command myself.

Activity

  1. self-assigned this
    on Oct 10, 2026
  2. ndossche commented on Oct 10, 2026

    @ndossche
    Member

    Reproduces on 8.4+

  3. linked a pull request that will close this issueFix a bunch of cache slot optimizer bugs #24237on Oct 10, 2026
  4. added a commit that references this issue on Oct 10, 2026
    8ea97de
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions