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

Implement final field detection without Class.forName(..) #297

Merged
merged 5 commits into from
Mar 28, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@

package org.jetbrains.kotlinx.lincheck.transformation

import org.jetbrains.kotlinx.lincheck.LincheckClassLoader.REMAPPED_PACKAGE_INTERNAL_NAME
import java.lang.reflect.Field
import java.lang.reflect.Modifier.*
import org.jetbrains.kotlinx.lincheck.LincheckClassLoader.*
import org.jetbrains.kotlinx.lincheck.transformation.FinalFields.FieldInfo.*
import org.jetbrains.kotlinx.lincheck.transformation.FinalFields.FinalFieldsVisitor
import org.jetbrains.kotlinx.lincheck.transformation.FinalFields.addFinalField
import org.jetbrains.kotlinx.lincheck.transformation.FinalFields.addMutableField
import org.jetbrains.kotlinx.lincheck.transformation.FinalFields.collectFieldInformation
import org.jetbrains.kotlinx.lincheck.transformation.FinalFields.isFinalField
import org.objectweb.asm.*

/**
* [CodeLocations] object is used to maintain the mapping between unique IDs and code locations.
Expand Down Expand Up @@ -53,37 +58,162 @@ internal object CodeLocations {

/**
* [FinalFields] object is used to track final fields across different classes.
* As a field may be declared in the parent class, [isFinalField] method recursively traverses all the
* hierarchy to find the field and check it.
* It is used only during byte-code transformation to get information about fields
* and decide should we track reads of a field or not.
*
* During transformation [addFinalField] and [addMutableField] methods are called when
* we meet a field declaration. Then, when we are faced with field read instruction, [isFinalField] method
* is called.
*
* However, sometimes due to the order of class processing, we may not have information about this field yet,
* as its class it is not loaded for now.
* Then, we fall back to the slow-path algorithm [collectFieldInformation]: we read bytecode and analyze it using [FinalFieldsVisitor].
* If field is found, we record this information and return the result, otherwise we scan superclass and all implemented
* interfaces in the same way.
*/
internal object FinalFields {

fun isFinalField(ownerInternal: String, fieldName: String): Boolean {
var internalName = ownerInternal
if (internalName.startsWith(REMAPPED_PACKAGE_INTERNAL_NAME)) {
internalName = internalName.substring(REMAPPED_PACKAGE_INTERNAL_NAME.length)
/**
* Stores a map INTERNAL_CLASS_NAME -> { FIELD_NAME -> IS FINAL } for each processed class.
*/
private val classToFieldsMap = HashMap<String, HashMap<String, FieldInfo>>()

/**
* Registers the field [fieldName] as a final field of the class [internalClassName].
*/
fun addFinalField(internalClassName: String, fieldName: String) {
val fields = classToFieldsMap.computeIfAbsent(internalClassName) { HashMap() }
fields[fieldName] = FINAL
}

/**
* Registers the field [fieldName] as a mutable field of the class [internalClassName].
*/
fun addMutableField(internalClassName: String, fieldName: String) {
val fields = classToFieldsMap.computeIfAbsent(internalClassName) { HashMap() }
fields[fieldName] = MUTABLE
}

/**
* Determines if this field is final or not.
*/
fun isFinalField(internalClassName: String, fieldName: String): Boolean {
val fields = classToFieldsMap.computeIfAbsent(internalClassName) { HashMap() }
// Fast-path, in case we already have information about this field.
fields[fieldName]?.let { return it == FINAL }
// If we haven't processed this class yet, fall back to a slow-path, reading the class byte-code.
collectFieldInformation(internalClassName, fieldName, fields)
// Here we must have information about this field, as we scanned all the hierarchy of this class.
val fieldInfo = fields[fieldName] ?: error("Internal error: can't find field with $fieldName in class $internalClassName")
return fieldInfo == FINAL
}

/**
* The slow-path of deciding if this field is final or not.
* Reads class from the classloader, scans it and extracts information about declared fields.
* If the field is not found, recursively searches in the superclass and implemented interfaces.
*/
private fun collectFieldInformation(
internalClassName: String,
fieldName: String,
fields: MutableMap<String, FieldInfo>
): Boolean {
// Read the class from classLoader.
val classReader = getClassReader(internalClassName)
val visitor = FinalFieldsVisitor()
// Scan class.
classReader.accept(visitor, 0)
// Store information about all final and mutable fields.
visitor.finalFields.forEach { field -> fields[field] = FINAL }
visitor.mutableFields.forEach { field -> fields[field] = MUTABLE }
// If the field is found - return it.
if (fieldName in visitor.finalFields || fieldName in visitor.mutableFields) return true
// If field is not present in this class - search in the superclass recursively.
visitor.superClassName?.let { internalSuperClassName ->
val internalSuperClassNameNormalized = internalSuperClassName.normalizedClassName
val superClassFields = classToFieldsMap.computeIfAbsent(internalSuperClassNameNormalized) { hashMapOf() }
val fieldFound = collectFieldInformation(internalSuperClassNameNormalized, fieldName, superClassFields)
// Copy all field information found in the superclass to the current class map.
addFieldsInfoFromSuperclass(internalSuperClassNameNormalized, internalClassName)
if (fieldFound) return true
}
return try {
val clazz = Class.forName(internalName.canonicalClassName)
val field = findField(clazz, fieldName) ?: throw NoSuchFieldException("No $fieldName in ${clazz.name}")
(field.modifiers and FINAL) == FINAL
} catch (e: ClassNotFoundException) {
throw RuntimeException(e)
} catch (e: NoSuchFieldException) {
throw RuntimeException(e)
// If field is not present in this class - search in the all implemented interfaces recursively.
visitor.implementedInterfaces.forEach { interfaceName ->
val normalizedInterfaceName = interfaceName.normalizedClassName
val interfaceFields = classToFieldsMap.computeIfAbsent(normalizedInterfaceName) { hashMapOf() }
val fieldFound = collectFieldInformation(normalizedInterfaceName, fieldName, interfaceFields)
// Copy all field information found in the interface to the current class map.
addFieldsInfoFromSuperclass(normalizedInterfaceName, internalClassName)
if (fieldFound) return true
}
// There is no such field in this class.
return false
}

private fun findField(clazz: Class<*>?, fieldName: String): Field? {
if (clazz == null) return null
val fields = clazz.declaredFields
for (field in fields) if (field.name == fieldName) return field
// No field found in this class.
// Search in super class first, then in interfaces.
findField(clazz.superclass, fieldName)?.let { return it }
clazz.interfaces.forEach { iClass ->
findField(iClass, fieldName)?.let { return it }
private fun getClassReader(internalClassName: String): ClassReader {
val resource = "$internalClassName.class"
val inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(resource)
?: error("Cannot create ClassReader for type $internalClassName")
return inputStream.use { ClassReader(inputStream) }
}

/**
* When processing of a superclass is done, it's important to add all information about
* parent fields to child map to avoid potential scanning of a child class in the search of a parent field.
*/
private fun addFieldsInfoFromSuperclass(superType: String, type: String) {
val superFields = classToFieldsMap[superType] ?: return
val fields = classToFieldsMap.computeIfAbsent(type) { hashMapOf() }
superFields.forEach { (fieldName, isFinal) ->
// If we have a field shadowing (the same field name in the child class),
// it's important not to override this information.
// For example, we may have a final field X in a parent class, and a mutable field X in a child class.
// So, while copying information from the parent class to the child class, we mustn't override that field
// X in the child class is mutable, as it may lead to omitted beforeRead events.
if (fieldName !in fields) {
fields[fieldName] = isFinal
}
}
}

private val String.normalizedClassName: String
get() {
var internalName = this
if (internalName.startsWith(REMAPPED_PACKAGE_INTERNAL_NAME)) {
internalName = internalName.substring(REMAPPED_PACKAGE_INTERNAL_NAME.length)
}
return internalName
}

/**
* This visitor collects information about fields, declared in this class,
* about superclass and implemented interfaces.
*/
private class FinalFieldsVisitor : ClassVisitor(ASM_API) {
val implementedInterfaces = arrayListOf<String>()
val finalFields = arrayListOf<String>()
val mutableFields = arrayListOf<String>()

var superClassName: String? = null


override fun visit(version: Int, access: Int, name: String, signature: String?, superName: String?, interfaces: Array<String>?) {
superClassName = superName
interfaces?.let { implementedInterfaces += interfaces }
super.visit(version, access, name, signature, superName, interfaces)
}
return null

override fun visitField(access: Int, name: String, descriptor: String?, signature: String?, value: Any?): FieldVisitor? {
if ((access and Opcodes.ACC_FINAL) != 0) {
finalFields.add(name)
} else {
mutableFields.add(name)
}
return super.visitField(access, name, descriptor, signature, value)
}
}

private enum class FieldInfo {
FINAL, MUTABLE
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ package org.jetbrains.kotlinx.lincheck.transformation

import org.jetbrains.kotlinx.lincheck.LincheckClassLoader.ASM_API
import org.jetbrains.kotlinx.lincheck.Injections
import org.jetbrains.kotlinx.lincheck.LincheckClassLoader
import org.jetbrains.kotlinx.lincheck.strategy.managed.JavaUtilRemapper
import org.objectweb.asm.*
import org.objectweb.asm.Opcodes.*
Expand All @@ -33,6 +34,21 @@ internal class LincheckClassVisitor(
private var classVersion = 0
private var fileName: String? = null

override fun visitField(
access: Int,
fieldName: String,
descriptor: String?,
signature: String?,
value: Any?
): FieldVisitor {
if (access and ACC_FINAL != 0) {
FinalFields.addFinalField(className, fieldName)
} else {
FinalFields.addMutableField(className, fieldName)
}
return super.visitField(access, fieldName, descriptor, signature, value)
}

override fun visit(
version: Int,
access: Int,
Expand Down Expand Up @@ -500,10 +516,7 @@ internal class LincheckClassVisitor(
lateinit var analyzer: AnalyzerAdapter

override fun visitFieldInsn(opcode: Int, owner: String, fieldName: String, desc: String) = adapter.run {
if (isCoroutineInternalClass(owner) || isCoroutineStateMachineClass(owner) || FinalFields.isFinalField(
owner,
fieldName
)
if (isCoroutineInternalClass(owner) || isCoroutineStateMachineClass(owner) || FinalFields.isFinalField(owner, fieldName)
) {
visitFieldInsn(opcode, owner, fieldName, desc)
return
Expand Down