• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

krakjoe / parallel / 25501592414

07 May 2026 02:19PM UTC coverage: 95.023% (-0.01%) from 95.033%
25501592414

Pull #370

github

web-flow
Merge ba89f72cf into 808abb25e
Pull Request #370: fix cache key collision

37 of 41 new or added lines in 2 files covered. (90.24%)

1 existing line in 1 file now uncovered.

2864 of 3014 relevant lines covered (95.02%)

1851.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.6
/src/cache.c
1
/*
2
  +----------------------------------------------------------------------+
3
  | parallel                                                             |
4
  +----------------------------------------------------------------------+
5
  | Copyright (c) Joe Watkins 2019-2024                                  |
6
  +----------------------------------------------------------------------+
7
  | This source file is subject to version 3.01 of the PHP license,      |
8
  | that is bundled with this package in the file LICENSE, and is        |
9
  | available through the world-wide-web at the following url:           |
10
  | http://www.php.net/license/3_01.txt                                  |
11
  | If you did not receive a copy of the PHP license and are unable to   |
12
  | obtain it through the world-wide-web, please send a note to          |
13
  | license@php.net so we can mail you a copy immediately.               |
14
  +----------------------------------------------------------------------+
15
  | Author: krakjoe                                                      |
16
  +----------------------------------------------------------------------+
17
 */
18
#ifndef HAVE_PARALLEL_CACHE
19
#define HAVE_PARALLEL_CACHE
20

21
#include "parallel.h"
22

23
static struct {
24
        pthread_mutex_t mutex;
25
        HashTable       table;
26
        struct {
27
                size_t size;
28
                size_t used;
29
                void  *mem;
30
                void  *block;
31
        } memory;
32
} php_parallel_cache_globals = {PTHREAD_MUTEX_INITIALIZER};
33

34
#define PCG(e) php_parallel_cache_globals.e
35
#define PCM(e) PCG(memory).e
36

37
#define PARALLEL_CACHE_CHUNK PARALLEL_PLATFORM_ALIGNED((1024 * 1024) * 8)
38

39
/* {{{ */
40
static zend_always_inline void *php_parallel_cache_alloc(size_t size)
1,354✔
41
{
42
        void  *mem;
1,354✔
43
        size_t aligned = PARALLEL_PLATFORM_ALIGNED(size);
1,354✔
44

45
        ZEND_ASSERT(size < PARALLEL_CACHE_CHUNK);
1,354✔
46

47
        if ((PCM(used) + aligned) >= PCM(size)) {
1,354✔
NEW
48
                PCM(size) = PARALLEL_PLATFORM_ALIGNED(PCM(size) + PARALLEL_CACHE_CHUNK);
×
NEW
49
                PCM(mem) = (void *)realloc(PCM(mem), PCM(size));
×
50

NEW
51
                if (!PCM(mem)) {
×
52
                        /* out of memory */
53
                        return NULL;
54
                }
55

NEW
56
                PCM(block) = (void *)(((char *)PCM(mem)) + PCM(used));
×
57
        }
58

59
        mem = PCM(block);
1,354✔
60
        PCM(block) = (void *)(((char *)PCM(block)) + aligned);
1,354✔
61
        PCM(used) += aligned;
1,354✔
62

63
        return mem;
1,354✔
64
}
65

66
static zend_always_inline void *php_parallel_cache_copy_mem(void *source, zend_long size)
1,116✔
67
{
68
        void *destination = php_parallel_cache_alloc(size);
27✔
69

70
        memcpy(destination, source, size);
1,089✔
71

72
        return destination;
1,116✔
73
} /* }}} */
74

75
static zend_always_inline HashTable *php_parallel_cache_statics(HashTable *statics)
7✔
76
{ /* {{{ */
77
        HashTable *cached = zend_hash_index_find_ptr(&PCG(table), (zend_ulong)statics);
14✔
78

UNCOV
79
        if (cached) {
×
80
                return cached;
81
        }
82

83
        cached = php_parallel_copy_hash_persistent(statics, php_parallel_copy_string_interned, php_parallel_cache_copy_mem,
7✔
84
                                                   PHP_PARALLEL_COPY_STORAGE_CACHE_POOL);
85

86
        return zend_hash_index_update_ptr(&PCG(table), (zend_ulong)statics, cached);
7✔
87
} /* }}} */
88

