Skip to content

Conversation

avikivity
Copy link
Member

Like continuations, coroutines are glue that cannot be allowed to fail. For example, if cleanup coroutine fails to allocate and returns an exception future, cleanup will not be done and the system will enter an undefined state. Better to crash.

This conflicts with test code that tries to inject memory failures and see how they are handled (file_io_test.cc handle_bad_alloc_test). The coroutine will throw std::bad_alloc (since we don't define get_return_object_on_allocation_failure()), and since it's declared noexcept, will call std::terminate.

To avoid all this, follow future::schedule() and prevent memory allocation failure injection from interfering with coroutine frame allocation. In this regard a coroutine frame is just like a continuation.

@avikivity
Copy link
Member Author

Helps #2899

@avikivity avikivity requested a review from Copilot August 3, 2025 17:35
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR allocates coroutine frames within a critical section to prevent memory allocation failures from causing system instability. The change treats coroutine frame allocation similarly to future continuation allocation, where failure is not an option since it would leave the system in an undefined state.

  • Adds custom operator new to coroutine promise types that uses memory::scoped_critical_alloc_section
  • Prevents memory allocation failure injection from interfering with coroutine frame allocation
  • Resolves conflicts with test code that injects memory failures to test error handling


static void* operator new(size_t size) {
memory::scoped_critical_alloc_section _;
return ::operator new(size + sizeof(promise_type));
Copy link

Copilot AI Aug 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The size calculation is incorrect. The size parameter already includes the size of the coroutine frame which contains the promise_type. Adding sizeof(promise_type) will result in over-allocation and potential memory corruption.

Suggested change
return ::operator new(size + sizeof(promise_type));
return ::operator new(size);

Copilot uses AI. Check for mistakes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You wrote that code.


static void* operator new(size_t size) {
memory::scoped_critical_alloc_section _;
return ::operator new(size + sizeof(promise_type));
Copy link

Copilot AI Aug 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The size calculation is incorrect. The size parameter already includes the size of the coroutine frame which contains the promise_type. Adding sizeof(promise_type) will result in over-allocation and potential memory corruption.

Suggested change
return ::operator new(size + sizeof(promise_type));
return ::operator new(size);

Copilot uses AI. Check for mistakes.

@avikivity avikivity force-pushed the coroutine-critical-section branch from 23fce6d to d4a5009 Compare August 3, 2025 17:36
@avikivity
Copy link
Member Author

v2: don't over-allocate (bad copilot)

Like continuations, coroutines are glue that cannot be allowed
to fail. For example, if cleanup coroutine fails to allocate
and returns an exception future, cleanup will not be done and
the system will enter an undefined state. Better to crash.

This conflicts with test code that tries to inject memory failures
and see how they are handled (file_io_test.cc handle_bad_alloc_test).
The coroutine will throw std::bad_alloc (since we don't define
get_return_object_on_allocation_failure()), and since it's declared
noexcept, will call std::terminate.

To avoid all this, follow future::schedule() and prevent memory
allocation failure injection from interfering with coroutine
frame allocation. In this regard a coroutine frame is just like
a continuation.

This is done by injecting class-specific operator new and operator
delete. Sized deallocation is also defined if supported by the compiler.
@avikivity avikivity force-pushed the coroutine-critical-section branch from d4a5009 to d91b2db Compare August 4, 2025 15:15
@avikivity
Copy link
Member Author

v3:

  • extract into helper class
  • supply matching operator delete (gcc seemed to want it) and matching sized delete.

@kelbon
Copy link

kelbon commented Sep 4, 2025

This breaks code that previously handled future allocation errors correctly.

@avikivity
Copy link
Member Author

Futures don't allocate.

@kelbon
Copy link

kelbon commented Sep 9, 2025

Futures don't allocate.

So why you added operator new/ operator delete in this pull request?

@avikivity
Copy link
Member Author

Futures don't allocate.

So why you added operator new/ operator delete in this pull request?

Because coroutine frames are allocated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants