Skip to content

Segfault on accessing enum cases with OPCache enabled #11807

@adlenton

Description

@adlenton

Description

In our project we've started adapting PHP enums and on our first "bigger scale" use case we're running into a segfault - seemingly on accessing an enum case.

gdb stacktrace:

Program received signal SIGSEGV, Segmentation fault.
zval_addref_p (pz=0x4412edb0) at ./Zend/zend_types.h:1240
1240    ./Zend/zend_types.h: No such file or directory.
(gdb) bt
#0  zval_addref_p (pz=0x4412edb0) at ./Zend/zend_types.h:1240
#1  zend_separate_class_constants_table (class_type=class_type@entry=0x4412e5a8) at ./Zend/zend_API.c:1331
#2  0x0000558eeb6306c8 in zend_class_constants_table (ce=0x4412e5a8) at ./Zend/zend_API.h:431
#3  zend_get_class_constant_ex (class_name=0x41758028, constant_name=0x41b8fd30, scope=scope@entry=0x4708c120, flags=512)
    at ./Zend/zend_constants.c:364
#4  0x0000558eeb6ca50c in zend_ast_evaluate (result=result@entry=0x7fff0b412fd0, ast=ast@entry=0x4708c4c8, scope=0x4708c120)
    at ./Zend/zend_ast.c:777
#5  0x0000558eeb6329ec in zval_update_constant_ex (scope=<optimized out>, p=0x7fb736c14440) at ./Zend/zend_execute_API.c:696
#6  zval_update_constant_ex (p=0x7fb736c14440, scope=<optimized out>) at ./Zend/zend_execute_API.c:671
#7  0x0000558eeb6ad06e in ZEND_RECV_INIT_SPEC_CONST_HANDLER () at ./Zend/zend_vm_execute.h:3736
#8  execute_ex (ex=0x4412e5a8) at ./Zend/zend_vm_execute.h:55951
#9  0x0000558eeb6b037d in zend_execute (op_array=0x7fb736c7b000, return_value=0x0) at ./Zend/zend_vm_execute.h:60163
#10 0x0000558eeb6421ed in zend_execute_scripts (type=type@entry=8, retval=retval@entry=0x0, file_count=file_count@entry=3)
    at ./Zend/zend.c:1846
#11 0x0000558eeb5dda41 in php_execute_script (primary_file=primary_file@entry=0x7fff0b415680) at ./main/main.c:2542
#12 0x0000558eeb487e0e in main (argc=<optimized out>, argv=<optimized out>) at ./sapi/fpm/fpm/fpm_main.c:1935
(gdb) zbacktrace
[0x7fb736c143d0] [...]\BoostHintVO->create("happiness_amount", 0)
[...]/BoostHintVO.php:16
[0x7fb736c14280] [...]\BonusOnSetAdjacencyAbilityFactory->buildAdjacencyBonuses(
array(3)[0x7fb736c142d0]) /[...]/BonusOnSetAdjacencyAbilityFactory.php:75
[...]
(gdb) backtrace full
#0  zval_addref_p (pz=0x4412edb0) at ./Zend/zend_types.h:1240
No locals.
#1  zend_separate_class_constants_table (class_type=class_type@entry=0x4412e5a8) at ./Zend/zend_API.c:1331
        _z = <optimized out>
        __ht = 0x4412e658
        _p = <optimized out>
        _end = 0x4412ec80
        mutable_data = <optimized out>
        constants_table = 0x7fb7314c14b8
        key = 0x41b8fd30
        new_c = <optimized out>
        c = 0x4412edb0
#2  0x0000558eeb6306c8 in zend_class_constants_table (ce=0x4412e5a8) at ./Zend/zend_API.h:431
        mutable_data = <optimized out>
#3  zend_get_class_constant_ex (class_name=0x41758028, constant_name=0x41b8fd30, scope=scope@entry=0x4708c120, flags=512)
    at ./Zend/zend_constants.c:364
        ce = 0x4412e5a8
        c = 0x0
        ret_constant = 0x0