89
static zend_always_inline void php_parallel_cache_type(zend_type *type)
45✔
90
{ /* {{{ */
91
        zend_type *single;
45✔
92

93
        if (!ZEND_TYPE_IS_SET(*type)) {
45✔
94
                return;
95
        }
96

97
        if (ZEND_TYPE_HAS_LIST(*type)) {
25✔
98
                zend_type_list *list = ZEND_TYPE_LIST(*type);
1✔
99

100
                list = php_parallel_cache_copy_mem(list, ZEND_TYPE_LIST_SIZE(list->num_types));
1✔
101

102
                if (ZEND_TYPE_USES_ARENA(*type)) {
1✔
103
                        ZEND_TYPE_FULL_MASK(*type) &= ~_ZEND_TYPE_ARENA_BIT;
1✔
104
                }
105

106
                ZEND_TYPE_SET_PTR(*type, list);
1✔
107
        }
108

109
        ZEND_TYPE_FOREACH(*type, single)
25✔
110
        {
111
                if (ZEND_TYPE_HAS_NAME(*single)) {
26✔
112
                        zend_string *name = ZEND_TYPE_NAME(*single);
15✔
113

114
                        ZEND_TYPE_SET_PTR(*single, php_parallel_copy_string_interned(name));
15✔
115
                }
116
        }
117
        ZEND_TYPE_FOREACH_END();
26✔
118
} /* }}} */
119

