[threading] QD-10938: Leak read access to service initializer inside modal progress

GitOrigin-RevId: edd8b8f31320adfa04fc54827e1f146b763b6fb7
This commit is contained in:
Konstantin Nisht
2025-02-07 16:10:02 +00:00
committed by intellij-monorepo-bot
parent 4c83dc3b61
commit 02933bbb0c
7 changed files with 37 additions and 10 deletions
@@ -687,7 +687,7 @@ public interface Application extends ComponentManager {
@ApiStatus.Experimental
@ApiStatus.Internal
default CoroutineContext getLockStateAsCoroutineContext(boolean shared) {
default CoroutineContext getLockStateAsCoroutineContext(CoroutineContext context, boolean shared) {
return EmptyCoroutineContext.INSTANCE;
}
@@ -319,7 +319,7 @@ interface ThreadingSupport {
fun isInsideUnlockedWriteIntentLock(): Boolean
@ApiStatus.Internal
fun getPermitAsContextElement(shared: Boolean): CoroutineContext
fun getPermitAsContextElement(baseContext: CoroutineContext, shared: Boolean): CoroutineContext
@ApiStatus.Internal
fun returnPermitFromContextElement(ctx: CoroutineContext)
@@ -13,6 +13,8 @@ import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.util.Computable
import com.intellij.openapi.util.IntellijInternalApi
import com.intellij.platform.util.progress.*
import com.intellij.platform.util.progress.internalCreateRawHandleFromContextStepIfExistsAndFresh
import com.intellij.platform.util.progress.reportRawProgress
import com.intellij.util.concurrency.BlockingJob
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.concurrency.annotations.RequiresBlockingContext
@@ -480,14 +482,20 @@ private fun assertBackgroundThreadOrWriteAction() {
@IntellijInternalApi
@Internal
fun getLockPermitContext(forSharing: Boolean = false): CoroutineContext {
return getLockPermitContext(currentThreadContext(), forSharing)
}
@IntellijInternalApi
@Internal
fun getLockPermitContext(baseContext: CoroutineContext, forSharing: Boolean): CoroutineContext {
val application = ApplicationManager.getApplication()
return if (application != null) {
if (isLockStoredInContext) {
if (EDT.isCurrentThreadEdt()) {
application.getLockStateAsCoroutineContext(forSharing) + LockLeakedFromEDTMarker
application.getLockStateAsCoroutineContext(baseContext, forSharing) + LockLeakedFromEDTMarker
}
else {
application.getLockStateAsCoroutineContext(forSharing)
application.getLockStateAsCoroutineContext(baseContext, forSharing)
}
}
else if (application.isReadAccessAllowed) {
@@ -139,12 +139,12 @@ internal object AnyThreadWriteThreadingSupport: ThreadingSupport {
@Volatile
private var myWriteAcquired: Thread? = null
override fun getPermitAsContextElement(shared: Boolean): CoroutineContext {
override fun getPermitAsContextElement(baseContext: CoroutineContext, shared: Boolean): CoroutineContext {
if (!isLockStoredInContext) {
return EmptyCoroutineContext
}
val element = currentThreadContext()[LockStateContextElement]
val element = baseContext[LockStateContextElement]
if (element?.threadState?.permit != null) {
if (shared) {
element.threadState.fork()
@@ -1285,8 +1285,8 @@ public final class ApplicationImpl extends ClientAwareComponentManager
}
@Override
public CoroutineContext getLockStateAsCoroutineContext(boolean shared) {
return getThreadingSupport().getPermitAsContextElement(shared);
public CoroutineContext getLockStateAsCoroutineContext(CoroutineContext baseContext, boolean shared) {
return getThreadingSupport().getPermitAsContextElement(baseContext, shared);
}
@Override
@@ -9,9 +9,12 @@ import com.intellij.openapi.application.ex.ApplicationEx
import com.intellij.openapi.application.impl.LaterInvocator
import com.intellij.openapi.application.impl.ModalCoroutineTest
import com.intellij.openapi.application.impl.processApplicationQueue
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
import com.intellij.testFramework.common.timeoutRunBlocking
import com.intellij.util.application
import com.intellij.util.concurrency.ImplicitBlockingContextTest
import kotlinx.coroutines.*
import kotlinx.coroutines.future.asCompletableFuture
@@ -21,7 +24,6 @@ import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.extension.ExtendWith
import java.lang.Runnable
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import kotlin.coroutines.ContinuationInterceptor
@@ -427,6 +429,23 @@ class RunWithModalProgressBlockingTest : ModalCoroutineTest() {
}
}
}
@Suppress("ForbiddenInSuspectContextMethod")
@Test
fun `service initializer inherits read lock from context`(): Unit = timeoutRunBlocking(context = Dispatchers.EDT) {
runWithModalProgressBlocking {
ReadAction.nonBlocking<Unit> {
application.service<MyTestService>()
}.executeSynchronously()
}
}
@Service
private class MyTestService {
init {
assertTrue { application.isReadAccessAllowed }
}
}
}
private fun CoroutineScope.runWithModalProgressBlockingCoroutine(action: suspend CoroutineScope.() -> Unit): Job {
@@ -1676,7 +1676,7 @@ private fun <X> runBlockingInitialization(action: suspend CoroutineScope.() -> X
val contextForInitializer =
(ctx.contextModality()?.asContextElement() ?: EmptyCoroutineContext) + // leak modality state into initialization coroutine
(ctx[Job] ?: EmptyCoroutineContext) + // bind to caller Job
getLockPermitContext() + // capture whether the caller holds the read lock
getLockPermitContext(ctx, false) + // capture whether the caller holds the read lock
(currentTemporaryThreadContextOrNull() ?: EmptyCoroutineContext) + // propagate modality state/CurrentlyInitializingInstance
NestedBlockingEventLoop(Thread.currentThread()) // avoid processing events from outer runBlocking (if any)
@Suppress("RAW_RUN_BLOCKING")