Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor byte-code transformation #285

Merged
merged 56 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
a921c2d
New transformation implemented as a part of javaagents
Mar 4, 2024
411ca48
Merge branch 'develop' into transformation
Mar 4, 2024
f92784c
Test logging enabled to find ConcurrentModification bug
Mar 5, 2024
4562867
Test logging enabled to find ConcurrentModification bug
Mar 5, 2024
649d4cb
Some more logging...
Mar 5, 2024
b305dfc
Fix collectTrace
ndkoval Mar 5, 2024
6904cd5
Avoid undesired effects by previous runner in the trace collection.
Mar 6, 2024
5bfc47c
Java 21 random transformation bug fixed
Mar 7, 2024
95fb495
Coroutines bug fixed
Mar 14, 2024
b81de56
Output fixes
Mar 14, 2024
8f35d2c
Redundant final field checks removed
Mar 14, 2024
a62648f
Review fixes
Mar 14, 2024
11ab86f
Merge branch 'develop' into transformation
Mar 14, 2024
e48d2d8
Merge with develop
Mar 14, 2024
488c6ba
Review fixes
Mar 14, 2024
6c1a8fb
Review fixes
Mar 14, 2024
50aa14d
Review fixes
Mar 14, 2024
a31db20
Review fixes
Mar 14, 2024
61e3f04
ConcurrentModification bug fix attempt 2
Mar 15, 2024
1246649
Output fix
Mar 15, 2024
88258a5
Output fix
Mar 15, 2024
9516e34
Review fixes
Mar 15, 2024
ffd8ef8
Documentation added
Mar 15, 2024
66315ad
Review fixes
Mar 15, 2024
8526733
Review fixes
Mar 16, 2024
bfc0960
Review fixes
Mar 16, 2024
4c88880
Review fixes
Mar 17, 2024
fdec41d
Documentation added
Mar 18, 2024
4618ec5
Refactoring
Mar 18, 2024
0f5db96
Refactoring
Mar 18, 2024
6167b77
Non-determenism fix attempt
Mar 18, 2024
7d70e51
Review
ndkoval Mar 18, 2024
aa4fbe0
Fix Random support
ndkoval Mar 18, 2024
ffc2583
Review fixes
Mar 18, 2024
399520a
Merge remote-tracking branch 'origin/transformation' into transformation
Mar 18, 2024
8c8b4c5
Fix OperationsInAbstractClassTest
ndkoval Mar 18, 2024
2ab9fd0
Merge remote-tracking branch 'origin/transformation' into transformation
Mar 19, 2024
c40f9f1
Review fixes
Mar 19, 2024
a11de54
Review fixes
Mar 19, 2024
8a0a5f8
Review + refactoring
ndkoval Mar 19, 2024
771af8e
Review + refactoring
ndkoval Mar 19, 2024
076f7bd
Review + refactoring
ndkoval Mar 19, 2024
83937b1
Review + refactoring
ndkoval Mar 19, 2024
2c9da61
Review + refactoring
ndkoval Mar 19, 2024
c51ab93
Review + refactoring
ndkoval Mar 19, 2024
98e9385
Documentation
ndkoval Mar 19, 2024
0e1b891
Attempt to fix the representation of atomic constructs
ndkoval Mar 19, 2024
bda1166
Attempt to fix the representation of atomic constructs
ndkoval Mar 19, 2024
63663de
Small refactoring
ndkoval Mar 19, 2024
0fb7ba8
Refactoring
ndkoval Mar 19, 2024
66a5039
Small renaming
ndkoval Mar 19, 2024
d1fa745
Review fixes
Mar 19, 2024
6d3fb40
Review fixes
Mar 19, 2024
03b4506
Review fixes
Mar 19, 2024
f96995f
Do not run JdkUnsafeTraceRepresentationTest on java <13
ndkoval Mar 19, 2024
a4b099d
Merge branch 'transformation' of github.com:JetBrains/lincheck into t…
ndkoval Mar 19, 2024
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: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ kotlin {
val kotlinxCoroutinesVersion: String by project
val asmVersion: String by project
val reflectionsVersion: String by project
val byteBuddyVersion: String by project
dependencies {
api("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion")
api("org.jetbrains.kotlin:kotlin-stdlib-common:$kotlinVersion")
Expand All @@ -51,6 +52,7 @@ kotlin {
api("org.ow2.asm:asm-commons:$asmVersion")
api("org.ow2.asm:asm-util:$asmVersion")
api("org.reflections:reflections:$reflectionsVersion")
api("net.bytebuddy:byte-buddy-agent:$byteBuddyVersion")
avpotapov00 marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ asmVersion=9.6
atomicfuVersion=0.20.2
# For java.util.* remapping
reflectionsVersion=0.10.2
byteBuddyVersion=1.14.0
avpotapov00 marked this conversation as resolved.
Show resolved Hide resolved

junitVersion=4.13.1
jctoolsVersion=3.3.0
Expand Down
4 changes: 2 additions & 2 deletions src/jvm/main/org/jetbrains/kotlinx/lincheck/Reporter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ package org.jetbrains.kotlinx.lincheck

import org.jetbrains.kotlinx.lincheck.LoggingLevel.*
import org.jetbrains.kotlinx.lincheck.execution.*
import org.jetbrains.kotlinx.lincheck.runner.*
import org.jetbrains.kotlinx.lincheck.strategy.*
import org.jetbrains.kotlinx.lincheck.strategy.managed.*
import sun.nio.ch.lincheck.TestThread
import java.io.*
import kotlin.math.max

Expand Down Expand Up @@ -573,7 +573,7 @@ private fun StringBuilder.appendDeadlockWithDumpFailure(failure: DeadlockWithDum
failure.threadDump?.let { threadDump ->
// Sort threads to produce same output for the same results
for ((t, stackTrace) in threadDump.entries.sortedBy { it.key.id }) {
val threadNumber = if (t is FixedActiveThreadsExecutor.TestThread) t.iThread.toString() else "?"
val threadNumber = (t as? TestThread)?.threadNumber?.toString() ?: "?"
avpotapov00 marked this conversation as resolved.
Show resolved Hide resolved
appendLine("Thread-$threadNumber:")
stackTrace.map {
StackTraceElement(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,22 @@

package org.jetbrains.kotlinx.lincheck;

import kotlin.Unit;
avpotapov00 marked this conversation as resolved.
Show resolved Hide resolved
import org.jetbrains.kotlinx.lincheck.runner.*;
import org.jetbrains.kotlinx.lincheck.strategy.*;
import org.jetbrains.kotlinx.lincheck.strategy.managed.*;
import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.*;
import org.jetbrains.kotlinx.lincheck.strategy.stress.*;
import org.jetbrains.kotlinx.lincheck.transformation.LincheckClassVisitor;
import org.jetbrains.kotlinx.lincheck.transformation.TransformationMode;
import org.objectweb.asm.*;
import org.objectweb.asm.commons.*;
import org.objectweb.asm.util.*;

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
import java.util.stream.Collectors;

import static org.jetbrains.kotlinx.lincheck.TransformationClassLoader.*;
import static org.jetbrains.kotlinx.lincheck.UtilsKt.getCanonicalClassName;
Expand All @@ -49,18 +50,12 @@ public class TransformationClassLoader extends ExecutionClassLoader {
private static final Map<String, byte[]> stressStrategyBytecodeCache = new ConcurrentHashMap<>();
private static final Map<String, byte[]> modelCheckingStrategyBytecodeCache = new ConcurrentHashMap<>();

public TransformationClassLoader(Strategy strategy, Runner runner) {
public TransformationClassLoader(Strategy strategy) {
classTransformers = new ArrayList<>();
// Apply the strategy's transformer at first, then the runner's one.
if (strategy.needsTransformation()) classTransformers.add(strategy::createTransformer);
if (runner.needsTransformation()) classTransformers.add(runner::createTransformer);
remapper = UtilsKt.getRemapperByTransformers(
// create transformers just for their class information
classTransformers.stream()
// pass any parameter to Transformer constructors - it won't be used
.map(constructor -> constructor.apply(new TraceClassVisitor(null)))
.collect(Collectors.toList())
);
TransformationMode transformationMode = strategy instanceof ModelCheckingStrategy ? TransformationMode.MODEL_CHECKING : TransformationMode.STRESS;
avpotapov00 marked this conversation as resolved.
Show resolved Hide resolved
classTransformers.add((cw) -> new LincheckClassVisitor(transformationMode, cw));
ndkoval marked this conversation as resolved.
Show resolved Hide resolved
remapper = UtilsKt.getRemapperByTransformers(transformationMode);
if (strategy instanceof StressStrategy) {
bytecodeCache = stressStrategyBytecodeCache;
} else if (strategy instanceof ManagedStrategy && ((ManagedStrategy) strategy).useBytecodeCache()) {
Expand All @@ -78,7 +73,7 @@ public TransformationClassLoader(Function<ClassVisitor, ClassVisitor> classTrans
*/
private static boolean doNotTransform(String className) {
if (className.startsWith(REMAPPED_PACKAGE_CANONICAL_NAME)) return false;
if (ManagedStrategyTransformerKt.isImpossibleToTransformApiClass(className)) return true;
if (isImpossibleToTransformApiClass(className)) return true;
return className.startsWith("sun.") ||
className.startsWith("java.") ||
className.startsWith("jdk.internal.") ||
Expand All @@ -89,8 +84,7 @@ private static boolean doNotTransform(String className) {
) ||
className.startsWith("com.intellij.rt.coverage.") ||
(className.startsWith("org.jetbrains.kotlinx.lincheck.") &&
!className.startsWith("org.jetbrains.kotlinx.lincheck_test.") &&
!className.equals(ManagedStrategyStateHolder.class.getName())
!className.startsWith("org.jetbrains.kotlinx.lincheck_test.")
) ||
className.equals(kotlinx.coroutines.DebugKt.class.getName()) ||
className.equals(kotlin.coroutines.CoroutineContext.class.getName()) ||
Expand All @@ -104,6 +98,16 @@ private static boolean doNotTransform(String className) {
className.equals("kotlinx.coroutines.CancelHandlerBase");
}

/**
* Some API classes cannot be transformed due to the [sun.reflect.CallerSensitive] annotation.
*/
public static boolean isImpossibleToTransformApiClass(String className) {
return Objects.equals(className, "sun.misc.Unsafe") ||
Objects.equals(className, "jdk.internal.misc.Unsafe") ||
avpotapov00 marked this conversation as resolved.
Show resolved Hide resolved
Objects.equals(className, "java.lang.invoke.VarHandle") ||
className.startsWith("java.util.concurrent.atomic.Atomic") && className.endsWith("FieldUpdater");
}

/**
* Returns `true` if the specified class should not be transformed.
* Note that this method takes into consideration whether class name will be remapped.
Expand Down
47 changes: 32 additions & 15 deletions src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import kotlinx.coroutines.*
import org.jetbrains.kotlinx.lincheck.execution.*
import org.jetbrains.kotlinx.lincheck.runner.*
import org.jetbrains.kotlinx.lincheck.strategy.managed.*
import org.jetbrains.kotlinx.lincheck.transformation.TransformationMode
import org.jetbrains.kotlinx.lincheck.verifier.*
import org.objectweb.asm.*
import org.objectweb.asm.commons.*
import sun.nio.ch.lincheck.TestThread
import java.io.*
import java.lang.ref.*
import java.lang.reflect.*
Expand Down Expand Up @@ -143,7 +144,16 @@ internal class StoreExceptionHandler :
internal fun <T> CancellableContinuation<T>.cancelByLincheck(promptCancellation: Boolean): CancellationResult {
val exceptionHandler = context[CoroutineExceptionHandler] as StoreExceptionHandler
exceptionHandler.exception = null
val cancelled = cancel(cancellationByLincheckException)

val currentThread = Thread.currentThread() as? TestThread
val inIgnoredSection = currentThread?.inIgnoredSection ?: false
currentThread?.inIgnoredSection = false
val cancelled = try {
cancel(cancellationByLincheckException)
} finally {
currentThread?.inIgnoredSection = inIgnoredSection
avpotapov00 marked this conversation as resolved.
Show resolved Hide resolved
}

exceptionHandler.exception?.let {
throw it.cause!! // let's throw the original exception, ignoring the internal coroutines details
}
Expand Down Expand Up @@ -172,8 +182,8 @@ object CancellableContinuationHolder {

fun storeCancellableContinuation(cont: CancellableContinuation<*>) {
val t = Thread.currentThread()
if (t is FixedActiveThreadsExecutor.TestThread) {
t.cont = cont
if (t is TestThread) {
t.suspendedContinuation = cont
} else {
CancellableContinuationHolder.storedLastCancellableCont = cont
avpotapov00 marked this conversation as resolved.
Show resolved Hide resolved
}
Expand Down Expand Up @@ -282,17 +292,17 @@ private class CustomObjectInputStream(val loader: ClassLoader, inputStream: Inpu
* threads that are related to the specified [runner].
*/
internal fun collectThreadDump(runner: Runner) = Thread.getAllStackTraces().filter { (t, _) ->
t is FixedActiveThreadsExecutor.TestThread && t.runnerHash == runner.hashCode()
t is TestThread && t.runnerHash == runner.hashCode()
avpotapov00 marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* This method helps to encapsulate remapper logic from strategy interface.
* The remapper is determined based on the used transformers.
*/
internal fun getRemapperByTransformers(classTransformers: List<ClassVisitor>): Remapper? =
when {
classTransformers.any { it is ManagedStrategyTransformer } -> JavaUtilRemapper()
else -> null
internal fun getRemapperByTransformers(transformationMode: TransformationMode): Remapper? =
when(transformationMode) {
TransformationMode.MODEL_CHECKING -> JavaUtilRemapper()
TransformationMode.STRESS -> null
}

internal val String.canonicalClassName get() = this.replace('/', '.')
Expand Down Expand Up @@ -336,13 +346,20 @@ internal object InternalLincheckTestUnexpectedException : Exception()
*/
internal class LincheckInternalBugException(cause: Throwable): Exception(cause)

internal fun stackTraceRepresentation(stackTrace: Array<StackTraceElement>): List<String> {
return transformStackTraceBackFromRemapped(stackTrace).map { it.toString() }.filter { line ->
"org.jetbrains.kotlinx.lincheck.strategy" !in line
&& "org.jetbrains.kotlinx.lincheck.runner" !in line
&& "org.jetbrains.kotlinx.lincheck.UtilsKt" !in line
internal inline fun <R> runInIgnoredSection(block: () -> R): R =
avpotapov00 marked this conversation as resolved.
Show resolved Hide resolved
runInIgnoredSection(Thread.currentThread(), block)

ndkoval marked this conversation as resolved.
Show resolved Hide resolved
private inline fun <R> runInIgnoredSection(currentThread: Thread, block: () -> R): R =
if (currentThread is TestThread && !currentThread.inIgnoredSection) {
currentThread.inIgnoredSection = true
try {
block()
} finally {
currentThread.inIgnoredSection = false
}
} else {
block()
}
}

internal fun transformStackTraceBackFromRemapped(stackTrace: Array<StackTraceElement>) = stackTrace.map {
StackTraceElement(it.className.removePrefix(TransformationClassLoader.REMAPPED_PACKAGE_CANONICAL_NAME), it.methodName, it.fileName, it.lineNumber)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* 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/.
*/

// we need to use some "legal" package for the bootstrap class loader
@file:Suppress("PackageDirectoryMismatch")

package sun.nio.ch.lincheck

import sun.misc.*
import java.lang.reflect.Modifier.*
import java.util.concurrent.atomic.*

/**
* [CodeLocations] object is used to maintain the mapping between unique IDs and code locations.
* When Lincheck detects an error in the model checking mode, it provides a detailed interleaving trace.
* This trace includes a list of all shared memory events that occurred during the execution of the program,
* along with their corresponding code locations. To minimize overhead, Lincheck assigns unique IDs to all
* code locations it analyses, and stores more detailed information necessary for trace generation in this object.
*/
internal object CodeLocations {
avpotapov00 marked this conversation as resolved.
Show resolved Hide resolved
private val codeLocations = ArrayList<StackTraceElement>()

/**
* Registers a new code location and returns its unique ID.
*
* @param stackTraceElement Stack trace element representing the new code location.
* @return Unique ID of the new code location.
*/
@JvmStatic
@Synchronized
fun newCodeLocation(stackTraceElement: StackTraceElement): Int {
val id = codeLocations.size
codeLocations.add(stackTraceElement)
return id
}

/**
* Returns the [StackTraceElement] associated with the specified code location ID.
*
* @param codeLocationId ID of the code location.
* @return [StackTraceElement] corresponding to the given ID.
*/
@JvmStatic
@Synchronized
fun stackTrace(codeLocationId: Int): StackTraceElement {
return codeLocations[codeLocationId]
}
}

/**
* [AtomicFields] is used to map atomic objects to their names (usually, the corresponding field names)
* and class owners. The weak identity hash map ensures that atomic objects are compared using reference
* equality and does not prevent them from being garbage collected.
*/
internal object AtomicFields {
private val unsafe: Unsafe = try {
val unsafeField = Unsafe::class.java.getDeclaredField("theUnsafe")
unsafeField.isAccessible = true
unsafeField.get(null) as Unsafe
} catch (ex: Exception) {
throw RuntimeException(ex)
}

fun getAtomicFieldName(updater: Any): String? {
if (updater !is AtomicIntegerFieldUpdater<*> && updater !is AtomicLongFieldUpdater<*> && updater !is AtomicReferenceFieldUpdater<*, *>) {
throw IllegalArgumentException("Provided object is not a recognized Atomic*FieldUpdater type.")
}
// Extract the private offset value and find the matching field.
try {
// Cannot use neither reflection not MethodHandles.Lookup, as they lead to a warning.
val tclassField = updater.javaClass.getDeclaredField("tclass")
val targetType = unsafe.getObject(updater, unsafe.objectFieldOffset(tclassField)) as Class<*>

val offsetField = updater.javaClass.getDeclaredField("offset")
val offset = unsafe.getLong(updater, unsafe.objectFieldOffset(offsetField))

for (field in targetType.declaredFields) {
try {
if (isNative(field.modifiers)) continue
val fieldOffset = if (isStatic(field.modifiers)) unsafe.staticFieldOffset(field)
else unsafe.objectFieldOffset(field)
if (fieldOffset == offset) return field.name
} catch (t: Throwable) {
t.printStackTrace()
}
}
} catch (t: Throwable) {
t.printStackTrace()
}

return null // Field not found
}
}

/**
* [FinalFields] object is used to track final fields across different classes.
* It is used to maintain a set of all final fields and
* provides helper functions to add and check final fields.
*/
internal object FinalFields {
avpotapov00 marked this conversation as resolved.
Show resolved Hide resolved
private val finalFields = HashSet<String>() // className + SEPARATOR + fieldName
private const val SEPARATOR = "$^&*-#"

/**
* Adds a new final field to the set.
*/
fun addFinalField(className: String, fieldName: String) {
finalFields.add(className + SEPARATOR + fieldName)
}

/**
* Checks if the given field of a class is final.
*
* @param className Name of the class that contains the field.
* @param fieldName Name of the field to be checked.
* @return `true` if the field is final, `false` otherwise.
*/
fun isFinalField(className: String, fieldName: String) =
finalFields.contains(className + SEPARATOR + fieldName)
}
Loading