120
/* {{{ */
121
static zend_op_array *php_parallel_cache_create(const zend_function *source, bool statics)
820✔
122
{
123
        zend_op_array *cached = php_parallel_cache_copy_mem((void *)source, sizeof(zend_op_array));
820✔
124
        uint32_t      *literal_map = NULL;
820✔
125
        uint32_t      *offset_map = NULL;
820✔
126
        uint32_t       new_last_literal = cached->last_literal;
820✔
127

128
        cached->fn_flags |= ZEND_ACC_IMMUTABLE;
820✔
129

130
        if (statics && cached->static_variables) {
820✔
131
                cached->static_variables = php_parallel_cache_statics(cached->static_variables);
14✔
132
        }
133

134
#if PHP_VERSION_ID >= 80200
135
        ZEND_MAP_PTR_INIT(cached->static_variables_ptr, cached->static_variables);
423✔
136
#else
137
        ZEND_MAP_PTR_INIT(cached->static_variables_ptr, &cached->static_variables);
397✔
138
#endif
139

140
        ZEND_MAP_PTR_INIT(cached->run_time_cache, NULL);
820✔
141

142
#if PHP_VERSION_ID >= 80100
143
        if (cached->num_dynamic_func_defs) {
699✔
144
                uint32_t it = 0;
60✔
145

146
                cached->dynamic_func_defs = php_parallel_cache_copy_mem(
120✔
147
                    cached->dynamic_func_defs, sizeof(zend_op_array *) * cached->num_dynamic_func_defs);
60✔
148

149
                while (it < cached->num_dynamic_func_defs) {
130✔
150
                        cached->dynamic_func_defs[it] =
140✔
151
                            (zend_op_array *)php_parallel_cache_create((zend_function *)cached->dynamic_func_defs[it], statics);
70✔
152
                        it++;
70✔
153
                }
154
        }
155
#endif
156

157
        if (!cached->refcount) {
820✔
158
                goto _php_parallel_cached_function_return;
674✔
159
        }
160

161
        cached->refcount = NULL;
146✔
162

163
        if (cached->last_literal) {
146✔
164
                zend_op *src_opline = source->op_array.opcodes;
146✔
165
                zend_op *src_end = src_opline + source->op_array.last;
146✔
166

167
                // A map to keep track of which literals are referenced by the
168
                // `ZEND_INIT_FCALL` opcodes we found so that we can expand those later
169
                literal_map = emalloc(sizeof(uint32_t) * cached->last_literal);
146✔
170
                memset(literal_map, 0, sizeof(uint32_t) * cached->last_literal);
146✔
171

172
                // Search for `ZEND_INIT_FCALL` opcodes and remember the indexes for the
173
                // literals, as we are rewriting them later to `ZEND_INIT_FCALL_BY_NAME`
174
                // which requires a second, lower cased literal just in the next literal
175
                // slot.
176
                while (src_opline < src_end) {
1,128✔
177
                        if (src_opline->opcode == ZEND_INIT_FCALL && src_opline->op2_type == IS_CONST) {
982✔
178
                                uint32_t idx;
46✔
179
#if ZEND_USE_ABS_CONST_ADDR
180
                                idx = (zval *)src_opline->op2.zv - source->op_array.literals;
181
#else
182
                                idx = ((zval *)((char *)src_opline + src_opline->op2.constant) - source->op_array.literals);
46✔
183
#endif
184
                                if (idx < cached->last_literal) {
46✔
185
                                        if (literal_map[idx] == 0) {
46✔
186
                                                literal_map[idx] = 1;
46✔
187
                                                new_last_literal++;
46✔
188
                                        }
189
                                }
190
                        }
191
                        src_opline++;
982✔
192
                }
193
        }
194

195
        if (new_last_literal) {
146✔
196
                zval    *literal = source->op_array.literals;
146✔
197
                zval    *slot = php_parallel_cache_alloc(sizeof(zval) * new_last_literal);
146✔
198
                uint32_t idx = 0;
146✔
199

200
                offset_map = emalloc(sizeof(uint32_t) * cached->last_literal);
146✔
201

202
                cached->literals = slot;
146✔
203

204
                for (uint32_t i = 0; i < cached->last_literal; i++) {
734✔
205
                        /* Record the mapping from old literal index (i) to new literal index (idx)
206
                           so we can update opcode operands later. */
207
                        offset_map[i] = idx;
588✔
208

209
                        if (Z_TYPE_P(literal) == IS_ARRAY) {
588✔
210
                                ZVAL_ARR(slot, php_parallel_copy_hash_persistent(Z_ARRVAL_P(literal), php_parallel_copy_string_interned,
7✔
211
                                                                                 php_parallel_cache_copy_mem,
212
                                                                                 PHP_PARALLEL_COPY_STORAGE_CACHE_POOL));
213
                        } else if (Z_TYPE_P(literal) == IS_STRING) {
581✔
214
                                ZVAL_STR(slot, php_parallel_copy_string_interned(Z_STR_P(literal)));
337✔
215
                        } else {
216
                                *slot = *literal;
244✔
217
                        }
218

219
                        Z_TYPE_FLAGS_P(slot) &= ~(IS_TYPE_REFCOUNTED | IS_TYPE_COLLECTABLE);
588✔
220

221
                        /* If this literal was used by INIT_FCALL, insert its lowercased version next. */
222
                        if (literal_map[i]) {
588✔
223
                                zend_string *lower = zend_string_tolower(Z_STR_P(slot));
46✔
224
                                slot++;
46✔
225
                                idx++;
46✔
226
                                ZVAL_STR(slot, php_parallel_copy_string_interned(lower));
46✔
227
                                zend_string_release(lower);
46✔
228
                                Z_TYPE_FLAGS_P(slot) &= ~(IS_TYPE_REFCOUNTED | IS_TYPE_COLLECTABLE);
46✔
229
                        }
230

231
                        literal++;
588✔
232
                        slot++;
588✔
233
                        idx++;
588✔
234
                }
235
                cached->last_literal = new_last_literal;
146✔
236
        }
237

238
        if (cached->last_var) {
146✔
239
                zend_string **vars = cached->vars;
92✔
240
                uint32_t      it = 0, end = cached->last_var;
92✔
241
                zend_string **heap = php_parallel_cache_alloc(cached->last_var * sizeof(zend_string *));
92✔
242

243
                while (it < end) {
218✔
244
                        heap[it] = php_parallel_copy_string_interned(vars[it]);
126✔
245
                        it++;
126✔
246
                }
247
                cached->vars = heap;
92✔
248
        }
249

250
        if (cached->last) {
146✔
251
                zend_op *opcodes = php_parallel_cache_copy_mem(cached->opcodes, sizeof(zend_op) * cached->last);
146✔
252
                zend_op *opline = opcodes, *end = opline + cached->last;
146✔
253

254
                while (opline < end) {
1,128✔
255
                        /* Replace ZEND_INIT_FCALL with ZEND_INIT_FCALL_BY_NAME.
256
                           We must clear op1_type (IS_UNUSED) and op1.var (0) to invalidate the
257
                           original thread's cache slot. */
258
                        if (opline->opcode == ZEND_INIT_FCALL) {
982✔
259
                                opline->opcode = ZEND_INIT_FCALL_BY_NAME;
46✔
260
                                opline->op1_type = IS_UNUSED;
46✔
261
                                opline->op1.var = 0;
46✔
262
                                ZEND_VM_SET_OPCODE_HANDLER(opline);
46✔
263
                        }
264

265
                        /* Remap IS_CONST operands to their new locations in the expanded literal table
266
                           using the offset_map we built earlier. */
267
                        if (opline->op1_type == IS_CONST) {
982✔
268
                                uint32_t idx;
308✔
269
                                zend_op *src_opline = source->op_array.opcodes + (opline - opcodes);
308✔
270
#if ZEND_USE_ABS_CONST_ADDR
271
                                idx = (zval *)src_opline->op1.zv - source->op_array.literals;
272
                                opline->op1.zv = &cached->literals[offset_map[idx]];
273
#else
274
                                idx = ((zval *)((char *)src_opline + src_opline->op1.constant) - source->op_array.literals);
308✔
275
                                opline->op1.constant = (char *)&cached->literals[offset_map[idx]] - (char *)opline;
308✔
276
#endif
277
                                if (opline->opcode == ZEND_SEND_VAL || opline->opcode == ZEND_SEND_VAL_EX ||
308✔
278
                                    opline->opcode == ZEND_QM_ASSIGN) {
279
                                        zend_vm_set_opcode_handler_ex(opline, 0, 0, 0);
45✔
280
                                }
281
                        }
282
                        if (opline->op2_type == IS_CONST) {
982✔
283
                                uint32_t idx;
174✔
284
                                zend_op *src_opline = source->op_array.opcodes + (opline - opcodes);
174✔
285
#if ZEND_USE_ABS_CONST_ADDR
286
                                idx = (zval *)src_opline->op2.zv - source->op_array.literals;
287
                                opline->op2.zv = &cached->literals[offset_map[idx]];
288
#else
289
                                idx = ((zval *)((char *)src_opline + src_opline->op2.constant) - source->op_array.literals);
174✔
290
                                opline->op2.constant = (char *)&cached->literals[offset_map[idx]] - (char *)opline;
174✔
291
#endif
292
                        }
293
#if ZEND_USE_ABS_JMP_ADDR
294
                        switch (opline->opcode) {
295
                        case ZEND_JMP:
296
                        case ZEND_FAST_CALL:
297
                                opline->op1.jmp_addr = &opcodes[opline->op1.jmp_addr - source->op_array.opcodes];
298
                                break;
299
#if PHP_VERSION_ID < 80200
300
                        case ZEND_JMPZNZ:
301
#endif
302
                        case ZEND_JMPZ:
303
                        case ZEND_JMPNZ:
304
                        case ZEND_JMPZ_EX:
305
                        case ZEND_JMPNZ_EX:
306
                        case ZEND_JMP_SET:
307
                        case ZEND_COALESCE:
308
                        case ZEND_FE_RESET_R:
309
                        case ZEND_FE_RESET_RW:
310
                        case ZEND_ASSERT_CHECK:
311
                                opline->op2.jmp_addr = &opcodes[opline->op2.jmp_addr - source->op_array.opcodes];
312
                                break;
313

314
                        case ZEND_CATCH:
315
                                if (!(opline->extended_value & ZEND_LAST_CATCH)) {
316
                                        opline->op2.jmp_addr = &opcodes[opline->op2.jmp_addr - source->op_array.opcodes];
317
                                }
318
                                break;
319
                        }
320
#endif
321

322
                        opline++;
982✔
323
                }
324
                cached->opcodes = opcodes;
146✔
325
        }
326

327
        if (literal_map) {
146✔
328
                efree(literal_map);
146✔
329
        }
330

331
        if (offset_map) {
146✔
332
                efree(offset_map);
146✔
333
        }
334

335
        if (cached->arg_info) {
146✔
336
                zend_arg_info *it = cached->arg_info, *end = it + cached->num_args, *info;
37✔
337

338
                if (cached->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) {
37✔
339
                        it--;
6✔
340
                }
341
                if (cached->fn_flags & ZEND_ACC_VARIADIC) {
37✔
342
                        end++;
2✔
343
                }
344

345
                cached->arg_info = info = php_parallel_cache_copy_mem(it, (end - it) * sizeof(zend_arg_info));
37✔
346

347
                while (it < end) {
82✔
348
                        if (info->name) {
45✔
349
                                info->name = php_parallel_copy_string_interned(it->name);
39✔
350
                        }
351

352
                        php_parallel_cache_type(&info->type);
45✔
353

354
                        info++;
45✔
355
                        it++;
45✔
356
                }
357
                if (cached->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) {
37✔
358
                        cached->arg_info++;
6✔
359
                }
360
        }
361

362
        if (cached->try_catch_array) {
146✔
363
                cached->try_catch_array = php_parallel_cache_copy_mem(cached->try_catch_array,
6✔
364
                                                                      sizeof(zend_try_catch_element) * cached->last_try_catch);
6✔
365
        }
366

367
        if (cached->live_range) {
146✔
368
                cached->live_range =
38✔
369
                    php_parallel_cache_copy_mem(cached->live_range, sizeof(zend_live_range) * cached->last_live_range);
38✔
370
        }
371

372
        if (cached->function_name)
146✔
373
                cached->function_name = php_parallel_copy_string_interned(cached->function_name);
146✔
374

375
        if (cached->filename)
146✔
376
                cached->filename = php_parallel_copy_string_interned(cached->filename);
146✔
377

378
        if (cached->doc_comment)
146✔
379
                cached->doc_comment = php_parallel_copy_string_interned(cached->doc_comment);
1✔
380

381
_php_parallel_cached_function_return:
145✔
382
        return cached;
820✔
383
} /* }}} */
384

