Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bootstrap/src/sun/nio/ch/lincheck/EventTracker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ interface EventTracker {
fun afterWrite()

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

Expand Down
4 changes: 2 additions & 2 deletions bootstrap/src/sun/nio/ch/lincheck/Injections.java
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,8 @@ public static void beforeMethodCall(Object owner, String className, String metho
* 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 methodName, int codeLocation, Object[] params) {
getEventTracker().beforeAtomicMethodCall(owner, methodName, codeLocation, params);
public static void beforeAtomicMethodCall(Object owner, String className, String methodName, int codeLocation, Object[] params) {
getEventTracker().beforeAtomicMethodCall(owner, className, methodName, codeLocation, params);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

package org.jetbrains.kotlinx.lincheck

import org.jetbrains.kotlinx.lincheck.strategy.managed.getObjectNumber
import org.jetbrains.kotlinx.lincheck.strategy.managed.ObjectLabelFactory.getObjectNumber
import org.jetbrains.kotlinx.lincheck.util.readFieldViaUnsafe
import sun.misc.Unsafe
import java.lang.reflect.Field
Expand Down
33 changes: 33 additions & 0 deletions src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import sun.nio.ch.lincheck.TestThread
import org.jetbrains.kotlinx.lincheck.runner.*
import org.jetbrains.kotlinx.lincheck.strategy.managed.*
import org.jetbrains.kotlinx.lincheck.transformation.LincheckClassFileTransformer
import org.jetbrains.kotlinx.lincheck.util.UnsafeHolder
import org.jetbrains.kotlinx.lincheck.verifier.*
import java.io.PrintWriter
import java.io.StringWriter
Expand Down Expand Up @@ -194,6 +195,38 @@ internal val Throwable.text: String get() {
return writer.buffer.toString()
}

/**
* Returns all found fields in the hierarchy.
* Multiple fields with the same name and the same type may be returned
* if they appear in the subclass and a parent class.
*/
internal val Class<*>.allDeclaredFieldWithSuperclasses get(): List<Field> {
val fields: MutableList<Field> = ArrayList<Field>()
var currentClass: Class<*>? = this
while (currentClass != null) {
val declaredFields: Array<Field> = currentClass.declaredFields
fields.addAll(declaredFields)
currentClass = currentClass.superclass
}
return fields
}

internal fun findFieldNameByOffset(targetType: Class<*>, offset: Long): String? {
// Extract the private offset value and find the matching field.
for (field in targetType.declaredFields) {
try {
if (Modifier.isNative(field.modifiers)) continue
val fieldOffset = if (Modifier.isStatic(field.modifiers)) UnsafeHolder.UNSAFE.staticFieldOffset(field)
else UnsafeHolder.UNSAFE.objectFieldOffset(field)
if (fieldOffset == offset) return field.name
} catch (t: Throwable) {
t.printStackTrace()
}
}

return null // Field not found
}

/**
* Utility exception for test purposes.
* When this exception is thrown by an operation, it will halt testing with [UnexpectedExceptionInvocationResult].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,18 @@ internal open class ParallelThreadsRunner(

private fun createTestInstance() {
testInstance = testClass.newInstance()
// In the model checking mode, we need to ensure
// that all the necessary classes and instrumented
// after creating a test instance.
if (strategy is ModelCheckingStrategy && !ensuredTestInstanceIsTransformed) {
LincheckJavaAgent.ensureObjectIsTransformed(testInstance)
ensuredTestInstanceIsTransformed = true
if (strategy is ModelCheckingStrategy) {
// We pass the test instance to the strategy to initialize the call stack.
// It should be done here as we create the test instance in the `run` method in the runner, after
// `initializeInvocation` method call of ManagedStrategy.
strategy.initializeCallStack(testInstance)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this change, the comment above is incorrect.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besides, it's unclear what this call does and why you need it in the runner.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this logic added to Runner instead of ManagedStrategy.initializeInvocation?

// In the model checking mode, we need to ensure
// that all the necessary classes and instrumented
// after creating a test instance.
if (!ensuredTestInstanceIsTransformed) {
LincheckJavaAgent.ensureObjectIsTransformed(testInstance)
ensuredTestInstanceIsTransformed = true
}
}
testThreadExecutions.forEach { it.testInstance = testInstance }
validationPartExecution?.let { it.testInstance = testInstance }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

package org.jetbrains.kotlinx.lincheck.strategy.managed

import org.jetbrains.kotlinx.lincheck.findFieldNameByOffset
import org.jetbrains.kotlinx.lincheck.util.UnsafeHolder.UNSAFE
import java.lang.reflect.Modifier
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater
import java.util.concurrent.atomic.AtomicLongFieldUpdater
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater
Expand All @@ -22,7 +22,8 @@ import java.util.concurrent.atomic.AtomicReferenceFieldUpdater
* equality and does not prevent them from being garbage collected.
*/
internal object AtomicFieldUpdaterNames {
fun getAtomicFieldUpdaterName(updater: Any): String? {

internal fun getAtomicFieldUpdaterName(updater: Any): String? {
if (updater !is AtomicIntegerFieldUpdater<*> && updater !is AtomicLongFieldUpdater<*> && updater !is AtomicReferenceFieldUpdater<*, *>) {
throw IllegalArgumentException("Provided object is not a recognized Atomic*FieldUpdater type.")
}
Expand All @@ -35,16 +36,7 @@ internal object AtomicFieldUpdaterNames {
val offsetField = updater.javaClass.getDeclaredField("offset")
val offset = UNSAFE.getLong(updater, UNSAFE.objectFieldOffset(offsetField))

for (field in targetType.declaredFields) {
try {
if (Modifier.isNative(field.modifiers)) continue
val fieldOffset = if (Modifier.isStatic(field.modifiers)) UNSAFE.staticFieldOffset(field)
else UNSAFE.objectFieldOffset(field)
if (fieldOffset == offset) return field.name
} catch (t: Throwable) {
t.printStackTrace()
}
}
return findFieldNameByOffset(targetType, offset)
} catch (t: Throwable) {
t.printStackTrace()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* Lincheck
*
* Copyright (C) 2019 - 2024 JetBrains s.r.o.
*
* This Source Code Form is subject to the terms of the
* Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
* with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

package org.jetbrains.kotlinx.lincheck.strategy.managed

import kotlinx.atomicfu.AtomicArray
import kotlinx.atomicfu.AtomicBooleanArray
import kotlinx.atomicfu.AtomicIntArray
import org.jetbrains.kotlinx.lincheck.allDeclaredFieldWithSuperclasses
import org.jetbrains.kotlinx.lincheck.strategy.managed.AtomicReferenceMethodType.*
import org.jetbrains.kotlinx.lincheck.strategy.managed.AtomicReferenceNames.AtomicReferenceOwnerWithName.*
import org.jetbrains.kotlinx.lincheck.strategy.managed.AtomicReferenceNames.TraverseResult.*
import java.lang.reflect.Modifier
import java.util.*
import java.util.concurrent.atomic.AtomicIntegerArray
import java.util.concurrent.atomic.AtomicLongArray
import java.util.concurrent.atomic.AtomicReferenceArray

/**
* Provides method call type to create a more convenient trace point
* with a owner of this AtomicReference field and a name if it can be found.
* Recursively scans the test object, trying to find the provided AtomicReference
* instance as a field. If two or more fields contain this AtomicReference field, then we
* fall back to the default behavior.
*/
internal object AtomicReferenceNames {

internal fun getMethodCallType(
testObject: Any,
atomicReference: Any,
parameters: Array<Any?>
): AtomicReferenceMethodType {
val receiverAndName = getAtomicReferenceReceiverAndName(testObject, atomicReference)
return if (receiverAndName != null) {
if (isAtomicArrayIndexMethodCall(atomicReference, parameters)) {
when (receiverAndName) {
is InstanceOwnerWithName -> InstanceFieldAtomicArrayMethod(receiverAndName.receiver, receiverAndName.fieldName, parameters[0] as Int)
is StaticOwnerWithName -> StaticFieldAtomicArrayMethod(receiverAndName.clazz, receiverAndName.fieldName, parameters[0] as Int)
}
} else {
when (receiverAndName) {
is InstanceOwnerWithName -> AtomicReferenceInstanceMethod(receiverAndName.receiver, receiverAndName.fieldName)
is StaticOwnerWithName -> AtomicReferenceStaticMethod(receiverAndName.clazz, receiverAndName.fieldName)
}
}
} else {
if (isAtomicArrayIndexMethodCall(atomicReference, parameters)) {
AtomicArrayMethod(atomicReference, parameters[0] as Int)
} else {
TreatAsDefaultMethod
}
}
}

private fun isAtomicArrayIndexMethodCall(atomicReference: Any, parameters: Array<Any?>): Boolean {
if (parameters.firstOrNull() !is Int) return false
return atomicReference is AtomicReferenceArray<*> ||
atomicReference is AtomicLongArray ||
atomicReference is AtomicIntegerArray ||
atomicReference is AtomicIntArray ||
atomicReference is AtomicArray<*> ||
atomicReference is AtomicBooleanArray
}

private fun getAtomicReferenceReceiverAndName(testObject: Any, reference: Any): AtomicReferenceOwnerWithName? =
runCatching {
val visitedObjects: MutableSet<Any> = Collections.newSetFromMap(IdentityHashMap())
return when (val result = findObjectField(testObject, reference, visitedObjects)) {
is FieldName -> result.fieldName
MultipleFieldsMatching, NotFound -> null
}
}.getOrElse { exception ->
exception.printStackTrace()
null
}

private sealed interface TraverseResult {
data object NotFound : TraverseResult
data class FieldName(val fieldName: AtomicReferenceOwnerWithName) : TraverseResult
data object MultipleFieldsMatching : TraverseResult
}

private fun findObjectField(testObject: Any?, value: Any, visitedObjects: MutableSet<Any>): TraverseResult {
if (testObject == null) return NotFound
var fieldName: AtomicReferenceOwnerWithName? = null
// We take all the fields from the hierarchy.
// If two or more fields match (===) the AtomicReference object, we fall back to the default behavior,
// so there is no problem that we can receive some fields of the same name and the same type.
for (field in testObject::class.java.allDeclaredFieldWithSuperclasses) {
if (field.type.isPrimitive || !field.trySetAccessible()) continue
val fieldValue = field.get(testObject)

if (fieldValue in visitedObjects) continue
visitedObjects += testObject

if (fieldValue === value) {
if (fieldName != null) return MultipleFieldsMatching

fieldName = if (Modifier.isStatic(field.modifiers)) {
StaticOwnerWithName(field.name, testObject::class.java)
} else {
InstanceOwnerWithName(field.name, testObject)
}
continue
}
when (val result = findObjectField(fieldValue, value, visitedObjects)) {
is FieldName -> {
if (fieldName != null) {
return MultipleFieldsMatching
} else {
fieldName = result.fieldName
}
}

MultipleFieldsMatching -> return result
NotFound -> {}
}
}
return if (fieldName != null) FieldName(fieldName) else NotFound
}

private sealed class AtomicReferenceOwnerWithName(val fieldName: String) {
class StaticOwnerWithName(fieldName: String, val clazz: Class<*>) :
AtomicReferenceOwnerWithName(fieldName)

class InstanceOwnerWithName(fieldName: String, val receiver: Any) :
AtomicReferenceOwnerWithName(fieldName)
}
}

/**
* The type of the AtomicReference method call.
*/
internal sealed interface AtomicReferenceMethodType {
/**
* AtomicArray method call. In this case, we cannot find the owner of this atomic array.
*/
data class AtomicArrayMethod(val atomicArray: Any, val index: Int) : AtomicReferenceMethodType

/**
* AtomicArray method call. Returned if we found the [owner] and the [field], containing this AtomicArray.
*/
data class InstanceFieldAtomicArrayMethod(val owner: Any, val fieldName: String, val index: Int) :
AtomicReferenceMethodType

/**
* Static AtomicArray method call.
*/
data class StaticFieldAtomicArrayMethod(val ownerClass: Class<*>, val fieldName: String, val index: Int) :
AtomicReferenceMethodType

/**
* AtomicReference method call. Returned if we cannot find the owner of this atomic reference.
*/
data object TreatAsDefaultMethod : AtomicReferenceMethodType

/**
* Instance AtomicReference method call. Returned if we found the [owner] and the [fieldName], containing this AtomicArray
*/
data class AtomicReferenceInstanceMethod(val owner: Any, val fieldName: String) : AtomicReferenceMethodType

/**
* Static AtomicReference method call. Returned if we found the [ownerClass] and the [fieldName], containing this AtomicArray
*/
data class AtomicReferenceStaticMethod(val ownerClass: Class<*>, val fieldName: String) : AtomicReferenceMethodType
}
Loading