Native: prohibit calling suspend functions from autoreleasepool {}

If a suspend function is called from `autoreleasepool {}` block, this
might cause the autoreleasepool frame to be removed earlier than
expected. See https://youtrack.jetbrains.com/issue/KT-50786 for more
details.

^KT-50786 Fixed
This commit is contained in:
Svyatoslav Scherbina
2022-08-22 17:59:17 +02:00
committed by Space
parent 55cfb3af20
commit 860f99482a
4 changed files with 135 additions and 0 deletions
@@ -36,6 +36,9 @@ import org.jetbrains.kotlin.ir.symbols.impl.IrSimpleFunctionSymbolImpl
import org.jetbrains.kotlin.ir.symbols.impl.IrValueParameterSymbolImpl
import org.jetbrains.kotlin.ir.types.*
import org.jetbrains.kotlin.ir.util.*
import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid
import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid
import org.jetbrains.kotlin.ir.visitors.acceptVoid
import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
@@ -682,6 +685,42 @@ private class InteropLoweringPart1(val context: Context) : BaseInteropIrTransfor
}
}
override fun visitBlock(expression: IrBlock): IrExpression {
if (expression is IrReturnableBlock && expression.inlineFunctionSymbol?.isAutoreleasepool() == true) {
// Prohibit calling suspend functions from `autoreleasepool {}` block.
// See https://youtrack.jetbrains.com/issue/KT-50786 for more details.
// Note: we can't easily check this in frontend, because we need to prohibit indirect cases like
/// inline fun <T> myAutoreleasepool(block: () -> T) = autoreleasepool(block)
/// myAutoreleasepool { suspendHere() }
expression.acceptVoid(object : IrElementVisitorVoid {
override fun visitElement(element: IrElement) {
element.acceptChildrenVoid(this)
}
override fun visitCall(expression: IrCall) {
super.visitCall(expression)
if (expression.symbol.owner.isSuspend) {
context.reportCompilationError(
"Calling suspend functions from `autoreleasepool {}` is prohibited, " +
"see https://youtrack.jetbrains.com/issue/KT-50786",
currentFile,
expression
)
}
}
})
}
return super.visitBlock(expression)
}
private fun IrFunctionSymbol.isAutoreleasepool(): Boolean {
return this.owner.name.asString() == "autoreleasepool" && this.owner.parent.let { parent ->
parent is IrPackageFragment && parent.fqName == InteropFqNames.packageName
}
}
private fun IrBuilderWithScope.callAllocAndInit(
classPtr: IrExpression,
initMethodInfo: ObjCMethodInfo,
@@ -0,0 +1,30 @@
import kotlinx.cinterop.*
import kotlin.coroutines.*
open class EmptyContinuation(override val context: CoroutineContext = EmptyCoroutineContext) : Continuation<Any?> {
companion object : EmptyContinuation()
override fun resumeWith(result: Result<Any?>) { result.getOrThrow() }
}
lateinit var continuation: Continuation<Unit>
suspend fun suspendHere(): Unit = suspendCoroutine { cont ->
continuation = cont
}
fun startCoroutine(block: suspend () -> Unit) {
block.startCoroutine(EmptyContinuation)
}
fun main() {
autoreleasepool {
startCoroutine {
autoreleasepool {
suspendHere()
}
}
}
continuation.resume(Unit)
}
@@ -0,0 +1,32 @@
import kotlinx.cinterop.*
import kotlin.coroutines.*
open class EmptyContinuation(override val context: CoroutineContext = EmptyCoroutineContext) : Continuation<Any?> {
companion object : EmptyContinuation()
override fun resumeWith(result: Result<Any?>) { result.getOrThrow() }
}
lateinit var continuation: Continuation<Unit>
suspend fun suspendHere(): Unit = suspendCoroutine { cont ->
continuation = cont
}
fun startCoroutine(block: suspend () -> Unit) {
block.startCoroutine(EmptyContinuation)
}
inline fun <T> myAutoreleasepool(block: () -> T) = autoreleasepool(block)
fun main() {
autoreleasepool {
startCoroutine {
myAutoreleasepool {
suspendHere()
}
}
}
continuation.resume(Unit)
}
@@ -0,0 +1,34 @@
package kt50786
import kotlinx.cinterop.*
import kotlin.coroutines.*
open class EmptyContinuation(override val context: CoroutineContext = EmptyCoroutineContext) : Continuation<Any?> {
companion object : EmptyContinuation()
override fun resumeWith(result: Result<Any?>) { result.getOrThrow() }
}
lateinit var continuation: Continuation<Unit>
suspend fun suspendHere(): Unit = suspendCoroutine { cont ->
continuation = cont
}
fun startCoroutine(block: suspend () -> Unit) {
block.startCoroutine(EmptyContinuation)
}
@kotlin.test.Test
fun testSafeSuspensionIsAllowedInAutoreleasepool() {
// This test checks that the compiler doesn't prohibit calling suspend functions from `autoreleasepool {}`
// if this call is not actually in the block, but in the local declaration inside it.
// See https://youtrack.jetbrains.com/issue/KT-50786 for more details.
autoreleasepool {
startCoroutine {
suspendHere()
}
}
continuation.resume(Unit)
}