Reflection: fix exceptions on concurrent access to some properties

Using `ReflectionProperties.lazy` is incorrect because it allows several
threads to observe different resulting values if they're computing it
simultaneously (unlike `lazy(PUBLICATION)`, which always returns the
value that "won the race").

In the case of property delegates, for example, if we're invoking
`isAccessible = true` and then `getDelegate()` concurrently, it might
happen that when some thread invokes `getDelegate()`, it gets the
underlying Field object which was written by another thread and which
has not yet been made accessible, leading to
IllegalPropertyDelegateAccessException.

 #KT-27585 Fixed
This commit is contained in:
Alexander Udalov
2023-01-24 16:34:00 +01:00
committed by Space Team
parent e8f95a3376
commit 99b38ccb74
9 changed files with 137 additions and 31 deletions
@@ -34,25 +34,15 @@ class ReflectionIntegrationTest : KtUsefulTestCase() {
val lib = CompilerTestUtil.compileJvmLibrary(File("$root/test.kt"))
val javaHome = System.getProperty("java.home")
val javaExe = File(javaHome, "bin/java.exe").takeIf(File::exists)
?: File(javaHome, "bin/java").takeIf(File::exists)
?: error("Can't find 'java' executable in $javaHome")
val command = arrayOf(
javaExe.absolutePath,
runJava(
"-ea",
"-classpath",
tmpdir.absolutePath,
"Main",
lib.absolutePath,
ForTestCompileRuntime.runtimeJarForTests().absolutePath,
ForTestCompileRuntime.reflectJarForTests().absolutePath
ForTestCompileRuntime.reflectJarForTests().absolutePath,
)
val process = ProcessBuilder(*command).inheritIO().start()
process.waitFor(1, TimeUnit.MINUTES)
assertEquals(0, process.exitValue())
}
// This test checks that simultaneous access to kotlin-reflect from different threads works, in case the URLClassLoader instance is
@@ -95,4 +85,41 @@ class ReflectionIntegrationTest : KtUsefulTestCase() {
error.get()?.let { throw it }
}
fun testConcurrentAccessToPropertyDelegate() {
compileAndRunProgram(KtTestUtil.getTestDataPathBase() + "/reflection/concurrentAccessToPropertyDelegate")
}
fun testConcurrentAccessToPrivateFunction() {
compileAndRunProgram(KtTestUtil.getTestDataPathBase() + "/reflection/concurrentAccessToPrivateFunction")
}
private fun compileAndRunProgram(root: String) {
val lib = CompilerTestUtil.compileJvmLibrary(File("$root/test.kt"))
runJava(
"-ea",
"-classpath",
listOf(
ForTestCompileRuntime.runtimeJarForTests().absolutePath,
ForTestCompileRuntime.reflectJarForTests().absolutePath,
lib.absolutePath,
).joinToString(File.pathSeparator),
"TestKt",
)
}
private fun runJava(vararg args: String) {
val javaHome = System.getProperty("java.home")
val javaExe = File(javaHome, "bin/java.exe").takeIf(File::exists)
?: File(javaHome, "bin/java").takeIf(File::exists)
?: error("Can't find 'java' executable in $javaHome")
val process = ProcessBuilder(javaExe.absolutePath, *args).start()
process.waitFor(1, TimeUnit.MINUTES)
val stderr = process.errorStream.reader().readText()
val stdout = process.inputStream.reader().readText()
val exitCode = process.exitValue()
assertEquals("Program exited with exit code $exitCode.\nStdout:\n$stdout\nStderr:\n$stderr", 0, exitCode)
}
}