diff --git a/CHANGELOG.md b/CHANGELOG.md index d95defb..9a60bc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/thread.c b/thread.c index 036a2a6..9d35e4d 100644 --- a/thread.c +++ b/thread.c @@ -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