refactor(YouTube - Litho): Use a simpler hook that does not require using a thread local (#5281)

This commit is contained in:
LisoUseInAIKyrios 2025-06-28 11:51:29 +04:00 committed by GitHub
parent 9131c50f1b
commit ed617094ea
3 changed files with 88 additions and 141 deletions

View file

@ -7,26 +7,18 @@ import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
internal val componentContextParserFingerprint = fingerprint {
strings(
"TreeNode result must be set.",
// String is a partial match and changed slightly in 20.03+
"it was removed due to duplicate converter bindings."
)
strings("Number of bits must be positive")
}
/**
* Resolves to the class found in [componentContextParserFingerprint].
* When patching 19.16 this fingerprint matches the same method as [componentContextParserFingerprint].
*/
internal val componentContextSubParserFingerprint = fingerprint {
internal val componentCreateFingerprint = fingerprint {
strings(
"Number of bits must be positive"
"Element missing correct type extension",
"Element missing type"
)
}
internal val lithoFilterFingerprint = fingerprint {
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
returns("V")
custom { _, classDef ->
classDef.endsWith("/LithoFilterPatch;")
}
@ -58,7 +50,7 @@ internal val lithoThreadExecutorFingerprint = fingerprint {
parameters("I", "I", "I")
custom { method, classDef ->
classDef.superclass == "Ljava/util/concurrent/ThreadPoolExecutor;" &&
method.containsLiteralInstruction(1L) // 1L = default thread timeout.
method.containsLiteralInstruction(1L) // 1L = default thread timeout.
}
}

View file

@ -9,13 +9,13 @@ import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.playservice.is_19_17_or_greater
import app.revanced.patches.youtube.misc.playservice.is_19_25_or_greater
import app.revanced.patches.youtube.misc.playservice.is_20_05_or_greater
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
import app.revanced.patches.youtube.shared.conversionContextFingerprintToString
import app.revanced.util.addInstructionsAtControlFlowLabel
import app.revanced.util.findFreeRegister
import app.revanced.util.findInstructionIndicesReversedOrThrow
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
@ -24,7 +24,6 @@ import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
lateinit var addLithoFilter: (String) -> Unit
private set
@ -66,17 +65,11 @@ val lithoFilterPatch = bytecodePatch(
* }
* }
*
* class ComponentContextParser {
* public Component parseComponent() {
* class CreateComponentClass {
* public Component createComponent() {
* ...
*
* // Checks if the component should be filtered.
* // Sets a thread local with the filtering result.
* extensionClass.filter(identifier, pathBuilder); // Inserted by this patch.
*
* ...
*
* if (extensionClass.shouldFilter()) { // Inserted by this patch.
* if (extensionClass.shouldFilter(identifier, path)) { // Inserted by this patch.
* return emptyComponent;
* }
* return originalUnpatchedComponent; // Original code.
@ -116,95 +109,68 @@ val lithoFilterPatch = bytecodePatch(
// Allow the method to run to completion, and override the
// return value with an empty component if it should be filtered.
// It is important to allow the original code to always run to completion,
// otherwise memory leaks and poor app performance can occur.
//
// The extension filtering result needs to be saved off somewhere, but cannot
// save to a class field since the target class is called by multiple threads.
// It would be great if there was a way to change the register count of the
// method implementation and save the result to a high register to later use
// in the method, but there is no simple way to do that.
// Instead save the extension filter result to a thread local and check the
// filtering result at each method return index.
// String field for the litho identifier.
componentContextParserFingerprint.method.apply {
val conversionContextClass = conversionContextFingerprintToString.originalClassDef
// otherwise high memory usage and poor app performance can occur.
val conversionContextIdentifierField = componentContextSubParserFingerprint.match(
componentContextParserFingerprint.originalClassDef
).let {
// Identifier field is loaded just before the string declaration.
val index = it.method.indexOfFirstInstructionReversedOrThrow(
it.stringMatches!!.first().index
) {
val reference = getReference<FieldReference>()
reference?.definingClass == conversionContextClass.type
&& reference.type == "Ljava/lang/String;"
}
it.method.getInstruction<ReferenceInstruction>(index).getReference<FieldReference>()
// Find the identifier/path fields of the conversion context.
val conversionContextIdentifierField = componentContextParserFingerprint.let {
// Identifier field is loaded just before the string declaration.
val index = it.method.indexOfFirstInstructionReversedOrThrow(
it.stringMatches!!.first().index
) {
val reference = getReference<FieldReference>()
reference?.definingClass == conversionContextFingerprintToString.originalClassDef.type
&& reference.type == "Ljava/lang/String;"
}
// StringBuilder field for the litho path.
val conversionContextPathBuilderField = conversionContextClass.fields
.single { field -> field.type == "Ljava/lang/StringBuilder;" }
it.method.getInstruction<ReferenceInstruction>(index).getReference<FieldReference>()!!
}
val conversionContextResultIndex = indexOfFirstInstructionOrThrow {
val reference = getReference<MethodReference>()
reference?.returnType == conversionContextClass.type
} + 1
val conversionContextPathBuilderField = conversionContextFingerprintToString.originalClassDef
.fields.single { field -> field.type == "Ljava/lang/StringBuilder;" }
val conversionContextResultRegister = getInstruction<OneRegisterInstruction>(
conversionContextResultIndex
).registerA
val identifierRegister = findFreeRegister(
conversionContextResultIndex, conversionContextResultRegister
)
val stringBuilderRegister = findFreeRegister(
conversionContextResultIndex, conversionContextResultRegister, identifierRegister
)
// Check if the component should be filtered, and save the result to a thread local.
addInstructionsAtControlFlowLabel(
conversionContextResultIndex + 1,
"""
iget-object v$identifierRegister, v$conversionContextResultRegister, $conversionContextIdentifierField
iget-object v$stringBuilderRegister, v$conversionContextResultRegister, $conversionContextPathBuilderField
invoke-static { v$identifierRegister, v$stringBuilderRegister }, $EXTENSION_CLASS_DESCRIPTOR->filter(Ljava/lang/String;Ljava/lang/StringBuilder;)V
"""
)
// Get the only static method in the class.
val builderMethodDescriptor = emptyComponentFingerprint.classDef.methods.single {
// Find class and methods to create an empty component.
val builderMethodDescriptor = emptyComponentFingerprint.classDef.methods.single {
// The only static method in the class.
method -> AccessFlags.STATIC.isSet(method.accessFlags)
}
// Only one field.
val emptyComponentField = classBy { classDef ->
classDef.type == builderMethodDescriptor.returnType
}!!.immutableClass.fields.single()
}
val emptyComponentField = classBy {
// Only one field that matches.
it.type == builderMethodDescriptor.returnType
}!!.immutableClass.fields.single()
// Check at each return value if the component is filtered,
// and return an empty component if filtering is needed.
findInstructionIndicesReversedOrThrow(Opcode.RETURN_OBJECT).forEach { returnIndex ->
val freeRegister = findFreeRegister(returnIndex)
addInstructionsAtControlFlowLabel(
returnIndex,
"""
invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->shouldFilter()Z
move-result v$freeRegister
if-eqz v$freeRegister, :unfiltered
move-object/from16 v$freeRegister, p1
invoke-static { v$freeRegister }, $builderMethodDescriptor
move-result-object v$freeRegister
iget-object v$freeRegister, v$freeRegister, $emptyComponentField
return-object v$freeRegister
:unfiltered
nop
"""
)
componentCreateFingerprint.method.apply {
val insertIndex = if (is_19_17_or_greater) {
indexOfFirstInstructionOrThrow(Opcode.RETURN_OBJECT)
} else {
// 19.16 clobbers p2 so must check at start of the method and not at the return index.
0
}
val freeRegister = findFreeRegister(insertIndex)
val identifierRegister = findFreeRegister(insertIndex, freeRegister)
val pathRegister = findFreeRegister(insertIndex, freeRegister, identifierRegister)
addInstructionsAtControlFlowLabel(
insertIndex,
"""
move-object/from16 v$freeRegister, p2
iget-object v$identifierRegister, v$freeRegister, $conversionContextIdentifierField
iget-object v$pathRegister, v$freeRegister, $conversionContextPathBuilderField
invoke-static { v$identifierRegister, v$pathRegister }, $EXTENSION_CLASS_DESCRIPTOR->shouldFilter(Ljava/lang/String;Ljava/lang/StringBuilder;)Z
move-result v$freeRegister
if-eqz v$freeRegister, :unfiltered
# Return an empty component
move-object/from16 v$freeRegister, p1
invoke-static { v$freeRegister }, $builderMethodDescriptor
move-result-object v$freeRegister
iget-object v$freeRegister, v$freeRegister, $emptyComponentField
return-object v$freeRegister
:unfiltered
nop
"""
)
}
// endregion