Skip to content

Commit c2c91d4

Browse files
committed
Fix phpGH-8065: Rework checksum skip list in a pragmatic way
Fixes phpGH-8065 (see also for analysis) The class inheritance cache must be excluded from the checksum computation because it may change (legally) in between the computation and recomputation. Furthermore the handler pointer in the zend_op structure may be modified by the JIT, and the op_array->reserved[] could be changed as well. This approach is based on iluuu1994's original approach, with some slight changes. First let's explain the main idea. The idea is to keep a "skip list" of offsets to skip in the checksum computation. This will include the offsets described above (class inheritance cache, handler, reserved). These skips are of size pointer_size (equals to the native pointer size). It differs in the sense that this is a pragmatic approach. It was found that in the original approach it was very difficult to figure out what can be exactly changed when, and let to redundant computation of the CFG for example. Furthermore, such a process is also error-prone and complex to maintain. Instead of that, we just always skip the handler pointer and the reserved area. The positive is that it leads to code which is easier to understand and maintain. The negative is that we won't catch corruptions in those memory areas. However, I believe this is acceptable because of the following reasons: * We'd have to be very unlucky to *only* have corruptions in *those* areas. Usually, corruptions are not that well-targetted. * A checksum isn't a fully error-proof thing either, it's very possible that there is a corruption in other areas that the checksum doesn't capture. Let's now talk about the implementation. Keeping all those entries in a skip list wouldn't be efficient: we'd need an entry for every opcode, which would lead to many many entries. However, this approach actually implements a "compression" scheme. Instead of just storing the offset, we actually store a triple: <offset, size of the area to be checked, amount of repetitions>. The offset indicates which pointer needs to be skipped. If the number of repetitions is greater than zero, it will do the following `repetitions` times: * Checksum the bytes at [offset + pointer_size, offset + pointer_size + size of area to be checked] * Move the offset to after the area that was just checksummed + pointer_size. This allows us to only use one skip list entry for all the opcodes in an op_array. The offsets also aren't checked in the zend_adler32() function itself, the zend_accel_script_checksum() function will checksum in "blocks" of bytes that should not be skipped, by setting the size for zend_alder32() in such a way that it computes the checksum until the next offset to skip. The main advantage of this is better performance, since there are fewer checks, and the accelerated SSE2 (or future other accelerated) routines can keep being used. Finally some other minor differences: * We precompute the exact size needed for the skip list. This avoids reallocations and also fixes one issue where there was a warning about the computed size not being equals to the actual size.
1 parent 8a8c69c commit c2c91d4

11 files changed

+111
-147
lines changed

ext/opcache/ZendAccelerator.c

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4763,9 +4763,8 @@ static int accel_finish_startup(void)
47634763
ZCG(cwd_key_len) = 0;
47644764
ZCG(cwd_check) = 1;
47654765

4766-
ZCG(checksum_skip_list) = NULL;
4767-
ZCG(checksum_skip_list_count) = 0;
4768-
ZCG(checksum_skip_list_capacity) = 0;
4766+
ZCG(mem_checksum_skip_list) = NULL;
4767+
ZCG(mem_checksum_skip_list_count) = 0;
47694768

