Fix data race and memory corruption in when_any_state::set - #64
Fix data race and memory corruption in when_any_state::set#64zerodefect wants to merge 2 commits into
Conversation
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.
|
Attempts to resolve #63 . |
|
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. |
|
I don't like this because the atomic is redundant with the one in |
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.
69c54e2 to
8c1ef78
Compare
|
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
left a comment
There was a problem hiding this comment.
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.
| bool set_with(Func&& f) | ||
| { | ||
| return this->set_internal_with([&f] { | ||
| f(); |
There was a problem hiding this comment.
| f(); | |
| std::forward<Func>(f)(); |
| LIBASYNC_TRY { | ||
| // Store the result and finish | ||
| get_internal_task(*this)->set_result(std::forward<T>(result)); | ||
| get_internal_task(*this)->set_result(f()); |
There was a problem hiding this comment.
| get_internal_task(*this)->set_result(f()); | |
| get_internal_task(*this)->set_result(std::forward<Func>(f)()); |
| template<typename T> | ||
| bool set_internal(T&& result) const | ||
| { | ||
| return set_internal_with([&] { return std::forward<T>(result); }); |
There was a problem hiding this comment.
| return set_internal_with([&] { return std::forward<T>(result); }); | |
| return set_internal_with([&]() -> T&& { return std::forward<T>(result); }); |
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.