-
Notifications
You must be signed in to change notification settings - Fork 953
transform (coroutines): fix memory corruption for tail calls that reference stack allocations #2117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -600,11 +600,11 @@ func (c *coroutineLoweringPass) lowerFuncsPass() { | |
continue | ||
} | ||
|
||
if len(fn.normalCalls) == 0 { | ||
// No suspend points. Lower without turning it into a coroutine. | ||
if len(fn.normalCalls) == 0 && fn.fn.FirstBasicBlock().FirstInstruction().IsAAllocaInst().IsNil() { | ||
// No suspend points or stack allocations. Lower without turning it into a coroutine. | ||
c.lowerFuncFast(fn) | ||
} else { | ||
// There are suspend points, so it is necessary to turn this into a coroutine. | ||
// There are suspend points or stack allocations, so it is necessary to turn this into a coroutine. | ||
c.lowerFuncCoro(fn) | ||
} | ||
} | ||
|
@@ -763,6 +763,27 @@ func (c *coroutineLoweringPass) lowerCallReturn(caller *asyncFunc, call llvm.Val | |
// lowerFuncCoro transforms an async function into a coroutine by lowering async operations to `llvm.coro` intrinsics. | ||
// See https://llvm.org/docs/Coroutines.html for more information on these intrinsics. | ||
func (c *coroutineLoweringPass) lowerFuncCoro(fn *asyncFunc) { | ||
// Ensure that any alloca instructions in the entry block are at the start. | ||
// Otherwise, block splitting would result in unintended behavior. | ||
{ | ||
// Skip alloca instructions at the start of the block. | ||
inst := fn.fn.FirstBasicBlock().FirstInstruction() | ||
for !inst.IsAAllocaInst().IsNil() { | ||
inst = llvm.NextInstruction(inst) | ||
} | ||
Comment on lines
+771
to
+773
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this won't work correctly if there are no alloca instructions in the entry block: it will eventually reach the last instruction in the block and crash on a nil pointer dereference. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The loop condition is the other way around. If there are no alloca instructions this will run 0 times. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, I see. I got confused by the double negation. |
||
|
||
// Find any other alloca instructions and move them after the other allocas. | ||
c.builder.SetInsertPointBefore(inst) | ||
for !inst.IsNil() { | ||
next := llvm.NextInstruction(inst) | ||
if !inst.IsAAllocaInst().IsNil() { | ||
inst.RemoveFromParentAsInstruction() | ||
c.builder.Insert(inst) | ||
} | ||
inst = next | ||
} | ||
Comment on lines
+775
to
+784
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This moves alloca instructions together, which I guess is fine, but I don't see how this addresses the issue I raised before?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This moves all alloca instructions to the start of the entry block, so later the check does correctly apply to all allocas in the entry block. This also fixes the potential issues where an alloca could be moved by a later transformation into another block by |
||
} | ||
|
||
returnType := fn.fn.Type().ElementType().ReturnType() | ||
|
||
// Prepare coroutine state. | ||
|
@@ -827,6 +848,7 @@ func (c *coroutineLoweringPass) lowerFuncCoro(fn *asyncFunc) { | |
} | ||
|
||
// Lower returns. | ||
var postTail llvm.BasicBlock | ||
for _, ret := range fn.returns { | ||
// Get terminator instruction. | ||
terminator := ret.block.LastInstruction() | ||
|
@@ -886,10 +908,37 @@ func (c *coroutineLoweringPass) lowerFuncCoro(fn *asyncFunc) { | |
call.EraseFromParentAsInstruction() | ||
} | ||
|
||
// Replace terminator with branch to cleanup. | ||
// Replace terminator with a branch to the exit. | ||
var exit llvm.BasicBlock | ||
if ret.kind == returnNormal || ret.kind == returnVoid || fn.fn.FirstBasicBlock().FirstInstruction().IsAAllocaInst().IsNil() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here. |
||
// Exit through the cleanup path. | ||
exit = cleanup | ||
} else { | ||
if postTail.IsNil() { | ||
// Create a path with a suspend that never reawakens. | ||
postTail = c.ctx.AddBasicBlock(fn.fn, "post.tail") | ||
c.builder.SetInsertPointAtEnd(postTail) | ||
// %coro.save = call token @llvm.coro.save(i8* %coro.state) | ||
save := c.builder.CreateCall(c.coroSave, []llvm.Value{coroState}, "coro.save") | ||
// %call.suspend = llvm.coro.suspend(token %coro.save, i1 false) | ||
// switch i8 %call.suspend, label %suspend [i8 0, label %wakeup | ||
// i8 1, label %cleanup] | ||
suspendValue := c.builder.CreateCall(c.coroSuspend, []llvm.Value{save, llvm.ConstInt(c.ctx.Int1Type(), 0, false)}, "call.suspend") | ||
sw := c.builder.CreateSwitch(suspendValue, suspend, 2) | ||
unreachableBlock := c.ctx.AddBasicBlock(fn.fn, "unreachable") | ||
sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 0, false), unreachableBlock) | ||
sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 1, false), cleanup) | ||
c.builder.SetInsertPointAtEnd(unreachableBlock) | ||
c.builder.CreateUnreachable() | ||
} | ||
|
||
// Exit through a permanent suspend. | ||
exit = postTail | ||
} | ||
|
||
terminator.EraseFromParentAsInstruction() | ||
c.builder.SetInsertPointAtEnd(ret.block) | ||
c.builder.CreateBr(cleanup) | ||
c.builder.CreateBr(exit) | ||
} | ||
|
||
// Lower regular calls. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think alloca instructions are always at the start of the entry block. I think it would be safer to check the entire entry block for alloca instructions, just in case some are not the first instruction.
(Technically they can be anywhere in the function but we check in other places that this isn't possible).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there are any alloca instructions that are not at the start of the entry block, the coroutine lowering pass crashes iirc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC it crashes if they are not in the entry block. but I think they are allowed if they are not directly at the start of the entry block.