#4  0x0000558eeb6ca50c in zend_ast_evaluate (result=result@entry=0x7fff0b412fd0, ast=ast@entry=0x4708c4c8, scope=0x4708c120)
    at ./Zend/zend_ast.c:777
        class_name = <optimized out>
        const_name = <optimized out>
        zv = <optimized out>
        op1 = {value = {lval = 1103188432, dval = 5.450475051406721e-315, counted = 0x41c151d0, str = 0x41c151d0, arr = 0x41c151d0,
            obj = 0x41c151d0, res = 0x41c151d0, ref = 0x41c151d0, ast = 0x41c151d0, zv = 0x41c151d0, ptr = 0x41c151d0, ce = 0x41c151d0,
            func = 0x41c151d0, ww = {w1 = 1103188432, w2 = 0}}, u1 = {type_info = 3949145561, v = {type = 217 '\331', type_flags = 45 '-',
              u = {extra = 60259}}}, u2 = {next = 21902, cache_slot = 21902, opline_num = 21902, lineno = 21902, num_args = 21902,
            fe_pos = 21902, fe_iter_idx = 21902, property_guard = 21902, constant_flags = 21902, extra = 21902}}
        op2 = {value = {lval = 94072325571128, dval = 4.6477904289087367e-310, counted = 0x558eeb8c8238 <zend_autoload>,
            str = 0x558eeb8c8238 <zend_autoload>, arr = 0x558eeb8c8238 <zend_autoload>, obj = 0x558eeb8c8238 <zend_autoload>,
            res = 0x558eeb8c8238 <zend_autoload>, ref = 0x558eeb8c8238 <zend_autoload>, ast = 0x558eeb8c8238 <zend_autoload>,
            zv = 0x558eeb8c8238 <zend_autoload>, ptr = 0x558eeb8c8238 <zend_autoload>, ce = 0x558eeb8c8238 <zend_autoload>,
            func = 0x558eeb8c8238 <zend_autoload>, ww = {w1 = 3951854136, w2 = 21902}}, u1 = {type_info = 7, v = {type = 7 '\a',
              type_flags = 0 '\000', u = {extra = 0}}}, u2 = {next = 21902, cache_slot = 21902, opline_num = 21902, lineno = 21902,
            num_args = 21902, fe_pos = 21902, fe_iter_idx = 21902, property_guard = 21902, constant_flags = 21902, extra = 21902}}
        ret = SUCCESS
#5  0x0000558eeb6329ec in zval_update_constant_ex (scope=<optimized out>, p=0x7fb736c14440) at ./Zend/zend_execute_API.c:696
        tmp = {value = {lval = 776, dval = 3.8339494117280732e-321, counted = 0x308, str = 0x308, arr = 0x308, obj = 0x308, res = 0x308,
            ref = 0x308, ast = 0x308, zv = 0x308, ptr = 0x308, ce = 0x308, func = 0x308, ww = {w1 = 776, w2 = 0}}, u1 = {
            type_info = 2236897536, v = {type = 0 '\000', type_flags = 89 'Y', u = {extra = 34132}}}, u2 = {next = 1646290205,
            cache_slot = 1646290205, opline_num = 1646290205, lineno = 1646290205, num_args = 1646290205, fe_pos = 1646290205,
            fe_iter_idx = 1646290205, property_guard = 1646290205, constant_flags = 1646290205, extra = 1646290205}}
        ast_ref = 0x4708c4c0
        ast_is_refcounted = <optimized out>
        result = <optimized out>
        ast = 0x4708c4c8
#6  zval_update_constant_ex (p=0x7fb736c14440, scope=<optimized out>) at ./Zend/zend_execute_API.c:671
        ast = <optimized out>
        name = <optimized out>
        zv = <optimized out>
        _z1 = <optimized out>
        _z2 = <optimized out>
        _gc = <optimized out>
        _t = <optimized out>
        tmp = {value = {lval = <optimized out>, dval = <optimized out>, counted = <optimized out>, str = <optimized out>,
            arr = <optimized out>, obj = <optimized out>, res = <optimized out>, ref = <optimized out>, ast = <optimized out>,
            zv = <optimized out>, ptr = <optimized out>, ce = <optimized out>, func = <optimized out>, ww = {w1 = <optimized out>,
              w2 = <optimized out>}}, u1 = {type_info = <optimized out>, v = {type = <optimized out>, type_flags = <optimized out>, u = {
--Type <RET> for more, q to quit, c to continue without paging--c
                extra = <optimized out>}}}, u2 = {next = <optimized out>, cache_slot = <optimized out>, opline_num = <optimized out>, lineno = <optimized out>, num_args = <optimized out>, fe_pos = <optimized out>, fe_iter_idx = <optimized out>, property_guard = <optimized out>, constant_flags = <optimized out>, extra = <optimized out>}}
        ast_ref = <optimized out>
        ast_is_refcounted = <optimized out>
        result = <optimized out>
        _z1 = <optimized out>
        _z2 = <optimized out>
        _gc = <optimized out>
        _t = <optimized out>
