-
Notifications
You must be signed in to change notification settings - Fork 7.9k
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
Changes from all commits
14e7c16
91e64af
b5c8b91
f3027d5
bb68202
ef4d15e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 */ | ||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mean There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought this is breaking since |
||
|
||
/* All entries that shouldn't be counted in the ADLER32 | ||
* checksum must be declared in this struct | ||
*/ | ||
|
@@ -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 { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
<?php | ||
|
||
class Bar extends Foo {} |
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 | ||
iluuu1994 marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we don't need sorting? |
||
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; | ||
|
@@ -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) { | ||
|
@@ -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; | ||
|
@@ -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)) { | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't understand the purpose of this allocation. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 */ | ||
|
@@ -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; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
|
||
|
@@ -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); | ||
|
@@ -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, | ||
|
@@ -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)) { | ||
|
@@ -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; | ||
|
@@ -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)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems, the compression doesn't reduce SHM memory consumption. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
||
|
Uh oh!
There was an error while loading. Please reload this page.