Skip to content

Commit

Permalink
Detect atomic primitive method calls dynamically (#336)
Browse files Browse the repository at this point in the history

---------

Signed-off-by: Evgeniy Moiseenko <evg.moiseenko94@gmail.com>
  • Loading branch information
eupp authored Jun 28, 2024
1 parent 8c3ed5d commit 11eefac
Show file tree
Hide file tree
Showing 9 changed files with 511 additions and 304 deletions.
1 change: 0 additions & 1 deletion bootstrap/src/sun/nio/ch/lincheck/EventTracker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ interface EventTracker {
fun afterReflectiveSetter(receiver: Any?, value: Any?)

fun beforeMethodCall(owner: Any?, className: String, methodName: String, codeLocation: Int, params: Array<Any?>)
fun beforeAtomicMethodCall(owner: Any?, className: String, methodName: String, codeLocation: Int, params: Array<Any?>)
fun onMethodCallReturn(result: Any?)
fun onMethodCallException(t: Throwable)

Expand Down
9 changes: 0 additions & 9 deletions bootstrap/src/sun/nio/ch/lincheck/Injections.java
Original file line number Diff line number Diff line change
Expand Up @@ -249,15 +249,6 @@ public static void beforeMethodCall(Object owner, String className, String metho
getEventTracker().beforeMethodCall(owner, className, methodName, codeLocation, params);
}

/**
* Called from the instrumented code before any atomic method call.
* This is just an optimization of [beforeMethodCall] for trusted
* atomic constructs to avoid wrapping the invocations into try-finally blocks.
*/
public static void beforeAtomicMethodCall(Object owner, String className, String methodName, int codeLocation, Object[] params) {
getEventTracker().beforeAtomicMethodCall(owner, className, methodName, codeLocation, params);
}

/**
* Called from the instrumented code after any method successful call, i.e., without any exception.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -878,69 +878,30 @@ abstract class ManagedStrategy(
codeLocation: Int,
params: Array<Any?>
) {
val guarantee = methodGuaranteeType(owner, className, methodName)
when (guarantee) {
ManagedGuaranteeType.IGNORE -> {
if (collectTrace) {
runInIgnoredSection {
val params = if (isSuspendFunction(className, methodName, params)) {
params.dropLast(1).toTypedArray()
} else {
params
}
beforeMethodCall(owner, currentThread, codeLocation, className, methodName, params)
}
}
// It's important that this method can't be called inside runInIgnoredSection, as the ignored section
// flag would be set to false when leaving runInIgnoredSection,
// so enterIgnoredSection would have no effect
enterIgnoredSection()
val guarantee = runInIgnoredSection {
val atomicMethodDescriptor = getAtomicMethodDescriptor(owner, methodName)
val guarantee = when {
(atomicMethodDescriptor != null) -> ManagedGuaranteeType.TREAT_AS_ATOMIC
else -> methodGuaranteeType(owner, className, methodName)
}

ManagedGuaranteeType.TREAT_AS_ATOMIC -> {
runInIgnoredSection {
if (collectTrace) {
beforeMethodCall(owner, currentThread, codeLocation, className, methodName, params)
}
newSwitchPointOnAtomicMethodCall(codeLocation)
}
// It's important that this method can't be called inside runInIgnoredSection, as the ignored section
// flag would be set to false when leaving runInIgnoredSection,
// so enterIgnoredSection would have no effect
enterIgnoredSection()
if (owner == null && atomicMethodDescriptor == null && guarantee == null) { // static method
LincheckJavaAgent.ensureClassHierarchyIsTransformed(className.canonicalClassName)
}

null -> {
if (owner == null) { // static method
runInIgnoredSection {
LincheckJavaAgent.ensureClassHierarchyIsTransformed(className.canonicalClassName)
}
}
if (collectTrace) {
runInIgnoredSection {
val params = if (isSuspendFunction(className, methodName, params)) {
params.dropLast(1).toTypedArray()
} else {
params
}
beforeMethodCall(owner, currentThread, codeLocation, className, methodName, params)
}
}
if (collectTrace) {
addBeforeMethodCallTracePoint(owner, codeLocation, className, methodName, params, atomicMethodDescriptor)
}
if (guarantee == ManagedGuaranteeType.TREAT_AS_ATOMIC) {
newSwitchPointOnAtomicMethodCall(codeLocation)
}
guarantee
}
}

override fun beforeAtomicMethodCall(
owner: Any?,
className: String,
methodName: String,
codeLocation: Int,
params: Array<Any?>
) = runInIgnoredSection {
if (collectTrace) {
beforeMethodCall(owner, currentThread, codeLocation, className, methodName, params)
if (guarantee == ManagedGuaranteeType.IGNORE ||
guarantee == ManagedGuaranteeType.TREAT_AS_ATOMIC) {
// It's important that this method can't be called inside runInIgnoredSection, as the ignored section
// flag would be set to false when leaving runInIgnoredSection,
// so enterIgnoredSection would have no effect
enterIgnoredSection()
}
newSwitchPointOnAtomicMethodCall(codeLocation)
}

override fun onMethodCallReturn(result: Any?) {
Expand Down Expand Up @@ -1056,20 +1017,15 @@ abstract class ManagedStrategy(
suspendedFunctionsStack[iThread].clear()
}

/**
* This method is invoked by a test thread
* before each method invocation.
* @param codeLocation the byte-code location identifier of this invocation
* @param iThread number of invoking thread
*/
private fun beforeMethodCall(
private fun addBeforeMethodCallTracePoint(
owner: Any?,
iThread: Int,
codeLocation: Int,
className: String,
methodName: String,
params: Array<Any?>,
methodParams: Array<Any?>,
atomicMethodDescriptor: AtomicMethodDescriptor?,
) {
val iThread = currentThread
val callStackTrace = callStackTrace[iThread]
val suspendedMethodStack = suspendedFunctionsStack[iThread]
val methodId = if (suspendedMethodStack.isNotEmpty()) {
Expand All @@ -1081,8 +1037,13 @@ abstract class ManagedStrategy(
} else {
methodCallNumber++
}
val params = if (isSuspendFunction(className, methodName, methodParams)) {
methodParams.dropLast(1).toTypedArray()
} else {
methodParams
}
// Code location of the new method call is currently the last one
val tracePoint = createBeforeMethodCallTracePoint(owner, iThread, className, methodName, params, codeLocation)
val tracePoint = createBeforeMethodCallTracePoint(owner, iThread, className, methodName, params, codeLocation, atomicMethodDescriptor)
methodCallTracePointStack[iThread] += tracePoint
callStackTrace.add(CallStackTraceElement(tracePoint, methodId))
if (owner == null) {
Expand All @@ -1098,7 +1059,8 @@ abstract class ManagedStrategy(
className: String,
methodName: String,
params: Array<Any?>,
codeLocation: Int
codeLocation: Int,
atomicMethodDescriptor: AtomicMethodDescriptor?,
): MethodCallTracePoint {
val callStackTrace = callStackTrace[iThread]
val tracePoint = MethodCallTracePoint(
Expand All @@ -1108,27 +1070,29 @@ abstract class ManagedStrategy(
methodName = methodName,
stackTraceElement = CodeLocations.stackTrace(codeLocation)
)
if (owner is VarHandle) {
return initializeVarHandleMethodCallTracePoint(tracePoint, owner, params)
// handle non-atomic methods
if (atomicMethodDescriptor == null) {
val ownerName = if (owner != null) findOwnerName(owner) else simpleClassName(className)
if (ownerName != null) {
tracePoint.initializeOwnerName(ownerName)
}
tracePoint.initializeParameters(params.map { adornedStringRepresentation(it) })
return tracePoint
}
if (owner is AtomicIntegerFieldUpdater<*> || owner is AtomicLongFieldUpdater<*> || owner is AtomicReferenceFieldUpdater<*, *>) {
return initializeAtomicUpdaterMethodCallTracePoint(tracePoint, owner, params)
// handle atomic methods
if (isVarHandle(owner)) {
return initializeVarHandleMethodCallTracePoint(tracePoint, owner as VarHandle, params)
}
if (isAtomicReference(owner)) {
if (isAtomicFieldUpdater(owner)) {
return initializeAtomicUpdaterMethodCallTracePoint(tracePoint, owner!!, params)
}
if (isAtomic(owner) || isAtomicArray(owner)) {
return initializeAtomicReferenceMethodCallTracePoint(tracePoint, owner!!, params)
}
if (isUnsafe(owner)) {
return initializeUnsafeMethodCallTracePoint(tracePoint, owner!!, params)
}

tracePoint.initializeParameters(params.map { adornedStringRepresentation(it) })

val ownerName = if (owner != null) findOwnerName(owner) else simpleClassName(className)
if (ownerName != null) {
tracePoint.initializeOwnerName(ownerName)
}

return tracePoint
error("Unknown atomic method $className::$methodName")
}

private fun simpleClassName(className: String) = className.takeLastWhile { it != '/' }
Expand Down Expand Up @@ -1178,10 +1142,6 @@ abstract class ManagedStrategy(
tracePoint.initializeOwnerName((receiverName?.let { "$it." } ?: "") + "${atomicReferenceInfo.fieldName}[${atomicReferenceInfo.index}]")
tracePoint.initializeParameters(params.drop(1).map { adornedStringRepresentation(it) })
}
AtomicReferenceMethodType.TreatAsDefaultMethod -> {
tracePoint.initializeOwnerName(adornedStringRepresentation(receiver))
tracePoint.initializeParameters(params.map { adornedStringRepresentation(it) })
}
is AtomicReferenceInstanceMethod -> {
val receiverName = findOwnerName(atomicReferenceInfo.owner)
tracePoint.initializeOwnerName(receiverName?.let { "$it.${atomicReferenceInfo.fieldName}" } ?: atomicReferenceInfo.fieldName)
Expand All @@ -1195,6 +1155,10 @@ abstract class ManagedStrategy(
tracePoint.initializeOwnerName("${atomicReferenceInfo.ownerClass.simpleName}.${atomicReferenceInfo.fieldName}[${atomicReferenceInfo.index}]")
tracePoint.initializeParameters(params.drop(1).map { adornedStringRepresentation(it) })
}
AtomicReferenceMethodType.TreatAsDefaultMethod -> {
tracePoint.initializeOwnerName(adornedStringRepresentation(receiver))
tracePoint.initializeParameters(params.map { adornedStringRepresentation(it) })
}
}
return tracePoint
}
Expand Down Expand Up @@ -1237,20 +1201,6 @@ abstract class ManagedStrategy(
return tracePoint
}

private fun isAtomicReference(receiver: Any?) = receiver is AtomicReference<*> ||
receiver is AtomicLong ||
receiver is AtomicInteger ||
receiver is AtomicBoolean ||
receiver is AtomicIntegerArray ||
receiver is AtomicReferenceArray<*> ||
receiver is AtomicLongArray

private fun isUnsafe(receiver: Any?): Boolean {
if (receiver == null) return false
val className = receiver::class.java.name
return className == "sun.misc.Unsafe" || className == "jdk.internal.misc.Unsafe"
}

/**
* Returns beautiful string representation of the [owner].
* If the [owner] is `this` of the current method, then returns `null`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@

package org.jetbrains.kotlinx.lincheck.transformation.transformers

import org.jetbrains.kotlinx.lincheck.*
import org.jetbrains.kotlinx.lincheck.transformation.*
import org.jetbrains.kotlinx.lincheck.util.*
import org.objectweb.asm.Opcodes.*
import org.objectweb.asm.Type
import org.objectweb.asm.Type.*
Expand Down Expand Up @@ -40,17 +42,6 @@ internal class MethodCallTransformer(
}
return
}
if (isAtomicPrimitiveMethod(owner, name)) {
invokeIfInTestingCode(
original = {
visitMethodInsn(opcode, owner, name, desc, itf)
},
code = {
processAtomicMethodCall(desc, opcode, owner, name, itf)
}
)
return
}
invokeIfInTestingCode(
original = {
visitMethodInsn(opcode, owner, name, desc, itf)
Expand Down Expand Up @@ -104,57 +95,6 @@ internal class MethodCallTransformer(
// STACK: result
}

private fun processAtomicMethodCall(desc: String, opcode: Int, owner: String, name: String, itf: Boolean) = adapter.run {
// In the cases of Atomic*FieldUpdater, Unsafe and VarHandle we edit
// the params list before creating a trace point
// to remove redundant parameters as receiver and offset.
// To determine how we should process it, we provide the owner instance.
val provideOwner = opcode != INVOKESTATIC && (
isAtomicClass(owner) ||
isAtomicArrayClass(owner) ||
owner == "sun/misc/Unsafe" ||
owner == "jdk/internal/misc/Unsafe" ||
owner == "java/lang/invoke/VarHandle" ||
(owner.startsWith("java/util/concurrent/atomic") && owner.endsWith("FieldUpdater"))
)
// STACK [INVOKEVIRTUAL]: owner, arguments
// STACK [INVOKESTATIC] : arguments
val argumentLocals = storeArguments(desc)
// STACK [INVOKEVIRTUAL]: owner
// STACK [INVOKESTATIC] : <empty>
if (provideOwner) {
dup()
} else {
visitInsn(ACONST_NULL)
}
// STACK [INVOKEVIRTUAL atomic updater]: owner, owner
// STACK [INVOKESTATIC atomic updater]: null
// STACK [INVOKEVIRTUAL atomic]: owner
// STACK [INVOKESTATIC atomic]: <empty>
push(owner)
push(name)
loadNewCodeLocationId()
// STACK [INVOKEVIRTUAL atomic updater]: owner, owner, className, methodName, codeLocation
// STACK [INVOKESTATIC atomic updater]: null , className, methodName, codeLocation
// STACK [INVOKEVIRTUAL atomic]: owner, className, methodName, codeLocation
// STACK [INVOKESTATIC atomic]: className, methodName, codeLocation
pushArray(argumentLocals)
// STACK: ..., argumentsArray
invokeStatic(Injections::beforeAtomicMethodCall)
invokeBeforeEventIfPluginEnabled("atomic method call $methodName")
// STACK [INVOKEVIRTUAL]: owner
// STACK [INVOKESTATIC] : <empty>
loadLocals(argumentLocals)
// STACK [INVOKEVIRTUAL]: owner, arguments
// STACK [INVOKESTATIC] : arguments
invokeInIgnoredSection {
visitMethodInsn(opcode, owner, name, desc, itf)
}
// STACK: result
processMethodCallResult(desc)
// STACK: result
}

private fun processMethodCallResult(desc: String) = adapter.run {
// STACK: result?
val resultType = Type.getReturnType(desc)
Expand All @@ -173,39 +113,21 @@ internal class MethodCallTransformer(
}
}

private fun isIgnoredMethod(owner: String, methodName: String) =
owner.startsWith("sun/nio/ch/lincheck/") ||
owner.startsWith("org/jetbrains/kotlinx/lincheck/") ||
owner == "kotlin/jvm/internal/Intrinsics" ||
owner == "java/util/Objects" ||
owner == "java/lang/String" ||
owner == "java/lang/Boolean" ||
owner == "java/lang/Long" ||
owner == "java/lang/Integer" ||
owner == "java/lang/Short" ||
owner == "java/lang/Byte" ||
owner == "java/lang/Double" ||
owner == "java/lang/Float" ||
owner == "java/util/Locale" ||
owner == "org/slf4j/helpers/Util" ||
owner == "java/util/Properties"

private fun isAtomicClass(className: String) =
className == "java/util/concurrent/atomic/AtomicInteger" ||
className == "java/util/concurrent/atomic/AtomicLong" ||
className == "java/util/concurrent/atomic/AtomicBoolean" ||
className == "java/util/concurrent/atomic/AtomicReference"

private fun isAtomicArrayClass(className: String) =
className == "java/util/concurrent/atomic/AtomicIntegerArray" ||
className == "java/util/concurrent/atomic/AtomicLongArray" ||
className == "java/util/concurrent/atomic/AtomicReferenceArray"

private fun isAtomicPrimitiveMethod(owner: String, methodName: String) =
owner == "sun/misc/Unsafe" ||
owner == "jdk/internal/misc/Unsafe" ||
owner == "java/lang/invoke/VarHandle" ||
owner.startsWith("java/util/concurrent/") && (owner.contains("Atomic")) ||
owner.startsWith("kotlinx/atomicfu/") && (owner.contains("Atomic"))
private fun isIgnoredMethod(className: String, methodName: String) =
className.startsWith("sun/nio/ch/lincheck/") ||
className.startsWith("org/jetbrains/kotlinx/lincheck/") ||
className == "kotlin/jvm/internal/Intrinsics" ||
className == "java/util/Objects" ||
className == "java/lang/String" ||
className == "java/lang/Boolean" ||
className == "java/lang/Long" ||
className == "java/lang/Integer" ||
className == "java/lang/Short" ||
className == "java/lang/Byte" ||
className == "java/lang/Double" ||
className == "java/lang/Float" ||
className == "java/util/Locale" ||
className == "org/slf4j/helpers/Util" ||
className == "java/util/Properties"

}
Loading

0 comments on commit 11eefac

Please sign in to comment.