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

perf: resolve fingerprints using method maps #185

Merged
merged 49 commits into from Jun 27, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
5a0e0e2
speed up resolving by using a signature and string map
LisoUseInAIKyrios May 31, 2023
a8a7f79
use parameter values. cleanup
LisoUseInAIKyrios May 31, 2023
b82fe11
removing debug code
LisoUseInAIKyrios May 31, 2023
91a6a83
distinguish between a signature without parameters specified, and a s…
LisoUseInAIKyrios May 31, 2023
78b596a
refactoring
LisoUseInAIKyrios Jun 1, 2023
4441be9
changing performance logging to trace
LisoUseInAIKyrios Jun 1, 2023
afeb772
clear cache when done patching. Not sure if it makes any difference b…
LisoUseInAIKyrios Jun 1, 2023
3d579b9
reuse objects
LisoUseInAIKyrios Jun 1, 2023
b117163
logging
LisoUseInAIKyrios Jun 1, 2023
7a161c5
fix if empty string list is specified
LisoUseInAIKyrios Jun 1, 2023
a1490f0
fixing logging
LisoUseInAIKyrios Jun 2, 2023
57a966b
logging
LisoUseInAIKyrios Jun 2, 2023
9e54b3a
extracting code. Consolidate methods with a large number of paramete…
LisoUseInAIKyrios Jun 2, 2023
5f37968
changing back to trace
LisoUseInAIKyrios Jun 2, 2023
165f2ec
cleanup
LisoUseInAIKyrios Jun 2, 2023
7ba6c08
comments
LisoUseInAIKyrios Jun 2, 2023
08ebd1e
cleanup
LisoUseInAIKyrios Jun 4, 2023
05496c9
comments
LisoUseInAIKyrios Jun 4, 2023
dac8e88
Merge remote-tracking branch 'upstream/dev' into fingerprint_signatur…
LisoUseInAIKyrios Jun 7, 2023
5b988b1
Update src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/M…
LisoUseInAIKyrios Jun 11, 2023
8c9b90e
Update src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/M…
LisoUseInAIKyrios Jun 11, 2023
034e944
Update src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/M…
LisoUseInAIKyrios Jun 11, 2023
fbe7099
Update src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/M…
LisoUseInAIKyrios Jun 11, 2023
e253694
remove clear function
LisoUseInAIKyrios Jun 11, 2023
af94f9c
Merge remote-tracking branch 'origin/fingerprint_signature_map' into …
LisoUseInAIKyrios Jun 11, 2023
66de76b
rename methods
LisoUseInAIKyrios Jun 11, 2023
7d71ad0
remove logging, rename methods, comments.
LisoUseInAIKyrios Jun 11, 2023
cd5c2ab
comments, rearrange code.
LisoUseInAIKyrios Jun 11, 2023
d59fb2a
Update src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/M…
LisoUseInAIKyrios Jun 11, 2023
1d771d9
Update src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/M…
LisoUseInAIKyrios Jun 11, 2023
0dee824
Update src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/M…
LisoUseInAIKyrios Jun 11, 2023
4c25390
Update src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/M…
LisoUseInAIKyrios Jun 11, 2023
168f399
comments, rename container class
LisoUseInAIKyrios Jun 11, 2023
84b6bcb
comments
LisoUseInAIKyrios Jun 11, 2023
208b63a
comments, cleanup
LisoUseInAIKyrios Jun 11, 2023
1f41ee4
comments
LisoUseInAIKyrios Jun 11, 2023
3ae9d26
comments
LisoUseInAIKyrios Jun 11, 2023
0f028ee
comments
LisoUseInAIKyrios Jun 11, 2023
ae1b437
Merge remote-tracking branch 'upstream/dev' into fingerprint_signatur…
LisoUseInAIKyrios Jun 11, 2023
59dbab8
comments
LisoUseInAIKyrios Jun 11, 2023
93ccd6c
comments
LisoUseInAIKyrios Jun 11, 2023
052bbf2
comments
LisoUseInAIKyrios Jun 15, 2023
e0bd1b1
Merge remote-tracking branch 'upstream/dev' into fingerprint_signatur…
LisoUseInAIKyrios Jun 15, 2023
3c8279f
cleanup
LisoUseInAIKyrios Jun 15, 2023
d689fdb
comments
LisoUseInAIKyrios Jun 15, 2023
be13723
Improve semantics
oSumAtrIX Jun 18, 2023
5edf781
Merge branch 'dev' into fingerprint_signature_map
LisoUseInAIKyrios Jun 18, 2023
3658fbc
chore: move comment
oSumAtrIX Jun 27, 2023
ec7857a
feat: log start of patches executionm
oSumAtrIX Jun 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 12 additions & 6 deletions src/main/kotlin/app/revanced/patcher/Patcher.kt
Expand Up @@ -4,8 +4,14 @@ import app.revanced.patcher.data.Context
import app.revanced.patcher.extensions.PatchExtensions.dependencies
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.extensions.PatchExtensions.requiresIntegrations
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolve
import app.revanced.patcher.patch.*
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolveUsingLookupMap
import app.revanced.patcher.patch.BytecodePatch
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.PatchResult
import app.revanced.patcher.patch.PatchResultError
import app.revanced.patcher.patch.PatchResultSuccess
import app.revanced.patcher.patch.ResourcePatch
import app.revanced.patcher.util.VersionReader
import brut.androlib.Androlib
import brut.androlib.meta.UsesFramework
Expand Down Expand Up @@ -259,6 +265,8 @@ class Patcher(private val options: PatcherOptions) {
metadata.metaInfo.versionInfo = resourceTable.versionInfo
metadata.metaInfo.sdkInfo = resourceTable.sdkInfo
}