385
/* {{{ */
386
static zend_always_inline zend_function *php_parallel_cache_function_ex(const zend_function *source, bool statics)
976✔
387
{
388
        zend_op_array *cached;
976✔
389

390
        pthread_mutex_lock(&PCG(mutex));
976✔
391

392
        if ((cached = zend_hash_index_find_ptr(&PCG(table), (zend_ulong)source->op_array.opcodes))) {
1,202✔
393
#if PHP_VERSION_ID >= 80400
394
                /* On PHP 8.4+ closures have unique names like {closure:file.php:42}.
395
                 * Validate that the cached entry matches the source to detect when
396
                 * the allocator reuses an opcodes address for a different closure (#309). */
397
                if (!zend_string_equals(cached->function_name, source->op_array.function_name)) {
398
                        goto _php_parallel_cached_function_create;
399
                }
400
#endif
401
                goto _php_parallel_cached_function_return;
226✔
402
        }
403

404
#if PHP_VERSION_ID >= 80400
405
_php_parallel_cached_function_create:
406
#endif
407
        cached = php_parallel_cache_create(source, statics);
750✔
408

409
        zend_hash_index_update_ptr(&PCG(table), (zend_ulong)source->op_array.opcodes, cached);
750✔
410

411
_php_parallel_cached_function_return:
976✔
412
        pthread_mutex_unlock(&PCG(mutex));
976✔
413

414
        return (zend_function *)cached;
976✔
415
} /* }}} */
416

