diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 45bf16d0..531c327c 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -17,10 +17,18 @@ dependencies { testImplementation("org.jetbrains.kotlin:kotlin-test") implementation("org.apache.logging.log4j:log4j-core:2.23.1") implementation("org.apache.logging.log4j:log4j-slf4j-impl:2.23.1") + testCompileOnly(project(":runtime")) } tasks.test { useJUnitPlatform() + val jvmti = project(":jvmti") + val jdk = project(":instrumentation:jdk") + val agent = project(":instrumentation:agent") + executable("${jdk.layout.buildDirectory.get().asFile}/java-inst/bin/java") + jvmArgs("-agentpath:${jvmti.layout.buildDirectory.get().asFile}/native-libs/libjvmti.so") + jvmArgs("-javaagent:${agent.layout.buildDirectory.get().asFile}/libs/" + + "${agent.name}-${agent.version}-shadow.jar") } tasks.named("build") { diff --git a/core/src/main/kotlin/org/pastalab/fray/core/RunContext.kt b/core/src/main/kotlin/org/pastalab/fray/core/RunContext.kt index 3d8e2f67..1e9d4f0f 100644 --- a/core/src/main/kotlin/org/pastalab/fray/core/RunContext.kt +++ b/core/src/main/kotlin/org/pastalab/fray/core/RunContext.kt @@ -281,14 +281,19 @@ class RunContext(val config: Configuration) { context.pendingOperation = ObjectWaitOperation(objId) context.state = ThreadState.Enabled scheduleNextOperation(true) - // If we resume executing, the Object.wait is executed. We should update the - // state of current thread. + // First we need to check if current thread is interrupted. if (canInterrupt) { context.checkInterrupt() } - // No matter if an interrupt is signaled, we need to enter the `wait` method - // first which will unlock the reentrant lock and tries to reacquire it. + + // We also need to check if current thread holds the monitor lock. + if (!lockManager.getLockContext(lockObject).isLockHolder(lockObject, t)) { + // If current thread is not lock holder, we should just continue because + // JVM will throw IllegalMonitorStateException. + return + } + if (lockObject == waitingObject) { context.pendingOperation = ObjectWaitBlocking(waitingObject) } else { diff --git a/core/src/main/kotlin/org/pastalab/fray/core/concurrency/locks/LockContext.kt b/core/src/main/kotlin/org/pastalab/fray/core/concurrency/locks/LockContext.kt index eda4180f..9699d20a 100644 --- a/core/src/main/kotlin/org/pastalab/fray/core/concurrency/locks/LockContext.kt +++ b/core/src/main/kotlin/org/pastalab/fray/core/concurrency/locks/LockContext.kt @@ -24,4 +24,6 @@ interface LockContext : Interruptible { fun hasQueuedThread(tid: Long): Boolean fun isEmpty(): Boolean + + fun isLockHolder(lock: Any, tid: Long): Boolean } diff --git a/core/src/main/kotlin/org/pastalab/fray/core/concurrency/locks/ReentrantLockContext.kt b/core/src/main/kotlin/org/pastalab/fray/core/concurrency/locks/ReentrantLockContext.kt index 9ed2c1df..3be8e73e 100644 --- a/core/src/main/kotlin/org/pastalab/fray/core/concurrency/locks/ReentrantLockContext.kt +++ b/core/src/main/kotlin/org/pastalab/fray/core/concurrency/locks/ReentrantLockContext.kt @@ -5,7 +5,7 @@ import org.pastalab.fray.core.ThreadState import org.pastalab.fray.core.concurrency.operations.ThreadResumeOperation class ReentrantLockContext : LockContext { - private var lockHolder: Long? = null + var lockHolder: Long? = null private val lockTimes = mutableMapOf() // Mapping from thread id to whether the thread is interruptible. private val lockWaiters = mutableMapOf() @@ -102,4 +102,8 @@ class ReentrantLockContext : LockContext { lockWaiters.remove(tid) } } + + override fun isLockHolder(lock: Any, tid: Long): Boolean { + return lockHolder == tid + } } diff --git a/core/src/main/kotlin/org/pastalab/fray/core/concurrency/locks/ReentrantReadWriteLockContext.kt b/core/src/main/kotlin/org/pastalab/fray/core/concurrency/locks/ReentrantReadWriteLockContext.kt index 68e96add..72cc2a84 100644 --- a/core/src/main/kotlin/org/pastalab/fray/core/concurrency/locks/ReentrantReadWriteLockContext.kt +++ b/core/src/main/kotlin/org/pastalab/fray/core/concurrency/locks/ReentrantReadWriteLockContext.kt @@ -185,4 +185,12 @@ class ReentrantReadWriteLockContext : LockContext { unlockReadWaiters() unlockWriteWaiters() } + + override fun isLockHolder(lock: Any, tid: Long): Boolean { + return if (lock is ReadLock) { + readLockHolder.contains(tid) + } else { + writeLockHolder == tid + } + } } diff --git a/core/src/test/java/org/pastalab/fray/core/test/FrayRunner.java b/core/src/test/java/org/pastalab/fray/core/test/FrayRunner.java new file mode 100644 index 00000000..a9a0222e --- /dev/null +++ b/core/src/test/java/org/pastalab/fray/core/test/FrayRunner.java @@ -0,0 +1,45 @@ +package org.pastalab.fray.core.test; + +import kotlin.Unit; +import kotlin.jvm.functions.Function0; +import org.pastalab.fray.core.TestRunner; +import org.pastalab.fray.core.command.Configuration; +import org.pastalab.fray.core.command.ExecutionInfo; +import org.pastalab.fray.core.command.LambdaExecutor; +import org.pastalab.fray.core.randomness.ControlledRandom; +import org.pastalab.fray.core.scheduler.FifoScheduler; +import org.pastalab.fray.core.scheduler.Scheduler; + + +public class FrayRunner { + public Throwable runTest(Function0 exec) { + return runTest(exec, new FifoScheduler(), 1); + } + + public Throwable runTest(Function0 exec, Scheduler scheduler, int iter) { + String testName = this.getClass().getSimpleName(); + Configuration config = new Configuration( + new ExecutionInfo( + new LambdaExecutor(() -> { + exec.invoke(); + return null; + }), + false, + true, + false, + 10000 + ), + "/tmp/report", + iter, + scheduler, + new ControlledRandom(), + true, + false, + true, + false, + false + ); + TestRunner runner = new TestRunner(config); + return runner.run(); + } +} diff --git a/core/src/test/java/org/pastalab/fray/core/test/primitives/WaitTest.java b/core/src/test/java/org/pastalab/fray/core/test/primitives/WaitTest.java new file mode 100644 index 00000000..f06d3673 --- /dev/null +++ b/core/src/test/java/org/pastalab/fray/core/test/primitives/WaitTest.java @@ -0,0 +1,39 @@ +package org.pastalab.fray.core.test.primitives; + +import org.junit.jupiter.api.Test; +import org.pastalab.fray.core.test.FrayRunner; +import org.pastalab.fray.runtime.DeadlockException; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class WaitTest extends FrayRunner { + + @Test + public void testWaitWithoutMonitorLock() { + Throwable result = runTest(() -> { + try { + wait(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return null; + }); + assertTrue(result instanceof IllegalMonitorStateException); + } + + @Test + public void testWaitWithMonitorLock() { + Throwable result = runTest(() -> { + try { + synchronized (this) { + wait(); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return null; + }); + assertTrue(result instanceof DeadlockException); + } +} diff --git a/instrumentation/agent/src/main/kotlin/ApplicationCodeTransformer.kt b/instrumentation/agent/src/main/kotlin/ApplicationCodeTransformer.kt index b17cfaf6..60e2c219 100644 --- a/instrumentation/agent/src/main/kotlin/ApplicationCodeTransformer.kt +++ b/instrumentation/agent/src/main/kotlin/ApplicationCodeTransformer.kt @@ -34,7 +34,7 @@ class ApplicationCodeTransformer : ClassFileTransformer { dotClassName.startsWith("com.github.ajalt") || (dotClassName.startsWith("org.pastalab.fray") && !dotClassName.startsWith("org.pastalab.fray.benchmark") && - !dotClassName.startsWith("org.pastalab.fray.it"))) { + !dotClassName.startsWith("org.pastalab.fray.core.test"))) { // This is likely a JDK class, so skip transformation return classfileBuffer }