98 thread pool#100
Merged
EdmondDantes merged 84 commits intomainfrom Apr 16, 2026
Merged
Conversation
Add zend_atomic_int64 and thread event API for spawn_thread support
… support
Remove full function/class table deep copy from snapshot. Child thread now recompiles code on demand via autoloader. Add two-phase zval transfer between threads (pemalloc as intermediate persistent storage). Track all pemalloc allocations for proper bulk cleanup in snapshot_destroy. Fix memory safety in child thread closure creation: prevent destroy_op_array on pemalloc'd data via refcount guard, avoid leaked intermediate static_variables and runtime cache.
…ointers and update related functions
…ry and bootloader callables
…cation and cleanup tracking
Bootloader is responsible for setting up its own autoloaders, so there is no need to transfer them from the parent thread.
Autoloader closures cannot be transferred between threads without deep-copying their op_arrays. Users must provide a bootloader closure that sets up autoloading in the child thread.
- Fix dispose crash when notify_cb hasn't fired: use result_loaded flag to distinguish pemalloc vs emalloc data for proper cleanup - Fix exit_code=0 when zend_call_function succeeds but throws exception - Deduplicate exception transfer code via thread_transfer_exception label - Remove dead code: fcall/bootloader release in dispose (now in snapshot) - Add ZEND_ASSERT for closure copy func pointer - Add ZEND_UNREACHABLE for non-ZTS code path - Validate entry param as Closure via Z_PARAM_OBJECT_OF_CLASS - Error on IS_REFERENCE and IS_RESOURCE in thread_transfer_zval_inner - Error on unknown zval types in thread_transfer_zval_inner - Reorder sections: transfer → load → release for better readability - Document ecalloc usage for thread event allocation
- Fix include order: replace php_main.h + php_globals.h with php.h to ensure PHPAPI is defined before php_globals.h is parsed - Remove dead code: internal_entry branch in libuv_thread_entry (field does not exist in zend_async_thread_event_t) - Remove unused zend_autoload.h include - Update comments to reflect autoloader removal from snapshot
Move thread entry (async_thread_run), request startup/shutdown, and closure creation from libuv_reactor.c to thread.c. Reactor now calls thread lifecycle through API macros (ZEND_ASYNC_THREAD_SNAPSHOT_CREATE, ZEND_ASYNC_THREAD_SNAPSHOT_DESTROY, zend_async_thread_run_fn). Register thread lifecycle in scheduler via async_API.c. Move parent SAPI context into snapshot. Remove libuv_thread_entry wrapper — async_thread_run passed directly to uv_thread_create. Remove obsolete thread tests for code cloning.
Fix pefree crash on non-pemalloc pointers: thread_xlat_put stored original (emalloc/interned) pointers in persistent_map, destructor tried to pefree them. Remove thread_xlat_put (dead code after class copy removal). Use HashTable destructor (thread_persistent_ptr_dtor) for automatic cleanup instead of manual iteration loop.
- Set op_array refcount to NULL instead of emalloc'd counter. zend_create_closure and destroy_op_array both check for NULL and skip increment/cleanup, avoiding leak and preventing efree on pemalloc'd op_array internals. - Set SG(request_info).path_translated from closure filename so error messages show the correct script path. - Free path_translated before php_request_shutdown since it's our allocation, not SAPI-owned.
Replace thread_transferred_object_t with direct zend_object memcpy. Repurpose ce/handlers fields for transit (class_name/prop_count). Load objects via zend_lookup_class (autoload) in parent thread. Move result/exception loading from libuv_reactor.c to thread.c as async_thread_load_result(), called via ZEND_ASYNC_THREAD_LOAD_RESULT API macro. Remove result_loaded bool from async_thread_event_t, use ZEND_THREAD_F_RESULT_LOADED event flag instead. Remove dynamic properties support from object transfer (deprecated since PHP 8.2).
- RemoteException wraps all child thread exceptions with clear thread boundary marker. Stores original exception and remote class name. - ThreadTransferException for data transfer failures (autoload errors, class not found, nesting depth exceeded). - async_thread_load_result handles both success and failure paths: wraps loaded exceptions in RemoteException, creates ThreadTransferException on autoload/load failures with original error as previous. - Add recursion depth protection (THREAD_TRANSFER_MAX_DEPTH) for transfer/load of nested arrays and objects. - Fix const qualifiers, improve variable naming throughout thread.c. - Add THREAD_TRANSFER_CHECK_DEPTH macro to reduce code duplication.
- Replace zend_call_function with zend_execute_ex in thread_call_closure to prevent uncaught exceptions from triggering fatal bailout (no current_execute_data = nowhere to propagate = bailout). - Split async_thread_request_startup: extract async_thread_tsrm_init() for TSRM initialization before zend_try (EG(bailout) not available until TSRM is initialized). Use zend_first_try for initial block. - Add bailout recovery: capture PG(last_error_message) after zend_catch, convert to ThreadTransferException in parent via bailout_error_message. - Pre-allocate fallback error message for OOM bailout safety. - Fix ZEND_THREAD_SET_RESULT_LOADED not set on exception-only path. - Wrap async_thread_request_shutdown in zend_try, call ts_free_thread separately after all zend_end_try blocks. - Add uv_unref for async handle in stop to prevent event loop hang. - Remove exit_code field, update tests for RemoteException.
- Use zend_async_throw(ZEND_ASYNC_EXCEPTION_THREAD_TRANSFER) instead of direct zend_throw_exception_ex for dynamic properties error. - Register thread exception classes in get_class_ce switch. - Handle EG(exception) after async_thread_transfer_zval in async_thread_run to catch transfer errors (unsupported types, depth limit). - Fix zend_exception_set_previous usage in thread_create_transfer_exception. - Add NULL checks for failed thread_transfer_object/hash_table. - Reject objects with dynamic properties (stdClass) with clear error. - Add 18 new thread tests: global context, scalar types, string/array return, empty array, duplicate strings, stdClass rejection, bootloader exception, bailout/exit, depth limit, captured variables, void return.
Destroy snapshot in async_thread_run after closures finish, instead of in dispose (parent thread). Snapshot is no longer needed once closures have been called — all pemalloc'd op_array data has been consumed. Dispose retains a safety-net check for cases where child never started. This also avoids potential cross-thread heap issues with persistent_map destructor running in the parent's context.
GCC cannot inline mutually recursive functions — remove zend_always_inline from thread_release_transferred_zval and thread_release_transferred_hash_table.
…tributes thread_copy_hash_table() was called on the original HashTable before copying its struct to persistent memory, corrupting the original's arData pointer. When the snapshot was destroyed, the original HashTable was left with a dangling pointer, causing use-after-free in the parent thread during closure cleanup. Fix: copy the HashTable struct first (thread_persist_copy_xlat), then call thread_copy_hash_table on the persistent copy. Same pattern already used correctly for IS_ARRAY in thread_copy_zval.
When xlat table returns an existing copy (string/HashTable/object), the pointer was returned without incrementing refcount. Multiple zvals pointing to the same persistent copy would cause double-free on release. - Add GC_ADDREF on xlat hit in thread_transfer_string/hash_table/object - Add GC_SET_REFCOUNT(dst, 1) after memcpy in thread_transfer_object - Add GC_DELREF check in thread_release_transferred_hash_table/object - Fix test 023: array literal copies values, use $arr[0] = &$a instead
Individual pemalloc calls for opcodes, literals, strings etc. placed them at arbitrary addresses in the heap. On ASAN builds (and potentially in production), the distance between opcodes and literals could exceed int32_t range, causing RT_CONSTANT relative offset overflow — the VM would read garbage instead of the correct literal. Replace the persistent_map (HashTable of individually pemalloc'd pointers freed via dtor) with a bump allocator arena: - Single large pemalloc block, grown as needed (64KB initial, 2x) - All op_array data (opcodes, literals, strings, arg_info, etc.) allocated contiguously within the arena - RT_CONSTANT offsets guaranteed small (within same arena block) - Snapshot destroy frees entire arena at once (no per-pointer iteration) - bound_vars remain as individual pemalloc (released separately) Also adds zend_always_inline on hot-path arena_alloc and xlat_get, EXPECTED/UNEXPECTED branch hints for fast path.
Closures created in child thread (e.g. autoloader callbacks registered by bootloader) are destroyed during php_request_shutdown. Their op_arrays reference strings in the snapshot arena. If snapshot is destroyed first, request shutdown accesses freed arena memory (use-after-free). Swap order: request shutdown first, then snapshot arena free.
… large data - 028: calling built-in functions (sort, implode, array_map) inside thread - 029: static variables in closure isolated per thread - 030: sequential stress (10 threads with string returns) - 031: concurrent stress (8 parallel threads) - 032: nested closures and array_map/array_reduce - 033: exception type propagation (TypeError, DivisionByZero, custom) - 034: large data transfer (100KB string, 1000-element array, nested) - 035: parent globals not visible in thread - 036: mixed scalar types in associative array
- 033: use getRemoteClass() and getRemoteException() (correct API) - 035: simplify to test only parent globals isolation (superglobals in child thread need separate feature work)
Child threads execute pre-compiled op_arrays (no compiler runs), so auto_globals_jit callbacks never fire. Call zend_is_auto_global() for _SERVER and _ENV after request startup to force initialization.
…d messaging Implements Async\ThreadChannel with persistent memory (pemalloc) structure separated from PHP object wrapper. Basic send/recv with pthread_mutex protection and zval transfer via async_thread_transfer_zval. Supports: constructor, send, recv, close, isClosed, capacity, count, isEmpty, isFull. Back-pressure (suspend on full/empty) and cross-thread uv_async_send notification not yet implemented.
…init - Replace waiter queues with trigger event HashTables (per-thread) - Use zend_async_trigger_event_t for cross-thread notification - Use zend_hash_index_find_ptr/add_ptr instead of manual zval wrapping - Add trigger_dtor for automatic cleanup on zend_hash_destroy - Fix event_init: remove ZEND_ASYNC_EVENT_F_ZEND_OBJ flag — event lives in persistent memory, separate from zend_object (different allocations) - Simplify ensure_trigger to single lock - Back-pressure logic (suspend on full/empty) implemented but not yet working — coroutine does not resume after send to non-full buffer
- task_group/035: gc_get handler walks task entries in PENDING / RUNNING
/ ERROR states. Closes the task_group_object_gc loop
body that was previously unreached.
- thread/041: status accessors and finally() — exercises isRunning,
isCompleted, isCancelled, getResult, getException,
cancel ("not yet implemented") and the synchronous
finally() path on an already-completed thread.
- thread_pool/031: closure transferring with try/catch, static vars and
a nested function definition exercises the op_array
field-rebinding branches in thread_persist_function_callback.
- context/007: three-level scope hierarchy walks past intermediate
contexts; covers the loop iteration in async_context_find()
that advances scope = scope->parent_scope.
…n, MINFO
- info/003: phpinfo() output for the async module — covers PHP_MINFO_FUNCTION.
- common/timeout_class_methods: forbidden direct construct, isCompleted,
isCancelled and idempotent cancel() on Async\Timeout — also
triggers async_timeout_object_create() factory.
- signal/004: Async\signal() with an already-resolved cancellation Future
— covers the IS_CLOSED fast-path that returns an immediately
rejected Future. (Existing 003 used a Timeout that never
actually transitioned to CLOSED, so this branch was dark.)
- edge_cases/014: explicit Async\graceful_shutdown() call from userland
— covers PHP_FUNCTION(Async_graceful_shutdown) entry.
- iterate/type_error_invalid_argument: Async\iterate() with a non-iterable
first argument throws TypeError, covering the type-error
branch in PHP_FUNCTION(Async_iterate).
Raises async.c line coverage from 76.5% to 84.5% (+8%).
Records the final numbers (74.3% → 77.45% lines across 28 new phpt tests in 5 commits), the per-file delta, the three real bugs that coverage testing surfaced (disposeAfterTimeout ref leak, composite_exception properties_table[7] corruption, pool_strategy_report_failure use-after-free), and the categorised dead-zone map that explains the phpt-only ceiling at ~80-84%. Companion to COVERAGE_PLAN.md, which holds the initial gap survey.
- pool.c: pool_strategy_report_failure() no longer captures a dangling exception pointer. Generic Exception is now constructed directly via object_init_ex(zend_ce_exception) + update_property(MESSAGE) instead of zend_throw_exception + zend_clear_exception, and its lifetime is managed locally via an owns_error flag and zval_ptr_dtor. - scope.c: disposeAfterTimeout() no longer leaks the scope refcount. The timeout callback gets a custom dispose handler that releases the scope ref when the callback is freed before firing; on the fire path, scope_timeout_callback() transfers ownership to the spawned cancellation coroutine, which releases after SCOPE_CANCEL. Raw ref_count++ was replaced with ZEND_ASYNC_EVENT_ADD_REF, and the previously-silent add_callback failure branch now releases the ref and frees the unclaimed callback. - exceptions.c: async_composite_exception_add_exception() no longer writes to hard-coded properties_table[7]. Reads and writes go through zend_read_property / zend_update_property using the property name, so the engine resolves the correct typed-property slot. getExceptions() switched to silent=1 so an empty composite reads back as [] instead of the typed-uninit fatal. Fixed a second latent bug: the PHP addException() method was passing transfer=true to the helper even though Z_PARAM_OBJECT_OF_CLASS only lends a borrowed reference, which caused stored zvals to alias to the last-inserted object once slot-7 corruption stopped masking it. Tests: - tests/pool/052-pool_strategy_report_failure_before_release.phpt exercises reportFailure() via a pool with beforeRelease returning false and verifies the strategy receives a well-formed Exception. - tests/edge_cases/013-composite_exception_direct.phpt expanded from a single-add stub to cover empty-read, three adds of different classes, and class/message round-trip. COVERAGE_REPORT.md updated with per-bug Fix paragraphs and §7 ROI list now marks the three-bug cleanup as done.
Async\Timeout::cancel() disposed the backing timer event, whose async_timeout_event_dispose() calls OBJ_RELEASE(object) assuming the event holds a counted reference. In the current architecture the event only stores a raw pointer (async_timeout_ext_t::std) without a matching GC_ADDREF at creation time — so the release actually decremented the caller's live refcount. The backing object was freed while the userland $t variable still pointed to it, and shutdown hit "IS_OBJ_VALID(objects_store.object_buckets[handle])" in zend_objects_store_del(). Mirror async_timeout_destroy_object() by clearing timeout_ext->std before dispatching the dispose, so the dispose handler sees a NULL std and skips the stray OBJ_RELEASE. Unblocks tests/common/timeout_class_methods.phpt. Full async suite now clean (833 passed, 0 failed, 1 pre-existing XFAIL-passes warn).
Add 7 phpt tests for future.c gaps identified in the gcov run: - 031 isCompleted/isCancelled across pending, completed, rejected and AsyncCancellation-rejected states. - 032 Future::cancel() default, custom cancellation and already-completed no-op branch. - 033 Future::getAwaitingInfo() returns the FutureState info string. - 034 FutureState::complete()/error() double-resolve error paths — exercises "already completed at %s:%d" location reporting. - 035 finally() handler throwing on a rejected parent chains the parent as previous via zend_exception_set_previous(). - 036 map()/catch()/finally() TypeError on non-callable argument. - 037 finally() on Future::completed()/failed() hits the eager-spawn branch in async_future_create_mapper(). New coverage tracking file COVERAGE_PROGRESS.md carries forward the §6 achievable budget from COVERAGE_REPORT.md and marks target #1 done.
Add 7 phpt tests for small surface-area gaps in async.c: - iterate/014 IteratorAggregate::getIterator() throwing propagates through Async\iterate(). - common/timeout_value_error Async\timeout(0/-1/-1000) → ValueError. - common/await_any_of_exception_releases_arrays non-iterable futures arg exercises the results/errors release branch. - common/current_coroutine_not_in_coroutine at script root. - common/current_context_at_root Async\current_context() and Async\coroutine_context() at script root. - common/await_same_cancellation passing the same event as awaitable and cancellation. - sleep/003-delay_zero_immediate Async\delay(0) fast path. The §6 report's "~50 lines" estimate for iterate() turned out to be optimistic — most of the remaining gap is cancel-pending exception merge paths that require both the iterator callback AND the cancel path to throw simultaneously, plus fault-injection branches. COVERAGE_PROGRESS.md target #2 marked done.
Add 5 phpt tests for uncovered TaskGroup synchronous-settled branches and small error surface: - 035 all() synchronous-reject when all tasks already failed before the all() call. - 036 race() synchronous-reject on first already-errored task. - 037 any() synchronous all-failed reject via CompositeException. - 038 bundle: empty any(), negative __construct concurrency, duplicate integer spawnWithKey key. - 039 direct getIterator() call hits the "invalid state" PHP method body — foreach goes through the get_iterator handler instead. Notes in COVERAGE_PROGRESS.md: - Latent segfault: keeping $future = $group->all() across a try/catch crashes at teardown in the synchronously-settled path (worked around in test 035). - Dead code: L1305-1307 "Cannot spawn on a completed TaskGroup" is shadowed by the earlier IS_SEALED check — the COMPLETED flag is only set on sealed groups, so that branch is unreachable.
Add 2 phpt tests for channel iteration gaps: - 039 foreach on an unbuffered (rendezvous) channel — existing test 008 only covered the buffered zval_circular_buffer_pop branch. - 040 foreach ($ch as &$v) → "Cannot iterate channel by reference" hits channel_get_iterator() by_ref error branch. COVERAGE_PROGRESS.md target #4 marked done. Notes: direct $channel->getIterator() call hits the METHOD body but fails the declared `: Iterator` return type with a fatal error (the method returns an __iterator_wrapper internally), so it can't be covered from phpt.
…l, context) Close out phase 2 of the achievable-coverage budget from §6 of the COVERAGE_REPORT. Final aggregate: lines 77.45% → 78.34% (+104 lines), functions 88% → 89.1% (+10 functions). - thread/042-thread_finally_non_callable — finally() non-callable rejection (thread.c L2309-2311). The §6 "~40 lines" estimate turned out to be mostly defensive dead code behind thread_event==NULL and bailout paths; attempting to register finally on a still-running thread surfaces a separate latent SIGSEGV (logged). - fs_watcher/target #6 SKIPPED: the RENAME-on-existing-CHANGE coalesce merge branch can't be triggered from userland on Linux inotify — rename() emits MOVED_FROM/MOVED_TO with a different filename than the preceding CHANGE, so the coalesce key doesn't match. - thread_pool/032-map_on_closed_pool — map() on a closed pool throws ThreadPoolException (thread_pool.c L515-522). - context/008-context_get_missing — Context::get() returning null for a missing key (context.c L242). COVERAGE_PROGRESS.md updated with final lcov summary and per-target notes explaining what remains and why (mostly fault-injection, deadlock-reporter info() helpers, and C-API-only error branches behind already-validated PHP-method type guards).
1. TaskGroup all()/race()/any() synchronous-settled use-after-free
The synchronously-settled fast paths in METHOD(all), METHOD(race)
and METHOD(any) created a waiter via task_group_waiter_future_new()
(which pushed it into group->waiter_events[]), resolved it, wrapped
it in a Future wrapper and returned — but never removed it from the
waiter_events[] vector. The drain-path in task_group_try_complete()
always calls task_group_waiter_event_remove() after resolving; the
sync path forgot to mirror that.
At shutdown, task_group_free_object() force-disposes everything
still in waiter_events[], which efrees the waiter. When the Future
wrapper is then destroyed and releases the waiter it had wrapped,
it touches freed memory → "Future was never used" warning from a
stale zend_future_t followed by a segfault.
Fix: call task_group_waiter_event_remove(waiter) at the end of
each synchronous-resolve branch, matching what
task_group_try_complete() does. Verified the intermediate-
$future form now runs cleanly (tests/task_group/035 expanded to
keep the intermediate variable).
2. Thread::finally() on a still-running thread NULL-scope crash
thread_object_dtor() dispatches registered finally handlers via
async_call_finally_handlers(). That helper unconditionally
dereferences context->scope:
zend_async_new_scope_fn(context->scope, false)
ZEND_ASYNC_EVENT_ADD_REF(&context->scope->event)
but thread.c was passing context->scope = NULL because the Thread
object has no PHP-side scope of its own.
Fix: borrow ZEND_ASYNC_MAIN_SCOPE as the run scope. If the async
subsystem is already off (ZEND_ASYNC_IS_OFF — e.g. dtor firing
during late zend_call_destructors), release the handlers
synchronously without spawning anything.
Also added thread_finally_handlers_dtor() so the GC_ADDREF that
keeps the Thread alive during handler execution gets paired with
an OBJ_RELEASE. Previously context->dtor was NULL and the Thread
object leaked 72 bytes every time dtor-time finally ran.
New test tests/thread/042-thread_finally_dispatch covers both
the non-callable rejection and the previously-crashing still-
running registration path.
Both bugs were identified and logged during coverage phase 2 in
COVERAGE_PROGRESS.md; they no longer block reachable coverage.
…cope Previous fix borrowed ZEND_ASYNC_MAIN_SCOPE as a workaround when dispatching Thread finally handlers, which broke scope semantics — handlers ran detached from whatever scope the thread was spawned in, losing inherited exception handlers and context values. Capture the active scope at spawn time and hold a refcount on its event so it outlives the Thread object. Release in thread_object_free(). The finally-handler dispatcher now uses this captured scope as the parent, so handlers run as children of the caller's scope hierarchy exactly like coroutine / task_group / scope finally do. Changes: - async_thread_object_t: new parent_scope field - Async_spawn_thread(): capture ZEND_ASYNC_CURRENT_SCOPE, ADDREF - thread_object_create(): init parent_scope = NULL - thread_object_dtor(): use thread->parent_scope instead of MAIN_SCOPE - thread_object_free(): release parent_scope ref The ZEND_ASYNC_IS_OFF guard is kept alongside a parent_scope==NULL check as a safety net for the edge case where a Thread outlives the async subsystem (late zend_call_destructors after RSHUTDOWN).
Closed
async_thread_snapshot_create() did not check EG(exception) after deep-copying captured variables, so a transfer_obj handler that threw (e.g. the second transfer of FutureState) still produced a "successful" snapshot with IS_NULL in place of the offending variable. libuv_new_thread_event() then started the child thread; on return from spawn_thread PHP unwound the pending exception, releasing the Thread object and freeing the event, while the worker was still inside thread_call_closure writing to event->exception. Check EG(exception) in snapshot_create, destroy the partial snapshot and return NULL on failure. libuv_new_thread_event now rolls back and returns NULL too, so the thread is never launched. Fixes the heap-use-after-free caught by ASAN in remote_future/006-future_state_transfer_multiple_threads.
The synchronous-settled branches in METHOD(all)/race()/any() call task_group_waiter_event_remove() to unlink a freshly created waiter from the group's waiter_events[] vector while keeping its wrapper Future alive as the method's return value. task_group_waiter_event_remove() only cleared the vector slot; waiter->group was left pointing at the task_group. When the coroutine later released both CVs, the task_group was freed first (its free_obj walked an already-empty vector and never touched the waiter), and then the Future's dispose ran waiter_future_dispose -> remove, which dereferenced the stale group pointer. Reset waiter->group = NULL at the end of task_group_waiter_event_remove() so every detach — whether it originated from dispose, notify, or the sync resolve path — leaves the waiter safe to outlive the group. Drops const from the parameter type accordingly. Fixes the heap-use-after-free caught by ASAN in task_group/035-task_group_all_synchronous_reject.
op_array_to_emalloc() used two independent emalloc() calls for the opcodes and the literals table, then rebased the int32_t RT_CONSTANT offset each opline stores. On 64-bit builds (ZEND_USE_ABS_CONST_ADDR = 0) emalloc can place the two blocks several GB apart, which silently overflows the signed 32-bit offset and leaves the VM reading a literal from an unrelated heap region — caught by ASan inside execute_ex while a child thread ran a closure returning a constant. Mirror pass_two()'s layout instead: allocate one block sized to aligned(opcodes) + literals, place the literals immediately after the aligned opcodes, and leave ZEND_ACC_DONE_PASS_TWO set so destroy_op_array continues to treat the literals as embedded in the opcodes allocation (no separate efree). Capture orig_opcodes before reassignment so the relative-offset path computes new offsets from the stable original layout. Fixes the heap-buffer-overflow caught by ASAN in thread_channel/037-closure_transfer.
thread_copy_op_array_ex memcpy's opcodes including opline->handler. When opcache JIT is enabled, those handlers are JIT stubs with the source op_array's literal addresses and run_time_cache baked in — executing them on a cross-thread copy crashes on a SEGV inside the JIT buffer. Call zend_vm_set_opcode_handler() on each opline after copy so the copy runs on the standard interpreter. Parent's SHM op_array stays JIT-compiled. Fixes all remote_future tests under CI with opcache.enable=1 opcache.jit=tracing.
When thread_transfer_hash_table encountered an error (depth limit, unsupported type like references/resources/dynamic properties), it called zend_throw_error() which triggered zend_bailout() via longjmp because there is no active EG(current_execute_data) during C-level thread transfer. The longjmp skipped all cleanup, leaking every intermediate pemalloc'd HashTable (up to 512 allocations). Fix: store error message in ctx->error instead of throwing during recursive transfer. Check ctx->error after each recursive call and clean up partial allocations before returning. The exception is thrown only at the top level (async_thread_transfer_zval) after all persistent memory has been properly released.
…criber's scope When a remote future completes via trigger callback, the callback handler fires in the scheduler context. ZEND_ASYNC_CURRENT_SCOPE at that point is the Scheduler scope, causing "You cannot use the Scheduler scope to create coroutines" error. Fix: capture scope at map()/catch()/finally() registration time in async_future_callback_t and use it when spawning the mapper coroutine. Also fix ASAN detection in proc_close signal test — USE_ZEND_ALLOC is not set without --asan flag, so additionally check SKIP_ASAN env and ldd output for libasan.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.