#7  0x0000558eeb6ad06e in ZEND_RECV_INIT_SPEC_CONST_HANDLER () at ./Zend/zend_vm_execute.h:3736
        cache_val = 0x7fb7314c14a8
        default_value = 0x7fb7306424b0
        arg_num = <optimized out>
        param = 0x7fb736c14440
        arg_num = <optimized out>
        param = <optimized out>
        default_value = <optimized out>
        cache_val = <optimized out>
        _z1 = <optimized out>
        _z2 = <optimized out>
        _gc = <optimized out>
        _t = <optimized out>
        _z1 = <optimized out>
        _z2 = <optimized out>
        _gc = <optimized out>
        _t = <optimized out>
        _z1 = <optimized out>
        _z2 = <optimized out>
        _gc = <optimized out>
        _t = <optimized out>
        _z1 = <optimized out>
        _z2 = <optimized out>
        _gc = <optimized out>
        _t = <optimized out>

Unfortunately I was not able to construct a minimal reproducable setup, therefore I will add some context on the project and the enums classes involved.

Our php modulues:

php -m
[PHP Modules]
apcu
bcmath
calendar
Core
ctype
curl
date
dba
dom
exif
FFI
fileinfo
filter
ftp
gd
gettext
hash
iconv
igbinary
json
libxml
mbstring
openssl
pcntl
pcre
PDO
pdo_pgsql
pgsql
Phar
posix
readline
redis
Reflection
session
shmop
SimpleXML
sockets
sodium
SPL
standard
sysvmsg
sysvsem
sysvshm
tideways
tokenizer
uuid
xml
xmlreader
xmlwriter
xsl
Zend OPcache
zip
zlib

[Zend Modules]
Zend OPcache

The enum we've added:

enum BoostTargetedFeatureEnum: string implements JsonSerializable
{
    use EnumSerializableTrait;

    public static function getFrom(?string $value): self
    {
        if (!$value) {
            return self::All;
        }

        return self::tryFrom($value) ?? self::All;
    }

    case GuildExpedition   = 'guild_expedition';
    case GuildBattleground = 'battleground';
    case All               = 'all';
}
<?php

The trait we're including:

trait EnumSerializableTrait
{
    public function jsonSerialize(): array
    {
        $value = $this->value ?? $this->name;

        return ['__enum__' => $this->getEnumName(), 'value' => $value];
    }

    public function getEnumName(): string
    {
        return str_replace('Enum', '', strip_namespace($this));
    }
}

The BoostHintVO class mentioned in the stacktrace:

class BoostHintVO
{
    public string $type;
    public int $value;
    public BoostTargetedFeatureEnum $targetedFeature = BoostTargetedFeatureEnum::All;

    public static function create(
        string $type,
        int $value,
        BoostTargetedFeatureEnum $target = BoostTargetedFeatureEnum::All
    ): BoostHintVO {
        $vo                  = new BoostHintVO();
        $vo->type            = $type;
        $vo->value           = $value;
        $vo->targetedFeature = $target;

        return $vo;
    }
}

line 16 from the stacktrace is the line of the constructor with the reference to the ::All case.

The segfault only occurs with opcache enabled. The first request on a freshly restarted php-fpm process will succeed, the second request fails with the segfault.
The enum case All is being referenced from 30 places in our codebase, a lot of times as a default argument on an optional method parameter.
The segfault also occurred when modifying the BoostHintVO::create to accept a nullable string when we called the BoostTargetedFeatureEnum::getFrom inside the class - in case it's relevant, this static create fuction (as well as other places in the code that will reference that new enum) will get called a lot from different factories in our code that produce these VOs that are sent to clients. If there is any more context I can or should provide please do let me know!

When searching for other segfaults related to php enums I found #10914 where the faulty/changed code looks fairly similar to where it's failing according the stacktrace:

ZEND_HASH_FOREACH_STR_KEY_PTR(&class_type->constants_table, key, c) {

vs
ndossche@e6abc93#diff-a9c2ca78a68a3ad33d5b00dd7f1d37aa2ac438dbff79d56d06918aefaf6fe4af

This might be absolutely unrelated as I have no prior experience in php core development, however I wanted to mention it in case the causes are indeed related.

PHP Version

PHP 8.1.21

Operating System

Debian 11.7

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