47704769
if (accel_preload(ZCG(accel_directives).preload, in_child) != SUCCESS) {
47714770
ret = FAILURE;

ext/opcache/ZendAccelerator.h

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,19 @@ typedef enum _zend_accel_restart_reason {
109109
ACCEL_RESTART_USER /* restart scheduled by opcache_reset() */
110110
} zend_accel_restart_reason;
111111

112+
typedef struct _zend_accel_skip_list_entry {
113+
/* The offset indicates which byte offset within the memory block must be skipped.
114+
* The size of the skip is equal to the system's pointer size. */
115+
uint32_t offset;
116+
/* To prevent creating a huge list with a lot of entries, we use a compression scheme
117+
* based on the following two fields. If repetitions > 0, then the checksum algorithm
118+
* will repeat `repetitions` times checksumming `checked_area_size` bytes, followed
119+
* by skipping a pointer. For example, we use this to skip the `zend_op->handler` pointer.
120+
* This allows us to only create one skip list entry per op array to handle *all* the opcodes. */
121+
uint32_t checked_area_size;
122+
uint32_t repetitions;
123+
} zend_accel_skip_list_entry;
124+
112125
typedef struct _zend_persistent_script {
113126
zend_script script;
114127
zend_long compiler_halt_offset; /* position of __HALT_COMPILER or -1 */
@@ -122,8 +135,10 @@ typedef struct _zend_persistent_script {
122135

123136
void *mem; /* shared memory area used by script structures */
124137
size_t size; /* size of used shared memory */
125-
/* list of offsets relative to (mem + ZEND_ALIGNED_SIZE(sizeof(zend_persistent_script))) to be skipped in the checksum calculation */
126-
uint32_t *checksum_skip_list;
138+
139+
/* Some bytes must be skipped in the checksum calculation because they could've changed (legally)
140+
* since the last checksum calculation. */
141+
zend_accel_skip_list_entry *mem_checksum_skip_list;
127142

128143
/* All entries that shouldn't be counted in the ADLER32
129144
* checksum must be declared in this struct
@@ -228,9 +243,8 @@ typedef struct _zend_accel_globals {
228243
zend_string key;
229244
char _key[MAXPATHLEN * 8];
230245

231-
uint32_t *checksum_skip_list;
232-
uint32_t checksum_skip_list_count;
233-
uint32_t checksum_skip_list_capacity;
246+
zend_accel_skip_list_entry *mem_checksum_skip_list;
247+
uint32_t mem_checksum_skip_list_count;
234248
} zend_accel_globals;
235249

236250
typedef struct _zend_string_table {

ext/opcache/jit/zend_jit.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
#include "Optimizer/zend_func_info.h"
3737
#include "Optimizer/zend_ssa.h"
3838
#include "Optimizer/zend_inference.h"
39+
#include "Optimizer/zend_call_graph.h"
3940
#include "Optimizer/zend_dump.h"
4041

4142
#if ZEND_JIT_TARGET_X86
@@ -1266,7 +1267,7 @@ static int zend_may_overflow(const zend_op *opline, const zend_ssa_op *ssa_op, c
12661267
}
12671268
}
12681269

1269-
ZEND_EXT_API int zend_jit_build_cfg(const zend_op_array *op_array, zend_cfg *cfg)
1270+
static int zend_jit_build_cfg(const zend_op_array *op_array, zend_cfg *cfg)
12701271
{
12711272
uint32_t flags;
12721273

ext/opcache/jit/zend_jit.h

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@
1919
#ifndef HAVE_JIT_H
2020
#define HAVE_JIT_H
2121

22-
#include "Optimizer/zend_call_graph.h"
23-
2422
#if defined(__x86_64__) || defined(i386) || defined(ZEND_WIN32)
2523
# define ZEND_JIT_TARGET_X86 1
2624
# define ZEND_JIT_TARGET_ARM64 0
@@ -156,7 +154,6 @@ ZEND_EXT_API void zend_jit_activate(void);
156154
ZEND_EXT_API void zend_jit_deactivate(void);
157155
ZEND_EXT_API void zend_jit_status(zval *ret);
158156
ZEND_EXT_API void zend_jit_restart(void);
159-
ZEND_EXT_API int zend_jit_build_cfg(const zend_op_array *op_array, zend_cfg *cfg);
160157

161158
typedef struct _zend_lifetime_interval zend_lifetime_interval;
162159
typedef struct _zend_life_range zend_life_range;

ext/opcache/jit/zend_jit_trace.c

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616
+----------------------------------------------------------------------+
1717
*/
1818

19-
#include "zend_persist.h"
20-
2119
static zend_op_array dummy_op_array;
2220
static zend_jit_trace_info *zend_jit_traces = NULL;
2321
static const void **zend_jit_exit_groups = NULL;
@@ -8264,7 +8262,6 @@ static int zend_jit_setup_hot_trace_counters(zend_op_array *op_array)
82648262
if (cfg.blocks[i].flags & ZEND_BB_LOOP_HEADER) {
82658263
/* loop header */
82668264
opline = op_array->opcodes + cfg.blocks[i].start;
8267-
checksum_skip_list_add(&opline->handler);
82688265
if (!(ZEND_OP_TRACE_INFO(opline, jit_extension->offset)->trace_flags & ZEND_JIT_TRACE_UNSUPPORTED)) {
82698266
opline->handler = (const void*)zend_jit_loop_trace_counter_handler;
82708267
if (!ZEND_OP_TRACE_INFO(opline, jit_extension->offset)->counter) {
@@ -8289,7 +8286,6 @@ static int zend_jit_setup_hot_trace_counters(zend_op_array *op_array)
82898286
}
82908287
}
82918288

8292-
checksum_skip_list_add(&opline->handler);
82938289
if (!ZEND_OP_TRACE_INFO(opline, jit_extension->offset)->trace_flags) {
82948290
/* function entry */
82958291
opline->handler = (const void*)zend_jit_func_trace_counter_handler;

ext/opcache/zend_accelerator_util_funcs.c

Lines changed: 36 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -299,33 +299,18 @@ zend_op_array* zend_accel_load_script(zend_persistent_script *persistent_script,
299299
#define ADLER32_DO4(buf, i) ADLER32_DO2(buf, i); ADLER32_DO2(buf, i + 2);
300300
#define ADLER32_DO8(buf, i) ADLER32_DO4(buf, i); ADLER32_DO4(buf, i + 4);
301301
#define ADLER32_DO16(buf) ADLER32_DO8(buf, 0); ADLER32_DO8(buf, 8);
302-
#define ADLER32_CHECK_SKIPPED(by) \
303-
do { \
304-
offset = buf - start; \
305-
while (*skip_list != 0 && *skip_list < offset) { \
306-
skip_list++; \
307-
} \
308-
skipped = *skip_list != 0 && *skip_list < (offset + (by)); \
309-
} while (0)
310-
311-
unsigned int zend_adler32(unsigned int checksum, unsigned char *buf, uint32_t len, uint32_t *skip_list)
302+
303+
unsigned int zend_adler32(unsigned int checksum, unsigned char *buf, uint32_t len)
312304
{
313-
unsigned char *start = buf;
314305
unsigned int s1 = checksum & 0xffff;
315306
unsigned int s2 = (checksum >> 16) & 0xffff;
316307
unsigned char *end;
317-
bool skipped;
318-
uint32_t offset;
319308

320309
while (len >= ADLER32_NMAX) {
321310
len -= ADLER32_NMAX;
322311
end = buf + ADLER32_NMAX;
323-
skipped = false;
324312
do {
325-
ADLER32_CHECK_SKIPPED(16);
326-
if (!skipped) {
327-
ADLER32_DO16(buf);
328-
}
313+
ADLER32_DO16(buf);
329314
buf += 16;
330315
} while (buf != end);
331316
s1 %= ADLER32_BASE;
@@ -336,27 +321,16 @@ unsigned int zend_adler32(unsigned int checksum, unsigned char *buf, uint32_t le
336321
if (len >= 16) {
337322
end = buf + (len & 0xfff0);
338323
len &= 0xf;
339-
skipped = false;
340324
do {
341-
ADLER32_CHECK_SKIPPED(16);
342-
if (!skipped) {
343-
ADLER32_DO16(buf);
344-
}
325+
ADLER32_DO16(buf);
345326
buf += 16;
346327
} while (buf != end);
347328
}
348329
if (len) {
349330
end = buf + len;
350-
skipped = false;
351331
do {
352-
ADLER32_CHECK_SKIPPED(1);
353-
if (!skipped) {
354-
ADLER32_DO1(buf);
355-
buf++;
356-
} else {
357-
// skip_list contains a list of pointers so we skip the entire pointer size
358-
buf += sizeof(void*);
359-
}
332+
ADLER32_DO1(buf);
333+
buf++;
360334
} while (buf != end);
361335
}
362336
s1 %= ADLER32_BASE;
@@ -372,20 +346,43 @@ unsigned int zend_accel_script_checksum(zend_persistent_script *persistent_scrip
372346
size_t size = persistent_script->size;
373347
size_t persistent_script_check_block_size = ((char *)&(persistent_script->dynamic_members)) - (char *)persistent_script;
374348
unsigned int checksum = ADLER32_INIT;
375-
uint32_t zero = 0;
376349

377350
if (mem < (unsigned char*)persistent_script) {
378-
checksum = zend_adler32(checksum, mem, (unsigned char*)persistent_script - mem, &zero);
351+
checksum = zend_adler32(checksum, mem, (unsigned char*)persistent_script - mem);
379352
size -= (unsigned char*)persistent_script - mem;
380353
mem += (unsigned char*)persistent_script - mem;
381354
}
382355

383-
checksum = zend_adler32(checksum, mem, persistent_script_check_block_size, &zero);
384-
mem += ZEND_ALIGNED_SIZE(sizeof(*persistent_script));
385-
size -= ZEND_ALIGNED_SIZE(sizeof(*persistent_script));
356+
checksum = zend_adler32(checksum, mem, persistent_script_check_block_size);
357+
mem += sizeof(*persistent_script);
358+
size -= sizeof(*persistent_script);
386359

387-
if (size > 0) {
388-
checksum = zend_adler32(checksum, mem, size, persistent_script->checksum_skip_list);
360+
if (UNEXPECTED(size == 0)) {
361+
return checksum;
389362
}
363+
364+
const zend_accel_skip_list_entry *skip_list_entry = persistent_script->mem_checksum_skip_list;
365+
366+
size_t offset = 0;
367+
uint32_t next_stop_read;
368+
while ((next_stop_read = skip_list_entry->offset) != 0) {
369+
checksum = zend_adler32(checksum, mem + offset, next_stop_read - offset);
370+
offset = next_stop_read + sizeof(void *); /* skip over the pointer */
371+
372+
uint32_t repetitions = skip_list_entry->repetitions;
373+
const uint32_t checked_area_size = skip_list_entry->checked_area_size;
374+
while (repetitions != 0) {
375+
checksum = zend_adler32(checksum, mem + offset, checked_area_size);
376+
offset += checked_area_size + sizeof(void *); /* skip over the pointer */
377+
repetitions--;
378+
}
379+
380+
skip_list_entry++;
381+
}
382+
383+
if (size > offset) {
384+
checksum = zend_adler32(checksum, mem + offset, size - offset);
385+
}
386+
390387
return checksum;
391388
}

ext/opcache/zend_accelerator_util_funcs.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ zend_op_array* zend_accel_load_script(zend_persistent_script *persistent_script,
3535

3636
#define ADLER32_INIT 1 /* initial Adler-32 value */
3737

38-
unsigned int zend_adler32(unsigned int checksum, unsigned char *buf, uint32_t len, uint32_t *skip_list);
38+
unsigned int zend_adler32(unsigned int checksum, unsigned char *buf, uint32_t len);
3939

4040
unsigned int zend_accel_script_checksum(zend_persistent_script *persistent_script);
4141

ext/opcache/zend_file_cache.c

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,11 +1030,8 @@ int zend_file_cache_script_store(zend_persistent_script *script, int in_shm)
10301030
}
10311031
zend_shared_alloc_destroy_xlat_table();
10321032

1033-
zend_string *const s = (zend_string*)ZCG(mem);
1034-
1035-
uint32_t zero = 0;
1036-
info.checksum = zend_adler32(ADLER32_INIT, buf, script->size, &zero);
1037-
info.checksum = zend_adler32(info.checksum, (unsigned char*)ZSTR_VAL(s), info.str_size, &zero);
1033+
info.checksum = zend_adler32(ADLER32_INIT, buf, script->size);
1034+
info.checksum = zend_adler32(info.checksum, (unsigned char*)ZSTR_VAL((zend_string*)ZCG(mem)), info.str_size);
10381035

10391036
#if __has_feature(memory_sanitizer)
10401037
/* The buffer may contain uninitialized regions. However, the uninitialized parts will not be
@@ -1778,9 +1775,8 @@ zend_persistent_script *zend_file_cache_script_load(zend_file_handle *file_handl
17781775
close(fd);
17791776

17801777
/* verify checksum */
1781-
uint32_t zero = 0;
17821778
if (ZCG(accel_directives).file_cache_consistency_checks &&
1783-
(actual_checksum = zend_adler32(ADLER32_INIT, mem, info.mem_size + info.str_size, &zero)) != info.checksum) {
1779+
(actual_checksum = zend_adler32(ADLER32_INIT, mem, info.mem_size + info.str_size)) != info.checksum) {
17841780
zend_accel_error(ACCEL_LOG_WARNING, "corrupted file '%s' excepted checksum: 0x%08x actual checksum: 0x%08x\n", filename, info.checksum, actual_checksum);
17851781
zend_file_cache_unlink(filename);
17861782
zend_arena_release(&CG(arena), checkpoint);

0 commit comments

Comments
 (0)