MethodFingerprint.createMethodLookupMap(context.bytecodeContext.classes.classes)
LisoUseInAIKyrios marked this conversation as resolved.
Show resolved Hide resolved
} finally {
extInputFile.close()
}
Expand Down Expand Up @@ -315,10 +323,7 @@ class Patcher(private val options: PatcherOptions) {
context.resourceContext
} else {
context.bytecodeContext.also { context ->
(patchInstance as BytecodePatch).fingerprints?.resolve(
context,
context.classes.classes
)
(patchInstance as BytecodePatch).fingerprints?.resolveUsingLookupMap(context, logger)
}
}

Expand Down Expand Up @@ -359,6 +364,7 @@ class Patcher(private val options: PatcherOptions) {
if (stopOnError && patchResult.isError()) return@sequence
}
} finally {
MethodFingerprint.clearMethodLookupMap() // No more resolving can occur.
LisoUseInAIKyrios marked this conversation as resolved.
Show resolved Hide resolved
executedPatches.values.reversed().forEach { (patch, _) ->
patch.close()
}
Expand Down
Expand Up @@ -2,29 +2,47 @@ package app.revanced.patcher.fingerprint.method.impl

import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.MethodFingerprintExtensions.fuzzyPatternScanMethod
import app.revanced.patcher.extensions.MethodFingerprintExtensions.name
import app.revanced.patcher.fingerprint.Fingerprint
import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod
import app.revanced.patcher.logging.Logger
import app.revanced.patcher.logging.impl.NopLogger
import app.revanced.patcher.patch.PatchResultError
import app.revanced.patcher.util.proxy.ClassProxy
import org.jf.dexlib2.AccessFlags
import org.jf.dexlib2.Opcode
import org.jf.dexlib2.iface.ClassDef
import org.jf.dexlib2.iface.Method
import org.jf.dexlib2.iface.instruction.Instruction
import org.jf.dexlib2.iface.instruction.ReferenceInstruction
import org.jf.dexlib2.iface.reference.StringReference
import org.jf.dexlib2.util.MethodUtil
import java.util.LinkedList

private typealias StringMatch = MethodFingerprintResult.MethodFingerprintScanResult.StringsScanResult.StringMatch
private typealias StringsScanResult = MethodFingerprintResult.MethodFingerprintScanResult.StringsScanResult

/**
* Represents the [MethodFingerprint] for a method.
* @param returnType The return type of the method.
* @param accessFlags The access flags of the method.
* @param parameters The parameters of the method.
* @param opcodes The list of opcodes of the method.
* A finger print for identifying a method.
LisoUseInAIKyrios marked this conversation as resolved.
Show resolved Hide resolved
*
* To improve patching performance:
* - fastest: specify at least [strings], with the first string being an exact (non-partial) match.
* - faster: specify at least [accessFlags], [returnType], [parameters].
* - fast: specify at least [accessFlags], [returnType].
* - slowest: specify only [opcodes] and nothing else.
LisoUseInAIKyrios marked this conversation as resolved.
Show resolved Hide resolved
*
* For target apps with only a few patches, the resolving speed does not matter and even the
* slowest resolving fingerprints will not be noticed. Only when using dozens of fingerprints
* does the patching speed become apparent.
LisoUseInAIKyrios marked this conversation as resolved.
Show resolved Hide resolved
*
* @param returnType The return type of the method. Partial matches are allowed, and values are compared using startWith.
* For example: "L" matches any object, while "Landroid/view/View;" matches only to an Android view parameter.
LisoUseInAIKyrios marked this conversation as resolved.
Show resolved Hide resolved
* @param accessFlags The access flags of the method using values of [AccessFlags]. Must be an exact match.
LisoUseInAIKyrios marked this conversation as resolved.
Show resolved Hide resolved
* @param parameters The parameters of the method. Partial matches allowed and follow same rules as [returnType].
Copy link
Member

Choose a reason for hiding this comment

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

Is this true? Are all parameters required?

Suggested change
* @param parameters The parameters of the method. Partial matches allowed and follow same rules as [returnType].
* @param parameters The method's parameters compared each using [String.startsWith].

* @param opcodes The list of opcodes of the method, and a `null` opcode means unknown or wildcard value.
LisoUseInAIKyrios marked this conversation as resolved.
Show resolved Hide resolved
* @param strings A list of strings which a method contains.
* Partial matches are allowed, and are compared using contains. For example, "app" matches "happy".
LisoUseInAIKyrios marked this conversation as resolved.
Show resolved Hide resolved
* @param customFingerprint A custom condition for this fingerprint.
* A `null` opcode is equals to an unknown opcode.
*/
abstract class MethodFingerprint(
internal val returnType: String? = null,
Expand All @@ -40,6 +58,136 @@ abstract class MethodFingerprint(
var result: MethodFingerprintResult? = null

companion object {

private class ClassAndMethod(val classDef: ClassDef, val method: Method)
/**
* All methods in the target app.
*/
private val allMethods = mutableListOf<ClassAndMethod>()
LisoUseInAIKyrios marked this conversation as resolved.
Show resolved Hide resolved
/**
* Map of all methods in the target app, keyed to the access/return/parameter signature.
*/
private val signatureMap = mutableMapOf<String, MutableList<ClassAndMethod>>()
/**
* Map of all Strings found in the target app, and the class/method they were found in.
*/
private val stringMap = mutableMapOf<String, MutableList<ClassAndMethod>>()
LisoUseInAIKyrios marked this conversation as resolved.
Show resolved Hide resolved

private fun StringBuilder.appendSignatureKeyParameters(parameters: Iterable<CharSequence>) {
LisoUseInAIKyrios marked this conversation as resolved.
Show resolved Hide resolved
// Maximum parameters to use in the signature key.
// Used to reduce the map size by grouping together uncommon methods.
val maxSignatureParameters = 5
append("p:")
parameters.forEachIndexed { index, parameter ->
if (index >= maxSignatureParameters) return
append(parameter.first())
}
Copy link
Member

Choose a reason for hiding this comment

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

This compound is designed arbitrarily. Why 5 maximum parameters? Why the random "p:" prefix?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The 'p' prefix is needed to distinguish methods that have no parameters. If it did not have "p", the signature would be the same as the key for just access + return (which is not correct).

The 5 parameter limit was chosen after trying different values and seeing what difference it had on performance. Less than 5 it started to get slower, and over 5 made no difference.

Using no limit on the number of parameters worked ok, but the signature map was getting a lot of junk added to it (some methods in YouTube have over 100 parameters!). So instead, all methods with over 5 parameters get binned together with all other 5+ parameter methods with the same return/access type.

I'll add some code comments explaining this.

}

/**
* @return all app methods that contain the first string declared in this signature,
* or NULL if no strings are declared or no exact matches exist.
LisoUseInAIKyrios marked this conversation as resolved.
Show resolved Hide resolved
*/
private fun MethodFingerprint.appMethodsWithSameStrings() : List<ClassAndMethod>? {
LisoUseInAIKyrios marked this conversation as resolved.
Show resolved Hide resolved
if (strings != null && strings.count() > 0) {
// Only check the first String declared
return stringMap[strings.first()]
}
return null
}

/**
* @return all app methods that could match this signature.
*/
private fun MethodFingerprint.appMethodsWithSameSignature() : List<ClassAndMethod> {
LisoUseInAIKyrios marked this conversation as resolved.
Show resolved Hide resolved
if (accessFlags == null) return allMethods

var returnTypeValue = returnType
if (returnTypeValue == null) {
if (AccessFlags.CONSTRUCTOR.isSet(accessFlags)) {
// Constructors always have void return type
returnTypeValue = "V"
} else {
return allMethods
}
}

val key = buildString {
append(accessFlags)
append(returnTypeValue.first())
if (parameters != null) appendSignatureKeyParameters(parameters)
}
return signatureMap[key]!!
}

internal fun clearMethodLookupMap() {
allMethods.clear()
signatureMap.clear()
stringMap.clear()
}

/**
* resolve faster, by creating culled lists based on method signature and Strings contained.
LisoUseInAIKyrios marked this conversation as resolved.
Show resolved Hide resolved
LisoUseInAIKyrios marked this conversation as resolved.
Show resolved Hide resolved
*/
internal fun createMethodLookupMap(classes: Iterable<ClassDef>) {
fun addMethodToMapList(map: MutableMap<String, MutableList<ClassAndMethod>>,
key: String, keyValue: ClassAndMethod) {
var list = map[key]
if (list == null) {
list = LinkedList()
map[key] = list
}
list += keyValue
}

for (classDef in classes) {
for (method in classDef.methods) {
// Key structure is: (access)(returnType)(optional: parameter types)
val accessFlagsReturnKey = method.accessFlags.toString() + method.returnType.first()
val accessFlagsReturnParametersKey = buildString {
append(accessFlagsReturnKey)
appendSignatureKeyParameters(method.parameterTypes)
}
val classAndMethod = ClassAndMethod(classDef, method)

// For signatures with no access or return type specified.
allMethods += classAndMethod
// Access and return type.
addMethodToMapList(signatureMap, accessFlagsReturnKey, classAndMethod)
// Access, return, and parameters.
addMethodToMapList(signatureMap, accessFlagsReturnParametersKey, classAndMethod)
LisoUseInAIKyrios marked this conversation as resolved.
Show resolved Hide resolved

// Look up by Strings (the fastest way to resolve).
method.implementation?.instructions?.forEach { instruction ->
if (instruction.opcode == Opcode.CONST_STRING || instruction.opcode == Opcode.CONST_STRING_JUMBO) {
val string = ((instruction as ReferenceInstruction).reference as StringReference).string
addMethodToMapList(stringMap, string, classAndMethod)
}
}

// The only additional lookup that could benefit, is a map of the full class name to its methods.
// This would require adding a 'class name' field to MethodFingerprint,
// as currently the class name can be specified only with a custom fingerprint.
}
}
}

/**
* Resolve using map built in [createMethodLookupMap]
LisoUseInAIKyrios marked this conversation as resolved.
Show resolved Hide resolved
*
* @param logger optional logger, to record the time to resolve each fingerprint.
*/
internal fun Iterable<MethodFingerprint>.resolveUsingLookupMap(context: BytecodeContext, logger : Logger = NopLogger) {
if (allMethods.isEmpty()) throw PatchResultError("lookup map not initialized")

for (fingerprint in this) {
var time = System.currentTimeMillis()
LisoUseInAIKyrios marked this conversation as resolved.
Show resolved Hide resolved
fingerprint.resolveUsingLookupMap(context, logger)
time = System.currentTimeMillis() - time
if (time > 20) logger.trace("${fingerprint.name} resolved in $time ms")
}
}

/**
* Resolve a list of [MethodFingerprint] against a list of [ClassDef].
*
Expand All @@ -54,6 +202,30 @@ abstract class MethodFingerprint(
break@classes // if the resolution succeeded, continue with the next fingerprint
}

/**
* Resolve using map built in [createMethodLookupMap]
*/
internal fun MethodFingerprint.resolveUsingLookupMap(context: BytecodeContext, logger : Logger = NopLogger): Boolean {
LisoUseInAIKyrios marked this conversation as resolved.
Show resolved Hide resolved
fun MethodFingerprint.resolveUsingClassMethod(classMethods: Iterable<ClassAndMethod>): Boolean {
for (classAndMethod in classMethods) {
if (resolve(context, classAndMethod.method, classAndMethod.classDef)) {
return true
}
}
return false
}

var methodsWithStrings = appMethodsWithSameStrings()
if (methodsWithStrings != null) {
if (resolveUsingClassMethod(methodsWithStrings)) return true
logger.trace("$name: could not quickly resolve using declared strings (verify first string is an exact match)")
}

// No String declared, or none matched (partial matches are allowed).
// Use signature matching.
return resolveUsingClassMethod(appMethodsWithSameSignature())
}

/**
* Resolve a [MethodFingerprint] against a [ClassDef].
*
Expand Down