Closed jacobsa closed 1 year ago
@ChuanqiXu9 thanks very much for working on this. Your commit does fix my reproducer from this thread, but unfortunately it doesn't fix the real example in my codebase mentioned in this comment. I've filed #65018 with a new, even simpler, reproducer. Could you please take a look if you have some time?
Got it. I'll try to take a look but I can't make a promise.
/branch ChuanqiXu9/llvm-project/release/17.x
/pull-request llvm/llvm-project-release-prs#655
/branch ChuanqiXu9/llvm-project/release/17.x
Merged into release/17.x
Remove this from LLVM17.x Release milestone since the fix wouldn't be there.
I've hit what I think is a miscompilation bug in clang, where a write is moved in an illegal way that introduces a data race and/or use of uninitialized memory. Here is a test case reduced from my real codebase (Compiler Explorer):
The idea is that the awaiter is implemented by calling a
Register
function in a foreign translation unit that decides what to do:If the coroutine should be resumed immediately, it returns a null handle to indicate this.
If the coroutine will be resumed later, it reduces some other handle to resume now, for symmetric control. (Maybe
std::noop_coroutine()
.)Further, when we don't actually wind up suspending we need
await_resume
to do some follow-up work, in this case represented by calling theDidntSuspend
function. So we use asuspended
member to track whether we actually suspended. This is written before callingRegister
, and read after resuming.The bug I see in my codebase is that the write of
true
tosuspended
is delayed until after the call toRegister
. In the reduced test case, we have something similar. Here is what Compiler Explorer gives me for clang with-std=c++20 -O1 -fno-exceptions
:The coroutine frame address is in
rbx
. After callingRegister
, the returned handle is stored into the coroutine frame at offset 24 and then resumed (or the original handle resumed if it's empty), and later in[clone .resume]
the handle in the frame at offset 24 is compared to zero to synthesize theif (!suspended)
condition.But it's not safe to store the returned handle in the coroutine frame unless it's zero: any other value indicates that
Register
took responsibility for the coroutine handle, and may have passed it off to another thread. So another thread may have calleddestroy
on the handle by the time we get around to writing into it. Similarly, the other thread may already have resumed the coroutine and see an uninitialized value at offset 24.I think this is a miscompilation. Consider for example that
Register
may contain a critical section under a mutex that hands the coroutine handle off to another thread to resume, with a similar critical section in the other thread synchronizing with the first. (This is the situation in my codebase.) So we have:The write of
suspended
inawait_suspend
is sequenced before the call toRegister
below it inawait_suspend
.The call to
Register
synchronizes with the function on the other thread that resumes the coroutine.That synchronization is sequenced before resuming the coroutine handle.
Resuming the coroutine handle is (I believe?) sequenced before the call to
await_resume
that readssuspended
.Therefore the write of
suspended
inter-thread happens before the read ofsuspended
.So there was no data race before, but clang has introduced one by delaying the write to the coroutine frame.
For what it's worth, I spent some time dumping IR after optimization passes with my real codebase, and in that case this seemed to be related to an interaction betweem
SROAPass
andCoroSplitPass
:Until
SROAPass
the write was a simple store to the coroutine frame, before the call toRegister
.SROAPass
eliminated the write altogether, turning it into phi nodes that plumbed the value directly into the branch. The value was plumbed from before the call toRegister
to after it.CoroSplitPass
re-introduced astore
instruction, after the call toRegister
.I am far from an expert here, but I wonder if
SROAPass
should be forbidden from making optimizatons of this sort across anllvm.coro.suspend
?