From e5d1566a1632c81507d95ff87d3ea292aa5bdb9d Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 22 Mar 2026 07:15:39 +0100 Subject: [PATCH 01/34] Implement libxposed 101 --- xposed/build.gradle.kts | 1 + xposed/libxposed | 2 +- .../org/matrix/vector/api/utils/DexParser.kt | 342 ++++++++++++++++++ .../vector/impl/VectorRemotePreferences.kt | 100 +++++ .../matrix/vector/impl/hooks/BaseInvoker.kt | 157 ++++++++ .../vector/impl/hooks/LegacyHookProcessor.kt | 54 +++ .../matrix/vector/impl/hooks/VectorChain.kt | 113 ++++++ .../matrix/vector/impl/hooks/VectorContext.kt | 191 ++++++++++ .../vector/impl/hooks/VectorNativeHooker.kt | 123 +++++++ .../vector/impl/utils/VectorDexParser.kt | 190 ++++------ .../vector/nativebridge/DexParserBridge.kt | 2 +- 11 files changed, 1144 insertions(+), 131 deletions(-) create mode 100644 xposed/src/main/kotlin/org/matrix/vector/api/utils/DexParser.kt create mode 100644 xposed/src/main/kotlin/org/matrix/vector/impl/VectorRemotePreferences.kt create mode 100644 xposed/src/main/kotlin/org/matrix/vector/impl/hooks/BaseInvoker.kt create mode 100644 xposed/src/main/kotlin/org/matrix/vector/impl/hooks/LegacyHookProcessor.kt create mode 100644 xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt create mode 100644 xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorContext.kt create mode 100644 xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt diff --git a/xposed/build.gradle.kts b/xposed/build.gradle.kts index 2d153bb91..db219aa13 100644 --- a/xposed/build.gradle.kts +++ b/xposed/build.gradle.kts @@ -15,6 +15,7 @@ android { } dependencies { + implementation(projects.services.daemonService) compileOnly(libs.androidx.annotation) compileOnly(projects.hiddenapi.stubs) } diff --git a/xposed/libxposed b/xposed/libxposed index 88cc0781e..eb63ec2c4 160000 --- a/xposed/libxposed +++ b/xposed/libxposed @@ -1 +1 @@ -Subproject commit 88cc0781e1138477c6b2a6570a0a27692f91f175 +Subproject commit eb63ec2c49a34a9c3cd08a7eee2b5cd8332f46d2 diff --git a/xposed/src/main/kotlin/org/matrix/vector/api/utils/DexParser.kt b/xposed/src/main/kotlin/org/matrix/vector/api/utils/DexParser.kt new file mode 100644 index 000000000..4f8cf6dcf --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/api/utils/DexParser.kt @@ -0,0 +1,342 @@ +package org.matrix.vector.api.utils + +import java.io.Closeable + +/** Xposed interface for parsing dex files. */ +@Suppress("unused") +interface DexParser : Closeable { + + companion object { + /** The constant NO_INDEX. */ + const val NO_INDEX: Int = -1 // 0xffffffff + } + + /** The interface Array. */ + interface Array { + /** + * Get values value [ ]. + * + * @return the value [ ] + */ + val values: kotlin.Array + } + + /** The interface Annotation. */ + interface Annotation { + /** + * Gets visibility. + * + * @return the visibility + */ + val visibility: Int + + /** + * Gets type. + * + * @return the type + */ + val type: TypeId + + /** + * Get elements element [ ]. + * + * @return the element [ ] + */ + val elements: kotlin.Array + } + + /** The interface Value. */ + interface Value { + /** + * Get value byte [ ]. + * + * @return the byte [ ] + */ + val value: ByteArray? + + /** + * Gets value type. + * + * @return the value type + */ + val valueType: Int + } + + /** The interface Element. */ + interface Element : Value { + /** + * Gets name. + * + * @return the name + */ + val name: StringId + } + + /** The interface Id. */ + interface Id : Comparable { + /** + * Gets id. + * + * @return the id + */ + val id: Int + } + + /** The interface Type id. */ + interface TypeId : Id { + /** + * Gets descriptor. + * + * @return the descriptor + */ + val descriptor: StringId + } + + /** The interface String id. */ + interface StringId : Id { + /** + * Gets string. + * + * @return the string + */ + val string: String + } + + /** The interface Field id. */ + interface FieldId : Id { + /** + * Gets type. + * + * @return the type + */ + val type: TypeId + + /** + * Gets declaring class. + * + * @return the declaring class + */ + val declaringClass: TypeId + + /** + * Gets name. + * + * @return the name + */ + val name: StringId + } + + /** The interface Method id. */ + interface MethodId : Id { + /** + * Gets declaring class. + * + * @return the declaring class + */ + val declaringClass: TypeId + + /** + * Gets prototype. + * + * @return the prototype + */ + val prototype: ProtoId + + /** + * Gets name. + * + * @return the name + */ + val name: StringId + } + + /** The interface Proto id. */ + interface ProtoId : Id { + /** + * Gets shorty. + * + * @return the shorty + */ + val shorty: StringId + + /** + * Gets return type. + * + * @return the return type + */ + val returnType: TypeId + + /** + * Get parameters type id [ ]. + * + * @return the type id [ ] + */ + val parameters: kotlin.Array? + } + + /** + * Get string id string id [ ]. + * + * @return the string id [ ] + */ + val stringId: kotlin.Array + + /** + * Get type id type id [ ]. + * + * @return the type id[ ] + */ + val typeId: kotlin.Array + + /** + * Get field id field id [ ]. + * + * @return the field id [ ] + */ + val fieldId: kotlin.Array + + /** + * Get method id method id [ ]. + * + * @return the method id [ ] + */ + val methodId: kotlin.Array + + /** + * Get proto id proto id [ ]. + * + * @return the proto id [ ] + */ + val protoId: kotlin.Array + + /** + * Get annotations annotation [ ]. + * + * @return the annotation [ ] + */ + val annotations: kotlin.Array + + /** + * Get arrays array [ ]. + * + * @return the array [ ] + */ + val arrays: kotlin.Array + + /** The interface Early stop visitor. */ + interface EarlyStopVisitor { + /** + * Stop boolean. + * + * @return the boolean + */ + fun stop(): Boolean + } + + /** The interface Member visitor. */ + interface MemberVisitor : EarlyStopVisitor + + /** The interface Class visitor. */ + interface ClassVisitor : EarlyStopVisitor { + /** + * Visit member visitor. + * + * @param clazz the clazz + * @param accessFlags the access flags + * @param superClass the super class + * @param interfaces the interfaces + * @param sourceFile the source file + * @param staticFields the static fields + * @param staticFieldsAccessFlags the static fields access flags + * @param instanceFields the instance fields + * @param instanceFieldsAccessFlags the instance fields access flags + * @param directMethods the direct methods + * @param directMethodsAccessFlags the direct methods access flags + * @param virtualMethods the virtual methods + * @param virtualMethodsAccessFlags the virtual methods access flags + * @param annotations the annotations + * @return the member visitor + */ + fun visit( + clazz: Int, + accessFlags: Int, + superClass: Int, + interfaces: IntArray, + sourceFile: Int, + staticFields: IntArray, + staticFieldsAccessFlags: IntArray, + instanceFields: IntArray, + instanceFieldsAccessFlags: IntArray, + directMethods: IntArray, + directMethodsAccessFlags: IntArray, + virtualMethods: IntArray, + virtualMethodsAccessFlags: IntArray, + annotations: IntArray, + ): MemberVisitor? + } + + /** The interface Field visitor. */ + interface FieldVisitor : MemberVisitor { + /** + * Visit. + * + * @param field the field + * @param accessFlags the access flags + * @param annotations the annotations + */ + fun visit(field: Int, accessFlags: Int, annotations: IntArray) + } + + /** The interface Method visitor. */ + interface MethodVisitor : MemberVisitor { + /** + * Visit method body visitor. + * + * @param method the method + * @param accessFlags the access flags + * @param hasBody the has body + * @param annotations the annotations + * @param parameterAnnotations the parameter annotations + * @return the method body visitor + */ + fun visit( + method: Int, + accessFlags: Int, + hasBody: Boolean, + annotations: IntArray, + parameterAnnotations: IntArray, + ): MethodBodyVisitor? + } + + /** The interface Method body visitor. */ + interface MethodBodyVisitor { + /** + * Visit. + * + * @param method the method + * @param accessFlags the access flags + * @param referredStrings the referred strings + * @param invokedMethods the invoked methods + * @param accessedFields the accessed fields + * @param assignedFields the assigned fields + * @param opcodes the opcodes + */ + fun visit( + method: Int, + accessFlags: Int, + referredStrings: IntArray, + invokedMethods: IntArray, + accessedFields: IntArray, + assignedFields: IntArray, + opcodes: ByteArray, + ) + } + + /** + * Visit defined classes. + * + * @param visitor the visitor + * @throws IllegalStateException the illegal state exception + */ + @Throws(IllegalStateException::class) fun visitDefinedClasses(visitor: ClassVisitor) +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/VectorRemotePreferences.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorRemotePreferences.kt new file mode 100644 index 000000000..c4bb8cada --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorRemotePreferences.kt @@ -0,0 +1,100 @@ +package org.matrix.vector.impl + +import android.content.SharedPreferences +import android.os.Bundle +import android.os.RemoteException +import android.util.ArraySet +import java.util.TreeMap +import java.util.concurrent.ConcurrentHashMap +import org.lsposed.lspd.service.ILSPInjectedModuleService +import org.lsposed.lspd.service.IRemotePreferenceCallback + +@Suppress("UNCHECKED_CAST") +internal class VectorRemotePreferences(service: ILSPInjectedModuleService, group: String) : + SharedPreferences { + + private val map = ConcurrentHashMap() + private val listeners = mutableSetOf() + + private val callback = + object : IRemotePreferenceCallback.Stub() { + @Synchronized + override fun onUpdate(bundle: Bundle) { + val changes = ArraySet() + + if (bundle.containsKey("delete")) { + val deletes = bundle.getSerializable("delete") as? Set + if (deletes != null) { + changes.addAll(deletes) + for (key in deletes) { + map.remove(key) + } + } + } + + if (bundle.containsKey("put")) { + val puts = bundle.getSerializable("put") as? Map + if (puts != null) { + map.putAll(puts) + changes.addAll(puts.keys) + } + } + + synchronized(listeners) { + for (key in changes) { + listeners.forEach { listener -> + listener.onSharedPreferenceChanged(this@VectorRemotePreferences, key) + } + } + } + } + } + + init { + try { + val output = service.requestRemotePreferences(group, callback) + if (output.containsKey("map")) { + val initialMap = output.getSerializable("map") as? Map + if (initialMap != null) { + map.putAll(initialMap) + } + } + } catch (e: RemoteException) { + // Initial load failed. Error handling is deferred to the caller's context if necessary. + } + } + + override fun getAll(): Map = TreeMap(map) + + override fun getString(key: String, defValue: String?): String? = + map[key] as? String ?: defValue + + override fun getStringSet(key: String, defValues: Set?): Set? = + map[key] as? Set ?: defValues + + override fun getInt(key: String, defValue: Int): Int = map[key] as? Int ?: defValue + + override fun getLong(key: String, defValue: Long): Long = map[key] as? Long ?: defValue + + override fun getFloat(key: String, defValue: Float): Float = map[key] as? Float ?: defValue + + override fun getBoolean(key: String, defValue: Boolean): Boolean = + map[key] as? Boolean ?: defValue + + override fun contains(key: String): Boolean = map.containsKey(key) + + override fun edit(): SharedPreferences.Editor = + throw UnsupportedOperationException("Read only implementation") + + override fun registerOnSharedPreferenceChangeListener( + listener: SharedPreferences.OnSharedPreferenceChangeListener + ) { + synchronized(listeners) { listeners.add(listener) } + } + + override fun unregisterOnSharedPreferenceChangeListener( + listener: SharedPreferences.OnSharedPreferenceChangeListener + ) { + synchronized(listeners) { listeners.remove(listener) } + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/BaseInvoker.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/BaseInvoker.kt new file mode 100644 index 000000000..fe7fcae27 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/BaseInvoker.kt @@ -0,0 +1,157 @@ +package org.matrix.vector.impl.hooks + +import io.github.libxposed.api.XposedInterface.CtorInvoker +import io.github.libxposed.api.XposedInterface.Invoker +import java.lang.reflect.Constructor +import java.lang.reflect.Executable +import java.lang.reflect.Method +import org.matrix.vector.nativebridge.HookBridge + +/** + * Base implementation of the Invoker system. Handles the resolution of [Invoker.Type] to determine + * whether to execute the original method directly or to construct a partial interceptor chain. + */ +internal abstract class BaseInvoker, U : Executable>( + protected val executable: U +) : Invoker { + + protected var type: Invoker.Type = Invoker.Type.Chain.FULL + + @Suppress("UNCHECKED_CAST") + override fun setType(type: Invoker.Type): T { + this.type = type + return this as T + } + + /** Resolves the current [type] and executes the underlying method. */ + protected fun proceedInvocation(thisObject: Any?, args: Array): Any? { + return when (val currentType = type) { + is Invoker.Type.Origin -> { + HookBridge.invokeOriginalMethod(executable, thisObject, *args) + } + is Invoker.Type.Chain -> { + val snapshots = + HookBridge.callbackSnapshot(VectorHookRecord::class.java, executable) + + @Suppress("UNCHECKED_CAST") + val allModernHooks = snapshots[0] as Array + val legacyHooks = snapshots[1] + + // Filter hooks to respect the maxPriority requested by the module + val filteredHooks = + allModernHooks.filter { it.priority <= currentType.maxPriority }.toTypedArray() + + val terminal: (Any?, Array) -> Any? = { tObj, tArgs -> + val processor = LegacySupport.processor + if (legacyHooks.isNotEmpty() && processor != null) { + processor.process(executable, tObj, tArgs, legacyHooks) { + HookBridge.invokeOriginalMethod(executable, tObj, *tArgs) + } + } else { + HookBridge.invokeOriginalMethod(executable, tObj, *tArgs) + } + } + + val chain = + VectorChain(executable, thisObject, arrayOf(*args), filteredHooks, 0, terminal) + chain.proceed() + } + } + } + + /** Helper to generate the JNI shorty for non-virtual special invocations. */ + protected fun getExecutableShorty(): CharArray { + val parameterTypes = executable.parameterTypes + val shorty = CharArray(parameterTypes.size + 1) + shorty[0] = getTypeShorty(if (executable is Method) executable.returnType else Void.TYPE) + for (i in 1..shorty.lastIndex) { + shorty[i] = getTypeShorty(parameterTypes[i - 1]) + } + return shorty + } + + private fun getTypeShorty(type: Class<*>): Char = + when (type) { + Int::class.javaPrimitiveType -> 'I' + Long::class.javaPrimitiveType -> 'J' + Float::class.javaPrimitiveType -> 'F' + Double::class.javaPrimitiveType -> 'D' + Boolean::class.javaPrimitiveType -> 'Z' + Byte::class.javaPrimitiveType -> 'B' + Char::class.javaPrimitiveType -> 'C' + Short::class.javaPrimitiveType -> 'S' + Void.TYPE -> 'V' + else -> 'L' + } +} + +/** Invoker implementation specifically for [Method] types. */ +internal class VectorMethodInvoker(method: Method) : + BaseInvoker(method) { + + override fun invoke(thisObject: Any?, vararg args: Any?): Any? { + return proceedInvocation(thisObject, args) + } + + override fun invokeSpecial(thisObject: Any, vararg args: Any?): Any? { + return HookBridge.invokeSpecialMethod( + executable, + getExecutableShorty(), + executable.declaringClass, + thisObject, + *args, + ) + } +} + +/** + * Invoker implementation specifically for [Constructor] types. Extends capabilities to allocate and + * initialize objects safely. + */ +internal class VectorCtorInvoker(constructor: Constructor) : + BaseInvoker, Constructor>(constructor), CtorInvoker { + + override fun invoke(thisObject: Any?, vararg args: Any?): Any? { + // Invoking a constructor as a method returns nothing (void/null) + proceedInvocation(thisObject, args) + return null + } + + override fun invokeSpecial(thisObject: Any, vararg args: Any?): Any? { + HookBridge.invokeSpecialMethod( + executable, + getExecutableShorty(), + executable.declaringClass, + thisObject, + *args, + ) + return null + } + + @Suppress("UNCHECKED_CAST") + override fun newInstance(vararg args: Any?): T { + // 1. Allocate memory without invoking + val obj = HookBridge.allocateObject(executable.declaringClass) as T + // 2. Drive the invocation (origin or chain) utilizing the allocated object + proceedInvocation(obj, args) + return obj + } + + @Suppress("UNCHECKED_CAST") + override fun newInstanceSpecial(subClass: Class, vararg args: Any?): U { + if (!executable.declaringClass.isAssignableFrom(subClass)) { + throw IllegalArgumentException( + "$subClass is not inherited from ${executable.declaringClass}" + ) + } + val obj = HookBridge.allocateObject(subClass) as U + HookBridge.invokeSpecialMethod( + executable, + getExecutableShorty(), + executable.declaringClass, + obj, + *args, + ) + return obj + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/LegacyHookProcessor.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/LegacyHookProcessor.kt new file mode 100644 index 000000000..ec87f617e --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/LegacyHookProcessor.kt @@ -0,0 +1,54 @@ +package org.matrix.vector.impl.hooks + +import java.lang.reflect.Executable + +fun interface OriginalInvoker { + fun invoke(): Any? +} + +interface LegacyHookProcessor { + /** Executes legacy hooks wrapped around the original method invocation. */ + fun process( + executable: Executable, + thisObject: Any?, + args: Array, + legacyHooks: Array, + invokeOriginal: OriginalInvoker, + ): Any? +} + +/** Registry to hold the injected LegacyHookProcessor. */ +object LegacySupport { + var processor: LegacyHookProcessor? = null +} + +/** + * Adapter for backward compatibility with [XposedBridge.LegacyApiSupport]. Contains state mutations + * strictly for legacy module support. + */ +class VectorLegacyCallback( + val method: T, + var thisObject: Any?, + var args: Array, +) { + var result: Any? = null + private set + + var throwable: Throwable? = null + private set + + var isSkipped = false + private set + + fun setResult(res: Any?) { + result = res + throwable = null + isSkipped = true + } + + fun setThrowable(t: Throwable?) { + result = null + throwable = t + isSkipped = true + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt new file mode 100644 index 000000000..d2820f144 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt @@ -0,0 +1,113 @@ +package org.matrix.vector.impl.hooks + +import io.github.libxposed.api.XposedInterface +import io.github.libxposed.api.XposedInterface.Chain +import io.github.libxposed.api.XposedInterface.ExceptionMode +import java.lang.reflect.Executable +import org.lsposed.lspd.util.Utils.Log + +/** Represents a registered hook configuration, stored natively by [HookBridge]. */ +data class VectorHookRecord( + val hooker: XposedInterface.Hooker, + val priority: Int, + val exceptionMode: ExceptionMode, +) + +/** + * Core interceptor chain engine. Manages recursive hook execution and enforces [ExceptionMode] + * protections. + */ +class VectorChain( + private val executable: Executable, + private val thisObj: Any?, + private val args: Array, + private val hooks: Array, + private val index: Int, + private val terminal: (thisObj: Any?, args: Array) -> Any?, +) : Chain { + + // Tracks if this specific chain node has forwarded execution downstream + internal var proceedCalled: Boolean = false + private set + + // Stores the actual result/exception from the rest of the chain/original method + internal var downstreamResult: Any? = null + internal var downstreamThrowable: Throwable? = null + + override fun getExecutable(): Executable = executable + + override fun getThisObject(): Any? = thisObj + + override fun getArgs(): List = args.toList() + + override fun getArg(index: Int): Any? = args[index] + + override fun proceed(): Any? = proceedWith(thisObj ?: Any(), args) + + override fun proceed(args: Array): Any? = proceedWith(thisObj ?: Any(), args) + + override fun proceedWith(thisObject: Any): Any? = proceedWith(thisObject, args) + + override fun proceedWith(thisObject: Any, args: Array): Any? { + if (proceedCalled) { + throw IllegalStateException("chain.proceed() can only be called once per interceptor.") + } + proceedCalled = true + + // Reached the end of the modern hooks; trigger the original executable (and legacy hooks) + if (index >= hooks.size) { + return executeDownstream { terminal(thisObject, args) } + } + + val record = hooks[index] + val nextChain = VectorChain(executable, thisObject, args, hooks, index + 1, terminal) + + return try { + executeDownstream { record.hooker.intercept(nextChain) } + } catch (t: Throwable) { + handleInterceptorException(t, record, nextChain, thisObject, args) + } + } + + /** + * Executes the block and caches the downstream state so parent chains can recover it if the + * current interceptor crashes during post-processing. + */ + private inline fun executeDownstream(block: () -> Any?): Any? { + return try { + val result = block() + downstreamResult = result + result + } catch (t: Throwable) { + downstreamThrowable = t + throw t + } + } + + /** Handles exceptions thrown by module interceptors. */ + private fun handleInterceptorException( + t: Throwable, + record: VectorHookRecord, + nextChain: VectorChain, + currentThis: Any, + currentArgs: Array, + ): Any? { + if (record.exceptionMode == ExceptionMode.PASSTHROUGH) { + throw t + } + + // DEFAULT or PROTECTIVE mode: log the crash and attempt to rescue the execution. + Log.e("VectorChain", "Hooker threw exception: ${record.hooker.javaClass.name}", t) + + if (!nextChain.proceedCalled) { + // Crash occurred BEFORE proceed(). Skip this hooker entirely and drive the chain + // manually. + return nextChain.proceedWith(currentThis, currentArgs) + } else { + // Crash occurred AFTER proceed(). Swallow the module's crash and return the real + // downstream state. + nextChain.downstreamThrowable?.let { throw it } + return nextChain.downstreamResult + } + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorContext.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorContext.kt new file mode 100644 index 000000000..3c9cedc42 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorContext.kt @@ -0,0 +1,191 @@ +package org.matrix.vector.impl + +import android.content.SharedPreferences +import android.content.pm.ApplicationInfo +import android.os.ParcelFileDescriptor +import io.github.libxposed.api.XposedInterface +import io.github.libxposed.api.XposedModule +import io.github.libxposed.api.XposedModuleInterface.* +import java.io.FileNotFoundException +import java.lang.reflect.Constructor +import java.lang.reflect.Executable +import java.lang.reflect.Method +import java.util.concurrent.ConcurrentHashMap +import org.lsposed.lspd.service.ILSPInjectedModuleService +import org.lsposed.lspd.util.Utils.Log +import org.matrix.vector.impl.hooks.VectorCtorInvoker +import org.matrix.vector.impl.hooks.VectorHookBuilder +import org.matrix.vector.impl.hooks.VectorMethodInvoker +import org.matrix.vector.nativebridge.HookBridge + +/** + * Main framework context implementation. Provides modules with capabilities to hook executables, + * request invokers, and interact with the system. + */ +class VectorContext( + private val packageName: String, + private val applicationInfo: ApplicationInfo, + private val service: ILSPInjectedModuleService, +) : XposedInterface { + + private val remotePrefs = ConcurrentHashMap() + + override fun getFrameworkName(): String = "Vector" + + override fun getFrameworkVersion(): String = "1.0.0" + + override fun getFrameworkVersionCode(): Long = 1L + + override fun getFrameworkProperties(): Long { + var props = 0L + // TODO: Dynamically verify capabilities through `service` if required + props = props or XposedInterface.PROP_CAP_REMOTE + props = props or XposedInterface.PROP_CAP_SYSTEM + return props + } + + override fun hook(origin: Executable): XposedInterface.HookBuilder { + return VectorHookBuilder(origin) + } + + override fun hookClassInitializer(origin: Class<*>): XposedInterface.HookBuilder { + val clinit = + HookBridge.getStaticInitializer(origin) + ?: throw IllegalArgumentException("Class ${origin.name} has no static initializer") + return VectorHookBuilder(clinit) + } + + override fun deoptimize(executable: Executable): Boolean { + return HookBridge.deoptimizeMethod(executable) + } + + override fun getInvoker(method: Method): XposedInterface.Invoker<*, Method> { + return VectorMethodInvoker(method) + } + + override fun getInvoker(constructor: Constructor): XposedInterface.CtorInvoker { + return VectorCtorInvoker(constructor) + } + + override fun getModuleApplicationInfo(): ApplicationInfo = applicationInfo + + override fun getRemotePreferences(name: String): SharedPreferences { + return remotePrefs.getOrPut(name) { VectorRemotePreferences(service, name) } + } + + override fun listRemoteFiles(): Array { + return service.remoteFileList + } + + override fun openRemoteFile(name: String): ParcelFileDescriptor { + return service.openRemoteFile(name) + ?: throw FileNotFoundException("Cannot open remote file: $name") + } + + override fun log(priority: Int, tag: String?, msg: String) { + log(priority, tag, msg, null) + } + + override fun log(priority: Int, tag: String?, msg: String, tr: Throwable?) { + val finalTag = tag ?: "VectorContext" + val prefix = if (packageName.isNotEmpty()) "$packageName: " else "" + val fullMsg = buildString { + append(prefix).append(msg) + if (tr != null) { + append("\n").append(android.util.Log.getStackTraceString(tr)) + } + } + Log.println(priority, finalTag, fullMsg) + } +} + +/** Manages the dispatching of modern lifecycle events to loaded modules. */ +object VectorLifecycleManager { + + val activeModules: MutableSet = ConcurrentHashMap.newKeySet() + + fun dispatchPackageLoaded( + packageName: String, + appInfo: ApplicationInfo, + isFirst: Boolean, + defaultClassLoader: ClassLoader, + ) { + val param = + object : PackageLoadedParam { + override fun getPackageName(): String = packageName + + override fun getApplicationInfo(): ApplicationInfo = appInfo + + override fun isFirstPackage(): Boolean = isFirst + + override fun getDefaultClassLoader(): ClassLoader = defaultClassLoader + } + + activeModules.forEach { module -> + runCatching { module.onPackageLoaded(param) } + .onFailure { + Log.e( + "VectorLifecycle", + "Error in onPackageLoaded for ${module.moduleApplicationInfo.packageName}", + it, + ) + } + } + } + + fun dispatchPackageReady( + packageName: String, + appInfo: ApplicationInfo, + isFirst: Boolean, + defaultClassLoader: ClassLoader, + classLoader: ClassLoader, + appComponentFactory: Any, // Abstracted for API compatibility + ) { + val param = + object : PackageReadyParam { + override fun getPackageName(): String = packageName + + override fun getApplicationInfo(): ApplicationInfo = appInfo + + override fun isFirstPackage(): Boolean = isFirst + + override fun getDefaultClassLoader(): ClassLoader = defaultClassLoader + + override fun getClassLoader(): ClassLoader = classLoader + + @Suppress("NewApi") + override fun getAppComponentFactory(): android.app.AppComponentFactory { + return appComponentFactory as android.app.AppComponentFactory + } + } + + activeModules.forEach { module -> + runCatching { module.onPackageReady(param) } + .onFailure { + Log.e( + "VectorLifecycle", + "Error in onPackageReady for ${module.moduleApplicationInfo.packageName}", + it, + ) + } + } + } + + fun dispatchSystemServerStarting(classLoader: ClassLoader) { + val param = + object : SystemServerStartingParam { + override fun getClassLoader(): ClassLoader = classLoader + } + + activeModules.forEach { module -> + runCatching { module.onSystemServerStarting(param) } + .onFailure { + Log.e( + "VectorLifecycle", + "Error in onSystemServerStarting for ${module.moduleApplicationInfo.packageName}", + it, + ) + } + } + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt new file mode 100644 index 000000000..0f818d71e --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt @@ -0,0 +1,123 @@ +package org.matrix.vector.impl.hooks + +import io.github.libxposed.api.XposedInterface +import io.github.libxposed.api.XposedInterface.ExceptionMode +import io.github.libxposed.api.XposedInterface.HookBuilder +import io.github.libxposed.api.XposedInterface.HookHandle +import io.github.libxposed.api.XposedInterface.Hooker +import io.github.libxposed.api.error.HookFailedError +import java.lang.reflect.Executable +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method +import java.lang.reflect.Modifier +import org.matrix.vector.nativebridge.HookBridge + +/** Builder for configuring and registering hooks. */ +class VectorHookBuilder(private val origin: Executable) : HookBuilder { + + private var priority = XposedInterface.PRIORITY_DEFAULT + private var exceptionMode = ExceptionMode.DEFAULT + + override fun setPriority(priority: Int): HookBuilder = apply { this.priority = priority } + + override fun setExceptionMode(mode: ExceptionMode): HookBuilder = apply { + this.exceptionMode = mode + } + + override fun intercept(hooker: Hooker): HookHandle { + if (Modifier.isAbstract(origin.modifiers)) { + throw IllegalArgumentException("Cannot hook abstract methods: $origin") + } else if (origin.declaringClass.classLoader == VectorHookBuilder::class.java.classLoader) { + throw IllegalArgumentException("Do not allow hooking inner methods") + } else if ( + origin is Method && + origin.declaringClass == Method::class.java && + origin.name == "invoke" + ) { + throw IllegalArgumentException("Cannot hook Method.invoke") + } + + val record = VectorHookRecord(hooker, priority, exceptionMode) + + // Register natively. HookBridge now stores VectorHookRecord instead of HookerCallback. + if ( + !HookBridge.hookMethod(true, origin, VectorNativeHooker::class.java, priority, record) + ) { + throw HookFailedError("Cannot hook $origin") + } + + return object : HookHandle { + override fun getExecutable(): Executable = origin + + override fun unhook() { + HookBridge.unhookMethod(true, origin, record) + } + } + } +} + +/** + * The native callback entrypoint. Instantiated natively by [HookBridge] when a hooked method is + * hit. + */ +class VectorNativeHooker(private val method: T) { + + private val isStatic = Modifier.isStatic(method.modifiers) + private val returnType = if (method is Method) method.returnType else null + + /** Invoked by C++ via JNI. */ + fun callback(args: Array): Any? { + val thisObject = if (isStatic) null else args[0] + val actualArgs = if (isStatic) args else args.sliceArray(1 until args.size) + + // Retrieve the hook snapshots + val snapshots = HookBridge.callbackSnapshot(VectorHookRecord::class.java, method) + + @Suppress("UNCHECKED_CAST") val modernHooks = snapshots[0] as Array + val legacyHooks = snapshots[1] + + // Fast path: No hooks active + if (modernHooks.isEmpty() && legacyHooks.isEmpty()) { + return invokeOriginalSafely(thisObject, actualArgs) + } + + val terminal: (Any?, Array) -> Any? = { tObj, tArgs -> + val processor = LegacySupport.processor + + if (legacyHooks.isNotEmpty() && processor != null) { + processor.process(method, tObj, tArgs, legacyHooks) { + invokeOriginalSafely(tObj, tArgs) + } + } else { + invokeOriginalSafely(tObj, tArgs) + } + } + // Start the modern API 101 Interceptor Chain + val rootChain = VectorChain(method, thisObject, actualArgs, modernHooks, 0, terminal) + + val result = rootChain.proceed() + + // Type safety validation before returning to C++ + if ( + returnType != null && + !returnType.isPrimitive && + result != null && + !HookBridge.instanceOf(result, returnType) + ) { + throw ClassCastException( + "Return value's type from hook callback does not match the hooked method" + ) + } + + return result + } + + /** Safely invokes the original method, unwrapping InvocationTargetExceptions. */ + private fun invokeOriginalSafely(tObj: Any?, tArgs: Array): Any? { + return try { + HookBridge.invokeOriginalMethod(method, tObj, *tArgs) + } catch (ite: InvocationTargetException) { + throw ite.cause ?: ite + } + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorDexParser.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorDexParser.kt index ac42e64ad..d95335061 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorDexParser.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorDexParser.kt @@ -1,9 +1,9 @@ package org.matrix.vector.impl.utils -import io.github.libxposed.api.utils.DexParser -import io.github.libxposed.api.utils.DexParser.* import java.io.IOException import java.nio.ByteBuffer +import org.matrix.vector.api.utils.DexParser +import org.matrix.vector.api.utils.DexParser.* import org.matrix.vector.nativebridge.DexParserBridge /** @@ -19,15 +19,15 @@ class VectorDexParser(buffer: ByteBuffer, includeAnnotations: Boolean) : DexPars private var cookie: Long = 0 private val data: ByteBuffer - // Internal storage for parsed DEX structures. - // We use private properties and explicit getter methods as requested. - private val internalStrings: Array - private val internalTypeIds: Array - private val internalProtoIds: Array - private val internalFieldIds: Array - private val internalMethodIds: Array - private val internalAnnotations: Array - private val internalArrays: Array + // Properties directly override the interface properties. + // Kotlin automatically generates private backing fields for these. + override val stringId: Array + override val typeId: Array + override val protoId: Array + override val fieldId: Array + override val methodId: Array + override val annotations: Array + override val arrays: Array init { // Ensure the buffer is Direct and accessible by native code @@ -45,7 +45,7 @@ class VectorDexParser(buffer: ByteBuffer, includeAnnotations: Boolean) : DexPars try { val args = LongArray(2) - args[1] = if (includeAnnotations) 1 else 0 + args[1] = if (includeAnnotations) 1L else 0L // Call Native Bridge // Returns a raw Object[] containing headers and pools @@ -54,20 +54,20 @@ class VectorDexParser(buffer: ByteBuffer, includeAnnotations: Boolean) : DexPars // --- Parse Strings (Index 0) --- val rawStrings = out[0] as Array - internalStrings = Array(rawStrings.size) { i -> VectorStringId(i, rawStrings[i]) } + stringId = Array(rawStrings.size) { i -> VectorStringId(i, rawStrings[i]) } // --- Parse Type IDs (Index 1) --- val rawTypeIds = out[1] as IntArray - internalTypeIds = Array(rawTypeIds.size) { i -> VectorTypeId(i, rawTypeIds[i]) } + typeId = Array(rawTypeIds.size) { i -> VectorTypeId(i, rawTypeIds[i]) } // --- Parse Proto IDs (Index 2) --- val rawProtoIds = out[2] as Array - internalProtoIds = Array(rawProtoIds.size) { i -> VectorProtoId(i, rawProtoIds[i]) } + protoId = Array(rawProtoIds.size) { i -> VectorProtoId(i, rawProtoIds[i]) } // --- Parse Field IDs (Index 3) --- val rawFieldIds = out[3] as IntArray // Each field is represented by 3 integers (class_idx, type_idx, name_idx) - internalFieldIds = + fieldId = Array(rawFieldIds.size / 3) { i -> VectorFieldId( i, @@ -80,7 +80,7 @@ class VectorDexParser(buffer: ByteBuffer, includeAnnotations: Boolean) : DexPars // --- Parse Method IDs (Index 4) --- val rawMethodIds = out[4] as IntArray // Each method is represented by 3 integers (class_idx, proto_idx, name_idx) - internalMethodIds = + methodId = Array(rawMethodIds.size / 3) { i -> VectorMethodId( i, @@ -94,10 +94,10 @@ class VectorDexParser(buffer: ByteBuffer, includeAnnotations: Boolean) : DexPars val rawAnnotationMetadata = out[5] as? IntArray val rawAnnotationValues = out[6] as? Array - internalAnnotations = + annotations = if (rawAnnotationMetadata != null && rawAnnotationValues != null) { Array(rawAnnotationMetadata.size / 2) { i -> - // Metadata: [visibility, type_idx] + // Metadata:[visibility, type_idx] // Values: [name_indices[], values[]] val elementsMeta = rawAnnotationValues[2 * i] as IntArray val elementsData = rawAnnotationValues[2 * i + 1] as Array @@ -114,7 +114,7 @@ class VectorDexParser(buffer: ByteBuffer, includeAnnotations: Boolean) : DexPars // --- Parse Arrays (Index 7) --- val rawArrays = out[7] as? Array - internalArrays = + arrays = if (rawArrays != null) { Array(rawArrays.size / 2) { i -> val types = rawArrays[2 * i] as IntArray @@ -133,24 +133,10 @@ class VectorDexParser(buffer: ByteBuffer, includeAnnotations: Boolean) : DexPars override fun close() { if (cookie != 0L) { DexParserBridge.closeDex(cookie) - cookie = 0 + cookie = 0L } } - override fun getStringId(): Array = internalStrings - - override fun getTypeId(): Array = internalTypeIds - - override fun getFieldId(): Array = internalFieldIds - - override fun getMethodId(): Array = internalMethodIds - - override fun getProtoId(): Array = internalProtoIds - - override fun getAnnotations(): Array = internalAnnotations - - override fun getArrays(): Array = internalArrays - override fun visitDefinedClasses(visitor: ClassVisitor) { if (cookie == 0L) { throw IllegalStateException("Closed") @@ -177,140 +163,86 @@ class VectorDexParser(buffer: ByteBuffer, includeAnnotations: Boolean) : DexPars } /** Base implementation for all Dex IDs. */ - private open class VectorId>(private val id: Int) : Id { - override fun getId(): Int = id - + private open class VectorId>(override val id: Int) : Id { override fun compareTo(other: Self): Int = id - other.id } - private inner class VectorStringId(id: Int, private val string: String) : - VectorId(id), StringId { - override fun getString(): String = string - } + private inner class VectorStringId(id: Int, override val string: String) : + VectorId(id), StringId private inner class VectorTypeId(id: Int, descriptorIdx: Int) : VectorId(id), TypeId { - private val descriptor: StringId = internalStrings[descriptorIdx] - - override fun getDescriptor(): StringId = descriptor + override val descriptor: StringId = stringId[descriptorIdx] } private inner class VectorProtoId(id: Int, protoData: IntArray) : VectorId(id), ProtoId { - private val shorty: StringId = internalStrings[protoData[0]] - private val returnType: TypeId = internalTypeIds[protoData[1]] - private val parameters: Array? - - init { + override val shorty: StringId = stringId[protoData[0]] + override val returnType: TypeId = typeId[protoData[1]] + override val parameters: Array? = if (protoData.size > 2) { - // protoData format: [shorty_idx, return_type_idx, param1_idx, param2_idx...] - parameters = Array(protoData.size - 2) { i -> internalTypeIds[protoData[i + 2]] } + // protoData format:[shorty_idx, return_type_idx, param1_idx, param2_idx...] + Array(protoData.size - 2) { i -> typeId[protoData[i + 2]] } } else { - parameters = null + null } - } - - override fun getShorty(): StringId = shorty - - override fun getReturnType(): TypeId = returnType - - override fun getParameters(): Array? = parameters } private inner class VectorFieldId(id: Int, classIdx: Int, typeIdx: Int, nameIdx: Int) : VectorId(id), FieldId { - private val declaringClass: TypeId = internalTypeIds[classIdx] - private val type: TypeId = internalTypeIds[typeIdx] - private val name: StringId = internalStrings[nameIdx] - - override fun getType(): TypeId = type - - override fun getDeclaringClass(): TypeId = declaringClass - - override fun getName(): StringId = name + override val declaringClass: TypeId = typeId[classIdx] + override val type: TypeId = typeId[typeIdx] + override val name: StringId = stringId[nameIdx] } private inner class VectorMethodId(id: Int, classIdx: Int, protoIdx: Int, nameIdx: Int) : VectorId(id), MethodId { - private val declaringClass: TypeId = internalTypeIds[classIdx] - private val prototype: ProtoId = internalProtoIds[protoIdx] - private val name: StringId = internalStrings[nameIdx] - - override fun getDeclaringClass(): TypeId = declaringClass - - override fun getPrototype(): ProtoId = prototype - - override fun getName(): StringId = name + override val declaringClass: TypeId = typeId[classIdx] + override val prototype: ProtoId = protoId[protoIdx] + override val name: StringId = stringId[nameIdx] } private class VectorArray(elementsTypes: IntArray, valuesData: Array) : DexParser.Array { - - private val values: Array - - init { - values = - Array(valuesData.size) { i -> - VectorValue(elementsTypes[i], valuesData[i] as? ByteBuffer) - } - } - - override fun getValues(): Array = values + override val values: Array = + Array(valuesData.size) { i -> + VectorValue(elementsTypes[i], valuesData[i] as? ByteBuffer) + } } private inner class VectorAnnotation( - private val visibility: Int, + override val visibility: Int, typeIdx: Int, elementNameIndices: IntArray, elementValues: Array, ) : DexParser.Annotation { - private val type: TypeId = internalTypeIds[typeIdx] - private val elements: Array - - init { - elements = - Array(elementValues.size) { i -> - // Flattened structure from JNI: names are at 2*i, types at 2*i+1 - VectorElement( - elementNameIndices[i * 2], - elementNameIndices[i * 2 + 1], // valueType - elementValues[i] as? ByteBuffer, - ) - } - } - - override fun getVisibility(): Int = visibility - - override fun getType(): TypeId = type - - override fun getElements(): Array = elements + override val type: TypeId = typeId[typeIdx] + override val elements: Array = + Array(elementValues.size) { i -> + // Flattened structure from JNI: names are at 2*i, types at 2*i+1 + VectorElement( + elementNameIndices[i * 2], + elementNameIndices[i * 2 + 1], // valueType + elementValues[i] as? ByteBuffer, + ) + } } - private open class VectorValue(private val valueType: Int, buffer: ByteBuffer?) : Value { - - private val value: ByteArray? + private open class VectorValue(override val valueType: Int, buffer: ByteBuffer?) : Value { - init { - if (buffer != null) { - value = ByteArray(buffer.remaining()) - buffer.get(value) - } else { - value = null + override val value: ByteArray? = + buffer?.let { + val bytes = ByteArray(it.remaining()) + it.get(bytes) + bytes } - } - - override fun getValue(): ByteArray? = value - - override fun getValueType(): Int = valueType } - private inner class VectorElement(nameIdx: Int, valueType: Int, value: ByteBuffer?) : - VectorValue(valueType, value), Element { - - private val name: StringId = internalStrings[nameIdx] + private inner class VectorElement(nameIdx: Int, valueType: Int, buffer: ByteBuffer?) : + VectorValue(valueType, buffer), Element { - override fun getName(): StringId = name + override val name: StringId = stringId[nameIdx] } } diff --git a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/DexParserBridge.kt b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/DexParserBridge.kt index 35d46ea5b..17f8a6ab2 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/DexParserBridge.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/DexParserBridge.kt @@ -1,10 +1,10 @@ package org.matrix.vector.nativebridge import dalvik.annotation.optimization.FastNative -import io.github.libxposed.api.utils.DexParser import java.io.IOException import java.lang.reflect.Method import java.nio.ByteBuffer +import org.matrix.vector.api.utils.DexParser object DexParserBridge { @JvmStatic From 18ca366a9376e56c0eff086b1758fef540888a32 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 22 Mar 2026 19:05:34 +0100 Subject: [PATCH 02/34] Refactor Java layer implementations into Kotin --- xposed/build.gradle.kts | 5 + .../vector/impl/VectorRemotePreferences.kt | 18 +- .../matrix/vector/impl/core/VectorDeopter.kt | 51 ++++++ .../vector/impl/core/VectorInlinedCallers.kt | 158 +++++++++++++++++ .../vector/impl/core/VectorServiceClient.kt | 61 +++++++ .../matrix/vector/impl/core/VectorStartup.kt | 83 +++++++++ .../matrix/vector/impl/di/VectorBootstrap.kt | 98 +++++++++++ .../vector/impl/hookers/AppAttachHooker.kt | 23 +++ .../vector/impl/hookers/CrashDumpHooker.kt | 20 +++ .../vector/impl/hookers/DexTrustHooker.kt | 31 ++++ .../vector/impl/hookers/LoadedApkHookers.kt | 121 +++++++++++++ .../impl/hookers/SystemServerHookers.kt | 60 +++++++ .../matrix/vector/impl/hooks/BaseInvoker.kt | 11 +- .../vector/impl/hooks/LegacyHookProcessor.kt | 54 ------ .../vector/impl/hooks/VectorNativeHooker.kt | 8 +- .../vector/impl/utils/VectorMetaDataReader.kt | 112 ++++++++++++ .../impl/utils/VectorModuleClassLoader.kt | 161 ++++++++++++++++++ .../impl/utils/VectorURLStreamHandler.kt | 113 ++++++++++++ .../matrix/vector/nativebridge/HookBridge.kt | 2 +- 19 files changed, 1123 insertions(+), 67 deletions(-) create mode 100644 xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorDeopter.kt create mode 100644 xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorInlinedCallers.kt create mode 100644 xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorServiceClient.kt create mode 100644 xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorStartup.kt create mode 100644 xposed/src/main/kotlin/org/matrix/vector/impl/di/VectorBootstrap.kt create mode 100644 xposed/src/main/kotlin/org/matrix/vector/impl/hookers/AppAttachHooker.kt create mode 100644 xposed/src/main/kotlin/org/matrix/vector/impl/hookers/CrashDumpHooker.kt create mode 100644 xposed/src/main/kotlin/org/matrix/vector/impl/hookers/DexTrustHooker.kt create mode 100644 xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt create mode 100644 xposed/src/main/kotlin/org/matrix/vector/impl/hookers/SystemServerHookers.kt delete mode 100644 xposed/src/main/kotlin/org/matrix/vector/impl/hooks/LegacyHookProcessor.kt create mode 100644 xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorMetaDataReader.kt create mode 100644 xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorModuleClassLoader.kt create mode 100644 xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorURLStreamHandler.kt diff --git a/xposed/build.gradle.kts b/xposed/build.gradle.kts index db219aa13..1bfc89544 100644 --- a/xposed/build.gradle.kts +++ b/xposed/build.gradle.kts @@ -16,6 +16,11 @@ android { dependencies { implementation(projects.services.daemonService) + implementation(projects.external.apache) + implementation(projects.external.axml) + implementation(projects.hiddenapi.bridge) + implementation(projects.services.daemonService) + implementation(projects.services.managerService) compileOnly(libs.androidx.annotation) compileOnly(projects.hiddenapi.stubs) } diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/VectorRemotePreferences.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorRemotePreferences.kt index c4bb8cada..60d209b7e 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/VectorRemotePreferences.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorRemotePreferences.kt @@ -9,6 +9,15 @@ import java.util.concurrent.ConcurrentHashMap import org.lsposed.lspd.service.ILSPInjectedModuleService import org.lsposed.lspd.service.IRemotePreferenceCallback +@Suppress("DEPRECATION", "UNCHECKED_CAST") +private inline fun Bundle.getSerializableCompat(key: String): T? { + return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { + getSerializable(key, java.io.Serializable::class.java) as? T + } else { + getSerializable(key) as? T + } +} + @Suppress("UNCHECKED_CAST") internal class VectorRemotePreferences(service: ILSPInjectedModuleService, group: String) : SharedPreferences { @@ -23,7 +32,8 @@ internal class VectorRemotePreferences(service: ILSPInjectedModuleService, group val changes = ArraySet() if (bundle.containsKey("delete")) { - val deletes = bundle.getSerializable("delete") as? Set + val deletes = + bundle.getSerializableCompat>("delete") as? Set if (deletes != null) { changes.addAll(deletes) for (key in deletes) { @@ -33,7 +43,8 @@ internal class VectorRemotePreferences(service: ILSPInjectedModuleService, group } if (bundle.containsKey("put")) { - val puts = bundle.getSerializable("put") as? Map + val puts = + bundle.getSerializableCompat>("put") as? Map if (puts != null) { map.putAll(puts) changes.addAll(puts.keys) @@ -54,7 +65,8 @@ internal class VectorRemotePreferences(service: ILSPInjectedModuleService, group try { val output = service.requestRemotePreferences(group, callback) if (output.containsKey("map")) { - val initialMap = output.getSerializable("map") as? Map + val initialMap = + output.getSerializableCompat>("map") as? Map if (initialMap != null) { map.putAll(initialMap) } diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorDeopter.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorDeopter.kt new file mode 100644 index 000000000..8f6fa90ba --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorDeopter.kt @@ -0,0 +1,51 @@ +package org.matrix.vector.impl.core + +import java.lang.reflect.Executable +import org.matrix.vector.nativebridge.HookBridge + +/** + * Engine responsible for scanning and deoptimizing prebuilt framework methods. Ensures that hooks + * placed on heavily inlined methods are respected by the ART runtime. + */ +object VectorDeopter { + + private fun deoptMethods(where: String, cl: ClassLoader?) { + val targets = VectorInlinedCallers.get(where) + if (targets.isEmpty()) return + + val searchClassLoader = cl ?: ClassLoader.getSystemClassLoader() + + for (target in targets) { + runCatching { + val clazz = Class.forName(target.className, false, searchClassLoader) + val executable: Executable = + if (target.isConstructor) { + clazz.getDeclaredConstructor(*target.params) + } else { + clazz.getDeclaredMethod(target.methodName, *target.params) + } + + // Allow access if restricted and pass to the native bridge + executable.isAccessible = true + HookBridge.deoptimizeMethod(executable) + } + .onFailure { + // Log resolution or deoptimization failure silently + } + } + } + + fun deoptBootMethods() { + deoptMethods(VectorInlinedCallers.KEY_BOOT_IMAGE, null) + } + + fun deoptResourceMethods(isMiui: Boolean) { + if (isMiui) { + deoptMethods(VectorInlinedCallers.KEY_BOOT_IMAGE_MIUI_RES, null) + } + } + + fun deoptSystemServerMethods(sysCL: ClassLoader) { + deoptMethods(VectorInlinedCallers.KEY_SYSTEM_SERVER, sysCL) + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorInlinedCallers.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorInlinedCallers.kt new file mode 100644 index 000000000..8b97f0bda --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorInlinedCallers.kt @@ -0,0 +1,158 @@ +package org.matrix.vector.impl.core + +import android.app.Instrumentation +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.res.AssetManager +import android.content.res.Configuration +import android.content.res.Resources +import android.util.DisplayMetrics +import android.util.TypedValue + +/** Defines a strongly-typed signature for an executable that requires deoptimization. */ +data class TargetExecutable( + val className: String, + val methodName: String, + val params: Array>, +) { + val isConstructor: Boolean + get() = methodName == "" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as TargetExecutable + if (className != other.className) return false + if (methodName != other.methodName) return false + return params.contentEquals(other.params) + } + + override fun hashCode(): Int { + var result = className.hashCode() + result = 31 * result + methodName.hashCode() + result = 31 * result + params.contentHashCode() + return result + } +} + +/** + * Provides a registry of methods known to inline target framework hooks. Deoptimizing these callers + * forces the ART runtime to fall back to intercept mode. + */ +object VectorInlinedCallers { + const val KEY_BOOT_IMAGE = "boot_image" + const val KEY_BOOT_IMAGE_MIUI_RES = "boot_image_miui_res" + const val KEY_SYSTEM_SERVER = "system_server" + + private val callers = mutableMapOf>() + + init { + callers[KEY_BOOT_IMAGE] = + listOf( + TargetExecutable( + "android.app.Instrumentation", + "newApplication", + arrayOf(ClassLoader::class.java, String::class.java, Context::class.java), + ), + TargetExecutable( + "android.app.Instrumentation", + "newApplication", + arrayOf(ClassLoader::class.java, Context::class.java), + ), + TargetExecutable( + "android.app.LoadedApk", + "makeApplicationInner", + arrayOf( + Boolean::class.javaPrimitiveType!!, + Instrumentation::class.java, + Boolean::class.javaPrimitiveType!!, + ), + ), + TargetExecutable( + "android.app.LoadedApk", + "makeApplicationInner", + arrayOf(Boolean::class.javaPrimitiveType!!, Instrumentation::class.java), + ), + TargetExecutable( + "android.app.LoadedApk", + "makeApplication", + arrayOf(Boolean::class.javaPrimitiveType!!, Instrumentation::class.java), + ), + TargetExecutable( + "android.app.ContextImpl", + "getSharedPreferencesPath", + arrayOf(String::class.java), + ), + ) + + callers[KEY_BOOT_IMAGE_MIUI_RES] = + listOf( + TargetExecutable( + "android.content.res.MiuiResources", + "init", + arrayOf(String::class.java), + ), + TargetExecutable( + "android.content.res.MiuiResources", + "updateMiuiImpl", + emptyArray(), + ), + // Simplified string-based resolution for unavailable classes + TargetExecutable( + "android.content.res.MiuiResources", + "loadOverlayValue", + arrayOf(TypedValue::class.java, Int::class.javaPrimitiveType!!), + ), + TargetExecutable( + "android.content.res.MiuiResources", + "getThemeString", + arrayOf(CharSequence::class.java), + ), + TargetExecutable( + "android.content.res.MiuiResources", + "", + arrayOf(ClassLoader::class.java), + ), + TargetExecutable("android.content.res.MiuiResources", "", emptyArray()), + TargetExecutable( + "android.content.res.MiuiResources", + "", + arrayOf( + AssetManager::class.java, + DisplayMetrics::class.java, + Configuration::class.java, + ), + ), + TargetExecutable( + "android.miui.ResourcesManager", + "initMiuiResource", + arrayOf(Resources::class.java, String::class.java), + ), + TargetExecutable( + "android.app.LoadedApk", + "getResources", + arrayOf(Resources::class.java), + ), + TargetExecutable( + "android.content.res.Resources", + "getSystem", + arrayOf(Resources::class.java), + ), + TargetExecutable( + "android.app.ApplicationPackageManager", + "getResourcesForApplication", + arrayOf(ApplicationInfo::class.java), + ), + TargetExecutable( + "android.app.ContextImpl", + "setResources", + arrayOf(Resources::class.java), + ), + ) + + callers[KEY_SYSTEM_SERVER] = emptyList() + callers["com.android.systemui"] = emptyList() + } + + fun get(where: String): List = callers[where] ?: emptyList() +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorServiceClient.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorServiceClient.kt new file mode 100644 index 000000000..a10e9520c --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorServiceClient.kt @@ -0,0 +1,61 @@ +package org.matrix.vector.impl.core + +import android.os.IBinder +import android.os.ParcelFileDescriptor +import org.lsposed.lspd.models.Module +import org.lsposed.lspd.service.ILSPApplicationService + +/** + * Singleton client for managing IPC communication with the injected manager service. Handles Binder + * death gracefully and ensures safe remote execution. + */ +object VectorServiceClient : ILSPApplicationService, IBinder.DeathRecipient { + + private var service: ILSPApplicationService? = null + var processName: String = "" + private set + + @Synchronized + fun init(appService: ILSPApplicationService?, niceName: String) { + val binder = appService?.asBinder() + if (service == null && binder != null) { + runCatching { + service = appService + processName = niceName + binder.linkToDeath(this, 0) + } + .onFailure { + // Log failure to link to death + } + } + } + + override fun isLogMuted(): Boolean { + return runCatching { service?.isLogMuted == true }.getOrDefault(false) + } + + override fun getLegacyModulesList(): List { + return runCatching { service?.legacyModulesList }.getOrNull() ?: emptyList() + } + + override fun getModulesList(): List { + return runCatching { service?.modulesList }.getOrNull() ?: emptyList() + } + + override fun getPrefsPath(packageName: String): String? { + return runCatching { service?.getPrefsPath(packageName) }.getOrNull() + } + + override fun requestInjectedManagerBinder(binder: List): ParcelFileDescriptor? { + return runCatching { service?.requestInjectedManagerBinder(binder) }.getOrNull() + } + + override fun asBinder(): IBinder? { + return service?.asBinder() + } + + override fun binderDied() { + service?.asBinder()?.unlinkToDeath(this, 0) + service = null + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorStartup.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorStartup.kt new file mode 100644 index 000000000..8e7eff646 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorStartup.kt @@ -0,0 +1,83 @@ +package org.matrix.vector.impl.core + +import android.os.IBinder +import dalvik.system.DexFile +import org.lsposed.lspd.service.ILSPApplicationService +import org.matrix.vector.impl.hookers.* +import org.matrix.vector.impl.hooks.VectorHookBuilder + +/** + * Modern framework initialization and bootstrap sequence. Deploys interceptors into the ART runtime + * and handles early process deoptimization. + */ +object VectorStartup { + + @JvmStatic + fun init( + isSystem: Boolean, + processName: String, + appDir: String, + service: ILSPApplicationService?, + ) { + VectorServiceClient.init(service, processName) + VectorDeopter.deoptBootMethods() + } + + @JvmStatic + fun bootstrap(isSystem: Boolean, systemServerStarted: Boolean) { + // 1. Crash Dump Interceptor + Thread::class + .java + .declaredMethods + .firstOrNull { it.name == "dispatchUncaughtException" } + ?.let { VectorHookBuilder(it).intercept(CrashDumpHooker) } + + // 2. Process-specific Interceptors + if (isSystem) { + val zygoteInitClass = Class.forName("com.android.internal.os.ZygoteInit") + zygoteInitClass.declaredMethods + .filter { it.name == "handleSystemServerProcess" } + .forEach { VectorHookBuilder(it).intercept(HandleSystemServerProcessHooker) } + } else { + DexFile::class + .java + .declaredMethods + .filter { + it.name == "openDexFile" || + it.name == "openInMemoryDexFile" || + it.name == "openInMemoryDexFiles" + } + .forEach { VectorHookBuilder(it).intercept(DexTrustHooker) } + } + + // 3. Application Load Interceptors + val loadedApkClass = Class.forName("android.app.LoadedApk") + loadedApkClass.declaredConstructors.forEach { + // Hook all constructors of LoadedApk to catch early instantiations securely + VectorHookBuilder(it).intercept(LoadedApkCtorHooker) + } + + loadedApkClass.declaredMethods + .filter { it.name == "createOrUpdateClassLoaderLocked" } + .forEach { VectorHookBuilder(it).intercept(LoadedApkCreateCLHooker) } + + // 4. ActivityThread Attachment Interceptor + val activityThreadClass = Class.forName("android.app.ActivityThread") + activityThreadClass.declaredMethods + .filter { it.name == "attach" } + .forEach { VectorHookBuilder(it).intercept(AppAttachHooker) } + + // 5. Late System Server Injection + if (systemServerStarted) { + val activityService: IBinder? = android.os.ServiceManager.getService("activity") + if (activityService != null) { + val classLoader = activityService.javaClass.classLoader + if (classLoader != null) { + // Manually trigger the routines that the hooks normally would + HandleSystemServerProcessHooker.initSystemServer(classLoader) + StartBootstrapServicesHooker.dispatchSystemServerLoaded(classLoader) + } + } + } + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/di/VectorBootstrap.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/di/VectorBootstrap.kt new file mode 100644 index 000000000..c63d26514 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/di/VectorBootstrap.kt @@ -0,0 +1,98 @@ +package org.matrix.vector.impl.di + +import android.content.pm.ApplicationInfo +import java.lang.reflect.Executable + +/** Data class representing package load information for the legacy bridge. */ +data class LegacyPackageInfo( + val packageName: String, + val processName: String, + val classLoader: ClassLoader, + val appInfo: ApplicationInfo, + val isFirstApplication: Boolean, +) + +/** + * Adapter for backward compatibility with [XposedBridge.LegacyApiSupport]. Contains state mutations + * strictly for legacy module support. + */ +class VectorLegacyCallback( + val method: T, + var thisObject: Any?, + var args: Array, +) { + var result: Any? = null + private set + + var throwable: Throwable? = null + private set + + var isSkipped = false + private set + + fun setResult(res: Any?) { + result = res + throwable = null + isSkipped = true + } + + fun setThrowable(t: Throwable?) { + result = null + throwable = t + isSkipped = true + } +} + +/** Functional interface for executing the original method within a legacy hook bypass. */ +fun interface OriginalInvoker { + fun invoke(): Any? +} + +/** + * The explicit contract that the `core` (legacy) module must fulfill. The modern framework will + * call these methods at the appropriate lifecycle moments. + */ +interface LegacyFrameworkDelegate { + /** Instructs the legacy bridge to load legacy modules. */ + fun loadModules(activityThread: Any) + + /** Dispatches a package load event to legacy XC_LoadPackage callbacks. */ + fun onPackageLoaded(info: LegacyPackageInfo) + + /** Dispatches the system server load event to legacy callbacks. */ + fun onSystemServerLoaded(classLoader: ClassLoader) + + /** Processes legacy hooks wrapped around the original method invocation. */ + fun processLegacyHook( + executable: Executable, + thisObject: Any?, + args: Array, + legacyHooks: Array, + invokeOriginal: OriginalInvoker, + ): Any? + + /** Checks if resource hooking is disabled by the legacy configuration. */ + val isResourceHookingDisabled: Boolean + + fun setPackageNameForResDir(packageName: String, resDir: String?) + + /** Checks if a legacy module is active for the given package name. */ + fun hasLegacyModule(packageName: String): Boolean +} + +/** The central registry for framework bootstrapping. */ +object VectorBootstrap { + @Volatile + var delegate: LegacyFrameworkDelegate? = null + private set + + fun init(frameworkDelegate: LegacyFrameworkDelegate) { + check(delegate == null) { "VectorBootstrap is already initialized!" } + delegate = frameworkDelegate + } + + /** Helper to safely execute operations requiring the legacy delegate. */ + inline fun withLegacy(block: (LegacyFrameworkDelegate) -> Unit) { + delegate?.let(block) + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/AppAttachHooker.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/AppAttachHooker.kt new file mode 100644 index 000000000..afd1fbe71 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/AppAttachHooker.kt @@ -0,0 +1,23 @@ +package org.matrix.vector.impl.hookers + +import io.github.libxposed.api.XposedInterface +import org.matrix.vector.impl.di.VectorBootstrap + +/** + * Intercepts the early ApplicationThread attachment phase. Triggers the legacy compatibility layer + * to load modules into the process. + */ +object AppAttachHooker : XposedInterface.Hooker { + override fun intercept(chain: XposedInterface.Chain): Any? { + // Execute the actual attach() method first + val result = chain.proceed() + + // Delegate legacy module loading via DI + val activityThread = chain.thisObject + if (activityThread != null) { + VectorBootstrap.withLegacy { delegate -> delegate.loadModules(activityThread) } + } + + return result + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/CrashDumpHooker.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/CrashDumpHooker.kt new file mode 100644 index 000000000..5316dc525 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/CrashDumpHooker.kt @@ -0,0 +1,20 @@ +package org.matrix.vector.impl.hookers + +import android.util.Log +import io.github.libxposed.api.XposedInterface + +/** + * Intercepts uncaught exceptions in the framework to provide diagnostic logging before the process + * completely terminates. + */ +object CrashDumpHooker : XposedInterface.Hooker { + override fun intercept(chain: XposedInterface.Chain): Any? { + try { + val throwable = chain.args.firstOrNull() as? Throwable + if (throwable != null) { + Log.e("Vector", "Crash unexpectedly", throwable) + } + } catch (ignored: Throwable) {} + return chain.proceed() + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/DexTrustHooker.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/DexTrustHooker.kt new file mode 100644 index 000000000..44ee159d3 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/DexTrustHooker.kt @@ -0,0 +1,31 @@ +package org.matrix.vector.impl.hookers + +import android.os.Build +import io.github.libxposed.api.XposedInterface +import org.matrix.vector.nativebridge.HookBridge + +/** + * Intercepts DEX file parsing to dynamically mark our framework's ClassLoader as trusted. Prevents + * ART from blocking reflective access by the hooking engine. + */ +object DexTrustHooker : XposedInterface.Hooker { + override fun intercept(chain: XposedInterface.Chain): Any? { + val result = chain.proceed() + + var classLoader = chain.args.filterIsInstance().firstOrNull() + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P && classLoader == null) { + classLoader = DexTrustHooker::class.java.classLoader + } + + while (classLoader != null) { + if (classLoader === DexTrustHooker::class.java.classLoader) { + // Inform the native bridge that this DEX cookie is safe + HookBridge.setTrusted(result) + break + } + classLoader = classLoader.parent + } + + return result + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt new file mode 100644 index 000000000..2b6eba6c8 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt @@ -0,0 +1,121 @@ +package org.matrix.vector.impl.hookers + +import android.content.pm.ApplicationInfo +import android.util.Log +import io.github.libxposed.api.XposedInterface +import java.util.concurrent.ConcurrentHashMap +import org.matrix.vector.impl.VectorLifecycleManager +import org.matrix.vector.impl.di.LegacyPackageInfo +import org.matrix.vector.impl.di.VectorBootstrap + +/** Safe reflection helper */ +private inline fun Any.getFieldValue(name: String): T? { + var clazz: Class<*>? = this.javaClass + while (clazz != null) { + try { + val field = clazz.getDeclaredField(name) + field.isAccessible = true + return field.get(this) as? T + } catch (ignored: NoSuchFieldException) { + clazz = clazz.superclass + } + } + return null +} + +/** Tracks and prepares Application instances when their LoadedApk is instantiated. */ +object LoadedApkCtorHooker : XposedInterface.Hooker { + val trackedApks = ConcurrentHashMap.newKeySet() + + override fun intercept(chain: XposedInterface.Chain): Any? { + val result = chain.proceed() + val loadedApk = chain.thisObject ?: return result + + val packageName = loadedApk.getFieldValue("mPackageName") ?: return result + + VectorBootstrap.withLegacy { delegate -> + if (!delegate.isResourceHookingDisabled) { + val resDir = loadedApk.getFieldValue("mResDir") + delegate.setPackageNameForResDir(packageName, resDir) + } + } + + // Avoid OnePlus custom opt crashing + if ( + Log.getStackTraceString(Throwable()) + .contains("android.app.ActivityThread\$ApplicationThread.schedulePreload") + ) { + return result + } + + trackedApks.add(loadedApk) + return result + } +} + +/** Triggers package loading events immediately after the Application ClassLoader is created. */ +object LoadedApkCreateCLHooker : XposedInterface.Hooker { + override fun intercept(chain: XposedInterface.Chain): Any? { + val result = chain.proceed() + val loadedApk = chain.thisObject ?: return result + + if ( + chain.args.firstOrNull() != null || !LoadedApkCtorHooker.trackedApks.contains(loadedApk) + ) { + return result + } + + try { + val activityThreadClass = Class.forName("android.app.ActivityThread") + val currentPkgMethod = activityThreadClass.getDeclaredMethod("currentPackageName") + val currentProcMethod = activityThreadClass.getDeclaredMethod("currentProcessName") + + var packageName = currentPkgMethod.invoke(null) as? String + var processName = currentProcMethod.invoke(null) as? String + val apkPackageName = loadedApk.getFieldValue("mPackageName") ?: return result + + val isFirstPackage = + packageName != null && processName != null && packageName == apkPackageName + if (!isFirstPackage) { + packageName = apkPackageName + processName = currentPkgMethod.invoke(null) as? String ?: apkPackageName + } else if (packageName == "android") { + packageName = "system" + } + + val classLoader = loadedApk.getFieldValue("mClassLoader") ?: return result + val mIncludeCode = loadedApk.getFieldValue("mIncludeCode") ?: true + + if (!isFirstPackage && !mIncludeCode) return result + val appInfo = + loadedApk.getFieldValue("mApplicationInfo") ?: return result + + // 1. Dispatch Modern Lifecycle + val defaultClassLoader = + loadedApk.getFieldValue("mDefaultClassLoader") ?: classLoader + VectorLifecycleManager.dispatchPackageLoaded( + packageName, + appInfo, + isFirstPackage, + defaultClassLoader, + ) + + // 2. Dispatch Legacy Lifecycle + VectorBootstrap.withLegacy { delegate -> + delegate.onPackageLoaded( + LegacyPackageInfo( + packageName, + processName, + classLoader, + appInfo, + isFirstPackage, + ) + ) + } + } finally { + LoadedApkCtorHooker.trackedApks.remove(loadedApk) + } + + return result + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/SystemServerHookers.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/SystemServerHookers.kt new file mode 100644 index 000000000..97b275a1d --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/SystemServerHookers.kt @@ -0,0 +1,60 @@ +package org.matrix.vector.impl.hookers + +import io.github.libxposed.api.XposedInterface +import org.matrix.vector.impl.VectorLifecycleManager +import org.matrix.vector.impl.core.VectorDeopter +import org.matrix.vector.impl.di.VectorBootstrap +import org.matrix.vector.impl.hooks.VectorHookBuilder + +/** + * Intercepts the system server initialization process to deoptimize targets and attach bootstrap + * hookers dynamically. + */ +object HandleSystemServerProcessHooker : XposedInterface.Hooker { + + @Volatile + var systemServerCL: ClassLoader? = null + private set + + override fun intercept(chain: XposedInterface.Chain): Any? { + val result = chain.proceed() + val classLoader = Thread.currentThread().contextClassLoader + if (classLoader != null) { + initSystemServer(classLoader) + } + return result + } + + /** Performs system server initialization. */ + fun initSystemServer(classLoader: ClassLoader) { + if (systemServerCL != null) return // Ensure this only runs once + systemServerCL = classLoader + + // Deoptimize heavily inlined system server paths + VectorDeopter.deoptSystemServerMethods(classLoader) + + // Dynamically locate and hook the bootstrap service initializer + val sysServerClass = Class.forName("com.android.server.SystemServer", false, classLoader) + val startMethod = sysServerClass.getDeclaredMethod("startBootstrapServices") + + VectorHookBuilder(startMethod).intercept(StartBootstrapServicesHooker) + } +} + +/** Dispatches the core system server bootstrap event to the module engines. */ +object StartBootstrapServicesHooker : XposedInterface.Hooker { + + override fun intercept(chain: XposedInterface.Chain): Any? { + HandleSystemServerProcessHooker.systemServerCL?.let { dispatchSystemServerLoaded(it) } + return chain.proceed() + } + + /** Dispatches module loading events. */ + fun dispatchSystemServerLoaded(classLoader: ClassLoader) { + // 1. Dispatch to modern framework modules + VectorLifecycleManager.dispatchSystemServerStarting(classLoader) + + // 2. Dispatch to legacy framework modules + VectorBootstrap.withLegacy { delegate -> delegate.onSystemServerLoaded(classLoader) } + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/BaseInvoker.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/BaseInvoker.kt index fe7fcae27..fa5abe2dc 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/BaseInvoker.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/BaseInvoker.kt @@ -5,6 +5,7 @@ import io.github.libxposed.api.XposedInterface.Invoker import java.lang.reflect.Constructor import java.lang.reflect.Executable import java.lang.reflect.Method +import org.matrix.vector.impl.di.VectorBootstrap import org.matrix.vector.nativebridge.HookBridge /** @@ -42,9 +43,9 @@ internal abstract class BaseInvoker, U : Executable>( allModernHooks.filter { it.priority <= currentType.maxPriority }.toTypedArray() val terminal: (Any?, Array) -> Any? = { tObj, tArgs -> - val processor = LegacySupport.processor - if (legacyHooks.isNotEmpty() && processor != null) { - processor.process(executable, tObj, tArgs, legacyHooks) { + val delegate = VectorBootstrap.delegate + if (legacyHooks.isNotEmpty() && delegate != null) { + delegate.processLegacyHook(executable, tObj, tArgs, legacyHooks) { HookBridge.invokeOriginalMethod(executable, tObj, *tArgs) } } else { @@ -131,7 +132,7 @@ internal class VectorCtorInvoker(constructor: Constructor) : @Suppress("UNCHECKED_CAST") override fun newInstance(vararg args: Any?): T { // 1. Allocate memory without invoking - val obj = HookBridge.allocateObject(executable.declaringClass) as T + val obj = HookBridge.allocateObject(executable.declaringClass) // 2. Drive the invocation (origin or chain) utilizing the allocated object proceedInvocation(obj, args) return obj @@ -144,7 +145,7 @@ internal class VectorCtorInvoker(constructor: Constructor) : "$subClass is not inherited from ${executable.declaringClass}" ) } - val obj = HookBridge.allocateObject(subClass) as U + val obj = HookBridge.allocateObject(subClass) HookBridge.invokeSpecialMethod( executable, getExecutableShorty(), diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/LegacyHookProcessor.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/LegacyHookProcessor.kt deleted file mode 100644 index ec87f617e..000000000 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/LegacyHookProcessor.kt +++ /dev/null @@ -1,54 +0,0 @@ -package org.matrix.vector.impl.hooks - -import java.lang.reflect.Executable - -fun interface OriginalInvoker { - fun invoke(): Any? -} - -interface LegacyHookProcessor { - /** Executes legacy hooks wrapped around the original method invocation. */ - fun process( - executable: Executable, - thisObject: Any?, - args: Array, - legacyHooks: Array, - invokeOriginal: OriginalInvoker, - ): Any? -} - -/** Registry to hold the injected LegacyHookProcessor. */ -object LegacySupport { - var processor: LegacyHookProcessor? = null -} - -/** - * Adapter for backward compatibility with [XposedBridge.LegacyApiSupport]. Contains state mutations - * strictly for legacy module support. - */ -class VectorLegacyCallback( - val method: T, - var thisObject: Any?, - var args: Array, -) { - var result: Any? = null - private set - - var throwable: Throwable? = null - private set - - var isSkipped = false - private set - - fun setResult(res: Any?) { - result = res - throwable = null - isSkipped = true - } - - fun setThrowable(t: Throwable?) { - result = null - throwable = t - isSkipped = true - } -} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt index 0f818d71e..a018192b0 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt @@ -10,6 +10,7 @@ import java.lang.reflect.Executable import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method import java.lang.reflect.Modifier +import org.matrix.vector.impl.di.VectorBootstrap import org.matrix.vector.nativebridge.HookBridge /** Builder for configuring and registering hooks. */ @@ -82,10 +83,9 @@ class VectorNativeHooker(private val method: T) { } val terminal: (Any?, Array) -> Any? = { tObj, tArgs -> - val processor = LegacySupport.processor - - if (legacyHooks.isNotEmpty() && processor != null) { - processor.process(method, tObj, tArgs, legacyHooks) { + val delegate = VectorBootstrap.delegate + if (legacyHooks.isNotEmpty() && delegate != null) { + delegate.processLegacyHook(method, tObj, tArgs, legacyHooks) { invokeOriginalSafely(tObj, tArgs) } } else { diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorMetaDataReader.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorMetaDataReader.kt new file mode 100644 index 000000000..d4489a87d --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorMetaDataReader.kt @@ -0,0 +1,112 @@ +package org.matrix.vector.impl.util + +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.util.jar.JarFile +import pxb.android.axml.AxmlReader +import pxb.android.axml.AxmlVisitor +import pxb.android.axml.NodeVisitor + +/** + * Utility for parsing metadata configuration strictly from AndroidManifest.xml. Utilizes AXML + * reading to extract runtime constraints like minimum framework versions without requiring full APK + * extraction. + */ +class VectorMetaDataReader private constructor(apk: File) { + + val metaData = mutableMapOf() + + init { + JarFile(apk).use { zip -> + val entry = zip.getEntry("AndroidManifest.xml") + zip.getInputStream(entry).use { inputStream -> + val reader = AxmlReader(getBytesFromInputStream(inputStream)) + reader.accept( + object : AxmlVisitor() { + override fun child(ns: String?, name: String?): NodeVisitor { + val child = super.child(ns, name) + return ManifestTagVisitor(child) + } + } + ) + } + } + } + + private inner class ManifestTagVisitor(child: NodeVisitor?) : NodeVisitor(child) { + override fun child(ns: String?, name: String?): NodeVisitor { + val childNode = super.child(ns, name) + if (name == "application") { + return ApplicationTagVisitor(childNode) + } + return childNode + } + + private inner class ApplicationTagVisitor(child: NodeVisitor?) : NodeVisitor(child) { + override fun child(ns: String?, name: String?): NodeVisitor { + val childNode = super.child(ns, name) + if (name == "meta-data") { + return MetaDataVisitor(childNode) + } + return childNode + } + } + } + + private inner class MetaDataVisitor(child: NodeVisitor?) : NodeVisitor(child) { + var attrName: String? = null + var attrValue: Any? = null + + override fun attr(ns: String?, name: String?, resourceId: Int, type: Int, obj: Any?) { + if (type == 3 && name == "name") { + attrName = obj as? String + } + if (name == "value") { + attrValue = obj + } + super.attr(ns, name, resourceId, type, obj) + } + + override fun end() { + if (attrName != null && attrValue != null) { + metaData[attrName!!] = attrValue!! + } + super.end() + } + } + + companion object { + @JvmStatic + @Throws(IOException::class) + fun getMetaData(apk: File): Map { + return VectorMetaDataReader(apk).metaData + } + + @Throws(IOException::class) + private fun getBytesFromInputStream(inputStream: InputStream): ByteArray { + ByteArrayOutputStream().use { bos -> + val b = ByteArray(1024) + var n: Int + while (inputStream.read(b).also { n = it } != -1) { + bos.write(b, 0, n) + } + return bos.toByteArray() + } + } + + @JvmStatic + fun extractIntPart(str: String): Int { + var result = 0 + for (c in str) { + if (c in '0'..'9') { + result = result * 10 + (c - '0') + } else { + break + } + } + return result + } + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorModuleClassLoader.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorModuleClassLoader.kt new file mode 100644 index 000000000..767342c20 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorModuleClassLoader.kt @@ -0,0 +1,161 @@ +package org.matrix.vector.impl.util + +import android.os.Build +import android.os.SharedMemory +import android.system.ErrnoException +import android.system.Os +import android.system.OsConstants +import android.util.Log +import hidden.ByteBufferDexClassLoader +import java.io.File +import java.io.IOException +import java.net.URL +import java.nio.ByteBuffer +import java.util.Collections +import java.util.Enumeration +import java.util.jar.JarFile +import java.util.zip.ZipEntry + +/** + * Custom ClassLoader for module execution. Utilizes in-memory DEX loading to prevent the need to + * extract module code to the disk, enhancing both security and performance during the module + * lifecycle. + */ +class VectorModuleClassLoader +private constructor( + dexBuffers: Array, + librarySearchPath: String?, + parent: ClassLoader?, + private val apkPath: String, +) : + ByteBufferDexClassLoader( + dexBuffers, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) librarySearchPath else null, + parent, + ) { + private val nativeLibraryDirs = mutableListOf() + + init { + val searchPath = librarySearchPath ?: "" + nativeLibraryDirs.addAll(splitPaths(searchPath)) + nativeLibraryDirs.addAll(SYSTEM_NATIVE_LIBRARY_DIRS) + } + + @Throws(ClassNotFoundException::class) + override fun loadClass(name: String, resolve: Boolean): Class<*> { + findLoadedClass(name)?.let { + return it + } + + try { + return Any::class.java.classLoader!!.loadClass(name) + } catch (ignored: ClassNotFoundException) {} + + var fromSuper: ClassNotFoundException? = null + try { + return findClass(name) + } catch (ex: ClassNotFoundException) { + fromSuper = ex + } + + try { + return parent?.loadClass(name) ?: throw fromSuper + } catch (cnfe: ClassNotFoundException) { + throw fromSuper + } + } + + override fun findLibrary(libraryName: String): String? { + val fileName = System.mapLibraryName(libraryName) + for (file in nativeLibraryDirs) { + val path = file.path + if (path.contains(ZIP_SEPARATOR)) { + val split = path.split(ZIP_SEPARATOR, limit = 2) + try { + JarFile(split[0]).use { jarFile -> + val entryName = "${split[1]}/$fileName" + val entry = jarFile.getEntry(entryName) + if (entry != null && entry.method == ZipEntry.STORED) { + return "${split[0]}$ZIP_SEPARATOR$entryName" + } + } + } catch (e: IOException) { + Log.e(TAG, "Cannot open ${split[0]}", e) + } + } else if (file.isDirectory) { + val entryPath = File(file, fileName).path + try { + val fd = Os.open(entryPath, OsConstants.O_RDONLY, 0) + Os.close(fd) + return entryPath + } catch (ignored: ErrnoException) {} + } + } + return null + } + + override fun findResource(name: String): URL? { + return try { + val urlHandler = VectorURLStreamHandler(apkPath) + urlHandler.getEntryUrlOrNull(name) + } catch (e: IOException) { + null + } + } + + override fun findResources(name: String): Enumeration { + val url = findResource(name) + val result = if (url != null) listOf(url) else emptyList() + return Collections.enumeration(result) + } + + override fun toString(): String { + return "VectorModuleClassLoader[module=$apkPath, ${super.toString()}]" + } + + companion object { + private const val TAG = "VectorModuleClassLoader" + private const val ZIP_SEPARATOR = "!/" + private val SYSTEM_NATIVE_LIBRARY_DIRS = splitPaths(System.getProperty("java.library.path")) + + private fun splitPaths(searchPath: String?): List { + if (searchPath.isNullOrEmpty()) return emptyList() + return searchPath.split(File.pathSeparator).map { File(it) } + } + + /** + * Loads an APK into memory securely. Maps the provided [SharedMemory] instances into + * read-only [ByteBuffer]s and cleans up the memory file descriptors once the ClassLoader is + * fully instantiated. + */ + @JvmStatic + fun loadApk( + apk: String, + dexes: List, + librarySearchPath: String, + parent: ClassLoader?, + ): ClassLoader { + val dexBuffers = + dexes + .parallelStream() + .map { dex -> + try { + dex.mapReadOnly() + } catch (e: ErrnoException) { + Log.w(TAG, "Cannot map $dex", e) + null + } + } + .toList() + .filterNotNull() + .toTypedArray() + + val cl = VectorModuleClassLoader(dexBuffers, librarySearchPath, parent, apk) + + dexBuffers.toList().parallelStream().forEach { SharedMemory.unmap(it) } + dexes.parallelStream().forEach { it.close() } + + return cl + } + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorURLStreamHandler.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorURLStreamHandler.kt new file mode 100644 index 000000000..39ce09ad9 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorURLStreamHandler.kt @@ -0,0 +1,113 @@ +package org.matrix.vector.impl.util + +import android.net.Uri +import java.io.File +import java.io.FileNotFoundException +import java.io.FilterInputStream +import java.io.IOException +import java.io.InputStream +import java.net.JarURLConnection +import java.net.MalformedURLException +import java.net.URL +import java.net.URLConnection +import java.util.jar.JarFile +import java.util.zip.ZipEntry +import sun.net.www.protocol.jar.Handler + +/** + * Custom URLStreamHandler for loading resources directly from the module's APK. Optimized to handle + * internal JAR/ZIP entries without extracting them to the filesystem. + */ +internal class VectorURLStreamHandler(jarFileName: String) : Handler() { + private val fileUri: String = File(jarFileName).toURI().toString() + private val jarFile: JarFile = JarFile(jarFileName) + + fun getEntryUrlOrNull(entryName: String): URL? { + if (jarFile.getEntry(entryName) != null) { + return try { + // Using Android's Uri.encode instead of internal sun.net.www.ParseUtil + val encodedName = Uri.encode(entryName, "/") + URL("jar", null, -1, "$fileUri!/$encodedName", this) + } catch (e: MalformedURLException) { + throw RuntimeException("Invalid entry name: $entryName", e) + } + } + return null + } + + @Throws(IOException::class) + override fun openConnection(url: URL): URLConnection { + return ClassPathURLConnection(url) + } + + @Suppress("deprecation") + @Throws(IOException::class) + protected fun finalize() { + jarFile.close() + } + + private inner class ClassPathURLConnection(url: URL) : JarURLConnection(url) { + private var connectionJarFile: JarFile? = null + private var jarEntry: ZipEntry? = null + private var jarInput: InputStream? = null + private var isClosed = false + + init { + useCaches = false + } + + override fun setUseCaches(usecaches: Boolean) { + super.setUseCaches(false) + } + + @Throws(IOException::class) + override fun connect() { + check(!isClosed) { "JarURLConnection has been closed" } + if (!connected) { + jarEntry = + jarFile.getEntry(entryName) + ?: throw FileNotFoundException("URL=$url, zipfile=${jarFile.name}") + connected = true + } + } + + @Throws(IOException::class) + override fun getJarFile(): JarFile { + connect() + return connectionJarFile + ?: JarFile(this@VectorURLStreamHandler.jarFile.name).also { connectionJarFile = it } + } + + @Throws(IOException::class) + override fun getInputStream(): InputStream { + connect() + return jarInput + ?: object : + FilterInputStream( + this@VectorURLStreamHandler.jarFile.getInputStream(jarEntry) + ) { + @Throws(IOException::class) + override fun close() { + super.close() + isClosed = true + this@VectorURLStreamHandler.jarFile.close() + connectionJarFile?.close() + } + } + .also { jarInput = it } + } + + override fun getContentType(): String { + return guessContentTypeFromName(entryName) ?: "content/unknown" + } + + override fun getContentLength(): Int { + return try { + connect() + jarEntry?.size?.toInt() ?: -1 + } catch (ignored: IOException) { + -1 + } + } + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/HookBridge.kt b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/HookBridge.kt index 527fb8307..ea2a5bfd6 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/HookBridge.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/HookBridge.kt @@ -57,5 +57,5 @@ object HookBridge { @JvmStatic external fun callbackSnapshot(hooker_callback: Class<*>, method: Executable): Array> - @JvmStatic external fun getStaticInitializer(clazz: Class<*>): Method + @JvmStatic external fun getStaticInitializer(clazz: Class<*>): Method? } From ba75f92a279ac4f552a6172c6e8394d50774bfa5 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 22 Mar 2026 19:17:21 +0100 Subject: [PATCH 03/34] Delete old java implementations --- .../lspd/core/ApplicationServiceClient.java | 115 ------ .../java/org/lsposed/lspd/core/Startup.java | 105 ----- .../lspd/deopt/InlinedMethodCallers.java | 106 ----- .../lspd/deopt/PrebuiltMethodsDeopter.java | 81 ---- .../org/lsposed/lspd/hooker/AttachHooker.java | 13 - .../lsposed/lspd/hooker/CrashDumpHooker.java | 17 - .../HandleSystemServerProcessHooker.java | 58 --- .../lspd/hooker/LoadedApkCreateCLHooker.java | 204 ---------- .../lspd/hooker/LoadedApkCtorHooker.java | 76 ---- .../lspd/hooker/OpenDexFileHooker.java | 31 -- .../hooker/StartBootstrapServicesHooker.java | 63 --- .../org/lsposed/lspd/impl/LSPosedBridge.java | 292 ------------- .../org/lsposed/lspd/impl/LSPosedContext.java | 385 ------------------ .../org/lsposed/lspd/impl/LSPosedHelper.java | 46 --- .../lspd/impl/LSPosedHookCallback.java | 94 ----- .../lspd/impl/LSPosedRemotePreferences.java | 129 ------ .../lspd/util/ClassPathURLStreamHandler.java | 120 ------ .../java/org/lsposed/lspd/util/Hookers.java | 37 -- .../lspd/util/LspModuleClassLoader.java | 207 ---------- .../org/lsposed/lspd/util/MetaDataReader.java | 136 ------- .../main/java/org/matrix/vector/Startup.java | 33 ++ .../vector/legacy/LegacyDelegateImpl.java | 139 +++++++ 22 files changed, 172 insertions(+), 2315 deletions(-) delete mode 100644 core/src/main/java/org/lsposed/lspd/core/ApplicationServiceClient.java delete mode 100644 core/src/main/java/org/lsposed/lspd/core/Startup.java delete mode 100644 core/src/main/java/org/lsposed/lspd/deopt/InlinedMethodCallers.java delete mode 100644 core/src/main/java/org/lsposed/lspd/deopt/PrebuiltMethodsDeopter.java delete mode 100644 core/src/main/java/org/lsposed/lspd/hooker/AttachHooker.java delete mode 100644 core/src/main/java/org/lsposed/lspd/hooker/CrashDumpHooker.java delete mode 100644 core/src/main/java/org/lsposed/lspd/hooker/HandleSystemServerProcessHooker.java delete mode 100644 core/src/main/java/org/lsposed/lspd/hooker/LoadedApkCreateCLHooker.java delete mode 100644 core/src/main/java/org/lsposed/lspd/hooker/LoadedApkCtorHooker.java delete mode 100644 core/src/main/java/org/lsposed/lspd/hooker/OpenDexFileHooker.java delete mode 100644 core/src/main/java/org/lsposed/lspd/hooker/StartBootstrapServicesHooker.java delete mode 100644 core/src/main/java/org/lsposed/lspd/impl/LSPosedBridge.java delete mode 100644 core/src/main/java/org/lsposed/lspd/impl/LSPosedContext.java delete mode 100644 core/src/main/java/org/lsposed/lspd/impl/LSPosedHelper.java delete mode 100644 core/src/main/java/org/lsposed/lspd/impl/LSPosedHookCallback.java delete mode 100644 core/src/main/java/org/lsposed/lspd/impl/LSPosedRemotePreferences.java delete mode 100644 core/src/main/java/org/lsposed/lspd/util/ClassPathURLStreamHandler.java delete mode 100644 core/src/main/java/org/lsposed/lspd/util/Hookers.java delete mode 100644 core/src/main/java/org/lsposed/lspd/util/LspModuleClassLoader.java delete mode 100644 core/src/main/java/org/lsposed/lspd/util/MetaDataReader.java create mode 100644 core/src/main/java/org/matrix/vector/Startup.java create mode 100644 core/src/main/java/org/matrix/vector/legacy/LegacyDelegateImpl.java diff --git a/core/src/main/java/org/lsposed/lspd/core/ApplicationServiceClient.java b/core/src/main/java/org/lsposed/lspd/core/ApplicationServiceClient.java deleted file mode 100644 index 6eb01d0b3..000000000 --- a/core/src/main/java/org/lsposed/lspd/core/ApplicationServiceClient.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.core; - -import android.os.Bundle; -import android.os.IBinder; -import android.os.ParcelFileDescriptor; -import android.os.RemoteException; - -import androidx.annotation.NonNull; - -import org.lsposed.lspd.models.Module; -import org.lsposed.lspd.service.ILSPApplicationService; -import org.lsposed.lspd.util.Utils; - -import java.util.Collections; -import java.util.List; - -public class ApplicationServiceClient implements ILSPApplicationService, IBinder.DeathRecipient { - public static ApplicationServiceClient serviceClient = null; - - final ILSPApplicationService service; - - final String processName; - - private ApplicationServiceClient(@NonNull ILSPApplicationService service, @NonNull String processName) throws RemoteException { - this.service = service; - this.processName = processName; - this.service.asBinder().linkToDeath(this, 0); - } - - synchronized static void Init(ILSPApplicationService service, String niceName) { - var binder = service.asBinder(); - if (serviceClient == null && binder != null) { - try { - serviceClient = new ApplicationServiceClient(service, niceName); - } catch (RemoteException e) { - Utils.logE("link to death error: ", e); - } - } - } - - @Override - public boolean isLogMuted() { - try { - return service.isLogMuted(); - } catch (RemoteException | NullPointerException ignored) { - } - return false; - } - - @Override - public List getLegacyModulesList() { - try { - return service.getLegacyModulesList(); - } catch (RemoteException | NullPointerException ignored) { - } - return Collections.emptyList(); - } - - @Override - public List getModulesList() { - try { - return service.getModulesList(); - } catch (RemoteException | NullPointerException ignored) { - } - return Collections.emptyList(); - } - - @Override - public String getPrefsPath(String packageName) { - try { - return service.getPrefsPath(packageName); - } catch (RemoteException | NullPointerException ignored) { - } - return null; - } - - @Override - public ParcelFileDescriptor requestInjectedManagerBinder(List binder) { - try { - return service.requestInjectedManagerBinder(binder); - } catch (RemoteException | NullPointerException ignored) { - } - return null; - } - - @Override - public IBinder asBinder() { - return service.asBinder(); - } - - @Override - public void binderDied() { - service.asBinder().unlinkToDeath(this, 0); - serviceClient = null; - } -} diff --git a/core/src/main/java/org/lsposed/lspd/core/Startup.java b/core/src/main/java/org/lsposed/lspd/core/Startup.java deleted file mode 100644 index 3970de86d..000000000 --- a/core/src/main/java/org/lsposed/lspd/core/Startup.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - -package org.lsposed.lspd.core; - -import android.app.ActivityThread; -import android.app.LoadedApk; -import android.content.pm.ApplicationInfo; -import android.content.res.CompatibilityInfo; -import android.os.IBinder; - -import com.android.internal.os.ZygoteInit; - -import org.lsposed.lspd.deopt.PrebuiltMethodsDeopter; -import org.lsposed.lspd.hooker.AttachHooker; -import org.lsposed.lspd.hooker.CrashDumpHooker; -import org.lsposed.lspd.hooker.HandleSystemServerProcessHooker; -import org.lsposed.lspd.hooker.LoadedApkCtorHooker; -import org.lsposed.lspd.hooker.LoadedApkCreateCLHooker; -import org.lsposed.lspd.hooker.OpenDexFileHooker; -import org.lsposed.lspd.hooker.StartBootstrapServicesHooker; -import org.lsposed.lspd.impl.LSPosedContext; -import org.lsposed.lspd.impl.LSPosedHelper; -import org.lsposed.lspd.service.ILSPApplicationService; -import org.lsposed.lspd.util.Utils; - -import java.util.List; - -import dalvik.system.DexFile; -import de.robv.android.xposed.XposedBridge; -import de.robv.android.xposed.XposedInit; - -public class Startup { - private static void startBootstrapHook(boolean isSystem) { - Utils.logD("startBootstrapHook starts: isSystem = " + isSystem); - LSPosedHelper.hookMethod(CrashDumpHooker.class, Thread.class, "dispatchUncaughtException", Throwable.class); - if (isSystem) { - LSPosedHelper.hookAllMethods(HandleSystemServerProcessHooker.class, ZygoteInit.class, "handleSystemServerProcess"); - } else { - LSPosedHelper.hookAllMethods(OpenDexFileHooker.class, DexFile.class, "openDexFile"); - LSPosedHelper.hookAllMethods(OpenDexFileHooker.class, DexFile.class, "openInMemoryDexFile"); - LSPosedHelper.hookAllMethods(OpenDexFileHooker.class, DexFile.class, "openInMemoryDexFiles"); - } - LSPosedHelper.hookConstructor(LoadedApkCtorHooker.class, LoadedApk.class, - ActivityThread.class, ApplicationInfo.class, CompatibilityInfo.class, - ClassLoader.class, boolean.class, boolean.class, boolean.class); - LSPosedHelper.hookMethod(LoadedApkCreateCLHooker.class, LoadedApk.class, "createOrUpdateClassLoaderLocked", List.class); - LSPosedHelper.hookAllMethods(AttachHooker.class, ActivityThread.class, "attach"); - } - - public static void bootstrapXposed(boolean systemServerStarted) { - // Initialize the Xposed framework - try { - startBootstrapHook(XposedInit.startsSystemServer); - XposedInit.loadLegacyModules(); - } catch (Throwable t) { - Utils.logE("error during Xposed initialization", t); - } - - if (systemServerStarted) { - Utils.logD("Manually triggering system_server module load for late injection"); - - IBinder activityService = android.os.ServiceManager.getService("activity"); - if (activityService == null) { - Utils.logE("Activity service not found! Cannot get SystemServer ClassLoader."); - return; - } - - // Maintain state consistency for the rest of the Vector framework - HandleSystemServerProcessHooker.systemServerCL = activityService.getClass().getClassLoader(); - HandleSystemServerProcessHooker.after(); - StartBootstrapServicesHooker.before(); - - Utils.logI("Late system_server injection successfully completed."); - } - } - - public static void initXposed(boolean isSystem, String processName, String appDir, ILSPApplicationService service) { - // init logger - ApplicationServiceClient.Init(service, processName); - XposedBridge.initXResources(); - XposedInit.startsSystemServer = isSystem; - LSPosedContext.isSystemServer = isSystem; - LSPosedContext.appDir = appDir; - LSPosedContext.processName = processName; - PrebuiltMethodsDeopter.deoptBootMethods(); // do it once for secondary zygote - } -} diff --git a/core/src/main/java/org/lsposed/lspd/deopt/InlinedMethodCallers.java b/core/src/main/java/org/lsposed/lspd/deopt/InlinedMethodCallers.java deleted file mode 100644 index 4b7f999b7..000000000 --- a/core/src/main/java/org/lsposed/lspd/deopt/InlinedMethodCallers.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.deopt; - -import android.app.Instrumentation; -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.res.AssetManager; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.util.DisplayMetrics; -import android.util.TypedValue; - -import java.util.HashMap; - -/** - * Providing a whitelist of methods which are the callers of the target methods we want to hook. - * Because the target methods are inlined into the callers, we deoptimize the callers to - * run in intercept mode to make target methods hookable. - *

- * Only for methods which are included in pre-compiled framework codes. - * TODO recompile system apps and priv-apps since their original dex files are available - */ -public class InlinedMethodCallers { - - public static final String KEY_BOOT_IMAGE = "boot_image"; - public static final String KEY_BOOT_IMAGE_MIUI_RES = "boot_image_miui_res"; - public static final String KEY_SYSTEM_SERVER = "system_server"; - - /** - * Key should be {@link #KEY_BOOT_IMAGE}, {@link #KEY_SYSTEM_SERVER}, or a package name - * of system apps or priv-apps i.e. com.android.systemui - */ - private static final HashMap CALLERS = new HashMap<>(); - - /** - * format for each row: {className, methodName, methodSig} - */ - private static final Object[][] BOOT_IMAGE = { - // callers of Application#attach(Context) - {"android.app.Instrumentation", "newApplication", ClassLoader.class, String.class, Context.class}, - {"android.app.Instrumentation", "newApplication", ClassLoader.class, Context.class}, - - // callers of Instrumentation#newApplication(ClassLoader, String, Context) - {"android.app.LoadedApk", "makeApplicationInner", Boolean.TYPE, Instrumentation.class, Boolean.TYPE}, - {"android.app.LoadedApk", "makeApplicationInner", Boolean.TYPE, Instrumentation.class}, - {"android.app.LoadedApk", "makeApplication", Boolean.TYPE, Instrumentation.class}, - - {"android.app.ContextImpl", "getSharedPreferencesPath", String.class} - }; - - // TODO deprecate this - private static final Object[][] BOOT_IMAGE_FOR_MIUI_RES = { - // for MIUI resources hooking - {"android.content.res.MiuiResources", "init", String.class}, - {"android.content.res.MiuiResources", "updateMiuiImpl"}, - {"android.content.res.MiuiResources", "setImpl", "android.content.res.ResourcesImpl"}, - {"android.content.res.MiuiResources", "loadOverlayValue", TypedValue.class, int.class}, - {"android.content.res.MiuiResources", "getThemeString", CharSequence.class}, - {"android.content.res.MiuiResources", "", ClassLoader.class}, - {"android.content.res.MiuiResources", ""}, - {"android.content.res.MiuiResources", "", AssetManager.class, DisplayMetrics.class, Configuration.class}, - {"android.miui.ResourcesManager", "initMiuiResource", Resources.class, String.class}, - {"android.app.LoadedApk", "getResources", Resources.class}, - {"android.content.res.Resources", "getSystem", Resources.class}, - {"android.app.ApplicationPackageManager", "getResourcesForApplication", ApplicationInfo.class}, - {"android.app.ContextImpl", "setResources", Resources.class}, - }; - - private static final Object[][] SYSTEM_SERVER = {}; - - private static final Object[][] SYSTEM_UI = {}; - - static { - CALLERS.put(KEY_BOOT_IMAGE, BOOT_IMAGE); - CALLERS.put(KEY_BOOT_IMAGE_MIUI_RES, BOOT_IMAGE_FOR_MIUI_RES); - CALLERS.put(KEY_SYSTEM_SERVER, SYSTEM_SERVER); - CALLERS.put("com.android.systemui", SYSTEM_UI); - } - - public static HashMap getAll() { - return CALLERS; - } - - public static Object[][] get(String where) { - return CALLERS.get(where); - } -} diff --git a/core/src/main/java/org/lsposed/lspd/deopt/PrebuiltMethodsDeopter.java b/core/src/main/java/org/lsposed/lspd/deopt/PrebuiltMethodsDeopter.java deleted file mode 100644 index ad0b104cc..000000000 --- a/core/src/main/java/org/lsposed/lspd/deopt/PrebuiltMethodsDeopter.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.deopt; - -import static org.lsposed.lspd.deopt.InlinedMethodCallers.KEY_BOOT_IMAGE; -import static org.lsposed.lspd.deopt.InlinedMethodCallers.KEY_BOOT_IMAGE_MIUI_RES; -import static org.lsposed.lspd.deopt.InlinedMethodCallers.KEY_SYSTEM_SERVER; - -import org.matrix.vector.nativebridge.HookBridge; -import org.lsposed.lspd.util.Hookers; -import org.lsposed.lspd.util.Utils; - -import java.lang.reflect.Executable; -import java.util.Arrays; - -import de.robv.android.xposed.XposedHelpers; - -public class PrebuiltMethodsDeopter { - - public static void deoptMethods(String where, ClassLoader cl) { - Object[][] callers = InlinedMethodCallers.get(where); - if (callers == null) { - return; - } - for (Object[] caller : callers) { - try { - if (caller.length < 2) continue; - if (!(caller[0] instanceof String)) continue; - if (!(caller[1] instanceof String)) continue; - Executable method; - Object[] params = new Object[caller.length - 2]; - System.arraycopy(caller, 2, params, 0, params.length); - if ("".equals(caller[1])) { - method = XposedHelpers.findConstructorExactIfExists((String) caller[0], cl, params); - } else { - method = XposedHelpers.findMethodExactIfExists((String) caller[0], cl, (String) caller[1], params); - } - if (method != null) { - Hookers.logD("deoptimizing " + method); - HookBridge.deoptimizeMethod(method); - } - } catch (Throwable throwable) { - Utils.logE("error when deopting method: " + Arrays.toString(caller), throwable); - } - } - } - - public static void deoptBootMethods() { - // todo check if has been done before - deoptMethods(KEY_BOOT_IMAGE, null); - } - - public static void deoptResourceMethods() { - if (Utils.isMIUI) { - //deopt these only for MIUI - deoptMethods(KEY_BOOT_IMAGE_MIUI_RES, null); - } - } - - public static void deoptSystemServerMethods(ClassLoader sysCL) { - deoptMethods(KEY_SYSTEM_SERVER, sysCL); - } -} diff --git a/core/src/main/java/org/lsposed/lspd/hooker/AttachHooker.java b/core/src/main/java/org/lsposed/lspd/hooker/AttachHooker.java deleted file mode 100644 index 3e77ba156..000000000 --- a/core/src/main/java/org/lsposed/lspd/hooker/AttachHooker.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.lsposed.lspd.hooker; - -import android.app.ActivityThread; - -import de.robv.android.xposed.XposedInit; -import io.github.libxposed.api.XposedInterface; - -public class AttachHooker implements XposedInterface.Hooker { - - public static void after(XposedInterface.AfterHookCallback callback) { - XposedInit.loadModules((ActivityThread) callback.getThisObject()); - } -} diff --git a/core/src/main/java/org/lsposed/lspd/hooker/CrashDumpHooker.java b/core/src/main/java/org/lsposed/lspd/hooker/CrashDumpHooker.java deleted file mode 100644 index 53ab5c0c7..000000000 --- a/core/src/main/java/org/lsposed/lspd/hooker/CrashDumpHooker.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.lsposed.lspd.hooker; - -import org.lsposed.lspd.impl.LSPosedBridge; -import org.lsposed.lspd.util.Utils.Log; - -import io.github.libxposed.api.XposedInterface; - -public class CrashDumpHooker implements XposedInterface.Hooker { - - public static void before(XposedInterface.BeforeHookCallback callback) { - try { - var e = (Throwable) callback.getArgs()[0]; - LSPosedBridge.log("Crash unexpectedly: " + Log.getStackTraceString(e)); - } catch (Throwable ignored) { - } - } -} diff --git a/core/src/main/java/org/lsposed/lspd/hooker/HandleSystemServerProcessHooker.java b/core/src/main/java/org/lsposed/lspd/hooker/HandleSystemServerProcessHooker.java deleted file mode 100644 index ef93feb19..000000000 --- a/core/src/main/java/org/lsposed/lspd/hooker/HandleSystemServerProcessHooker.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.hooker; - -import android.annotation.SuppressLint; - -import org.lsposed.lspd.deopt.PrebuiltMethodsDeopter; -import org.lsposed.lspd.impl.LSPosedHelper; -import org.lsposed.lspd.util.Hookers; - -import io.github.libxposed.api.XposedInterface; - -// system_server initialization -public class HandleSystemServerProcessHooker implements XposedInterface.Hooker { - - public interface Callback { - void onSystemServerLoaded(ClassLoader classLoader); - } - - public static volatile ClassLoader systemServerCL = null; - public static volatile Callback callback = null; - - @SuppressLint("PrivateApi") - public static void after() { - Hookers.logD("ZygoteInit#handleSystemServerProcess() starts"); - try { - if (systemServerCL == null) { - // get system_server classLoader - systemServerCL = Thread.currentThread().getContextClassLoader(); - } - // deopt methods in SYSTEMSERVERCLASSPATH - PrebuiltMethodsDeopter.deoptSystemServerMethods(systemServerCL); - var clazz = Class.forName("com.android.server.SystemServer", false, systemServerCL); - LSPosedHelper.hookAllMethods(StartBootstrapServicesHooker.class, clazz, "startBootstrapServices"); - if (callback != null) callback.onSystemServerLoaded(systemServerCL); - } catch (Throwable t) { - Hookers.logE("error when hooking systemMain", t); - } - } -} diff --git a/core/src/main/java/org/lsposed/lspd/hooker/LoadedApkCreateCLHooker.java b/core/src/main/java/org/lsposed/lspd/hooker/LoadedApkCreateCLHooker.java deleted file mode 100644 index b5c1bec08..000000000 --- a/core/src/main/java/org/lsposed/lspd/hooker/LoadedApkCreateCLHooker.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.hooker; - -import static org.lsposed.lspd.core.ApplicationServiceClient.serviceClient; - -import android.annotation.SuppressLint; -import android.app.ActivityThread; -import android.app.LoadedApk; -import android.content.pm.ApplicationInfo; -import android.os.Build; - -import androidx.annotation.NonNull; - -import org.lsposed.lspd.impl.LSPosedContext; -import org.lsposed.lspd.util.Hookers; -import org.lsposed.lspd.util.MetaDataReader; -import org.lsposed.lspd.util.Utils; - -import java.io.File; -import java.io.IOException; -import java.lang.reflect.Field; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -import de.robv.android.xposed.XC_MethodHook; -import de.robv.android.xposed.XC_MethodReplacement; -import de.robv.android.xposed.XposedBridge; -import de.robv.android.xposed.XposedHelpers; -import de.robv.android.xposed.XposedInit; -import de.robv.android.xposed.callbacks.XC_LoadPackage; -import io.github.libxposed.api.XposedInterface; -import io.github.libxposed.api.XposedModuleInterface; - -@SuppressLint("BlockedPrivateApi") -public class LoadedApkCreateCLHooker implements XposedInterface.Hooker { - private final static Field defaultClassLoaderField; - - private final static Set loadedApks = ConcurrentHashMap.newKeySet(); - - static { - Field field = null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - try { - field = LoadedApk.class.getDeclaredField("mDefaultClassLoader"); - field.setAccessible(true); - } catch (Throwable ignored) { - } - } - defaultClassLoaderField = field; - } - - static void addLoadedApk(LoadedApk loadedApk) { - loadedApks.add(loadedApk); - } - - public static void after(XposedInterface.AfterHookCallback callback) { - LoadedApk loadedApk = (LoadedApk) callback.getThisObject(); - - if (callback.getArgs()[0] != null || !loadedApks.contains(loadedApk)) { - return; - } - - try { - Hookers.logD("LoadedApk#createClassLoader starts"); - - String packageName = ActivityThread.currentPackageName(); - String processName = ActivityThread.currentProcessName(); - boolean isFirstPackage = packageName != null && processName != null && packageName.equals(loadedApk.getPackageName()); - if (!isFirstPackage) { - packageName = loadedApk.getPackageName(); - processName = ActivityThread.currentPackageName(); - } else if (packageName.equals("android")) { - packageName = "system"; - } - - Object mAppDir = XposedHelpers.getObjectField(loadedApk, "mAppDir"); - ClassLoader classLoader = (ClassLoader) XposedHelpers.getObjectField(loadedApk, "mClassLoader"); - Hookers.logD("LoadedApk#createClassLoader ends: " + mAppDir + " -> " + classLoader); - - if (classLoader == null) { - return; - } - - if (!isFirstPackage && !XposedHelpers.getBooleanField(loadedApk, "mIncludeCode")) { - Hookers.logD("LoadedApk# mIncludeCode == false: " + mAppDir); - return; - } - - if (!isFirstPackage && !XposedInit.getLoadedModules().getOrDefault(packageName, Optional.of("")).isPresent()) { - return; - } - - XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam( - XposedBridge.sLoadedPackageCallbacks); - lpparam.packageName = packageName; - lpparam.processName = processName; - lpparam.classLoader = classLoader; - lpparam.appInfo = loadedApk.getApplicationInfo(); - lpparam.isFirstApplication = isFirstPackage; - - if (isFirstPackage && XposedInit.getLoadedModules().getOrDefault(packageName, Optional.empty()).isPresent()) { - hookNewXSP(lpparam); - } - - Hookers.logD("Call handleLoadedPackage: packageName=" + lpparam.packageName + " processName=" + lpparam.processName + " isFirstPackage=" + isFirstPackage + " classLoader=" + lpparam.classLoader + " appInfo=" + lpparam.appInfo); - XC_LoadPackage.callAll(lpparam); - - LSPosedContext.callOnPackageLoaded(new XposedModuleInterface.PackageLoadedParam() { - @NonNull - @Override - public String getPackageName() { - return loadedApk.getPackageName(); - } - - @NonNull - @Override - public ApplicationInfo getApplicationInfo() { - return loadedApk.getApplicationInfo(); - } - - @NonNull - @Override - public ClassLoader getDefaultClassLoader() { - try { - return (ClassLoader) defaultClassLoaderField.get(loadedApk); - } catch (Throwable t) { - throw new IllegalStateException(t); - } - } - - @NonNull - @Override - public ClassLoader getClassLoader() { - return classLoader; - } - - @Override - public boolean isFirstPackage() { - return isFirstPackage; - } - }); - } catch (Throwable t) { - Hookers.logE("error when hooking LoadedApk#createClassLoader", t); - } finally { - loadedApks.remove(loadedApk); - } - } - - private static void hookNewXSP(XC_LoadPackage.LoadPackageParam lpparam) { - int xposedminversion = -1; - boolean xposedsharedprefs = false; - try { - Map metaData = MetaDataReader.getMetaData(new File(lpparam.appInfo.sourceDir)); - Object minVersionRaw = metaData.get("xposedminversion"); - if (minVersionRaw instanceof Integer) { - xposedminversion = (Integer) minVersionRaw; - } else if (minVersionRaw instanceof String) { - xposedminversion = MetaDataReader.extractIntPart((String) minVersionRaw); - } - xposedsharedprefs = metaData.containsKey("xposedsharedprefs"); - } catch (NumberFormatException | IOException e) { - Hookers.logE("ApkParser fails", e); - } - - if (xposedminversion > 92 || xposedsharedprefs) { - Utils.logI("New modules detected, hook preferences"); - XposedHelpers.findAndHookMethod("android.app.ContextImpl", lpparam.classLoader, "checkMode", int.class, new XC_MethodHook() { - @Override - protected void afterHookedMethod(MethodHookParam param) { - if (((int) param.args[0] & 1/*Context.MODE_WORLD_READABLE*/) != 0) { - param.setThrowable(null); - } - } - }); - XposedHelpers.findAndHookMethod("android.app.ContextImpl", lpparam.classLoader, "getPreferencesDir", new XC_MethodReplacement() { - @Override - protected Object replaceHookedMethod(MethodHookParam param) { - return new File(serviceClient.getPrefsPath(lpparam.packageName)); - } - }); - } - } -} diff --git a/core/src/main/java/org/lsposed/lspd/hooker/LoadedApkCtorHooker.java b/core/src/main/java/org/lsposed/lspd/hooker/LoadedApkCtorHooker.java deleted file mode 100644 index e0a4af96e..000000000 --- a/core/src/main/java/org/lsposed/lspd/hooker/LoadedApkCtorHooker.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.hooker; - -import android.app.LoadedApk; -import android.content.res.XResources; - -import org.lsposed.lspd.util.Hookers; -import org.lsposed.lspd.util.Utils.Log; - -import de.robv.android.xposed.XposedHelpers; -import de.robv.android.xposed.XposedInit; -import io.github.libxposed.api.XposedInterface; - -// when a package is loaded for an existing process, trigger the callbacks as well -public class LoadedApkCtorHooker implements XposedInterface.Hooker { - - public static void after(XposedInterface.AfterHookCallback callback) { - Hookers.logD("LoadedApk# starts"); - - try { - LoadedApk loadedApk = (LoadedApk) callback.getThisObject(); - assert loadedApk != null; - String packageName = loadedApk.getPackageName(); - Object mAppDir = XposedHelpers.getObjectField(loadedApk, "mAppDir"); - Hookers.logD("LoadedApk# ends: " + mAppDir); - - if (!XposedInit.disableResources) { - XResources.setPackageNameForResDir(packageName, loadedApk.getResDir()); - } - - if (packageName.equals("android")) { - if (XposedInit.startsSystemServer) { - Hookers.logD("LoadedApk# is android, skip: " + mAppDir); - return; - } else { - packageName = "system"; - } - } - - if (!XposedInit.loadedPackagesInProcess.add(packageName)) { - Hookers.logD("LoadedApk# has been loaded before, skip: " + mAppDir); - return; - } - - // OnePlus magic... - if (Log.getStackTraceString(new Throwable()). - contains("android.app.ActivityThread$ApplicationThread.schedulePreload")) { - Hookers.logD("LoadedApk# maybe oneplus's custom opt, skip"); - return; - } - - LoadedApkCreateCLHooker.addLoadedApk(loadedApk); - } catch (Throwable t) { - Hookers.logE("error when hooking LoadedApk.", t); - } - } -} diff --git a/core/src/main/java/org/lsposed/lspd/hooker/OpenDexFileHooker.java b/core/src/main/java/org/lsposed/lspd/hooker/OpenDexFileHooker.java deleted file mode 100644 index f50e14768..000000000 --- a/core/src/main/java/org/lsposed/lspd/hooker/OpenDexFileHooker.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.lsposed.lspd.hooker; - -import android.os.Build; - -import org.lsposed.lspd.impl.LSPosedBridge; -import org.matrix.vector.nativebridge.HookBridge; - -import io.github.libxposed.api.XposedInterface; - -public class OpenDexFileHooker implements XposedInterface.Hooker { - - public static void after(XposedInterface.AfterHookCallback callback) { - ClassLoader classLoader = null; - for (var arg : callback.getArgs()) { - if (arg instanceof ClassLoader) { - classLoader = (ClassLoader) arg; - } - } - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P && classLoader == null) { - classLoader = LSPosedBridge.class.getClassLoader(); - } - while (classLoader != null) { - if (classLoader == LSPosedBridge.class.getClassLoader()) { - HookBridge.setTrusted(callback.getResult()); - return; - } else { - classLoader = classLoader.getParent(); - } - } - } -} diff --git a/core/src/main/java/org/lsposed/lspd/hooker/StartBootstrapServicesHooker.java b/core/src/main/java/org/lsposed/lspd/hooker/StartBootstrapServicesHooker.java deleted file mode 100644 index 3594095fa..000000000 --- a/core/src/main/java/org/lsposed/lspd/hooker/StartBootstrapServicesHooker.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.hooker; - -import static org.lsposed.lspd.util.Utils.logD; - -import androidx.annotation.NonNull; - -import org.lsposed.lspd.impl.LSPosedContext; -import org.lsposed.lspd.util.Hookers; - -import de.robv.android.xposed.XposedBridge; -import de.robv.android.xposed.XposedInit; -import de.robv.android.xposed.callbacks.XC_LoadPackage; -import io.github.libxposed.api.XposedInterface; -import io.github.libxposed.api.XposedModuleInterface; - -public class StartBootstrapServicesHooker implements XposedInterface.Hooker { - - public static void before() { - logD("SystemServer#startBootstrapServices() starts"); - - try { - XposedInit.loadedPackagesInProcess.add("android"); - - XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam(XposedBridge.sLoadedPackageCallbacks); - lpparam.packageName = "android"; - lpparam.processName = "android"; // it's actually system_server, but other functions return this as well - lpparam.classLoader = HandleSystemServerProcessHooker.systemServerCL; - lpparam.appInfo = null; - lpparam.isFirstApplication = true; - XC_LoadPackage.callAll(lpparam); - - LSPosedContext.callOnSystemServerLoaded(new XposedModuleInterface.SystemServerLoadedParam() { - @Override - @NonNull - public ClassLoader getClassLoader() { - return HandleSystemServerProcessHooker.systemServerCL; - } - }); - } catch (Throwable t) { - Hookers.logE("error when hooking startBootstrapServices", t); - } - } -} diff --git a/core/src/main/java/org/lsposed/lspd/impl/LSPosedBridge.java b/core/src/main/java/org/lsposed/lspd/impl/LSPosedBridge.java deleted file mode 100644 index 53e033af7..000000000 --- a/core/src/main/java/org/lsposed/lspd/impl/LSPosedBridge.java +++ /dev/null @@ -1,292 +0,0 @@ -package org.lsposed.lspd.impl; - -import androidx.annotation.NonNull; - -import org.matrix.vector.nativebridge.HookBridge; -import org.lsposed.lspd.util.Utils.Log; - -import java.lang.reflect.Executable; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; - -import de.robv.android.xposed.XposedBridge; -import io.github.libxposed.api.XposedInterface; -import io.github.libxposed.api.errors.HookFailedError; - -public class LSPosedBridge { - - private static final String TAG = "LSPosed-Bridge"; - - private static final String castException = "Return value's type from hook callback does not match the hooked method"; - - private static final Method getCause; - - static { - Method tmp; - try { - tmp = InvocationTargetException.class.getMethod("getCause"); - } catch (Throwable e) { - tmp = null; - } - getCause = tmp; - } - - public static class HookerCallback { - @NonNull - final Method beforeInvocation; - @NonNull - final Method afterInvocation; - - final int beforeParams; - final int afterParams; - - public HookerCallback(@NonNull Method beforeInvocation, @NonNull Method afterInvocation) { - this.beforeInvocation = beforeInvocation; - this.afterInvocation = afterInvocation; - this.beforeParams = beforeInvocation.getParameterCount(); - this.afterParams = afterInvocation.getParameterCount(); - } - } - - public static void log(String text) { - Log.i(TAG, text); - } - - public static void log(Throwable t) { - String logStr = Log.getStackTraceString(t); - Log.e(TAG, logStr); - } - - public static class NativeHooker { - private final Object params; - - private NativeHooker(Executable method) { - var isStatic = Modifier.isStatic(method.getModifiers()); - Object returnType; - if (method instanceof Method) { - returnType = ((Method) method).getReturnType(); - } else { - returnType = null; - } - params = new Object[]{ - method, - returnType, - isStatic, - }; - } - - // This method is quite critical. We should try not to use system methods to avoid - // endless recursive - public Object callback(Object[] args) throws Throwable { - LSPosedHookCallback callback = new LSPosedHookCallback<>(); - - var array = ((Object[]) params); - - var method = (T) array[0]; - var returnType = (Class) array[1]; - var isStatic = (Boolean) array[2]; - - callback.method = method; - - if (isStatic) { - callback.thisObject = null; - callback.args = args; - } else { - callback.thisObject = args[0]; - callback.args = new Object[args.length - 1]; - //noinspection ManualArrayCopy - for (int i = 0; i < args.length - 1; ++i) { - callback.args[i] = args[i + 1]; - } - } - - Object[][] callbacksSnapshot = HookBridge.callbackSnapshot(HookerCallback.class, method); - Object[] modernSnapshot = callbacksSnapshot[0]; - Object[] legacySnapshot = callbacksSnapshot[1]; - - if (modernSnapshot.length == 0 && legacySnapshot.length == 0) { - try { - return HookBridge.invokeOriginalMethod(method, callback.thisObject, callback.args); - } catch (InvocationTargetException ite) { - throw (Throwable) HookBridge.invokeOriginalMethod(getCause, ite); - } - } - - Object[] ctxArray = new Object[modernSnapshot.length]; - XposedBridge.LegacyApiSupport legacy = null; - - // call "before method" callbacks - int beforeIdx; - for (beforeIdx = 0; beforeIdx < modernSnapshot.length; beforeIdx++) { - try { - var hooker = (HookerCallback) modernSnapshot[beforeIdx]; - if (hooker.beforeParams == 0) { - ctxArray[beforeIdx] = hooker.beforeInvocation.invoke(null); - } else { - ctxArray[beforeIdx] = hooker.beforeInvocation.invoke(null, callback); - } - } catch (Throwable t) { - LSPosedBridge.log(t); - - // reset result (ignoring what the unexpectedly exiting callback did) - callback.setResult(null); - callback.isSkipped = false; - continue; - } - - if (callback.isSkipped) { - // skip remaining "before" callbacks and corresponding "after" callbacks - beforeIdx++; - break; - } - } - - if (!callback.isSkipped && legacySnapshot.length != 0) { - // TODO: Separate classloader - legacy = new XposedBridge.LegacyApiSupport<>(callback, legacySnapshot); - legacy.handleBefore(); - } - - // call original method if not requested otherwise - if (!callback.isSkipped) { - try { - var result = HookBridge.invokeOriginalMethod(method, callback.thisObject, callback.args); - callback.setResult(result); - } catch (InvocationTargetException e) { - var throwable = (Throwable) HookBridge.invokeOriginalMethod(getCause, e); - callback.setThrowable(throwable); - } - } - - // call "after method" callbacks - for (int afterIdx = beforeIdx - 1; afterIdx >= 0; afterIdx--) { - Object lastResult = callback.getResult(); - Throwable lastThrowable = callback.getThrowable(); - var hooker = (HookerCallback) modernSnapshot[afterIdx]; - try { - if (hooker.afterParams == 0) { - hooker.afterInvocation.invoke(null); - } else if (hooker.afterParams == 1) { - hooker.afterInvocation.invoke(null, callback); - } else { - hooker.afterInvocation.invoke(null, callback, ctxArray[afterIdx]); - } - } catch (Throwable t) { - LSPosedBridge.log(t); - - // reset to last result (ignoring what the unexpectedly exiting callback did) - if (lastThrowable == null) { - callback.setResult(lastResult); - } else { - callback.setThrowable(lastThrowable); - } - } - } - - if (legacy != null) { - legacy.handleAfter(); - } - - // return - var t = callback.getThrowable(); - if (t != null) { - throw t; - } else { - var result = callback.getResult(); - if (returnType != null && !returnType.isPrimitive() && !HookBridge.instanceOf(result, returnType)) { - throw new ClassCastException(castException); - } - return result; - } - } - } - - public static void dummyCallback() { - } - - public static XposedInterface.MethodUnhooker - doHook(T hookMethod, int priority, Class hooker) { - if (Modifier.isAbstract(hookMethod.getModifiers())) { - throw new IllegalArgumentException("Cannot hook abstract methods: " + hookMethod); - } else if (hookMethod.getDeclaringClass().getClassLoader() == LSPosedContext.class.getClassLoader()) { - throw new IllegalArgumentException("Do not allow hooking inner methods"); - } else if (hookMethod.getDeclaringClass() == Method.class && hookMethod.getName().equals("invoke")) { - throw new IllegalArgumentException("Cannot hook Method.invoke"); - } else if (hooker == null) { - throw new IllegalArgumentException("hooker should not be null!"); - } - - Method beforeInvocation = null, afterInvocation = null; - var modifiers = Modifier.PUBLIC | Modifier.STATIC; - for (var method : hooker.getDeclaredMethods()) { - if (method.getName().equals("before")) { - if (beforeInvocation != null) { - throw new IllegalArgumentException("More than one method named before"); - } - boolean valid = (method.getModifiers() & modifiers) == modifiers; - var params = method.getParameterTypes(); - if (params.length == 1) { - valid &= params[0].equals(XposedInterface.BeforeHookCallback.class); - } else if (params.length != 0) { - valid = false; - } - if (!valid) { - throw new IllegalArgumentException("before method format is invalid"); - } - beforeInvocation = method; - } else if (method.getName().equals("after")) { - if (afterInvocation != null) { - throw new IllegalArgumentException("More than one method named after"); - } - boolean valid = (method.getModifiers() & modifiers) == modifiers; - valid &= method.getReturnType().equals(void.class); - var params = method.getParameterTypes(); - if (params.length == 1 || params.length == 2) { - valid &= params[0].equals(XposedInterface.AfterHookCallback.class); - } else if (params.length != 0) { - valid = false; - } - if (!valid) { - throw new IllegalArgumentException("after method format is invalid"); - } - afterInvocation = method; - } - } - if (beforeInvocation == null && afterInvocation == null) { - throw new IllegalArgumentException("No method named before or after found in " + hooker.getName()); - } - try { - if (beforeInvocation == null) { - beforeInvocation = LSPosedBridge.class.getMethod("dummyCallback"); - } else if (afterInvocation == null) { - afterInvocation = LSPosedBridge.class.getMethod("dummyCallback"); - } else { - var ret = beforeInvocation.getReturnType(); - var params = afterInvocation.getParameterTypes(); - if (ret != void.class && params.length == 2 && !ret.equals(params[1])) { - throw new IllegalArgumentException("before and after method format is invalid"); - } - } - } catch (NoSuchMethodException e) { - throw new HookFailedError(e); - } - - var callback = new LSPosedBridge.HookerCallback(beforeInvocation, afterInvocation); - if (HookBridge.hookMethod(true, hookMethod, LSPosedBridge.NativeHooker.class, priority, callback)) { - return new XposedInterface.MethodUnhooker<>() { - @NonNull - @Override - public T getOrigin() { - return hookMethod; - } - - @Override - public void unhook() { - HookBridge.unhookMethod(true, hookMethod, callback); - } - }; - } - throw new HookFailedError("Cannot hook " + hookMethod); - } -} diff --git a/core/src/main/java/org/lsposed/lspd/impl/LSPosedContext.java b/core/src/main/java/org/lsposed/lspd/impl/LSPosedContext.java deleted file mode 100644 index 4f4fe7abd..000000000 --- a/core/src/main/java/org/lsposed/lspd/impl/LSPosedContext.java +++ /dev/null @@ -1,385 +0,0 @@ -package org.lsposed.lspd.impl; - -import android.annotation.SuppressLint; -import android.app.ActivityThread; -import android.content.SharedPreferences; -import android.content.pm.ApplicationInfo; -import android.os.Build; -import android.os.ParcelFileDescriptor; -import android.os.Process; -import android.os.RemoteException; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.lsposed.lspd.core.BuildConfig; -import org.lsposed.lspd.models.Module; -import org.matrix.vector.nativebridge.HookBridge; -import org.matrix.vector.nativebridge.NativeAPI; -import org.lsposed.lspd.service.ILSPInjectedModuleService; -import org.lsposed.lspd.util.LspModuleClassLoader; -import org.lsposed.lspd.util.Utils.Log; - -import org.matrix.vector.impl.utils.VectorDexParser; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.lang.reflect.Constructor; -import java.lang.reflect.Executable; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.lang.reflect.Proxy; -import java.nio.ByteBuffer; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -import io.github.libxposed.api.XposedInterface; -import io.github.libxposed.api.XposedModule; -import io.github.libxposed.api.XposedModuleInterface; -import io.github.libxposed.api.errors.XposedFrameworkError; -import io.github.libxposed.api.utils.DexParser; - - -@SuppressLint("NewApi") -public class LSPosedContext implements XposedInterface { - - private static final String TAG = "LSPosedContext"; - - public static boolean isSystemServer; - public static String appDir; - public static String processName; - - static final Set modules = ConcurrentHashMap.newKeySet(); - - private final String mPackageName; - private final ApplicationInfo mApplicationInfo; - private final ILSPInjectedModuleService service; - private final Map mRemotePrefs = new ConcurrentHashMap<>(); - - LSPosedContext(String packageName, ApplicationInfo applicationInfo, ILSPInjectedModuleService service) { - this.mPackageName = packageName; - this.mApplicationInfo = applicationInfo; - this.service = service; - } - - public static void callOnPackageLoaded(XposedModuleInterface.PackageLoadedParam param) { - for (XposedModule module : modules) { - try { - module.onPackageLoaded(param); - } catch (Throwable t) { - Log.e(TAG, "Error when calling onPackageLoaded of " + module.getApplicationInfo().packageName, t); - } - } - } - - public static void callOnSystemServerLoaded(XposedModuleInterface.SystemServerLoadedParam param) { - for (XposedModule module : modules) { - try { - module.onSystemServerLoaded(param); - } catch (Throwable t) { - Log.e(TAG, "Error when calling onSystemServerLoaded of " + module.getApplicationInfo().packageName, t); - } - } - } - - @SuppressLint("DiscouragedPrivateApi") - public static boolean loadModule(ActivityThread at, Module module) { - try { - Log.d(TAG, "Loading module " + module.packageName); - var sb = new StringBuilder(); - var abis = Process.is64Bit() ? Build.SUPPORTED_64_BIT_ABIS : Build.SUPPORTED_32_BIT_ABIS; - for (String abi : abis) { - sb.append(module.apkPath).append("!/lib/").append(abi).append(File.pathSeparator); - } - var librarySearchPath = sb.toString(); - var initLoader = XposedModule.class.getClassLoader(); - var mcl = LspModuleClassLoader.loadApk(module.apkPath, module.file.preLoadedDexes, librarySearchPath, initLoader); - if (mcl.loadClass(XposedModule.class.getName()).getClassLoader() != initLoader) { - Log.e(TAG, " Cannot load module: " + module.packageName); - Log.e(TAG, " The Xposed API classes are compiled into the module's APK."); - Log.e(TAG, " This may cause strange issues and must be fixed by the module developer."); - return false; - } - var ctx = new LSPosedContext(module.packageName, module.applicationInfo, module.service); - for (var entry : module.file.moduleClassNames) { - var moduleClass = mcl.loadClass(entry); - Log.d(TAG, " Loading class " + moduleClass); - if (!XposedModule.class.isAssignableFrom(moduleClass)) { - Log.e(TAG, " This class doesn't implement any sub-interface of XposedModule, skipping it"); - continue; - } - try { - var moduleEntry = moduleClass.getConstructor(XposedInterface.class, XposedModuleInterface.ModuleLoadedParam.class); - var moduleContext = (XposedModule) moduleEntry.newInstance(ctx, new XposedModuleInterface.ModuleLoadedParam() { - @Override - public boolean isSystemServer() { - return isSystemServer; - } - - @NonNull - @Override - public String getProcessName() { - return processName; - } - }); - modules.add(moduleContext); - } catch (Throwable e) { - Log.e(TAG, " Failed to load class " + moduleClass, e); - } - } - module.file.moduleLibraryNames.forEach(NativeAPI::recordNativeEntrypoint); - Log.d(TAG, "Loaded module " + module.packageName + ": " + ctx); - } catch (Throwable e) { - Log.d(TAG, "Loading module " + module.packageName, e); - return false; - } - return true; - } - - @NonNull - @Override - public String getFrameworkName() { - return BuildConfig.FRAMEWORK_NAME; - } - - @NonNull - @Override - public String getFrameworkVersion() { - return BuildConfig.VERSION_NAME; - } - - @Override - public long getFrameworkVersionCode() { - return BuildConfig.VERSION_CODE; - } - - @Override - public int getFrameworkPrivilege() { - try { - return service.getFrameworkPrivilege(); - } catch (RemoteException ignored) { - return -1; - } - } - - @Override - @NonNull - public MethodUnhooker hook(@NonNull Method origin, @NonNull Class hooker) { - return LSPosedBridge.doHook(origin, PRIORITY_DEFAULT, hooker); - } - - @Override - @NonNull - public MethodUnhooker hook(@NonNull Method origin, int priority, @NonNull Class hooker) { - return LSPosedBridge.doHook(origin, priority, hooker); - } - - @Override - @NonNull - public MethodUnhooker> hook(@NonNull Constructor origin, @NonNull Class hooker) { - return LSPosedBridge.doHook(origin, PRIORITY_DEFAULT, hooker); - } - - @Override - @NonNull - public MethodUnhooker> hook(@NonNull Constructor origin, int priority, @NonNull Class hooker) { - return LSPosedBridge.doHook(origin, priority, hooker); - } - - @Override - @NonNull - public MethodUnhooker> hookClassInitializer(@NonNull Class origin, @NonNull Class hooker) { - return hookClassInitializer(origin, PRIORITY_DEFAULT, hooker); - } - - @Override - @NonNull - @SuppressWarnings({"unchecked", "rawtypes"}) - public MethodUnhooker> hookClassInitializer(@NonNull Class origin, int priority, @NonNull Class hooker) { - Method staticInitializer = HookBridge.getStaticInitializer(origin); - - // The class might not have a static initializer block - if (staticInitializer == null) { - throw new IllegalArgumentException("Class " + origin.getName() + " has no static initializer"); - } - - // Use the existing doHook logic. It will return a MethodUnhooker. - return (MethodUnhooker) LSPosedBridge.doHook(staticInitializer, priority, hooker); - } - - private static boolean doDeoptimize(@NonNull Executable method) { - if (Modifier.isAbstract(method.getModifiers())) { - throw new IllegalArgumentException("Cannot deoptimize abstract methods: " + method); - } else if (Proxy.isProxyClass(method.getDeclaringClass())) { - throw new IllegalArgumentException("Cannot deoptimize methods from proxy class: " + method); - } - return HookBridge.deoptimizeMethod(method); - } - - @Override - public boolean deoptimize(@NonNull Method method) { - return doDeoptimize(method); - } - - @Override - public boolean deoptimize(@NonNull Constructor constructor) { - return doDeoptimize(constructor); - } - - @Nullable - @Override - public Object invokeOrigin(@NonNull Method method, @Nullable Object thisObject, Object... args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException { - return HookBridge.invokeOriginalMethod(method, thisObject, args); - } - - @Override - public void invokeOrigin(@NonNull Constructor constructor, @NonNull T thisObject, Object... args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException { - // The bridge returns an Object (null for void/constructors), which we discard. - HookBridge.invokeOriginalMethod(constructor, thisObject, args); - } - - private static char getTypeShorty(Class type) { - if (type == int.class) { - return 'I'; - } else if (type == long.class) { - return 'J'; - } else if (type == float.class) { - return 'F'; - } else if (type == double.class) { - return 'D'; - } else if (type == boolean.class) { - return 'Z'; - } else if (type == byte.class) { - return 'B'; - } else if (type == char.class) { - return 'C'; - } else if (type == short.class) { - return 'S'; - } else if (type == void.class) { - return 'V'; - } else { - return 'L'; - } - } - - private static char[] getExecutableShorty(Executable executable) { - var parameterTypes = executable.getParameterTypes(); - var shorty = new char[parameterTypes.length + 1]; - shorty[0] = getTypeShorty(executable instanceof Method ? ((Method) executable).getReturnType() : void.class); - for (int i = 1; i < shorty.length; i++) { - shorty[i] = getTypeShorty(parameterTypes[i - 1]); - } - return shorty; - } - - @Nullable - @Override - public Object invokeSpecial(@NonNull Method method, @NonNull Object thisObject, Object... args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException { - if (Modifier.isStatic(method.getModifiers())) { - throw new IllegalArgumentException("Cannot invoke special on static method: " + method); - } - return HookBridge.invokeSpecialMethod(method, getExecutableShorty(method), method.getDeclaringClass(), thisObject, args); - } - - @Override - public void invokeSpecial(@NonNull Constructor constructor, @NonNull T thisObject, Object... args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException { - HookBridge.invokeSpecialMethod(constructor, getExecutableShorty(constructor), constructor.getDeclaringClass(), thisObject, args); - } - - @NonNull - @Override - public T newInstanceOrigin(@NonNull Constructor constructor, Object... args) throws InvocationTargetException, IllegalAccessException, InstantiationException { - var obj = HookBridge.allocateObject(constructor.getDeclaringClass()); - HookBridge.invokeOriginalMethod(constructor, obj, args); - return obj; - } - - @NonNull - @Override - public U newInstanceSpecial(@NonNull Constructor constructor, @NonNull Class subClass, Object... args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException, InstantiationException { - var superClass = constructor.getDeclaringClass(); - if (!superClass.isAssignableFrom(subClass)) { - throw new IllegalArgumentException(subClass + " is not inherited from " + superClass); - } - var obj = HookBridge.allocateObject(subClass); - HookBridge.invokeSpecialMethod(constructor, getExecutableShorty(constructor), superClass, obj, args); - return obj; - } - - @Override - public void log(@NonNull String message) { - log(Log.INFO, null, message, null); - } - - @Override - public void log(@NonNull String message, @NonNull Throwable throwable) { - log(Log.ERROR, null, message, throwable); - } - - @Override - public void log(int priority, @Nullable String tag, @NonNull String msg, @Nullable Throwable tr) { - String finalTag = (tag != null) ? tag : TAG; - - // Format the message with the package name prefix - String prefix = (mPackageName != null) ? mPackageName + ": " : ""; - StringBuilder fullMsg = new StringBuilder(prefix).append(msg); - - // Handle the Throwable if present - if (tr != null) { - fullMsg.append("\n").append(Log.getStackTraceString(tr)); - } - - // Use the low-level println to handle dynamic priorities - Log.println(priority, finalTag, fullMsg.toString()); - } - - @Override - public DexParser parseDex(@NonNull ByteBuffer dexData, boolean includeAnnotations) throws IOException { - return new VectorDexParser(dexData, includeAnnotations); - } - - @NonNull - @Override - public ApplicationInfo getApplicationInfo() { - return mApplicationInfo; - } - - @NonNull - @Override - public SharedPreferences getRemotePreferences(String name) { - if (name == null) throw new IllegalArgumentException("name must not be null"); - return mRemotePrefs.computeIfAbsent(name, n -> { - try { - return new LSPosedRemotePreferences(service, n); - } catch (RemoteException e) { - log("Failed to get remote preferences", e); - throw new XposedFrameworkError(e); - } - }); - } - - @NonNull - @Override - public String[] listRemoteFiles() { - try { - return service.getRemoteFileList(); - } catch (RemoteException e) { - log("Failed to list remote files", e); - throw new XposedFrameworkError(e); - } - } - - @NonNull - @Override - public ParcelFileDescriptor openRemoteFile(String name) throws FileNotFoundException { - if (name == null) throw new IllegalArgumentException("name must not be null"); - try { - return service.openRemoteFile(name); - } catch (RemoteException e) { - throw new FileNotFoundException(e.getMessage()); - } - } -} diff --git a/core/src/main/java/org/lsposed/lspd/impl/LSPosedHelper.java b/core/src/main/java/org/lsposed/lspd/impl/LSPosedHelper.java deleted file mode 100644 index 60f115e4b..000000000 --- a/core/src/main/java/org/lsposed/lspd/impl/LSPosedHelper.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.lsposed.lspd.impl; - -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.util.HashSet; -import java.util.Set; - -import io.github.libxposed.api.XposedInterface; -import io.github.libxposed.api.errors.HookFailedError; - -public class LSPosedHelper { - - @SuppressWarnings("UnusedReturnValue") - public static XposedInterface.MethodUnhooker - hookMethod(Class hooker, Class clazz, String methodName, Class... parameterTypes) { - try { - var method = clazz.getDeclaredMethod(methodName, parameterTypes); - return LSPosedBridge.doHook(method, XposedInterface.PRIORITY_DEFAULT, hooker); - } catch (NoSuchMethodException e) { - throw new HookFailedError(e); - } - } - - @SuppressWarnings("UnusedReturnValue") - public static Set> - hookAllMethods(Class hooker, Class clazz, String methodName) { - var unhooks = new HashSet>(); - for (var method : clazz.getDeclaredMethods()) { - if (method.getName().equals(methodName)) { - unhooks.add(LSPosedBridge.doHook(method, XposedInterface.PRIORITY_DEFAULT, hooker)); - } - } - return unhooks; - } - - @SuppressWarnings("UnusedReturnValue") - public static XposedInterface.MethodUnhooker> - hookConstructor(Class hooker, Class clazz, Class... parameterTypes) { - try { - var constructor = clazz.getDeclaredConstructor(parameterTypes); - return LSPosedBridge.doHook(constructor, XposedInterface.PRIORITY_DEFAULT, hooker); - } catch (NoSuchMethodException e) { - throw new HookFailedError(e); - } - } -} diff --git a/core/src/main/java/org/lsposed/lspd/impl/LSPosedHookCallback.java b/core/src/main/java/org/lsposed/lspd/impl/LSPosedHookCallback.java deleted file mode 100644 index 0caa22df8..000000000 --- a/core/src/main/java/org/lsposed/lspd/impl/LSPosedHookCallback.java +++ /dev/null @@ -1,94 +0,0 @@ -package org.lsposed.lspd.impl; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.lang.reflect.Executable; -import java.lang.reflect.Member; - -import io.github.libxposed.api.XposedInterface; - -public class LSPosedHookCallback implements XposedInterface.BeforeHookCallback, XposedInterface.AfterHookCallback { - - public Member method; - - public Object thisObject; - - public Object[] args; - - public Object result; - - public Throwable throwable; - - public boolean isSkipped; - - public LSPosedHookCallback() { - } - - // Both before and after - - @NonNull - @Override - public Member getMember() { - return this.method; - } - - @Nullable - @Override - public Object getThisObject() { - return this.thisObject; - } - - @NonNull - @Override - public Object[] getArgs() { - return this.args; - } - - // Before - - @Override - public void returnAndSkip(@Nullable Object result) { - this.result = result; - this.throwable = null; - this.isSkipped = true; - } - - @Override - public void throwAndSkip(@Nullable Throwable throwable) { - this.result = null; - this.throwable = throwable; - this.isSkipped = true; - } - - // After - - @Nullable - @Override - public Object getResult() { - return this.result; - } - - @Nullable - @Override - public Throwable getThrowable() { - return this.throwable; - } - - @Override - public boolean isSkipped() { - return this.isSkipped; - } - - @Override - public void setResult(@Nullable Object result) { - this.result = result; - this.throwable = null; - } - - @Override - public void setThrowable(@Nullable Throwable throwable) { - this.result = null; - this.throwable = throwable; - } -} diff --git a/core/src/main/java/org/lsposed/lspd/impl/LSPosedRemotePreferences.java b/core/src/main/java/org/lsposed/lspd/impl/LSPosedRemotePreferences.java deleted file mode 100644 index 5c5d3fa37..000000000 --- a/core/src/main/java/org/lsposed/lspd/impl/LSPosedRemotePreferences.java +++ /dev/null @@ -1,129 +0,0 @@ -package org.lsposed.lspd.impl; - -import android.content.SharedPreferences; -import android.os.Bundle; -import android.os.RemoteException; -import android.util.ArraySet; - -import androidx.annotation.Nullable; - -import org.lsposed.lspd.service.ILSPInjectedModuleService; -import org.lsposed.lspd.service.IRemotePreferenceCallback; - -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; -import java.util.concurrent.ConcurrentHashMap; - -@SuppressWarnings("unchecked") -public class LSPosedRemotePreferences implements SharedPreferences { - - private final Map mMap = new ConcurrentHashMap<>(); - - final HashSet mListeners = new HashSet<>(); - - IRemotePreferenceCallback callback = new IRemotePreferenceCallback.Stub() { - @Override - synchronized public void onUpdate(Bundle bundle) { - Set changes = new ArraySet<>(); - if (bundle.containsKey("delete")) { - var deletes = (Set) bundle.getSerializable("delete"); - changes.addAll(deletes); - for (var key : deletes) { - mMap.remove(key); - } - } - if (bundle.containsKey("put")) { - var puts = (Map) bundle.getSerializable("put"); - mMap.putAll(puts); - changes.addAll(puts.keySet()); - } - synchronized (mListeners) { - for (var key : changes) { - mListeners.forEach(listener -> listener.onSharedPreferenceChanged(LSPosedRemotePreferences.this, key)); - } - } - } - }; - - public LSPosedRemotePreferences(ILSPInjectedModuleService service, String group) throws RemoteException { - Bundle output = service.requestRemotePreferences(group, callback); - if (output.containsKey("map")) { - mMap.putAll((Map) output.getSerializable("map")); - } - } - - @Override - public Map getAll() { - return new TreeMap<>(mMap); - } - - @Nullable - @Override - public String getString(String key, @Nullable String defValue) { - var v = (String) mMap.getOrDefault(key, defValue); - if (v != null) return v; - return defValue; - } - - @Nullable - @Override - public Set getStringSet(String key, @Nullable Set defValues) { - var v = (Set) mMap.getOrDefault(key, defValues); - if (v != null) return v; - return defValues; - } - - @Override - public int getInt(String key, int defValue) { - var v = (Integer) mMap.getOrDefault(key, defValue); - if (v != null) return v; - return defValue; - } - - @Override - public long getLong(String key, long defValue) { - var v = (Long) mMap.getOrDefault(key, defValue); - if (v != null) return v; - return defValue; - } - - @Override - public float getFloat(String key, float defValue) { - var v = (Float) mMap.getOrDefault(key, defValue); - if (v != null) return v; - return defValue; - } - - @Override - public boolean getBoolean(String key, boolean defValue) { - var v = (Boolean) mMap.getOrDefault(key, defValue); - if (v != null) return v; - return defValue; - } - - @Override - public boolean contains(String key) { - return mMap.containsKey(key); - } - - @Override - public Editor edit() { - throw new UnsupportedOperationException("Read only implementation"); - } - - @Override - public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { - synchronized (mListeners) { - mListeners.add(listener); - } - } - - @Override - public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { - synchronized (mListeners) { - mListeners.remove(listener); - } - } -} diff --git a/core/src/main/java/org/lsposed/lspd/util/ClassPathURLStreamHandler.java b/core/src/main/java/org/lsposed/lspd/util/ClassPathURLStreamHandler.java deleted file mode 100644 index 9e4346137..000000000 --- a/core/src/main/java/org/lsposed/lspd/util/ClassPathURLStreamHandler.java +++ /dev/null @@ -1,120 +0,0 @@ -package org.lsposed.lspd.util; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.JarURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLConnection; -import java.util.jar.JarFile; -import java.util.zip.ZipEntry; - -import sun.net.www.ParseUtil; -import sun.net.www.protocol.jar.Handler; - -final class ClassPathURLStreamHandler extends Handler { - private final String fileUri; - private final JarFile jarFile; - - ClassPathURLStreamHandler(String jarFileName) throws IOException { - jarFile = new JarFile(jarFileName); - fileUri = new File(jarFileName).toURI().toString(); - } - - URL getEntryUrlOrNull(String entryName) { - if (jarFile.getEntry(entryName) != null) { - try { - String encodedName = ParseUtil.encodePath(entryName, false); - return new URL("jar", null, -1, fileUri + "!/" + encodedName, this); - } catch (MalformedURLException e) { - throw new RuntimeException("Invalid entry name", e); - } - } - return null; - } - - @Override - protected URLConnection openConnection(URL url) throws IOException { - return new ClassPathURLConnection(url); - } - - @Override - protected void finalize() throws IOException { - jarFile.close(); - } - - private final class ClassPathURLConnection extends JarURLConnection { - private JarFile connectionJarFile = null; - private ZipEntry jarEntry = null; - private InputStream jarInput = null; - private boolean closed = false; - - private ClassPathURLConnection(URL url) throws MalformedURLException { - super(url); - setUseCaches(false); - } - - @Override - public void setUseCaches(boolean usecaches) { - super.setUseCaches(false); - } - - @Override - public void connect() throws IOException { - if (closed) { - throw new IllegalStateException("JarURLConnection has been closed"); - } - if (!connected) { - jarEntry = jarFile.getEntry(getEntryName()); - if (jarEntry == null) { - throw new FileNotFoundException("URL=" + url + ", zipfile=" + jarFile.getName()); - } - connected = true; - } - } - - @Override - public JarFile getJarFile() throws IOException { - connect(); - if (connectionJarFile != null) return connectionJarFile; - return connectionJarFile = new JarFile(jarFile.getName()); - } - - @Override - public InputStream getInputStream() throws IOException { - connect(); - if (jarInput != null) return jarInput; - return jarInput = new FilterInputStream(jarFile.getInputStream(jarEntry)) { - @Override - public void close() throws IOException { - super.close(); - closed = true; - jarFile.close(); - if (connectionJarFile != null) connectionJarFile.close(); - } - }; - } - - @Override - public String getContentType() { - String cType = guessContentTypeFromName(getEntryName()); - if (cType == null) { - cType = "content/unknown"; - } - return cType; - } - - @Override - public int getContentLength() { - try { - connect(); - return (int) getJarEntry().getSize(); - } catch (IOException ignored) { - } - return -1; - } - } -} diff --git a/core/src/main/java/org/lsposed/lspd/util/Hookers.java b/core/src/main/java/org/lsposed/lspd/util/Hookers.java deleted file mode 100644 index 6805e8e9b..000000000 --- a/core/src/main/java/org/lsposed/lspd/util/Hookers.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.util; - -import android.app.ActivityThread; - -public class Hookers { - - public static void logD(String prefix) { - Utils.logD(String.format("%s: pkg=%s, prc=%s", prefix, ActivityThread.currentPackageName(), - ActivityThread.currentProcessName())); - } - - public static void logE(String prefix, Throwable throwable) { - Utils.logE(String.format("%s: pkg=%s, prc=%s", prefix, ActivityThread.currentPackageName(), - ActivityThread.currentProcessName()), throwable); - } - -} diff --git a/core/src/main/java/org/lsposed/lspd/util/LspModuleClassLoader.java b/core/src/main/java/org/lsposed/lspd/util/LspModuleClassLoader.java deleted file mode 100644 index 56f60b7e6..000000000 --- a/core/src/main/java/org/lsposed/lspd/util/LspModuleClassLoader.java +++ /dev/null @@ -1,207 +0,0 @@ -package org.lsposed.lspd.util; - -import static de.robv.android.xposed.XposedBridge.TAG; - -import android.os.Build; -import android.os.SharedMemory; -import android.system.ErrnoException; -import android.system.Os; -import android.system.OsConstants; - -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; - -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Enumeration; -import java.util.List; -import java.util.Objects; -import java.util.jar.JarFile; -import java.util.zip.ZipEntry; - -import org.lsposed.lspd.util.Utils.Log; - -import hidden.ByteBufferDexClassLoader; -import sun.misc.CompoundEnumeration; - -@SuppressWarnings("ConstantConditions") -public final class LspModuleClassLoader extends ByteBufferDexClassLoader { - private static final String zipSeparator = "!/"; - private static final List systemNativeLibraryDirs = - splitPaths(System.getProperty("java.library.path")); - private final String apk; - private final List nativeLibraryDirs = new ArrayList<>(); - - private static List splitPaths(String searchPath) { - var result = new ArrayList(); - if (searchPath == null) return result; - for (var path : searchPath.split(File.pathSeparator)) { - result.add(new File(path)); - } - return result; - } - - private LspModuleClassLoader(ByteBuffer[] dexBuffers, - ClassLoader parent, - String apk) { - super(dexBuffers, parent); - this.apk = apk; - } - - @RequiresApi(Build.VERSION_CODES.Q) - private LspModuleClassLoader(ByteBuffer[] dexBuffers, - String librarySearchPath, - ClassLoader parent, - String apk) { - super(dexBuffers, librarySearchPath, parent); - initNativeLibraryDirs(librarySearchPath); - this.apk = apk; - } - - private void initNativeLibraryDirs(String librarySearchPath) { - nativeLibraryDirs.addAll(splitPaths(librarySearchPath)); - nativeLibraryDirs.addAll(systemNativeLibraryDirs); - } - - @Override - protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { - var cl = findLoadedClass(name); - if (cl != null) { - return cl; - } - try { - return Object.class.getClassLoader().loadClass(name); - } catch (ClassNotFoundException ignored) { - } - ClassNotFoundException fromSuper; - try { - return findClass(name); - } catch (ClassNotFoundException ex) { - fromSuper = ex; - } - try { - return getParent().loadClass(name); - } catch (ClassNotFoundException cnfe) { - throw fromSuper; - } - } - - @Override - public String findLibrary(String libraryName) { - var fileName = System.mapLibraryName(libraryName); - for (var file : nativeLibraryDirs) { - var path = file.getPath(); - if (path.contains(zipSeparator)) { - var split = path.split(zipSeparator, 2); - try (var jarFile = new JarFile(split[0])) { - var entryName = split[1] + '/' + fileName; - var entry = jarFile.getEntry(entryName); - if (entry != null && entry.getMethod() == ZipEntry.STORED) { - return split[0] + zipSeparator + entryName; - } - } catch (IOException e) { - Log.e(TAG, "Can not open " + split[0], e); - } - } else if (file.isDirectory()) { - var entryPath = new File(file, fileName).getPath(); - try { - var fd = Os.open(entryPath, OsConstants.O_RDONLY, 0); - Os.close(fd); - return entryPath; - } catch (ErrnoException ignored) { - } - } - } - return null; - } - - @Override - public String getLdLibraryPath() { - var result = new StringBuilder(); - for (var directory : nativeLibraryDirs) { - if (result.length() > 0) { - result.append(':'); - } - result.append(directory); - } - return result.toString(); - } - - @Override - protected URL findResource(String name) { - try { - var urlHandler = new ClassPathURLStreamHandler(apk); - var url = urlHandler.getEntryUrlOrNull(name); - if (url == null) { - // noinspection FinalizeCalledExplicitly - urlHandler.finalize(); - } - return url; - } catch (IOException e) { - return null; - } - } - - @Override - protected Enumeration findResources(String name) { - var result = new ArrayList(); - var url = findResource(name); - if (url != null) result.add(url); - return Collections.enumeration(result); - } - - @Override - public URL getResource(String name) { - var resource = Object.class.getClassLoader().getResource(name); - if (resource != null) return resource; - resource = findResource(name); - if (resource != null) return resource; - final var cl = getParent(); - return (cl == null) ? null : cl.getResource(name); - } - - @Override - public Enumeration getResources(String name) throws IOException { - @SuppressWarnings("unchecked") final var resources = (Enumeration[]) new Enumeration[]{ - Object.class.getClassLoader().getResources(name), - findResources(name), - getParent() == null ? null : getParent().getResources(name)}; - return new CompoundEnumeration<>(resources); - } - - @NonNull - @Override - public String toString() { - if (apk == null) return "LspModuleClassLoader[instantiating]"; - return "LspModuleClassLoader[module=" + apk + ", " + super.toString() + "]"; - } - - public static ClassLoader loadApk(String apk, - List dexes, - String librarySearchPath, - ClassLoader parent) { - var dexBuffers = dexes.stream().parallel().map(dex -> { - try { - return dex.mapReadOnly(); - } catch (ErrnoException e) { - Log.w(TAG, "Can not map " + dex, e); - return null; - } - }).filter(Objects::nonNull).toArray(ByteBuffer[]::new); - LspModuleClassLoader cl; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - cl = new LspModuleClassLoader(dexBuffers, librarySearchPath, parent, apk); - } else { - cl = new LspModuleClassLoader(dexBuffers, parent, apk); - cl.initNativeLibraryDirs(librarySearchPath); - } - Arrays.stream(dexBuffers).parallel().forEach(SharedMemory::unmap); - dexes.stream().parallel().forEach(SharedMemory::close); - return cl; - } -} diff --git a/core/src/main/java/org/lsposed/lspd/util/MetaDataReader.java b/core/src/main/java/org/lsposed/lspd/util/MetaDataReader.java deleted file mode 100644 index 8a9400ff1..000000000 --- a/core/src/main/java/org/lsposed/lspd/util/MetaDataReader.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.util; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.Map; -import java.util.jar.JarFile; - -import pxb.android.axml.AxmlReader; -import pxb.android.axml.AxmlVisitor; -import pxb.android.axml.NodeVisitor; - -public class MetaDataReader { - private final HashMap metaData = new HashMap<>(); - - public static Map getMetaData(File apk) throws IOException { - return new MetaDataReader(apk).metaData; - } - - private MetaDataReader(File apk) throws IOException { - try (JarFile zip = new JarFile(apk); - var is = zip.getInputStream(zip.getEntry("AndroidManifest.xml"))) { - var reader = new AxmlReader(getBytesFromInputStream(is)); - reader.accept(new AxmlVisitor() { - @Override - public NodeVisitor child(String ns, String name) { - NodeVisitor child = super.child(ns, name); - return new ManifestTagVisitor(child); - } - }); - } - } - - public static byte[] getBytesFromInputStream(InputStream inputStream) throws IOException { - try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { - byte[] b = new byte[1024]; - int n; - while ((n = inputStream.read(b)) != -1) { - bos.write(b, 0, n); - } - return bos.toByteArray(); - } - } - - private class ManifestTagVisitor extends NodeVisitor { - public ManifestTagVisitor(NodeVisitor child) { - super(child); - } - - @Override - public NodeVisitor child(String ns, String name) { - NodeVisitor child = super.child(ns, name); - if ("application".equals(name)) { - return new ApplicationTagVisitor(child); - } - return child; - } - - private class ApplicationTagVisitor extends NodeVisitor { - public ApplicationTagVisitor(NodeVisitor child) { - super(child); - } - - @Override - public NodeVisitor child(String ns, String name) { - NodeVisitor child = super.child(ns, name); - if ("meta-data".equals(name)) { - return new MetaDataVisitor(child); - } - return child; - } - } - } - - private class MetaDataVisitor extends NodeVisitor { - public String name = null; - public Object value = null; - - public MetaDataVisitor(NodeVisitor child) { - super(child); - } - - @Override - public void attr(String ns, String name, int resourceId, int type, Object obj) { - if (type == 3 && "name".equals(name)) { - this.name = (String) obj; - } - if ("value".equals(name)) { - value = obj; - } - super.attr(ns, name, resourceId, type, obj); - } - - @Override - public void end() { - if (name != null && value != null) { - metaData.put(name, value); - } - super.end(); - } - } - - public static int extractIntPart(String str) { - int result = 0, length = str.length(); - for (int offset = 0; offset < length; offset++) { - char c = str.charAt(offset); - if ('0' <= c && c <= '9') - result = result * 10 + (c - '0'); - else - break; - } - return result; - } -} diff --git a/core/src/main/java/org/matrix/vector/Startup.java b/core/src/main/java/org/matrix/vector/Startup.java new file mode 100644 index 000000000..834ee7c18 --- /dev/null +++ b/core/src/main/java/org/matrix/vector/Startup.java @@ -0,0 +1,33 @@ +package org.matrix.vector; + +import org.lsposed.lspd.service.ILSPApplicationService; +import org.lsposed.lspd.util.Utils; +import org.matrix.vector.impl.core.VectorStartup; +import org.matrix.vector.impl.di.VectorBootstrap; + +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedInit; + +public class Startup { + + public static void bootstrapXposed(boolean systemServerStarted) { + try { + VectorStartup.bootstrap(XposedInit.startsSystemServer, systemServerStarted); + XposedInit.loadLegacyModules(); + } catch (Throwable t) { + Utils.logE("Error during framework initialization", t); + } + } + + public static void initXposed(boolean isSystem, String processName, String appDir, ILSPApplicationService service) { + // 1. Establish the Dependency Injection contract + VectorBootstrap.INSTANCE.init(new LegacyDelegateImpl()); + + // 2. Initialize legacy resources and state + XposedBridge.initXResources(); + XposedInit.startsSystemServer = isSystem; + + // 3. Hand off execution to the modern framework initialization + VectorStartup.init(isSystem, processName, appDir, service); + } +} diff --git a/core/src/main/java/org/matrix/vector/legacy/LegacyDelegateImpl.java b/core/src/main/java/org/matrix/vector/legacy/LegacyDelegateImpl.java new file mode 100644 index 000000000..9a12d4895 --- /dev/null +++ b/core/src/main/java/org/matrix/vector/legacy/LegacyDelegateImpl.java @@ -0,0 +1,139 @@ +package org.matrix.vector.legacy; + +import android.content.res.XResources; + +import org.lsposed.lspd.util.MetaDataReader; +import org.lsposed.lspd.util.Utils; +import org.matrix.vector.impl.core.VectorServiceClient; +import org.matrix.vector.impl.di.LegacyFrameworkDelegate; +import org.matrix.vector.impl.di.LegacyPackageInfo; +import org.matrix.vector.impl.di.OriginalInvoker; +import org.matrix.vector.impl.hooks.VectorLegacyCallback; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Executable; +import java.util.Map; + +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XC_MethodReplacement; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedHelpers; +import de.robv.android.xposed.XposedInit; +import de.robv.android.xposed.callbacks.XC_LoadPackage; + +/** + * Implementation of the explicit dependency injection contract. + * Translates modern lifecycle events and hooks into legacy Xposed API operations. + */ +public class LegacyDelegateImpl implements LegacyFrameworkDelegate { + + @Override + public void loadModules(Object activityThread) { + XposedInit.loadModules((android.app.ActivityThread) activityThread); + } + + @Override + public void onPackageLoaded(LegacyPackageInfo info) { + XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam(XposedBridge.sLoadedPackageCallbacks); + lpparam.packageName = info.getPackageName(); + lpparam.processName = info.getProcessName(); + lpparam.classLoader = info.getClassLoader(); + lpparam.appInfo = info.getAppInfo(); + lpparam.isFirstApplication = info.isFirstApplication(); + + if (info.isFirstApplication() && hasLegacyModule(info.getPackageName())) { + hookNewXSP(lpparam); + } + + XC_LoadPackage.callAll(lpparam); + } + + @Override + public void onSystemServerLoaded(ClassLoader classLoader) { + XposedInit.loadedPackagesInProcess.add("android"); + XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam(XposedBridge.sLoadedPackageCallbacks); + lpparam.packageName = "android"; + lpparam.processName = "android"; + lpparam.classLoader = classLoader; + lpparam.isFirstApplication = true; + XC_LoadPackage.callAll(lpparam); + } + + @Override + public Object processLegacyHook(Executable executable, Object thisObject, Object[] args, Object[] legacyHooks, OriginalInvoker invokeOriginal) { + VectorLegacyCallback callback = new VectorLegacyCallback<>(executable, thisObject, args); + XposedBridge.LegacyApiSupport legacy = new XposedBridge.LegacyApiSupport<>(callback, legacyHooks); + + legacy.handleBefore(); + + if (!callback.isSkipped()) { + try { + Object result = invokeOriginal.invoke(); + callback.setResult(result); + } catch (Throwable t) { + callback.setThrowable(t); + } + } + + legacy.handleAfter(); + + if (callback.getThrowable() != null) { + sneakyThrow(callback.getThrowable()); + } + return callback.getResult(); + } + + @Override + public boolean isResourceHookingDisabled() { + return XposedInit.disableResources; + } + + @Override + public boolean hasLegacyModule(String packageName) { + return XposedInit.getLoadedModules().containsKey(packageName); + } + + @Override + public void setPackageNameForResDir(String packageName, String resDir) { + XResources.setPackageNameForResDir(packageName, resDir); + } + + private void hookNewXSP(XC_LoadPackage.LoadPackageParam lpparam) { + int xposedminversion = -1; + boolean xposedsharedprefs = false; + try { + Map metaData = MetaDataReader.getMetaData(new File(lpparam.appInfo.sourceDir)); + Object minVersionRaw = metaData.get("xposedminversion"); + if (minVersionRaw instanceof Integer) { + xposedminversion = (Integer) minVersionRaw; + } else if (minVersionRaw instanceof String) { + xposedminversion = MetaDataReader.extractIntPart((String) minVersionRaw); + } + xposedsharedprefs = metaData.containsKey("xposedsharedprefs"); + } catch (NumberFormatException | IOException ignored) { + } + + if (xposedminversion > 92 || xposedsharedprefs) { + XposedHelpers.findAndHookMethod("android.app.ContextImpl", lpparam.classLoader, "checkMode", int.class, new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + if (((int) param.args[0] & 1) != 0) { + param.setThrowable(null); + } + } + }); + XposedHelpers.findAndHookMethod("android.app.ContextImpl", lpparam.classLoader, "getPreferencesDir", new XC_MethodReplacement() { + @Override + protected Object replaceHookedMethod(MethodHookParam param) { + return new File(VectorServiceClient.INSTANCE.getPrefsPath(lpparam.packageName)); + } + }); + } + } + + @SuppressWarnings("unchecked") + private static void sneakyThrow(Throwable t) throws T { + throw (T) t; + } +} From 0e4670981e427e73f8e2db4172b9cdcb1db07943 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 22 Mar 2026 21:18:13 +0100 Subject: [PATCH 04/34] Fix VectorContext implements --- .../vector/impl/{hooks => }/VectorContext.kt | 0 .../vector/impl/core/VectorModuleManager.kt | 128 ++++++++++++++++++ 2 files changed, 128 insertions(+) rename xposed/src/main/kotlin/org/matrix/vector/impl/{hooks => }/VectorContext.kt (100%) create mode 100644 xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorContext.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorContext.kt similarity index 100% rename from xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorContext.kt rename to xposed/src/main/kotlin/org/matrix/vector/impl/VectorContext.kt diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt new file mode 100644 index 000000000..1322e7c8b --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt @@ -0,0 +1,128 @@ +package org.matrix.vector.impl.core + +import android.os.Build +import android.os.Process +import io.github.libxposed.api.XposedInterface +import io.github.libxposed.api.XposedModule +import io.github.libxposed.api.XposedModuleInterface.ModuleLoadedParam +import java.io.File +import org.matrix.vector.impl.VectorContext +import org.matrix.vector.impl.VectorLifecycleManager +import org.matrix.vector.impl.util.VectorModuleClassLoader +import org.matrix.vector.lspd.models.Module +import org.matrix.vector.lspd.util.Utils.Log +import org.matrix.vector.nativebridge.NativeAPI + +/** + * Responsible for loading modern (API 101) modules into the target process. Handles ClassLoader + * isolation and injects the framework context into the module instances. + */ +object VectorModuleManager { + + private const val TAG = "VectorModuleManager" + + /** + * Loads a module APK, instantiates its entry classes, and binds them to the Vector framework. + */ + fun loadModule(module: Module, isSystemServer: Boolean, processName: String): Boolean { + try { + Log.d(TAG, "Loading module ${module.packageName}") + + // 1. Construct the native library search path + val librarySearchPath = buildString { + val abis = + if (Process.is64Bit()) Build.SUPPORTED_64_BIT_ABIS + else Build.SUPPORTED_32_BIT_ABIS + for (abi in abis) { + append(module.apkPath).append("!/lib/").append(abi).append(File.pathSeparator) + } + } + + // 2. Create the isolated ClassLoader for the module + val initLoader = XposedModule::class.java.classLoader + val moduleClassLoader = + VectorModuleClassLoader.loadApk( + module.apkPath, + module.file.preLoadedDexes, + librarySearchPath, + initLoader, + ) + + // Security/Integrity Check: Ensure the module isn't bundling its own API classes + if ( + moduleClassLoader.loadClass(XposedModule::class.java.name).classLoader !== + initLoader + ) { + Log.e(TAG, " Cannot load module: ${module.packageName}") + Log.e( + TAG, + " The Xposed API classes are compiled into the module's APK (implementation instead of compileOnly).", + ) + return false + } + + // 3. Create the Context that will be injected into the module + val vectorContext = + VectorContext( + packageName = module.packageName, + applicationInfo = module.applicationInfo, + service = VectorServiceClient, // Our IPC client + ) + + // 4. Instantiate the module entry classes + for (className in module.file.moduleClassNames) { + runCatching { + val moduleClass = moduleClassLoader.loadClass(className) + Log.d(TAG, " Loading class $moduleClass") + + if (!XposedModule::class.java.isAssignableFrom(moduleClass)) { + Log.e(TAG, " Class does not extend XposedModule, skipping.") + return@runCatching + } + + // Look for the constructor required by API 101 + val constructor = + moduleClass.getConstructor( + XposedInterface::class.java, + ModuleLoadedParam::class.java, + ) + + // Inject the Context and the loaded parameters! + val moduleInstance = + constructor.newInstance( + vectorContext, + object : ModuleLoadedParam { + override fun isSystemServer(): Boolean = isSystemServer + + override fun getProcessName(): String = processName + }, + ) as XposedModule + + // Register the active module to receive future lifecycle events + VectorLifecycleManager.activeModules.add(moduleInstance) + + // Trigger the initial onModuleLoaded callback + moduleInstance.onModuleLoaded( + object : ModuleLoadedParam { + override fun isSystemServer(): Boolean = isSystemServer + + override fun getProcessName(): String = processName + } + ) + } + .onFailure { e -> Log.e(TAG, " Failed to instantiate class $className", e) } + } + + // 5. Register any native JNI entrypoints declared by the module + module.file.moduleLibraryNames.forEach { libraryName -> + NativeAPI.recordNativeEntrypoint(libraryName) + } + + Log.d(TAG, "Loaded module ${module.packageName} successfully.") + return true + } catch (e: Throwable) { + Log.e(TAG, "Fatal error loading module ${module.packageName}", e) + return false + } + } +} From 05f29a713afadfca5421e1b27f8d0d4f519441f9 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 22 Mar 2026 22:29:47 +0100 Subject: [PATCH 05/34] Rename classes to compile core module --- .../android/xposed/XSharedPreferences.java | 13 +++-- .../de/robv/android/xposed/XposedBridge.java | 49 +++++++++---------- .../de/robv/android/xposed/XposedInit.java | 18 +++---- .../android/xposed/callbacks/XCallback.java | 4 +- .../main/java/org/matrix/vector/Startup.java | 1 + .../vector/legacy/LegacyDelegateImpl.java | 6 +-- .../matrix/vector/impl/core/VectorDeopter.kt | 9 ++-- .../vector/impl/core/VectorModuleManager.kt | 8 +-- .../matrix/vector/impl/di/VectorBootstrap.kt | 31 ------------ .../vector/impl/hooks/VectorLegacyCallback.kt | 34 +++++++++++++ .../vector/impl/utils/VectorMetaDataReader.kt | 2 +- .../impl/utils/VectorModuleClassLoader.kt | 2 +- .../impl/utils/VectorURLStreamHandler.kt | 2 +- 13 files changed, 92 insertions(+), 87 deletions(-) create mode 100644 xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorLegacyCallback.kt diff --git a/core/src/main/java/de/robv/android/xposed/XSharedPreferences.java b/core/src/main/java/de/robv/android/xposed/XSharedPreferences.java index 3f645932c..0a0b83837 100644 --- a/core/src/main/java/de/robv/android/xposed/XSharedPreferences.java +++ b/core/src/main/java/de/robv/android/xposed/XSharedPreferences.java @@ -20,8 +20,6 @@ package de.robv.android.xposed; -import static org.lsposed.lspd.core.ApplicationServiceClient.serviceClient; - import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; @@ -31,8 +29,9 @@ import com.android.internal.util.XmlUtils; import org.lsposed.lspd.core.BuildConfig; -import org.lsposed.lspd.util.MetaDataReader; import org.lsposed.lspd.util.Utils.Log; +import org.matrix.vector.impl.core.VectorServiceClient; +import org.matrix.vector.impl.utils.VectorMetaDataReader; import org.xmlpull.v1.XmlPullParserException; import java.io.File; @@ -167,14 +166,14 @@ public XSharedPreferences(String packageName, String prefFileName) { int xposedminversion = -1; boolean xposedsharedprefs = false; try { - Map metaData = MetaDataReader.getMetaData(new File(m.get())); + Map metaData = VectorMetaDataReader.getMetaData(new File(m.get())); isModule = metaData.containsKey("xposedminversion"); if (isModule) { Object minVersionRaw = metaData.get("xposedminversion"); if (minVersionRaw instanceof Integer) { xposedminversion = (Integer) minVersionRaw; } else if (minVersionRaw instanceof String) { - xposedminversion = MetaDataReader.extractIntPart((String) minVersionRaw); + xposedminversion = VectorMetaDataReader.extractIntPart((String) minVersionRaw); } xposedsharedprefs = metaData.containsKey("xposedsharedprefs"); } @@ -184,7 +183,7 @@ public XSharedPreferences(String packageName, String prefFileName) { newModule = isModule && (xposedminversion > 92 || xposedsharedprefs); } if (newModule) { - mFile = new File(serviceClient.getPrefsPath(packageName), prefFileName + ".xml"); + mFile = new File(VectorServiceClient.INSTANCE.getPrefsPath(packageName), prefFileName + ".xml"); } else { mFile = new File(Environment.getDataDirectory(), "data/" + packageName + "/shared_prefs/" + prefFileName + ".xml"); } @@ -201,7 +200,7 @@ private void tryRegisterWatcher() { Path path = mFile.toPath(); try { if (sWatcher == null) { - sWatcher = new File(serviceClient.getPrefsPath("")).toPath().getFileSystem().newWatchService(); + sWatcher = new File(VectorServiceClient.INSTANCE.getPrefsPath("")).toPath().getFileSystem().newWatchService(); if (BuildConfig.DEBUG) Log.d(TAG, "Created WatchService instance"); } mWatchKey = path.getParent().register(sWatcher, StandardWatchEventKinds.ENTRY_CREATE, diff --git a/core/src/main/java/de/robv/android/xposed/XposedBridge.java b/core/src/main/java/de/robv/android/xposed/XposedBridge.java index 95f8b9ef6..b6555874e 100644 --- a/core/src/main/java/de/robv/android/xposed/XposedBridge.java +++ b/core/src/main/java/de/robv/android/xposed/XposedBridge.java @@ -25,8 +25,8 @@ import android.content.res.TypedArray; import android.util.Log; -import org.lsposed.lspd.impl.LSPosedBridge; -import org.lsposed.lspd.impl.LSPosedHookCallback; +import org.matrix.vector.impl.hooks.VectorNativeHooker; +import org.matrix.vector.impl.hooks.VectorLegacyCallback; import org.matrix.vector.nativebridge.HookBridge; import org.matrix.vector.nativebridge.ResourcesHook; @@ -133,7 +133,7 @@ public static void initXResources() { * Returns the currently installed version of the Xposed framework. */ public static int getXposedVersion() { - return XposedInterface.API; + return XposedInterface.LIB_API; } /** @@ -207,7 +207,7 @@ public static XC_MethodHook.Unhook hookMethod(Member hookMethod, XC_MethodHook c throw new IllegalArgumentException("callback should not be null!"); } - if (!HookBridge.hookMethod(false, (Executable) hookMethod, LSPosedBridge.NativeHooker.class, callback.priority, callback)) { + if (!HookBridge.hookMethod(false, (Executable) hookMethod, VectorNativeHooker.class, callback.priority, callback)) { log("Failed to hook " + hookMethod); return null; } @@ -385,12 +385,12 @@ public synchronized void clear() { public static class LegacyApiSupport { private final XC_MethodHook.MethodHookParam param; - private final LSPosedHookCallback callback; + private final VectorLegacyCallback callback; private final Object[] snapshot; private int beforeIdx; - public LegacyApiSupport(LSPosedHookCallback callback, Object[] legacySnapshot) { + public LegacyApiSupport(VectorLegacyCallback callback, Object[] legacySnapshot) { this.param = new XC_MethodHook.MethodHookParam<>(); this.callback = callback; this.snapshot = legacySnapshot; @@ -404,15 +404,11 @@ public void handleBefore() { cb.beforeHookedMethod(param); } catch (Throwable t) { XposedBridge.log(t); - - // reset result (ignoring what the unexpectedly exiting callback did) param.setResult(null); param.returnEarly = false; - continue; } if (param.returnEarly) { - // skip remaining "before" callbacks and corresponding "after" callbacks beforeIdx++; break; } @@ -430,8 +426,6 @@ public void handleAfter() { cb.afterHookedMethod(param); } catch (Throwable t) { XposedBridge.log(t); - - // reset to last result (ignoring what the unexpectedly exiting callback did) if (lastThrowable == null) { param.setResult(lastResult); } else { @@ -442,21 +436,26 @@ public void handleAfter() { syncronizeApi(param, callback, false); } - private void syncronizeApi(XC_MethodHook.MethodHookParam param, LSPosedHookCallback callback, boolean forward) { + private void syncronizeApi(XC_MethodHook.MethodHookParam param, VectorLegacyCallback callback, boolean forward) { if (forward) { - param.method = callback.method; - param.thisObject = callback.thisObject; - param.args = callback.args; - param.result = callback.result; - param.throwable = callback.throwable; - param.returnEarly = callback.isSkipped; + param.method = callback.getMethod(); + param.thisObject = callback.getThisObject(); + param.args = callback.getArgs(); + param.result = callback.getResult(); + param.throwable = callback.getThrowable(); + param.returnEarly = callback.isSkipped(); } else { - callback.method = param.method; - callback.thisObject = param.thisObject; - callback.args = param.args; - callback.result = param.result; - callback.throwable = param.throwable; - callback.isSkipped = param.returnEarly; + callback.setThisObject(param.thisObject); + callback.setArgs(param.args); + + // Only write the result/throwable back if the legacy module explicitly skipped execution + if (param.returnEarly) { + if (param.throwable != null) { + callback.setThrowable(param.throwable); + } else { + callback.setResult(param.result); + } + } } } } diff --git a/core/src/main/java/de/robv/android/xposed/XposedInit.java b/core/src/main/java/de/robv/android/xposed/XposedInit.java index 39bbe4be5..2c924ba82 100644 --- a/core/src/main/java/de/robv/android/xposed/XposedInit.java +++ b/core/src/main/java/de/robv/android/xposed/XposedInit.java @@ -20,8 +20,6 @@ package de.robv.android.xposed; -import static org.lsposed.lspd.core.ApplicationServiceClient.serviceClient; -import static org.lsposed.lspd.deopt.PrebuiltMethodsDeopter.deoptResourceMethods; import static de.robv.android.xposed.XposedBridge.hookAllMethods; import static de.robv.android.xposed.XposedHelpers.callMethod; import static de.robv.android.xposed.XposedHelpers.findAndHookMethod; @@ -40,11 +38,13 @@ import android.os.Process; import android.util.ArrayMap; -import org.lsposed.lspd.impl.LSPosedContext; import org.lsposed.lspd.models.PreLoadedApk; +import org.matrix.vector.impl.core.VectorDeopter; +import org.matrix.vector.impl.core.VectorModuleManager; +import org.matrix.vector.impl.core.VectorServiceClient; +import org.matrix.vector.impl.utils.VectorModuleClassLoader; import org.matrix.vector.nativebridge.NativeAPI; import org.matrix.vector.nativebridge.ResourcesHook; -import org.lsposed.lspd.util.LspModuleClassLoader; import org.lsposed.lspd.util.Utils.Log; import java.io.File; @@ -74,7 +74,7 @@ public static void hookResources() throws Throwable { return; } - deoptResourceMethods(); + VectorDeopter.deoptResourceMethods(); if (!ResourcesHook.initXResourcesNative()) { Log.e(TAG, "Cannot hook resources"); @@ -224,7 +224,7 @@ public static Map> getLoadedModules() { } public static void loadLegacyModules() { - var moduleList = serviceClient.getLegacyModulesList(); + var moduleList = VectorServiceClient.INSTANCE.getLegacyModulesList(); moduleList.forEach(module -> { var apk = module.apkPath; var name = module.packageName; @@ -238,9 +238,9 @@ public static void loadLegacyModules() { public static void loadModules(ActivityThread at) { var packages = (ArrayMap) XposedHelpers.getObjectField(at, "mPackages"); - serviceClient.getModulesList().forEach(module -> { + VectorServiceClient.INSTANCE.getModulesList().forEach(module -> { loadedModules.put(module.packageName, Optional.empty()); - if (!LSPosedContext.loadModule(at, module)) { + if (!VectorModuleManager.INSTANCE.loadModule(module, startsSystemServer, VectorServiceClient.INSTANCE.getProcessName())) { loadedModules.remove(module.packageName); } else { packages.remove(module.packageName); @@ -311,7 +311,7 @@ private static boolean loadModule(String name, String apk, PreLoadedApk file) { var librarySearchPath = sb.toString(); var initLoader = XposedInit.class.getClassLoader(); - var mcl = LspModuleClassLoader.loadApk(apk, file.preLoadedDexes, librarySearchPath, initLoader); + var mcl = VectorModuleClassLoader.loadApk(apk, file.preLoadedDexes, librarySearchPath, initLoader); try { if (mcl.loadClass(XposedBridge.class.getName()).getClassLoader() != initLoader) { diff --git a/core/src/main/java/de/robv/android/xposed/callbacks/XCallback.java b/core/src/main/java/de/robv/android/xposed/callbacks/XCallback.java index 13958a65d..81f4fbe62 100644 --- a/core/src/main/java/de/robv/android/xposed/callbacks/XCallback.java +++ b/core/src/main/java/de/robv/android/xposed/callbacks/XCallback.java @@ -22,7 +22,7 @@ import android.os.Bundle; -import org.lsposed.lspd.deopt.PrebuiltMethodsDeopter; +import org.matrix.vector.impl.core.VectorDeopter; import java.io.Serializable; @@ -137,7 +137,7 @@ public static void callAll(Param param) { // deopt methods in system apps or priv-apps, this would be not necessary // only if we found out how to recompile their apks XC_LoadPackage.LoadPackageParam lpp = (XC_LoadPackage.LoadPackageParam) param; - PrebuiltMethodsDeopter.deoptMethods(lpp.packageName, lpp.classLoader); + VectorDeopter.deoptMethods(lpp.packageName, lpp.classLoader); } if (param.callbacks == null) diff --git a/core/src/main/java/org/matrix/vector/Startup.java b/core/src/main/java/org/matrix/vector/Startup.java index 834ee7c18..b1b1619d1 100644 --- a/core/src/main/java/org/matrix/vector/Startup.java +++ b/core/src/main/java/org/matrix/vector/Startup.java @@ -4,6 +4,7 @@ import org.lsposed.lspd.util.Utils; import org.matrix.vector.impl.core.VectorStartup; import org.matrix.vector.impl.di.VectorBootstrap; +import org.matrix.vector.legacy.LegacyDelegateImpl; import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.XposedInit; diff --git a/core/src/main/java/org/matrix/vector/legacy/LegacyDelegateImpl.java b/core/src/main/java/org/matrix/vector/legacy/LegacyDelegateImpl.java index 9a12d4895..5b4277f29 100644 --- a/core/src/main/java/org/matrix/vector/legacy/LegacyDelegateImpl.java +++ b/core/src/main/java/org/matrix/vector/legacy/LegacyDelegateImpl.java @@ -2,13 +2,13 @@ import android.content.res.XResources; -import org.lsposed.lspd.util.MetaDataReader; import org.lsposed.lspd.util.Utils; import org.matrix.vector.impl.core.VectorServiceClient; import org.matrix.vector.impl.di.LegacyFrameworkDelegate; import org.matrix.vector.impl.di.LegacyPackageInfo; import org.matrix.vector.impl.di.OriginalInvoker; import org.matrix.vector.impl.hooks.VectorLegacyCallback; +import org.matrix.vector.impl.utils.VectorMetaDataReader; import java.io.File; import java.io.IOException; @@ -103,12 +103,12 @@ private void hookNewXSP(XC_LoadPackage.LoadPackageParam lpparam) { int xposedminversion = -1; boolean xposedsharedprefs = false; try { - Map metaData = MetaDataReader.getMetaData(new File(lpparam.appInfo.sourceDir)); + Map metaData = VectorMetaDataReader.getMetaData(new File(lpparam.appInfo.sourceDir)); Object minVersionRaw = metaData.get("xposedminversion"); if (minVersionRaw instanceof Integer) { xposedminversion = (Integer) minVersionRaw; } else if (minVersionRaw instanceof String) { - xposedminversion = MetaDataReader.extractIntPart((String) minVersionRaw); + xposedminversion = VectorMetaDataReader.extractIntPart((String) minVersionRaw); } xposedsharedprefs = metaData.containsKey("xposedsharedprefs"); } catch (NumberFormatException | IOException ignored) { diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorDeopter.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorDeopter.kt index 8f6fa90ba..0b8e4bf8f 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorDeopter.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorDeopter.kt @@ -1,6 +1,7 @@ package org.matrix.vector.impl.core import java.lang.reflect.Executable +import org.lsposed.lspd.util.Utils import org.matrix.vector.nativebridge.HookBridge /** @@ -9,7 +10,8 @@ import org.matrix.vector.nativebridge.HookBridge */ object VectorDeopter { - private fun deoptMethods(where: String, cl: ClassLoader?) { + @JvmStatic + fun deoptMethods(where: String, cl: ClassLoader?) { val targets = VectorInlinedCallers.get(where) if (targets.isEmpty()) return @@ -39,8 +41,9 @@ object VectorDeopter { deoptMethods(VectorInlinedCallers.KEY_BOOT_IMAGE, null) } - fun deoptResourceMethods(isMiui: Boolean) { - if (isMiui) { + @JvmStatic + fun deoptResourceMethods() { + if (Utils.isMIUI) { deoptMethods(VectorInlinedCallers.KEY_BOOT_IMAGE_MIUI_RES, null) } } diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt index 1322e7c8b..96e9178f9 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt @@ -6,11 +6,11 @@ import io.github.libxposed.api.XposedInterface import io.github.libxposed.api.XposedModule import io.github.libxposed.api.XposedModuleInterface.ModuleLoadedParam import java.io.File +import org.lsposed.lspd.models.Module +import org.lsposed.lspd.util.Utils.Log import org.matrix.vector.impl.VectorContext import org.matrix.vector.impl.VectorLifecycleManager -import org.matrix.vector.impl.util.VectorModuleClassLoader -import org.matrix.vector.lspd.models.Module -import org.matrix.vector.lspd.util.Utils.Log +import org.matrix.vector.impl.utils.VectorModuleClassLoader import org.matrix.vector.nativebridge.NativeAPI /** @@ -66,7 +66,7 @@ object VectorModuleManager { VectorContext( packageName = module.packageName, applicationInfo = module.applicationInfo, - service = VectorServiceClient, // Our IPC client + service = module.service, // Our IPC client ) // 4. Instantiate the module entry classes diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/di/VectorBootstrap.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/di/VectorBootstrap.kt index c63d26514..ef1e0fc8c 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/di/VectorBootstrap.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/di/VectorBootstrap.kt @@ -12,37 +12,6 @@ data class LegacyPackageInfo( val isFirstApplication: Boolean, ) -/** - * Adapter for backward compatibility with [XposedBridge.LegacyApiSupport]. Contains state mutations - * strictly for legacy module support. - */ -class VectorLegacyCallback( - val method: T, - var thisObject: Any?, - var args: Array, -) { - var result: Any? = null - private set - - var throwable: Throwable? = null - private set - - var isSkipped = false - private set - - fun setResult(res: Any?) { - result = res - throwable = null - isSkipped = true - } - - fun setThrowable(t: Throwable?) { - result = null - throwable = t - isSkipped = true - } -} - /** Functional interface for executing the original method within a legacy hook bypass. */ fun interface OriginalInvoker { fun invoke(): Any? diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorLegacyCallback.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorLegacyCallback.kt new file mode 100644 index 000000000..73366a4a5 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorLegacyCallback.kt @@ -0,0 +1,34 @@ +package org.matrix.vector.impl.hooks + +import java.lang.reflect.Executable + +/** + * Adapter for backward compatibility with [XposedBridge.LegacyApiSupport]. Contains state mutations + * strictly for legacy module support. + */ +class VectorLegacyCallback( + val method: T, + var thisObject: Any?, + var args: Array, +) { + var result: Any? = null + private set + + var throwable: Throwable? = null + private set + + var isSkipped = false + private set + + fun setResult(res: Any?) { + result = res + throwable = null + isSkipped = true + } + + fun setThrowable(t: Throwable?) { + result = null + throwable = t + isSkipped = true + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorMetaDataReader.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorMetaDataReader.kt index d4489a87d..3379bc8f5 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorMetaDataReader.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorMetaDataReader.kt @@ -1,4 +1,4 @@ -package org.matrix.vector.impl.util +package org.matrix.vector.impl.utils import java.io.ByteArrayOutputStream import java.io.File diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorModuleClassLoader.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorModuleClassLoader.kt index 767342c20..63a2a9106 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorModuleClassLoader.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorModuleClassLoader.kt @@ -1,4 +1,4 @@ -package org.matrix.vector.impl.util +package org.matrix.vector.impl.utils import android.os.Build import android.os.SharedMemory diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorURLStreamHandler.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorURLStreamHandler.kt index 39ce09ad9..6fa51a304 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorURLStreamHandler.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorURLStreamHandler.kt @@ -1,4 +1,4 @@ -package org.matrix.vector.impl.util +package org.matrix.vector.impl.utils import android.net.Uri import java.io.File From e80a908ffdb579e205f3f8cdd219ca4388f15090 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 22 Mar 2026 23:26:10 +0100 Subject: [PATCH 06/34] Adapt changes into zygisk module --- .../impl/hookers/SystemServerHookers.kt | 7 ++ .../matrix/vector/ParasiticManagerHooker.kt | 34 ++++-- .../vector/ParasiticManagerSystemHooker.kt | 108 +++++++++--------- .../kotlin/org/matrix/vector/core/Main.kt | 6 +- 4 files changed, 86 insertions(+), 69 deletions(-) diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/SystemServerHookers.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/SystemServerHookers.kt index 97b275a1d..c890da790 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/SystemServerHookers.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/SystemServerHookers.kt @@ -12,6 +12,12 @@ import org.matrix.vector.impl.hooks.VectorHookBuilder */ object HandleSystemServerProcessHooker : XposedInterface.Hooker { + interface Callback { + fun onSystemServerLoaded(classLoader: ClassLoader) + } + + @Volatile var callback: Callback? = null + @Volatile var systemServerCL: ClassLoader? = null private set @@ -38,6 +44,7 @@ object HandleSystemServerProcessHooker : XposedInterface.Hooker { val startMethod = sysServerClass.getDeclaredMethod("startBootstrapServices") VectorHookBuilder(startMethod).intercept(StartBootstrapServicesHooker) + callback?.onSystemServerLoaded(classLoader) } } diff --git a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt index 5c06706de..819d901a4 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt @@ -23,9 +23,8 @@ import java.io.FileOutputStream import java.lang.reflect.Method import java.util.concurrent.ConcurrentHashMap import org.lsposed.lspd.ILSPManagerService -import org.lsposed.lspd.core.ApplicationServiceClient.serviceClient -import org.lsposed.lspd.util.Hookers import org.lsposed.lspd.util.Utils +import org.matrix.vector.impl.core.VectorServiceClient /** The "Parasite" logic. Injects the LSPosed Manager APK into a host process (shell). */ @SuppressLint("StaticFieldLeak") @@ -39,6 +38,19 @@ object ParasiticManagerHooker { private val states = ConcurrentHashMap() private val persistentStates = ConcurrentHashMap() + private fun logD(msg: String) { + Utils.logD( + "ParasiticHooker: pkg=${ActivityThread.currentPackageName()}, prc=${ActivityThread.currentProcessName()} - $msg" + ) + } + + private fun logE(msg: String, t: Throwable) { + Utils.logE( + "ParasiticHooker: pkg=${ActivityThread.currentPackageName()}, prc=${ActivityThread.currentProcessName()} - $msg", + t, + ) + } + /** Constructs a hybrid PackageInfo. Combines the Manager's code with the Host's environment. */ @Synchronized private fun getManagerPkgInfo(appInfo: ApplicationInfo?): PackageInfo? { @@ -64,7 +76,7 @@ object ParasiticManagerHooker { } sourcePath = dstPath } - .onFailure { Hookers.logE("Failed to copy parasitic APK", it) } + .onFailure { logE("Failed to copy parasitic APK", it) } } val pkgInfo = @@ -140,7 +152,7 @@ object ParasiticManagerHooker { "android.app.ActivityThread\$AppBindData", object : XC_MethodHook() { override fun beforeHookedMethod(param: MethodHookParam<*>) { - Hookers.logD("ActivityThread#handleBindApplication() starts") + logD("ActivityThread#handleBindApplication() starts") val bindData = param.args[0] val hostAppInfo = XposedHelpers.getObjectField(bindData, "appInfo") as ApplicationInfo @@ -165,7 +177,7 @@ object ParasiticManagerHooker { val dexPath = managerAppInfo.sourceDir val pathClassLoader = param.result as ClassLoader - Hookers.logD("Injecting DEX into LoadedApk ClassLoader: $pathClassLoader") + logD("Injecting DEX into LoadedApk ClassLoader: $pathClassLoader") val pathList = XposedHelpers.getObjectField(pathClassLoader, "pathList") val dexPaths = XposedHelpers.callMethod(pathList, "getDexPaths") as List<*> @@ -241,7 +253,7 @@ object ParasiticManagerHooker { override fun afterHookedMethod(param: MethodHookParam<*>) { if (!activityClientRecordClass.isInstance(param.thisObject)) return param.args.filterIsInstance().forEach { aInfo -> - Hookers.logD("Restoring state for Activity: ${aInfo.name}") + logD("Restoring state for Activity: ${aInfo.name}") states[aInfo.name]?.let { XposedHelpers.setObjectField(param.thisObject, "state", it) } @@ -368,10 +380,10 @@ object ParasiticManagerHooker { "sProviderInstance", instance, ) - Hookers.logD("WebView provider initialized: $instance") + logD("WebView provider initialized: $instance") instance } catch (e: Exception) { - Hookers.logE("WebView initialization failed", e) + logE("WebView initialization failed", e) throw AndroidRuntimeException(e) } } @@ -406,9 +418,9 @@ object ParasiticManagerHooker { state?.let { states[aInfo.name] = it } pState?.let { persistentStates[aInfo.name] = it } - Hookers.logD("Saved state for ${aInfo.name}") + logD("Saved state for ${aInfo.name}") } - .onFailure { Hookers.logE("Failed to save activity state", it) } + .onFailure { logE("Failed to save activity state", it) } } } XposedBridge.hookAllMethods( @@ -434,7 +446,7 @@ object ParasiticManagerHooker { fun start(): Boolean { val binderList = mutableListOf() return try { - serviceClient.requestInjectedManagerBinder(binderList).use { pfd -> + VectorServiceClient.requestInjectedManagerBinder(binderList)!!.use { pfd -> managerFd = pfd.detachFd() val managerService = ILSPManagerService.Stub.asInterface(binderList[0]) hookForManager(managerService) diff --git a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt index 8b3ce43d8..4775b057c 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt @@ -1,20 +1,18 @@ package org.matrix.vector import android.annotation.SuppressLint -import android.app.ProfilerInfo import android.content.Intent import android.content.pm.ActivityInfo import android.content.pm.ResolveInfo -import io.github.libxposed.api.XposedInterface -import org.lsposed.lspd.hooker.HandleSystemServerProcessHooker -import org.lsposed.lspd.impl.LSPosedHelper import org.lsposed.lspd.util.Utils +import org.matrix.vector.impl.hookers.HandleSystemServerProcessHooker +import org.matrix.vector.impl.hooks.VectorHookBuilder import org.matrix.vector.service.BridgeService /** * Handles System-Server side logic for the Parasitic Manager. * - * When a user tries to open the LSPosed Manager, the system normally wouldn't know how to handle it + * When a user tries to open the Vector Manager, the system normally wouldn't know how to handle it * because it isn't "installed." This class intercepts the activity resolution and tells the system * to launch it in a special process. */ @@ -23,51 +21,12 @@ class ParasiticManagerSystemHooker : HandleSystemServerProcessHooker.Callback { companion object { @JvmStatic fun start() { - // Register this class as the handler for system_server initialization + // Register this class as the handler for system_server initialization. + // This ensures the hook is deferred until the System Server ClassLoader is fully ready. HandleSystemServerProcessHooker.callback = ParasiticManagerSystemHooker() } } - /** Intercepts Activity resolution in the System Server. */ - object Hooker : XposedInterface.Hooker { - @JvmStatic - fun after(callback: XposedInterface.AfterHookCallback) { - val intent = callback.args[0] as? Intent ?: return - - // Check if this intent is meant for the LSPosed Manager - if (!intent.hasCategory(BuildConfig.ManagerPackageName + ".LAUNCH_MANAGER")) return - - val result = callback.result as? ActivityInfo ?: return - - // We only intercept if it's currently resolving to the shell/fallback - if (result.packageName != BuildConfig.InjectedPackageName) return - - // --- Redirection Logic --- - // We create a copy of the ActivityInfo to avoid polluting the system's cache. - val redirectedInfo = - ActivityInfo(result).apply { - // Force the manager to run in its own dedicated process name - processName = BuildConfig.ManagerPackageName - - // Set a standard theme so transition animations work correctly - theme = android.R.style.Theme_DeviceDefault_Settings - - // Ensure the activity isn't excluded from recents by host flags - flags = - flags and - (ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS or - ActivityInfo.FLAG_FINISH_ON_CLOSE_SYSTEM_DIALOGS) - .inv() - } - - // Notify the bridge service that we are about to start the manager - BridgeService.getService()?.preStartManager() - - // Replace the original ResolveInfo with our parasitic one - callback.result = redirectedInfo - } - } - @SuppressLint("PrivateApi") override fun onSystemServerLoaded(classLoader: ClassLoader) { runCatching { @@ -100,16 +59,55 @@ class ParasiticManagerSystemHooker : HandleSystemServerProcessHooker.Callback { } } + // Locate the exact resolveActivity method + val resolveMethod = + supervisorClass.declaredMethods.first { it.name == "resolveActivity" } + // Hook the resolution method to inject our redirection logic - LSPosedHelper.hookMethod( - Hooker::class.java, - supervisorClass, - "resolveActivity", - Intent::class.java, - ResolveInfo::class.java, - Int::class.javaPrimitiveType, - ProfilerInfo::class.java, - ) + VectorHookBuilder(resolveMethod).intercept { chain -> + // 1. Execute the original resolution first + val result = chain.proceed() + + val intent = chain.args[0] as? Intent ?: return@intercept result + + // Check if this intent is meant for the LSPosed Manager + if (!intent.hasCategory(BuildConfig.ManagerPackageName + ".LAUNCH_MANAGER")) + return@intercept result + + // In standard Android, resolveActivity returns a ResolveInfo. + // We safely extract the nested ActivityInfo. + val resolveInfo = result as? ResolveInfo ?: return@intercept result + val originalActivityInfo = resolveInfo.activityInfo ?: return@intercept result + + // We only intercept if it's currently resolving to the shell/fallback + if (originalActivityInfo.packageName != BuildConfig.InjectedPackageName) + return@intercept result + + // --- Redirection Logic --- + // We create a copy of the ActivityInfo to avoid polluting the system's cache. + val redirectedInfo = + ActivityInfo(originalActivityInfo).apply { + // Force the manager to run in its own dedicated process name + processName = BuildConfig.ManagerPackageName + + // Set a standard theme so transition animations work correctly + theme = android.R.style.Theme_DeviceDefault_Settings + + // Ensure the activity isn't excluded from recents by host flags + flags = + flags and + (ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS or + ActivityInfo.FLAG_FINISH_ON_CLOSE_SYSTEM_DIALOGS) + .inv() + } + + // Notify the bridge service that we are about to start the manager + BridgeService.getService()?.preStartManager() + + // Replace the original ResolveInfo's ActivityInfo with our parasitic one + resolveInfo.activityInfo = redirectedInfo + resolveInfo + } Utils.logD("Successfully hooked Activity Supervisor for Manager redirection.") } diff --git a/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt b/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt index 164d86d84..c85401914 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt @@ -2,13 +2,13 @@ package org.matrix.vector.core import android.os.IBinder import android.os.Process -import org.lsposed.lspd.core.ApplicationServiceClient.serviceClient -import org.lsposed.lspd.core.Startup import org.lsposed.lspd.service.ILSPApplicationService import org.lsposed.lspd.util.Utils import org.matrix.vector.BuildConfig import org.matrix.vector.ParasiticManagerHooker import org.matrix.vector.ParasiticManagerSystemHooker +import org.matrix.vector.Startup +import org.matrix.vector.impl.core.VectorServiceClient /** Main entry point for the Java-side loader, invoked via JNI from the Vector Zygisk module. */ object Main { @@ -40,7 +40,7 @@ object Main { Startup.initXposed(isSystem, niceName, appDir, appService) // Configure logging levels from the service client - runCatching { Utils.Log.muted = serviceClient.isLogMuted } + runCatching { Utils.Log.muted = VectorServiceClient.isLogMuted } .onFailure { t -> Utils.logE("Failed to configure logs from service", t) } // Check if this process is the designated Vector Manager. From 675dc5a87ab1ee91cccfc088d871e40d80fc85dd Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Mon, 23 Mar 2026 01:18:07 +0100 Subject: [PATCH 07/34] Fix crashes during testing --- native/src/jni/hook_bridge.cpp | 103 +++++------------- .../matrix/vector/impl/core/VectorStartup.kt | 6 +- 2 files changed, 32 insertions(+), 77 deletions(-) diff --git a/native/src/jni/hook_bridge.cpp b/native/src/jni/hook_bridge.cpp index b4d841cbd..5bc19975f 100644 --- a/native/src/jni/hook_bridge.cpp +++ b/native/src/jni/hook_bridge.cpp @@ -9,15 +9,6 @@ #include "jni/jni_hooks.h" namespace { -/** - * @struct ModuleCallback - * @brief Stores the jmethodIDs for the "modern" callback API. - * This API separates the logic that runs before and after the original method. - */ -struct ModuleCallback { - jmethodID before_method; - jmethodID after_method; -}; /** * @struct HookItem @@ -33,7 +24,7 @@ struct HookItem { // Callbacks are stored in multimaps, keyed by priority. // std::greater<> ensures that higher priority numbers are processed first. std::multimap> legacy_callbacks; - std::multimap> modern_callbacks; + std::multimap> modern_callbacks; private: // The backup is an atomic jobject. @@ -92,11 +83,7 @@ using SharedHashMap = phmap::parallel_flat_hash_map> hooked_methods; // Cached JNI method and field IDs for performance. -// Looking these up frequently is slow, so they are cached on first use. jmethodID invoke = nullptr; -jmethodID callback_ctor = nullptr; -jfieldID before_method_field = nullptr; -jfieldID after_method_field = nullptr; } // namespace namespace vector::native::jni { @@ -168,28 +155,10 @@ VECTOR_DEF_NATIVE_METHOD(jboolean, HookBridge, hookMethod, jboolean useModernApi // ensuring thread-safe modification of the callback lists. lsplant::JNIMonitor monitor(env, backup); + // Store a global reference to the callback object itself. if (useModernApi) { - // Lazy initialization of JNI IDs for the modern API. - if (before_method_field == nullptr) { - auto callback_class = env->GetObjectClass(callback); - callback_ctor = - env->GetMethodID(callback_class, "", - "(Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;)V"); - before_method_field = - env->GetFieldID(callback_class, "beforeInvocation", "Ljava/lang/reflect/Method;"); - after_method_field = - env->GetFieldID(callback_class, "afterInvocation", "Ljava/lang/reflect/Method;"); - } - // Extract the before/after methods from the Java callback object. - auto before_method = env->GetObjectField(callback, before_method_field); - auto after_method = env->GetObjectField(callback, after_method_field); - auto callback_type = ModuleCallback{ - .before_method = env->FromReflectedMethod(before_method), - .after_method = env->FromReflectedMethod(after_method), - }; - hook_item->modern_callbacks.emplace(priority, callback_type); + hook_item->modern_callbacks.emplace(priority, env->NewGlobalRef(callback)); } else { - // For the legacy API, store a global reference to the callback object itself. hook_item->legacy_callbacks.emplace(priority, env->NewGlobalRef(callback)); } return JNI_TRUE; @@ -213,28 +182,18 @@ VECTOR_DEF_NATIVE_METHOD(jboolean, HookBridge, unhookMethod, jboolean useModernA // Lock to safely modify the callback list. lsplant::JNIMonitor monitor(env, backup); - if (useModernApi) { - auto before_method = env->GetObjectField(callback, before_method_field); - auto before = env->FromReflectedMethod(before_method); - // Find the callback by comparing the before_method's ID. - for (auto i = hook_item->modern_callbacks.begin(); i != hook_item->modern_callbacks.end(); - ++i) { - if (before == i->second.before_method) { - hook_item->modern_callbacks.erase(i); - return JNI_TRUE; - } - } - } else { - // Find the callback by comparing the jobject directly. - for (auto i = hook_item->legacy_callbacks.begin(); i != hook_item->legacy_callbacks.end(); - ++i) { - if (env->IsSameObject(i->second, callback)) { - env->DeleteGlobalRef(i->second); // Clean up the global reference. - hook_item->legacy_callbacks.erase(i); - return JNI_TRUE; - } + // Select the correct multimap + auto &callbacks = useModernApi ? hook_item->modern_callbacks : hook_item->legacy_callbacks; + + // Find the callback by comparing the jobject directly. + for (auto i = callbacks.begin(); i != callbacks.end(); ++i) { + if (env->IsSameObject(i->second, callback)) { + env->DeleteGlobalRef(i->second); // Clean up the global reference. + callbacks.erase(i); + return JNI_TRUE; } } + return JNI_FALSE; } @@ -561,33 +520,29 @@ VECTOR_DEF_NATIVE_METHOD(jobjectArray, HookBridge, callbackSnapshot, jclass call // Lock to ensure a consistent snapshot of the callback lists. lsplant::JNIMonitor monitor(env, backup); - auto res = env->NewObjectArray(2, env->FindClass("[Ljava/lang/Object;"), nullptr); - auto modern = env->NewObjectArray((jsize)hook_item->modern_callbacks.size(), - env->FindClass("java/lang/Object"), nullptr); - auto legacy = env->NewObjectArray((jsize)hook_item->legacy_callbacks.size(), - env->FindClass("java/lang/Object"), nullptr); + // Get the generic Object class + jclass obj_class = env->FindClass("java/lang/Object"); + + // Construct the result array Object[2][] + // Use an existing array to reliably get the Class for Object[] + jobjectArray dummy_array = env->NewObjectArray(0, obj_class, nullptr); + jclass obj_array_class = env->GetObjectClass(dummy_array); + jobjectArray res = env->NewObjectArray(2, obj_array_class, nullptr); + + // Create modern and legacy arrays + // Use 'callback_class' (VectorHookRecord) for the modern array for strict type safety + jobjectArray modern = + env->NewObjectArray((jsize)hook_item->modern_callbacks.size(), callback_class, nullptr); + jobjectArray legacy = + env->NewObjectArray((jsize)hook_item->legacy_callbacks.size(), obj_class, nullptr); jsize i = 0; for (const auto &callback_pair : hook_item->modern_callbacks) { - // The clazz argument refers to the Java class where the native method is - // declared, provided by the macro VECTOR_DEF_NATIVE_METHOD. - auto before_method = - env->ToReflectedMethod(clazz, callback_pair.second.before_method, JNI_FALSE); - auto after_method = - env->ToReflectedMethod(clazz, callback_pair.second.after_method, JNI_FALSE); - // Re-create the Java callback object from the stored method IDs. - auto callback_object = - env->NewObject(callback_class, callback_ctor, before_method, after_method); - env->SetObjectArrayElement(modern, i++, callback_object); - // Clean up local references created during object construction. - env->DeleteLocalRef(before_method); - env->DeleteLocalRef(after_method); - env->DeleteLocalRef(callback_object); + env->SetObjectArrayElement(modern, i++, callback_pair.second); } i = 0; for (const auto &callback_pair : hook_item->legacy_callbacks) { - // The legacy list already stores a global ref to the callback object. env->SetObjectArrayElement(legacy, i++, callback_pair.second); } diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorStartup.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorStartup.kt index 8e7eff646..5d5ecb668 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorStartup.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorStartup.kt @@ -15,11 +15,11 @@ object VectorStartup { @JvmStatic fun init( isSystem: Boolean, - processName: String, - appDir: String, + processName: String?, + appDir: String?, service: ILSPApplicationService?, ) { - VectorServiceClient.init(service, processName) + VectorServiceClient.init(service, processName ?: "android") VectorDeopter.deoptBootMethods() } From 5a2a34a803a7cb678091d30c0394a5118df562f8 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Mon, 23 Mar 2026 01:26:34 +0100 Subject: [PATCH 08/34] Allow multiple chain proceeding --- .../main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt index d2820f144..44b4caefd 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt @@ -49,9 +49,6 @@ class VectorChain( override fun proceedWith(thisObject: Any): Any? = proceedWith(thisObject, args) override fun proceedWith(thisObject: Any, args: Array): Any? { - if (proceedCalled) { - throw IllegalStateException("chain.proceed() can only be called once per interceptor.") - } proceedCalled = true // Reached the end of the modern hooks; trigger the original executable (and legacy hooks) From 58812c4a7d132611b84b41f8afd57ea70c978746 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Mon, 23 Mar 2026 09:00:00 +0100 Subject: [PATCH 09/34] Avoid depending on dummy class --- native/src/jni/resources_hook.cpp | 2 +- .../kotlin/org/matrix/vector/nativebridge/ResourcesHook.kt | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/native/src/jni/resources_hook.cpp b/native/src/jni/resources_hook.cpp index ea95e3953..52765bb87 100644 --- a/native/src/jni/resources_hook.cpp +++ b/native/src/jni/resources_hook.cpp @@ -300,7 +300,7 @@ static JNINativeMethod gMethods[] = { "(Ljava/lang/ClassLoader;Ljava/lang/String;Ljava/lang/" "String;)Ljava/lang/ClassLoader;"), VECTOR_NATIVE_METHOD(ResourcesHook, rewriteXmlReferencesNative, - "(JLxposed/dummy/XResourcesSuperClass;Landroid/content/res/Resources;)V")}; + "(JLjava/lang/Object;Landroid/content/res/Resources;)V")}; void RegisterResourcesHook(JNIEnv *env) { REGISTER_VECTOR_NATIVE_METHODS(ResourcesHook); } } // namespace vector::native::jni diff --git a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/ResourcesHook.kt b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/ResourcesHook.kt index 42955ed72..5b5552862 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/ResourcesHook.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/ResourcesHook.kt @@ -2,7 +2,6 @@ package org.matrix.vector.nativebridge import android.content.res.Resources import dalvik.annotation.optimization.FastNative -import xposed.dummy.XResourcesSuperClass object ResourcesHook { @JvmStatic external fun initXResourcesNative(): Boolean @@ -20,7 +19,7 @@ object ResourcesHook { @FastNative external fun rewriteXmlReferencesNative( parserPtr: Long, - origRes: XResourcesSuperClass, + origRes: Any, repRes: Resources, ) } From f7bb788b9171e295861f88a2515e1bc901e004c4 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Mon, 23 Mar 2026 09:19:21 +0100 Subject: [PATCH 10/34] Fix class resolution errors --- .../org/matrix/vector/legacy/LegacyDelegateImpl.java | 11 ++++++++++- .../matrix/vector/impl/hookers/SystemServerHookers.kt | 9 ++++++++- .../org/matrix/vector/nativebridge/ResourcesHook.kt | 6 +----- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/org/matrix/vector/legacy/LegacyDelegateImpl.java b/core/src/main/java/org/matrix/vector/legacy/LegacyDelegateImpl.java index 5b4277f29..549997ee6 100644 --- a/core/src/main/java/org/matrix/vector/legacy/LegacyDelegateImpl.java +++ b/core/src/main/java/org/matrix/vector/legacy/LegacyDelegateImpl.java @@ -96,7 +96,16 @@ public boolean hasLegacyModule(String packageName) { @Override public void setPackageNameForResDir(String packageName, String resDir) { - XResources.setPackageNameForResDir(packageName, resDir); + // By calling a separate static inner class, we prevent the verifier + // from looking at XResources when LegacyDelegateImpl is loaded. + ResourceProxy.set(packageName, resDir); + } + + // This class is only verified the FIRST time 'set' is called + private static class ResourceProxy { + static void set(String p, String r) { + XResources.setPackageNameForResDir(p, r); + } } private void hookNewXSP(XC_LoadPackage.LoadPackageParam lpparam) { diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/SystemServerHookers.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/SystemServerHookers.kt index c890da790..360907031 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/SystemServerHookers.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/SystemServerHookers.kt @@ -41,7 +41,14 @@ object HandleSystemServerProcessHooker : XposedInterface.Hooker { // Dynamically locate and hook the bootstrap service initializer val sysServerClass = Class.forName("com.android.server.SystemServer", false, classLoader) - val startMethod = sysServerClass.getDeclaredMethod("startBootstrapServices") + val startMethod = + sysServerClass.declaredMethods.find { it.name == "startBootstrapServices" } + ?: throw NoSuchMethodException( + "com.android.server.SystemServer.startBootstrapServices not found" + ) + + // Ensure we can hook the private method + startMethod.isAccessible = true VectorHookBuilder(startMethod).intercept(StartBootstrapServicesHooker) callback?.onSystemServerLoaded(classLoader) diff --git a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/ResourcesHook.kt b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/ResourcesHook.kt index 5b5552862..a3f8ed057 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/ResourcesHook.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/ResourcesHook.kt @@ -17,9 +17,5 @@ object ResourcesHook { @JvmStatic @FastNative - external fun rewriteXmlReferencesNative( - parserPtr: Long, - origRes: Any, - repRes: Resources, - ) + external fun rewriteXmlReferencesNative(parserPtr: Long, origRes: Any, repRes: Resources) } From dbf1f4d206d4fd7284eeae89cdbf2987130a6a4e Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Mon, 23 Mar 2026 09:32:29 +0100 Subject: [PATCH 11/34] Fix typo --- zygisk/src/main/cpp/module.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zygisk/src/main/cpp/module.cpp b/zygisk/src/main/cpp/module.cpp index f35efcf69..bd25f6696 100644 --- a/zygisk/src/main/cpp/module.cpp +++ b/zygisk/src/main/cpp/module.cpp @@ -35,7 +35,7 @@ constexpr int PER_USER_RANGE = 100000; // Defined via CMake generated marcos constexpr uid_t kHostPackageUid = INJECTED_PACKAGE_UID; const char *const kHostPackageName = INJECTED_PACKAGE_NAME; -const char *const kManagePackageName = MANAGER_PACKAGE_NAME; +const char *const kManagerPackageName = MANAGER_PACKAGE_NAME; constexpr uid_t GID_INET = 3003; // Android's Internet group ID. enum RuntimeFlags : uint32_t { @@ -238,7 +238,7 @@ void VectorModule::preAppSpecialize(zygisk::AppSpecializeArgs *args) { // grant it internet permissions by adding it to the INET group. if (args->uid == kHostPackageUid) { lsplant::JUTFString nice_name_str(env_, args->nice_name); - if (nice_name_str.get() == std::string(kManagePackageName)) { + if (nice_name_str.get() == std::string(kManagerPackageName)) { LOGI("Manager app detected. Granting internet permissions."); is_manager_app_ = true; @@ -303,7 +303,7 @@ void VectorModule::postAppSpecialize(const zygisk::AppSpecializeArgs *args) { } if (is_manager_app_) { - args->nice_name = env_->NewStringUTF(kManagePackageName); + args->nice_name = env_->NewStringUTF(kManagerPackageName); } // --- Framework Injection --- From 059a0f799034c498964f0d0bfe9e6eefc5357c65 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Mon, 23 Mar 2026 09:32:45 +0100 Subject: [PATCH 12/34] Debug manager --- .../kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt index 4775b057c..ecb564ee4 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt @@ -65,10 +65,12 @@ class ParasiticManagerSystemHooker : HandleSystemServerProcessHooker.Callback { // Hook the resolution method to inject our redirection logic VectorHookBuilder(resolveMethod).intercept { chain -> + Utils.logD("inside resolveMethod, calling proceed") // 1. Execute the original resolution first val result = chain.proceed() val intent = chain.args[0] as? Intent ?: return@intercept result + Utils.logD("proceed called, intent ${intent}") // Check if this intent is meant for the LSPosed Manager if (!intent.hasCategory(BuildConfig.ManagerPackageName + ".LAUNCH_MANAGER")) From f461953d5baff4b787cfdb66573c340079ad78f7 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Mon, 23 Mar 2026 10:06:57 +0100 Subject: [PATCH 13/34] Fix resolveActivity hook --- .../vector/ParasiticManagerSystemHooker.kt | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt index ecb564ee4..8e5bc18ce 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt @@ -3,7 +3,6 @@ package org.matrix.vector import android.annotation.SuppressLint import android.content.Intent import android.content.pm.ActivityInfo -import android.content.pm.ResolveInfo import org.lsposed.lspd.util.Utils import org.matrix.vector.impl.hookers.HandleSystemServerProcessHooker import org.matrix.vector.impl.hooks.VectorHookBuilder @@ -76,15 +75,20 @@ class ParasiticManagerSystemHooker : HandleSystemServerProcessHooker.Callback { if (!intent.hasCategory(BuildConfig.ManagerPackageName + ".LAUNCH_MANAGER")) return@intercept result - // In standard Android, resolveActivity returns a ResolveInfo. - // We safely extract the nested ActivityInfo. - val resolveInfo = result as? ResolveInfo ?: return@intercept result - val originalActivityInfo = resolveInfo.activityInfo ?: return@intercept result + val originalActivityInfo = + result as? ActivityInfo + ?: run { + Utils.logD( + "Redirection: result is not ActivityInfo (was ${result?.javaClass?.name})" + ) + return@intercept result + } // We only intercept if it's currently resolving to the shell/fallback if (originalActivityInfo.packageName != BuildConfig.InjectedPackageName) return@intercept result + Utils.logD("creat redirectedInfo") // --- Redirection Logic --- // We create a copy of the ActivityInfo to avoid polluting the system's cache. val redirectedInfo = @@ -106,9 +110,8 @@ class ParasiticManagerSystemHooker : HandleSystemServerProcessHooker.Callback { // Notify the bridge service that we are about to start the manager BridgeService.getService()?.preStartManager() - // Replace the original ResolveInfo's ActivityInfo with our parasitic one - resolveInfo.activityInfo = redirectedInfo - resolveInfo + Utils.logD("returning redirectedInfo ${redirectedInfo}") + redirectedInfo } Utils.logD("Successfully hooked Activity Supervisor for Manager redirection.") From 83bcda107c9e088f1869856d640d5ff421b1d38a Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Mon, 23 Mar 2026 10:33:11 +0100 Subject: [PATCH 14/34] Add docs in README --- xposed/README.md | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/xposed/README.md b/xposed/README.md index bbc76abaf..758b4ece5 100644 --- a/xposed/README.md +++ b/xposed/README.md @@ -1,5 +1,36 @@ # Xposed API implementation of the Vector framework -LSPosed is being refactored into a new project `Vector`. +This subproject implements the modern **libxposed API** for the Vector framework. It serves as the primary bridge between the native ART hooking engine (`lsplant`) and module developers, providing a type-safe, OkHttp-style interceptor chain architecture. -This sub-project `xposed`, written in Kotlin, will be refactored from the `core` sub-project written in `Java`. +## Architectural Overview + +The `xposed` module is designed with strict boundaries to ensure stability during the Android boot process and application lifecycles. It is written entirely in Kotlin and operates independently of the legacy Xposed API (`de.robv.android.xposed`). + +To prevent circular dependencies and ART class verification deadlocks, this module does **not** depend on the `core` (legacy Java) subproject. Instead, it defines a Dependency Injection (DI) contract (`LegacyFrameworkDelegate`) which the `core` module must implement and inject during startup. + +## Core Components + +### 1. The Hooking Engine (`org.matrix.vector.impl.hooks`) + +* **`VectorHookBuilder`**: Implements the API 101 `HookBuilder`. It validates the target `Executable`, bundles the module's `Hooker`, `priority`, and `ExceptionMode` into a `VectorHookRecord`, and registers it natively via JNI. +* **`VectorNativeHooker`**: The JNI trampoline target. When a hooked method is executed, the C++ layer invokes `callback(Array)` on this class. It fetches the active hooks (both modern and legacy) from the native registry as global `jobject` references, constructs the root `VectorChain`, and initiates execution. +* **`VectorChain`**: Implements the recursive `proceed()` state machine. + * **Exception Handling**: It implements the logic for `ExceptionMode`. In `PROTECTIVE` mode, if an interceptor throws an exception *before* calling `proceed()`, the chain skips the interceptor. If it throws *after* calling `proceed()`, the chain catches the exception and restores the cached downstream result/throwable to protect the host process. + +### 2. The Invocation System (`org.matrix.vector.impl.hooks.BaseInvoker`) +The `Invoker` system allows modules to execute methods while bypassing standard JVM access checks, with granular control over hook execution. + +* **`Type.Origin`**: Dispatches directly to JNI (`HookBridge.invokeOriginalMethod`), bypassing all active hooks. +* **`Type.Chain`**: Constructs a localized `VectorChain` containing only hooks with a priority less than or equal to the requested `maxPriority`, allowing modules to execute partial hook chains. +* **`VectorCtorInvoker`**: Handles constructor invocation. It separates memory allocation (`HookBridge.allocateObject`) from initialization (`invokeOriginalMethod` / `invokeSpecialMethod`) to support safe `newInstanceSpecial` logic. + +### 3. Dependency Injection Contract (`org.matrix.vector.impl.di`) +To maintain the separation of concerns, the `xposed` module communicates with the legacy Xposed ecosystem via `VectorBootstrap` and `LegacyFrameworkDelegate`. + +When `xposed` intercepts an Android lifecycle event (e.g., `LoadedApk.createClassLoader`), it dispatches the event internally via `VectorLifecycleManager` (for API 101 modules) and then delegates the raw parameters to `LegacyFrameworkDelegate` so the `core` module can construct and dispatch the legacy `XC_LoadPackage` callbacks. + +### 4. In-Memory Module ClassLoading (`org.matrix.vector.impl.util.VectorModuleClassLoader`) +Modules are loaded directly from memory to prevent disk I/O bottlenecks and enhance security. +* It utilizes `SharedMemory` buffers passed over IPC, mapping them to `ByteBuffer`s. +* It extends `ByteBufferDexClassLoader` to evaluate the module's DEX files. +* `VectorURLStreamHandler` intercepts resource requests (like `assets/`) to read directly from the original APK path without extracting the APK locally. From 7479987eec0bee096f8818863485fbd3b3e89f37 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Mon, 23 Mar 2026 10:18:47 +0100 Subject: [PATCH 15/34] Minor fixes --- .../matrix/vector/impl/hooks/BaseInvoker.kt | 7 ++++++- .../matrix/vector/impl/hooks/VectorChain.kt | 10 +++++----- .../vector/impl/hooks/VectorNativeHooker.kt | 20 ++++++++++--------- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/BaseInvoker.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/BaseInvoker.kt index fa5abe2dc..5112e4071 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/BaseInvoker.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/BaseInvoker.kt @@ -4,6 +4,7 @@ import io.github.libxposed.api.XposedInterface.CtorInvoker import io.github.libxposed.api.XposedInterface.Invoker import java.lang.reflect.Constructor import java.lang.reflect.Executable +import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method import org.matrix.vector.impl.di.VectorBootstrap import org.matrix.vector.nativebridge.HookBridge @@ -28,7 +29,11 @@ internal abstract class BaseInvoker, U : Executable>( protected fun proceedInvocation(thisObject: Any?, args: Array): Any? { return when (val currentType = type) { is Invoker.Type.Origin -> { - HookBridge.invokeOriginalMethod(executable, thisObject, *args) + try { + HookBridge.invokeOriginalMethod(executable, thisObject, args) + } catch (e: InvocationTargetException) { + throw e.cause ?: e + } } is Invoker.Type.Chain -> { val snapshots = diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt index 44b4caefd..8c6c8b234 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt @@ -86,8 +86,8 @@ class VectorChain( t: Throwable, record: VectorHookRecord, nextChain: VectorChain, - currentThis: Any, - currentArgs: Array, + recoverThis: Any, + recoverArgs: Array, ): Any? { if (record.exceptionMode == ExceptionMode.PASSTHROUGH) { throw t @@ -97,9 +97,9 @@ class VectorChain( Log.e("VectorChain", "Hooker threw exception: ${record.hooker.javaClass.name}", t) if (!nextChain.proceedCalled) { - // Crash occurred BEFORE proceed(). Skip this hooker entirely and drive the chain - // manually. - return nextChain.proceedWith(currentThis, currentArgs) + // Crash occurred BEFORE proceed(). + // Skip this hooker entirely and drive the chain manually. + return nextChain.proceedWith(recoverThis, recoverArgs) } else { // Crash occurred AFTER proceed(). Swallow the module's crash and return the real // downstream state. diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt index a018192b0..b210134d2 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt @@ -98,15 +98,17 @@ class VectorNativeHooker(private val method: T) { val result = rootChain.proceed() // Type safety validation before returning to C++ - if ( - returnType != null && - !returnType.isPrimitive && - result != null && - !HookBridge.instanceOf(result, returnType) - ) { - throw ClassCastException( - "Return value's type from hook callback does not match the hooked method" - ) + if (returnType != null && returnType != Void.TYPE) { + if (result == null && returnType.isPrimitive) { + throw NullPointerException( + "Hook returned null for a primitive return type: $method" + ) + } + if (result != null && HookBridge.instanceOf(result, returnType)) { + throw ClassCastException( + "Hook return type mismatch. Expected ${returnType.name}, got ${result.javaClass.name}" + ) + } } return result From bdd7c3bbf99f267b94f6c1dd85a1d184ed7babc2 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Mon, 23 Mar 2026 14:31:26 +0100 Subject: [PATCH 16/34] Refactor `core` module into `legacy` Moreover, we add serval minor improvements. --- core/.gitignore | 3 - core/build.gradle.kts | 35 -- core/proguard-rules.pro | 37 -- .../de/robv/android/xposed/IXposedMod.java | 27 -- .../src/main/java/sun/net/www/ParseUtil.java | 7 - legacy/build.gradle.kts | 18 + legacy/consumer-rules.pro | 3 + .../java/android/app/AndroidAppHelper.java | 20 - .../android/content/res/XModuleResources.java | 20 - .../android/content/res/XResForwarder.java | 20 - .../java/android/content/res/XResources.java | 20 - .../android/xposed/IXposedHookCmdInit.java | 20 - .../IXposedHookInitPackageResources.java | 0 .../xposed/IXposedHookLoadPackage.java | 0 .../android/xposed/IXposedHookZygoteInit.java | 0 .../de/robv/android/xposed/IXposedMod.java | 7 + .../de/robv/android/xposed/SELinuxHelper.java | 20 - .../de/robv/android/xposed/XC_MethodHook.java | 20 - .../android/xposed/XC_MethodReplacement.java | 20 - .../android/xposed/XSharedPreferences.java | 22 +- .../de/robv/android/xposed/XposedBridge.java | 20 - .../de/robv/android/xposed/XposedHelpers.java | 20 - .../de/robv/android/xposed/XposedInit.java | 22 +- .../android/xposed/callbacks/IXUnhook.java | 20 - .../callbacks/XC_InitPackageResources.java | 20 - .../xposed/callbacks/XC_LayoutInflated.java | 20 - .../xposed/callbacks/XC_LoadPackage.java | 20 - .../android/xposed/callbacks/XCallback.java | 20 - .../android/xposed/services/BaseService.java | 20 - .../xposed/services/DirectAccessService.java | 20 - .../android/xposed/services/FileResult.java | 20 - .../main/java/org/matrix/vector/Startup.java | 6 +- .../vector/legacy/LegacyDelegateImpl.java | 6 +- settings.gradle.kts | 3 +- xposed/README.md | 20 +- xposed/build.gradle.kts | 15 +- xposed/consumer-rules.pro | 32 ++ .../org/matrix/vector/api/utils/DexParser.kt | 342 ------------------ .../org/matrix/vector/impl/VectorContext.kt | 14 +- .../vector/impl/VectorRemotePreferences.kt | 5 +- .../matrix/vector/impl/core/VectorDeopter.kt | 7 +- .../vector/impl/core/VectorInlinedCallers.kt | 6 +- .../vector/impl/core/VectorModuleManager.kt | 25 +- .../vector/impl/core/VectorServiceClient.kt | 6 +- .../matrix/vector/impl/core/VectorStartup.kt | 10 +- .../matrix/vector/impl/di/VectorBootstrap.kt | 4 +- .../vector/impl/hookers/CrashDumpHooker.kt | 4 +- .../vector/impl/hookers/LoadedApkHookers.kt | 7 +- .../impl/hookers/SystemServerHookers.kt | 4 +- .../matrix/vector/impl/hooks/BaseInvoker.kt | 4 +- .../matrix/vector/impl/hooks/VectorChain.kt | 8 +- .../vector/impl/utils/VectorDexParser.kt | 248 ------------- .../impl/utils/VectorURLStreamHandler.kt | 1 - .../vector/nativebridge/DexParserBridge.kt | 30 -- zygisk/build.gradle.kts | 2 +- .../matrix/vector/ParasiticManagerHooker.kt | 2 +- 56 files changed, 149 insertions(+), 1203 deletions(-) delete mode 100644 core/.gitignore delete mode 100644 core/build.gradle.kts delete mode 100644 core/proguard-rules.pro delete mode 100644 core/src/main/java/de/robv/android/xposed/IXposedMod.java delete mode 100644 hiddenapi/stubs/src/main/java/sun/net/www/ParseUtil.java create mode 100644 legacy/build.gradle.kts create mode 100644 legacy/consumer-rules.pro rename {core => legacy}/src/main/java/android/app/AndroidAppHelper.java (88%) rename {core => legacy}/src/main/java/android/content/res/XModuleResources.java (72%) rename {core => legacy}/src/main/java/android/content/res/XResForwarder.java (55%) rename {core => legacy}/src/main/java/android/content/res/XResources.java (98%) rename {core => legacy}/src/main/java/de/robv/android/xposed/IXposedHookCmdInit.java (56%) rename {core => legacy}/src/main/java/de/robv/android/xposed/IXposedHookInitPackageResources.java (100%) rename {core => legacy}/src/main/java/de/robv/android/xposed/IXposedHookLoadPackage.java (100%) rename {core => legacy}/src/main/java/de/robv/android/xposed/IXposedHookZygoteInit.java (100%) create mode 100644 legacy/src/main/java/de/robv/android/xposed/IXposedMod.java rename {core => legacy}/src/main/java/de/robv/android/xposed/SELinuxHelper.java (67%) rename {core => legacy}/src/main/java/de/robv/android/xposed/XC_MethodHook.java (88%) rename {core => legacy}/src/main/java/de/robv/android/xposed/XC_MethodReplacement.java (79%) rename {core => legacy}/src/main/java/de/robv/android/xposed/XSharedPreferences.java (95%) rename {core => legacy}/src/main/java/de/robv/android/xposed/XposedBridge.java (96%) rename {core => legacy}/src/main/java/de/robv/android/xposed/XposedHelpers.java (98%) rename {core => legacy}/src/main/java/de/robv/android/xposed/XposedInit.java (94%) rename {core => legacy}/src/main/java/de/robv/android/xposed/callbacks/IXUnhook.java (50%) rename {core => legacy}/src/main/java/de/robv/android/xposed/callbacks/XC_InitPackageResources.java (71%) rename {core => legacy}/src/main/java/de/robv/android/xposed/callbacks/XC_LayoutInflated.java (81%) rename {core => legacy}/src/main/java/de/robv/android/xposed/callbacks/XC_LoadPackage.java (74%) rename {core => legacy}/src/main/java/de/robv/android/xposed/callbacks/XCallback.java (85%) rename {core => legacy}/src/main/java/de/robv/android/xposed/services/BaseService.java (91%) rename {core => legacy}/src/main/java/de/robv/android/xposed/services/DirectAccessService.java (83%) rename {core => legacy}/src/main/java/de/robv/android/xposed/services/FileResult.java (69%) rename {core => legacy}/src/main/java/org/matrix/vector/Startup.java (85%) rename {core => legacy}/src/main/java/org/matrix/vector/legacy/LegacyDelegateImpl.java (97%) create mode 100644 xposed/consumer-rules.pro delete mode 100644 xposed/src/main/kotlin/org/matrix/vector/api/utils/DexParser.kt delete mode 100644 xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorDexParser.kt delete mode 100644 xposed/src/main/kotlin/org/matrix/vector/nativebridge/DexParserBridge.kt diff --git a/core/.gitignore b/core/.gitignore deleted file mode 100644 index 035d07e58..000000000 --- a/core/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/build -/.cxx -/src/main/jni/src/config.cpp diff --git a/core/build.gradle.kts b/core/build.gradle.kts deleted file mode 100644 index 5e6695814..000000000 --- a/core/build.gradle.kts +++ /dev/null @@ -1,35 +0,0 @@ -val versionCodeProvider: Provider by rootProject.extra -val versionNameProvider: Provider by rootProject.extra - -plugins { alias(libs.plugins.agp.lib) } - -android { - namespace = "org.lsposed.lspd.core" - - androidResources { enable = false } - - defaultConfig { - consumerProguardFiles("proguard-rules.pro") - buildConfigField("String", "FRAMEWORK_NAME", """"${rootProject.name}"""") - buildConfigField("String", "VERSION_NAME", """"${versionCodeProvider.get()}"""") - buildConfigField("long", "VERSION_CODE", versionCodeProvider.get()) - } - - buildTypes { - release { - isMinifyEnabled = true - proguardFiles("proguard-rules.pro") - } - } -} - -dependencies { - api(projects.xposed) - implementation(projects.external.apache) - implementation(projects.external.axml) - implementation(projects.hiddenapi.bridge) - implementation(projects.services.daemonService) - implementation(projects.services.managerService) - compileOnly(libs.androidx.annotation) - compileOnly(projects.hiddenapi.stubs) -} diff --git a/core/proguard-rules.pro b/core/proguard-rules.pro deleted file mode 100644 index 844704e3c..000000000 --- a/core/proguard-rules.pro +++ /dev/null @@ -1,37 +0,0 @@ --keep class android.** { *; } --keep class de.robv.android.xposed.** {*;} --keep class io.github.libxposed.** {*;} --keep class org.lsposed.lspd.core.* {*;} --keep class org.lsposed.lspd.hooker.HandleSystemServerProcessHooker {*;} --keep class org.lsposed.lspd.hooker.HandleSystemServerProcessHooker$Callback {*;} --keep class org.lsposed.lspd.impl.LSPosedBridge$NativeHooker {*;} --keep class org.lsposed.lspd.impl.LSPosedBridge$HookerCallback {*;} --keep class org.lsposed.lspd.util.Hookers {*;} - --keepnames class org.lsposed.lspd.impl.LSPosedHelper { - public ; -} - --keepattributes RuntimeVisibleAnnotations --keepclasseswithmembers,includedescriptorclasses class * { - native ; -} --keepclassmembers class org.lsposed.lspd.impl.LSPosedContext { - public ; -} --keepclassmembers class org.lsposed.lspd.impl.LSPosedHookCallback { - public ; -} --keepclassmembers,allowoptimization class ** implements io.github.libxposed.api.XposedInterface$Hooker { - public static *** before(); - public static *** before(io.github.libxposed.api.XposedInterface$BeforeHookCallback); - public static void after(); - public static void after(io.github.libxposed.api.XposedInterface$AfterHookCallback); - public static void after(io.github.libxposed.api.XposedInterface$AfterHookCallback, ***); -} --assumenosideeffects class android.util.Log { - public static *** v(...); - public static *** d(...); -} --repackageclasses --allowaccessmodification diff --git a/core/src/main/java/de/robv/android/xposed/IXposedMod.java b/core/src/main/java/de/robv/android/xposed/IXposedMod.java deleted file mode 100644 index 319fa560f..000000000 --- a/core/src/main/java/de/robv/android/xposed/IXposedMod.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - -package de.robv.android.xposed; - -/** - * Marker interface for Xposed modules. Cannot be implemented directly. - */ -/* package */ interface IXposedMod { -} diff --git a/hiddenapi/stubs/src/main/java/sun/net/www/ParseUtil.java b/hiddenapi/stubs/src/main/java/sun/net/www/ParseUtil.java deleted file mode 100644 index a3755eeca..000000000 --- a/hiddenapi/stubs/src/main/java/sun/net/www/ParseUtil.java +++ /dev/null @@ -1,7 +0,0 @@ -package sun.net.www; - -public class ParseUtil { - public static String encodePath(String path, boolean flag) { - throw new RuntimeException("Stub!"); - } -} diff --git a/legacy/build.gradle.kts b/legacy/build.gradle.kts new file mode 100644 index 000000000..f28dec801 --- /dev/null +++ b/legacy/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { alias(libs.plugins.agp.lib) } + +android { + namespace = "org.matrix.vector.legacy" + + androidResources { enable = false } + + defaultConfig { consumerProguardFiles("consumer-rules.pro") } +} + +dependencies { + api(projects.xposed) + implementation(projects.external.apache) + implementation(projects.hiddenapi.bridge) + implementation(projects.services.daemonService) + compileOnly(libs.androidx.annotation) + compileOnly(projects.hiddenapi.stubs) +} diff --git a/legacy/consumer-rules.pro b/legacy/consumer-rules.pro new file mode 100644 index 000000000..689d78067 --- /dev/null +++ b/legacy/consumer-rules.pro @@ -0,0 +1,3 @@ +-keep class android.** { *; } +-keep class de.robv.android.xposed.** {*;} +-keep class org.matrix.vector.Startup {*;} diff --git a/core/src/main/java/android/app/AndroidAppHelper.java b/legacy/src/main/java/android/app/AndroidAppHelper.java similarity index 88% rename from core/src/main/java/android/app/AndroidAppHelper.java rename to legacy/src/main/java/android/app/AndroidAppHelper.java index ed6636f7e..8d12fc5b7 100644 --- a/core/src/main/java/android/app/AndroidAppHelper.java +++ b/legacy/src/main/java/android/app/AndroidAppHelper.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package android.app; import static de.robv.android.xposed.XposedHelpers.findClass; diff --git a/core/src/main/java/android/content/res/XModuleResources.java b/legacy/src/main/java/android/content/res/XModuleResources.java similarity index 72% rename from core/src/main/java/android/content/res/XModuleResources.java rename to legacy/src/main/java/android/content/res/XModuleResources.java index e5d8cf694..67396b194 100644 --- a/core/src/main/java/android/content/res/XModuleResources.java +++ b/legacy/src/main/java/android/content/res/XModuleResources.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package android.content.res; import android.app.AndroidAppHelper; diff --git a/core/src/main/java/android/content/res/XResForwarder.java b/legacy/src/main/java/android/content/res/XResForwarder.java similarity index 55% rename from core/src/main/java/android/content/res/XResForwarder.java rename to legacy/src/main/java/android/content/res/XResForwarder.java index 9df971630..7d659052c 100644 --- a/core/src/main/java/android/content/res/XResForwarder.java +++ b/legacy/src/main/java/android/content/res/XResForwarder.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package android.content.res; /** diff --git a/core/src/main/java/android/content/res/XResources.java b/legacy/src/main/java/android/content/res/XResources.java similarity index 98% rename from core/src/main/java/android/content/res/XResources.java rename to legacy/src/main/java/android/content/res/XResources.java index 349a885f5..833ea926e 100644 --- a/core/src/main/java/android/content/res/XResources.java +++ b/legacy/src/main/java/android/content/res/XResources.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package android.content.res; import static org.matrix.vector.nativebridge.ResourcesHook.rewriteXmlReferencesNative; diff --git a/core/src/main/java/de/robv/android/xposed/IXposedHookCmdInit.java b/legacy/src/main/java/de/robv/android/xposed/IXposedHookCmdInit.java similarity index 56% rename from core/src/main/java/de/robv/android/xposed/IXposedHookCmdInit.java rename to legacy/src/main/java/de/robv/android/xposed/IXposedHookCmdInit.java index 655453b8d..0132c58be 100644 --- a/core/src/main/java/de/robv/android/xposed/IXposedHookCmdInit.java +++ b/legacy/src/main/java/de/robv/android/xposed/IXposedHookCmdInit.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed; diff --git a/core/src/main/java/de/robv/android/xposed/IXposedHookInitPackageResources.java b/legacy/src/main/java/de/robv/android/xposed/IXposedHookInitPackageResources.java similarity index 100% rename from core/src/main/java/de/robv/android/xposed/IXposedHookInitPackageResources.java rename to legacy/src/main/java/de/robv/android/xposed/IXposedHookInitPackageResources.java diff --git a/core/src/main/java/de/robv/android/xposed/IXposedHookLoadPackage.java b/legacy/src/main/java/de/robv/android/xposed/IXposedHookLoadPackage.java similarity index 100% rename from core/src/main/java/de/robv/android/xposed/IXposedHookLoadPackage.java rename to legacy/src/main/java/de/robv/android/xposed/IXposedHookLoadPackage.java diff --git a/core/src/main/java/de/robv/android/xposed/IXposedHookZygoteInit.java b/legacy/src/main/java/de/robv/android/xposed/IXposedHookZygoteInit.java similarity index 100% rename from core/src/main/java/de/robv/android/xposed/IXposedHookZygoteInit.java rename to legacy/src/main/java/de/robv/android/xposed/IXposedHookZygoteInit.java diff --git a/legacy/src/main/java/de/robv/android/xposed/IXposedMod.java b/legacy/src/main/java/de/robv/android/xposed/IXposedMod.java new file mode 100644 index 000000000..50e6418d1 --- /dev/null +++ b/legacy/src/main/java/de/robv/android/xposed/IXposedMod.java @@ -0,0 +1,7 @@ +package de.robv.android.xposed; + +/** + * Marker interface for Xposed modules. Cannot be implemented directly. + */ +/* package */ interface IXposedMod { +} diff --git a/core/src/main/java/de/robv/android/xposed/SELinuxHelper.java b/legacy/src/main/java/de/robv/android/xposed/SELinuxHelper.java similarity index 67% rename from core/src/main/java/de/robv/android/xposed/SELinuxHelper.java rename to legacy/src/main/java/de/robv/android/xposed/SELinuxHelper.java index a5c8fe8cf..72fde6f77 100644 --- a/core/src/main/java/de/robv/android/xposed/SELinuxHelper.java +++ b/legacy/src/main/java/de/robv/android/xposed/SELinuxHelper.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed; import de.robv.android.xposed.services.BaseService; diff --git a/core/src/main/java/de/robv/android/xposed/XC_MethodHook.java b/legacy/src/main/java/de/robv/android/xposed/XC_MethodHook.java similarity index 88% rename from core/src/main/java/de/robv/android/xposed/XC_MethodHook.java rename to legacy/src/main/java/de/robv/android/xposed/XC_MethodHook.java index 12efd52a3..d9431bea9 100644 --- a/core/src/main/java/de/robv/android/xposed/XC_MethodHook.java +++ b/legacy/src/main/java/de/robv/android/xposed/XC_MethodHook.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed; import java.lang.reflect.Executable; diff --git a/core/src/main/java/de/robv/android/xposed/XC_MethodReplacement.java b/legacy/src/main/java/de/robv/android/xposed/XC_MethodReplacement.java similarity index 79% rename from core/src/main/java/de/robv/android/xposed/XC_MethodReplacement.java rename to legacy/src/main/java/de/robv/android/xposed/XC_MethodReplacement.java index a7839f8f9..d7f2b6508 100644 --- a/core/src/main/java/de/robv/android/xposed/XC_MethodReplacement.java +++ b/legacy/src/main/java/de/robv/android/xposed/XC_MethodReplacement.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed; import de.robv.android.xposed.callbacks.XCallback; diff --git a/core/src/main/java/de/robv/android/xposed/XSharedPreferences.java b/legacy/src/main/java/de/robv/android/xposed/XSharedPreferences.java similarity index 95% rename from core/src/main/java/de/robv/android/xposed/XSharedPreferences.java rename to legacy/src/main/java/de/robv/android/xposed/XSharedPreferences.java index 0a0b83837..47cd192ee 100644 --- a/core/src/main/java/de/robv/android/xposed/XSharedPreferences.java +++ b/legacy/src/main/java/de/robv/android/xposed/XSharedPreferences.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - package de.robv.android.xposed; import android.annotation.SuppressLint; @@ -28,10 +8,10 @@ import com.android.internal.util.XmlUtils; -import org.lsposed.lspd.core.BuildConfig; import org.lsposed.lspd.util.Utils.Log; import org.matrix.vector.impl.core.VectorServiceClient; import org.matrix.vector.impl.utils.VectorMetaDataReader; +import org.matrix.vector.legacy.BuildConfig; import org.xmlpull.v1.XmlPullParserException; import java.io.File; diff --git a/core/src/main/java/de/robv/android/xposed/XposedBridge.java b/legacy/src/main/java/de/robv/android/xposed/XposedBridge.java similarity index 96% rename from core/src/main/java/de/robv/android/xposed/XposedBridge.java rename to legacy/src/main/java/de/robv/android/xposed/XposedBridge.java index b6555874e..4e37cf25b 100644 --- a/core/src/main/java/de/robv/android/xposed/XposedBridge.java +++ b/legacy/src/main/java/de/robv/android/xposed/XposedBridge.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - package de.robv.android.xposed; import android.app.ActivityThread; diff --git a/core/src/main/java/de/robv/android/xposed/XposedHelpers.java b/legacy/src/main/java/de/robv/android/xposed/XposedHelpers.java similarity index 98% rename from core/src/main/java/de/robv/android/xposed/XposedHelpers.java rename to legacy/src/main/java/de/robv/android/xposed/XposedHelpers.java index 592be83d8..a3bf8dd26 100644 --- a/core/src/main/java/de/robv/android/xposed/XposedHelpers.java +++ b/legacy/src/main/java/de/robv/android/xposed/XposedHelpers.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed; import android.content.res.AssetManager; diff --git a/core/src/main/java/de/robv/android/xposed/XposedInit.java b/legacy/src/main/java/de/robv/android/xposed/XposedInit.java similarity index 94% rename from core/src/main/java/de/robv/android/xposed/XposedInit.java rename to legacy/src/main/java/de/robv/android/xposed/XposedInit.java index 2c924ba82..31f2e5057 100644 --- a/core/src/main/java/de/robv/android/xposed/XposedInit.java +++ b/legacy/src/main/java/de/robv/android/xposed/XposedInit.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed; import static de.robv.android.xposed.XposedBridge.hookAllMethods; @@ -38,13 +18,13 @@ import android.os.Process; import android.util.ArrayMap; -import org.lsposed.lspd.models.PreLoadedApk; import org.matrix.vector.impl.core.VectorDeopter; import org.matrix.vector.impl.core.VectorModuleManager; import org.matrix.vector.impl.core.VectorServiceClient; import org.matrix.vector.impl.utils.VectorModuleClassLoader; import org.matrix.vector.nativebridge.NativeAPI; import org.matrix.vector.nativebridge.ResourcesHook; +import org.lsposed.lspd.models.PreLoadedApk; import org.lsposed.lspd.util.Utils.Log; import java.io.File; diff --git a/core/src/main/java/de/robv/android/xposed/callbacks/IXUnhook.java b/legacy/src/main/java/de/robv/android/xposed/callbacks/IXUnhook.java similarity index 50% rename from core/src/main/java/de/robv/android/xposed/callbacks/IXUnhook.java rename to legacy/src/main/java/de/robv/android/xposed/callbacks/IXUnhook.java index e2fa76c93..9867153c9 100644 --- a/core/src/main/java/de/robv/android/xposed/callbacks/IXUnhook.java +++ b/legacy/src/main/java/de/robv/android/xposed/callbacks/IXUnhook.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed.callbacks; import de.robv.android.xposed.IXposedHookZygoteInit; diff --git a/core/src/main/java/de/robv/android/xposed/callbacks/XC_InitPackageResources.java b/legacy/src/main/java/de/robv/android/xposed/callbacks/XC_InitPackageResources.java similarity index 71% rename from core/src/main/java/de/robv/android/xposed/callbacks/XC_InitPackageResources.java rename to legacy/src/main/java/de/robv/android/xposed/callbacks/XC_InitPackageResources.java index 5c39ec968..cb23c43ee 100644 --- a/core/src/main/java/de/robv/android/xposed/callbacks/XC_InitPackageResources.java +++ b/legacy/src/main/java/de/robv/android/xposed/callbacks/XC_InitPackageResources.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed.callbacks; import android.content.res.XResources; diff --git a/core/src/main/java/de/robv/android/xposed/callbacks/XC_LayoutInflated.java b/legacy/src/main/java/de/robv/android/xposed/callbacks/XC_LayoutInflated.java similarity index 81% rename from core/src/main/java/de/robv/android/xposed/callbacks/XC_LayoutInflated.java rename to legacy/src/main/java/de/robv/android/xposed/callbacks/XC_LayoutInflated.java index 76de46c9a..790b565d7 100644 --- a/core/src/main/java/de/robv/android/xposed/callbacks/XC_LayoutInflated.java +++ b/legacy/src/main/java/de/robv/android/xposed/callbacks/XC_LayoutInflated.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed.callbacks; import android.content.res.XResources; diff --git a/core/src/main/java/de/robv/android/xposed/callbacks/XC_LoadPackage.java b/legacy/src/main/java/de/robv/android/xposed/callbacks/XC_LoadPackage.java similarity index 74% rename from core/src/main/java/de/robv/android/xposed/callbacks/XC_LoadPackage.java rename to legacy/src/main/java/de/robv/android/xposed/callbacks/XC_LoadPackage.java index 848e3eeb3..a502a62b9 100644 --- a/core/src/main/java/de/robv/android/xposed/callbacks/XC_LoadPackage.java +++ b/legacy/src/main/java/de/robv/android/xposed/callbacks/XC_LoadPackage.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed.callbacks; import android.content.pm.ApplicationInfo; diff --git a/core/src/main/java/de/robv/android/xposed/callbacks/XCallback.java b/legacy/src/main/java/de/robv/android/xposed/callbacks/XCallback.java similarity index 85% rename from core/src/main/java/de/robv/android/xposed/callbacks/XCallback.java rename to legacy/src/main/java/de/robv/android/xposed/callbacks/XCallback.java index 81f4fbe62..209d15279 100644 --- a/core/src/main/java/de/robv/android/xposed/callbacks/XCallback.java +++ b/legacy/src/main/java/de/robv/android/xposed/callbacks/XCallback.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed.callbacks; import android.os.Bundle; diff --git a/core/src/main/java/de/robv/android/xposed/services/BaseService.java b/legacy/src/main/java/de/robv/android/xposed/services/BaseService.java similarity index 91% rename from core/src/main/java/de/robv/android/xposed/services/BaseService.java rename to legacy/src/main/java/de/robv/android/xposed/services/BaseService.java index 7c69ee079..b912f44d3 100644 --- a/core/src/main/java/de/robv/android/xposed/services/BaseService.java +++ b/legacy/src/main/java/de/robv/android/xposed/services/BaseService.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed.services; import java.io.ByteArrayInputStream; diff --git a/core/src/main/java/de/robv/android/xposed/services/DirectAccessService.java b/legacy/src/main/java/de/robv/android/xposed/services/DirectAccessService.java similarity index 83% rename from core/src/main/java/de/robv/android/xposed/services/DirectAccessService.java rename to legacy/src/main/java/de/robv/android/xposed/services/DirectAccessService.java index a8421c855..a56c14336 100644 --- a/core/src/main/java/de/robv/android/xposed/services/DirectAccessService.java +++ b/legacy/src/main/java/de/robv/android/xposed/services/DirectAccessService.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed.services; import java.io.BufferedInputStream; diff --git a/core/src/main/java/de/robv/android/xposed/services/FileResult.java b/legacy/src/main/java/de/robv/android/xposed/services/FileResult.java similarity index 69% rename from core/src/main/java/de/robv/android/xposed/services/FileResult.java rename to legacy/src/main/java/de/robv/android/xposed/services/FileResult.java index 415f5cb9c..699583962 100644 --- a/core/src/main/java/de/robv/android/xposed/services/FileResult.java +++ b/legacy/src/main/java/de/robv/android/xposed/services/FileResult.java @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - package de.robv.android.xposed.services; import java.io.InputStream; diff --git a/core/src/main/java/org/matrix/vector/Startup.java b/legacy/src/main/java/org/matrix/vector/Startup.java similarity index 85% rename from core/src/main/java/org/matrix/vector/Startup.java rename to legacy/src/main/java/org/matrix/vector/Startup.java index b1b1619d1..f47fff407 100644 --- a/core/src/main/java/org/matrix/vector/Startup.java +++ b/legacy/src/main/java/org/matrix/vector/Startup.java @@ -21,14 +21,14 @@ public static void bootstrapXposed(boolean systemServerStarted) { } public static void initXposed(boolean isSystem, String processName, String appDir, ILSPApplicationService service) { - // 1. Establish the Dependency Injection contract + // Establish the Dependency Injection contract VectorBootstrap.INSTANCE.init(new LegacyDelegateImpl()); - // 2. Initialize legacy resources and state + // Initialize legacy resources and state XposedBridge.initXResources(); XposedInit.startsSystemServer = isSystem; - // 3. Hand off execution to the modern framework initialization + // Hand off execution to the modern framework initialization VectorStartup.init(isSystem, processName, appDir, service); } } diff --git a/core/src/main/java/org/matrix/vector/legacy/LegacyDelegateImpl.java b/legacy/src/main/java/org/matrix/vector/legacy/LegacyDelegateImpl.java similarity index 97% rename from core/src/main/java/org/matrix/vector/legacy/LegacyDelegateImpl.java rename to legacy/src/main/java/org/matrix/vector/legacy/LegacyDelegateImpl.java index 549997ee6..50006d138 100644 --- a/core/src/main/java/org/matrix/vector/legacy/LegacyDelegateImpl.java +++ b/legacy/src/main/java/org/matrix/vector/legacy/LegacyDelegateImpl.java @@ -54,7 +54,7 @@ public void onSystemServerLoaded(ClassLoader classLoader) { XposedInit.loadedPackagesInProcess.add("android"); XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam(XposedBridge.sLoadedPackageCallbacks); lpparam.packageName = "android"; - lpparam.processName = "android"; + lpparam.processName = "system_server"; lpparam.classLoader = classLoader; lpparam.isFirstApplication = true; XC_LoadPackage.callAll(lpparam); @@ -96,12 +96,12 @@ public boolean hasLegacyModule(String packageName) { @Override public void setPackageNameForResDir(String packageName, String resDir) { - // By calling a separate static inner class, we prevent the verifier + // Call a separate static inner class to prevent the verifier // from looking at XResources when LegacyDelegateImpl is loaded. ResourceProxy.set(packageName, resDir); } - // This class is only verified the FIRST time 'set' is called + // This class is only verified the FIRST time 'set' is called. private static class ResourceProxy { static void set(String p, String r) { XResources.setPackageNameForResDir(p, r); diff --git a/settings.gradle.kts b/settings.gradle.kts index abc95ae74..c623fd46f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -20,14 +20,13 @@ rootProject.name = "Vector" include( ":app", - ":core", ":daemon", ":dex2oat", ":external:axml", ":external:apache", ":hiddenapi:stubs", ":hiddenapi:bridge", - ":magisk-loader", + ":legacy", ":services:manager-service", ":services:daemon-service", ":xposed", diff --git a/xposed/README.md b/xposed/README.md index 758b4ece5..138deb4d0 100644 --- a/xposed/README.md +++ b/xposed/README.md @@ -1,35 +1,37 @@ # Xposed API implementation of the Vector framework -This subproject implements the modern **libxposed API** for the Vector framework. It serves as the primary bridge between the native ART hooking engine (`lsplant`) and module developers, providing a type-safe, OkHttp-style interceptor chain architecture. +This module implements the [libxposed](https://github.com/libxposed/api) API for the Vector framework. It serves as the primary bridge between the native ART hooking engine (`lsplant`) and module developers, providing a type-safe, OkHttp-style interceptor chain architecture. ## Architectural Overview The `xposed` module is designed with strict boundaries to ensure stability during the Android boot process and application lifecycles. It is written entirely in Kotlin and operates independently of the legacy Xposed API (`de.robv.android.xposed`). - -To prevent circular dependencies and ART class verification deadlocks, this module does **not** depend on the `core` (legacy Java) subproject. Instead, it defines a Dependency Injection (DI) contract (`LegacyFrameworkDelegate`) which the `core` module must implement and inject during startup. +It defines a Dependency Injection (DI) contract (`LegacyFrameworkDelegate`) which the `core` module must implement and inject during startup. ## Core Components -### 1. The Hooking Engine (`org.matrix.vector.impl.hooks`) +### 1. The Hooking Engine -* **`VectorHookBuilder`**: Implements the API 101 `HookBuilder`. It validates the target `Executable`, bundles the module's `Hooker`, `priority`, and `ExceptionMode` into a `VectorHookRecord`, and registers it natively via JNI. +* **`VectorHookBuilder`**: Implements the `HookBuilder` API. It validates the target `Executable`, bundles the module's `Hooker`, `priority`, and `ExceptionMode` into a `VectorHookRecord`, and registers it natively via JNI. * **`VectorNativeHooker`**: The JNI trampoline target. When a hooked method is executed, the C++ layer invokes `callback(Array)` on this class. It fetches the active hooks (both modern and legacy) from the native registry as global `jobject` references, constructs the root `VectorChain`, and initiates execution. * **`VectorChain`**: Implements the recursive `proceed()` state machine. * **Exception Handling**: It implements the logic for `ExceptionMode`. In `PROTECTIVE` mode, if an interceptor throws an exception *before* calling `proceed()`, the chain skips the interceptor. If it throws *after* calling `proceed()`, the chain catches the exception and restores the cached downstream result/throwable to protect the host process. -### 2. The Invocation System (`org.matrix.vector.impl.hooks.BaseInvoker`) +### 2. The Invocation System + The `Invoker` system allows modules to execute methods while bypassing standard JVM access checks, with granular control over hook execution. * **`Type.Origin`**: Dispatches directly to JNI (`HookBridge.invokeOriginalMethod`), bypassing all active hooks. * **`Type.Chain`**: Constructs a localized `VectorChain` containing only hooks with a priority less than or equal to the requested `maxPriority`, allowing modules to execute partial hook chains. * **`VectorCtorInvoker`**: Handles constructor invocation. It separates memory allocation (`HookBridge.allocateObject`) from initialization (`invokeOriginalMethod` / `invokeSpecialMethod`) to support safe `newInstanceSpecial` logic. -### 3. Dependency Injection Contract (`org.matrix.vector.impl.di`) +### 3. Dependency Injection Contract + To maintain the separation of concerns, the `xposed` module communicates with the legacy Xposed ecosystem via `VectorBootstrap` and `LegacyFrameworkDelegate`. -When `xposed` intercepts an Android lifecycle event (e.g., `LoadedApk.createClassLoader`), it dispatches the event internally via `VectorLifecycleManager` (for API 101 modules) and then delegates the raw parameters to `LegacyFrameworkDelegate` so the `core` module can construct and dispatch the legacy `XC_LoadPackage` callbacks. +When `xposed` intercepts an Android lifecycle event (e.g., `LoadedApk.createClassLoader`), it dispatches the event internally via `VectorLifecycleManager` and then delegates the raw parameters to `LegacyFrameworkDelegate` so the `legacy` module can construct and dispatch the legacy `XC_LoadPackage` callbacks. + +### 4. In-Memory Module ClassLoading -### 4. In-Memory Module ClassLoading (`org.matrix.vector.impl.util.VectorModuleClassLoader`) Modules are loaded directly from memory to prevent disk I/O bottlenecks and enhance security. * It utilizes `SharedMemory` buffers passed over IPC, mapping them to `ByteBuffer`s. * It extends `ByteBufferDexClassLoader` to evaluate the module's DEX files. diff --git a/xposed/build.gradle.kts b/xposed/build.gradle.kts index 1bfc89544..82c10fc6b 100644 --- a/xposed/build.gradle.kts +++ b/xposed/build.gradle.kts @@ -1,3 +1,6 @@ +val versionCodeProvider: Provider by rootProject.extra +val versionNameProvider: Provider by rootProject.extra + plugins { alias(libs.plugins.agp.lib) alias(libs.plugins.kotlin) @@ -7,20 +10,24 @@ plugins { ktfmt { kotlinLangStyle() } android { - namespace = "org.matrix.vector.xposed" + namespace = "org.matrix.vector.impl" androidResources { enable = false } + defaultConfig { + consumerProguardFiles("consumer-rules.pro") + buildConfigField("String", "FRAMEWORK_NAME", """"${rootProject.name}"""") + buildConfigField("String", "VERSION_NAME", """"${versionCodeProvider.get()}"""") + buildConfigField("long", "VERSION_CODE", versionCodeProvider.get()) + } + sourceSets { named("main") { java.srcDirs("src/main/kotlin", "libxposed/api/src/main/java") } } } dependencies { - implementation(projects.services.daemonService) - implementation(projects.external.apache) implementation(projects.external.axml) implementation(projects.hiddenapi.bridge) implementation(projects.services.daemonService) - implementation(projects.services.managerService) compileOnly(libs.androidx.annotation) compileOnly(projects.hiddenapi.stubs) } diff --git a/xposed/consumer-rules.pro b/xposed/consumer-rules.pro new file mode 100644 index 000000000..125832dec --- /dev/null +++ b/xposed/consumer-rules.pro @@ -0,0 +1,32 @@ +# Preserve the libxposed public API surface for module developers +-keep class io.github.libxposed.** { *; } + +# Preserve all native methods (HookBridge, ResourcesHook, NativeAPI, etc.) +-keepclasseswithmembers,includedescriptorclasses class * { + native ; +} + +# Preserve the JNI Hook Trampoline +-keep class org.matrix.vector.impl.hooks.VectorNativeHooker { + public (java.lang.reflect.Executable); + public java.lang.Object callback(java.lang.Object[]); +} + +# Preserve System Server hooks accessed reflectively by the `zygisk` module +-keep class org.matrix.vector.impl.hookers.HandleSystemServerProcessHooker { *; } +-keep class org.matrix.vector.impl.hookers.HandleSystemServerProcessHooker$Callback { *; } + +# Preserve the Initialization entry points called by the `legacy` module +-keep class org.matrix.vector.impl.core.VectorStartup { + public static ; +} +-keep class org.matrix.vector.impl.di.VectorBootstrap { + public static ; +} +-keep class org.matrix.vector.impl.di.LegacyFrameworkDelegate { *; } + +# Strip debug logs in release builds +-assumenosideeffects class android.util.Log { + public static *** v(...); + public static *** d(...); +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/api/utils/DexParser.kt b/xposed/src/main/kotlin/org/matrix/vector/api/utils/DexParser.kt deleted file mode 100644 index 4f8cf6dcf..000000000 --- a/xposed/src/main/kotlin/org/matrix/vector/api/utils/DexParser.kt +++ /dev/null @@ -1,342 +0,0 @@ -package org.matrix.vector.api.utils - -import java.io.Closeable - -/** Xposed interface for parsing dex files. */ -@Suppress("unused") -interface DexParser : Closeable { - - companion object { - /** The constant NO_INDEX. */ - const val NO_INDEX: Int = -1 // 0xffffffff - } - - /** The interface Array. */ - interface Array { - /** - * Get values value [ ]. - * - * @return the value [ ] - */ - val values: kotlin.Array - } - - /** The interface Annotation. */ - interface Annotation { - /** - * Gets visibility. - * - * @return the visibility - */ - val visibility: Int - - /** - * Gets type. - * - * @return the type - */ - val type: TypeId - - /** - * Get elements element [ ]. - * - * @return the element [ ] - */ - val elements: kotlin.Array - } - - /** The interface Value. */ - interface Value { - /** - * Get value byte [ ]. - * - * @return the byte [ ] - */ - val value: ByteArray? - - /** - * Gets value type. - * - * @return the value type - */ - val valueType: Int - } - - /** The interface Element. */ - interface Element : Value { - /** - * Gets name. - * - * @return the name - */ - val name: StringId - } - - /** The interface Id. */ - interface Id : Comparable { - /** - * Gets id. - * - * @return the id - */ - val id: Int - } - - /** The interface Type id. */ - interface TypeId : Id { - /** - * Gets descriptor. - * - * @return the descriptor - */ - val descriptor: StringId - } - - /** The interface String id. */ - interface StringId : Id { - /** - * Gets string. - * - * @return the string - */ - val string: String - } - - /** The interface Field id. */ - interface FieldId : Id { - /** - * Gets type. - * - * @return the type - */ - val type: TypeId - - /** - * Gets declaring class. - * - * @return the declaring class - */ - val declaringClass: TypeId - - /** - * Gets name. - * - * @return the name - */ - val name: StringId - } - - /** The interface Method id. */ - interface MethodId : Id { - /** - * Gets declaring class. - * - * @return the declaring class - */ - val declaringClass: TypeId - - /** - * Gets prototype. - * - * @return the prototype - */ - val prototype: ProtoId - - /** - * Gets name. - * - * @return the name - */ - val name: StringId - } - - /** The interface Proto id. */ - interface ProtoId : Id { - /** - * Gets shorty. - * - * @return the shorty - */ - val shorty: StringId - - /** - * Gets return type. - * - * @return the return type - */ - val returnType: TypeId - - /** - * Get parameters type id [ ]. - * - * @return the type id [ ] - */ - val parameters: kotlin.Array? - } - - /** - * Get string id string id [ ]. - * - * @return the string id [ ] - */ - val stringId: kotlin.Array - - /** - * Get type id type id [ ]. - * - * @return the type id[ ] - */ - val typeId: kotlin.Array - - /** - * Get field id field id [ ]. - * - * @return the field id [ ] - */ - val fieldId: kotlin.Array - - /** - * Get method id method id [ ]. - * - * @return the method id [ ] - */ - val methodId: kotlin.Array - - /** - * Get proto id proto id [ ]. - * - * @return the proto id [ ] - */ - val protoId: kotlin.Array - - /** - * Get annotations annotation [ ]. - * - * @return the annotation [ ] - */ - val annotations: kotlin.Array - - /** - * Get arrays array [ ]. - * - * @return the array [ ] - */ - val arrays: kotlin.Array - - /** The interface Early stop visitor. */ - interface EarlyStopVisitor { - /** - * Stop boolean. - * - * @return the boolean - */ - fun stop(): Boolean - } - - /** The interface Member visitor. */ - interface MemberVisitor : EarlyStopVisitor - - /** The interface Class visitor. */ - interface ClassVisitor : EarlyStopVisitor { - /** - * Visit member visitor. - * - * @param clazz the clazz - * @param accessFlags the access flags - * @param superClass the super class - * @param interfaces the interfaces - * @param sourceFile the source file - * @param staticFields the static fields - * @param staticFieldsAccessFlags the static fields access flags - * @param instanceFields the instance fields - * @param instanceFieldsAccessFlags the instance fields access flags - * @param directMethods the direct methods - * @param directMethodsAccessFlags the direct methods access flags - * @param virtualMethods the virtual methods - * @param virtualMethodsAccessFlags the virtual methods access flags - * @param annotations the annotations - * @return the member visitor - */ - fun visit( - clazz: Int, - accessFlags: Int, - superClass: Int, - interfaces: IntArray, - sourceFile: Int, - staticFields: IntArray, - staticFieldsAccessFlags: IntArray, - instanceFields: IntArray, - instanceFieldsAccessFlags: IntArray, - directMethods: IntArray, - directMethodsAccessFlags: IntArray, - virtualMethods: IntArray, - virtualMethodsAccessFlags: IntArray, - annotations: IntArray, - ): MemberVisitor? - } - - /** The interface Field visitor. */ - interface FieldVisitor : MemberVisitor { - /** - * Visit. - * - * @param field the field - * @param accessFlags the access flags - * @param annotations the annotations - */ - fun visit(field: Int, accessFlags: Int, annotations: IntArray) - } - - /** The interface Method visitor. */ - interface MethodVisitor : MemberVisitor { - /** - * Visit method body visitor. - * - * @param method the method - * @param accessFlags the access flags - * @param hasBody the has body - * @param annotations the annotations - * @param parameterAnnotations the parameter annotations - * @return the method body visitor - */ - fun visit( - method: Int, - accessFlags: Int, - hasBody: Boolean, - annotations: IntArray, - parameterAnnotations: IntArray, - ): MethodBodyVisitor? - } - - /** The interface Method body visitor. */ - interface MethodBodyVisitor { - /** - * Visit. - * - * @param method the method - * @param accessFlags the access flags - * @param referredStrings the referred strings - * @param invokedMethods the invoked methods - * @param accessedFields the accessed fields - * @param assignedFields the assigned fields - * @param opcodes the opcodes - */ - fun visit( - method: Int, - accessFlags: Int, - referredStrings: IntArray, - invokedMethods: IntArray, - accessedFields: IntArray, - assignedFields: IntArray, - opcodes: ByteArray, - ) - } - - /** - * Visit defined classes. - * - * @param visitor the visitor - * @throws IllegalStateException the illegal state exception - */ - @Throws(IllegalStateException::class) fun visitDefinedClasses(visitor: ClassVisitor) -} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/VectorContext.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorContext.kt index 3c9cedc42..b81d316ac 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/VectorContext.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorContext.kt @@ -30,11 +30,11 @@ class VectorContext( private val remotePrefs = ConcurrentHashMap() - override fun getFrameworkName(): String = "Vector" + override fun getFrameworkName(): String = BuildConfig.FRAMEWORK_NAME - override fun getFrameworkVersion(): String = "1.0.0" + override fun getFrameworkVersion(): String = BuildConfig.VERSION_NAME - override fun getFrameworkVersionCode(): Long = 1L + override fun getFrameworkVersionCode(): Long = BuildConfig.VERSION_CODE override fun getFrameworkProperties(): Long { var props = 0L @@ -102,6 +102,8 @@ class VectorContext( /** Manages the dispatching of modern lifecycle events to loaded modules. */ object VectorLifecycleManager { + private const val TAG = "VectorLifecycle" + val activeModules: MutableSet = ConcurrentHashMap.newKeySet() fun dispatchPackageLoaded( @@ -125,7 +127,7 @@ object VectorLifecycleManager { runCatching { module.onPackageLoaded(param) } .onFailure { Log.e( - "VectorLifecycle", + TAG, "Error in onPackageLoaded for ${module.moduleApplicationInfo.packageName}", it, ) @@ -163,7 +165,7 @@ object VectorLifecycleManager { runCatching { module.onPackageReady(param) } .onFailure { Log.e( - "VectorLifecycle", + TAG, "Error in onPackageReady for ${module.moduleApplicationInfo.packageName}", it, ) @@ -181,7 +183,7 @@ object VectorLifecycleManager { runCatching { module.onSystemServerStarting(param) } .onFailure { Log.e( - "VectorLifecycle", + TAG, "Error in onSystemServerStarting for ${module.moduleApplicationInfo.packageName}", it, ) diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/VectorRemotePreferences.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorRemotePreferences.kt index 60d209b7e..db50b0cc6 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/VectorRemotePreferences.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorRemotePreferences.kt @@ -4,10 +4,12 @@ import android.content.SharedPreferences import android.os.Bundle import android.os.RemoteException import android.util.ArraySet +import io.github.libxposed.api.error.XposedFrameworkError import java.util.TreeMap import java.util.concurrent.ConcurrentHashMap import org.lsposed.lspd.service.ILSPInjectedModuleService import org.lsposed.lspd.service.IRemotePreferenceCallback +import org.lsposed.lspd.util.Utils.Log @Suppress("DEPRECATION", "UNCHECKED_CAST") private inline fun Bundle.getSerializableCompat(key: String): T? { @@ -72,7 +74,8 @@ internal class VectorRemotePreferences(service: ILSPInjectedModuleService, group } } } catch (e: RemoteException) { - // Initial load failed. Error handling is deferred to the caller's context if necessary. + Log.e("VectorContext", "Failed to request remote preferences for group: $group", e) + throw XposedFrameworkError("Remote preferences IPC failure", e) } } diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorDeopter.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorDeopter.kt index 0b8e4bf8f..4ca9f47a8 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorDeopter.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorDeopter.kt @@ -10,6 +10,8 @@ import org.matrix.vector.nativebridge.HookBridge */ object VectorDeopter { + private const val TAG = "VectorDeopter" + @JvmStatic fun deoptMethods(where: String, cl: ClassLoader?) { val targets = VectorInlinedCallers.get(where) @@ -32,7 +34,10 @@ object VectorDeopter { HookBridge.deoptimizeMethod(executable) } .onFailure { - // Log resolution or deoptimization failure silently + Utils.Log.v( + TAG, + "Skipping deopt for ${target.className}#${target.methodName}: ${it.message}", + ) } } } diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorInlinedCallers.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorInlinedCallers.kt index 8b97f0bda..839ecca84 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorInlinedCallers.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorInlinedCallers.kt @@ -35,10 +35,7 @@ data class TargetExecutable( } } -/** - * Provides a registry of methods known to inline target framework hooks. Deoptimizing these callers - * forces the ART runtime to fall back to intercept mode. - */ +/** Provides a registry of methods known to inline target framework hooks. */ object VectorInlinedCallers { const val KEY_BOOT_IMAGE = "boot_image" const val KEY_BOOT_IMAGE_MIUI_RES = "boot_image_miui_res" @@ -151,7 +148,6 @@ object VectorInlinedCallers { ) callers[KEY_SYSTEM_SERVER] = emptyList() - callers["com.android.systemui"] = emptyList() } fun get(where: String): List = callers[where] ?: emptyList() diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt index 96e9178f9..5cc491bde 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt @@ -14,8 +14,8 @@ import org.matrix.vector.impl.utils.VectorModuleClassLoader import org.matrix.vector.nativebridge.NativeAPI /** - * Responsible for loading modern (API 101) modules into the target process. Handles ClassLoader - * isolation and injects the framework context into the module instances. + * Responsible for loading modules into the target process. Handles ClassLoader isolation and + * injects the framework context into the module instances. */ object VectorModuleManager { @@ -28,7 +28,7 @@ object VectorModuleManager { try { Log.d(TAG, "Loading module ${module.packageName}") - // 1. Construct the native library search path + // Construct the native library search path val librarySearchPath = buildString { val abis = if (Process.is64Bit()) Build.SUPPORTED_64_BIT_ABIS @@ -38,7 +38,7 @@ object VectorModuleManager { } } - // 2. Create the isolated ClassLoader for the module + // Create the isolated ClassLoader for the module val initLoader = XposedModule::class.java.classLoader val moduleClassLoader = VectorModuleClassLoader.loadApk( @@ -53,15 +53,11 @@ object VectorModuleManager { moduleClassLoader.loadClass(XposedModule::class.java.name).classLoader !== initLoader ) { - Log.e(TAG, " Cannot load module: ${module.packageName}") - Log.e( - TAG, - " The Xposed API classes are compiled into the module's APK (implementation instead of compileOnly).", - ) + Log.e(TAG, "The Xposed API classes are compiled into ${module.packageName}") return false } - // 3. Create the Context that will be injected into the module + // Create the Context that will be injected into the module val vectorContext = VectorContext( packageName = module.packageName, @@ -69,18 +65,17 @@ object VectorModuleManager { service = module.service, // Our IPC client ) - // 4. Instantiate the module entry classes + // Instantiate the module entry classes for (className in module.file.moduleClassNames) { runCatching { val moduleClass = moduleClassLoader.loadClass(className) - Log.d(TAG, " Loading class $moduleClass") + Log.d(TAG, "Loading class $moduleClass") if (!XposedModule::class.java.isAssignableFrom(moduleClass)) { - Log.e(TAG, " Class does not extend XposedModule, skipping.") + Log.e(TAG, "Class does not extend XposedModule, skipping.") return@runCatching } - // Look for the constructor required by API 101 val constructor = moduleClass.getConstructor( XposedInterface::class.java, @@ -113,7 +108,7 @@ object VectorModuleManager { .onFailure { e -> Log.e(TAG, " Failed to instantiate class $className", e) } } - // 5. Register any native JNI entrypoints declared by the module + // Register any native JNI entrypoints declared by the module module.file.moduleLibraryNames.forEach { libraryName -> NativeAPI.recordNativeEntrypoint(libraryName) } diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorServiceClient.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorServiceClient.kt index a10e9520c..fc70e9097 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorServiceClient.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorServiceClient.kt @@ -4,6 +4,7 @@ import android.os.IBinder import android.os.ParcelFileDescriptor import org.lsposed.lspd.models.Module import org.lsposed.lspd.service.ILSPApplicationService +import org.lsposed.lspd.util.Utils.Log /** * Singleton client for managing IPC communication with the injected manager service. Handles Binder @@ -11,6 +12,8 @@ import org.lsposed.lspd.service.ILSPApplicationService */ object VectorServiceClient : ILSPApplicationService, IBinder.DeathRecipient { + private const val TAG = "VectorServiceClient" + private var service: ILSPApplicationService? = null var processName: String = "" private set @@ -25,7 +28,8 @@ object VectorServiceClient : ILSPApplicationService, IBinder.DeathRecipient { binder.linkToDeath(this, 0) } .onFailure { - // Log failure to link to death + Log.e(TAG, "Failed to link to death for service in process: $niceName", it) + service = null } } } diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorStartup.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorStartup.kt index 5d5ecb668..cbdb2afb9 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorStartup.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorStartup.kt @@ -25,14 +25,14 @@ object VectorStartup { @JvmStatic fun bootstrap(isSystem: Boolean, systemServerStarted: Boolean) { - // 1. Crash Dump Interceptor + // Crash Dump Interceptor Thread::class .java .declaredMethods .firstOrNull { it.name == "dispatchUncaughtException" } ?.let { VectorHookBuilder(it).intercept(CrashDumpHooker) } - // 2. Process-specific Interceptors + // Process-specific Interceptors if (isSystem) { val zygoteInitClass = Class.forName("com.android.internal.os.ZygoteInit") zygoteInitClass.declaredMethods @@ -50,7 +50,7 @@ object VectorStartup { .forEach { VectorHookBuilder(it).intercept(DexTrustHooker) } } - // 3. Application Load Interceptors + // Application Load Interceptors val loadedApkClass = Class.forName("android.app.LoadedApk") loadedApkClass.declaredConstructors.forEach { // Hook all constructors of LoadedApk to catch early instantiations securely @@ -61,13 +61,13 @@ object VectorStartup { .filter { it.name == "createOrUpdateClassLoaderLocked" } .forEach { VectorHookBuilder(it).intercept(LoadedApkCreateCLHooker) } - // 4. ActivityThread Attachment Interceptor + // ActivityThread Attachment Interceptor val activityThreadClass = Class.forName("android.app.ActivityThread") activityThreadClass.declaredMethods .filter { it.name == "attach" } .forEach { VectorHookBuilder(it).intercept(AppAttachHooker) } - // 5. Late System Server Injection + // Late System Server Injection if (systemServerStarted) { val activityService: IBinder? = android.os.ServiceManager.getService("activity") if (activityService != null) { diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/di/VectorBootstrap.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/di/VectorBootstrap.kt index ef1e0fc8c..b10b0c8f7 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/di/VectorBootstrap.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/di/VectorBootstrap.kt @@ -18,8 +18,8 @@ fun interface OriginalInvoker { } /** - * The explicit contract that the `core` (legacy) module must fulfill. The modern framework will - * call these methods at the appropriate lifecycle moments. + * The explicit contract that the `legacy` module must fulfill. The modern framework will call these + * methods at the appropriate lifecycle moments. */ interface LegacyFrameworkDelegate { /** Instructs the legacy bridge to load legacy modules. */ diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/CrashDumpHooker.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/CrashDumpHooker.kt index 5316dc525..bf696175c 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/CrashDumpHooker.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/CrashDumpHooker.kt @@ -1,7 +1,7 @@ package org.matrix.vector.impl.hookers -import android.util.Log import io.github.libxposed.api.XposedInterface +import org.lsposed.lspd.util.Utils /** * Intercepts uncaught exceptions in the framework to provide diagnostic logging before the process @@ -12,7 +12,7 @@ object CrashDumpHooker : XposedInterface.Hooker { try { val throwable = chain.args.firstOrNull() as? Throwable if (throwable != null) { - Log.e("Vector", "Crash unexpectedly", throwable) + Utils.logE("Crash unexpectedly", throwable) } } catch (ignored: Throwable) {} return chain.proceed() diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt index 2b6eba6c8..53751f57c 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt @@ -59,6 +59,9 @@ object LoadedApkCreateCLHooker : XposedInterface.Hooker { val result = chain.proceed() val loadedApk = chain.thisObject ?: return result + // LoadedApk.createOrUpdateClassLoaderLocked(List addedPaths) is called with + // addedPaths == null when Android is creating the ClassLoader for the very first time for + // this app. if ( chain.args.firstOrNull() != null || !LoadedApkCtorHooker.trackedApks.contains(loadedApk) ) { @@ -90,7 +93,7 @@ object LoadedApkCreateCLHooker : XposedInterface.Hooker { val appInfo = loadedApk.getFieldValue("mApplicationInfo") ?: return result - // 1. Dispatch Modern Lifecycle + // Dispatch Modern Lifecycle val defaultClassLoader = loadedApk.getFieldValue("mDefaultClassLoader") ?: classLoader VectorLifecycleManager.dispatchPackageLoaded( @@ -100,7 +103,7 @@ object LoadedApkCreateCLHooker : XposedInterface.Hooker { defaultClassLoader, ) - // 2. Dispatch Legacy Lifecycle + // Dispatch Legacy Lifecycle VectorBootstrap.withLegacy { delegate -> delegate.onPackageLoaded( LegacyPackageInfo( diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/SystemServerHookers.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/SystemServerHookers.kt index 360907031..3fa05b3cf 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/SystemServerHookers.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/SystemServerHookers.kt @@ -65,10 +65,10 @@ object StartBootstrapServicesHooker : XposedInterface.Hooker { /** Dispatches module loading events. */ fun dispatchSystemServerLoaded(classLoader: ClassLoader) { - // 1. Dispatch to modern framework modules + // Dispatch to modern framework modules VectorLifecycleManager.dispatchSystemServerStarting(classLoader) - // 2. Dispatch to legacy framework modules + // Dispatch to legacy framework modules VectorBootstrap.withLegacy { delegate -> delegate.onSystemServerLoaded(classLoader) } } } diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/BaseInvoker.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/BaseInvoker.kt index 5112e4071..013d57914 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/BaseInvoker.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/BaseInvoker.kt @@ -136,9 +136,9 @@ internal class VectorCtorInvoker(constructor: Constructor) : @Suppress("UNCHECKED_CAST") override fun newInstance(vararg args: Any?): T { - // 1. Allocate memory without invoking + // Allocate memory without invoking val obj = HookBridge.allocateObject(executable.declaringClass) - // 2. Drive the invocation (origin or chain) utilizing the allocated object + // Drive the invocation (origin or chain) utilizing the allocated object proceedInvocation(obj, args) return obj } diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt index 8c6c8b234..78dd625f0 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt @@ -4,7 +4,7 @@ import io.github.libxposed.api.XposedInterface import io.github.libxposed.api.XposedInterface.Chain import io.github.libxposed.api.XposedInterface.ExceptionMode import java.lang.reflect.Executable -import org.lsposed.lspd.util.Utils.Log +import org.lsposed.lspd.util.Utils /** Represents a registered hook configuration, stored natively by [HookBridge]. */ data class VectorHookRecord( @@ -94,15 +94,15 @@ class VectorChain( } // DEFAULT or PROTECTIVE mode: log the crash and attempt to rescue the execution. - Log.e("VectorChain", "Hooker threw exception: ${record.hooker.javaClass.name}", t) + Utils.logE("Hooker threw exception: ${record.hooker.javaClass.name}", t) if (!nextChain.proceedCalled) { // Crash occurred BEFORE proceed(). // Skip this hooker entirely and drive the chain manually. return nextChain.proceedWith(recoverThis, recoverArgs) } else { - // Crash occurred AFTER proceed(). Swallow the module's crash and return the real - // downstream state. + // Crash occurred AFTER proceed(). + // Swallow the module's crash and return the real downstream state. nextChain.downstreamThrowable?.let { throw it } return nextChain.downstreamResult } diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorDexParser.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorDexParser.kt deleted file mode 100644 index d95335061..000000000 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorDexParser.kt +++ /dev/null @@ -1,248 +0,0 @@ -package org.matrix.vector.impl.utils - -import java.io.IOException -import java.nio.ByteBuffer -import org.matrix.vector.api.utils.DexParser -import org.matrix.vector.api.utils.DexParser.* -import org.matrix.vector.nativebridge.DexParserBridge - -/** - * Kotlin implementation of [DexParser] for Vector. - * - * This class acts as a high-level wrapper around the native C++ DexParser. It maps raw JNI data - * structures (integer arrays, flat buffers) into usable object graphs (StringId, TypeId, MethodId, - * etc.). - */ -@Suppress("UNCHECKED_CAST") -class VectorDexParser(buffer: ByteBuffer, includeAnnotations: Boolean) : DexParser { - - private var cookie: Long = 0 - private val data: ByteBuffer - - // Properties directly override the interface properties. - // Kotlin automatically generates private backing fields for these. - override val stringId: Array - override val typeId: Array - override val protoId: Array - override val fieldId: Array - override val methodId: Array - override val annotations: Array - override val arrays: Array - - init { - // Ensure the buffer is Direct and accessible by native code - data = - if (!buffer.isDirect || !buffer.asReadOnlyBuffer().hasArray()) { - ByteBuffer.allocateDirect(buffer.capacity()).apply { - put(buffer) - // Ensure position is reset for reading if needed, - // though native uses address - flip() - } - } else { - buffer - } - - try { - val args = LongArray(2) - args[1] = if (includeAnnotations) 1L else 0L - - // Call Native Bridge - // Returns a raw Object[] containing headers and pools - val out = DexParserBridge.openDex(data, args) as Array - cookie = args[0] - - // --- Parse Strings (Index 0) --- - val rawStrings = out[0] as Array - stringId = Array(rawStrings.size) { i -> VectorStringId(i, rawStrings[i]) } - - // --- Parse Type IDs (Index 1) --- - val rawTypeIds = out[1] as IntArray - typeId = Array(rawTypeIds.size) { i -> VectorTypeId(i, rawTypeIds[i]) } - - // --- Parse Proto IDs (Index 2) --- - val rawProtoIds = out[2] as Array - protoId = Array(rawProtoIds.size) { i -> VectorProtoId(i, rawProtoIds[i]) } - - // --- Parse Field IDs (Index 3) --- - val rawFieldIds = out[3] as IntArray - // Each field is represented by 3 integers (class_idx, type_idx, name_idx) - fieldId = - Array(rawFieldIds.size / 3) { i -> - VectorFieldId( - i, - rawFieldIds[3 * i], - rawFieldIds[3 * i + 1], - rawFieldIds[3 * i + 2], - ) - } - - // --- Parse Method IDs (Index 4) --- - val rawMethodIds = out[4] as IntArray - // Each method is represented by 3 integers (class_idx, proto_idx, name_idx) - methodId = - Array(rawMethodIds.size / 3) { i -> - VectorMethodId( - i, - rawMethodIds[3 * i], - rawMethodIds[3 * i + 1], - rawMethodIds[3 * i + 2], - ) - } - - // --- Parse Annotations (Index 5 & 6) --- - val rawAnnotationMetadata = out[5] as? IntArray - val rawAnnotationValues = out[6] as? Array - - annotations = - if (rawAnnotationMetadata != null && rawAnnotationValues != null) { - Array(rawAnnotationMetadata.size / 2) { i -> - // Metadata:[visibility, type_idx] - // Values: [name_indices[], values[]] - val elementsMeta = rawAnnotationValues[2 * i] as IntArray - val elementsData = rawAnnotationValues[2 * i + 1] as Array - VectorAnnotation( - rawAnnotationMetadata[2 * i], - rawAnnotationMetadata[2 * i + 1], - elementsMeta, - elementsData, - ) - } - } else { - emptyArray() - } - - // --- Parse Arrays (Index 7) --- - val rawArrays = out[7] as? Array - arrays = - if (rawArrays != null) { - Array(rawArrays.size / 2) { i -> - val types = rawArrays[2 * i] as IntArray - val values = rawArrays[2 * i + 1] as Array - VectorArray(types, values) - } - } else { - emptyArray() - } - } catch (e: Throwable) { - throw IOException("Invalid dex file", e) - } - } - - @Synchronized - override fun close() { - if (cookie != 0L) { - DexParserBridge.closeDex(cookie) - cookie = 0L - } - } - - override fun visitDefinedClasses(visitor: ClassVisitor) { - if (cookie == 0L) { - throw IllegalStateException("Closed") - } - - // Accessing [0] is fragile - val classVisitMethod = ClassVisitor::class.java.declaredMethods[0] - val fieldVisitMethod = FieldVisitor::class.java.declaredMethods[0] - val methodVisitMethod = MethodVisitor::class.java.declaredMethods[0] - val methodBodyVisitMethod = MethodBodyVisitor::class.java.declaredMethods[0] - val stopMethod = EarlyStopVisitor::class.java.declaredMethods[0] - - DexParserBridge.visitClass( - cookie, - visitor, - FieldVisitor::class.java, - MethodVisitor::class.java, - classVisitMethod, - fieldVisitMethod, - methodVisitMethod, - methodBodyVisitMethod, - stopMethod, - ) - } - - /** Base implementation for all Dex IDs. */ - private open class VectorId>(override val id: Int) : Id { - override fun compareTo(other: Self): Int = id - other.id - } - - private inner class VectorStringId(id: Int, override val string: String) : - VectorId(id), StringId - - private inner class VectorTypeId(id: Int, descriptorIdx: Int) : VectorId(id), TypeId { - override val descriptor: StringId = stringId[descriptorIdx] - } - - private inner class VectorProtoId(id: Int, protoData: IntArray) : - VectorId(id), ProtoId { - - override val shorty: StringId = stringId[protoData[0]] - override val returnType: TypeId = typeId[protoData[1]] - override val parameters: Array? = - if (protoData.size > 2) { - // protoData format:[shorty_idx, return_type_idx, param1_idx, param2_idx...] - Array(protoData.size - 2) { i -> typeId[protoData[i + 2]] } - } else { - null - } - } - - private inner class VectorFieldId(id: Int, classIdx: Int, typeIdx: Int, nameIdx: Int) : - VectorId(id), FieldId { - - override val declaringClass: TypeId = typeId[classIdx] - override val type: TypeId = typeId[typeIdx] - override val name: StringId = stringId[nameIdx] - } - - private inner class VectorMethodId(id: Int, classIdx: Int, protoIdx: Int, nameIdx: Int) : - VectorId(id), MethodId { - - override val declaringClass: TypeId = typeId[classIdx] - override val prototype: ProtoId = protoId[protoIdx] - override val name: StringId = stringId[nameIdx] - } - - private class VectorArray(elementsTypes: IntArray, valuesData: Array) : DexParser.Array { - override val values: Array = - Array(valuesData.size) { i -> - VectorValue(elementsTypes[i], valuesData[i] as? ByteBuffer) - } - } - - private inner class VectorAnnotation( - override val visibility: Int, - typeIdx: Int, - elementNameIndices: IntArray, - elementValues: Array, - ) : DexParser.Annotation { - - override val type: TypeId = typeId[typeIdx] - override val elements: Array = - Array(elementValues.size) { i -> - // Flattened structure from JNI: names are at 2*i, types at 2*i+1 - VectorElement( - elementNameIndices[i * 2], - elementNameIndices[i * 2 + 1], // valueType - elementValues[i] as? ByteBuffer, - ) - } - } - - private open class VectorValue(override val valueType: Int, buffer: ByteBuffer?) : Value { - - override val value: ByteArray? = - buffer?.let { - val bytes = ByteArray(it.remaining()) - it.get(bytes) - bytes - } - } - - private inner class VectorElement(nameIdx: Int, valueType: Int, buffer: ByteBuffer?) : - VectorValue(valueType, buffer), Element { - - override val name: StringId = stringId[nameIdx] - } -} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorURLStreamHandler.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorURLStreamHandler.kt index 6fa51a304..ac741adfa 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorURLStreamHandler.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorURLStreamHandler.kt @@ -25,7 +25,6 @@ internal class VectorURLStreamHandler(jarFileName: String) : Handler() { fun getEntryUrlOrNull(entryName: String): URL? { if (jarFile.getEntry(entryName) != null) { return try { - // Using Android's Uri.encode instead of internal sun.net.www.ParseUtil val encodedName = Uri.encode(entryName, "/") URL("jar", null, -1, "$fileUri!/$encodedName", this) } catch (e: MalformedURLException) { diff --git a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/DexParserBridge.kt b/xposed/src/main/kotlin/org/matrix/vector/nativebridge/DexParserBridge.kt deleted file mode 100644 index 17f8a6ab2..000000000 --- a/xposed/src/main/kotlin/org/matrix/vector/nativebridge/DexParserBridge.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.matrix.vector.nativebridge - -import dalvik.annotation.optimization.FastNative -import java.io.IOException -import java.lang.reflect.Method -import java.nio.ByteBuffer -import org.matrix.vector.api.utils.DexParser - -object DexParserBridge { - @JvmStatic - @FastNative - @Throws(IOException::class) - external fun openDex(data: ByteBuffer, args: LongArray): Any - - @JvmStatic @FastNative external fun closeDex(cookie: Long) - - @JvmStatic - @FastNative - external fun visitClass( - cookie: Long, - visitor: Any, - fieldVisitorClass: Class, - methodVisitorClass: Class, - classVisitMethod: Method, - fieldVisitMethod: Method, - methodVisitMethod: Method, - methodBodyVisitMethod: Method, - stopMethod: Method, - ) -} diff --git a/zygisk/build.gradle.kts b/zygisk/build.gradle.kts index 2a3b31354..1cb090d84 100644 --- a/zygisk/build.gradle.kts +++ b/zygisk/build.gradle.kts @@ -54,8 +54,8 @@ abstract class Injected @Inject constructor(val moduleDir: String) { } dependencies { - implementation(projects.core) implementation(projects.hiddenapi.bridge) + implementation(projects.legacy) implementation(projects.services.managerService) implementation(projects.services.daemonService) compileOnly(libs.androidx.annotation) diff --git a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt index 819d901a4..b8037f12f 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt @@ -319,7 +319,7 @@ object ParasiticManagerHooker { // Create a fake original context to satisfy internal package checks info.applicationInfo.packageName = "$managerPackage.origin" val compatibilityInfo = - HiddenApiBridge.Resources_getCompatibilityInfo(ctx!!.resources) + HiddenApiBridge.Resources_getCompatibilityInfo(ctx.resources) val originalPkgInfo = ActivityThread.currentActivityThread() .getPackageInfoNoCheck(info.applicationInfo, compatibilityInfo) From 6862c5f17f83de945a031608f99fd80e564ef373 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Mon, 23 Mar 2026 15:54:30 +0100 Subject: [PATCH 17/34] Fix typos and disable DexParserBridge --- native/src/core/context.cpp | 2 +- .../vector/impl/hooks/VectorNativeHooker.kt | 42 +++++++++++++++---- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/native/src/core/context.cpp b/native/src/core/context.cpp index 2db2e1032..79a2f2bb9 100644 --- a/native/src/core/context.cpp +++ b/native/src/core/context.cpp @@ -102,7 +102,7 @@ void Context::InitHooks(JNIEnv *env) { jni::RegisterResourcesHook(env); jni::RegisterHookBridge(env); jni::RegisterNativeApiBridge(env); - jni::RegisterDexParserBridge(env); + // jni::RegisterDexParserBridge(env); } lsplant::ScopedLocalRef Context::FindClassFromLoader(JNIEnv *env, jobject class_loader, diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt index b210134d2..e9645fb50 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt @@ -10,6 +10,7 @@ import java.lang.reflect.Executable import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method import java.lang.reflect.Modifier +import org.lsposed.lspd.util.Utils import org.matrix.vector.impl.di.VectorBootstrap import org.matrix.vector.nativebridge.HookBridge @@ -99,21 +100,44 @@ class VectorNativeHooker(private val method: T) { // Type safety validation before returning to C++ if (returnType != null && returnType != Void.TYPE) { - if (result == null && returnType.isPrimitive) { - throw NullPointerException( - "Hook returned null for a primitive return type: $method" - ) - } - if (result != null && HookBridge.instanceOf(result, returnType)) { - throw ClassCastException( - "Hook return type mismatch. Expected ${returnType.name}, got ${result.javaClass.name}" - ) + if (result == null) { + if (returnType.isPrimitive) { + throw NullPointerException( + "Hook returned null for a primitive return type: $method" + ) + } + } else { + // Use the JNI bridge for the most reliable type check across ClassLoaders + if ( + !HookBridge.instanceOf(result, returnType) && + !isBoxingCompatible(result, returnType) + ) { + Utils.logD( + "Hook return type mismatch. Expected ${returnType.name}, got ${result.javaClass.name}" + ) + } } } return result } + /** Handles primitive boxing compatibility (e.g., Integer object vs int primitive). */ + private fun isBoxingCompatible(obj: Any, targetType: Class<*>): Boolean { + if (!targetType.isPrimitive) return false + return when (targetType) { + Int::class.javaPrimitiveType -> obj is Int + Long::class.javaPrimitiveType -> obj is Long + Boolean::class.javaPrimitiveType -> obj is Boolean + Double::class.javaPrimitiveType -> obj is Double + Float::class.javaPrimitiveType -> obj is Float + Byte::class.javaPrimitiveType -> obj is Byte + Char::class.javaPrimitiveType -> obj is Char + Short::class.javaPrimitiveType -> obj is Short + else -> false + } + } + /** Safely invokes the original method, unwrapping InvocationTargetExceptions. */ private fun invokeOriginalSafely(tObj: Any?, tArgs: Array): Any? { return try { From 2dcf37689230ab04d8c9bffe086ef247902e178c Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Mon, 23 Mar 2026 18:00:00 +0100 Subject: [PATCH 18/34] Handle exceptions gracefully --- .../matrix/vector/impl/hooks/VectorChain.kt | 26 +++++++------ .../vector/impl/utils/VectorMetaDataReader.kt | 38 +++++++++---------- 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt index 78dd625f0..cd8b75058 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorChain.kt @@ -81,28 +81,32 @@ class VectorChain( } } - /** Handles exceptions thrown by module interceptors. */ + /** Handles exceptions thrown by a hooker according to its [ExceptionMode]. */ private fun handleInterceptorException( t: Throwable, record: VectorHookRecord, nextChain: VectorChain, - recoverThis: Any, - recoverArgs: Array, + recoveryThis: Any, + recoveryArgs: Array, ): Any? { - if (record.exceptionMode == ExceptionMode.PASSTHROUGH) { + // Check if the exception originated from downstream (lower hooks or original method) + if (nextChain.proceedCalled && t === nextChain.downstreamThrowable) { throw t } - // DEFAULT or PROTECTIVE mode: log the crash and attempt to rescue the execution. - Utils.logE("Hooker threw exception: ${record.hooker.javaClass.name}", t) + // Passthrough mode does not rescue the process from hooker crashes + if (record.exceptionMode == ExceptionMode.PASSTHROUGH) { + throw t + } + val hookerName = record.hooker.javaClass.name if (!nextChain.proceedCalled) { - // Crash occurred BEFORE proceed(). - // Skip this hooker entirely and drive the chain manually. - return nextChain.proceedWith(recoverThis, recoverArgs) + // Crash occurred before calling proceed(); skip hooker and continue the chain + Utils.logD("Hooker [$hookerName] crashed before proceed. Skipping.", t) + return nextChain.proceedWith(recoveryThis, recoveryArgs) } else { - // Crash occurred AFTER proceed(). - // Swallow the module's crash and return the real downstream state. + // Crash occurred after calling proceed(); suppress and restore downstream state + Utils.logD("Hooker [$hookerName] crashed after proceed. Restoring state.", t) nextChain.downstreamThrowable?.let { throw it } return nextChain.downstreamResult } diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorMetaDataReader.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorMetaDataReader.kt index 3379bc8f5..949416cc5 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorMetaDataReader.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorMetaDataReader.kt @@ -25,9 +25,11 @@ class VectorMetaDataReader private constructor(apk: File) { val reader = AxmlReader(getBytesFromInputStream(inputStream)) reader.accept( object : AxmlVisitor() { - override fun child(ns: String?, name: String?): NodeVisitor { - val child = super.child(ns, name) - return ManifestTagVisitor(child) + override fun child(ns: String?, name: String?): NodeVisitor? { + // We only care about the root tag. + // Returning ManifestTagVisitor() tells the parser to start + // looking at things inside . + return ManifestTagVisitor() } } ) @@ -35,27 +37,25 @@ class VectorMetaDataReader private constructor(apk: File) { } } - private inner class ManifestTagVisitor(child: NodeVisitor?) : NodeVisitor(child) { - override fun child(ns: String?, name: String?): NodeVisitor { - val childNode = super.child(ns, name) - if (name == "application") { - return ApplicationTagVisitor(childNode) - } - return childNode + private inner class ManifestTagVisitor : NodeVisitor() { + override fun child(ns: String?, name: String?): NodeVisitor? { + // If we see , we return a visitor to go deeper. + if (name == "application") return ApplicationTagVisitor() + // If we see or , we return null to skip them entirely. + return null } + } - private inner class ApplicationTagVisitor(child: NodeVisitor?) : NodeVisitor(child) { - override fun child(ns: String?, name: String?): NodeVisitor { - val childNode = super.child(ns, name) - if (name == "meta-data") { - return MetaDataVisitor(childNode) - } - return childNode - } + // Handles the inside of + private inner class ApplicationTagVisitor : NodeVisitor() { + override fun child(ns: String?, name: String?): NodeVisitor? { + // We only care about tags. + if (name == "meta-data") return MetaDataVisitor() + return null } } - private inner class MetaDataVisitor(child: NodeVisitor?) : NodeVisitor(child) { + private inner class MetaDataVisitor : NodeVisitor() { var attrName: String? = null var attrValue: Any? = null From 7f549c49d4b3668f4aeffdd6097e30d9751c2c75 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Mon, 23 Mar 2026 18:04:35 +0100 Subject: [PATCH 19/34] Fix comments --- .../kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt index e9645fb50..cbfdb7506 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hooks/VectorNativeHooker.kt @@ -93,7 +93,7 @@ class VectorNativeHooker(private val method: T) { invokeOriginalSafely(tObj, tArgs) } } - // Start the modern API 101 Interceptor Chain + val rootChain = VectorChain(method, thisObject, actualArgs, modernHooks, 0, terminal) val result = rootChain.proceed() From 99f6e95906d519e5f38828980ab8a3b9117f8b62 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Mon, 23 Mar 2026 18:55:58 +0100 Subject: [PATCH 20/34] First step of updating service to API 101 --- .../org/lsposed/manager/ConfigManager.java | 2 +- daemon/build.gradle.kts | 36 +++++------------- .../service/LSPInjectedModuleService.java | 5 ++- .../lspd/service/LSPManagerService.java | 4 +- .../lspd/service/LSPModuleService.java | 37 ++++++++++--------- .../lspd/service/LSPNotificationManager.java | 2 +- .../lsposed/lspd/service/LSPosedService.java | 13 ++++--- .../service/ILSPInjectedModuleService.aidl | 2 +- services/libxposed | 2 +- .../org/lsposed/lspd/ILSPManagerService.aidl | 2 +- 10 files changed, 46 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/org/lsposed/manager/ConfigManager.java b/app/src/main/java/org/lsposed/manager/ConfigManager.java index 79a1236a7..9be97ba5e 100644 --- a/app/src/main/java/org/lsposed/manager/ConfigManager.java +++ b/app/src/main/java/org/lsposed/manager/ConfigManager.java @@ -63,7 +63,7 @@ public static String getXposedVersionName() { } } - public static int getXposedVersionCode() { + public static long getXposedVersionCode() { try { return LSPManagerServiceHolder.getService().getXposedVersionCode(); } catch (RemoteException e) { diff --git a/daemon/build.gradle.kts b/daemon/build.gradle.kts index 93abf08fd..8c34f14ad 100644 --- a/daemon/build.gradle.kts +++ b/daemon/build.gradle.kts @@ -1,39 +1,18 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - import com.android.build.api.dsl.ApplicationExtension import com.android.ide.common.signing.KeystoreHelper import java.io.PrintStream +val defaultManagerPackageName: String by rootProject.extra +val injectedPackageName: String by rootProject.extra +val injectedPackageUid: Int by rootProject.extra +val versionCodeProvider: Provider by rootProject.extra +val versionNameProvider: Provider by rootProject.extra + plugins { alias(libs.plugins.agp.app) alias(libs.plugins.lsplugin.resopt) } -val daemonName = "LSPosed" - -val injectedPackageName: String by rootProject.extra -val injectedPackageUid: Int by rootProject.extra - -val agpVersion: String by project - -val defaultManagerPackageName: String by rootProject.extra android { buildFeatures { @@ -49,8 +28,11 @@ android { "DEFAULT_MANAGER_PACKAGE_NAME", """"$defaultManagerPackageName"""", ) + buildConfigField("String", "FRAMEWORK_NAME", """"${rootProject.name}"""") buildConfigField("String", "MANAGER_INJECTED_PKG_NAME", """"$injectedPackageName"""") buildConfigField("int", "MANAGER_INJECTED_UID", """$injectedPackageUid""") + buildConfigField("String", "VERSION_NAME", """"${versionCodeProvider.get()}"""") + buildConfigField("long", "VERSION_CODE", versionCodeProvider.get()) } buildTypes { diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPInjectedModuleService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPInjectedModuleService.java index ae1f6e80f..03d24e287 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPInjectedModuleService.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/LSPInjectedModuleService.java @@ -30,8 +30,9 @@ public class LSPInjectedModuleService extends ILSPInjectedModuleService.Stub { } @Override - public int getFrameworkPrivilege() { - return IXposedService.FRAMEWORK_PRIVILEGE_ROOT; + public long getFrameworkProperties() { + // TODO: correctly return PROP_RT_API_PROTECTION if set + return IXposedService.PROP_CAP_SYSTEM | IXposedService.PROP_CAP_REMOTE; } @Override diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java index 46a3cd542..0ed6b21f7 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java @@ -268,11 +268,11 @@ public IBinder asBinder() { @Override public int getXposedApiVersion() { - return IXposedService.API; + return IXposedService.LIB_API; } @Override - public int getXposedVersionCode() { + public long getXposedVersionCode() { return BuildConfig.VERSION_CODE; } diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java index 6e1a3ed4a..a222a666a 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java @@ -123,15 +123,15 @@ private int ensureModule() throws RemoteException { } @Override - public int getAPIVersion() throws RemoteException { + public int getApiVersion() throws RemoteException { ensureModule(); - return API; + return IXposedService.LIB_API; } @Override public String getFrameworkName() throws RemoteException { ensureModule(); - return "LSPosed"; + return BuildConfig.FRAMEWORK_NAME; } @Override @@ -147,9 +147,10 @@ public long getFrameworkVersionCode() throws RemoteException { } @Override - public int getFrameworkPrivilege() throws RemoteException { + public long getFrameworkProperties() throws RemoteException { ensureModule(); - return IXposedService.FRAMEWORK_PRIVILEGE_ROOT; + // Properly handle the return value + return IXposedService.PROP_CAP_SYSTEM | IXposedService.PROP_CAP_REMOTE; } @Override @@ -165,26 +166,28 @@ public List getScope() throws RemoteException { } @Override - public void requestScope(String packageName, IXposedScopeCallback callback) throws RemoteException { + public void requestScope(List packages, IXposedScopeCallback callback) throws RemoteException { var userId = ensureModule(); - if (ConfigManager.getInstance().scopeRequestBlocked(loadedModule.packageName)) { - callback.onScopeRequestDenied(packageName); + if (!ConfigManager.getInstance().scopeRequestBlocked(loadedModule.packageName)) { + for (String packageName : packages) { + LSPNotificationManager.requestModuleScope(loadedModule.packageName, userId, packageName, callback); + } } else { - LSPNotificationManager.requestModuleScope(loadedModule.packageName, userId, packageName, callback); - callback.onScopeRequestPrompted(packageName); + callback.onScopeRequestFailed("Scope request blocked by user configuration"); } } @Override - public String removeScope(String packageName) throws RemoteException { + public void removeScope(List packages) throws RemoteException { var userId = ensureModule(); - try { - if (!ConfigManager.getInstance().removeModuleScope(loadedModule.packageName, packageName, userId)) { - return "Invalid request"; + for (String packageName : packages) { + try { + if (!ConfigManager.getInstance().removeModuleScope(loadedModule.packageName, packageName, userId)) { + Log.w(TAG, "Failed to remove scope: " + packageName + " (Invalid request)"); + } + } catch (Throwable e) { + Log.e(TAG, "Error removing scope for " + packageName, e); } - return null; - } catch (Throwable e) { - return e.getMessage(); } } diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java index 878837eee..93b67b789 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java @@ -305,7 +305,7 @@ static void requestModuleScope(String modulePackageName, int moduleUserId, Strin notification, 0); } catch (RemoteException e) { try { - callback.onScopeRequestFailed(scopePackageName, e.getMessage()); + callback.onScopeRequestFailed(e.getMessage()); } catch (RemoteException ignored) { } Log.e(TAG, "request module scope", e); diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java index 198957362..4af2632a4 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java @@ -49,6 +49,7 @@ import java.io.IOException; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.function.Consumer; @@ -301,26 +302,26 @@ private void dispatchModuleScope(Intent intent) { try { var applicationInfo = PackageService.getApplicationInfo(scopePackageName, 0, userId); if (applicationInfo == null) { - iCallback.onScopeRequestFailed(scopePackageName, "Package not found"); + iCallback.onScopeRequestFailed("Package not found"); return; } switch (action) { case "approve" -> { ConfigManager.getInstance().setModuleScope(packageName, scopePackageName, userId); - iCallback.onScopeRequestApproved(scopePackageName); + iCallback.onScopeRequestApproved(Collections.singletonList(scopePackageName)); } - case "deny" -> iCallback.onScopeRequestDenied(scopePackageName); - case "delete" -> iCallback.onScopeRequestTimeout(scopePackageName); + case "deny" -> iCallback.onScopeRequestFailed("Request denied by user"); + case "delete" -> iCallback.onScopeRequestFailed("Request timeout"); case "block" -> { ConfigManager.getInstance().blockScopeRequest(packageName); - iCallback.onScopeRequestDenied(scopePackageName); + iCallback.onScopeRequestFailed("Request blocked by configuration"); } } Log.i(TAG, action + " scope " + scopePackageName + " for " + packageName + " in user " + userId); } catch (RemoteException e) { try { - iCallback.onScopeRequestFailed(scopePackageName, e.getMessage()); + iCallback.onScopeRequestFailed(e.getMessage()); } catch (RemoteException ignored) { // callback died } diff --git a/services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPInjectedModuleService.aidl b/services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPInjectedModuleService.aidl index e9df2f508..c77ece391 100644 --- a/services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPInjectedModuleService.aidl +++ b/services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPInjectedModuleService.aidl @@ -3,7 +3,7 @@ package org.lsposed.lspd.service; import org.lsposed.lspd.service.IRemotePreferenceCallback; interface ILSPInjectedModuleService { - int getFrameworkPrivilege(); + long getFrameworkProperties(); Bundle requestRemotePreferences(String group, IRemotePreferenceCallback callback); diff --git a/services/libxposed b/services/libxposed index 496b76fa3..11f8945de 160000 --- a/services/libxposed +++ b/services/libxposed @@ -1 +1 @@ -Subproject commit 496b76fa3e5af87958ebef97bd160319e05da79b +Subproject commit 11f8945de4e24efc0eb0e2e87a2dd8284d8f7b66 diff --git a/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl b/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl index aeded3c62..11e2385e9 100644 --- a/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl +++ b/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl @@ -34,7 +34,7 @@ interface ILSPManagerService { ParcelFileDescriptor getModulesLog() = 17; - int getXposedVersionCode() = 18; + long getXposedVersionCode() = 18; String getXposedVersionName() = 19; From 38f6d73322cb59df1fc075f52ac548a8f5929a5e Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Mon, 23 Mar 2026 19:40:02 +0100 Subject: [PATCH 21/34] Adapt constructor changes in API 101 --- daemon/build.gradle.kts | 1 - .../vector/impl/core/VectorModuleManager.kt | 23 +++++-------------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/daemon/build.gradle.kts b/daemon/build.gradle.kts index 8c34f14ad..0ecd07565 100644 --- a/daemon/build.gradle.kts +++ b/daemon/build.gradle.kts @@ -13,7 +13,6 @@ plugins { alias(libs.plugins.lsplugin.resopt) } - android { buildFeatures { prefab = true diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt index 5cc491bde..abd8443a7 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt @@ -2,7 +2,6 @@ package org.matrix.vector.impl.core import android.os.Build import android.os.Process -import io.github.libxposed.api.XposedInterface import io.github.libxposed.api.XposedModule import io.github.libxposed.api.XposedModuleInterface.ModuleLoadedParam import java.io.File @@ -76,22 +75,12 @@ object VectorModuleManager { return@runCatching } - val constructor = - moduleClass.getConstructor( - XposedInterface::class.java, - ModuleLoadedParam::class.java, - ) - - // Inject the Context and the loaded parameters! - val moduleInstance = - constructor.newInstance( - vectorContext, - object : ModuleLoadedParam { - override fun isSystemServer(): Boolean = isSystemServer - - override fun getProcessName(): String = processName - }, - ) as XposedModule + val constructor = moduleClass.getDeclaredConstructor() + constructor.isAccessible = true + val moduleInstance = constructor.newInstance() as XposedModule + + // Attach the framework context to the module + moduleInstance.attachFramework(vectorContext) // Register the active module to receive future lifecycle events VectorLifecycleManager.activeModules.add(moduleInstance) From 80b59013047f7f471cd239209998d1e954484241 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Mon, 23 Mar 2026 19:51:55 +0100 Subject: [PATCH 22/34] Fix BuildConfig fields --- daemon/build.gradle.kts | 2 +- .../org/lsposed/lspd/service/LSPNotificationManager.java | 7 ++++--- xposed/build.gradle.kts | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/daemon/build.gradle.kts b/daemon/build.gradle.kts index 0ecd07565..cee0e4d42 100644 --- a/daemon/build.gradle.kts +++ b/daemon/build.gradle.kts @@ -30,7 +30,7 @@ android { buildConfigField("String", "FRAMEWORK_NAME", """"${rootProject.name}"""") buildConfigField("String", "MANAGER_INJECTED_PKG_NAME", """"$injectedPackageName"""") buildConfigField("int", "MANAGER_INJECTED_UID", """$injectedPackageUid""") - buildConfigField("String", "VERSION_NAME", """"${versionCodeProvider.get()}"""") + buildConfigField("String", "VERSION_NAME", """"${versionNameProvider.get()}"""") buildConfigField("long", "VERSION_CODE", versionCodeProvider.get()) } diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java index 93b67b789..e2ee5148e 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java @@ -24,6 +24,7 @@ import android.os.RemoteException; import android.util.Log; +import org.lsposed.daemon.BuildConfig; import org.lsposed.daemon.R; import org.lsposed.lspd.util.FakeContext; @@ -164,7 +165,7 @@ static void notifyStatusNotification() { .setOngoing(true) .setAutoCancel(false) .build(); - notification.extras.putString("android.substName", "LSPosed"); + notification.extras.putString("android.substName", BuildConfig.FRAMEWORK_NAME); try { var nm = getNotificationManager(); createNotificationChannel(nm); @@ -251,7 +252,7 @@ static void notifyModuleUpdated(String modulePackageName, .setAutoCancel(true) .setStyle(style) .build(); - notification.extras.putString("android.substName", "LSPosed"); + notification.extras.putString("android.substName", BuildConfig.FRAMEWORK_NAME); try { var nm = getNotificationManager(); nm.enqueueNotificationWithTag("android", opPkg, modulePackageName, @@ -297,7 +298,7 @@ static void requestModuleScope(String modulePackageName, int moduleUserId, Strin getModuleScopeIntent(modulePackageName, moduleUserId, scopePackageName, "block", callback)) .build() ).build(); - notification.extras.putString("android.substName", "LSPosed"); + notification.extras.putString("android.substName", BuildConfig.FRAMEWORK_NAME); try { var nm = getNotificationManager(); nm.enqueueNotificationWithTag("android", opPkg, modulePackageName, diff --git a/xposed/build.gradle.kts b/xposed/build.gradle.kts index 82c10fc6b..8edcc5cc4 100644 --- a/xposed/build.gradle.kts +++ b/xposed/build.gradle.kts @@ -17,7 +17,7 @@ android { defaultConfig { consumerProguardFiles("consumer-rules.pro") buildConfigField("String", "FRAMEWORK_NAME", """"${rootProject.name}"""") - buildConfigField("String", "VERSION_NAME", """"${versionCodeProvider.get()}"""") + buildConfigField("String", "VERSION_NAME", """"${versionNameProvider.get()}"""") buildConfigField("long", "VERSION_CODE", versionCodeProvider.get()) } From 1282701bb77bb5475129fb68a8e6210c6de9821a Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Mon, 23 Mar 2026 21:59:20 +0100 Subject: [PATCH 23/34] Properly implement getFrameworkProperties --- .../org/lsposed/lspd/service/LSPInjectedModuleService.java | 7 +++++-- .../java/org/lsposed/lspd/service/LSPModuleService.java | 7 +++++-- .../main/kotlin/org/matrix/vector/impl/VectorContext.kt | 6 +----- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPInjectedModuleService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPInjectedModuleService.java index 03d24e287..fa402923c 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPInjectedModuleService.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/LSPInjectedModuleService.java @@ -31,8 +31,11 @@ public class LSPInjectedModuleService extends ILSPInjectedModuleService.Stub { @Override public long getFrameworkProperties() { - // TODO: correctly return PROP_RT_API_PROTECTION if set - return IXposedService.PROP_CAP_SYSTEM | IXposedService.PROP_CAP_REMOTE; + var prop = IXposedService.PROP_CAP_SYSTEM | IXposedService.PROP_CAP_REMOTE; + if (ConfigManager.getInstance().dexObfuscate()) { + prop = prop | IXposedService.PROP_RT_API_PROTECTION; + } + return prop; } @Override diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java index a222a666a..1b770c3cb 100644 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java +++ b/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java @@ -149,8 +149,11 @@ public long getFrameworkVersionCode() throws RemoteException { @Override public long getFrameworkProperties() throws RemoteException { ensureModule(); - // Properly handle the return value - return IXposedService.PROP_CAP_SYSTEM | IXposedService.PROP_CAP_REMOTE; + var prop = IXposedService.PROP_CAP_SYSTEM | IXposedService.PROP_CAP_REMOTE; + if (ConfigManager.getInstance().dexObfuscate()) { + prop = prop | IXposedService.PROP_RT_API_PROTECTION; + } + return prop; } @Override diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/VectorContext.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorContext.kt index b81d316ac..b5152b1b2 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/VectorContext.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorContext.kt @@ -37,11 +37,7 @@ class VectorContext( override fun getFrameworkVersionCode(): Long = BuildConfig.VERSION_CODE override fun getFrameworkProperties(): Long { - var props = 0L - // TODO: Dynamically verify capabilities through `service` if required - props = props or XposedInterface.PROP_CAP_REMOTE - props = props or XposedInterface.PROP_CAP_SYSTEM - return props + return service.getFrameworkProperties() } override fun hook(origin: Executable): XposedInterface.HookBuilder { From 4f10187e9692a1239d1b1ebbd48e78787243d75a Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Tue, 24 Mar 2026 10:29:13 +0100 Subject: [PATCH 24/34] Skip slicer processing for unmodified DEX files Slicer's `CreateFullIr` and `CreateImage` operations are highly CPU-intensive and cause significant load latency for large DEX files. We hence introduce a fast-path optimization using `memmem` to perform a raw binary scan of the DEX buffer for target signatures prior to processing. If no matching signatures are found, the expensive IR parsing and image rebuilding phases are bypassed entirely, and the original file descriptor is safely returned. --- daemon/src/main/jni/obfuscation.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/daemon/src/main/jni/obfuscation.cpp b/daemon/src/main/jni/obfuscation.cpp index 96d7c2048..9965f42f1 100644 --- a/daemon/src/main/jni/obfuscation.cpp +++ b/daemon/src/main/jni/obfuscation.cpp @@ -221,6 +221,26 @@ extern "C" JNIEXPORT jobject JNICALL Java_org_lsposed_lspd_service_ObfuscationMa return nullptr; } + bool needs_obfuscation = false; + for (const auto &sig : signatures) { + if (memmem(mem, size, sig.first.c_str(), sig.first.length()) != nullptr) { + needs_obfuscation = true; + break; + } + } + + if (!needs_obfuscation) { + LOGD("No target signatures found in fd=%d, skipping slicer.", fd); + munmap(mem, size); + + // Wrap the duplicated FD into Java objects and return instantly + auto java_fd = + lsplant::JNI_NewObject(env, class_file_descriptor, method_file_descriptor_ctor, fd); + auto java_sm = + lsplant::JNI_NewObject(env, class_shared_memory, method_shared_memory_ctor, java_fd); + return java_sm.release(); + } + // Process the DEX and obtain a new file descriptor for the output int new_fd = obfuscateDexBuffer(mem, size); From 377da0b33ae87ea7c3eb2291762a61e60b8eacd3 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Tue, 24 Mar 2026 11:23:29 +0100 Subject: [PATCH 25/34] Add debugging logs --- .../main/kotlin/org/matrix/vector/impl/VectorContext.kt | 5 ++++- .../org/matrix/vector/impl/hookers/LoadedApkHookers.kt | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/VectorContext.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorContext.kt index b5152b1b2..96e1f3028 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/VectorContext.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorContext.kt @@ -158,7 +158,10 @@ object VectorLifecycleManager { } activeModules.forEach { module -> - runCatching { module.onPackageReady(param) } + runCatching { + Log.d(TAG, "dispatchPackageReady $param") + module.onPackageReady(param) + } .onFailure { Log.e( TAG, diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt index 53751f57c..2d2966f46 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt @@ -1,9 +1,9 @@ package org.matrix.vector.impl.hookers import android.content.pm.ApplicationInfo -import android.util.Log import io.github.libxposed.api.XposedInterface import java.util.concurrent.ConcurrentHashMap +import org.lsposed.lspd.util.Utils import org.matrix.vector.impl.VectorLifecycleManager import org.matrix.vector.impl.di.LegacyPackageInfo import org.matrix.vector.impl.di.VectorBootstrap @@ -42,7 +42,7 @@ object LoadedApkCtorHooker : XposedInterface.Hooker { // Avoid OnePlus custom opt crashing if ( - Log.getStackTraceString(Throwable()) + Utils.Log.getStackTraceString(Throwable()) .contains("android.app.ActivityThread\$ApplicationThread.schedulePreload") ) { return result @@ -79,6 +79,9 @@ object LoadedApkCreateCLHooker : XposedInterface.Hooker { val isFirstPackage = packageName != null && processName != null && packageName == apkPackageName + Utils.logD( + "LoadedApkCreateCLHooker: $packageName $processName, isFirst: $isFirstPackage" + ) if (!isFirstPackage) { packageName = apkPackageName processName = currentPkgMethod.invoke(null) as? String ?: apkPackageName From f229047ed39f71ce8764a81bbb26016faf505536 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Tue, 24 Mar 2026 12:32:01 +0100 Subject: [PATCH 26/34] Refactor LoadedApk hooks to strictly follow libxposed API 101 - Split classloader hooking to correctly implement API 101 lifecycle phases and prevent AppComponentFactory evasion. - Hook LoadedApk.createAppFactory to dispatch onPackageLoaded immediately before factory instantiation (API 29+). - Hook LoadedApk.createOrUpdateClassLoaderLocked to dispatch onPackageReady after the custom classloader is fully initialized. - Cache reflection targets using lazy delegates to eliminate redundant lookups during classloader updates. --- .../org/matrix/vector/impl/VectorContext.kt | 97 ------------ .../vector/impl/VectorLifecycleManager.kt | 143 +++++++++++++++++ .../matrix/vector/impl/core/VectorStartup.kt | 9 +- .../vector/impl/hookers/LoadedApkHookers.kt | 146 ++++++++++++------ .../impl/hookers/SystemServerHookers.kt | 25 +-- 5 files changed, 267 insertions(+), 153 deletions(-) create mode 100644 xposed/src/main/kotlin/org/matrix/vector/impl/VectorLifecycleManager.kt diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/VectorContext.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorContext.kt index 96e1f3028..d92faabb6 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/VectorContext.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorContext.kt @@ -4,7 +4,6 @@ import android.content.SharedPreferences import android.content.pm.ApplicationInfo import android.os.ParcelFileDescriptor import io.github.libxposed.api.XposedInterface -import io.github.libxposed.api.XposedModule import io.github.libxposed.api.XposedModuleInterface.* import java.io.FileNotFoundException import java.lang.reflect.Constructor @@ -94,99 +93,3 @@ class VectorContext( Log.println(priority, finalTag, fullMsg) } } - -/** Manages the dispatching of modern lifecycle events to loaded modules. */ -object VectorLifecycleManager { - - private const val TAG = "VectorLifecycle" - - val activeModules: MutableSet = ConcurrentHashMap.newKeySet() - - fun dispatchPackageLoaded( - packageName: String, - appInfo: ApplicationInfo, - isFirst: Boolean, - defaultClassLoader: ClassLoader, - ) { - val param = - object : PackageLoadedParam { - override fun getPackageName(): String = packageName - - override fun getApplicationInfo(): ApplicationInfo = appInfo - - override fun isFirstPackage(): Boolean = isFirst - - override fun getDefaultClassLoader(): ClassLoader = defaultClassLoader - } - - activeModules.forEach { module -> - runCatching { module.onPackageLoaded(param) } - .onFailure { - Log.e( - TAG, - "Error in onPackageLoaded for ${module.moduleApplicationInfo.packageName}", - it, - ) - } - } - } - - fun dispatchPackageReady( - packageName: String, - appInfo: ApplicationInfo, - isFirst: Boolean, - defaultClassLoader: ClassLoader, - classLoader: ClassLoader, - appComponentFactory: Any, // Abstracted for API compatibility - ) { - val param = - object : PackageReadyParam { - override fun getPackageName(): String = packageName - - override fun getApplicationInfo(): ApplicationInfo = appInfo - - override fun isFirstPackage(): Boolean = isFirst - - override fun getDefaultClassLoader(): ClassLoader = defaultClassLoader - - override fun getClassLoader(): ClassLoader = classLoader - - @Suppress("NewApi") - override fun getAppComponentFactory(): android.app.AppComponentFactory { - return appComponentFactory as android.app.AppComponentFactory - } - } - - activeModules.forEach { module -> - runCatching { - Log.d(TAG, "dispatchPackageReady $param") - module.onPackageReady(param) - } - .onFailure { - Log.e( - TAG, - "Error in onPackageReady for ${module.moduleApplicationInfo.packageName}", - it, - ) - } - } - } - - fun dispatchSystemServerStarting(classLoader: ClassLoader) { - val param = - object : SystemServerStartingParam { - override fun getClassLoader(): ClassLoader = classLoader - } - - activeModules.forEach { module -> - runCatching { module.onSystemServerStarting(param) } - .onFailure { - Log.e( - TAG, - "Error in onSystemServerStarting for ${module.moduleApplicationInfo.packageName}", - it, - ) - } - } - } -} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/VectorLifecycleManager.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorLifecycleManager.kt new file mode 100644 index 000000000..e1d73ac92 --- /dev/null +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/VectorLifecycleManager.kt @@ -0,0 +1,143 @@ +package org.matrix.vector.impl + +import android.content.pm.ApplicationInfo +import android.os.Build +import androidx.annotation.RequiresApi +import io.github.libxposed.api.XposedModule +import io.github.libxposed.api.XposedModuleInterface.* +import java.util.concurrent.ConcurrentHashMap +import org.lsposed.lspd.util.Utils.Log + +/** Manages the dispatching of modern lifecycle events to loaded modules. */ +object VectorLifecycleManager { + + private const val TAG = "VectorLifecycle" + + val activeModules: MutableSet = ConcurrentHashMap.newKeySet() + + fun dispatchPackageLoaded( + packageName: String, + appInfo: ApplicationInfo, + isFirst: Boolean, + defaultClassLoader: ClassLoader, + ) { + val param = + object : PackageLoadedParam { + override fun getPackageName(): String = packageName + + override fun getApplicationInfo(): ApplicationInfo = appInfo + + override fun isFirstPackage(): Boolean = isFirst + + override fun getDefaultClassLoader(): ClassLoader = defaultClassLoader + } + + activeModules.forEach { module -> + runCatching { module.onPackageLoaded(param) } + .onFailure { + Log.e( + TAG, + "Error in onPackageLoaded for ${module.moduleApplicationInfo.packageName}", + it, + ) + } + } + } + + fun dispatchPackageReady( + packageName: String, + appInfo: ApplicationInfo, + isFirst: Boolean, + defaultClassLoader: ClassLoader, + classLoader: ClassLoader, + appComponentFactory: Any?, // Abstracted for API compatibility + ) { + val param = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && appComponentFactory != null) { + PackageReadyParamImplP( + packageName, + appInfo, + isFirst, + defaultClassLoader, + classLoader, + appComponentFactory, + ) + } else { + // Fallback for API < 28 (or if factory is null). + object : PackageReadyParam { + override fun getPackageName() = packageName + + override fun getApplicationInfo() = appInfo + + override fun isFirstPackage() = isFirst + + override fun getDefaultClassLoader() = defaultClassLoader + + override fun getClassLoader() = classLoader + + override fun getAppComponentFactory(): android.app.AppComponentFactory { + throw UnsupportedOperationException( + "AppComponentFactory is not available on this API level" + ) + } + } + } + + activeModules.forEach { module -> + runCatching { + Log.d(TAG, "dispatchPackageReady $param") + module.onPackageReady(param) + } + .onFailure { + Log.e( + TAG, + "Error in onPackageReady for ${module.moduleApplicationInfo.packageName}", + it, + ) + } + } + } + + fun dispatchSystemServerStarting(classLoader: ClassLoader) { + val param = + object : SystemServerStartingParam { + override fun getClassLoader(): ClassLoader = classLoader + } + + activeModules.forEach { module -> + runCatching { module.onSystemServerStarting(param) } + .onFailure { + Log.e( + TAG, + "Error in onSystemServerStarting for ${module.moduleApplicationInfo.packageName}", + it, + ) + } + } + } +} + +// Isolate the class so the Verifier doesn't crash on Android 8.1 and below +@RequiresApi(Build.VERSION_CODES.P) +private class PackageReadyParamImplP( + private val packageName: String, + private val appInfo: ApplicationInfo, + private val isFirst: Boolean, + private val defaultClassLoader: ClassLoader, + private val classLoader: ClassLoader, + private val appComponentFactory: Any, +) : PackageReadyParam { + override fun getPackageName() = packageName + + override fun getApplicationInfo() = appInfo + + override fun isFirstPackage() = isFirst + + override fun getDefaultClassLoader() = defaultClassLoader + + override fun getClassLoader() = classLoader + + override fun getAppComponentFactory(): android.app.AppComponentFactory { + return appComponentFactory as android.app.AppComponentFactory + } +} diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorStartup.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorStartup.kt index cbdb2afb9..4020d5e63 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorStartup.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorStartup.kt @@ -1,5 +1,6 @@ package org.matrix.vector.impl.core +import android.os.Build import android.os.IBinder import dalvik.system.DexFile import org.lsposed.lspd.service.ILSPApplicationService @@ -57,6 +58,12 @@ object VectorStartup { VectorHookBuilder(it).intercept(LoadedApkCtorHooker) } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + loadedApkClass.declaredMethods + .filter { it.name == "createAppFactory" } + .forEach { VectorHookBuilder(it).intercept(LoadedApkCreateAppFactoryHooker) } + } + loadedApkClass.declaredMethods .filter { it.name == "createOrUpdateClassLoaderLocked" } .forEach { VectorHookBuilder(it).intercept(LoadedApkCreateCLHooker) } @@ -74,7 +81,7 @@ object VectorStartup { val classLoader = activityService.javaClass.classLoader if (classLoader != null) { // Manually trigger the routines that the hooks normally would - HandleSystemServerProcessHooker.initSystemServer(classLoader) + HandleSystemServerProcessHooker.initSystemServer(classLoader, isLate = true) StartBootstrapServicesHooker.dispatchSystemServerLoaded(classLoader) } } diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt index 2d2966f46..79bbe6bf8 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt @@ -1,9 +1,10 @@ package org.matrix.vector.impl.hookers import android.content.pm.ApplicationInfo +import android.os.Build +import androidx.annotation.RequiresApi import io.github.libxposed.api.XposedInterface import java.util.concurrent.ConcurrentHashMap -import org.lsposed.lspd.util.Utils import org.matrix.vector.impl.VectorLifecycleManager import org.matrix.vector.impl.di.LegacyPackageInfo import org.matrix.vector.impl.di.VectorBootstrap @@ -23,6 +24,40 @@ private inline fun Any.getFieldValue(name: String): T? { return null } +/** Centralized helper for determining context details */ +private object PackageContextHelper { + private val activityThreadClass by lazy { Class.forName("android.app.ActivityThread") } + private val currentPkgMethod by lazy { + activityThreadClass.getDeclaredMethod("currentPackageName").apply { isAccessible = true } + } + private val currentProcMethod by lazy { + activityThreadClass.getDeclaredMethod("currentProcessName").apply { isAccessible = true } + } + + data class ContextInfo( + val packageName: String, + val processName: String, + val isFirstPackage: Boolean, + ) + + fun resolve(loadedApk: Any, apkPackageName: String): ContextInfo { + var packageName = currentPkgMethod.invoke(null) as? String + var processName = currentProcMethod.invoke(null) as? String + + val isFirstPackage = + packageName != null && processName != null && packageName == apkPackageName + + if (!isFirstPackage) { + packageName = apkPackageName + processName = currentPkgMethod.invoke(null) as? String ?: apkPackageName + } else if (packageName == "android") { + packageName = "system" + } + + return ContextInfo(packageName!!, processName!!, isFirstPackage) + } +} + /** Tracks and prepares Application instances when their LoadedApk is instantiated. */ object LoadedApkCtorHooker : XposedInterface.Hooker { val trackedApks = ConcurrentHashMap.newKeySet() @@ -41,10 +76,13 @@ object LoadedApkCtorHooker : XposedInterface.Hooker { } // Avoid OnePlus custom opt crashing - if ( - Utils.Log.getStackTraceString(Throwable()) - .contains("android.app.ActivityThread\$ApplicationThread.schedulePreload") - ) { + val isPreload = + Throwable().stackTrace.any { + it.className == "android.app.ActivityThread\$ApplicationThread" && + it.methodName == "schedulePreload" + } + + if (isPreload) { return result } @@ -53,68 +91,88 @@ object LoadedApkCtorHooker : XposedInterface.Hooker { } } -/** Triggers package loading events immediately after the Application ClassLoader is created. */ +/** Hooking `createAppFactory` is critical to defeating the `` evasion exploit. */ +@RequiresApi(Build.VERSION_CODES.P) +object LoadedApkCreateAppFactoryHooker : XposedInterface.Hooker { + override fun intercept(chain: XposedInterface.Chain): Any? { + val loadedApk = chain.thisObject ?: return chain.proceed() + + val appInfo = chain.args[0] as ApplicationInfo + val defaultClassLoader = + chain.args[1] as? ClassLoader + ?: return chain.proceed() // Skip dispatch if there's no ClassLoader + val apkPackageName = loadedApk.getFieldValue("mPackageName") ?: appInfo.packageName + + val ctx = PackageContextHelper.resolve(loadedApk, apkPackageName) + + // Only dispatch if on API 29+ per libxposed API specification + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + VectorLifecycleManager.dispatchPackageLoaded( + ctx.packageName, + appInfo, + ctx.isFirstPackage, + defaultClassLoader, + ) + } + + return chain.proceed() + } +} + +/** + * Triggers package ready events immediately after the final Application ClassLoader is created. + * Also acts as a fallback dispatcher for `onPackageLoaded` for resource-only APKs where + * `mIncludeCode` is false (meaning `createAppFactory` was never executed). + */ object LoadedApkCreateCLHooker : XposedInterface.Hooker { override fun intercept(chain: XposedInterface.Chain): Any? { - val result = chain.proceed() - val loadedApk = chain.thisObject ?: return result + val loadedApk = chain.thisObject ?: return chain.proceed() - // LoadedApk.createOrUpdateClassLoaderLocked(List addedPaths) is called with - // addedPaths == null when Android is creating the ClassLoader for the very first time for - // this app. + // Fast path exit: Ignore if addedPaths is not null, or untracked if ( chain.args.firstOrNull() != null || !LoadedApkCtorHooker.trackedApks.contains(loadedApk) ) { - return result + return chain.proceed() } - try { - val activityThreadClass = Class.forName("android.app.ActivityThread") - val currentPkgMethod = activityThreadClass.getDeclaredMethod("currentPackageName") - val currentProcMethod = activityThreadClass.getDeclaredMethod("currentProcessName") + // Proceed with Android's internal ClassLoader creation sequence + val result = chain.proceed() - var packageName = currentPkgMethod.invoke(null) as? String - var processName = currentProcMethod.invoke(null) as? String + try { val apkPackageName = loadedApk.getFieldValue("mPackageName") ?: return result - - val isFirstPackage = - packageName != null && processName != null && packageName == apkPackageName - Utils.logD( - "LoadedApkCreateCLHooker: $packageName $processName, isFirst: $isFirstPackage" - ) - if (!isFirstPackage) { - packageName = apkPackageName - processName = currentPkgMethod.invoke(null) as? String ?: apkPackageName - } else if (packageName == "android") { - packageName = "system" - } - - val classLoader = loadedApk.getFieldValue("mClassLoader") ?: return result val mIncludeCode = loadedApk.getFieldValue("mIncludeCode") ?: true + val ctx = PackageContextHelper.resolve(loadedApk, apkPackageName) + + if (!ctx.isFirstPackage && !mIncludeCode) return result - if (!isFirstPackage && !mIncludeCode) return result val appInfo = loadedApk.getFieldValue("mApplicationInfo") ?: return result - - // Dispatch Modern Lifecycle + val classLoader = loadedApk.getFieldValue("mClassLoader") ?: return result val defaultClassLoader = loadedApk.getFieldValue("mDefaultClassLoader") ?: classLoader - VectorLifecycleManager.dispatchPackageLoaded( - packageName, - appInfo, - isFirstPackage, - defaultClassLoader, - ) + + // Dispatch Modern Lifecycle: onPackageReady + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val appComponentFactory = loadedApk.getFieldValue("mAppComponentFactory") + VectorLifecycleManager.dispatchPackageReady( + ctx.packageName, + appInfo, + ctx.isFirstPackage, + defaultClassLoader, + classLoader, + appComponentFactory, + ) + } // Dispatch Legacy Lifecycle VectorBootstrap.withLegacy { delegate -> delegate.onPackageLoaded( LegacyPackageInfo( - packageName, - processName, + ctx.packageName, + ctx.processName, classLoader, appInfo, - isFirstPackage, + ctx.isFirstPackage, ) ) } diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/SystemServerHookers.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/SystemServerHookers.kt index 3fa05b3cf..a52b56f05 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/SystemServerHookers.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/SystemServerHookers.kt @@ -32,25 +32,28 @@ object HandleSystemServerProcessHooker : XposedInterface.Hooker { } /** Performs system server initialization. */ - fun initSystemServer(classLoader: ClassLoader) { + fun initSystemServer(classLoader: ClassLoader, isLate: Boolean = false) { if (systemServerCL != null) return // Ensure this only runs once systemServerCL = classLoader // Deoptimize heavily inlined system server paths VectorDeopter.deoptSystemServerMethods(classLoader) - // Dynamically locate and hook the bootstrap service initializer - val sysServerClass = Class.forName("com.android.server.SystemServer", false, classLoader) - val startMethod = - sysServerClass.declaredMethods.find { it.name == "startBootstrapServices" } - ?: throw NoSuchMethodException( - "com.android.server.SystemServer.startBootstrapServices not found" - ) + if (!isLate) { + // Dynamically locate and hook the bootstrap service initializer + val sysServerClass = + Class.forName("com.android.server.SystemServer", false, classLoader) + val startMethod = + sysServerClass.declaredMethods.find { it.name == "startBootstrapServices" } + ?: throw NoSuchMethodException( + "com.android.server.SystemServer.startBootstrapServices not found" + ) - // Ensure we can hook the private method - startMethod.isAccessible = true + // Ensure we can hook the private method + startMethod.isAccessible = true + VectorHookBuilder(startMethod).intercept(StartBootstrapServicesHooker) + } - VectorHookBuilder(startMethod).intercept(StartBootstrapServicesHooker) callback?.onSystemServerLoaded(classLoader) } } From 93eabcc5f85c1324574886f9db15b55ec187e4e6 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Tue, 24 Mar 2026 18:21:40 +0100 Subject: [PATCH 27/34] Completely remove dexparser It is said as we have spent time to improve it. Now it is better to use third party library for module developers --- native/README.md | 1 - native/include/jni/jni_hooks.h | 3 - native/src/core/context.cpp | 1 - native/src/jni/dex_parser_bridge.cpp | 960 --------------------------- 4 files changed, 965 deletions(-) delete mode 100644 native/src/jni/dex_parser_bridge.cpp diff --git a/native/README.md b/native/README.md index 3afe933ec..fdf947a02 100644 --- a/native/README.md +++ b/native/README.md @@ -31,7 +31,6 @@ This is the most significant module and represents the library's primary service - **`jni_bridge.h`**: Provides a set of helper macros (`VECTOR_NATIVE_METHOD`, `REGISTER_VECTOR_NATIVE_METHODS`, etc.) to standardize and simplify the tedious process of writing JNI boilerplate. - **`HookBridge`**: The engine for ART method hooking. It maintains a thread-safe map of all active hooks. It also includes some stability controls, such as using atomic operations to set the backup method trampoline and throwing a Java exception instead of causing a native crash if a user tries to invoke the original method of a failed hook. - **`ResourcesHook`**: Provides the functionality to intercept and rewrite Android's binary XML resources on the fly. It relies on non-public structures from `libandroidfw.so` and uses the `elf` module to find the necessary function symbols at runtime. -- **`DexParserBridge`**: Exposes a native DEX parser to the Java layer using a visitor pattern. This allows for analysis of an app's bytecode without the overhead of instantiating the entire DEX structure as Java objects. - **`NativeApiBridge`**: The JNI counterpart to the `core/native_api`. It exposes a method for the Java framework to register the filenames of third-party native modules. ### `common` & `framework` diff --git a/native/include/jni/jni_hooks.h b/native/include/jni/jni_hooks.h index 703ae0114..ff232f7cd 100644 --- a/native/include/jni/jni_hooks.h +++ b/native/include/jni/jni_hooks.h @@ -9,9 +9,6 @@ namespace vector::native::jni { -/// Registers the JNI methods for the DexParserBridge. -void RegisterDexParserBridge(JNIEnv *env); - /// Registers the JNI methods for the HookBridge. void RegisterHookBridge(JNIEnv *env); diff --git a/native/src/core/context.cpp b/native/src/core/context.cpp index 79a2f2bb9..0fa95790d 100644 --- a/native/src/core/context.cpp +++ b/native/src/core/context.cpp @@ -102,7 +102,6 @@ void Context::InitHooks(JNIEnv *env) { jni::RegisterResourcesHook(env); jni::RegisterHookBridge(env); jni::RegisterNativeApiBridge(env); - // jni::RegisterDexParserBridge(env); } lsplant::ScopedLocalRef Context::FindClassFromLoader(JNIEnv *env, jobject class_loader, diff --git a/native/src/jni/dex_parser_bridge.cpp b/native/src/jni/dex_parser_bridge.cpp deleted file mode 100644 index 41549a7e0..000000000 --- a/native/src/jni/dex_parser_bridge.cpp +++ /dev/null @@ -1,960 +0,0 @@ -#include -#include - -#include - -#include "jni/jni_bridge.h" -#include "jni/jni_hooks.h" - -/** - * @file dex_parser_bridge.cpp - * @brief Implements a JNI bridge to a native DEX file parser. - * - * This bridge provides a memory-efficient way for Java code to parse Android DEX files. - * It avoids creating a complete object representation of the DEX file in memory, - * which can be very large. - * - * It employs a visitor pattern: - * 1. The `openDex` method performs an initial parse of the DEX file's main sections - * (strings, types, fields, methods, classes) and returns them - * to the Java caller as primitive arrays. - * It stores the detailed parsed data in a native `DexParser` object. - * 2. The `visitClass` method then iterates through the parsed classes and - * invokes callback methods on a Java "visitor" object for - * each class, field, and method. - * - * This approach minimizes JNI overhead and memory consumption by processing data - * in a streaming fashion and only creating Java objects as needed for the visitor callbacks. - */ - -namespace { -// Type aliases for representing DEX encoded values and annotations. -// These structures temporarily hold parsed annotation data before it's converted to Java objects. - -// A DEX encoded value, represented as a tuple of its type and raw byte data. -using Value = std::tuple /*data*/>; -// A DEX encoded array, which is a vector of encoded values. -using Array = std::vector; -// A list of encoded arrays. A list is used because its elements won't be -// reallocated, which is important when indices are stored. -using ArrayList = std::list; -// An element of an annotation, consisting of a name (index into string table) and a value. -using Element = std::tuple; -// A list of annotation elements. -using ElementList = std::vector; -// A DEX annotation, containing its visibility, type, and a list of its elements. -using Annotation = std::tuple; -// A list of annotations. -using AnnotationList = std::vector; - -/** - * @class DexParser - * @brief Extends slicer's dex::Reader to hold parsed class, method, and annotation data. - * - * This class serves as the main native handle for a parsed DEX file. - * It stores structured data that has been read from the DEX file, - * making it readily available for the `visitClass` function. - */ -class DexParser : public dex::Reader { -public: - DexParser(const dex::u1 *data, size_t size) : dex::Reader(data, size, nullptr, 0) {} - - /** - * @struct ClassData - * @brief Holds all relevant information for a single class definition. - * - * This structure is populated during the `openDex` phase and contains indices - * pointing to the DEX file's various data pools (types, fields, methods). - */ - struct ClassData { - std::vector interfaces; - std::vector static_fields; - std::vector static_fields_access_flags; - std::vector instance_fields; - std::vector instance_fields_access_flags; - std::vector direct_methods; - std::vector direct_methods_access_flags; - std::vector direct_methods_code; // Pointers to method bytecode - std::vector virtual_methods; - std::vector virtual_methods_access_flags; - std::vector virtual_methods_code; // Pointers to method bytecode - std::vector annotations; - }; - - /** - * @struct MethodBody - * @brief Lazily-parsed information from a method's bytecode. - * - * This data is only computed when a method is visited in `visitClass`, - * saving significant processing time if the caller is not interested in method body details. - */ - struct MethodBody { - bool loaded = false; // Flag to indicate if this body has been parsed yet. - std::vector referred_strings; - std::vector accessed_fields; // Fields read from (iget/sget) - std::vector assigned_fields; // Fields written to (iput/sput) - std::vector invoked_methods; - std::vector opcodes; - }; - - // Parsed data storage - std::vector class_data; // One entry per ClassDef in the DEX file. - // Mappings from an item's index to a list of annotation indices. - // Using phmap::flat_hash_map for fast lookups. - phmap::flat_hash_map> field_annotations; - phmap::flat_hash_map> method_annotations; - phmap::flat_hash_map> parameter_annotations; - - // Lazily populated map of method index to its parsed body. - phmap::flat_hash_map method_bodies; -}; - -/** - * @brief Parses a variable-length integer from the DEX byte stream. - * @tparam T The integral type to parse (e.g., int8_t, int32_t). - * @param pptr Pointer to the current position in the byte stream. - * @param size The number of bytes to read (1 to sizeof(T)). - * @return A vector of bytes containing the parsed value. - */ -template -static std::vector ParseIntValue(const dex::u1 **pptr, size_t size) { - static_assert(std::is_integral::value, "must be an integral type"); - std::vector ret(sizeof(T)); - // Use reinterpret_cast to type-pun the byte vector's data into the target integer type. - T &value = *reinterpret_cast(ret.data()); - value = 0; // Ensure starting from a clean state. - for (size_t i = 0; i < size; ++i) { - value |= T(*(*pptr)++) << (i * 8); - } - - // If the type is signed and we read fewer bytes than its full size, - // we need to manually sign-extend the value. - if constexpr (std::is_signed_v) { - size_t shift = (sizeof(T) - size) * 8; - if (shift > 0) { - value = T(value << shift) >> shift; - } - } - return ret; -} - -/** - * @brief Parses a variable-length float from the DEX byte stream. - * @tparam T The floating-point type to parse (float or double). - * @param pptr Pointer to the current position in the byte stream. - * @param size The number of bytes to read. - * @return A vector of bytes containing the parsed value. - */ -template -static std::vector ParseFloatValue(const dex::u1 **pptr, size_t size) { - std::vector ret(sizeof(T), 0); - T &value = *reinterpret_cast(ret.data()); - // The value is right-padded with zero bytes, so we copy into the higher-order bytes. - int start_byte = sizeof(T) - size; - for (dex::u1 *p = reinterpret_cast(&value) + start_byte; size > 0; --size) { - *p++ = *(*pptr)++; - } - return ret; -} - -// Forward declarations for recursive parsing functions. -Annotation ParseAnnotation(const dex::u1 **annotation, AnnotationList &annotation_list, - ArrayList &array_list); - -Array ParseArray(const dex::u1 **array, AnnotationList &annotation_list, ArrayList &array_list); - -/** - * @brief Parses a single `encoded_value` from the byte stream. - * This is the core of the annotation parsing logic and - * handles all possible value types recursively. - */ -Value ParseValue(const dex::u1 **value, AnnotationList &annotation_list, ArrayList &array_list) { - Value res; - auto &[type, value_content] = res; - auto header = *(*value)++; - type = header & dex::kEncodedValueTypeMask; - dex::u1 arg = header >> dex::kEncodedValueArgShift; - switch (type) { - // For numeric types, `arg` is `size - 1`. - case dex::kEncodedByte: - value_content = ParseIntValue(value, arg + 1); - break; - case dex::kEncodedShort: - value_content = ParseIntValue(value, arg + 1); - break; - case dex::kEncodedChar: - value_content = ParseIntValue(value, arg + 1); - break; - case dex::kEncodedInt: - value_content = ParseIntValue(value, arg + 1); - break; - case dex::kEncodedLong: - value_content = ParseIntValue(value, arg + 1); - break; - case dex::kEncodedFloat: - value_content = ParseFloatValue(value, arg + 1); - break; - case dex::kEncodedDouble: - value_content = ParseFloatValue(value, arg + 1); - break; - // For index types, the value is the index itself. - case dex::kEncodedMethodType: - case dex::kEncodedMethodHandle: - case dex::kEncodedString: - case dex::kEncodedType: - case dex::kEncodedField: - case dex::kEncodedMethod: - case dex::kEncodedEnum: - value_content = ParseIntValue(value, arg + 1); - break; - // For complex types, we parse them recursively and store an index to the - // parsed object. - case dex::kEncodedArray: - value_content.resize(sizeof(jint)); - *reinterpret_cast(value_content.data()) = static_cast(array_list.size()); - array_list.emplace_back(ParseArray(value, annotation_list, array_list)); - break; - case dex::kEncodedAnnotation: - value_content.resize(sizeof(jint)); - *reinterpret_cast(value_content.data()) = static_cast(annotation_list.size()); - annotation_list.emplace_back(ParseAnnotation(value, annotation_list, array_list)); - break; - case dex::kEncodedNull: - // No value content needed. - break; - case dex::kEncodedBoolean: - // The boolean value is stored in the `arg` part of the header. - value_content = {static_cast(arg == 1)}; - break; - default: - // This should never be reached for a valid DEX file. - __builtin_unreachable(); - } - return res; -} - -/** - * @brief Parses an `encoded_annotation` structure. - */ -Annotation ParseAnnotation(const dex::u1 **annotation, AnnotationList &annotation_list, - ArrayList &array_list) { - Annotation ret = {dex::kVisibilityEncoded, dex::ReadULeb128(annotation), ElementList{}}; - auto &[vis, type, element_list] = ret; - auto size = dex::ReadULeb128(annotation); - element_list.resize(size); - for (size_t j = 0; j < size; ++j) { - auto &[name, value] = element_list[j]; - name = static_cast(dex::ReadULeb128(annotation)); - value = ParseValue(annotation, annotation_list, array_list); - } - return ret; -} - -/** - * @brief Parses an `encoded_array` structure. - */ -Array ParseArray(const dex::u1 **array, AnnotationList &annotation_list, ArrayList &array_list) { - auto size = dex::ReadULeb128(array); - Array ret; - ret.reserve(size); - for (size_t i = 0; i < size; ++i) { - ret.emplace_back(ParseValue(array, annotation_list, array_list)); - } - return ret; -} - -/** - * @brief Parses an `AnnotationSetItem`, which is a collection of annotations. - */ -void ParseAnnotationSet(dex::Reader &dex, AnnotationList &annotation_list, ArrayList &array_list, - std::vector &indices, const dex::AnnotationSetItem *annotation_set) { - if (annotation_set == nullptr) { - return; - } - for (size_t i = 0; i < annotation_set->size; ++i) { - auto *item = dex.dataPtr(annotation_set->entries[i]); - auto *annotation_data = item->annotation; - // Store the index of the new annotation in the output list. - indices.emplace_back(annotation_list.size()); - // Parse the annotation and add it to the global list. - auto &[visibility, type, element_list] = annotation_list.emplace_back( - ParseAnnotation(&annotation_data, annotation_list, array_list)); - // The visibility is stored in the item, not the encoded annotation itself. - visibility = item->visibility; - } -} -} // namespace - -namespace vector::native::jni { -/** - * @brief JNI method to open a DEX file and perform initial parsing. - * @param data A direct java.nio.ByteBuffer containing the DEX file. - * @param args A jlongArray used for passing arguments. - * args[0] is an output parameter to store the native DexParser pointer (cookie). - * args[1] is an input flag to control whether to parse annotations. - * @return A java.lang.Object[] array containing the top-level DEX structures. - */ -VECTOR_DEF_NATIVE_METHOD(jobject, DexParserBridge, openDex, jobject data, jlongArray args) { - auto dex_size = env->GetDirectBufferCapacity(data); - if (dex_size == -1) { - env->ThrowNew(env->FindClass("java/io/IOException"), - "DEX data must be in a direct ByteBuffer"); - return nullptr; - } - auto *dex_data = env->GetDirectBufferAddress(data); - if (dex_data == nullptr) { - env->ThrowNew(env->FindClass("java/io/IOException"), "Failed to get direct buffer address"); - return nullptr; - } - - // Create the native parser object. - // This will be the handle for subsequent calls. - auto *dex_reader = new DexParser(reinterpret_cast(dex_data), dex_size); - auto *args_ptr = env->GetLongArrayElements(args, nullptr); - auto include_annotations = args_ptr[1]; - env->ReleaseLongArrayElements(args, args_ptr, JNI_ABORT); - // Store the pointer to the native object in the first element of the args array. - // This "cookie" will be passed back to other native methods. - env->SetLongArrayRegion(args, 0, 1, reinterpret_cast(&dex_reader)); - auto &dex = *dex_reader; - if (dex.IsCompact()) { - env->ThrowNew(env->FindClass("java/io/IOException"), "Compact dex is not supported"); - delete dex_reader; // Clean up before returning. - return nullptr; - } - - // Find classes needed for creating Java objects. - auto object_class = env->FindClass("java/lang/Object"); - auto string_class = env->FindClass("java/lang/String"); - auto int_array_class = env->FindClass("[I"); - // This is the main output array that will be returned to Java. - auto out = env->NewObjectArray(8, object_class, nullptr); - - // 1. Parse String IDs - auto out0 = - env->NewObjectArray(static_cast(dex.StringIds().size()), string_class, nullptr); - auto strings = dex.StringIds(); - for (size_t i = 0; i < strings.size(); ++i) { - const auto *ptr = dex.dataPtr(strings[i].string_data_off); - // The string data is MUTF-8 encoded. We skip the length prefix. - [[maybe_unused]] size_t len = dex::ReadULeb128(&ptr); - auto str = env->NewStringUTF(reinterpret_cast(ptr)); - env->SetObjectArrayElement(out0, static_cast(i), str); - env->DeleteLocalRef(str); - } - env->SetObjectArrayElement(out, 0, out0); - env->DeleteLocalRef(out0); - - // 2. Parse Type IDs - auto types = dex.TypeIds(); - auto out1 = env->NewIntArray(static_cast(types.size())); - auto *out1_ptr = env->GetIntArrayElements(out1, nullptr); - for (size_t i = 0; i < types.size(); ++i) { - out1_ptr[i] = static_cast(types[i].descriptor_idx); // Index into String table - } - env->ReleaseIntArrayElements(out1, out1_ptr, 0); - env->SetObjectArrayElement(out, 1, out1); - env->DeleteLocalRef(out1); - - // 3. Parse Proto IDs (Method Prototypes) - auto protos = dex.ProtoIds(); - auto out2 = env->NewObjectArray(static_cast(protos.size()), int_array_class, nullptr); - auto empty_type_list = dex::TypeList{.size = 0, .list = {}}; - for (size_t i = 0; i < protos.size(); ++i) { - auto &proto = protos[i]; - const auto ¶ms = proto.parameters_off - ? *dex.dataPtr(proto.parameters_off) - : empty_type_list; - - auto out2i = env->NewIntArray(static_cast(2 + params.size)); - auto *out2i_ptr = env->GetIntArrayElements(out2i, nullptr); - out2i_ptr[0] = static_cast(proto.shorty_idx); - out2i_ptr[1] = static_cast(proto.return_type_idx); - for (size_t j = 0; j < params.size; ++j) { - out2i_ptr[2 + j] = static_cast(params.list[j].type_idx); - } - env->ReleaseIntArrayElements(out2i, out2i_ptr, 0); - env->SetObjectArrayElement(out2, static_cast(i), out2i); - env->DeleteLocalRef(out2i); - } - env->SetObjectArrayElement(out, 2, out2); - env->DeleteLocalRef(out2); - - // 4. Parse Field IDs - auto fields = dex.FieldIds(); - auto out3 = env->NewIntArray(static_cast(3 * fields.size())); - auto *out3_ptr = env->GetIntArrayElements(out3, nullptr); - for (size_t i = 0; i < fields.size(); ++i) { - auto &field = fields[i]; - out3_ptr[3 * i] = static_cast(field.class_idx); // Defining class type index - out3_ptr[3 * i + 1] = static_cast(field.type_idx); // Field type index - out3_ptr[3 * i + 2] = static_cast(field.name_idx); // Field name string index - } - env->ReleaseIntArrayElements(out3, out3_ptr, 0); - env->SetObjectArrayElement(out, 3, out3); - env->DeleteLocalRef(out3); - - // 5. Parse Method IDs - auto methods = dex.MethodIds(); - auto out4 = env->NewIntArray(static_cast(3 * methods.size())); - auto *out4_ptr = env->GetIntArrayElements(out4, nullptr); - for (size_t i = 0; i < methods.size(); ++i) { - out4_ptr[3 * i] = static_cast(methods[i].class_idx); // Defining class type index - out4_ptr[3 * i + 1] = static_cast(methods[i].proto_idx); // Method prototype index - out4_ptr[3 * i + 2] = static_cast(methods[i].name_idx); // Method name string index - } - env->ReleaseIntArrayElements(out4, out4_ptr, 0); - env->SetObjectArrayElement(out, 4, out4); - env->DeleteLocalRef(out4); - - // 6. Parse Class Definitions and their data - auto classes = dex.ClassDefs(); - dex.class_data.resize(classes.size()); - - // These lists will store all annotations found in the DEX file. - AnnotationList annotation_list; - ArrayList array_list; - - for (size_t i = 0; i < classes.size(); ++i) { - auto &class_def = classes[i]; - - // Pointers to various parts of the class data. Initialize to safe defaults. - dex::u4 static_fields_count = 0; - dex::u4 instance_fields_count = 0; - dex::u4 direct_methods_count = 0; - dex::u4 virtual_methods_count = 0; - const dex::u1 *class_data_ptr = nullptr; - - const dex::AnnotationsDirectoryItem *annotations = nullptr; - const dex::AnnotationSetItem *class_annotation = nullptr; - dex::u4 field_annotations_count = 0; - dex::u4 method_annotations_count = 0; - dex::u4 parameter_annotations_count = 0; - - auto &class_data = dex.class_data[i]; - - // Parse implemented interfaces. - if (class_def.interfaces_off) { - auto defined_interfaces = dex.dataPtr(class_def.interfaces_off); - class_data.interfaces.resize(defined_interfaces->size); - for (size_t k = 0; k < class_data.interfaces.size(); ++k) { - class_data.interfaces[k] = defined_interfaces->list[k].type_idx; - } - } - - // Locate the annotations directory for this class, if it exists. - if (class_def.annotations_off != 0) { - annotations = dex.dataPtr(class_def.annotations_off); - if (annotations->class_annotations_off != 0) { - class_annotation = - dex.dataPtr(annotations->class_annotations_off); - } - field_annotations_count = annotations->fields_size; - method_annotations_count = annotations->methods_size; - parameter_annotations_count = annotations->parameters_size; - } - - // Read the core class data: fields and methods. - if (class_def.class_data_off != 0) { - class_data_ptr = dex.dataPtr(class_def.class_data_off); - static_fields_count = dex::ReadULeb128(&class_data_ptr); - instance_fields_count = dex::ReadULeb128(&class_data_ptr); - direct_methods_count = dex::ReadULeb128(&class_data_ptr); - virtual_methods_count = dex::ReadULeb128(&class_data_ptr); - // Pre-allocate vectors to improve performance. - class_data.static_fields.resize(static_fields_count); - class_data.static_fields_access_flags.resize(static_fields_count); - class_data.instance_fields.resize(instance_fields_count); - class_data.instance_fields_access_flags.resize(instance_fields_count); - class_data.direct_methods.resize(direct_methods_count); - class_data.direct_methods_access_flags.resize(direct_methods_count); - class_data.direct_methods_code.resize(direct_methods_count); - class_data.virtual_methods.resize(virtual_methods_count); - class_data.virtual_methods_access_flags.resize(virtual_methods_count); - class_data.virtual_methods_code.resize(virtual_methods_count); - } - - // Now, decode the field and method lists. - if (class_data_ptr) { - // Static fields - for (size_t k = 0, field_idx = 0; k < static_fields_count; ++k) { - field_idx += - dex::ReadULeb128(&class_data_ptr); // field_idx is a diff from previous - class_data.static_fields[k] = static_cast(field_idx); - class_data.static_fields_access_flags[k] = - static_cast(dex::ReadULeb128(&class_data_ptr)); - } - - // Instance fields - for (size_t k = 0, field_idx = 0; k < instance_fields_count; ++k) { - field_idx += dex::ReadULeb128(&class_data_ptr); - class_data.instance_fields[k] = static_cast(field_idx); - class_data.instance_fields_access_flags[k] = - static_cast(dex::ReadULeb128(&class_data_ptr)); - } - - // Direct methods (static, private, constructors) - for (size_t k = 0, method_idx = 0; k < direct_methods_count; ++k) { - method_idx += dex::ReadULeb128(&class_data_ptr); - class_data.direct_methods[k] = static_cast(method_idx); - class_data.direct_methods_access_flags[k] = - static_cast(dex::ReadULeb128(&class_data_ptr)); - auto code_off = dex::ReadULeb128(&class_data_ptr); - class_data.direct_methods_code[k] = - code_off ? dex.dataPtr(code_off) : nullptr; - } - - // Virtual methods - for (size_t k = 0, method_idx = 0; k < virtual_methods_count; ++k) { - method_idx += dex::ReadULeb128(&class_data_ptr); - class_data.virtual_methods[k] = static_cast(method_idx); - class_data.virtual_methods_access_flags[k] = - static_cast(dex::ReadULeb128(&class_data_ptr)); - auto code_off = dex::ReadULeb128(&class_data_ptr); - class_data.virtual_methods_code[k] = - code_off ? dex.dataPtr(code_off) : nullptr; - } - } - - // Optionally skip the expensive annotation parsing. - if (!include_annotations) continue; - - // Parse annotations for the class, its fields, methods, and parameters. - ParseAnnotationSet(dex, annotation_list, array_list, class_data.annotations, - class_annotation); - - auto *field_annotations = - annotations ? reinterpret_cast(annotations + 1) - : nullptr; - for (size_t k = 0; k < field_annotations_count; ++k) { - auto *field_annotation = - dex.dataPtr(field_annotations[k].annotations_off); - ParseAnnotationSet( - dex, annotation_list, array_list, - dex.field_annotations[static_cast(field_annotations[k].field_idx)], - field_annotation); - } - - auto *method_annotations = field_annotations - ? reinterpret_cast( - field_annotations + field_annotations_count) - : nullptr; - for (size_t k = 0; k < method_annotations_count; ++k) { - auto *method_annotation = - dex.dataPtr(method_annotations[k].annotations_off); - ParseAnnotationSet( - dex, annotation_list, array_list, - dex.method_annotations[static_cast(method_annotations[k].method_idx)], - method_annotation); - } - - auto *parameter_annotations = method_annotations - ? reinterpret_cast( - method_annotations + method_annotations_count) - : nullptr; - for (size_t k = 0; k < parameter_annotations_count; ++k) { - auto *parameter_annotation = - dex.dataPtr(parameter_annotations[k].annotations_off); - auto &indices = - dex.parameter_annotations[static_cast(parameter_annotations[k].method_idx)]; - for (size_t l = 0; l < parameter_annotation->size; ++l) { - if (parameter_annotation->list[l].annotations_off != 0) { - auto *parameter_annotation_item = dex.dataPtr( - parameter_annotation->list[l].annotations_off); - ParseAnnotationSet(dex, annotation_list, array_list, indices, - parameter_annotation_item); - } - // A kNoIndex entry serves as a separator between parameter annotation sets. - indices.emplace_back(dex::kNoIndex); - } - } - } - - // If annotations were skipped, we are done. - if (!include_annotations) return out; - - // 7. Convert parsed C++ annotation structures to Java objects. - auto out5 = env->NewIntArray(static_cast(2 * annotation_list.size())); - auto out6 = - env->NewObjectArray(static_cast(2 * annotation_list.size()), object_class, nullptr); - auto out5_ptr = env->GetIntArrayElements(out5, nullptr); - size_t i = 0; - for (auto &[visibility, type, items] : annotation_list) { - auto out6i0 = env->NewIntArray(static_cast(2 * items.size())); - auto out6i0_ptr = env->GetIntArrayElements(out6i0, nullptr); - auto out6i1 = env->NewObjectArray(static_cast(items.size()), object_class, nullptr); - size_t j = 0; - for (auto &[name, value] : items) { - auto &[value_type, value_data] = value; - // The raw value data is passed in a direct ByteBuffer. - auto java_value = value_data.empty() - ? nullptr - : env->NewDirectByteBuffer(value_data.data(), value_data.size()); - env->SetObjectArrayElement(out6i1, static_cast(j), java_value); - out6i0_ptr[2 * j] = name; - out6i0_ptr[2 * j + 1] = value_type; - env->DeleteLocalRef(java_value); - ++j; - } - env->ReleaseIntArrayElements(out6i0, out6i0_ptr, 0); - env->SetObjectArrayElement(out6, static_cast(2 * i), out6i0); - env->SetObjectArrayElement(out6, static_cast(2 * i + 1), out6i1); - out5_ptr[2 * i] = visibility; - out5_ptr[2 * i + 1] = type; - env->DeleteLocalRef(out6i0); - env->DeleteLocalRef(out6i1); - ++i; - } - env->ReleaseIntArrayElements(out5, out5_ptr, 0); - env->SetObjectArrayElement(out, 5, out5); - env->SetObjectArrayElement(out, 6, out6); - env->DeleteLocalRef(out5); - env->DeleteLocalRef(out6); - - // 8. Convert parsed C++ array values to Java objects. - auto out7 = - env->NewObjectArray(static_cast(2 * array_list.size()), object_class, nullptr); - i = 0; - for (auto &array : array_list) { - auto out7i0 = env->NewIntArray(static_cast(array.size())); - auto out7i0_ptr = env->GetIntArrayElements(out7i0, nullptr); - auto out7i1 = env->NewObjectArray(static_cast(array.size()), object_class, nullptr); - size_t j = 0; - for (auto &value : array) { - auto &[value_type, value_data] = value; - auto java_value = value_data.empty() - ? nullptr - : env->NewDirectByteBuffer(value_data.data(), value_data.size()); - out7i0_ptr[j] = value_type; - env->SetObjectArrayElement(out7i1, static_cast(j), java_value); - env->DeleteLocalRef(java_value); - ++j; - } - env->ReleaseIntArrayElements(out7i0, out7i0_ptr, 0); - env->SetObjectArrayElement(out7, static_cast(2 * i), out7i0); - env->SetObjectArrayElement(out7, static_cast(2 * i + 1), out7i1); - env->DeleteLocalRef(out7i0); - env->DeleteLocalRef(out7i1); - ++i; - } - env->SetObjectArrayElement(out, 7, out7); - env->DeleteLocalRef(out7); - - return out; -} - -/** - * @brief JNI method to release the native DexParser object. - * @param cookie The pointer to the DexParser object created by `openDex`. - */ -VECTOR_DEF_NATIVE_METHOD(void, DexParserBridge, closeDex, jlong cookie) { - if (cookie != 0) delete reinterpret_cast(cookie); -} - -/** - * @brief Iterates through classes, fields, and methods, calling back to a Java - * visitor. - * @param cookie The pointer to the DexParser object. - * @param visitor The main Java visitor object. - * @param ...visitor_class/.._method Java reflection objects used to - * get method IDs and perform type checks. - */ -VECTOR_DEF_NATIVE_METHOD(void, DexParserBridge, visitClass, jlong cookie, jobject visitor, - jclass field_visitor_class, jclass method_visitor_class, - jobject class_visit_method, jobject field_visit_method, - jobject method_visit_method, jobject method_body_visit_method, - jobject stop_method) { - // Constants for DEX opcodes used in method body parsing. - static constexpr dex::u1 kOpcodeMask = 0xff; - static constexpr dex::u1 kOpcodeNoOp = 0x00; - static constexpr dex::u1 kOpcodeConstString = 0x1a; - static constexpr dex::u1 kOpcodeConstStringJumbo = 0x1b; - static constexpr dex::u1 kOpcodeIGetStart = 0x52; - static constexpr dex::u1 kOpcodeIGetEnd = 0x58; - static constexpr dex::u1 kOpcodeSGetStart = 0x60; - static constexpr dex::u1 kOpcodeSGetEnd = 0x66; - static constexpr dex::u1 kOpcodeIPutStart = 0x59; - static constexpr dex::u1 kOpcodeIPutEnd = 0x5f; - static constexpr dex::u1 kOpcodeSPutStart = 0x67; - static constexpr dex::u1 kOpcodeSPutEnd = 0x6d; - static constexpr dex::u1 kOpcodeInvokeStart = 0x6e; - static constexpr dex::u1 kOpcodeInvokeEnd = 0x72; - static constexpr dex::u1 kOpcodeInvokeRangeStart = 0x74; - static constexpr dex::u1 kOpcodeInvokeRangeEnd = 0x78; - // Constants for special "payload" opcodes that follow a NOP instruction. - static constexpr dex::u2 kInstPackedSwitchPlayLoad = 0x0100; - static constexpr dex::u2 kInstSparseSwitchPlayLoad = 0x0200; - static constexpr dex::u2 kInstFillArrayDataPlayLoad = 0x0300; - - if (cookie == 0) { - return; - } - auto &dex = *reinterpret_cast(cookie); - // Get jmethodIDs from the reflected java.lang.reflect.Method objects. - auto *visit_class = env->FromReflectedMethod(class_visit_method); - auto *visit_field = env->FromReflectedMethod(field_visit_method); - auto *visit_method = env->FromReflectedMethod(method_visit_method); - auto *visit_method_body = env->FromReflectedMethod(method_body_visit_method); - auto *stop = env->FromReflectedMethod(stop_method); - - auto classes = dex.ClassDefs(); - - for (size_t i = 0; i < classes.size(); ++i) { - auto &class_def = classes[i]; - auto &class_data = dex.class_data[i]; - - // --- Prepare arguments for the visit_class callback --- - // This involves converting C++ vectors of integers into Java int arrays. - auto interfaces = env->NewIntArray(static_cast(class_data.interfaces.size())); - env->SetIntArrayRegion(interfaces, 0, static_cast(class_data.interfaces.size()), - class_data.interfaces.data()); - auto static_fields = env->NewIntArray(static_cast(class_data.static_fields.size())); - env->SetIntArrayRegion(static_fields, 0, static_cast(class_data.static_fields.size()), - class_data.static_fields.data()); - auto static_fields_access_flags = - env->NewIntArray(static_cast(class_data.static_fields_access_flags.size())); - env->SetIntArrayRegion(static_fields_access_flags, 0, - static_cast(class_data.static_fields_access_flags.size()), - class_data.static_fields_access_flags.data()); - auto instance_fields = - env->NewIntArray(static_cast(class_data.instance_fields.size())); - env->SetIntArrayRegion(instance_fields, 0, - static_cast(class_data.instance_fields.size()), - class_data.instance_fields.data()); - auto instance_fields_access_flags = - env->NewIntArray(static_cast(class_data.instance_fields_access_flags.size())); - env->SetIntArrayRegion(instance_fields_access_flags, 0, - static_cast(class_data.instance_fields_access_flags.size()), - class_data.instance_fields_access_flags.data()); - auto direct_methods = env->NewIntArray(static_cast(class_data.direct_methods.size())); - env->SetIntArrayRegion(direct_methods, 0, - static_cast(class_data.direct_methods.size()), - class_data.direct_methods.data()); - auto direct_methods_access_flags = - env->NewIntArray(static_cast(class_data.direct_methods_access_flags.size())); - env->SetIntArrayRegion(direct_methods_access_flags, 0, - static_cast(class_data.direct_methods_access_flags.size()), - class_data.direct_methods_access_flags.data()); - auto virtual_methods = - env->NewIntArray(static_cast(class_data.virtual_methods.size())); - env->SetIntArrayRegion(virtual_methods, 0, - static_cast(class_data.virtual_methods.size()), - class_data.virtual_methods.data()); - auto virtual_methods_access_flags = - env->NewIntArray(static_cast(class_data.virtual_methods_access_flags.size())); - env->SetIntArrayRegion(virtual_methods_access_flags, 0, - static_cast(class_data.virtual_methods_access_flags.size()), - class_data.virtual_methods_access_flags.data()); - auto class_annotations = env->NewIntArray(static_cast(class_data.annotations.size())); - env->SetIntArrayRegion(class_annotations, 0, - static_cast(class_data.annotations.size()), - class_data.annotations.data()); - - // --- Call back to the Java visitor for the class --- - jobject member_visitor = env->CallObjectMethod( - visitor, visit_class, static_cast(class_def.class_idx), - static_cast(class_def.access_flags), static_cast(class_def.superclass_idx), - interfaces, static_cast(class_def.source_file_idx), static_fields, - static_fields_access_flags, instance_fields, instance_fields_access_flags, - direct_methods, direct_methods_access_flags, virtual_methods, - virtual_methods_access_flags, class_annotations); - - // --- Clean up local JNI references --- - env->DeleteLocalRef(interfaces); - env->DeleteLocalRef(static_fields); - env->DeleteLocalRef(static_fields_access_flags); - env->DeleteLocalRef(instance_fields); - env->DeleteLocalRef(instance_fields_access_flags); - env->DeleteLocalRef(direct_methods); - env->DeleteLocalRef(direct_methods_access_flags); - env->DeleteLocalRef(virtual_methods); - env->DeleteLocalRef(virtual_methods_access_flags); - env->DeleteLocalRef(class_annotations); - - // --- Visit fields --- - if (member_visitor && env->IsInstanceOf(member_visitor, field_visitor_class)) { - jboolean stopped = JNI_FALSE; - // This structured binding provides a clean way to iterate over both - // static and instance field collections. - for (auto &[fields, fields_access_flags] : - {std::tie(class_data.static_fields, class_data.static_fields_access_flags), - std::tie(class_data.instance_fields, class_data.instance_fields_access_flags)}) { - for (size_t j = 0; j < fields.size(); j++) { - auto field_idx = fields[j]; - auto access_flags = fields_access_flags[j]; - auto &field_annotations = dex.field_annotations[field_idx]; - auto annotations = - env->NewIntArray(static_cast(field_annotations.size())); - env->SetIntArrayRegion(annotations, 0, - static_cast(field_annotations.size()), - field_annotations.data()); - // Call back to Java for this field. - env->CallVoidMethod(member_visitor, visit_field, field_idx, access_flags, - annotations); - env->DeleteLocalRef(annotations); - // Check if the visitor wants to stop iteration. - stopped = env->CallBooleanMethod(member_visitor, stop); - if (stopped == JNI_TRUE) break; - } - if (stopped == JNI_TRUE) break; - } - } - - // --- Visit methods --- - if (member_visitor && env->IsInstanceOf(member_visitor, method_visitor_class)) { - jboolean stopped = JNI_FALSE; - // Iterate over both direct and virtual methods. - for (auto &[methods, methods_access_flags, methods_code] : - {std::tie(class_data.direct_methods, class_data.direct_methods_access_flags, - class_data.direct_methods_code), - std::tie(class_data.virtual_methods, class_data.virtual_methods_access_flags, - class_data.virtual_methods_code)}) { - for (size_t j = 0; j < methods.size(); j++) { - auto method_idx = methods[j]; - auto access_flags = methods_access_flags[j]; - auto code = methods_code[j]; - auto method_annotation = dex.method_annotations[method_idx]; - auto method_annotations = - env->NewIntArray(static_cast(method_annotation.size())); - env->SetIntArrayRegion(method_annotations, 0, - static_cast(method_annotation.size()), - method_annotation.data()); - auto parameter_annotation = dex.parameter_annotations[method_idx]; - auto parameter_annotations = - env->NewIntArray(static_cast(parameter_annotation.size())); - env->SetIntArrayRegion(parameter_annotations, 0, - static_cast(parameter_annotation.size()), - parameter_annotation.data()); - // Call back to Java for this method. - // This may return a "body visitor". - auto body_visitor = env->CallObjectMethod( - member_visitor, visit_method, method_idx, access_flags, code != nullptr, - method_annotations, parameter_annotations); - env->DeleteLocalRef(method_annotations); - env->DeleteLocalRef(parameter_annotations); - // --- Lazily parse the method body if requested --- - if (body_visitor && code != nullptr) { - auto &body = dex.method_bodies[method_idx]; - if (!body.loaded) { - // Using hash sets for efficient collection of unique indices. - phmap::flat_hash_set referred_strings; - phmap::flat_hash_set assigned_fields; - phmap::flat_hash_set accessed_fields; - phmap::flat_hash_set invoked_methods; - - const dex::u2 *inst = code->insns; - const dex::u2 *end = inst + code->insns_size; - // Iterate through the bytecode instructions. - while (inst < end) { - dex::u1 opcode = *inst & kOpcodeMask; - body.opcodes.push_back(static_cast(opcode)); - // Check for opcodes of interest. - if (opcode == kOpcodeConstString) { - auto str_idx = inst[1]; - referred_strings.emplace(str_idx); - } - if (opcode == kOpcodeConstStringJumbo) { - auto str_idx = *reinterpret_cast(&inst[1]); - referred_strings.emplace(static_cast(str_idx)); - } - if ((opcode >= kOpcodeIGetStart && opcode <= kOpcodeIGetEnd) || - (opcode >= kOpcodeSGetStart && opcode <= kOpcodeSGetEnd)) { - auto field_idx = inst[1]; - accessed_fields.emplace(field_idx); - } - if ((opcode >= kOpcodeIPutStart && opcode <= kOpcodeIPutEnd) || - (opcode >= kOpcodeSPutStart && opcode <= kOpcodeSPutEnd)) { - auto field_idx = inst[1]; - assigned_fields.emplace(field_idx); - } - if ((opcode >= kOpcodeInvokeStart && opcode <= kOpcodeInvokeEnd) || - (opcode >= kOpcodeInvokeRangeStart && - opcode <= kOpcodeInvokeRangeEnd)) { - auto callee = inst[1]; - invoked_methods.emplace(callee); - } - // Handle special payload instructions which have variable - // length. - if (opcode == kOpcodeNoOp) { - if (*inst == kInstPackedSwitchPlayLoad) { - inst += inst[1] * 2 + 3; - } else if (*inst == kInstSparseSwitchPlayLoad) { - inst += inst[1] * 4 + 1; - } else if (*inst == kInstFillArrayDataPlayLoad) { - inst += (*reinterpret_cast(&inst[2]) * - inst[1] + - 1) / - 2 + - 3; - } - } - // Advance instruction pointer by the known length of - // the current opcode. - inst += dex::opcode_len[opcode]; - } - // Copy the collected unique indices into the body's vectors. - body.referred_strings.assign(referred_strings.begin(), - referred_strings.end()); - body.assigned_fields.assign(assigned_fields.begin(), - assigned_fields.end()); - body.accessed_fields.assign(accessed_fields.begin(), - accessed_fields.end()); - body.invoked_methods.assign(invoked_methods.begin(), - invoked_methods.end()); - body.loaded = true; - } - // --- Prepare arguments and call back for the method body --- - auto referred_strings = - env->NewIntArray(static_cast(body.referred_strings.size())); - env->SetIntArrayRegion(referred_strings, 0, - static_cast(body.referred_strings.size()), - body.referred_strings.data()); - auto accessed_fields = - env->NewIntArray(static_cast(body.accessed_fields.size())); - env->SetIntArrayRegion(accessed_fields, 0, - static_cast(body.accessed_fields.size()), - body.accessed_fields.data()); - auto assigned_fields = - env->NewIntArray(static_cast(body.assigned_fields.size())); - env->SetIntArrayRegion(assigned_fields, 0, - static_cast(body.assigned_fields.size()), - body.assigned_fields.data()); - auto invoked_methods = - env->NewIntArray(static_cast(body.invoked_methods.size())); - env->SetIntArrayRegion(invoked_methods, 0, - static_cast(body.invoked_methods.size()), - body.invoked_methods.data()); - auto opcodes = env->NewByteArray(static_cast(body.opcodes.size())); - env->SetByteArrayRegion(opcodes, 0, static_cast(body.opcodes.size()), - body.opcodes.data()); - env->CallVoidMethod(body_visitor, visit_method_body, referred_strings, - invoked_methods, accessed_fields, assigned_fields, - opcodes); - } - stopped = env->CallBooleanMethod(member_visitor, stop); - if (stopped == JNI_TRUE) break; - } - if (stopped == JNI_TRUE) break; - } - } - // Check if the top-level visitor wants to stop. - if (env->CallBooleanMethod(visitor, stop) == JNI_TRUE) break; - } -} - -// Array of native method descriptors for JNI registration. -static JNINativeMethod gMethods[] = { - VECTOR_NATIVE_METHOD(DexParserBridge, openDex, "(Ljava/nio/ByteBuffer;[J)Ljava/lang/Object;"), - VECTOR_NATIVE_METHOD(DexParserBridge, closeDex, "(J)V"), - VECTOR_NATIVE_METHOD(DexParserBridge, visitClass, - "(JLjava/lang/Object;Ljava/lang/Class;Ljava/lang/Class;Ljava/lang/" - "reflect/Method;Ljava/lang/reflect/Method;Ljava/lang/reflect/" - "Method;Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;)V"), -}; - -/** - * @brief Registers the native methods with the JVM. - */ -void RegisterDexParserBridge(JNIEnv *env) { REGISTER_VECTOR_NATIVE_METHODS(DexParserBridge); } - -} // namespace vector::native::jni From 77f8e5be360da84e8eaa0a52525f115196de1a3a Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Tue, 24 Mar 2026 19:06:51 +0100 Subject: [PATCH 28/34] Trigger onPackageReady for split APKs --- .../vector/impl/hookers/LoadedApkHookers.kt | 88 +++++++++++-------- 1 file changed, 49 insertions(+), 39 deletions(-) diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt index 79bbe6bf8..e71f076c6 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/hookers/LoadedApkHookers.kt @@ -4,7 +4,10 @@ import android.content.pm.ApplicationInfo import android.os.Build import androidx.annotation.RequiresApi import io.github.libxposed.api.XposedInterface +import java.util.Collections +import java.util.WeakHashMap import java.util.concurrent.ConcurrentHashMap +import org.lsposed.lspd.util.Utils import org.matrix.vector.impl.VectorLifecycleManager import org.matrix.vector.impl.di.LegacyPackageInfo import org.matrix.vector.impl.di.VectorBootstrap @@ -58,6 +61,13 @@ private object PackageContextHelper { } } +/** Identity-based tracking for LoadedApk instances. */ +private object LoadedApkTracker { + // Tracks LoadedApk instances that are currently in their initial bootstrap phase + val activeApks: MutableSet = + Collections.synchronizedSet(Collections.newSetFromMap(WeakHashMap())) +} + /** Tracks and prepares Application instances when their LoadedApk is instantiated. */ object LoadedApkCtorHooker : XposedInterface.Hooker { val trackedApks = ConcurrentHashMap.newKeySet() @@ -75,38 +85,40 @@ object LoadedApkCtorHooker : XposedInterface.Hooker { } } - // Avoid OnePlus custom opt crashing + // OnePlus workaround to avoid custom opt crashing val isPreload = Throwable().stackTrace.any { it.className == "android.app.ActivityThread\$ApplicationThread" && it.methodName == "schedulePreload" } - - if (isPreload) { - return result + if (!isPreload) { + LoadedApkTracker.activeApks.add(loadedApk) } - trackedApks.add(loadedApk) return result } } -/** Hooking `createAppFactory` is critical to defeating the `` evasion exploit. */ +/** Modern API Phase: onPackageLoaded */ @RequiresApi(Build.VERSION_CODES.P) object LoadedApkCreateAppFactoryHooker : XposedInterface.Hooker { override fun intercept(chain: XposedInterface.Chain): Any? { val loadedApk = chain.thisObject ?: return chain.proceed() + // Ensure we only dispatch for instances we are tracking + if (!LoadedApkTracker.activeApks.contains(loadedApk)) return chain.proceed() + val appInfo = chain.args[0] as ApplicationInfo val defaultClassLoader = chain.args[1] as? ClassLoader ?: return chain.proceed() // Skip dispatch if there's no ClassLoader - val apkPackageName = loadedApk.getFieldValue("mPackageName") ?: appInfo.packageName - - val ctx = PackageContextHelper.resolve(loadedApk, apkPackageName) // Only dispatch if on API 29+ per libxposed API specification if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val apkPackageName = + loadedApk.getFieldValue("mPackageName") ?: appInfo.packageName + val ctx = PackageContextHelper.resolve(loadedApk, apkPackageName) + VectorLifecycleManager.dispatchPackageLoaded( ctx.packageName, appInfo, @@ -119,38 +131,27 @@ object LoadedApkCreateAppFactoryHooker : XposedInterface.Hooker { } } -/** - * Triggers package ready events immediately after the final Application ClassLoader is created. - * Also acts as a fallback dispatcher for `onPackageLoaded` for resource-only APKs where - * `mIncludeCode` is false (meaning `createAppFactory` was never executed). - */ +/** Modern API Phase: onPackageReady and Legacy Phase: handleLoadPackage */ object LoadedApkCreateCLHooker : XposedInterface.Hooker { + // intercepting createOrUpdateClassLoaderLocked(List addedPaths) override fun intercept(chain: XposedInterface.Chain): Any? { val loadedApk = chain.thisObject ?: return chain.proceed() - // Fast path exit: Ignore if addedPaths is not null, or untracked - if ( - chain.args.firstOrNull() != null || !LoadedApkCtorHooker.trackedApks.contains(loadedApk) - ) { - return chain.proceed() - } - - // Proceed with Android's internal ClassLoader creation sequence + // Proceed: Modern modules need onPackageReady even for Split APKs (args[0] != null) + val isInitialLoad = + chain.args.firstOrNull() == null && LoadedApkTracker.activeApks.contains(loadedApk) val result = chain.proceed() try { val apkPackageName = loadedApk.getFieldValue("mPackageName") ?: return result - val mIncludeCode = loadedApk.getFieldValue("mIncludeCode") ?: true - val ctx = PackageContextHelper.resolve(loadedApk, apkPackageName) - - if (!ctx.isFirstPackage && !mIncludeCode) return result - val appInfo = loadedApk.getFieldValue("mApplicationInfo") ?: return result val classLoader = loadedApk.getFieldValue("mClassLoader") ?: return result val defaultClassLoader = loadedApk.getFieldValue("mDefaultClassLoader") ?: classLoader + val ctx = PackageContextHelper.resolve(loadedApk, apkPackageName) + // Dispatch Modern Lifecycle: onPackageReady if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { val appComponentFactory = loadedApk.getFieldValue("mAppComponentFactory") @@ -164,20 +165,29 @@ object LoadedApkCreateCLHooker : XposedInterface.Hooker { ) } - // Dispatch Legacy Lifecycle - VectorBootstrap.withLegacy { delegate -> - delegate.onPackageLoaded( - LegacyPackageInfo( - ctx.packageName, - ctx.processName, - classLoader, - appInfo, - ctx.isFirstPackage, - ) - ) + // Legacy API: Only dispatch once during initial load + if (isInitialLoad) { + val mIncludeCode = loadedApk.getFieldValue("mIncludeCode") ?: true + if (ctx.isFirstPackage || mIncludeCode) { + VectorBootstrap.withLegacy { delegate -> + delegate.onPackageLoaded( + LegacyPackageInfo( + ctx.packageName, + ctx.processName, + classLoader, + appInfo, + ctx.isFirstPackage, + ) + ) + } + } } + } catch (t: Throwable) { + Utils.logE("LoadedApkCreateCLHooker failed in post-proceed phase", t) } finally { - LoadedApkCtorHooker.trackedApks.remove(loadedApk) + // Cleanup: Once the initial load is done, we remove it from activeApks. + // Subsequent calls (Split APKs) will now be recognized as non-initial loads. + LoadedApkTracker.activeApks.remove(loadedApk) } return result From f767241d77fb2ed3eb3e652819274837b90c541f Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Tue, 24 Mar 2026 23:12:14 +0100 Subject: [PATCH 29/34] Fix release build issue --- legacy/consumer-rules.pro | 7 +++++-- xposed/consumer-rules.pro | 6 ------ zygisk/proguard-rules.pro | 7 ------- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/legacy/consumer-rules.pro b/legacy/consumer-rules.pro index 689d78067..1762163ce 100644 --- a/legacy/consumer-rules.pro +++ b/legacy/consumer-rules.pro @@ -1,3 +1,6 @@ -keep class android.** { *; } --keep class de.robv.android.xposed.** {*;} --keep class org.matrix.vector.Startup {*;} +-keep class de.robv.android.xposed.** { *; } +-keep class org.matrix.vector.Startup { *; } + +# Workaround to bypass verification of in-memory built class xposed.dummy.XResourcesSuperClass +-keep class org.matrix.vector.legacy.LegacyDelegateImpl$ResourceProxy { *; } diff --git a/xposed/consumer-rules.pro b/xposed/consumer-rules.pro index 125832dec..9dccf3fee 100644 --- a/xposed/consumer-rules.pro +++ b/xposed/consumer-rules.pro @@ -24,9 +24,3 @@ public static ; } -keep class org.matrix.vector.impl.di.LegacyFrameworkDelegate { *; } - -# Strip debug logs in release builds --assumenosideeffects class android.util.Log { - public static *** v(...); - public static *** d(...); -} diff --git a/zygisk/proguard-rules.pro b/zygisk/proguard-rules.pro index f908e5529..b8166bd58 100644 --- a/zygisk/proguard-rules.pro +++ b/zygisk/proguard-rules.pro @@ -7,12 +7,5 @@ -keepclasseswithmembers class org.matrix.vector.service.BridgeService { public static boolean *(android.os.IBinder, int, long, long, int); } - --assumenosideeffects class android.util.Log { - public static *** v(...); - public static *** d(...); -} -repackageclasses -allowaccessmodification --dontwarn org.lsposed.lspd.core.* --dontwarn org.lsposed.lspd.util.Hookers From 630db9863479e144000197d947d527ce07049580 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Tue, 24 Mar 2026 23:45:09 +0100 Subject: [PATCH 30/34] Reduce detection surface for injected target --- legacy/consumer-rules.pro | 3 +-- .../java/de/robv/android/xposed/XposedInit.java | 4 ++-- xposed/consumer-rules.pro | 15 +-------------- .../vector/impl/core/VectorModuleManager.kt | 2 +- .../main/kotlin/org/matrix/vector/core/Main.kt | 2 +- 5 files changed, 6 insertions(+), 20 deletions(-) diff --git a/legacy/consumer-rules.pro b/legacy/consumer-rules.pro index 1762163ce..7da43889d 100644 --- a/legacy/consumer-rules.pro +++ b/legacy/consumer-rules.pro @@ -1,6 +1,5 @@ -keep class android.** { *; } -keep class de.robv.android.xposed.** { *; } --keep class org.matrix.vector.Startup { *; } # Workaround to bypass verification of in-memory built class xposed.dummy.XResourcesSuperClass --keep class org.matrix.vector.legacy.LegacyDelegateImpl$ResourceProxy { *; } +-keepclassmembers class org.matrix.vector.legacy.LegacyDelegateImpl$ResourceProxy { *; } diff --git a/legacy/src/main/java/de/robv/android/xposed/XposedInit.java b/legacy/src/main/java/de/robv/android/xposed/XposedInit.java index 31f2e5057..9c23e3380 100644 --- a/legacy/src/main/java/de/robv/android/xposed/XposedInit.java +++ b/legacy/src/main/java/de/robv/android/xposed/XposedInit.java @@ -240,7 +240,7 @@ private static boolean initModule(ClassLoader mcl, String apk, List modu var count = 0; for (var moduleClassName : moduleClassNames) { try { - Log.i(TAG, " Loading class " + moduleClassName); + Log.v(TAG, " Loading class " + moduleClassName); Class moduleClass = mcl.loadClass(moduleClassName); @@ -281,7 +281,7 @@ private static boolean initModule(ClassLoader mcl, String apk, List modu * in assets/xposed_init. */ private static boolean loadModule(String name, String apk, PreLoadedApk file) { - Log.i(TAG, "Loading legacy module " + name + " from " + apk); + Log.v(TAG, "Loading legacy module " + name + " from " + apk); var sb = new StringBuilder(); var abis = Process.is64Bit() ? Build.SUPPORTED_64_BIT_ABIS : Build.SUPPORTED_32_BIT_ABIS; diff --git a/xposed/consumer-rules.pro b/xposed/consumer-rules.pro index 9dccf3fee..d18731d55 100644 --- a/xposed/consumer-rules.pro +++ b/xposed/consumer-rules.pro @@ -7,20 +7,7 @@ } # Preserve the JNI Hook Trampoline --keep class org.matrix.vector.impl.hooks.VectorNativeHooker { +-keepclassmembers class org.matrix.vector.impl.hooks.VectorNativeHooker { public (java.lang.reflect.Executable); public java.lang.Object callback(java.lang.Object[]); } - -# Preserve System Server hooks accessed reflectively by the `zygisk` module --keep class org.matrix.vector.impl.hookers.HandleSystemServerProcessHooker { *; } --keep class org.matrix.vector.impl.hookers.HandleSystemServerProcessHooker$Callback { *; } - -# Preserve the Initialization entry points called by the `legacy` module --keep class org.matrix.vector.impl.core.VectorStartup { - public static ; -} --keep class org.matrix.vector.impl.di.VectorBootstrap { - public static ; -} --keep class org.matrix.vector.impl.di.LegacyFrameworkDelegate { *; } diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt index abd8443a7..3081789be 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/core/VectorModuleManager.kt @@ -68,7 +68,7 @@ object VectorModuleManager { for (className in module.file.moduleClassNames) { runCatching { val moduleClass = moduleClassLoader.loadClass(className) - Log.d(TAG, "Loading class $moduleClass") + Log.v(TAG, "Loading class $moduleClass") if (!XposedModule::class.java.isAssignableFrom(moduleClass)) { Log.e(TAG, "Class does not extend XposedModule, skipping.") diff --git a/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt b/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt index c85401914..279e1e90b 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt @@ -52,7 +52,7 @@ object Main { } // Standard Xposed module loading for third-party apps - Utils.logI("Loading Vector/Xposed for $niceName (UID: ${Process.myUid()})") + Utils.logV("Loading Vector/Xposed for $niceName (UID: ${Process.myUid()})") Startup.bootstrapXposed(isSystem && isLateInject) } } From 06cc4f881cbf78d00e900f943371b9f71a6af5fe Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Wed, 25 Mar 2026 00:00:20 +0100 Subject: [PATCH 31/34] Customize generated class and source names of hooks In general, it will be stealthier to use randomized names. --- zygisk/src/main/cpp/module.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zygisk/src/main/cpp/module.cpp b/zygisk/src/main/cpp/module.cpp index bd25f6696..d4c303f6c 100644 --- a/zygisk/src/main/cpp/module.cpp +++ b/zygisk/src/main/cpp/module.cpp @@ -126,6 +126,8 @@ class VectorModule : public zygisk::ModuleBase, public vector::native::Context { [](auto symbol) { return ElfSymbolCache::GetArt()->getSymbAddress(symbol); }, .art_symbol_prefix_resolver = [](auto symbol) { return ElfSymbolCache::GetArt()->getSymbPrefixFirstAddress(symbol); }, + .generated_class_name = "Vector_", + .generated_source_name = "Dobby", }; // State managed within the class instance for each forked process. From d691000d9bfc30357cbd97300588c41192ee0223 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Wed, 25 Mar 2026 07:24:39 +0100 Subject: [PATCH 32/34] Add missing log utils --- .../src/main/java/org/lsposed/lspd/util/Utils.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/services/daemon-service/src/main/java/org/lsposed/lspd/util/Utils.java b/services/daemon-service/src/main/java/org/lsposed/lspd/util/Utils.java index 49807810f..cfdd92bea 100644 --- a/services/daemon-service/src/main/java/org/lsposed/lspd/util/Utils.java +++ b/services/daemon-service/src/main/java/org/lsposed/lspd/util/Utils.java @@ -109,6 +109,14 @@ public static void logD(String msg, Throwable throwable) { Log.d(LOG_TAG, msg, throwable); } + public static void logV(Object msg) { + Log.v(LOG_TAG, msg.toString()); + } + + public static void logV(String msg, Throwable throwable) { + Log.v(LOG_TAG, msg, throwable); + } + public static void logW(String msg) { Log.w(LOG_TAG, msg); } From 59180198b19be02a4219f75ce30abb243b154015 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Wed, 25 Mar 2026 11:50:45 +0100 Subject: [PATCH 33/34] Fix class loader --- xposed/README.md | 10 ++-- .../impl/utils/VectorModuleClassLoader.kt | 46 +++++++++++++------ .../impl/utils/VectorURLStreamHandler.kt | 6 ++- 3 files changed, 41 insertions(+), 21 deletions(-) diff --git a/xposed/README.md b/xposed/README.md index 138deb4d0..73680d59e 100644 --- a/xposed/README.md +++ b/xposed/README.md @@ -30,9 +30,9 @@ To maintain the separation of concerns, the `xposed` module communicates with th When `xposed` intercepts an Android lifecycle event (e.g., `LoadedApk.createClassLoader`), it dispatches the event internally via `VectorLifecycleManager` and then delegates the raw parameters to `LegacyFrameworkDelegate` so the `legacy` module can construct and dispatch the legacy `XC_LoadPackage` callbacks. -### 4. In-Memory Module ClassLoading +### 4. In-Memory ClassLoading & Isolation -Modules are loaded directly from memory to prevent disk I/O bottlenecks and enhance security. -* It utilizes `SharedMemory` buffers passed over IPC, mapping them to `ByteBuffer`s. -* It extends `ByteBufferDexClassLoader` to evaluate the module's DEX files. -* `VectorURLStreamHandler` intercepts resource requests (like `assets/`) to read directly from the original APK path without extracting the APK locally. +Modules are executed strictly from memory using an isolated ClassLoader, ensuring zero disk footprint and maximum stealth against anti-cheat mechanisms. +* The module APK is loaded into `SharedMemory` (ashmem) to bypass Java heap limitations. Once the Android Runtime (ART) ingests the DEX buffers, the ashmem is instantly unmapped, preventing memory leaks and leaving no residual file descriptors. +* The `VectorModuleClassLoader` is attached exclusively to the Xposed Framework's classloader branch, preventing the target app from discovering the module via reflection or `ClassLoader.getParent()` chain-walking. +* `VectorURLStreamHandler` intercepts standard `jar:` requests, reading assets and resources natively from the module path without triggering Android's global `JarFile` cache, preventing OS-level file locks. diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorModuleClassLoader.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorModuleClassLoader.kt index 63a2a9106..eb930b0f4 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorModuleClassLoader.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorModuleClassLoader.kt @@ -6,6 +6,7 @@ import android.system.ErrnoException import android.system.Os import android.system.OsConstants import android.util.Log +import androidx.annotation.RequiresApi import hidden.ByteBufferDexClassLoader import java.io.File import java.io.IOException @@ -21,21 +22,33 @@ import java.util.zip.ZipEntry * extract module code to the disk, enhancing both security and performance during the module * lifecycle. */ -class VectorModuleClassLoader -private constructor( - dexBuffers: Array, - librarySearchPath: String?, - parent: ClassLoader?, - private val apkPath: String, -) : - ByteBufferDexClassLoader( - dexBuffers, - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) librarySearchPath else null, - parent, - ) { +class VectorModuleClassLoader : ByteBufferDexClassLoader { + + private val apkPath: String private val nativeLibraryDirs = mutableListOf() - init { + @RequiresApi(Build.VERSION_CODES.Q) + private constructor( + dexBuffers: Array, + librarySearchPath: String?, + parent: ClassLoader?, + apkPath: String, + ) : super(dexBuffers, librarySearchPath, parent) { + this.apkPath = apkPath + initNativeDirs(librarySearchPath) + } + + private constructor( + dexBuffers: Array, + parent: ClassLoader?, + apkPath: String, + librarySearchPath: String?, + ) : super(dexBuffers, parent) { + this.apkPath = apkPath + initNativeDirs(librarySearchPath) + } + + private fun initNativeDirs(librarySearchPath: String?) { val searchPath = librarySearchPath ?: "" nativeLibraryDirs.addAll(splitPaths(searchPath)) nativeLibraryDirs.addAll(SYSTEM_NATIVE_LIBRARY_DIRS) @@ -150,7 +163,12 @@ private constructor( .filterNotNull() .toTypedArray() - val cl = VectorModuleClassLoader(dexBuffers, librarySearchPath, parent, apk) + val cl = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + VectorModuleClassLoader(dexBuffers, librarySearchPath, parent, apk) + } else { + VectorModuleClassLoader(dexBuffers, parent, apk, librarySearchPath) + } dexBuffers.toList().parallelStream().forEach { SharedMemory.unmap(it) } dexes.parallelStream().forEach { it.close() } diff --git a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorURLStreamHandler.kt b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorURLStreamHandler.kt index ac741adfa..8f146a4da 100644 --- a/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorURLStreamHandler.kt +++ b/xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorURLStreamHandler.kt @@ -64,8 +64,10 @@ internal class VectorURLStreamHandler(jarFileName: String) : Handler() { check(!isClosed) { "JarURLConnection has been closed" } if (!connected) { jarEntry = - jarFile.getEntry(entryName) - ?: throw FileNotFoundException("URL=$url, zipfile=${jarFile.name}") + this@VectorURLStreamHandler.jarFile.getEntry(entryName) + ?: throw FileNotFoundException( + "URL=$url, zipfile=${this@VectorURLStreamHandler.jarFile.name}" + ) connected = true } } From 492aa663dc3126f7d8b6c4abafef40f8069f054f Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 26 Mar 2026 16:14:00 +0100 Subject: [PATCH 34/34] [skip ci] fix docs --- xposed/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xposed/README.md b/xposed/README.md index 73680d59e..9c652d733 100644 --- a/xposed/README.md +++ b/xposed/README.md @@ -5,7 +5,7 @@ This module implements the [libxposed](https://github.com/libxposed/api) API for ## Architectural Overview The `xposed` module is designed with strict boundaries to ensure stability during the Android boot process and application lifecycles. It is written entirely in Kotlin and operates independently of the legacy Xposed API (`de.robv.android.xposed`). -It defines a Dependency Injection (DI) contract (`LegacyFrameworkDelegate`) which the `core` module must implement and inject during startup. +It defines a Dependency Injection (DI) contract (`LegacyFrameworkDelegate`) which the `legacy` module must implement and inject during startup. ## Core Components