Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **Every closure a worker materialized was freed by a rule that holds only on 64-bit** (#255). `op_array_to_emalloc` laid the literals inside the opcode block, which is the layout `pass_two()` leaves and `destroy_op_array` expects there. Under `ZEND_USE_ABS_CONST_ADDR` — 32-bit — the two are separate allocations and `destroy_op_array` frees `op_array->literals` on its own, so the allocator was handed a pointer into the middle of a block it never issued, and the block was then freed a second time. The copy now follows the same rule as the compiler on both platforms.
- **`ThreadPool::getWorkerCount()` reported the number of workers the pool was constructed with, whatever became of the threads afterwards** (#231). It now counts the workers that are running, so a closed pool reports 0 once its threads have drained, and no test could state "no worker died" before, because the value that says so did not exist. `reload()` sizes its cohort from that count instead of the constructed one, so a pool that lost a thread no longer waits for an exit token nobody will post, and it reports a rotation that left the pool with no worker at all. `submit()` and `map()` on an open pool with no live worker throw `Async\ThreadPoolException` instead of accepting a task whose Future never settles.
- **A `ThreadPool` task in coroutine mode left its un-awaited children running on the worker** (#245). The per-task scope inherited `DISPOSE_SAFELY`, so a leftover child was zombified rather than cancelled, and nothing reached that scope's disposal anyway while the child was alive: the awaiter had its result and the child went on running, out of the active count and past the worker's drain. The sync path already cancelled at task end; both modes now answer the same.
- **`pdo_mysql/009-pdo_cancellation` cancelled on a wall clock and failed whenever the runner was busy** (#247). The test slept a fixed 100 ms before cancelling, so its expected output claimed that connecting to MySQL finishes inside that budget; on CI it does not, and the cancellation landed during connect. The coroutine now says when the query is under way and the caller waits for that. Test only.
Expand Down
43 changes: 31 additions & 12 deletions thread.c
Original file line number Diff line number Diff line change
Expand Up @@ -2494,28 +2494,47 @@ static void op_array_to_emalloc(zend_op_array *op_array)

/* opcodes + literals
*
* Under !ZEND_USE_ABS_CONST_ADDR (64-bit) RT_CONSTANT stores the literal
* as an int32_t offset from the opline. emalloc may place independent
* allocations several GB apart, so we cannot call emalloc separately for
* opcodes and literals — the offset would overflow and the VM would
* read from unrelated memory. Mirror pass_two()'s layout: allocate one
* block with opcodes first (16-byte aligned) and literals immediately
* after. destroy_op_array() already knows this layout via the
* ZEND_ACC_DONE_PASS_TWO flag, which we leave set. */
* The layout is the one pass_two() leaves behind, and it differs by
* platform, because destroy_op_array() frees the two by that same rule:
* one block on 64-bit, where the literals live inside the opcode block and
* only the opcode block is freed, and two blocks under
* ZEND_USE_ABS_CONST_ADDR, where op_array->literals is freed on its own
* and must therefore be an allocation of its own. */
const zval *orig_literals = op_array->literals;
const zend_op *orig_opcodes = op_array->opcodes;

if (op_array->opcodes) {
const size_t literals_size = sizeof(zval) * op_array->last_literal;

zend_op *new_opcodes;
zval *new_literals = NULL;

#if ZEND_USE_ABS_CONST_ADDR
new_opcodes = (zend_op *) emalloc(sizeof(zend_op) * op_array->last);

if (op_array->last_literal) {
new_literals = (zval *) emalloc(literals_size);
}
#else
/* RT_CONSTANT stores the literal as an int32_t offset from the opline.
* emalloc may place independent allocations several GB apart, so
* separate blocks would overflow the offset and send the VM into
* unrelated memory. Opcodes first, 16-byte aligned, literals
* immediately after; destroy_op_array() reads that layout from the
* ZEND_ACC_DONE_PASS_TWO flag, which we leave set. */
const size_t opcodes_size =
ZEND_MM_ALIGNED_SIZE_EX(sizeof(zend_op) * op_array->last, 16);
const size_t literals_size = sizeof(zval) * op_array->last_literal;

zend_op *new_opcodes = (zend_op *) emalloc(opcodes_size + literals_size);
memcpy(new_opcodes, orig_opcodes, sizeof(zend_op) * op_array->last);
new_opcodes = (zend_op *) emalloc(opcodes_size + literals_size);

zval *new_literals = NULL;
if (op_array->last_literal) {
new_literals = (zval *) ((char *) new_opcodes + opcodes_size);
}
#endif

memcpy(new_opcodes, orig_opcodes, sizeof(zend_op) * op_array->last);

if (op_array->last_literal) {
for (uint32_t i = 0; i < op_array->last_literal; i++) {
/* Deep-copy refcounted literals into the worker's heap.
* ZVAL_COPY alone only bumps the refcount and leaves the
Expand Down
Loading