417
zend_function *php_parallel_cache_closure(const zend_function *source, zend_function *closure)
964✔
418
{ /* {{{ */
419
        zend_op_array *cache;
964✔
420

421
        cache = (zend_op_array *)php_parallel_cache_function_ex((zend_function *)source, 0);
964✔
422

423
        if (!closure) {
964✔
424
                closure = php_parallel_copy_mem(cache, sizeof(zend_op_array), 1);
741✔
425
        } else {
426
                memcpy(closure, cache, sizeof(zend_op_array));
223✔
427
        }
428

429
        if (source->op_array.static_variables) {
964✔
430
                HashTable *statics = ZEND_MAP_PTR_GET(source->op_array.static_variables_ptr);
228✔
431

432
                if (statics) {
228✔
433
                        closure->op_array.static_variables = php_parallel_copy_hash_ctor(statics, 1);
183✔
434

435
#if PHP_VERSION_ID >= 80200
436
                        ZEND_MAP_PTR_INIT(closure->op_array.static_variables_ptr, closure->op_array.static_variables);
93✔
437
#else
438
                        ZEND_MAP_PTR_INIT(closure->op_array.static_variables_ptr, &closure->op_array.static_variables);
90✔
439
#endif
440
                }
441
        }
442

443
#if PHP_VERSION_ID >= 80100
444
        if (source->op_array.num_dynamic_func_defs) {
821✔
445
                uint32_t it = 0;
65✔
446
                /* Use regular persistent memory for dynamic_func_defs array, not cache pool */
447
                closure->op_array.dynamic_func_defs =
130✔
448
                    pemalloc(sizeof(zend_op_array *) * source->op_array.num_dynamic_func_defs, 1);
65✔
449
                memcpy(closure->op_array.dynamic_func_defs, source->op_array.dynamic_func_defs,
205✔
450
                       sizeof(zend_op_array *) * source->op_array.num_dynamic_func_defs);
65✔
451
                while (it < source->op_array.num_dynamic_func_defs) {
140✔
452
                        closure->op_array.dynamic_func_defs[it] = (zend_op_array *)php_parallel_cache_closure(
150✔
453
                            (zend_function *)source->op_array.dynamic_func_defs[it], NULL);
75✔
454
                        it++;
75✔
455
                }
456
        }
457
#endif
458

459
        return closure;
964✔
460
} /* }}} */
461

462
zend_function *php_parallel_cache_function(const zend_function *source)
12✔
463
{ /* {{{ */
464
        return php_parallel_cache_function_ex(source, 1);
12✔
465
} /* }}} */
466

467
/* {{{ */
468
PHP_MINIT_FUNCTION(PARALLEL_CACHE)
1,078✔
469
{
470
        zend_hash_init(&PCG(table), 32, NULL, NULL, 1);
1,078✔
471

472
        PCM(size) = PARALLEL_CACHE_CHUNK;
1,078✔
473
        PCM(mem) = PCM(block) = malloc(PCM(size));
1,078✔
474

475
        if (!PCM(mem)) {
1,078✔
476
                /* out of memory */
477
        }
1,078✔
478

479
        return SUCCESS;
1,078✔
480
}
481

482
PHP_MSHUTDOWN_FUNCTION(PARALLEL_CACHE)
1,078✔
483
{
484
        zend_hash_destroy(&PCG(table));
1,078✔
485

486
        if (PCM(mem))
1,078✔
487
                free(PCM(mem));
1,078✔
488

489
        return SUCCESS;
1,078✔
490
} /* }}} */
491
#endif
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc