Skip to content

Fix GH-8065: opcache.consistency_checks > 0 causes segfaults in PHP >= 8.1.5 in fpm context (by reworking checksum skip list in a pragmatic way) #10624

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion ext/opcache/ZendAccelerator.c
Original file line number Diff line number Diff line change
Expand Up @@ -2120,7 +2120,7 @@ zend_op_array *persistent_compile_file(zend_file_handle *file_handle, int type)
unsigned int checksum = zend_accel_script_checksum(persistent_script);
if (checksum != persistent_script->dynamic_members.checksum ) {
/* The checksum is wrong */
zend_accel_error(ACCEL_LOG_INFO, "Checksum failed for '%s': expected=0x%08x, found=0x%08x",
zend_accel_error(ACCEL_LOG_WARNING, "Checksum failed for '%s': expected=0x%08x, found=0x%08x",
ZSTR_VAL(persistent_script->script.filename), persistent_script->dynamic_members.checksum, checksum);
zend_shared_alloc_lock();
if (!persistent_script->corrupted) {
Expand Down Expand Up @@ -4763,6 +4763,12 @@ static int accel_finish_startup(void)
ZCG(cwd_key_len) = 0;
ZCG(cwd_check) = 1;

ZCG(mem_checksum_skip_list) = NULL;
ZCG(mem_checksum_skip_list_count) = 0;
#if ZEND_DEBUG
ZCG(mem_checksum_skip_list_capacity) = 0;
#endif

if (accel_preload(ZCG(accel_directives).preload, in_child) != SUCCESS) {
ret = FAILURE;
}
Expand Down
27 changes: 27 additions & 0 deletions ext/opcache/ZendAccelerator.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,19 @@ typedef enum _zend_accel_restart_reason {
ACCEL_RESTART_USER /* restart scheduled by opcache_reset() */
} zend_accel_restart_reason;

typedef struct _zend_accel_skip_list_entry {
/* The offset indicates which byte offset within the memory block must be skipped.
* The size of the skip is equal to the system's pointer size. */
size_t offset;
/* To prevent creating a huge list with a lot of entries, we use a compression scheme
* based on the following two fields. If repetitions > 0, then the checksum algorithm
* will repeat `repetitions` times checksumming `checked_area_size` bytes, followed
* by skipping a pointer. For example, we use this to skip the `zend_op->handler` pointer.
* This allows us to only create one skip list entry per op array to handle *all* the opcodes. */
uint32_t checked_area_size;
uint32_t repetitions;
} zend_accel_skip_list_entry;

typedef struct _zend_persistent_script {
zend_script script;
zend_long compiler_halt_offset; /* position of __HALT_COMPILER or -1 */
Expand All @@ -123,6 +136,10 @@ typedef struct _zend_persistent_script {
void *mem; /* shared memory area used by script structures */
size_t size; /* size of used shared memory */

/* Some bytes must be skipped in the checksum calculation because they could've changed (legally)
* since the last checksum calculation. */
zend_accel_skip_list_entry *mem_checksum_skip_list;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed this in the previous review, I don't think we can even do this in a patch due to the ABI break.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean zend_inheritance_cache_entry **inheritance_cache;? We at least may try this for master. Fix for PHP-8.1 should keep binary compatibility, but I agree, it's dangerous. On the other hand targeting this PR to PHP-8.1 is also dangerous, because this is not a simple fix and it starts making changes even if opcache.consistency_checks == 0

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought this is breaking since sizeof(zend_persistent_script) and offsetof(zend_persistent_script, dynamic_members) change. Maybe that's not the case for dynamic libraries, I don't know the details of what dynamic linking does.


/* All entries that shouldn't be counted in the ADLER32
* checksum must be declared in this struct
*/
Expand Down Expand Up @@ -225,6 +242,16 @@ typedef struct _zend_accel_globals {
/* preallocated buffer for keys */
zend_string key;
char _key[MAXPATHLEN * 8];

zend_accel_skip_list_entry *mem_checksum_skip_list;
uint32_t mem_checksum_skip_list_count;
/* We don't actually need the capacity because the skip list is preallocated to the right size.
* We only use this in a debug build to check for bugs. If an assertion failure would ever trigger
* for this field, then it's signifies a bug in the persistence code because the calculation and
* actual size don't match. */
#if ZEND_DEBUG
uint32_t mem_checksum_skip_list_capacity;
#endif
} zend_accel_globals;

typedef struct _zend_string_table {
Expand Down
3 changes: 3 additions & 0 deletions ext/opcache/tests/gh8065.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

class Bar extends Foo {}
22 changes: 22 additions & 0 deletions ext/opcache/tests/gh8065.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
--TEST--
Bug GH-8065: Opcache zend_class_entry.inheritance_cache breaks persistent script checksum
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.consistency_checks=1
opcache.log_verbosity_level=2
opcache.protect_memory=1
--EXTENSIONS--
opcache
--FILE--
<?php

class Foo {}

require_once __DIR__ . '/gh8065.inc';

echo "Done\n";

?>
--EXPECT--
Done
30 changes: 27 additions & 3 deletions ext/opcache/zend_accelerator_util_funcs.c
Original file line number Diff line number Diff line change
Expand Up @@ -353,12 +353,36 @@ unsigned int zend_accel_script_checksum(zend_persistent_script *persistent_scrip
mem += (unsigned char*)persistent_script - mem;
}

zend_adler32(checksum, mem, persistent_script_check_block_size);
checksum = zend_adler32(checksum, mem, persistent_script_check_block_size);
mem += sizeof(*persistent_script);
size -= sizeof(*persistent_script);

if (size > 0) {
checksum = zend_adler32(checksum, mem, size);
if (UNEXPECTED(size == 0)) {
return checksum;
}

const zend_accel_skip_list_entry *skip_list_entry = persistent_script->mem_checksum_skip_list;

size_t offset = 0;
uint32_t next_stop_read;
while ((next_stop_read = skip_list_entry->offset) != 0) {
checksum = zend_adler32(checksum, mem + offset, next_stop_read - offset);
offset = next_stop_read + sizeof(void *); /* skip over the pointer */

uint32_t repetitions = skip_list_entry->repetitions;
const uint32_t checked_area_size = skip_list_entry->checked_area_size;
while (repetitions != 0) {
checksum = zend_adler32(checksum, mem + offset, checked_area_size);
offset += checked_area_size + sizeof(void *); /* skip over the pointer */
repetitions--;
}

skip_list_entry++;
}

if (size > offset) {
checksum = zend_adler32(checksum, mem + offset, size - offset);
}

return checksum;
}
71 changes: 70 additions & 1 deletion ext/opcache/zend_persist.c
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include "zend_operators.h"
#include "zend_interfaces.h"
#include "zend_attributes.h"
#include "zend_sort.h"

#ifdef HAVE_JIT
# include "Optimizer/zend_func_info.h"
Expand Down Expand Up @@ -87,6 +88,49 @@ static void zend_persist_op_array(zval *zv);
static const uint32_t uninitialized_bucket[-HT_MIN_MASK] =
{HT_INVALID_IDX, HT_INVALID_IDX};

static void mem_checksum_skip_list_add(void *p, uint32_t checked_area_size, uint32_t repetitions)
{
ZEND_ASSERT(ZCG(mem_checksum_skip_list) != NULL);
char *base_ptr = (char *) ZCG(current_persistent_script)->mem + sizeof(zend_persistent_script);
ZEND_ASSERT((char *) p >= base_ptr);
#if ZEND_DEBUG
ZEND_ASSERT(ZCG(mem_checksum_skip_list_count) < ZCG(mem_checksum_skip_list_capacity));
#endif
zend_accel_skip_list_entry *entry = &ZCG(mem_checksum_skip_list)[ZCG(mem_checksum_skip_list_count)++];
entry->offset = (char *) p - base_ptr;
entry->checked_area_size = checked_area_size - sizeof(char*);
entry->repetitions = repetitions;
}

static int mem_checksum_skip_list_compare(const zend_accel_skip_list_entry *l, const zend_accel_skip_list_entry *r)
{
if (l->offset < r->offset) {
return -1;
} else if (l->offset == r->offset) {
return 0;
} else {
return 1;
}
}

static void mem_checksum_skip_list_swap(zend_accel_skip_list_entry *l, zend_accel_skip_list_entry *r)
{
zend_accel_skip_list_entry tmp = *r;
*r = *l;
*l = tmp;
}

static zend_accel_skip_list_entry *mem_checksum_skip_list_persist(void)
{
zend_sort(ZCG(mem_checksum_skip_list), ZCG(mem_checksum_skip_list_count), sizeof(zend_accel_skip_list_entry), (compare_func_t) mem_checksum_skip_list_compare, (swap_func_t) mem_checksum_skip_list_swap);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we don't need sorting? zend_persist.c stores data in a preallocated linear space, so the initial skip-list should be already sorted. I may be wrong. Please check this.

zend_accel_skip_list_entry *result = zend_shared_memdup(ZCG(mem_checksum_skip_list), (ZCG(mem_checksum_skip_list_count) + 1) * sizeof(zend_accel_skip_list_entry));
zend_accel_skip_list_entry *last_sentinel = &result[ZCG(mem_checksum_skip_list_count)];
last_sentinel->offset = 0;
last_sentinel->checked_area_size = 0;
last_sentinel->repetitions = 0;
return result;
}

static void zend_hash_persist(HashTable *ht)
{
uint32_t idx, nIndex;
Expand Down Expand Up @@ -527,6 +571,13 @@ static void zend_persist_op_array_ex(zend_op_array *op_array, zend_persistent_sc
zend_op *end = new_opcodes + op_array->last;
int offset = 0;

#ifdef HAVE_JIT
if (ZCG(mem_checksum_skip_list) && JIT_G(on)) {
/* There is already one skip with 0 repetitions, so we have to subtract one */
mem_checksum_skip_list_add(new_opcodes, sizeof(zend_op), op_array->last - 1);
}
#endif

for (; opline < end ; opline++, offset++) {
#if ZEND_USE_ABS_CONST_ADDR
if (opline->op1_type == IS_CONST) {
Expand Down Expand Up @@ -760,6 +811,10 @@ static void zend_persist_class_method(zval *zv, zend_class_entry *ce)
return;
}
op_array = Z_PTR_P(zv) = zend_shared_memdup_put(op_array, sizeof(zend_op_array));
if (ZCG(mem_checksum_skip_list)) {
/* There is already one skip with 0 repetitions, so we have to subtract one */
mem_checksum_skip_list_add(&op_array->reserved, sizeof(char*), sizeof(op_array->reserved) / sizeof(op_array->reserved[0]) - 1);
}
zend_persist_op_array_ex(op_array, NULL);
if (ce->ce_flags & ZEND_ACC_IMMUTABLE) {
op_array->fn_flags |= ZEND_ACC_IMMUTABLE;
Expand Down Expand Up @@ -866,6 +921,9 @@ zend_class_entry *zend_persist_class_entry(zend_class_entry *orig_ce)
ce->ce_flags |= ZEND_ACC_FILE_CACHED;
}
ce->inheritance_cache = NULL;
if (ZCG(mem_checksum_skip_list)) {
mem_checksum_skip_list_add(&ce->inheritance_cache, 0, 0);
}

if (!(ce->ce_flags & ZEND_ACC_CACHED)) {
if (ZSTR_HAS_CE_CACHE(ce->name)) {
Expand Down Expand Up @@ -1282,6 +1340,13 @@ zend_persistent_script *zend_accel_script_persist(zend_persistent_script *script
{
Bucket *p;

/* The skip list count is still set by the persist_calc routines, which always precedes a call to this function. */
ZCG(mem_checksum_skip_list) = safe_emalloc(ZCG(mem_checksum_skip_list_count), sizeof(zend_accel_skip_list_entry), 0);
Copy link
Member

@dstogov dstogov Mar 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't understand the purpose of this allocation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, we may store entries directly in SHM.

#if ZEND_DEBUG
ZCG(mem_checksum_skip_list_capacity) = ZCG(mem_checksum_skip_list_count);
#endif
ZCG(mem_checksum_skip_list_count) = 0;

script->mem = ZCG(mem);

ZEND_ASSERT(((zend_uintptr_t)ZCG(mem) & 0x7) == 0); /* should be 8 byte aligned */
Expand Down Expand Up @@ -1346,7 +1411,11 @@ zend_persistent_script *zend_accel_script_persist(zend_persistent_script *script
}
#endif

script->corrupted = 0;
script->mem_checksum_skip_list = mem_checksum_skip_list_persist();
efree(ZCG(mem_checksum_skip_list));
ZCG(mem_checksum_skip_list) = NULL;

script->corrupted = false;
ZCG(current_persistent_script) = NULL;

return script;
Expand Down
19 changes: 18 additions & 1 deletion ext/opcache/zend_persist_calc.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
#include "zend_operators.h"
#include "zend_attributes.h"

#ifdef HAVE_JIT
#include "jit/zend_jit.h"
#endif

#define ADD_DUP_SIZE(m,s) ZCG(current_persistent_script)->size += zend_shared_memdup_size((void*)m, s)
#define ADD_SIZE(m) ZCG(current_persistent_script)->size += ZEND_ALIGNED_SIZE(m)

Expand Down Expand Up @@ -236,6 +240,11 @@ static void zend_persist_op_array_calc_ex(zend_op_array *op_array)

zend_shared_alloc_register_xlat_entry(op_array->opcodes, op_array->opcodes);
ADD_SIZE(sizeof(zend_op) * op_array->last);
#ifdef HAVE_JIT
if (JIT_G(on)) {
ZCG(mem_checksum_skip_list_count)++;
}
#endif

if (op_array->filename) {
ADD_STRING(op_array->filename);
Expand Down Expand Up @@ -340,6 +349,7 @@ static void zend_persist_class_method_calc(zval *zv)
if (!old_op_array) {
ADD_SIZE(sizeof(zend_op_array));
zend_persist_op_array_calc_ex(Z_PTR_P(zv));
ZCG(mem_checksum_skip_list_count)++;
zend_shared_alloc_register_xlat_entry(op_array, Z_PTR_P(zv));
} else {
/* If op_array is shared, the function name refcount is still incremented for each use,
Expand Down Expand Up @@ -399,6 +409,8 @@ void zend_persist_class_entry_calc(zend_class_entry *ce)

ADD_SIZE(sizeof(zend_class_entry));

ZCG(mem_checksum_skip_list_count)++;

if (!(ce->ce_flags & ZEND_ACC_CACHED)) {
ADD_INTERNED_STRING(ce->name);
if (ce->parent_name && !(ce->ce_flags & ZEND_ACC_LINKED)) {
Expand Down Expand Up @@ -572,6 +584,9 @@ uint32_t zend_accel_script_persist_calc(zend_persistent_script *new_persistent_s
{
Bucket *p;

/* Account for the final zero element that acts as a terminator. */
ZCG(mem_checksum_skip_list_count) = 1;

new_persistent_script->mem = NULL;
new_persistent_script->size = 0;
new_persistent_script->corrupted = 0;
Expand Down Expand Up @@ -607,7 +622,9 @@ uint32_t zend_accel_script_persist_calc(zend_persistent_script *new_persistent_s
zend_persist_warnings_calc(
new_persistent_script->num_warnings, new_persistent_script->warnings);

new_persistent_script->corrupted = 0;
ADD_SIZE(ZCG(mem_checksum_skip_list_count) * sizeof(zend_accel_skip_list_entry));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should lead to request of memory amount to keep uncompressed skip list together with the script. Right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems, the compression doesn't reduce SHM memory consumption.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, this is the reason of "wrong size calculation" you just found.


new_persistent_script->corrupted = false;

ZCG(current_persistent_script) = NULL;

Expand Down