Skip to content

Fix data race and memory corruption in when_any_state::set - #64

Open
zerodefect wants to merge 2 commits into
Amanieu:masterfrom
zerodefect:bugfix/when_any_race_condition
Open

Fix data race and memory corruption in when_any_state::set#64
zerodefect wants to merge 2 commits into
Amanieu:masterfrom
zerodefect:bugfix/when_any_race_condition

Conversation

@zerodefect

Copy link
Copy Markdown
Contributor

Concurrent task completions in when_any cause multiple threads to invoke when_any_state::set simultaneously. Due to C++ eager argument evaluation, std::move(result) is evaluated by multiple threads before the internal event.set() lock-free check occurs. Moving from the shared std::vector or std::tuple concurrently leads to double-frees and memory corruption.

This commit introduces a wait-free std::atomic is_set guard into when_any_state. By wrapping the execution in a compare_exchange_strong block, we guarantee that only the first arriving thread evaluates the std::move and triggers the event, safely preserving memory isolation without introducing mutex overhead.

Concurrent task completions in when_any cause multiple threads
to invoke when_any_state::set simultaneously. Due to C++ eager
argument evaluation, std::move(result) is evaluated by
multiple threads before the internal event.set() lock-free
check occurs. Moving from the shared std::vector or std::tuple
concurrently leads to double-frees and memory corruption.

This commit introduces a wait-free std::atomic<bool> is_set
guard into when_any_state. By wrapping the execution in a
compare_exchange_strong block, we guarantee that only the
first arriving thread evaluates the std::move and triggers the
event, safely preserving memory isolation without introducing
mutex overhead.
@zerodefect

Copy link
Copy Markdown
Contributor Author

Attempts to resolve #63 .

@zerodefect

Copy link
Copy Markdown
Contributor Author

Hi @Amanieu , I appreciate that you have moved over to the world of Rust and probaly have you head buried elsewhere, but is maybe something you could pick up?

I like to think this change is quite small, and very obvious when you see it. Thanks.

@Amanieu

Amanieu commented Sep 4, 2026

Copy link
Copy Markdown
Owner

I don't like this because the atomic is redundant with the one in basic_event. A better solution seems to be to add a set_internal_with method that takes a lambda, which is only executed if that atomic operation succeeds. set_internal would then be implemented in terms of that. It may even make sense to expose this publicly on event_task as set_with.

When multiple monitored tasks completed simultaneously, multiple worker threads concurrently evaluated the arguments for event.set({i, std::move(result)}). Because C++ evaluates function arguments before jumping into the function body, this caused a data race on the shared result state before the internal event_task lock could be acquired.

Changes:
* Introduced set_internal_with to basic_event to execute a lambda only after the atomic lock is successfully acquired.
* Exposed set_with on all event_task specializations (Result, Result&, and void).
* Refactored when_any_state::set to use event.set_with(), wrapping the payload construction and std::move inside a lambda.
* Guaranteed that only the single winning thread constructs the payload and executes the move operation, preventing undefined behavior.
* Refactored the original set_internal to forward calls through the new lambda architecture to maintain backwards compatibility.
@zerodefect
zerodefect force-pushed the bugfix/when_any_race_condition branch from 69c54e2 to 8c1ef78 Compare September 4, 2026 09:44
@zerodefect

zerodefect commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Hi @Amanieu , thanks for looking this over. I've revisited the code. I'm still testing it, so these changes should not yet be merged. Nonetheless, I'd appreciate any feedback in the interim.

@Amanieu Amanieu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I'm out of practice with C++ so I had Codex review your changes. It pointed out 2 things:

  • Lambda deduced return types will strip references, this breaks event_task<Value&>.
  • Callable types should still be forwarded so the call can be invoked as an rvalue.

Comment thread include/async++/task.h
bool set_with(Func&& f)
{
return this->set_internal_with([&f] {
f();

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Suggested change
f();
std::forward<Func>(f)();

Comment thread include/async++/task.h
LIBASYNC_TRY {
// Store the result and finish
get_internal_task(*this)->set_result(std::forward<T>(result));
get_internal_task(*this)->set_result(f());

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Suggested change
get_internal_task(*this)->set_result(f());
get_internal_task(*this)->set_result(std::forward<Func>(f)());

Comment thread include/async++/task.h
template<typename T>
bool set_internal(T&& result) const
{
return set_internal_with([&] { return std::forward<T>(result); });

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Suggested change
return set_internal_with([&] { return std::forward<T>(result); });
return set_internal_with([&]() -> T&& { return std::forward<T>(result); });

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