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 @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.3.0] - TBD

### Added
- **Bailout Tests**: Added 15 tests covering memory exhaustion and stack overflow scenarios in async operations
- **Garbage Collection Support**: Implemented comprehensive GC handlers for async objects
- Added `async_coroutine_object_gc()` function to track all ZVALs in coroutine structures
- Added `async_scope_object_gc()` function to track ZVALs in scope structures
Expand Down
7 changes: 1 addition & 6 deletions async_API.c
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,6 @@ zend_coroutine_t *spawn(zend_async_scope_t *scope, zend_object * scope_provider,
return &coroutine->coroutine;
}

static void graceful_shutdown(void)
{
start_graceful_shutdown();
}

static void engine_shutdown(void)
{
ZEND_ASYNC_REACTOR_SHUTDOWN();
Expand Down Expand Up @@ -906,7 +901,7 @@ void async_api_register(void)
async_coroutine_resume,
async_coroutine_cancel,
async_spawn_and_throw,
graceful_shutdown,
start_graceful_shutdown,
get_coroutines,
add_microtask,
get_awaiting_info,
Expand Down
52 changes: 35 additions & 17 deletions coroutine.c
Original file line number Diff line number Diff line change
Expand Up @@ -626,31 +626,31 @@ void async_coroutine_finalize(zend_fiber_transfer *transfer, async_coroutine_t *
async_rethrow_exception(exception);
}

if (EG(exception)) {
if (!(coroutine->flags & ZEND_FIBER_FLAG_DESTROYED)
|| !(zend_is_graceful_exit(EG(exception)) || zend_is_unwind_exit(EG(exception)))
) {
coroutine->flags |= ZEND_FIBER_FLAG_THREW;
transfer->flags = ZEND_FIBER_TRANSFER_FLAG_ERROR;

ZVAL_OBJ_COPY(&transfer->value, EG(exception));
}

zend_clear_exception();
}
} zend_catch {
do_bailout = true;
} zend_end_try();

if (UNEXPECTED(EG(exception))) {
if (!(coroutine->flags & ZEND_FIBER_FLAG_DESTROYED)
|| !(zend_is_graceful_exit(EG(exception)) || zend_is_unwind_exit(EG(exception)))
) {
coroutine->flags |= ZEND_FIBER_FLAG_THREW;
transfer->flags = ZEND_FIBER_TRANSFER_FLAG_ERROR;

ZVAL_OBJ_COPY(&transfer->value, EG(exception));
}

zend_clear_exception();
}

if (EXPECTED(ZEND_ASYNC_SCHEDULER != &coroutine->coroutine)) {
// Permanently remove the coroutine from the Scheduler.
if (UNEXPECTED(zend_hash_index_del(&ASYNC_G(coroutines), coroutine->std.handle) == FAILURE)) {
zend_error(E_CORE_ERROR, "Failed to remove coroutine from the list");
}

// Decrease the active coroutine count if the coroutine is not a zombie and is started.
if (ZEND_COROUTINE_IS_STARTED(&coroutine->coroutine)
&& false == ZEND_COROUTINE_IS_ZOMBIE(&coroutine->coroutine)) {
// Decrease the active coroutine count if the coroutine is not a zombie.
if (false == ZEND_COROUTINE_IS_ZOMBIE(&coroutine->coroutine)) {
ZEND_ASYNC_DECREASE_COROUTINE_COUNT
}
}
Expand Down Expand Up @@ -731,6 +731,7 @@ ZEND_STACK_ALIGNED void async_coroutine_execute(zend_fiber_transfer *transfer)
}

EG(vm_stack) = NULL;
bool should_start_graceful_shutdown = false;

zend_first_try {
zend_vm_stack stack = zend_vm_stack_new_page(ZEND_FIBER_VM_STACK_SIZE, NULL);
Expand Down Expand Up @@ -767,16 +768,31 @@ ZEND_STACK_ALIGNED void async_coroutine_execute(zend_fiber_transfer *transfer)
coroutine->coroutine.internal_entry();
}

async_coroutine_finalize(transfer, coroutine);
} zend_catch {
coroutine->flags |= ZEND_FIBER_FLAG_BAILOUT;
transfer->flags = ZEND_FIBER_TRANSFER_FLAG_BAILOUT;
should_start_graceful_shutdown = true;
} zend_end_try();

zend_first_try {
async_coroutine_finalize(transfer, coroutine);
} zend_catch {
coroutine->flags |= ZEND_FIBER_FLAG_BAILOUT;
transfer->flags = ZEND_FIBER_TRANSFER_FLAG_BAILOUT;
should_start_graceful_shutdown = true;
} zend_end_try();

coroutine->context.cleanup = &async_coroutine_cleanup;
coroutine->vm_stack = EG(vm_stack);

if (UNEXPECTED(should_start_graceful_shutdown)) {
zend_first_try {
ZEND_ASYNC_SHUTDOWN();
} zend_catch {
zend_error(E_CORE_WARNING, "A critical error was detected during the initiation of the graceful shutdown mode.");
} zend_end_try();
}

//
// The scheduler coroutine always terminates into the main execution flow.
//
Expand All @@ -787,7 +803,9 @@ ZEND_STACK_ALIGNED void async_coroutine_execute(zend_fiber_transfer *transfer)
if (transfer != ASYNC_G(main_transfer)) {

if (UNEXPECTED(Z_TYPE(transfer->value) == IS_OBJECT)) {
zval_ptr_dtor(&transfer->value);
zend_first_try {
zval_ptr_dtor(&transfer->value);
} zend_end_try();
zend_error(E_CORE_WARNING, "The transfer value must be NULL when the main coroutine is resumed");
}

Expand Down
45 changes: 40 additions & 5 deletions exceptions.c
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,15 @@ ZEND_API ZEND_COLD zend_object * async_throw_error(const char *format, ...)
zend_string *message = zend_vstrpprintf(0, format, args);
va_end(args);

zend_object *obj = zend_throw_exception(async_ce_async_exception, ZSTR_VAL(message), 0);
zend_object *obj = NULL;

if (EXPECTED(EG(current_execute_data))) {
obj = zend_throw_exception(async_ce_async_exception, ZSTR_VAL(message), 0);
} else {
obj = async_new_exception(async_ce_async_exception, ZSTR_VAL(message));
async_apply_exception_to_context(obj);
}

zend_string_release(message);
return obj;
}
Expand All @@ -124,7 +132,14 @@ ZEND_API ZEND_COLD zend_object * async_throw_cancellation(const char *format, ..
va_list args;
va_start(args, format);

zend_object *obj = zend_throw_exception_ex(async_ce_cancellation_exception, 0, format, args);
zend_object *obj = NULL;

if (EXPECTED(EG(current_execute_data))) {
obj = zend_throw_exception_ex(async_ce_cancellation_exception, 0, format, args);
} else {
obj = async_new_exception(async_ce_cancellation_exception, format, args);
async_apply_exception_to_context(obj);
}

va_end(args);
return obj;
Expand All @@ -137,7 +152,14 @@ ZEND_API ZEND_COLD zend_object * async_throw_input_output(const char *format, ..
va_list args;
va_start(args, format);

zend_object *obj = zend_throw_exception_ex(async_ce_input_output_exception, 0, format, args);
zend_object *obj = NULL;

if (EXPECTED(EG(current_execute_data))) {
obj = zend_throw_exception_ex(async_ce_input_output_exception, 0, format, args);
} else {
obj = async_new_exception(async_ce_input_output_exception, format, args);
async_apply_exception_to_context(obj);
}

va_end(args);
return obj;
Expand All @@ -147,15 +169,28 @@ ZEND_API ZEND_COLD zend_object * async_throw_timeout(const char *format, const z
{
format = format ? format : "A timeout of %u microseconds occurred";

return zend_throw_exception_ex(async_ce_timeout_exception, 0, format, timeout);
if (EXPECTED(EG(current_execute_data))) {
return zend_throw_exception_ex(async_ce_timeout_exception, 0, format, timeout);
} else {
zend_object *obj = async_new_exception(async_ce_timeout_exception, format, timeout);
async_apply_exception_to_context(obj);
return obj;
}
}

ZEND_API ZEND_COLD zend_object * async_throw_poll(const char *format, ...)
{
va_list args;
va_start(args, format);

zend_object *obj = zend_throw_exception_ex(async_ce_poll_exception, 0, format, args);
zend_object *obj = NULL;

if (EXPECTED(EG(current_execute_data))) {
obj = zend_throw_exception_ex(async_ce_poll_exception, 0, format, args);
} else {
obj = async_new_exception(async_ce_poll_exception, format, args);
async_apply_exception_to_context(obj);
}

va_end(args);
return obj;
Expand Down
4 changes: 2 additions & 2 deletions scheduler.c
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ void start_graceful_shutdown(void)

// If the exit exception is not defined, we will define it.
if (EG(exception) == NULL && ZEND_ASYNC_EXIT_EXCEPTION == NULL) {
async_throw_error("Graceful shutdown mode is activated");
zend_error(E_CORE_WARNING, "Graceful shutdown mode was started");
}

if (EG(exception) != NULL) {
Expand Down Expand Up @@ -676,7 +676,7 @@ void async_scheduler_main_coroutine_suspend(void)
} zend_end_try();

ZEND_ASYNC_CURRENT_COROUTINE = NULL;
ZEND_ASSERT(ZEND_ASYNC_ACTIVE_COROUTINE_COUNT == 0 && "The active coroutine counter must be 1 at this point");
ZEND_ASSERT(ZEND_ASYNC_ACTIVE_COROUTINE_COUNT == 0 && "The active coroutine counter must be 0 at this point");
ZEND_ASYNC_DEACTIVATE;

if (ASYNC_G(main_transfer)) {
Expand Down
43 changes: 42 additions & 1 deletion scope.c
Original file line number Diff line number Diff line change
Expand Up @@ -1412,12 +1412,29 @@ static zend_always_inline bool try_to_handle_exception(
}
}

async_scope_object_t *scope_object = NULL;

if (UNEXPECTED(current_scope->scope.scope_object == NULL)) {
// The PHP Scope object might already be destroyed by the time the internal Scope still exists.
// To normalize this situation, we’ll create a fake Scope object that will serve as a bridge.
scope_object = ZEND_OBJECT_ALLOC_EX(sizeof(async_scope_object_t), async_ce_scope);
zend_object_std_init(&scope_object->std, async_ce_scope);
object_properties_init(&scope_object->std, async_ce_scope);

if (UNEXPECTED(EG(exception))) {
OBJ_RELEASE(&scope_object->std);
return false;
}

GC_ADDREF(&scope_object->std);
scope_object->scope = current_scope;
}

// Prototype: function (Async\Scope $scope, Async\Coroutine $coroutine, Throwable $e)
zval retval;
zval parameters[3];
ZVAL_UNDEF(&retval);
ZVAL_OBJ(&parameters[0], current_scope->scope.scope_object);
ZVAL_OBJ(&parameters[0], scope_object != NULL ? &scope_object->std : current_scope->scope.scope_object);
ZVAL_OBJ(&parameters[1], &coroutine->std);
ZVAL_OBJ(&parameters[2], exception);

Expand All @@ -1438,6 +1455,14 @@ static zend_always_inline bool try_to_handle_exception(
exception_fci->params = NULL;

if (result == SUCCESS && EG(exception) == NULL) {
if (UNEXPECTED(scope_object != NULL)) {
scope_object->scope = NULL; // Clear reference to avoid double release
if (GC_REFCOUNT(&scope_object->std) > 1) {
GC_DELREF(&scope_object->std);
}
OBJ_RELEASE(&scope_object->std);
}

return true;
}
}
Expand All @@ -1458,10 +1483,26 @@ static zend_always_inline bool try_to_handle_exception(
exception_fci->params = NULL;

if (result == SUCCESS && EG(exception) == NULL) {
if (UNEXPECTED(scope_object != NULL)) {
scope_object->scope = NULL;
if (GC_REFCOUNT(&scope_object->std) > 1) {
GC_DELREF(&scope_object->std);
}
OBJ_RELEASE(&scope_object->std);
}

return true;
}
}

if (UNEXPECTED(scope_object != NULL)) {
scope_object->scope = NULL;
if (GC_REFCOUNT(&scope_object->std) > 1) {
GC_DELREF(&scope_object->std);
}
OBJ_RELEASE(&scope_object->std);
}

return false; // Exception not handled
}

Expand Down
40 changes: 40 additions & 0 deletions tests/bailout/001-memory-exhaustion-simple.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
--TEST--
Memory exhaustion bailout in simple async operation
--SKIPIF--
<?php
$zend_mm_enabled = getenv("USE_ZEND_ALLOC");
if ($zend_mm_enabled === "0") {
die("skip Zend MM disabled");
}
?>
--INI--
memory_limit=2M
--FILE--
<?php

use function Async\spawn;

register_shutdown_function(function() {
echo "Shutdown function called\n";
});

echo "Before spawn\n";

spawn(function() {
echo "Before memory exhaustion\n";
str_repeat('x', 10000000);
echo "After memory exhaustion (should not reach)\n";
});

echo "After spawn\n";

?>
--EXPECTF--
Before spawn
After spawn
Before memory exhaustion

Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d

Warning: Graceful shutdown mode was started in %s on line %d
Shutdown function called
48 changes: 48 additions & 0 deletions tests/bailout/002-memory-exhaustion-nested.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
--TEST--
Memory exhaustion bailout in nested async operations
--SKIPIF--
<?php
$zend_mm_enabled = getenv("USE_ZEND_ALLOC");
if ($zend_mm_enabled === "0") {
die("skip Zend MM disabled");
}
?>
--INI--
memory_limit=2M
--FILE--
<?php

use function Async\spawn;

register_shutdown_function(function() {
echo "Shutdown function called\n";
});

echo "Before spawn\n";

spawn(function() {
echo "Outer async started\n";

spawn(function() {
echo "Inner async started\n";
str_repeat('x', 10000000);
echo "Inner async after memory exhaustion (should not reach)\n";
});

echo "Outer async continues\n";
});

echo "After spawn\n";

?>
--EXPECTF--
Before spawn
After spawn
Outer async started
Outer async continues
Inner async started

Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d

Warning: Graceful shutdown mode was started in %s on line %d
Shutdown function called
Loading